"""Rich UI module for Dominoes game visualization.
This module provides rich console UI components for visualizing the Dominoes game.
"""
import logging
from typing import Any
from rich.align import Align
from rich.box import ROUNDED
from rich.console import Console
from rich.layout import Layout
from rich.panel import Panel
from rich.table import Table
from rich.text import Text
from haive.games.dominoes.state import DominoesState
logger = logging.getLogger(__name__)
[docs]
class DominoesUI:
"""Rich UI for Dominoes game visualization."""
def __init__(self, console: Console | None = None):
"""Initialize the UI.
Args:
console: Optional Rich console instance
"""
self.console = console or Console()
def _is_valid_game_state_dict(self, data: dict) -> bool:
"""Check if a dict contains the required fields for DominoesState.
Args:
data: Dictionary to check
Returns:
True if the dict appears to be a valid game state
"""
required_fields = {
"players",
"hands",
"board",
"boneyard",
"turn",
"game_status",
}
return all(field in data for field in required_fields)
[docs]
def create_board_panel(self, game_state: DominoesState) -> Panel:
"""Create a visual representation of the dominoes board.
Args:
game_state: Current game state
Returns:
Rich panel representing the board
"""
if not game_state.board:
board_text = Text("🀱 Empty Board - Play the first tile! 🀱", style="yellow")
return Panel(
Align.center(board_text),
title="[bold]Game Board[/bold]",
border_style="blue",
box=ROUNDED,
)
# Create a visual representation of the dominoes
board_tiles = []
for tile in game_state.board:
tile_str = f"[{tile.left}|{tile.right}]"
# Add color based on the tile values
if tile.is_double():
board_tiles.append(f"[bold magenta]{tile_str}[/bold magenta]")
else:
board_tiles.append(f"[cyan]{tile_str}[/cyan]")
# Join the tiles with connectors
board_display = " - ".join(board_tiles)
# Highlight the open ends
if game_state.left_value is not None:
board_display = (
f"[bold green]{game_state.left_value}[/bold green] << " + board_display
)
if game_state.right_value is not None:
board_display = (
board_display + f" >> [bold green]{game_state.right_value}[/bold green]"
)
# Create panel
return Panel(
Align.center(Text(board_display)),
title="[bold]Game Board[/bold]",
border_style="blue",
box=ROUNDED,
)
[docs]
def create_player_hand_panel(self, game_state: DominoesState, player: str) -> Panel:
"""Create a panel showing a player's hand.
Args:
game_state: Current game state
player: Player whose hand to display
Returns:
Rich panel with player's hand
"""
hand = game_state.hands[player]
if not hand:
hand_text = "[dim]No tiles in hand[/dim]"
return Panel(
hand_text,
title=f"[bold]{player}'s Hand[/bold]",
border_style="red" if player == "player1" else "blue",
box=ROUNDED,
)
# Create a table for the tiles
hand_table = Table(show_header=False, box=None, padding=1)
# Calculate tiles per row
tiles_per_row = 5
# Create formatted tile strings
tile_strs = []
for tile in hand:
# Color doubles differently
if tile.is_double():
tile_strs.append(
f"[bold magenta][{tile.left}|{tile.right}][/bold magenta]"
)
else:
tile_strs.append(f"[cyan][{tile.left}|{tile.right}][/cyan]")
# Add rows to the table
for i in range(0, len(tile_strs), tiles_per_row):
row_tiles = tile_strs[i : i + tiles_per_row]
hand_table.add_row(*row_tiles)
# Add pip count
pip_count = sum(tile.left + tile.right for tile in hand)
pip_text = Text(f"\nTotal Pip Count: {pip_count}", style="dim")
# Create the panel
panel_style = "red" if player == "player1" else "blue"
is_current = player == game_state.turn
title = f"[bold]{player}'s Hand[/bold]" + (
" [bold green](Current Turn)[/bold green]" if is_current else ""
)
return Panel(
Layout(
renderable=hand_table,
size=len(hand_table.rows) + 1, # Adjust size based on content
).split(Layout(pip_text)),
title=title,
border_style=panel_style,
box=ROUNDED,
)
[docs]
def create_game_info_panel(self, game_state: DominoesState) -> Panel:
"""Create a panel with game information.
Args:
game_state: Current game state
Returns:
Rich panel with game info
"""
# Current player indicator
current_player = f"🎮 {game_state.turn}'s Turn"
player_color = "red" if game_state.turn == "player1" else "blue"
# Game status
if game_state.game_status == "ongoing":
status_text = "[green]Game in Progress[/green]"
elif "win" in game_state.game_status:
winner = game_state.winner or game_state.game_status.split("_")[0]
status_text = f"[yellow]{winner} Wins![/yellow]"
else:
status_text = f"[yellow]{game_state.game_status}[/yellow]"
# Boneyard count
boneyard_text = f"🎲 Boneyard: [bold]{len(game_state.boneyard)}[/bold] tiles"
# Last passes
passes_text = f"⏭️ Consecutive Passes: [bold]{game_state.last_passes}[/bold]"
# Scores
scores_text = "📊 Scores:"
for player, score in game_state.scores.items():
color = "red" if player == "player1" else "blue"
scores_text += f"\n [{color}]{player}[/{color}]: [bold]{score}[/bold]"
info_text = f"""[bold {player_color}]{current_player}[/bold {player_color}]
{status_text}
{boneyard_text}
{passes_text}
{scores_text}"""
return Panel(
info_text, title="[bold]Game Info[/bold]", border_style="green", box=ROUNDED
)
[docs]
def create_last_move_panel(self, game_state: DominoesState) -> Panel:
"""Create a panel showing the last move.
Args:
game_state: Current game state
Returns:
Rich panel with last move info
"""
if not game_state.move_history:
last_move_text = "[dim]No moves yet[/dim]"
else:
last_move = game_state.move_history[-1]
if last_move == "pass":
last_move_text = "[yellow]Pass[/yellow]"
else:
player_idx = (game_state.players.index(game_state.turn) - 1) % len(
game_state.players
)
last_player = game_state.players[player_idx]
color = "red" if last_player == "player1" else "blue"
tile_str = f"[{last_move.tile.left}|{last_move.tile.right}]"
if last_move.tile.is_double():
tile_str = f"[bold magenta]{tile_str}[/bold magenta]"
else:
tile_str = f"[cyan]{tile_str}[/cyan]"
last_move_text = f"[{color}]{last_player}[/{color}] played {tile_str} on the {last_move.location} end"
return Panel(
last_move_text,
title="[bold]Last Move[/bold]",
border_style="yellow",
box=ROUNDED,
)
[docs]
def create_analysis_panel(self, game_state: DominoesState) -> Panel:
"""Create a panel showing the latest analysis.
Args:
game_state: Current game state
Returns:
Rich panel with analysis info
"""
analysis_text = ""
if hasattr(game_state, "player1_analysis") and game_state.player1_analysis:
latest_analysis = game_state.player1_analysis[-1]
summary = f"Hand Strength: {latest_analysis.hand_strength}/10"
if hasattr(latest_analysis, "suggested_strategy"):
summary += f"\nStrategy: {latest_analysis.suggested_strategy}"
analysis_text += f"[red]Player1 Analysis:[/red]\n{summary}\n\n"
if hasattr(game_state, "player2_analysis") and game_state.player2_analysis:
latest_analysis = game_state.player2_analysis[-1]
summary = f"Hand Strength: {latest_analysis.hand_strength}/10"
if hasattr(latest_analysis, "suggested_strategy"):
summary += f"\nStrategy: {latest_analysis.suggested_strategy}"
analysis_text += f"[blue]Player2 Analysis:[/blue]\n{summary}"
if not analysis_text:
analysis_text = "[dim]No analysis yet[/dim]"
return Panel(
analysis_text,
title="[bold]Latest Analysis[/bold]",
border_style="magenta",
box=ROUNDED,
)
[docs]
def create_layout(self, game_state: DominoesState) -> Layout:
"""Create the complete rich UI layout.
Args:
game_state: Current game state
Returns:
Complete rich layout
"""
# Create layout
layout = Layout()
# Split into header and body
layout.split_column(Layout(name="header", size=3), Layout(name="body"))
# Header
title = Text("🎲 Dominoes Game 🎲", style="bold magenta", justify="center")
layout["header"].update(Panel(title, box=ROUNDED))
# Split body into sections
layout["body"].split_column(
Layout(name="board", size=5),
Layout(name="hands"),
Layout(name="info", size=15),
)
# Board section
layout["body"]["board"].update(self.create_board_panel(game_state))
# Hands section - split into player hands
layout["body"]["hands"].split_row(
Layout(name="player1_hand"),
Layout(name="player2_hand"),
)
layout["body"]["hands"]["player1_hand"].update(
self.create_player_hand_panel(game_state, "player1")
)
layout["body"]["hands"]["player2_hand"].update(
self.create_player_hand_panel(game_state, "player2")
)
# Info section - split into game info, last move, and analysis
layout["body"]["info"].split_row(
Layout(name="game_info"),
Layout(name="last_move"),
Layout(name="analysis"),
)
layout["body"]["info"]["game_info"].update(
self.create_game_info_panel(game_state)
)
layout["body"]["info"]["last_move"].update(
self.create_last_move_panel(game_state)
)
layout["body"]["info"]["analysis"].update(
self.create_analysis_panel(game_state)
)
return layout
[docs]
def display_state(self, state_data: Any) -> bool:
"""Display the game state using rich UI.
Args:
state_data: State data in various formats
Returns:
True if display was successful, False otherwise
"""
try:
game_state = self.extract_game_state(state_data)
if game_state is None:
logger.error("Could not extract valid game state for display")
return False
layout = self.create_layout(game_state)
self.console.print(layout)
return True
except Exception as e:
logger.error(f"Error displaying state: {e}", exc_info=True)
return False
[docs]
def display_welcome(self) -> None:
"""Display welcome message."""
welcome_text = Text(
"""
🎲 Welcome to Dominoes! 🎲
This is a classic tile-based game where:
• Players take turns placing matching dominoes
• The first player to use all their tiles wins
• If the game is locked, the player with the lowest pip count wins
• Strategic tile placement is key to victory
The game will be played by AI agents with real-time visualization!
""".strip(),
style="bold blue",
)
self.console.print(
Panel(
welcome_text,
title="[bold magenta]🎮 Dominoes Game 🎮[/bold magenta]",
border_style="green",
)
)
[docs]
def display_final_results(self, final_state: Any) -> None:
"""Display final game results.
Args:
final_state: Final game state
"""
try:
game_state = self.extract_game_state(final_state)
if game_state is None:
self.console.print("[red]Could not extract final game state[/red]")
return
self.console.print("\n" + "=" * 60)
self.console.print("[bold green]🎮 Game Complete! 🎮[/bold green]")
status = game_state.game_status
winner = game_state.winner
# Calculate final pip counts
pip_counts = {}
for player, hand in game_state.hands.items():
pip_counts[player] = sum(tile.left + tile.right for tile in hand)
# Create result text
if "win" in status:
player_color = "red" if winner == "player1" else "blue"
result_text = (
f"[bold {player_color}]{winner} Wins![/bold {player_color}] 🎉\n"
)
result_text += f"Final Score: {game_state.scores.get(winner, 0)}\n"
else:
result_text = "[bold yellow]Game ended in a draw![/bold yellow]\n"
result_text += "\nFinal Pip Counts:"
for player, count in pip_counts.items():
player_color = "red" if player == "player1" else "blue"
result_text += f"\n[{player_color}]{player}[/{player_color}]: {count}"
self.console.print(
Panel(
result_text,
title="[bold]Final Results[/bold]",
border_style="yellow",
)
)
except Exception as e:
logger.error(f"Error displaying final results: {e}", exc_info=True)
self.console.print(f"[red]Error displaying final results: {e}[/red]")
[docs]
def print_debug_info(self, state_data: Any, context: str = "") -> None:
"""Print debug information about the state.
Args:
state_data: State data to debug
context: Context string for debugging
"""
logger.debug(f"=== DEBUG INFO {context} ===")
logger.debug(f"State type: {type(state_data)}")
if state_data is None:
logger.debug("State data is None")
elif isinstance(state_data, dict):
logger.debug(f"Dict keys: {list(state_data.keys())}")
for key, value in state_data.items():
logger.debug(f" {key}: {type(value)}")
if isinstance(value, dict) and len(value) < 10:
logger.debug(f" Value: {value}")
elif isinstance(state_data, DominoesState):
logger.debug(
f"Game state - Turn: {state_data.turn}, Status: {state_data.game_status}"
)
logger.debug(f"Board: {state_data.board_string}")
logger.debug(f"Hands: {len(state_data.hands)}")
for player, hand in state_data.hands.items():
logger.debug(f" {player}: {[str(tile) for tile in hand]}")
else:
logger.debug(f"Raw value: {str(state_data)[:200]}")
logger.debug("=== END DEBUG INFO ===")