From 5bf2ed137ddaeff9990c30bf748384c5aafc540d Mon Sep 17 00:00:00 2001 From: anuragts Date: Tue, 25 Feb 2025 17:30:02 +0530 Subject: [PATCH] feat: tic tac toe --- .../ai_tic_tac_toe_game/.gitignore | 1 + .../ai_tic_tac_toe_game/README.md | 103 +++++ .../ai_tic_tac_toe_game/agents.py | 165 +++++++ ai_agent_tutorials/ai_tic_tac_toe_game/app.py | 261 +++++++++++ .../ai_tic_tac_toe_game/requirements.txt | 238 ++++++++++ .../ai_tic_tac_toe_game/utils.py | 416 ++++++++++++++++++ 6 files changed, 1184 insertions(+) create mode 100644 ai_agent_tutorials/ai_tic_tac_toe_game/.gitignore create mode 100644 ai_agent_tutorials/ai_tic_tac_toe_game/README.md create mode 100644 ai_agent_tutorials/ai_tic_tac_toe_game/agents.py create mode 100644 ai_agent_tutorials/ai_tic_tac_toe_game/app.py create mode 100644 ai_agent_tutorials/ai_tic_tac_toe_game/requirements.txt create mode 100644 ai_agent_tutorials/ai_tic_tac_toe_game/utils.py diff --git a/ai_agent_tutorials/ai_tic_tac_toe_game/.gitignore b/ai_agent_tutorials/ai_tic_tac_toe_game/.gitignore new file mode 100644 index 0000000..ed8ebf5 --- /dev/null +++ b/ai_agent_tutorials/ai_tic_tac_toe_game/.gitignore @@ -0,0 +1 @@ +__pycache__ \ No newline at end of file diff --git a/ai_agent_tutorials/ai_tic_tac_toe_game/README.md b/ai_agent_tutorials/ai_tic_tac_toe_game/README.md new file mode 100644 index 0000000..7e8681f --- /dev/null +++ b/ai_agent_tutorials/ai_tic_tac_toe_game/README.md @@ -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. diff --git a/ai_agent_tutorials/ai_tic_tac_toe_game/agents.py b/ai_agent_tutorials/ai_tic_tac_toe_game/agents.py new file mode 100644 index 0000000..6be0dc8 --- /dev/null +++ b/ai_agent_tutorials/ai_tic_tac_toe_game/agents.py @@ -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 diff --git a/ai_agent_tutorials/ai_tic_tac_toe_game/app.py b/ai_agent_tutorials/ai_tic_tac_toe_game/app.py new file mode 100644 index 0000000..17faeff --- /dev/null +++ b/ai_agent_tutorials/ai_tic_tac_toe_game/app.py @@ -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( + "

Watch Agents play Tic Tac Toe

", + 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"

{selected_p_x} vs {selected_p_o}

", + 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"""
+
+
🔄
+ Player {player_num} ({current_model_name}) is thinking... +
+
""", + 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() diff --git a/ai_agent_tutorials/ai_tic_tac_toe_game/requirements.txt b/ai_agent_tutorials/ai_tic_tac_toe_game/requirements.txt new file mode 100644 index 0000000..c275c18 --- /dev/null +++ b/ai_agent_tutorials/ai_tic_tac_toe_game/requirements.txt @@ -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 diff --git a/ai_agent_tutorials/ai_tic_tac_toe_game/utils.py b/ai_agent_tutorials/ai_tic_tac_toe_game/utils.py new file mode 100644 index 0000000..ffc97dc --- /dev/null +++ b/ai_agent_tutorials/ai_tic_tac_toe_game/utils.py @@ -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 = '
' + + for i in range(3): + for j in range(3): + cell_value = board.board[i][j] + board_html += f'
{cell_value}
' + + board_html += "
" + 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"""
+ 🤖 {agent_name}: {status} +
""", + 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 = '
' + 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'
{board_state[i][j]}
' + html += "
" + return html + + +def display_move_history(): + """Display the move history with mini boards in two columns""" + st.markdown( + '

📜 Game History

', + 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"""
+ {create_mini_board_html(board_copy, (row, col), is_player1)} +
+
Move #{move["number"]}
+
{move["player"]}
+
Position: ({row}, {col})
+
+
""" + + 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 = '
' + + # Left column (Player 1) + history_content += '
' + 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 += "
" + + # Right column (Player 2) + history_content += '
' + 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 += "
" + + history_content += "
" + + # Display the content + history_container.markdown(history_content, unsafe_allow_html=True) + else: + history_container.markdown( + """
+ No moves yet. Start the game to see the history! +
""", + unsafe_allow_html=True, + ) + + +CUSTOM_CSS = """ + +"""