Created comprehensive terminal testing tool with two modes: 1. Interactive REPL (recommended) - Persistent in-memory state 2. Standalone CLI commands - Config file persistence Features: - Interactive REPL using Python cmd module - Persistent event loop prevents DB connection issues - 11 commands for full game control (new_game, defensive, offensive, resolve, etc.) - Beautiful Rich formatting with colors and panels - Auto-generated test lineups for rapid testing - Direct GameEngine access (no WebSocket overhead) - Config file (~/.terminal_client_config.json) for state persistence Files added: - terminal_client/repl.py (525 lines) - Interactive REPL - terminal_client/main.py (516 lines) - Click standalone commands - terminal_client/display.py (218 lines) - Rich formatting - terminal_client/config.py (89 lines) - Persistent config - terminal_client/__main__.py - Dual mode entry point - terminal_client/CLAUDE.md (725 lines) - Full documentation Updated: - backend/CLAUDE.md - Added terminal client to testing section - requirements.txt - Added rich==13.9.4 Perfect for rapid iteration on game engine without building frontend! 🚀 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
226 lines
7.0 KiB
Python
226 lines
7.0 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
|
||
if state.runners:
|
||
state_text.append("\nRunners: ", style="bold green")
|
||
runner_bases = [f"{r.on_base}B(#{r.lineup_id})" for r in state.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_lineup_id:
|
||
state_text.append(f"\nBatter: Lineup #{state.current_batter_lineup_id}\n")
|
||
if state.current_pitcher_lineup_id:
|
||
state_text.append(f"Pitcher: Lineup #{state.current_pitcher_lineup_id}\n")
|
||
|
||
# Pending decision
|
||
if state.pending_decision:
|
||
state_text.append(f"\nPending: ", style="bold red")
|
||
state_text.append(f"{state.pending_decision} decision\n", style="red")
|
||
|
||
# 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]")
|