feat: tic tac toe
This commit is contained in:
parent
38b7f87f24
commit
5bf2ed137d
6 changed files with 1184 additions and 0 deletions
1
ai_agent_tutorials/ai_tic_tac_toe_game/.gitignore
vendored
Normal file
1
ai_agent_tutorials/ai_tic_tac_toe_game/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
__pycache__
|
||||
103
ai_agent_tutorials/ai_tic_tac_toe_game/README.md
Normal file
103
ai_agent_tutorials/ai_tic_tac_toe_game/README.md
Normal 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.
|
||||
165
ai_agent_tutorials/ai_tic_tac_toe_game/agents.py
Normal file
165
ai_agent_tutorials/ai_tic_tac_toe_game/agents.py
Normal 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
|
||||
261
ai_agent_tutorials/ai_tic_tac_toe_game/app.py
Normal file
261
ai_agent_tutorials/ai_tic_tac_toe_game/app.py
Normal 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()
|
||||
238
ai_agent_tutorials/ai_tic_tac_toe_game/requirements.txt
Normal file
238
ai_agent_tutorials/ai_tic_tac_toe_game/requirements.txt
Normal 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
|
||||
416
ai_agent_tutorials/ai_tic_tac_toe_game/utils.py
Normal file
416
ai_agent_tutorials/ai_tic_tac_toe_game/utils.py
Normal 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>
|
||||
"""
|
||||
Loading…
Reference in a new issue