Added comprehensive documentation for the GameEngine refactor:
- refactor_overview.md: Detailed plan for forward-looking play tracking
- status-2025-10-24-1430.md: Session summary from Phase 2 implementation
These documents capture the architectural design decisions and
implementation roadmap that guided the refactor completed in commit 13e924a.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
139 lines
5.5 KiB
Markdown
139 lines
5.5 KiB
Markdown
⎿ 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
|