CLAUDE: Fix game recovery and add required field validation for plays
Fixed critical bugs in game recovery and play persistence: 1. Terminal REPL Auto-Recovery: - Added _ensure_game_loaded() helper to auto-recover games from database - Calls state_manager.recover_game() when game not in memory - Calls _prepare_next_play() after recovery to populate snapshot fields - Enables seamless continuation of games across REPL sessions 2. Play Validation: - Added verification in _save_play_to_db() for required fields - Ensures batter_id, pitcher_id, catcher_id are never NULL - Raises ValueError with clear error message if fields missing - Prevents database constraint violations 3. Updated Commands: - All REPL commands now call _ensure_game_loaded() - Commands: defensive, offensive, resolve, status, quick_play, box_score - Fixes "Game state not found" errors on recovered games Root Cause: - state_manager.recover_game() rebuilds GameState from database - But didn't populate snapshot fields (current_batter_lineup_id, etc.) - _save_play_to_db() requires these fields to save plays - Solution: Call _prepare_next_play() after recovery Files Modified: - app/core/game_engine.py - Added verification in _save_play_to_db() - terminal_client/repl.py - Added _ensure_game_loaded() and integrated Testing: Successfully recovered game, submitted decisions, and resolved plays 🚀 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
918beadf24
commit
05fc037f2b
@ -425,6 +425,9 @@ class GameEngine:
|
||||
Save play to database using snapshot from GameState.
|
||||
|
||||
Uses the pre-calculated snapshot fields (no database lookbacks).
|
||||
|
||||
Raises:
|
||||
ValueError: If required player IDs are missing
|
||||
"""
|
||||
# Use snapshot from GameState (set by _prepare_next_play)
|
||||
batter_id = state.current_batter_lineup_id
|
||||
@ -432,6 +435,23 @@ class GameEngine:
|
||||
catcher_id = state.current_catcher_lineup_id
|
||||
on_base_code = state.current_on_base_code
|
||||
|
||||
# VERIFY required fields are present
|
||||
if batter_id is None:
|
||||
raise ValueError(
|
||||
f"Cannot save play: batter_id is None. "
|
||||
f"Game {state.game_id} may need _prepare_next_play() called after recovery."
|
||||
)
|
||||
if pitcher_id is None:
|
||||
raise ValueError(
|
||||
f"Cannot save play: pitcher_id is None. "
|
||||
f"Game {state.game_id} may need _prepare_next_play() called after recovery."
|
||||
)
|
||||
if catcher_id is None:
|
||||
raise ValueError(
|
||||
f"Cannot save play: catcher_id is None. "
|
||||
f"Game {state.game_id} may need _prepare_next_play() called after recovery."
|
||||
)
|
||||
|
||||
# Runners on base BEFORE play (from state.runners)
|
||||
on_first_id = next((r.lineup_id for r in state.runners if r.on_base == 1), None)
|
||||
on_second_id = next((r.lineup_id for r in state.runners if r.on_base == 2), None)
|
||||
|
||||
@ -71,6 +71,36 @@ Note: Use underscores in command names (e.g., 'new_game' not 'new-game')
|
||||
raise ValueError("No current game")
|
||||
return self.current_game_id
|
||||
|
||||
async def _ensure_game_loaded(self, game_id: UUID) -> None:
|
||||
"""
|
||||
Ensure game is loaded in state_manager.
|
||||
|
||||
If game exists in database but not in memory, recover it using
|
||||
state_manager.recover_game() which replays plays to rebuild state,
|
||||
then prepare the next play to populate snapshot fields.
|
||||
"""
|
||||
# Check if already in memory
|
||||
state = state_manager.get_state(game_id)
|
||||
if state is not None:
|
||||
return # Already loaded
|
||||
|
||||
# Try to recover from database
|
||||
try:
|
||||
display.print_info(f"Loading game {game_id} from database...")
|
||||
recovered_state = await state_manager.recover_game(game_id)
|
||||
|
||||
if recovered_state and recovered_state.status == "active":
|
||||
# Call _prepare_next_play to populate snapshot fields
|
||||
# (batter_id, pitcher_id, catcher_id, on_base_code)
|
||||
await game_engine._prepare_next_play(recovered_state)
|
||||
logger.debug(f"Prepared snapshot for recovered game {game_id}")
|
||||
|
||||
display.print_success("Game loaded successfully")
|
||||
except Exception as e:
|
||||
display.print_error(f"Failed to load game: {e}")
|
||||
logger.error(f"Game recovery failed for {game_id}: {e}", exc_info=True)
|
||||
raise ValueError(f"Game {game_id} not found")
|
||||
|
||||
def _run_async(self, coro):
|
||||
"""
|
||||
Helper to run async functions using persistent event loop.
|
||||
@ -201,6 +231,7 @@ Note: Use underscores in command names (e.g., 'new_game' not 'new-game')
|
||||
async def _defensive():
|
||||
try:
|
||||
gid = self._ensure_game()
|
||||
await self._ensure_game_loaded(gid)
|
||||
|
||||
# Parse arguments
|
||||
args = arg.split()
|
||||
@ -265,6 +296,7 @@ Note: Use underscores in command names (e.g., 'new_game' not 'new-game')
|
||||
async def _offensive():
|
||||
try:
|
||||
gid = self._ensure_game()
|
||||
await self._ensure_game_loaded(gid)
|
||||
|
||||
# Parse arguments
|
||||
args = arg.split()
|
||||
@ -320,6 +352,7 @@ Note: Use underscores in command names (e.g., 'new_game' not 'new-game')
|
||||
async def _resolve():
|
||||
try:
|
||||
gid = self._ensure_game()
|
||||
await self._ensure_game_loaded(gid)
|
||||
|
||||
result = await game_engine.resolve_play(gid)
|
||||
state = await game_engine.get_game_state(gid)
|
||||
@ -345,8 +378,9 @@ Note: Use underscores in command names (e.g., 'new_game' not 'new-game')
|
||||
async def _status():
|
||||
try:
|
||||
gid = self._ensure_game()
|
||||
state = await game_engine.get_game_state(gid)
|
||||
await self._ensure_game_loaded(gid)
|
||||
|
||||
state = await game_engine.get_game_state(gid)
|
||||
if state:
|
||||
display.display_game_state(state)
|
||||
else:
|
||||
@ -373,6 +407,8 @@ Note: Use underscores in command names (e.g., 'new_game' not 'new-game')
|
||||
async def _quick_play():
|
||||
try:
|
||||
gid = self._ensure_game()
|
||||
await self._ensure_game_loaded(gid)
|
||||
|
||||
count = int(arg) if arg.strip() else 1
|
||||
|
||||
for i in range(count):
|
||||
@ -420,6 +456,7 @@ Note: Use underscores in command names (e.g., 'new_game' not 'new-game')
|
||||
async def _box_score():
|
||||
try:
|
||||
gid = self._ensure_game()
|
||||
await self._ensure_game_loaded(gid)
|
||||
state = await game_engine.get_game_state(gid)
|
||||
|
||||
if state:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user