feat: tic tac toe

This commit is contained in:
anuragts 2025-02-25 17:30:02 +05:30
parent 38b7f87f24
commit 5bf2ed137d
6 changed files with 1184 additions and 0 deletions

View file

@ -0,0 +1 @@
__pycache__

View file

@ -0,0 +1,103 @@
# Agent Tic Tac Toe
This example shows how to build an interactive Tic Tac Toe game where AI agents compete against each other. The application showcases how to:
- Coordinate multiple AI agents in a turn-based game
- Use different language models for different players
- Create an interactive web interface with Streamlit
- Handle game state and move validation
- Display real-time game progress and move history
## Features
- Multiple AI models support (GPT-4, Claude, Gemini, etc.)
- Real-time game visualization
- Move history tracking with board states
- Interactive player selection
- Game state management
- Move validation and coordination
### 1. Create a virtual environment
```shell
python3 -m venv .venv
source .venv/bin/activate
```
### 2. Install dependencies
```shell
pip install -r ai_agent_tutorials/ai_tic_tac_toe_game/requirements.txt
```
### 3. Export API Keys
The game supports multiple AI models. Export the API keys for the models you want to use:
```shell
# Required for OpenAI models
export OPENAI_API_KEY=***
# Optional - for additional models
export ANTHROPIC_API_KEY=*** # For Claude models
export GOOGLE_API_KEY=*** # For Gemini models
export GROQ_API_KEY=*** # For Groq models
```
### 4. Run the Game
```shell
streamlit run ai_agent_tutorials/ai_tic_tac_toe_game/app.py
```
- Open [localhost:8501](http://localhost:8501) to view the game interface
## How It Works
The game consists of three agents:
1. **Master Agent (Referee)**
- Coordinates the game
- Validates moves
- Maintains game state
- Determines game outcome
2. **Two Player Agents**
- Make strategic moves
- Analyze board state
- Follow game rules
- Respond to opponent moves
## Available Models
The game supports various AI models:
- GPT-4o (OpenAI)
- GPT-o3-mini (OpenAI)
- Gemini (Google)
- Llama 3 (Groq)
- Claude (Anthropic)
## Game Features
1. **Interactive Board**
- Real-time updates
- Visual move tracking
- Clear game status display
2. **Move History**
- Detailed move tracking
- Board state visualization
- Player action timeline
3. **Game Controls**
- Start/Pause game
- Reset board
- Select AI models
- View game history
4. **Performance Analysis**
- Move timing
- Strategy tracking
- Game statistics
## Support
Join our [Discord community](https://agno.link/discord) for help and discussions.

View file

@ -0,0 +1,165 @@
"""
Tic Tac Toe Battle
---------------------------------
This example shows how to build a Tic Tac Toe game where two AI agents play against each other.
The game features a referee agent coordinating between two player agents using different
language models.
Usage Examples:
---------------
1. Quick game with default settings:
referee_agent = get_tic_tac_toe_referee()
play_tic_tac_toe()
2. Game with debug mode off:
referee_agent = get_tic_tac_toe_referee(debug_mode=False)
play_tic_tac_toe(debug_mode=False)
The game integrates:
- Multiple AI models (Claude, GPT-4, etc.)
- Turn-based gameplay coordination
- Move validation and game state management
"""
import sys
from pathlib import Path
from textwrap import dedent
from typing import Tuple
from agno.agent import Agent
from agno.models.anthropic import Claude
from agno.models.google import Gemini
from agno.models.groq import Groq
from agno.models.openai import OpenAIChat
project_root = str(Path(__file__).parent.parent.parent.parent)
if project_root not in sys.path:
sys.path.append(project_root)
def get_model_for_provider(provider: str, model_name: str):
"""
Creates and returns the appropriate model instance based on the provider.
Args:
provider: The model provider (e.g., 'openai', 'google', 'anthropic', 'groq')
model_name: The specific model name/ID
Returns:
An instance of the appropriate model class
Raises:
ValueError: If the provider is not supported
"""
if provider == "openai":
return OpenAIChat(id=model_name)
elif provider == "google":
return Gemini(id=model_name)
elif provider == "anthropic":
if model_name == "claude-3-5-sonnet":
return Claude(id="claude-3-5-sonnet-20241022", max_tokens=8192)
elif model_name == "claude-3-7-sonnet":
return Claude(
id="claude-3-7-sonnet-20250219",
max_tokens=8192,
)
elif model_name == "claude-3-7-sonnet-thinking":
return Claude(
id="claude-3-7-sonnet-20250219",
max_tokens=8192,
thinking={"type": "enabled", "budget_tokens": 4096},
)
else:
return Claude(id=model_name)
elif provider == "groq":
return Groq(id=model_name)
else:
raise ValueError(f"Unsupported model provider: {provider}")
def get_tic_tac_toe_players(
model_x: str = "openai:gpt-4o",
model_o: str = "openai:o3-mini",
debug_mode: bool = True,
) -> Tuple[Agent, Agent]:
"""
Returns an instance of the Tic Tac Toe Referee Agent that coordinates the game.
Args:
model_x: ModelConfig for player X
model_o: ModelConfig for player O
model_referee: ModelConfig for the referee agent
debug_mode: Enable logging and debug features
Returns:
An instance of the configured Referee Agent
"""
# Parse model provider and name
provider_x, model_name_x = model_x.split(":")
provider_o, model_name_o = model_o.split(":")
# Create model instances using the helper function
model_x = get_model_for_provider(provider_x, model_name_x)
model_o = get_model_for_provider(provider_o, model_name_o)
player_x = Agent(
name="Player X",
description=dedent("""\
You are Player X in a Tic Tac Toe game. Your goal is to win by placing three X's in a row (horizontally, vertically, or diagonally).
BOARD LAYOUT:
- The board is a 3x3 grid with coordinates from (0,0) to (2,2)
- Top-left is (0,0), bottom-right is (2,2)
RULES:
- You can only place X in empty spaces (shown as " " on the board)
- Players take turns placing their marks
- First to get 3 marks in a row (horizontal, vertical, or diagonal) wins
- If all spaces are filled with no winner, the game is a draw
YOUR RESPONSE:
- Provide ONLY two numbers separated by a space (row column)
- Example: "1 2" places your X in row 1, column 2
- Choose only from the valid moves list provided to you
STRATEGY TIPS:
- Study the board carefully and make strategic moves
- Block your opponent's potential winning moves
- Create opportunities for multiple winning paths
- Pay attention to the valid moves and avoid illegal moves
"""),
model=model_x,
debug_mode=debug_mode,
)
player_o = Agent(
name="Player O",
description=dedent("""\
You are Player O in a Tic Tac Toe game. Your goal is to win by placing three O's in a row (horizontally, vertically, or diagonally).
BOARD LAYOUT:
- The board is a 3x3 grid with coordinates from (0,0) to (2,2)
- Top-left is (0,0), bottom-right is (2,2)
RULES:
- You can only place X in empty spaces (shown as " " on the board)
- Players take turns placing their marks
- First to get 3 marks in a row (horizontal, vertical, or diagonal) wins
- If all spaces are filled with no winner, the game is a draw
YOUR RESPONSE:
- Provide ONLY two numbers separated by a space (row column)
- Example: "1 2" places your X in row 1, column 2
- Choose only from the valid moves list provided to you
STRATEGY TIPS:
- Study the board carefully and make strategic moves
- Block your opponent's potential winning moves
- Create opportunities for multiple winning paths
- Pay attention to the valid moves and avoid illegal moves
"""),
model=model_o,
debug_mode=debug_mode,
)
return player_x, player_o

View file

@ -0,0 +1,261 @@
import nest_asyncio
import streamlit as st
from agents import get_tic_tac_toe_players
from agno.utils.log import logger
from utils import (
CUSTOM_CSS,
TicTacToeBoard,
display_board,
display_move_history,
show_agent_status,
)
nest_asyncio.apply()
# Page configuration
st.set_page_config(
page_title="Agent Tic Tac Toe",
page_icon="🎮",
layout="wide",
initial_sidebar_state="expanded",
)
# Load custom CSS with dark mode support
st.markdown(CUSTOM_CSS, unsafe_allow_html=True)
def main():
####################################################################
# App header
####################################################################
st.markdown(
"<h1 class='main-title'>Watch Agents play Tic Tac Toe</h1>",
unsafe_allow_html=True,
)
####################################################################
# Initialize session state
####################################################################
if "game_started" not in st.session_state:
st.session_state.game_started = False
st.session_state.game_paused = False
st.session_state.move_history = []
with st.sidebar:
st.markdown("### Game Controls")
model_options = {
"gpt-4o": "openai:gpt-4o",
"o3-mini": "openai:o3-mini",
"claude-3.5": "anthropic:claude-3-5-sonnet",
"claude-3.7": "anthropic:claude-3-7-sonnet",
"claude-3.7-thinking": "anthropic:claude-3-7-sonnet-thinking",
"gemini-flash": "google:gemini-2.0-flash",
"gemini-pro": "google:gemini-2.0-pro-exp-02-05",
"llama-3.3": "groq:llama-3.3-70b-versatile",
}
################################################################
# Model selection
################################################################
selected_p_x = st.selectbox(
"Select Player X",
list(model_options.keys()),
index=list(model_options.keys()).index("claude-3.7-thinking"),
key="model_p1",
)
selected_p_o = st.selectbox(
"Select Player O",
list(model_options.keys()),
index=list(model_options.keys()).index("o3-mini"),
key="model_p2",
)
################################################################
# Game controls
################################################################
col1, col2 = st.columns(2)
with col1:
if not st.session_state.game_started:
if st.button("▶️ Start Game"):
st.session_state.player_x, st.session_state.player_o = (
get_tic_tac_toe_players(
model_x=model_options[selected_p_x],
model_o=model_options[selected_p_o],
debug_mode=True,
)
)
st.session_state.game_board = TicTacToeBoard()
st.session_state.game_started = True
st.session_state.game_paused = False
st.session_state.move_history = []
st.rerun()
else:
game_over, _ = st.session_state.game_board.get_game_state()
if not game_over:
if st.button(
"⏸️ Pause" if not st.session_state.game_paused else "▶️ Resume"
):
st.session_state.game_paused = not st.session_state.game_paused
st.rerun()
with col2:
if st.session_state.game_started:
if st.button("🔄 New Game"):
st.session_state.player_x, st.session_state.player_o = (
get_tic_tac_toe_players(
model_x=model_options[selected_p_x],
model_o=model_options[selected_p_o],
debug_mode=True,
)
)
st.session_state.game_board = TicTacToeBoard()
st.session_state.game_paused = False
st.session_state.move_history = []
st.rerun()
####################################################################
# Header showing current models
####################################################################
if st.session_state.game_started:
st.markdown(
f"<h3 style='color:#87CEEB; text-align:center;'>{selected_p_x} vs {selected_p_o}</h3>",
unsafe_allow_html=True,
)
####################################################################
# Main game area
####################################################################
if st.session_state.game_started:
game_over, status = st.session_state.game_board.get_game_state()
display_board(st.session_state.game_board)
# Show game status (winner/draw/current player)
if game_over:
winner_player = (
"X" if "X wins" in status else "O" if "O wins" in status else None
)
if winner_player:
winner_num = "1" if winner_player == "X" else "2"
winner_model = selected_p_x if winner_player == "X" else selected_p_o
st.success(f"🏆 Game Over! Player {winner_num} ({winner_model}) wins!")
else:
st.info("🤝 Game Over! It's a draw!")
else:
# Show current player status
current_player = st.session_state.game_board.current_player
player_num = "1" if current_player == "X" else "2"
current_model_name = selected_p_x if current_player == "X" else selected_p_o
show_agent_status(
f"Player {player_num} ({current_model_name})",
"It's your turn",
)
display_move_history()
if not st.session_state.game_paused and not game_over:
# Thinking indicator
st.markdown(
f"""<div class="thinking-container">
<div class="agent-thinking">
<div style="margin-right: 10px; display: inline-block;">🔄</div>
Player {player_num} ({current_model_name}) is thinking...
</div>
</div>""",
unsafe_allow_html=True,
)
valid_moves = st.session_state.game_board.get_valid_moves()
current_agent = (
st.session_state.player_x
if current_player == "X"
else st.session_state.player_o
)
response = current_agent.run(
f"""\
Current board state:\n{st.session_state.game_board.get_board_state()}\n
Available valid moves (row, col): {valid_moves}\n
Choose your next move from the valid moves above.
Respond with ONLY two numbers for row and column, e.g. "1 2".""",
stream=False,
)
try:
import re
numbers = re.findall(r"\d+", response.content if response else "")
row, col = map(int, numbers[:2])
success, message = st.session_state.game_board.make_move(row, col)
if success:
move_number = len(st.session_state.move_history) + 1
st.session_state.move_history.append(
{
"number": move_number,
"player": f"Player {player_num} ({current_model_name})",
"move": f"{row},{col}",
}
)
logger.info(
f"Move {move_number}: Player {player_num} ({current_model_name}) placed at position ({row}, {col})"
)
logger.info(
f"Board state:\n{st.session_state.game_board.get_board_state()}"
)
# Check game state after move
game_over, status = st.session_state.game_board.get_game_state()
if game_over:
logger.info(f"Game Over - {status}")
if "wins" in status:
st.success(f"🏆 Game Over! {status}")
else:
st.info(f"🤝 Game Over! {status}")
st.session_state.game_paused = True
st.rerun()
else:
logger.error(f"Invalid move attempt: {message}")
response = current_agent.run(
f"""\
Invalid move: {message}
Current board state:\n{st.session_state.game_board.get_board_state()}\n
Available valid moves (row, col): {valid_moves}\n
Please choose a valid move from the list above.
Respond with ONLY two numbers for row and column, e.g. "1 2".""",
stream=False,
)
st.rerun()
except Exception as e:
logger.error(f"Error processing move: {str(e)}")
st.error(f"Error processing move: {str(e)}")
st.rerun()
else:
st.info("👈 Press 'Start Game' to begin!")
####################################################################
# About section
####################################################################
st.sidebar.markdown(f"""
### 🎮 Agent Tic Tac Toe Battle
Watch two agents compete in real-time!
**Current Players:**
* 🔵 Player X: `{selected_p_x}`
* 🔴 Player O: `{selected_p_o}`
**How it Works:**
Each Agent analyzes the board and employs strategic thinking to:
* 🏆 Find winning moves
* 🛡 Block opponent victories
* Control strategic positions
* 🤔 Plan multiple moves ahead
Built with Streamlit and Agno
""")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,238 @@
# This file was autogenerated by uv via the following command:
# ./generate_requirements.sh
agno==1.1.6
# via -r cookbook/examples/apps/tic_tac_toe/requirements.in
altair==5.5.0
# via streamlit
annotated-types==0.7.0
# via pydantic
anthropic==0.47.1
# via -r cookbook/examples/apps/tic_tac_toe/requirements.in
anyio==4.8.0
# via
# anthropic
# groq
# httpx
# openai
attrs==25.1.0
# via
# jsonschema
# referencing
blinker==1.9.0
# via streamlit
build==1.2.2.post1
# via pip-tools
cachetools==5.5.2
# via
# google-auth
# streamlit
certifi==2025.1.31
# via
# httpcore
# httpx
# requests
charset-normalizer==3.4.1
# via requests
click==8.1.8
# via
# pip-tools
# streamlit
# typer
distro==1.9.0
# via
# anthropic
# groq
# openai
docstring-parser==0.16
# via agno
gitdb==4.0.12
# via gitpython
gitpython==3.1.44
# via
# agno
# streamlit
google-auth==2.38.0
# via google-genai
google-genai==1.3.0
# via -r cookbook/examples/apps/tic_tac_toe/requirements.in
groq==0.18.0
# via -r cookbook/examples/apps/tic_tac_toe/requirements.in
h11==0.14.0
# via httpcore
httpcore==1.0.7
# via httpx
httpx==0.28.1
# via
# agno
# anthropic
# google-genai
# groq
# ollama
# openai
idna==3.10
# via
# anyio
# httpx
# requests
jinja2==3.1.5
# via
# altair
# pydeck
jiter==0.8.2
# via
# anthropic
# openai
jsonschema==4.23.0
# via altair
jsonschema-specifications==2024.10.1
# via jsonschema
markdown-it-py==3.0.0
# via rich
markupsafe==3.0.2
# via jinja2
mdurl==0.1.2
# via markdown-it-py
narwhals==1.28.0
# via altair
nest-asyncio==1.6.0
# via -r cookbook/examples/apps/tic_tac_toe/requirements.in
numpy==2.2.3
# via
# pandas
# pydeck
# streamlit
ollama==0.4.7
# via -r cookbook/examples/apps/tic_tac_toe/requirements.in
openai==1.64.0
# via -r cookbook/examples/apps/tic_tac_toe/requirements.in
packaging==24.2
# via
# altair
# build
# streamlit
pandas==2.2.3
# via streamlit
pathlib==1.0.1
# via -r cookbook/examples/apps/tic_tac_toe/requirements.in
pillow==11.1.0
# via
# -r cookbook/examples/apps/tic_tac_toe/requirements.in
# streamlit
pip==25.0.1
# via pip-tools
pip-tools==7.4.1
# via -r cookbook/examples/apps/tic_tac_toe/requirements.in
protobuf==5.29.3
# via streamlit
pyarrow==19.0.1
# via streamlit
pyasn1==0.6.1
# via
# pyasn1-modules
# rsa
pyasn1-modules==0.4.1
# via google-auth
pydantic==2.10.6
# via
# agno
# anthropic
# google-genai
# groq
# ollama
# openai
# pydantic-settings
pydantic-core==2.27.2
# via pydantic
pydantic-settings==2.8.0
# via agno
pydeck==0.9.1
# via streamlit
pygments==2.19.1
# via rich
pyproject-hooks==1.2.0
# via
# build
# pip-tools
python-dateutil==2.9.0.post0
# via pandas
python-dotenv==1.0.1
# via
# -r cookbook/examples/apps/tic_tac_toe/requirements.in
# agno
# pydantic-settings
python-multipart==0.0.20
# via agno
pytz==2025.1
# via pandas
pyyaml==6.0.2
# via agno
referencing==0.36.2
# via
# jsonschema
# jsonschema-specifications
requests==2.32.3
# via
# google-genai
# streamlit
rich==13.9.4
# via
# -r cookbook/examples/apps/tic_tac_toe/requirements.in
# agno
# streamlit
# typer
rpds-py==0.23.1
# via
# jsonschema
# referencing
rsa==4.9
# via google-auth
setuptools==75.8.0
# via pip-tools
shellingham==1.5.4
# via typer
six==1.17.0
# via python-dateutil
smmap==5.0.2
# via gitdb
sniffio==1.3.1
# via
# anthropic
# anyio
# groq
# openai
streamlit==1.42.2
# via -r cookbook/examples/apps/tic_tac_toe/requirements.in
tenacity==9.0.0
# via streamlit
toml==0.10.2
# via streamlit
tomli==2.2.1
# via agno
tornado==6.4.2
# via streamlit
tqdm==4.67.1
# via openai
typer==0.15.1
# via agno
typing-extensions==4.12.2
# via
# agno
# altair
# anthropic
# anyio
# google-genai
# groq
# openai
# pydantic
# pydantic-core
# referencing
# streamlit
# typer
tzdata==2025.1
# via pandas
urllib3==2.3.0
# via requests
websockets==14.2
# via google-genai
wheel==0.45.1
# via pip-tools

View file

@ -0,0 +1,416 @@
from typing import List, Optional, Tuple
import streamlit as st
# Define constants for players
X_PLAYER = "X"
O_PLAYER = "O"
EMPTY = " "
class TicTacToeBoard:
def __init__(self):
# Initialize empty 3x3 board
self.board = [[EMPTY for _ in range(3)] for _ in range(3)]
self.current_player = X_PLAYER
def make_move(self, row: int, col: int) -> Tuple[bool, str]:
"""
Make a move on the board.
Args:
row (int): Row index (0-2)
col (int): Column index (0-2)
Returns:
Tuple[bool, str]: (Success status, Message with current board state or error)
"""
# Validate move coordinates
if not (0 <= row <= 2 and 0 <= col <= 2):
return (
False,
"Invalid move: Position out of bounds. Please choose row and column between 0 and 2.",
)
# Check if position is already occupied
if self.board[row][col] != EMPTY:
return False, f"Invalid move: Position ({row}, {col}) is already occupied."
# Make the move
self.board[row][col] = self.current_player
# Get board state
board_state = self.get_board_state()
# Switch player
self.current_player = O_PLAYER if self.current_player == X_PLAYER else X_PLAYER
return True, f"Move successful!\n{board_state}"
def get_board_state(self) -> str:
"""
Returns a string representation of the current board state.
"""
board_str = "\n-------------\n"
for row in self.board:
board_str += f"| {' | '.join(row)} |\n-------------\n"
return board_str
def check_winner(self) -> Optional[str]:
"""
Check if there's a winner.
Returns:
Optional[str]: The winning player (X or O) or None if no winner
"""
# Check rows
for row in self.board:
if row.count(row[0]) == 3 and row[0] != EMPTY:
return row[0]
# Check columns
for col in range(3):
column = [self.board[row][col] for row in range(3)]
if column.count(column[0]) == 3 and column[0] != EMPTY:
return column[0]
# Check diagonals
diagonal1 = [self.board[i][i] for i in range(3)]
if diagonal1.count(diagonal1[0]) == 3 and diagonal1[0] != EMPTY:
return diagonal1[0]
diagonal2 = [self.board[i][2 - i] for i in range(3)]
if diagonal2.count(diagonal2[0]) == 3 and diagonal2[0] != EMPTY:
return diagonal2[0]
return None
def is_board_full(self) -> bool:
"""
Check if the board is full (draw condition).
"""
return all(cell != EMPTY for row in self.board for cell in row)
def get_valid_moves(self) -> List[Tuple[int, int]]:
"""
Get a list of valid moves (empty positions).
Returns:
List[Tuple[int, int]]: List of (row, col) tuples representing valid moves
"""
valid_moves = []
for row in range(3):
for col in range(3):
if self.board[row][col] == EMPTY:
valid_moves.append((row, col))
return valid_moves
def get_game_state(self) -> Tuple[bool, str]:
"""
Get the current game state.
Returns:
Tuple[bool, str]: (is_game_over, status_message)
"""
winner = self.check_winner()
if winner:
return True, f"Player {winner} wins!"
if self.is_board_full():
return True, "It's a draw!"
return False, "Game in progress"
def display_board(board: TicTacToeBoard):
"""Display the Tic Tac Toe board using Streamlit"""
board_html = '<div class="game-board">'
for i in range(3):
for j in range(3):
cell_value = board.board[i][j]
board_html += f'<div class="board-cell">{cell_value}</div>'
board_html += "</div>"
st.markdown(board_html, unsafe_allow_html=True)
def show_agent_status(agent_name: str, status: str):
"""Display the current agent status"""
st.markdown(
f"""<div class="agent-status">
🤖 <b>{agent_name}</b>: {status}
</div>""",
unsafe_allow_html=True,
)
def create_mini_board_html(
board_state: list, highlight_pos: tuple = None, is_player1: bool = True
) -> str:
"""Create HTML for a mini board with player-specific highlighting"""
html = '<div class="mini-board">'
for i in range(3):
for j in range(3):
highlight = (
f"highlight player{1 if is_player1 else 2}"
if highlight_pos and (i, j) == highlight_pos
else ""
)
html += f'<div class="mini-cell {highlight}">{board_state[i][j]}</div>'
html += "</div>"
return html
def display_move_history():
"""Display the move history with mini boards in two columns"""
st.markdown(
'<h3 style="margin-bottom: 30px;">📜 Game History</h3>',
unsafe_allow_html=True,
)
history_container = st.empty()
if "move_history" in st.session_state and st.session_state.move_history:
# Split moves into player 1 and player 2 moves
p1_moves = []
p2_moves = []
current_board = [[" " for _ in range(3)] for _ in range(3)]
# Process all moves first
for move in st.session_state.move_history:
row, col = map(int, move["move"].split(","))
is_player1 = "Player 1" in move["player"]
symbol = "X" if is_player1 else "O"
current_board[row][col] = symbol
board_copy = [row[:] for row in current_board]
move_html = f"""<div class="move-entry player{1 if is_player1 else 2}">
{create_mini_board_html(board_copy, (row, col), is_player1)}
<div class="move-info">
<div class="move-number player{1 if is_player1 else 2}">Move #{move["number"]}</div>
<div>{move["player"]}</div>
<div style="font-size: 0.9em; color: #888">Position: ({row}, {col})</div>
</div>
</div>"""
if is_player1:
p1_moves.append(move_html)
else:
p2_moves.append(move_html)
max_moves = max(len(p1_moves), len(p2_moves))
history_content = '<div class="history-grid">'
# Left column (Player 1)
history_content += '<div class="history-column-left">'
for i in range(max_moves):
entry_html = ""
# Player 1 move
if i < len(p1_moves):
entry_html += p1_moves[i]
history_content += entry_html
history_content += "</div>"
# Right column (Player 2)
history_content += '<div class="history-column-right">'
for i in range(max_moves):
entry_html = ""
# Player 2 move
if i < len(p2_moves):
entry_html += p2_moves[i]
history_content += entry_html
history_content += "</div>"
history_content += "</div>"
# Display the content
history_container.markdown(history_content, unsafe_allow_html=True)
else:
history_container.markdown(
"""<div style="text-align: center; color: #666; padding: 20px;">
No moves yet. Start the game to see the history!
</div>""",
unsafe_allow_html=True,
)
CUSTOM_CSS = """
<style>
/* Main Styles */
.main-title {
text-align: center;
background: linear-gradient(45deg, #FF4B2B, #FF416C);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-size: 3em;
font-weight: bold;
padding: 0.5em 0;
}
.subtitle {
text-align: center;
color: #666;
margin-bottom: 1em;
}
.game-board {
display: grid;
grid-template-columns: repeat(3, 80px);
gap: 5px;
justify-content: center;
margin: 1em auto;
background: #666;
padding: 5px;
border-radius: 8px;
width: fit-content;
}
.board-cell {
width: 80px;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
font-size: 2em;
font-weight: bold;
background-color: #2b2b2b;
color: #fff;
transition: all 0.3s ease;
margin: 0;
padding: 0;
}
.board-cell:hover {
background-color: #3b3b3b;
}
.agent-status {
background-color: #1e1e1e;
border-left: 4px solid #4CAF50;
padding: 10px;
margin: 10px auto;
border-radius: 4px;
max-width: 600px;
text-align: center;
}
.agent-thinking {
display: flex;
justify-content: center;
background-color: #2b2b2b;
padding: 10px;
border-radius: 5px;
margin: 10px auto;
border-left: 4px solid #FFA500;
max-width: 600px;
}
.move-history {
background-color: #2b2b2b;
padding: 15px;
border-radius: 10px;
margin: 10px 0;
}
.thinking-container {
position: fixed;
bottom: 20px;
left: 50%;
z-index: 1000;
min-width: 300px;
}
.agent-thinking {
background-color: rgba(43, 43, 43, 0.95);
border: 1px solid #4CAF50;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
}
/* Move History Updates */
.history-header {
text-align: center;
margin-bottom: 30px;
}
.history-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px; /* Controls spacing between columns */
width: 100%;
margin: 0; /* Remove left/right margins */
padding: 0;
}
.history-column-left,
.history-column-right {
display: flex;
flex-direction: column;
align-items: flex-start; /* Ensures columns fill available space nicely */
margin: 0;
padding: 0;
width: 100%;
}
.move-entry {
display: flex;
align-items: center;
padding: 12px;
margin: 8px 0;
background-color: #2b2b2b;
border-radius: 4px;
width: 100%; /* Removed fixed width so entries span the column */
box-sizing: border-box;
}
.move-entry.player1 {
border-left: 4px solid #4CAF50;
}
.move-entry.player2 {
border-left: 4px solid #f44336;
}
/* Mini-board styling inside moves */
.mini-board {
display: grid;
grid-template-columns: repeat(3, 25px);
gap: 2px;
background: #444;
padding: 2px;
border-radius: 4px;
margin-right: 15px;
}
.mini-cell {
width: 25px;
height: 25px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: bold;
background-color: #2b2b2b;
color: #fff;
}
.mini-cell.highlight.player1 {
background-color: #4CAF50;
color: white;
}
.mini-cell.highlight.player2 {
background-color: #f44336;
color: white;
}
/* Move info styling */
.move-info {
flex-grow: 1;
padding-left: 12px;
}
.move-number {
font-weight: bold;
margin-right: 10px;
}
.move-number.player1 {
color: #4CAF50;
}
.move-number.player2 {
color: #f44336;
}
</style>
"""