Updated terminal client REPL to work with refactored GameState structure where current_batter/pitcher/catcher are now LineupPlayerState objects instead of integer IDs. Also standardized all documentation to properly show 'uv run' prefixes for Python commands. REPL Updates: - terminal_client/display.py: Access lineup_id from LineupPlayerState objects - terminal_client/repl.py: Fix typos (self.current_game → self.current_game_id) - tests/unit/terminal_client/test_commands.py: Create proper LineupPlayerState objects in test fixtures (2 tests fixed, all 105 terminal client tests passing) Documentation Updates (100+ command examples): - CLAUDE.md: Updated pytest examples to use 'uv run' prefix - terminal_client/CLAUDE.md: Updated ~40 command examples - tests/CLAUDE.md: Updated all test commands (unit, integration, debugging) - app/*/CLAUDE.md: Updated test and server startup commands (5 files) All Python commands now consistently use 'uv run' prefix to align with project's UV migration, improving developer experience and preventing confusion about virtual environment activation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
248 lines
8.2 KiB
Python
248 lines
8.2 KiB
Python
"""
|
||
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"Alignment: {decision.alignment}\n")
|
||
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(state: GameState) -> None:
|
||
"""
|
||
Display simple box score (placeholder for future enhancement).
|
||
|
||
Args:
|
||
state: Current game state
|
||
"""
|
||
table = Table(title="Box Score", box=box.SIMPLE)
|
||
|
||
table.add_column("Team", style="cyan")
|
||
table.add_column("Score", justify="right", style="bold")
|
||
|
||
table.add_row("Away", str(state.away_score))
|
||
table.add_row("Home", str(state.home_score))
|
||
|
||
console.print(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]")
|