""" Rich display formatting for game state. Provides formatted console output using the Rich library. Author: Claude Date: 2025-10-26 """ import logging from typing import Optional from rich.console import Console from rich.table import Table from rich.panel import Panel from rich.text import Text from rich import box from app.models.game_models import GameState, DefensiveDecision, OffensiveDecision from app.core.play_resolver import PlayResult logger = logging.getLogger(f'{__name__}.display') console = Console() def display_game_state(state: GameState) -> None: """ Display current game state in formatted panel. Args: state: Current game state """ # Status color based on game status status_color = { "pending": "yellow", "active": "green", "paused": "yellow", "completed": "blue" } # Build state display state_text = Text() state_text.append(f"Game ID: {state.game_id}\n", style="bold") state_text.append(f"League: {state.league_id.upper()}\n") state_text.append(f"Status: ", style="bold") state_text.append(f"{state.status}\n", style=status_color.get(state.status, "white")) state_text.append("\n") # Score state_text.append("Score: ", style="bold cyan") state_text.append(f"Away {state.away_score} - {state.home_score} Home\n", style="cyan") # Inning state_text.append("Inning: ", style="bold magenta") state_text.append(f"{state.inning} {state.half.capitalize()}\n", style="magenta") # Outs state_text.append("Outs: ", style="bold yellow") state_text.append(f"{state.outs}\n", style="yellow") # Runners runners = state.get_all_runners() if runners: state_text.append("\nRunners: ", style="bold green") runner_bases = [f"{base}B(#{player.lineup_id})" for base, player in runners] state_text.append(f"{', '.join(runner_bases)}\n", style="green") else: state_text.append("\nBases: ", style="bold") state_text.append("Empty\n", style="dim") # Current players if state.current_batter: state_text.append(f"\nBatter: Lineup #{state.current_batter.lineup_id}\n") if state.current_pitcher: state_text.append(f"Pitcher: Lineup #{state.current_pitcher.lineup_id}\n") if state.current_catcher: state_text.append(f"Catcher: Lineup #{state.current_catcher.lineup_id}\n") # Pending decision - provide clear guidance if state.pending_decision: state_text.append(f"\n", style="bold") state_text.append("⚠️ WAITING FOR ACTION\n", style="bold yellow") state_text.append("─" * 40 + "\n", style="dim") if state.pending_decision == "defensive": state_text.append("The defense needs to submit their decision.\n", style="yellow") state_text.append("Run: ", style="bold cyan") state_text.append("defensive", style="green") state_text.append(" [OPTIONS]\n", style="dim") elif state.pending_decision == "offensive": state_text.append("The offense needs to submit their decision.\n", style="yellow") state_text.append("Run: ", style="bold cyan") state_text.append("offensive", style="green") state_text.append(" [OPTIONS]\n", style="dim") elif state.pending_decision == "result_selection": state_text.append("Ready to resolve play - both teams have decided.\n", style="yellow") state_text.append("Run: ", style="bold cyan") state_text.append("resolve", style="green") state_text.append("\n", style="dim") else: state_text.append(f"Waiting for: {state.pending_decision}\n", style="yellow") # Last play result if state.last_play_result: state_text.append(f"\nLast Play: ", style="bold") state_text.append(f"{state.last_play_result}\n", style="italic") # Display panel panel = Panel( state_text, title=f"[bold]Game State[/bold]", border_style="blue", box=box.ROUNDED ) console.print(panel) def display_play_result(result: PlayResult, state: GameState) -> None: """ Display play result with rich formatting. Args: result: Play result from resolver state: Updated game state """ result_text = Text() # Outcome result_text.append("Outcome: ", style="bold") result_text.append(f"{result.outcome.value}\n", style="cyan") # Description result_text.append("Result: ", style="bold") result_text.append(f"{result.description}\n\n", style="white") # Dice roll result_text.append("Roll: ", style="bold yellow") result_text.append(f"{result.ab_roll}\n", style="yellow") # Stats if result.outs_recorded > 0: result_text.append(f"Outs: ", style="bold red") result_text.append(f"+{result.outs_recorded}\n", style="red") if result.runs_scored > 0: result_text.append(f"Runs: ", style="bold green") result_text.append(f"+{result.runs_scored}\n", style="green") # Runner advancement if result.runners_advanced: result_text.append(f"\nRunner Movement:\n", style="bold") for from_base, to_base in result.runners_advanced: if to_base == 4: result_text.append(f" {from_base}B → SCORES\n", style="green") else: result_text.append(f" {from_base}B → {to_base}B\n") # Batter result if result.batter_result: if result.batter_result < 4: result_text.append(f"\nBatter: Reaches {result.batter_result}B\n", style="cyan") elif result.batter_result == 4: result_text.append(f"\nBatter: HOME RUN!\n", style="bold green") # Display panel panel = Panel( result_text, title=f"[bold]⚾ Play Result[/bold]", border_style="green", box=box.HEAVY ) console.print(panel) # Show updated score console.print(f"\n[bold cyan]Score: Away {state.away_score} - {state.home_score} Home[/bold cyan]") def display_decision(decision_type: str, decision: Optional[DefensiveDecision | OffensiveDecision]) -> None: """ Display submitted decision. Args: decision_type: 'defensive' or 'offensive' decision: Decision object """ if not decision: console.print(f"[yellow]No {decision_type} decision to display[/yellow]") return decision_text = Text() if isinstance(decision, DefensiveDecision): decision_text.append(f"Infield Depth: {decision.infield_depth}\n") decision_text.append(f"Outfield Depth: {decision.outfield_depth}\n") if decision.hold_runners: decision_text.append(f"Hold Runners: {decision.hold_runners}\n") elif isinstance(decision, OffensiveDecision): decision_text.append(f"Approach: {decision.approach}\n") if decision.steal_attempts: decision_text.append(f"Steal Attempts: {decision.steal_attempts}\n") decision_text.append(f"Hit-and-Run: {decision.hit_and_run}\n") decision_text.append(f"Bunt Attempt: {decision.bunt_attempt}\n") panel = Panel( decision_text, title=f"[bold]{decision_type.capitalize()} Decision[/bold]", border_style="yellow", box=box.ROUNDED ) console.print(panel) def display_box_score(box_score: dict) -> None: """ Display complete box score with game stats, batting stats, and pitching stats. Args: box_score: Box score data with keys: - game_stats: Team totals and linescore - batting_stats: List of player batting lines - pitching_stats: List of player pitching lines """ game_stats = box_score.get('game_stats', {}) batting_stats = box_score.get('batting_stats', []) pitching_stats = box_score.get('pitching_stats', []) # Team Totals Section totals_table = Table(title="[bold]Game Summary[/bold]", box=box.ROUNDED) totals_table.add_column("Team", style="cyan") totals_table.add_column("Runs", justify="right", style="bold green") totals_table.add_column("Hits", justify="right", style="yellow") totals_table.add_column("Errors", justify="right", style="red") totals_table.add_row( "Away", str(game_stats.get('away_runs', 0)), str(game_stats.get('away_hits', 0)), str(game_stats.get('away_errors', 0)) ) totals_table.add_row( "Home", str(game_stats.get('home_runs', 0)), str(game_stats.get('home_hits', 0)), str(game_stats.get('home_errors', 0)) ) console.print(totals_table) # Linescore Section home_linescore = game_stats.get('home_linescore', []) away_linescore = game_stats.get('away_linescore', []) if home_linescore or away_linescore: console.print() linescore_table = Table(title="[bold]Linescore[/bold]", box=box.SIMPLE) linescore_table.add_column("Team", style="cyan") # Add inning columns (1-9 or however many innings) num_innings = max(len(home_linescore), len(away_linescore)) for inning in range(1, num_innings + 1): linescore_table.add_column(str(inning), justify="center") # Away linescore away_row = ["Away"] + [str(away_linescore[i]) if i < len(away_linescore) else "-" for i in range(num_innings)] linescore_table.add_row(*away_row) # Home linescore home_row = ["Home"] + [str(home_linescore[i]) if i < len(home_linescore) else "-" for i in range(num_innings)] linescore_table.add_row(*home_row) console.print(linescore_table) # Batting Stats Section if batting_stats: console.print() batting_table = Table(title="[bold]Batting Stats[/bold]", box=box.ROUNDED) batting_table.add_column("Lineup", style="cyan", width=6) batting_table.add_column("Card", style="yellow", width=6) batting_table.add_column("PA", justify="right", width=4) batting_table.add_column("AB", justify="right", width=4) batting_table.add_column("R", justify="right", width=4) batting_table.add_column("H", justify="right", width=4) batting_table.add_column("2B", justify="right", width=4) batting_table.add_column("3B", justify="right", width=4) batting_table.add_column("HR", justify="right", width=4, style="bold green") batting_table.add_column("RBI", justify="right", width=4) batting_table.add_column("BB", justify="right", width=4) batting_table.add_column("SO", justify="right", width=4) for batter in batting_stats: batting_table.add_row( str(batter.get('lineup_id', '')), str(batter.get('player_card_id', '')), str(batter.get('pa', 0)), str(batter.get('ab', 0)), str(batter.get('run', 0)), str(batter.get('hit', 0)), str(batter.get('double', 0)), str(batter.get('triple', 0)), str(batter.get('hr', 0)), str(batter.get('rbi', 0)), str(batter.get('bb', 0)), str(batter.get('so', 0)) ) console.print(batting_table) # Pitching Stats Section if pitching_stats: console.print() pitching_table = Table(title="[bold]Pitching Stats[/bold]", box=box.ROUNDED) pitching_table.add_column("Lineup", style="cyan", width=6) pitching_table.add_column("Card", style="yellow", width=6) pitching_table.add_column("IP", justify="right", width=6) pitching_table.add_column("BF", justify="right", width=4) pitching_table.add_column("H", justify="right", width=4) pitching_table.add_column("R", justify="right", width=4) pitching_table.add_column("ER", justify="right", width=4) pitching_table.add_column("BB", justify="right", width=4) pitching_table.add_column("SO", justify="right", width=4, style="bold green") pitching_table.add_column("HR", justify="right", width=4) for pitcher in pitching_stats: # Format IP to show fractional innings (e.g., 5.1 for 5 1/3 innings) ip = pitcher.get('ip', 0.0) ip_str = f"{ip:.1f}" if ip else "0.0" pitching_table.add_row( str(pitcher.get('lineup_id', '')), str(pitcher.get('player_card_id', '')), ip_str, str(pitcher.get('batters_faced', 0)), str(pitcher.get('hit_allowed', 0)), str(pitcher.get('run_allowed', 0)), str(pitcher.get('erun', 0)), str(pitcher.get('bb', 0)), str(pitcher.get('so', 0)), str(pitcher.get('hr_allowed', 0)) ) console.print(pitching_table) def print_success(message: str) -> None: """Print success message.""" console.print(f"✓ [green]{message}[/green]") def print_error(message: str) -> None: """Print error message.""" console.print(f"✗ [red]{message}[/red]") def print_info(message: str) -> None: """Print info message.""" console.print(f"[blue]ℹ {message}[/blue]") def print_warning(message: str) -> None: """Print warning message.""" console.print(f"[yellow]⚠ {message}[/yellow]")