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