Add comprehensive unit and integration tests for Week 5 deliverables: - test_play_resolver.py: 18 tests covering outcome resolution and runner advancement - test_validators.py: 36 tests covering game state, decisions, lineups, and flow - test_game_engine.py: 7 test classes for complete game flow integration Update implementation documentation to reflect completed status: - 00-index.md: Mark Phase 2 Weeks 4-5 complete with test coverage - 02-week5-game-logic.md: Comprehensive test details and completion status - 02-game-engine.md: Forward-looking snapshot pattern documentation Week 5 now fully complete with 54 unit tests + 7 integration test classes passing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
14 KiB
Phase 2: Game Engine Core
Duration: Weeks 4-6 Status: ✅ Weeks 4-5 Complete, Week 6 Pending Prerequisites: Phase 1 Complete
Overview
Build the core game simulation engine with in-memory state management, play resolution logic, and database persistence. Implement the polymorphic player model system and league configuration framework.
Key Objectives (Actual Status)
By end of Phase 2, you should have:
- ✅ COMPLETE: In-memory game state management working (Week 4)
- ✅ COMPLETE: Play resolution engine with dice rolls (Week 5, enhanced with AbRoll)
- 🔲 PENDING: League configuration system (SBA and PD configs) - Week 6
- ✅ COMPLETE: Polymorphic player models (Lineup & RosterLink polymorphic, not BasePlayer hierarchy)
- ✅ COMPLETE: Database persistence layer with async operations (Week 4)
- ✅ COMPLETE: State recovery mechanism from database (Week 4)
- ✅ COMPLETE: Basic game flow (start → plays → end) - Working in manual tests
Major Components to Implement
1. Game Engine (backend/app/core/game_engine.py)
- Game session initialization
- Turn management (defensive → stolen base → offensive → resolution)
- Action processing and validation
- State update coordination
- Event emission to WebSocket clients
2. State Manager (backend/app/core/state_manager.py)
- In-memory game state dictionary
- State CRUD operations
- State lifecycle management
- Cache eviction for idle games
- State recovery from database
3. Play Resolver (backend/app/core/play_resolver.py)
- Cryptographic dice rolling
- Result chart lookup (league-specific)
- Play outcome determination
- Runner advancement logic
- Score calculation
4. Dice System (backend/app/core/dice.py)
- Secure random number generation
- Roll logging and verification
- Distribution testing
5. Polymorphic Player Models (backend/app/models/player_models.py)
- BasePlayer abstract class
- SbaPlayer implementation
- PdPlayer implementation
- Lineup factory method
- Type guards for league-specific logic
6. League Configuration (backend/app/config/)
- BaseGameConfig class
- SbaConfig and PdConfig subclasses
- Result charts (d20 tables) for each league
- Config loader and versioning
- Config validation
7. Database Operations (backend/app/database/operations.py)
- Async play persistence
- Game metadata updates
- Lineup operations
- State snapshot management
- Bulk recovery queries
8. API Client (backend/app/data/api_client.py)
- HTTP client for league REST APIs
- Team data fetching
- Roster data fetching
- Player/card data fetching
- Error handling and retries
- Response caching (optional)
Implementation Order (Actual Status)
-
Week 4: State Manager + Database Operations ✅ COMPLETE
- ✅ In-memory state structure (GameState Pydantic models)
- ✅ Basic CRUD operations (StateManager with dictionary storage)
- ✅ Database persistence layer (DatabaseOperations async methods)
- ✅ State recovery mechanism (Implemented and tested)
-
Week 5: Game Engine + Play Resolver ✅ COMPLETE
- ✅ Game initialization flow (start_game with lineup validation)
- ✅ Turn management (Forward-looking snapshot pattern, refactored 2025-10-25)
- ✅ Dice rolling system (Enhanced AbRoll with batch persistence)
- ✅ Basic play resolution (SimplifiedResultChart with wild pitch/passed ball)
-
Week 6: League Configs + Player Models 🔲 PENDING
- ✅ Polymorphic player architecture (Done differently: Lineup & RosterLink polymorphic)
- 🔲 League configuration system (Pending)
- 🔲 Complete result charts (Using simplified charts for now)
- 🔲 API client integration (Pending)
- 🟡 End-to-end testing (Manual test script works, formal tests missing)
Testing Strategy
Unit Tests
- State manager operations
- Dice roll distribution
- Play resolver outcomes
- Player model instantiation
- Config loading
Integration Tests
- Full game flow from start to end
- State recovery from database
- Multi-turn sequences
- API client with mocked responses
E2E Tests
- Play a complete 9-inning game
- Verify database persistence
- Test state recovery mid-game
Key Files (Actual Implementation)
backend/app/
├── core/
│ ├── game_engine.py # ✅ Main game logic with forward-looking snapshots
│ ├── state_manager.py # ✅ In-memory state dictionary
│ ├── play_resolver.py # ✅ Play outcomes with SimplifiedResultChart
│ ├── dice.py # ✅ Advanced dice system with batch persistence
│ ├── roll_types.py # ✅ BONUS: AbRoll, CheckRoll, ResolutionRoll dataclasses
│ └── validators.py # ✅ Rule validation with lineup position checks
├── config/ # 🔲 NOT YET CREATED (Week 6)
│ ├── base_config.py # 🔲 Base configuration
│ ├── league_configs.py # 🔲 SBA/PD configs
│ ├── result_charts.py # 🔲 d20 tables
│ └── loader.py # 🔲 Config utilities
├── models/
│ ├── db_models.py # ✅ SQLAlchemy ORM models (polymorphic Lineup & RosterLink)
│ └── game_models.py # ✅ Pydantic game state models
├── database/
│ └── operations.py # ✅ DatabaseOperations with async methods
└── data/ # 🔲 NOT YET CREATED (Week 6)
└── api_client.py # 🔲 League API client
scripts/
└── test_game_flow.py # ✅ BONUS: Manual test script (5 test scenarios)
tests/
├── unit/
│ ├── models/
│ │ └── test_game_models.py # ✅ 60 tests
│ └── core/
│ ├── test_dice.py # ✅ Distribution tests
│ ├── test_roll_types.py # ✅ Roll type tests
│ ├── test_state_manager.py # ✅ 26 tests
│ ├── test_play_resolver.py # ❌ MISSING
│ └── test_validators.py # ❌ MISSING
└── integration/
├── database/
│ └── test_operations.py # ✅ 21 tests
├── test_state_persistence.py # ✅ 8 tests
└── test_game_engine.py # ❌ MISSING
Actual Implementation Patterns
Forward-Looking Snapshot Pattern (Refactored 2025-10-25)
The GameEngine uses a sophisticated snapshot-before-execution pattern:
Problem Solved
Original approach had database lookbacks during play saving, causing:
- Multiple redundant database queries per play
- Inconsistent snapshots if lineup changes during play
- Difficult to reason about "state at time of play"
Solution: _prepare_next_play()
Before each play execution, we prepare a complete snapshot:
async def _prepare_next_play(self, state: GameState) -> None:
"""Prepare snapshot for the next play."""
# 1. Determine and advance batting order index
if state.half == "top":
current_idx = state.away_team_batter_idx
state.away_team_batter_idx = (current_idx + 1) % 9
batting_team = state.away_team_id
fielding_team = state.home_team_id
else:
current_idx = state.home_team_batter_idx
state.home_team_batter_idx = (current_idx + 1) % 9
batting_team = state.home_team_id
fielding_team = state.away_team_id
# 2. Fetch lineups from database
batting_lineup = await self.db_ops.get_active_lineup(state.game_id, batting_team)
fielding_lineup = await self.db_ops.get_active_lineup(state.game_id, fielding_team)
# 3. Set snapshot fields
state.current_batter_lineup_id = batting_order[current_idx].id
state.current_pitcher_lineup_id = pitcher.id
state.current_catcher_lineup_id = catcher.id
# 4. Calculate on_base_code (bit field: 1=1st, 2=2nd, 4=3rd)
state.current_on_base_code = 0
for runner in state.runners:
if runner.on_base == 1: state.current_on_base_code |= 1
elif runner.on_base == 2: state.current_on_base_code |= 2
elif runner.on_base == 3: state.current_on_base_code |= 4
Benefits
- Single Truth: Snapshot captured once, used throughout play
- No Lookbacks:
_save_play_to_db()just reads snapshot fields - Consistent: State cannot change between snapshot and save
- Independent Batting Orders: Each team tracks their own
batter_idx - Bit Field Optimization:
on_base_codeenables efficient database queries
Play Execution Sequence
The resolve_play() method follows explicit orchestration:
async def resolve_play(self, game_id: UUID) -> PlayResult:
# STEP 1: Resolve play (dice roll + outcome determination)
result = play_resolver.resolve_play(state, defensive_decision, offensive_decision)
# STEP 2: Save play to DB (uses snapshot from GameState)
await self._save_play_to_db(state, result)
# STEP 3: Apply result to state (outs, score, runners)
self._apply_play_result(state, result)
# STEP 4: Update game state in DB
await self.db_ops.update_game_state(...)
# STEP 5: Check for inning change
if state.outs >= 3:
await self._advance_inning(state, game_id)
await self.db_ops.update_game_state(...) # Update again after inning change
await self._batch_save_inning_rolls(game_id)
# STEP 6: Prepare next play (always last step)
if state.status == "active":
await self._prepare_next_play(state)
Advanced Dice System (AbRoll)
Instead of simple d20 rolls, we use a structured roll system:
Roll Types (roll_types.py):
CheckRoll: Initial d20 check (1=wild pitch, 2=passed ball, 3-20=normal)ResolutionRoll: Secondary d20 for confirming special events or determining specificsAbRoll: Complete at-bat roll combining check + resolution + context
Context Tracking:
@dataclass
class AbRoll:
check_roll: CheckRoll
resolution_roll: Optional[ResolutionRoll]
game_id: UUID
inning: int
half: str
play_number: int
# ... full audit trail
Batch Persistence:
- Rolls accumulated in memory during half-inning
_batch_save_inning_rolls()saves all at inning boundary- Reduces database writes from N (per play) to 1 (per half-inning)
Lineup Validation Strategy
At Game Start (Strict):
- Both teams' lineups validated (minimum 9 players)
- Both teams' defensive positions validated
- Exception: This is the only time we validate BOTH teams
At Inning Change (Defensive Only):
- Only defensive team's positions validated
- Allows offensive substitutions without validation delay
- Rationale: Offensive lineup can be incomplete mid-game (pinch hitter scenarios)
Validation Rules:
- Must have exactly one: P, C, 1B, 2B, 3B, SS, LF, CF, RF
- DH is optional (not validated as required)
Reference Documents
- Backend Architecture - Complete backend structure
- Database Design - Schema and queries
- WebSocket Protocol - Event specifications
- PRD Lines 378-551 - Polymorphic player architecture
- PRD Lines 780-846 - League configuration system
Deliverable
A working game backend that can:
- Initialize a game with teams from league APIs
- Process a complete at-bat (decisions → dice roll → outcome)
- Update game state in memory and persist to database
- Recover game state after backend restart
- Handle basic substitutions
Notes
- Focus on getting one at-bat working perfectly before expanding
- Test dice roll distribution extensively
- Validate all state transitions
- Use simplified result charts initially, expand in Phase 3
- Don't implement UI yet - test via WebSocket events or Python scripts
Implementation Approach
Key Decisions (2025-10-22)
-
Development Order: SBA First → PD Second
- Build each component for SBA league first
- Learn lessons and apply to PD implementation
- Ensures simpler case works before tackling complexity
-
Testing Strategy: Automated Python Tests
- Unit tests for each component
- Integration tests for full workflows
- No WebSocket UI testing in Phase 2
- Python scripts to simulate game flows
-
Result Selection Models:
- SBA: Players see dice roll, then select outcome from available results
- PD: Flexible approach
- Human players can manually select results
- AI/Auto mode uses scouting model to determine results automatically
- Initial implementation uses placeholder/simplified charts
-
Build Philosophy: One Perfect At-Bat
- Focus on completing a single defensive decision → dice roll → offensive decision → resolution flow
- Validate all state transitions work correctly
- Expand features in Phase 3
Detailed Weekly Plans
Detailed implementation instructions for each week:
- Week 4: State Management & Persistence
- Week 5: Game Logic & Play Resolution
- Week 6: League Features & Integration
Status: ✅ Weeks 4-5 Complete (2025-10-25) | Week 6 Pending Last Updated: 2025-10-25 Completed:
- Week 4: State Management & Persistence (2025-10-22)
- Week 5: Game Logic with forward-looking snapshots (2025-10-24)
- Week 5 Testing: All unit and integration tests (2025-10-25)
Test Coverage:
- Unit tests: 54 passing (dice, roll types, play resolver, validators)
- Integration tests: 7 test classes (complete game flows with database)
- Manual test script: 5 comprehensive scenarios
Pending: Week 6 (League configs, API client) OR proceed to Phase 3 Next Milestone: Week 6 - League Features & Integration