After changing runners_advanced from list[tuple] to list[RunnerAdvancementData], three locations were still trying to unpack as tuples causing "cannot unpack non-iterable RunnerAdvancementData object" error on triples/doubles with runners. Fixed locations: - app/core/game_engine.py:772 - _apply_play_result() advancement map - app/core/game_engine.py:1081 - _save_play() finals lookup - terminal_client/display.py:150 - Runner movement display Also renamed refactor_overview.md to game-engine-play-tracking-design.md for clarity and updated the example code to use dataclass access pattern. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
139 lines
5.4 KiB
Markdown
139 lines
5.4 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 (RunnerAdvancementData is a dataclass)
|
|
finals = {adv.from_base: adv.to_base for adv 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
|