This commit includes Week 6 player models implementation and critical performance optimizations discovered during testing. ## Player Models (Week 6 - 50% Complete) **New Files:** - app/models/player_models.py (516 lines) - BasePlayer abstract class with polymorphic interface - SbaPlayer with API parsing factory method - PdPlayer with batting/pitching scouting data support - Supporting models: PdCardset, PdRarity, PdBattingCard, PdPitchingCard - tests/unit/models/test_player_models.py (692 lines) - 32 comprehensive unit tests, all passing - Tests for BasePlayer, SbaPlayer, PdPlayer, polymorphism **Architecture:** - Simplified single-layer approach vs planned two-layer - Factory methods handle API → Game transformation directly - SbaPlayer.from_api_response(data) - parses SBA API inline - PdPlayer.from_api_response(player_data, batting_data, pitching_data) - Full Pydantic validation, type safety, and polymorphism ## Performance Optimizations **Database Query Reduction (60% fewer queries per play):** - Before: 5 queries per play (INSERT play, SELECT play with JOINs, SELECT games, 2x SELECT lineups) - After: 2 queries per play (INSERT play, UPDATE games conditionally) Changes: 1. Lineup caching (game_engine.py:384-425) - Check state_manager.get_lineup() cache before DB fetch - Eliminates 2 SELECT queries per play 2. Remove unnecessary refresh (operations.py:281-302) - Removed session.refresh(play) after INSERT - Eliminates 1 SELECT with 3 expensive LEFT JOINs 3. Direct UPDATE statement (operations.py:109-165) - Changed update_game_state() to use direct UPDATE - No longer does SELECT + modify + commit 4. Conditional game state updates (game_engine.py:200-217) - Only UPDATE games table when score/inning/status changes - Captures state before/after and compares - ~40-60% fewer updates (many plays don't score) ## Bug Fixes 1. Fixed outs_before tracking (game_engine.py:551) - Was incorrectly calculating: state.outs - result.outs_recorded - Now correctly captures: state.outs (before applying result) - All play records now have accurate out counts 2. Fixed game recovery (state_manager.py:312-314) - AttributeError when recovering: 'GameState' has no attribute 'runners' - Changed to use state.get_all_runners() method - Games can now be properly recovered from database ## Enhanced Terminal Client **Status Display Improvements (terminal_client/display.py:75-97):** - Added "⚠️ WAITING FOR ACTION" section when play is pending - Shows specific guidance: - "The defense needs to submit their decision" → Run defensive [OPTIONS] - "The offense needs to submit their decision" → Run offensive [OPTIONS] - "Ready to resolve play" → Run resolve - Color-coded command hints for better UX ## Documentation Updates **backend/CLAUDE.md:** - Added comprehensive Player Models section (204 lines) - Updated Current Phase status to Week 6 (~50% complete) - Documented all optimizations and bug fixes - Added integration examples and usage patterns **New Files:** - .claude/implementation/week6-status-assessment.md - Comprehensive Week 6 progress review - Architecture decision rationale (single-layer vs two-layer) - Completion status and next priorities - Updated roadmap for remaining Week 6 work ## Test Results - Player models: 32/32 tests passing - All existing tests continue to pass - Performance improvements verified with terminal client ## Next Steps (Week 6 Remaining) 1. Configuration system (BaseConfig, SbaConfig, PdConfig) 2. Result charts & PD play resolution with ratings 3. API client for live roster data (deferred) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
246 lines
8.1 KiB
Python
246 lines
8.1 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_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 - 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]")
|