Source code for haive.games.checkers.ui

"""Checkers game UI module.

This module provides a rich text-based UI for the checkers game, including:
    - Beautiful board visualization with colors
    - Game information display
    - Move history tracking
    - Captured pieces visualization
    - Position analysis display
    - Game status and winner announcements
    - Move and thinking animations

The UI uses the rich library to create a visually appealing terminal interface
that makes the game more engaging and easier to follow.

"""

import time
from datetime import datetime

from rich import box
from rich.align import Align
from rich.box import DOUBLE, HEAVY, ROUNDED
from rich.console import Console
from rich.layout import Layout
from rich.panel import Panel
from rich.progress import Progress, SpinnerColumn, TextColumn
from rich.table import Table
from rich.text import Text

from haive.games.checkers.models import CheckersMove
from haive.games.checkers.state import CheckersState


[docs] class CheckersUI: """Rich UI for beautiful checkers game visualization. This class provides a visually appealing terminal UI for checkers games, with styled components, animations, and comprehensive game information. Features: - Colorful board display with piece symbols - Move history panel - Captured pieces tracking - Game status and information - Position analysis display - Move and thinking animations - Game over screen Attributes: console (Console): Rich console for output layout (Layout): Layout manager for UI components colors (dict): Color schemes for different UI elements pieces (dict): Unicode symbols for different piece types game_log (List[str]): Log of game events move_count (int): Counter for moves start_time (datetime): Game start time Examples: >>> ui = CheckersUI() >>> state = CheckersState.initialize() >>> ui.display_state(state) # Display the initial board >>> >>> # Show thinking animation during move generation >>> ui.show_thinking("red") >>> >>> # Display a move >>> move = CheckersMove(from_position="a3", to_position="b4", player="red") >>> ui.show_move(move) """ def __init__(self): """Initialize the checkers UI. Sets up the console, layout, color schemes, piece symbols, and tracking variables for the UI. """ self.console = Console() self.layout = Layout() self.game_log: list[str] = [] self.move_count = 0 self.start_time = datetime.now() # Color schemes self.colors = { "red_piece": "bold bright_red", "red_king": "bold bright_red on yellow3", "black_piece": "bold bright_white on grey39", "black_king": "bold bright_white on yellow3", "dark_square": "on grey19", "light_square": "on grey27", "highlight": "bold green", "last_move": "on bright_blue", "last_move_red": "bold bright_red on bright_blue", "last_move_black": "bold bright_white on bright_blue", "captured": "dim red", "board_border": "bold cyan", "player_red": "bold bright_red", "player_black": "bold bright_white", } # Unicode pieces self.pieces = { "red": "●", "red_king": "◆", # Diamond for red king "black": "○", "black_king": "◇", # Diamond outline for black king "empty": " ", } self._setup_layout() def _setup_layout(self): """Setup the main layout structure. Creates a layout with the following components: - Header: Game title and current turn - Main area: Board and sidebars - Footer: Status messages and controls - Left sidebar: Game info and captured pieces - Right sidebar: Analysis and move history """ self.layout.split_column( Layout(name="header", size=3), Layout(name="main"), Layout(name="footer", size=3), ) self.layout["main"].split_row( Layout(name="sidebar_left", size=30), Layout(name="board", size=60), Layout(name="sidebar_right", size=40), ) self.layout["sidebar_left"].split_column( Layout(name="game_info", size=15), Layout(name="captured", size=10), Layout(name="controls"), ) self.layout["sidebar_right"].split_column( Layout(name="analysis", size=20), Layout(name="move_history") ) def _create_board_display( self, state: CheckersState, last_move: CheckersMove | None = None ) -> Panel: """Create a beautiful board visualization. Generates a rich Panel containing the styled checkers board with pieces, coordinates, and optional highlighting for the last move. Args: state (CheckersState): Current game state last_move (Optional[CheckersMove], optional): Last move to highlight. Defaults to None. Returns: Panel: A styled panel containing the board visualization """ board_lines = [] # Column headers with style col_header = " " + " ".join( f"[bold cyan]{chr(97 + i)}[/bold cyan]" for i in range(8) ) board_lines.append(col_header) board_lines.append(" " + "─" * 33) # Convert last move positions to indices if available last_from = last_to = None if last_move: last_from = self._notation_to_index(last_move.from_position) last_to = self._notation_to_index(last_move.to_position) # Build board rows for row in range(8): row_display = f"[bold cyan]{8 - row}[/bold cyan] │" for col in range(8): # Determine square color is_dark = (row + col) % 2 == 1 square_style = ( self.colors["dark_square"] if is_dark else self.colors["light_square"] ) # Check if this square was part of the last move is_last_move_highlight = "" if (row, col) == last_from or (row, col) == last_to: square_style = self.colors["last_move"] # We'll set a flag to apply special styling to the piece # later if last_move and (row, col) == last_to: is_last_move_highlight = last_move.player # Get piece at this position piece_value = state.board[row][col] piece_display = self._get_piece_display( piece_value, is_last_move_highlight ) # Add spacing and style cell = f" {piece_display} " styled_cell = f"[{square_style}]{cell}[/{square_style}]" row_display += styled_cell row_display += f" │ [bold cyan]{8 - row}[/bold cyan]" board_lines.append(row_display) # Bottom border and column labels board_lines.append(" " + "─" * 33) board_lines.append(col_header) # Create panel with fancy border board_text = "\n".join(board_lines) return Panel( board_text, title="♔ CHECKERS BOARD ♚", title_align="center", border_style=self.colors["board_border"], box=DOUBLE, padding=(1, 2), ) def _get_piece_display(self, piece_value: int, last_move_player: str = "") -> str: """Get styled piece display. Converts a numeric piece value to a styled Unicode symbol. Args: piece_value (int): Piece value (0-4) last_move_player (str, optional): Player color for last move highlighting. Defaults to "". Returns: str: Styled Unicode representation of the piece Note: Piece values: - 0: Empty square - 1: Red piece - 2: Red king - 3: Black piece - 4: Black king """ if piece_value == 0: return self.pieces["empty"] # Special highlight for pieces that were just moved if last_move_player == "red" and piece_value in [1, 2]: # Red piece that was just moved if piece_value == 1: # Regular piece return f"[{self.colors['last_move_red']}]{self.pieces['red']}[/{self.colors['last_move_red']}]" else: # King return f"[{self.colors['last_move_red']}]{self.pieces['red_king']}[/{self.colors['last_move_red']}]" elif last_move_player == "black" and piece_value in [3, 4]: # Black piece that was just moved if piece_value == 3: # Regular piece return f"[{self.colors['last_move_black']}]{self.pieces['black']}[/{self.colors['last_move_black']}]" else: # King return f"[{self.colors['last_move_black']}]{self.pieces['black_king']}[/{self.colors['last_move_black']}]" # Normal piece styling elif piece_value == 1: # Red piece return f"[{self.colors['red_piece']}]{self.pieces['red']}[/{self.colors['red_piece']}]" elif piece_value == 2: # Red king return f"[{self.colors['red_king']}]{self.pieces['red_king']}[/{self.colors['red_king']}]" elif piece_value == 3: # Black piece return f"[{self.colors['black_piece']}]{self.pieces['black']}[/{self.colors['black_piece']}]" elif piece_value == 4: # Black king return f"[{self.colors['black_king']}]{self.pieces['black_king']}[/{self.colors['black_king']}]" return " " def _notation_to_index(self, notation: str) -> tuple[int, int]: """Convert algebraic notation to board indices. Converts algebraic notation (e.g., "a3") to zero-based row and column indices. Args: notation (str): Position in algebraic notation (e.g., "a3") Returns: tuple[int, int]: (row, col) indices Examples: >>> ui = CheckersUI() >>> ui._notation_to_index("a8") (0, 0) >>> ui._notation_to_index("h1") (7, 7) """ col = ord(notation[0]) - 97 row = 8 - int(notation[1]) return row, col def _create_header(self, state: CheckersState) -> Panel: """Create the header panel. Creates a styled header with the game title, current turn, and elapsed time. Args: state (CheckersState): Current game state Returns: Panel: A styled panel for the header """ current_player = state.turn.upper() player_color = self.colors[f"player_{state.turn}"] elapsed = datetime.now() - self.start_time elapsed_str = f"{elapsed.seconds // 60:02d}:{elapsed.seconds % 60:02d}" header_text = Text() header_text.append("♔ ", style="bold bright_red") header_text.append("ROYAL CHECKERS", style="bold bright_yellow") header_text.append(" ♚", style="bold bright_white") # More prominent current turn display turn_indicator = "\n🎮 CURRENT TURN: " if current_player == "RED": piece_symbol = self.pieces["red"] header_text.append(turn_indicator, style="dim") header_text.append( f"{piece_symbol} {current_player} {piece_symbol}", style=f"bold {player_color}", ) else: piece_symbol = self.pieces["black"] header_text.append(turn_indicator, style="dim") header_text.append( f"{piece_symbol} {current_player} {piece_symbol}", style=f"bold {player_color}", ) header_text.append(f" ⏱️ Time: {elapsed_str}", style="dim cyan") return Panel(Align.center(header_text), border_style="bright_blue", box=HEAVY) def _create_game_info(self, state: CheckersState) -> Panel: """Create game information panel. Creates a panel with game status, move count, winner (if any), and piece counts. Args: state (CheckersState): Current game state Returns: Panel: A styled panel with game information """ table = Table(show_header=False, box=None, padding=0) table.add_column(style="bold cyan") table.add_column() table.add_row("📊 Status:", state.game_status.replace("_", " ").title()) table.add_row("🎯 Moves:", str(self.move_count)) if state.winner: winner_color = self.colors[f"player_{state.winner}"] table.add_row( "🏆 Winner:", f"[{winner_color}]{state.winner.upper()}[/{winner_color}]" ) # Piece count with detailed breakdown red_regular = sum(1 for row in state.board for cell in row if cell == 1) red_kings = sum(1 for row in state.board for cell in row if cell == 2) red_pieces = red_regular + red_kings black_regular = sum(1 for row in state.board for cell in row if cell == 3) black_kings = sum(1 for row in state.board for cell in row if cell == 4) black_pieces = black_regular + black_kings table.add_row("", "") # Show pieces with their actual symbols red_info = f"{red_pieces} pieces" if red_kings > 0: red_info += f" ({red_kings} {self.pieces['red_king']})" black_info = f"{black_pieces} pieces" if black_kings > 0: black_info += f" ({black_kings} {self.pieces['black_king']})" table.add_row( f"🔴 Red [{self.pieces['red']}]:", red_info, style=self.colors["player_red"] ) table.add_row( f"⚫ Black [{self.pieces['black']}]:", black_info, style=self.colors["player_black"], ) return Panel( table, title="Game Info", title_align="left", border_style="blue", box=ROUNDED, ) def _create_captured_pieces(self, state: CheckersState) -> Panel: """Create captured pieces display. Creates a panel showing pieces captured by each player. Args: state (CheckersState): Current game state Returns: Panel: A styled panel showing captured pieces """ captured_text = Text() # Red captures with visual representation captured_text.append("🔴 Red captured: ", style=self.colors["player_red"]) red_caps = len(state.captured_pieces.get("red", [])) captured_text.append(f"{red_caps} pieces", style=self.colors["player_red"]) # Add visual representation of captured pieces if red_caps > 0: captured_text.append("\n") for _ in range(min(red_caps, 8)): # Show up to 8 pieces visually captured_text.append( f"{self.pieces['black']} ", style=self.colors["player_black"] ) if red_caps > 8: captured_text.append(f"+{red_caps - 8} more", style="dim") captured_text.append("\n\n") # Black captures captured_text.append("⚫ Black captured: ", style=self.colors["player_black"]) black_caps = len(state.captured_pieces.get("black", [])) captured_text.append(f"{black_caps} pieces", style=self.colors["player_black"]) # Add visual representation of captured pieces if black_caps > 0: captured_text.append("\n") for _ in range(min(black_caps, 8)): # Show up to 8 pieces visually captured_text.append( f"{self.pieces['red']} ", style=self.colors["player_red"] ) if black_caps > 8: captured_text.append(f"+{black_caps - 8} more", style="dim") return Panel( captured_text, title="Captured Pieces", title_align="left", border_style="red", box=ROUNDED, ) def _create_analysis_panel(self, state: CheckersState) -> Panel: """Create analysis panel showing latest analysis. Creates a panel with the latest position analysis for the current player, including material advantage, center control, and suggested moves. Args: state (CheckersState): Current game state Returns: Panel: A styled panel with position analysis """ analysis_text = Text() # Get latest analysis for current player if state.turn == "red" and state.red_analysis: analysis = state.red_analysis[-1] analysis_text.append("🔴 Red's Analysis\n\n", style="bold red") elif state.turn == "black" and state.black_analysis: analysis = state.black_analysis[-1] analysis_text.append("⚫ Black's Analysis\n\n", style="bold white") else: analysis_text.append("No analysis available yet", style="dim") return Panel( analysis_text, title="Position Analysis", title_align="left", border_style="yellow", box=ROUNDED, ) # Format analysis if hasattr(analysis, "material_advantage"): analysis_text.append("📊 Material: ", style="bold") analysis_text.append(f"{analysis.material_advantage}\n\n") analysis_text.append("🎯 Center Control: ", style="bold") analysis_text.append(f"{analysis.control_of_center}\n\n") analysis_text.append("💡 Evaluation: ", style="bold") analysis_text.append(f"{analysis.positional_evaluation}\n\n") if analysis.suggested_moves: analysis_text.append("🎲 Suggested Moves:\n", style="bold") for move in analysis.suggested_moves[:3]: analysis_text.append(f" • {move}\n", style="cyan") return Panel( analysis_text, title="Position Analysis", title_align="left", border_style="yellow", box=ROUNDED, ) def _create_move_history(self, state: CheckersState) -> Panel: """Create move history panel. Creates a panel showing the recent moves made in the game. Args: state (CheckersState): Current game state Returns: Panel: A styled panel with move history """ # Use a table for better formatting if not state.move_history: history_text = Text("No moves yet", style="dim") else: # Create a table for better formatting history_table = Table(show_header=True, box=box.SIMPLE) history_table.add_column("Move", style="dim", width=4) history_table.add_column("Red", style=self.colors["player_red"], width=8) history_table.add_column( "Black", style=self.colors["player_black"], width=8 ) # Process moves in pairs (Red & Black) moves = state.move_history # Round up to include incomplete pairs total_pairs = (len(moves) + 1) // 2 # Show last 8 move pairs (16 moves) at most start_pair = max(0, total_pairs - 8) for pair_idx in range(start_pair, total_pairs): move_num = pair_idx + 1 red_idx = pair_idx * 2 black_idx = red_idx + 1 red_move = str(moves[red_idx]) if red_idx < len(moves) else "" black_move = str(moves[black_idx]) if black_idx < len(moves) else "" # Style the most recent moves differently red_style = "bold bright_red" if red_idx == len(moves) - 1 else "" black_style = "bold bright_white" if black_idx == len(moves) - 1 else "" # Add the row history_table.add_row( str(move_num), f"[{red_style}]{red_move}[/{red_style}]" if red_style else red_move, ( f"[{black_style}]{black_move}[/{black_style}]" if black_style else black_move ), ) history_text = history_table return Panel( history_text, title="Move History", title_align="left", border_style="green", box=ROUNDED, ) def _create_footer(self, message: str = "") -> Panel: """Create footer with status message. Creates a footer panel with a status message or default controls. Args: message (str, optional): Custom message to display. Defaults to "". Returns: Panel: A styled footer panel """ footer_text = Text() if message: footer_text.append(message, style="bold yellow") else: footer_text.append("🎮 ", style="bold") footer_text.append("Use arrow keys to navigate • ", style="dim") footer_text.append("SPACE ", style="bold cyan") footer_text.append("to select • ", style="dim") footer_text.append("Q ", style="bold red") footer_text.append("to quit", style="dim") return Panel( Align.center(footer_text), border_style="bright_blue", box=box.MINIMAL )
[docs] def display_state(self, state: CheckersState, message: str = ""): """Display the complete game state. Updates and displays all UI components with the current game state. Args: state (CheckersState): Current game state message (str, optional): Custom message for the footer. Defaults to "". Examples: >>> ui = CheckersUI() >>> state = CheckersState.initialize() >>> ui.display_state(state) >>> >>> # Display with a custom message >>> ui.display_state(state, "Red is thinking...") """ # Get last move if available last_move = state.move_history[-1] if state.move_history else None # Update all panels self.layout["header"].update(self._create_header(state)) self.layout["board"].update(self._create_board_display(state, last_move)) self.layout["game_info"].update(self._create_game_info(state)) self.layout["captured"].update(self._create_captured_pieces(state)) self.layout["analysis"].update(self._create_analysis_panel(state)) self.layout["move_history"].update(self._create_move_history(state)) self.layout["footer"].update(self._create_footer(message)) # Update move count self.move_count = len(state.move_history) # Clear and print self.console.clear() self.console.print(self.layout)
[docs] def show_thinking(self, player: str): """Show thinking animation. Displays a spinner animation while a player is thinking about a move. Args: player (str): Player who is thinking ("red" or "black") Examples: >>> ui = CheckersUI() >>> ui.show_thinking("red") # Shows a spinner for red's thinking """ with Progress( SpinnerColumn(), TextColumn("[progress.description]{task.description}"), console=self.console, transient=True, ) as progress: progress.add_task( f"[{self.colors[f'player_{player}']}]{ player.capitalize() } is thinking...", total=None, ) time.sleep(0.5)
[docs] def show_move(self, move: CheckersMove): """Show move animation. Displays a brief animation/message when a move is made. Args: move (CheckersMove): The move that was made Examples: >>> ui = CheckersUI() >>> move = CheckersMove(from_position="a3", to_position="b4", player="red") >>> ui.show_move(move) # Shows move message """ move_text = Text() move_text.append("➜ ", style="bold green") move_text.append( f"{move.player.capitalize()} ", style=self.colors[f"player_{move.player}"] ) move_text.append("plays: ", style="dim") move_text.append(str(move), style="bold yellow") panel = Panel(Align.center(move_text), border_style="green", box=box.MINIMAL) self.console.print(panel) time.sleep(1)
[docs] def show_game_over(self, state: CheckersState): """Show game over screen. Displays a game over message with the winner when the game ends. Args: state (CheckersState): Final game state Examples: >>> ui = CheckersUI() >>> state = CheckersState(game_status="game_over", winner="red") >>> ui.show_game_over(state) # Shows "RED WINS!" message """ if state.winner: # Create a much more impressive game over screen winner_text = Text() # Trophy decorations trophy_line = "🏆 " * 7 winner_text.append(f"{trophy_line}\n\n", style="bold bright_yellow") # Game over text winner_text.append( "G A M E O V E R !\n\n", style="bold bright_cyan underline" ) # Winner announcement with piece symbol if state.winner == "red": piece_symbol = self.pieces["red"] crown_symbol = self.pieces["red_king"] winner_text.append( f"{crown_symbol} {piece_symbol} ", style=self.colors[f"player_{state.winner}"], ) winner_text.append( f"{state.winner.upper()} WINS!", style=f"bold {self.colors[f'player_{state.winner}']}", ) winner_text.append( f" {piece_symbol} {crown_symbol}", style=self.colors[f"player_{state.winner}"], ) else: piece_symbol = self.pieces["black"] crown_symbol = self.pieces["black_king"] winner_text.append( f"{crown_symbol} {piece_symbol} ", style=self.colors[f"player_{state.winner}"], ) winner_text.append( f"{state.winner.upper()} WINS!", style=f"bold {self.colors[f'player_{state.winner}']}", ) winner_text.append( f" {piece_symbol} {crown_symbol}", style=self.colors[f"player_{state.winner}"], ) # Move stats winner_text.append( f"\n\nTotal Moves: {len(state.move_history)}", style="bright_green" ) # Trophy line at bottom winner_text.append(f"\n\n{trophy_line}", style="bold bright_yellow") panel = Panel( Align.center(winner_text), border_style="bright_yellow", box=DOUBLE, padding=(2, 8), ) self.console.print("\n") self.console.print(panel) self.console.print("\n")