strat-gameplay-webapp/backend/terminal_client/display.py
Cal Corum 197d91edfb CLAUDE: Remove defensive alignment field completely
Removed the unused alignment field from DefensiveDecision model and all
related code across backend and frontend.

Backend changes:
- models/game_models.py: Removed alignment field and validator
- terminal_client/display.py: Removed alignment from display
- core/ai_opponent.py: Updated log message
- tests/unit/models/test_game_models.py: Removed alignment tests
- tests/unit/core/test_validators.py: Removed alignment validation test

Frontend changes:
- types/game.ts: Removed alignment from DefensiveDecision interface
- components/Decisions/DefensiveSetup.vue:
  * Removed alignment section from template
  * Removed alignment from localSetup initialization
  * Removed alignmentOptions array
  * Removed alignmentDisplay computed property
  * Removed alignment from hasChanges comparison
  * Removed alignment from visual preview (reorganized to col-span-2)

Rationale: Defensive alignment is not active in the game and will not be
used. Per Cal's decision, remove completely rather than keep as dead code.

Tests: All 728 backend unit tests passing (100%)

Session 1 Part 3 - Change #6 complete
Part of cleanup work from demo review

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-14 13:02:22 -06:00

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