strat-gameplay-webapp/.claude/implementation/02-game-engine.md
Cal Corum f3238c4e6d CLAUDE: Complete Week 5 testing and update documentation
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>
2025-10-25 22:57:23 -05:00

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)

  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:

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:

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:

@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

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:


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