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>
371 lines
14 KiB
Markdown
371 lines
14 KiB
Markdown
# 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)
|
|
|
|
1. **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)
|
|
|
|
2. **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)
|
|
|
|
3. **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:
|
|
|
|
```python
|
|
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_code` enables efficient database queries
|
|
|
|
### Play Execution Sequence
|
|
|
|
The `resolve_play()` method follows explicit orchestration:
|
|
|
|
```python
|
|
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 specifics
|
|
- `AbRoll`: Complete at-bat roll combining check + resolution + context
|
|
|
|
**Context Tracking**:
|
|
```python
|
|
@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](./backend-architecture.md) - Complete backend structure
|
|
- [Database Design](./database-design.md) - Schema and queries
|
|
- [WebSocket Protocol](./websocket-protocol.md) - Event specifications
|
|
- [PRD Lines 378-551](../prd-web-scorecard-1.1.md) - Polymorphic player architecture
|
|
- [PRD Lines 780-846](../prd-web-scorecard-1.1.md) - 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)
|
|
|
|
1. **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
|
|
|
|
2. **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
|
|
|
|
3. **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
|
|
|
|
4. **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](./02-week4-state-management.md)
|
|
- [Week 5: Game Logic & Play Resolution](./02-week5-game-logic.md)
|
|
- [Week 6: League Features & Integration](./02-week6-league-features.md)
|
|
|
|
---
|
|
|
|
**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
|