strat-gameplay-webapp/backend/terminal_client/display.py
Cal Corum eab61ad966 CLAUDE: Phases 3.5, F1-F5 Complete - Statistics & Frontend Components
This commit captures work from multiple sessions building the statistics
system and frontend component library.

Backend - Phase 3.5: Statistics System
- Box score statistics with materialized views
- Play stat calculator for real-time updates
- Stat view refresher service
- Alembic migration for materialized views
- Test coverage: 41 new tests (all passing)

Frontend - Phase F1: Foundation
- Composables: useGameState, useGameActions, useWebSocket
- Type definitions and interfaces
- Store setup with Pinia

Frontend - Phase F2: Game Display
- ScoreBoard, GameBoard, CurrentSituation, PlayByPlay components
- Demo page at /demo

Frontend - Phase F3: Decision Inputs
- DefensiveSetup, OffensiveApproach, StolenBaseInputs components
- DecisionPanel orchestration
- Demo page at /demo-decisions
- Test coverage: 213 tests passing

Frontend - Phase F4: Dice & Manual Outcome
- DiceRoller component
- ManualOutcomeEntry with validation
- PlayResult display
- GameplayPanel orchestration
- Demo page at /demo-gameplay
- Test coverage: 119 tests passing

Frontend - Phase F5: Substitutions
- PinchHitterSelector, DefensiveReplacementSelector, PitchingChangeSelector
- SubstitutionPanel with tab navigation
- Demo page at /demo-substitutions
- Test coverage: 114 tests passing

Documentation:
- PHASE_3_5_HANDOFF.md - Statistics system handoff
- PHASE_F2_COMPLETE.md - Game display completion
- Frontend phase planning docs
- NEXT_SESSION.md updated for Phase F6

Configuration:
- Package updates (Nuxt 4 fixes)
- Tailwind config enhancements
- Game store updates

Test Status:
- Backend: 731/731 passing (100%)
- Frontend: 446/446 passing (100%)
- Total: 1,177 tests passing

Next Phase: F6 - Integration (wire all components into game page)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-14 09:52:30 -06:00

361 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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(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]")