Source code for haive.games.connect4.ui

"""Connect4 rich UI visualization module.

This module provides a visually appealing terminal UI for Connect4 games,
with styled components, animations, and comprehensive game information.

It uses the Rich library to create a console-based UI with:
    - Colorful board display with piece symbols
    - Move history panel
    - Game status and information
    - Position analysis display
    - Move and thinking animations

Example:
    >>> from haive.games.connect4.ui import Connect4UI
    >>> from haive.games.connect4.state import Connect4State
    >>>
    >>> ui = Connect4UI()
    >>> state = Connect4State.initialize()
    >>> ui.display_state(state)  # Display the initial board
    >>>
    >>> # Show thinking animation for player move
    >>> ui.show_thinking("red")
    >>>
    >>> # Display a move
    >>> from haive.games.connect4.models import Connect4Move
    >>> move = Connect4Move(column=3)
    >>> ui.show_move(move, "red")

"""

import time

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.progress import Progress, SpinnerColumn, TextColumn
from rich.table import Table
from rich.text import Text

from haive.games.connect4.models import Connect4Move
from haive.games.connect4.state import Connect4State


[docs] class Connect4UI: """Rich UI for beautiful Connect4 game visualization. This class provides a visually appealing terminal UI for Connect4 games, with styled components, animations, and comprehensive game information. Features: - Colorful board display with piece symbols - Move history panel - Game status and information - Position analysis display - Move and thinking animations Attributes: console (Console): Rich console for output layout (Layout): Layout manager for UI components colors (dict): Color schemes for different UI elements Examples: >>> ui = Connect4UI() >>> state = Connect4State.initialize() >>> ui.display_state(state) # Display the initial board """ def __init__(self): """Initialize the Connect4 UI with default settings.""" self.console = Console() self.layout = Layout() # Define colors and styles self.colors = { "red": {"piece": "bright_red", "bg": "on red", "text": "red"}, "yellow": {"piece": "bright_yellow", "bg": "on yellow", "text": "yellow"}, "board": "blue", "header": "bold cyan", "info": "bright_white", "success": "green", "warning": "bright_yellow", "error": "bright_red", } # Set up the layout self._setup_layout() # Move history self.move_history: list[str] = [] self.move_count = 0 def _setup_layout(self): """Set up the layout structure for the UI.""" # Main layout with header, board, and sidebar self.layout.split( Layout(name="header", size=3), Layout(name="main", ratio=1), ) # Split main area into board and sidebar self.layout["main"].split_row( Layout(name="board", ratio=3), Layout(name="sidebar", ratio=2), ) # Split sidebar into sections self.layout["sidebar"].split( Layout(name="game_info", size=6), Layout(name="analysis", ratio=2), Layout(name="move_history", ratio=1), ) def _render_header(self, state: Connect4State) -> Panel: """Render the game header with title and status. Args: state: Current game state Returns: Panel: Styled header panel """ title = Text("🔴 🟡 CONNECT 4 GAME 🟡 🔴", style=self.colors["header"]) status = Text(f"Status: {state.game_status.upper()}", style=self.colors["info"]) if state.game_status == "ongoing": turn_text = f"Current Turn: [bold {self.colors[state.turn]['text']}]{state.turn.upper()}[/]" else: winner_color = ( self.colors[state.winner]["text"] if state.winner else self.colors["info"] ) turn_text = f"Winner: [bold {winner_color}]{ state.winner.upper() if state.winner else 'DRAW' }[/]" return Panel( Align.center( Text.assemble( title, "\n", status, "\n", Text(turn_text, justify="center") ) ), box=ROUNDED, border_style=self.colors["header"], padding=(0, 2), ) def _render_board(self, state: Connect4State) -> Panel: """Render the Connect4 board with pieces. Args: state: Current game state Returns: Panel: Styled board panel """ # Create a table for the board board_table = Table( show_header=True, box=ROUNDED, expand=False, border_style=self.colors["board"], ) # Add column headers (0-6) for col in range(7): board_table.add_column( f"[bold white]{col}[/]", justify="center", vertical="middle", width=3 ) # Add board rows for _row_idx, row in enumerate(state.board): row_cells = [] for _col_idx, cell in enumerate(row): if cell is None: row_cells.append("⚪") # Empty cell elif cell == "red": row_cells.append( f"[{self.colors['red']['piece']}]🔴[/]" ) # Red piece elif cell == "yellow": row_cells.append( f"[{self.colors['yellow']['piece']}]🟡[/]" ) # Yellow piece board_table.add_row(*row_cells) return Panel( Align.center(board_table), title="Game Board", title_align="center", border_style=self.colors["board"], padding=(1, 1), ) def _render_game_info(self, state: Connect4State) -> Panel: """Render game information panel. Args: state: Current game state Returns: Panel: Game information panel """ info_table = Table( show_header=False, box=None, expand=True, padding=(0, 1), ) info_table.add_column("Key", style="bright_blue") info_table.add_column("Value", style="white") # Add game information info_table.add_row("Game Phase", state.game_status.upper()) info_table.add_row( "Current Turn", f"[{self.colors[state.turn]['text']}]{state.turn.upper()}[/]", ) info_table.add_row("Total Moves", str(len(state.move_history))) # Add winner information if game is over if state.game_status != "ongoing": winner_text = state.winner.upper() if state.winner else "DRAW" winner_style = ( self.colors[state.winner]["text"] if state.winner else "white" ) info_table.add_row("Result", f"[bold {winner_style}]{winner_text}[/]") return Panel( info_table, title="Game Info", title_align="center", border_style="bright_blue", padding=(1, 1), ) def _render_analysis(self, state: Connect4State) -> Panel: """Render analysis information panel. Args: state: Current game state Returns: Panel: Analysis information panel """ # Get the latest analysis for the previous player analysis = None analysis_player = None if state.red_analysis and state.turn == "yellow": analysis = state.red_analysis[-1] analysis_player = "red" elif state.yellow_analysis and state.turn == "red": analysis = state.yellow_analysis[-1] analysis_player = "yellow" if not analysis: return Panel( "[italic]No analysis available yet[/]", title="Position Analysis", title_align="center", border_style="bright_blue", padding=(1, 1), ) # Create analysis table analysis_table = Table( show_header=False, box=None, expand=True, padding=(0, 1), ) analysis_table.add_column("Metric", style="cyan") analysis_table.add_column("Value", style="white") # Add analysis data player_color = self.colors[analysis_player]["text"] analysis_table.add_row( "Player", f"[bold {player_color}]{analysis_player.upper()}[/]" ) # Position score score = analysis.get("position_score", 0) score_style = "green" if score > 0 else "red" if score < 0 else "white" analysis_table.add_row("Position Score", f"[{score_style}]{score}[/]") # Center control center_control = analysis.get("center_control", 5) center_style = ( "green" if center_control > 7 else "yellow" if center_control > 4 else "red" ) analysis_table.add_row( "Center Control", f"[{center_style}]{center_control}/10[/]" ) # Winning chances winning_chances = analysis.get("winning_chances", 50) chances_style = ( "green" if winning_chances > 70 else "yellow" if winning_chances > 40 else "red" ) analysis_table.add_row( "Winning Chances", f"[{chances_style}]{winning_chances}%[/]" ) # Threats threats = analysis.get("threats", {}) winning_moves = threats.get("winning_moves", []) blocking_moves = threats.get("blocking_moves", []) if winning_moves: winning_text = ", ".join(str(col) for col in winning_moves) analysis_table.add_row("Winning Moves", f"[bold green]{winning_text}[/]") if blocking_moves: blocking_text = ", ".join(str(col) for col in blocking_moves) analysis_table.add_row("Blocking Needed", f"[bold red]{blocking_text}[/]") # Suggested columns suggested = analysis.get("suggested_columns", []) if suggested: suggested_text = ", ".join(str(col) for col in suggested) analysis_table.add_row( "Suggested Columns", f"[bold cyan]{suggested_text}[/]" ) return Panel( analysis_table, title="Position Analysis", title_align="center", border_style="bright_blue", padding=(1, 1), ) def _render_move_history(self, state: Connect4State) -> Panel: """Render move history panel. Args: state: Current game state Returns: Panel: Move history panel """ if not state.move_history: return Panel( "[italic]No moves yet[/]", title="Move History", title_align="center", border_style="bright_blue", padding=(1, 1), ) # Create move history table history_table = Table( show_header=True, box=None, expand=True, padding=(0, 1), ) history_table.add_column("#", style="dim", width=3) history_table.add_column("Player", style="white") history_table.add_column("Move", style="white") # Display the most recent moves (up to 10) for i, move in enumerate(state.move_history[-10:]): actual_move_num = len(state.move_history) - 10 + i + 1 player = "red" if actual_move_num % 2 == 1 else "yellow" player_color = self.colors[player]["text"] history_table.add_row( str(actual_move_num), f"[{player_color}]{player.upper()}[/]", f"Column {move.column}", ) return Panel( history_table, title="Recent Moves", title_align="center", border_style="bright_blue", padding=(1, 1), )
[docs] def display_state(self, state: Connect4State | dict) -> None: """Display the current game state with rich formatting. Renders the complete game state including board, game info, analysis, and move history in a formatted layout. Args: state (Union[Connect4State, dict]): Current game state (Connect4State or dict) Returns: None Example: >>> ui = Connect4UI() >>> state = Connect4State.initialize() >>> ui.display_state(state) """ # Convert dict to Connect4State if needed if isinstance(state, dict): state = Connect4State(**state) # Update each component in the layout self.layout["header"].update(self._render_header(state)) self.layout["board"].update(self._render_board(state)) self.layout["game_info"].update(self._render_game_info(state)) self.layout["analysis"].update(self._render_analysis(state)) self.layout["move_history"].update(self._render_move_history(state)) # Render the complete layout self.console.clear() self.console.print(self.layout)
[docs] def show_thinking(self, player: str, message: str = "Thinking...") -> None: """Display a thinking animation for the current player. Shows a spinner animation with player-colored text to indicate that the player is thinking about their move. Args: player (str): Current player ("red" or "yellow") message (str, optional): Custom message to display. Defaults to "Thinking...". Returns: None Example: >>> ui = Connect4UI() >>> ui.show_thinking("red", "Analyzing position...") """ player_color = self.colors[player]["text"] with Progress( SpinnerColumn(), TextColumn(f"[{player_color}]{player.upper()}[/] {message}"), console=self.console, transient=True, ) as progress: progress.add_task("thinking", total=None) time.sleep(1.0) # Show thinking animation for 1 second
[docs] def show_move(self, move: Connect4Move, player: str) -> None: """Display a move animation. Shows a formatted message indicating which player made which move, with color-coded player name and piece symbol. Args: move (Connect4Move): The move being made player (str): Player making the move ("red" or "yellow") Returns: None Example: >>> ui = Connect4UI() >>> move = Connect4Move(column=3) >>> ui.show_move(move, "red") """ player_color = self.colors[player]["text"] piece_symbol = "🔴" if player == "red" else "🟡" self.console.print( f"\n[{player_color}]{player.upper()}[/] moves: {piece_symbol} Column {move.column}" ) time.sleep(0.5) # Brief pause after showing the move
[docs] def show_game_over(self, winner: str | None = None) -> None: """Display game over message with result. Shows a game over panel with the winner highlighted in their color, or indicating a draw if there's no winner. Args: winner (Optional[str], optional): Winning player or None for a draw. Defaults to None. Returns: None Example: >>> ui = Connect4UI() >>> ui.show_game_over("red") # Red player wins >>> ui.show_game_over(None) # Draw """ if winner: winner_color = self.colors[winner]["text"] message = f"[bold {winner_color}]{winner.upper()}[/] WINS!" self.console.print( Panel( Align.center(Text(message, justify="center")), title="🏆 GAME OVER 🏆", border_style="bright_green", padding=(1, 2), ) ) else: self.console.print( Panel( Align.center(Text("IT'S A DRAW!", justify="center")), title="🏆 GAME OVER 🏆", border_style="bright_yellow", padding=(1, 2), ) ) time.sleep(1.0) # Pause to show the game over message