⎿  Refactor GameEngine for Forward-Looking Play Tracking Problem Current implementation does awkward "lookbacks" to determine runner positions. Need clean "prepare → execute → save" pattern that was error-prone in legacy implementation. Solution Overview Enrich GameState with current play snapshot. Use start_game() to validate lineups and prepare first play. Each resolve_play() explicitly orchestrates the sequence. Add state recovery from last completed play. Changes Required 1. Update GameState Model (game_models.py) Add fields to track current play snapshot: # Batting order tracking (per team) away_team_batter_idx: int = 0 # 0-8, wraps for MVP (no subs yet) home_team_batter_idx: int = 0 # 0-8 # Current play snapshot (set by _prepare_next_play) current_batter_lineup_id: Optional[int] = None current_pitcher_lineup_id: Optional[int] = None current_catcher_lineup_id: Optional[int] = None current_on_base_code: int = 0 # Bit field (1=1st, 2=2nd, 4=3rd, 7=loaded) 2. Refactor start_game() in GameEngine - Validate BOTH lineups complete (minimum 9 players each) - HARD REQUIREMENT - Throw ValidationError if lineups incomplete or missing positions - After transitioning to active, call _prepare_next_play() - Return state with first play ready to execute 3. Create _prepare_next_play() Method # Determine current batter and advance index if state.half == "top": current_idx = state.away_team_batter_idx state.away_team_batter_idx = (current_idx + 1) % 9 else: current_idx = state.home_team_batter_idx state.home_team_batter_idx = (current_idx + 1) % 9 # Fetch active lineups, set snapshot state.current_batter_lineup_id = batting_lineup[current_idx].id state.current_pitcher_lineup_id = next(p for p in fielding if p.position == "P").id state.current_catcher_lineup_id = next(p for p in fielding if p.position == "C").id # Calculate on_base_code from state.runners state.current_on_base_code = 0 for runner in state.runners: if runner.on_base == 1: state.current_on_base_code |= 1 if runner.on_base == 2: state.current_on_base_code |= 2 if runner.on_base == 3: state.current_on_base_code |= 4 4. Refactor resolve_play() Orchestration Explicit sequence (no hidden side effects): 1. Resolve play with dice rolls 2. Save play to DB (uses snapshot from GameState) 3. Apply result to state (outs, score, runners) 4. Update game state in DB 5. If outs >= 3: - Advance inning (clear bases, reset outs, increment, batch save rolls) - Update game state in DB again 6. Prepare next play (always last step) 5. Update _save_play_to_db() Use snapshot from GameState (NO lookbacks): # From snapshot batter_id = state.current_batter_lineup_id pitcher_id = state.current_pitcher_lineup_id catcher_id = state.current_catcher_lineup_id on_base_code = state.current_on_base_code # 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) on_third_id = next((r.lineup_id for r in state.runners if r.on_base == 3), None) # Runners AFTER play (from result.runners_advanced) # Build dict of from_base -> to_base finals = {from_base: to_base for from_base, to_base in result.runners_advanced} on_first_final = finals.get(1) # None if out/scored, 1-4 if moved on_second_final = finals.get(2) on_third_final = finals.get(3) # Batter result batter_final = result.batter_result # None=out, 1-4=base reached 6. Keep _apply_play_result() Focused Only update in-memory state (NO database writes): - Update outs, score, runners, play_count - Database writes handled by orchestration layer 7. Keep _advance_inning() Focused Only handle inning transition: - Clear bases, reset outs, increment inning/half - Check game over, batch save rolls - NO prepare_next_play (orchestration handles) - NO database writes (orchestration handles) 8. Add State Recovery (state_manager.py) New method: recover_game_from_last_play(game_id) 1. Load games table → basic state 2. Query last completed play: SELECT * FROM plays WHERE game_id=X AND complete=true ORDER BY play_number DESC LIMIT 1 3. If play exists: - Runners: on_first_final, on_second_final, on_third_final (use lineup_ids) - Batting indices: derive from batting_order and team 4. If no play (just started): - Initialize: indices=0, no runners 5. Reconstruct GameState in memory 6. Call _prepare_next_play() → ready to resume Benefits ✅ No special case for first play ✅ No awkward lookbacks ✅ Clean validation (can't start without lineups) ✅ Single source of truth (GameState) ✅ Explicit orchestration (easy to understand) ✅ Fast state recovery (one query, no replay) ✅ Separate batter indices (18+ queries saved per game) Testing Updates Update test script to verify: - start_game() fails with incomplete lineups - on_base_code calculated correctly (bit field 1|2|4) - Runner lineup_ids tracked in Play records - Batting order cycles 0-8 per team independently - State recovery from last play works