strat-gameplay-webapp/backend/terminal_client/display.py
Cal Corum aabb90feb5 CLAUDE: Implement player models and optimize database queries
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>
2025-10-28 14:08:56 -05:00

246 lines
8.1 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_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]")