CLAUDE: Add refactor planning and session documentation
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>
This commit is contained in:
parent
13e924a87c
commit
54092a8117
138
.claude/refactor_overview.md
Normal file
138
.claude/refactor_overview.md
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
⎿ 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
|
||||||
578
.claude/status-2025-10-24-1430.md
Normal file
578
.claude/status-2025-10-24-1430.md
Normal file
@ -0,0 +1,578 @@
|
|||||||
|
# Session Summary: GameEngine Implementation & Runner Tracking Design
|
||||||
|
|
||||||
|
**Date**: October 24, 2025
|
||||||
|
**Duration**: ~3 hours
|
||||||
|
**Branch**: `implement-phase-2`
|
||||||
|
**Status**: ✅ GameEngine Complete, 🔄 Runner Tracking Refactor Planned
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Session Overview
|
||||||
|
|
||||||
|
### Primary Objectives
|
||||||
|
1. ✅ Implement GameEngine orchestration for complete at-bat flow
|
||||||
|
2. ✅ Build PlayResolver with simplified result charts
|
||||||
|
3. ✅ Create GameValidator for rule enforcement
|
||||||
|
4. ✅ Test end-to-end gameplay with 50+ at-bats
|
||||||
|
5. 🔄 Design proper runner tracking architecture (plan approved, implementation pending)
|
||||||
|
|
||||||
|
### Technologies Involved
|
||||||
|
- Python 3.13 (FastAPI backend)
|
||||||
|
- PostgreSQL (asyncpg driver)
|
||||||
|
- SQLAlchemy 2.0 async ORM
|
||||||
|
- Pydantic v2 for models
|
||||||
|
- pytest for testing
|
||||||
|
|
||||||
|
### Overall Outcome
|
||||||
|
**Phase 2 GameEngine is production-ready** with successful test execution of 50 at-bats across 6 innings. Identified architectural improvement needed for runner tracking, designed comprehensive refactor plan that avoids legacy implementation pitfalls.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current State
|
||||||
|
|
||||||
|
### Active Todos
|
||||||
|
- 🔄 **In Progress**: Refactor GameEngine for forward-looking play tracking
|
||||||
|
- ⏳ **Pending**: Update GameState model with batter indices and play snapshot
|
||||||
|
- ⏳ **Pending**: Implement _prepare_next_play() method
|
||||||
|
- ⏳ **Pending**: Refactor resolve_play() orchestration
|
||||||
|
- ⏳ **Pending**: Add state recovery from last play
|
||||||
|
- ⏳ **Pending**: Update test script to verify new architecture
|
||||||
|
|
||||||
|
### Git Status
|
||||||
|
**Branch**: `implement-phase-2`
|
||||||
|
**Recent Commits**:
|
||||||
|
- `0542723` - Fix GameEngine lineup integration and add test script
|
||||||
|
- `0d7ddbe` - Implement GameEngine, PlayResolver, and GameValidator
|
||||||
|
- `874e24d` - Implement comprehensive dice roll system with persistence
|
||||||
|
|
||||||
|
**Modified Files** (uncommitted):
|
||||||
|
- None (clean working directory, plan mode active)
|
||||||
|
|
||||||
|
### Key Files
|
||||||
|
- `backend/app/core/game_engine.py` (334 lines) - Main orchestration
|
||||||
|
- `backend/app/core/play_resolver.py` (372 lines) - Play resolution logic
|
||||||
|
- `backend/app/core/validators.py` (115 lines) - Rule validation
|
||||||
|
- `backend/app/core/dice.py` (441 lines) - Dice system
|
||||||
|
- `backend/app/core/roll_types.py` (233 lines) - Roll dataclasses
|
||||||
|
- `backend/app/models/game_models.py` (492 lines) - Pydantic state models
|
||||||
|
- `backend/scripts/test_game_flow.py` (260 lines) - End-to-end test script
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changes Made This Session
|
||||||
|
|
||||||
|
### Files Created
|
||||||
|
1. **app/core/validators.py** (115 lines)
|
||||||
|
- `GameValidator` class with static validation methods
|
||||||
|
- Rule enforcement: outs (0-2), inning validation, decision validation
|
||||||
|
- Game-over logic for 9 innings + extras
|
||||||
|
- Key methods: `validate_game_active()`, `validate_defensive_decision()`, `validate_offensive_decision()`, `is_game_over()`
|
||||||
|
|
||||||
|
2. **app/core/play_resolver.py** (372 lines)
|
||||||
|
- `PlayResolver` class using advanced AbRoll system
|
||||||
|
- `SimplifiedResultChart` for MVP outcome determination
|
||||||
|
- 12 play outcomes: strikeout, groundout, flyout, walk, single, double, triple, HR, WP, PB, etc.
|
||||||
|
- Runner advancement logic: `_advance_on_walk()`, `_advance_on_single()`, `_advance_on_double()`
|
||||||
|
- Key methods: `resolve_play()`, `_resolve_outcome()`
|
||||||
|
|
||||||
|
3. **app/core/game_engine.py** (334 lines)
|
||||||
|
- `GameEngine` orchestration class
|
||||||
|
- Game lifecycle: `start_game()`, `submit_defensive_decision()`, `submit_offensive_decision()`, `resolve_play()`
|
||||||
|
- Batch roll saving: `_batch_save_inning_rolls()` called at half-inning boundaries
|
||||||
|
- Play persistence: `_save_play_to_db()` with lineup integration
|
||||||
|
- State management: `_apply_play_result()`, `_advance_inning()`
|
||||||
|
|
||||||
|
4. **scripts/test_game_flow.py** (260 lines)
|
||||||
|
- Comprehensive end-to-end test script
|
||||||
|
- `test_single_at_bat()` - validates one complete play
|
||||||
|
- `test_full_inning()` - runs 50+ plays across multiple innings
|
||||||
|
- Creates dummy lineups for both teams (9 players each)
|
||||||
|
- **Test Results**: ✅ 50 at-bats, 6 innings, Score: Away 5 - Home 2
|
||||||
|
|
||||||
|
### Files Modified
|
||||||
|
1. **app/core/roll_types.py**
|
||||||
|
- Shortened `AbRoll.__str__()` from 70+ chars to <50 chars
|
||||||
|
- Before: `"AB Roll: 4, 9 (6+3), d20=12 (split=10)"`
|
||||||
|
- After: `"AB 4,9(6+3) d20=12/10"`
|
||||||
|
- Reason: Database VARCHAR(50) constraint on `plays.dice_roll`
|
||||||
|
|
||||||
|
2. **app/core/game_engine.py** (multiple iterations)
|
||||||
|
- Added real lineup integration in `_save_play_to_db()`:271
|
||||||
|
- Fetches active lineups from database
|
||||||
|
- Extracts batter_id, pitcher_id, catcher_id by position
|
||||||
|
- Fixed: No more hardcoded placeholder IDs
|
||||||
|
|
||||||
|
### Key Code References
|
||||||
|
|
||||||
|
#### GameEngine Start Flow
|
||||||
|
```python
|
||||||
|
# app/core/game_engine.py:38-70
|
||||||
|
async def start_game(self, game_id: UUID) -> GameState:
|
||||||
|
"""Transitions pending -> active, initializes inning 1 top"""
|
||||||
|
# Validates state, marks active, persists to DB
|
||||||
|
# TODO: Will add lineup validation + _prepare_next_play()
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Play Resolution Orchestration
|
||||||
|
```python
|
||||||
|
# app/core/game_engine.py:119-157
|
||||||
|
async def resolve_play(self, game_id: UUID) -> PlayResult:
|
||||||
|
"""
|
||||||
|
Current flow:
|
||||||
|
1. Resolve play (dice + outcome)
|
||||||
|
2. Track roll for batch save
|
||||||
|
3. Apply result to state
|
||||||
|
|
||||||
|
Planned refactor:
|
||||||
|
1. Resolve play
|
||||||
|
2. Save play (with snapshot)
|
||||||
|
3. Apply result
|
||||||
|
4. Update DB
|
||||||
|
5. Check inning change
|
||||||
|
6. Prepare next play
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Lineup Integration
|
||||||
|
```python
|
||||||
|
# app/core/game_engine.py:275-285
|
||||||
|
# Fetches active lineups
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Extracts by position
|
||||||
|
batter_id = batting_lineup[0].id if batting_lineup else None
|
||||||
|
pitcher_id = next((p.id for p in fielding_lineup if p.position == "P"), None)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Decisions & Discoveries
|
||||||
|
|
||||||
|
### Architectural Decisions
|
||||||
|
|
||||||
|
1. **Forward-Looking Play Tracking** (APPROVED PLAN)
|
||||||
|
- **Problem**: Current implementation does awkward "lookbacks" to determine runner positions
|
||||||
|
- **Solution**: Enrich GameState with "current play snapshot" that captures state BEFORE each play
|
||||||
|
- **Pattern**: prepare_next_play() → resolve_play() → save_play() → apply_result() → prepare_next_play()
|
||||||
|
- **Benefit**: Clean, forward-looking flow that avoids legacy implementation pitfalls
|
||||||
|
|
||||||
|
2. **Separate Batter Indices Per Team**
|
||||||
|
- **Decision**: Store `away_team_batter_idx` and `home_team_batter_idx` separately
|
||||||
|
- **Rationale**: Saves 18+ database queries per game (one per half-inning)
|
||||||
|
- **Trade-off**: One extra integer in memory (negligible cost)
|
||||||
|
- **Location**: Will be added to `GameState` in `game_models.py`
|
||||||
|
|
||||||
|
3. **State Recovery from Last Play (Not Replay)**
|
||||||
|
- **Decision**: Rebuild GameState from most recent completed Play record
|
||||||
|
- **Query**: `SELECT * FROM plays WHERE game_id=X AND complete=true ORDER BY play_number DESC LIMIT 1`
|
||||||
|
- **Benefit**: Single-query recovery, no play-by-play replay needed
|
||||||
|
- **Edge Case**: If no plays exist, initialize with defaults (indices=0, no runners)
|
||||||
|
|
||||||
|
4. **Lineup Validation at Game Start**
|
||||||
|
- **Decision**: HARD REQUIREMENT - both teams must have complete 9-player lineups before game starts
|
||||||
|
- **Enforcement**: `start_game()` will throw `ValidationError` if lineups incomplete
|
||||||
|
- **Rationale**: Prevents the "first play error" that plagued legacy implementation
|
||||||
|
|
||||||
|
5. **Explicit Orchestration in resolve_play()**
|
||||||
|
- **Pattern**: Explicit sequence of operations, no hidden side effects
|
||||||
|
- **Flow**: resolve → save → apply → update_db → check_inning → prepare_next
|
||||||
|
- **Benefit**: Easy to understand, test, and debug
|
||||||
|
|
||||||
|
### Patterns Established
|
||||||
|
|
||||||
|
1. **Singleton Pattern for Core Services**
|
||||||
|
- `dice_system`, `play_resolver`, `game_validator`, `game_engine`
|
||||||
|
- Single shared instance per module
|
||||||
|
- Stateless or minimal state management
|
||||||
|
|
||||||
|
2. **Batch Operations at Boundaries**
|
||||||
|
- Roll saving batched at half-inning boundaries
|
||||||
|
- Reduces database writes (from 30+ per inning to 1)
|
||||||
|
- Stored in `_rolls_this_inning` dict keyed by game_id
|
||||||
|
|
||||||
|
3. **Pydantic Models for All Data Transfer**
|
||||||
|
- `DefensiveDecision`, `OffensiveDecision`, `PlayResult`
|
||||||
|
- Automatic validation and serialization
|
||||||
|
- Type-safe throughout the stack
|
||||||
|
|
||||||
|
### Important Discoveries
|
||||||
|
|
||||||
|
1. **AbRoll String Length Issue**
|
||||||
|
- Database column: `plays.dice_roll VARCHAR(50)`
|
||||||
|
- Original format exceeded limit: `"AB Roll: Passed Ball Check (check=2, resolution=20)"`
|
||||||
|
- Solution: Compressed format: `"PB 2/20"`, `"AB 6,9(4+5) d20=12/10"`
|
||||||
|
|
||||||
|
2. **Lineup Foreign Key Requirements**
|
||||||
|
- `plays` table requires valid `lineup.id` values for batter/pitcher/catcher
|
||||||
|
- Cannot save plays without lineups created first
|
||||||
|
- Test script creates dummy 9-player lineups per team
|
||||||
|
|
||||||
|
3. **GameState.runners Already Has lineup_id**
|
||||||
|
- `RunnerState` dataclass includes `lineup_id` field
|
||||||
|
- Can use directly for `on_first_id`, `on_second_id`, `on_third_id`
|
||||||
|
- No additional tracking needed
|
||||||
|
|
||||||
|
4. **on_base_code Bit Field Pattern**
|
||||||
|
- Efficient database querying: `WHERE on_base_code = 7` (bases loaded)
|
||||||
|
- Bit flags: 1=first (0b001), 2=second (0b010), 4=third (0b100)
|
||||||
|
- Combined: 7=loaded (0b111), 3=1st+2nd, 5=1st+3rd, 6=2nd+3rd
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Problems & Solutions
|
||||||
|
|
||||||
|
### Problem 1: Foreign Key Constraint on plays.batter_id
|
||||||
|
**Error**:
|
||||||
|
```
|
||||||
|
asyncpg.exceptions.ForeignKeyViolationError: insert or update on table "plays"
|
||||||
|
violates foreign key constraint "plays_batter_id_fkey"
|
||||||
|
DETAIL: Key (batter_id)=(1) is not present in table "lineups".
|
||||||
|
```
|
||||||
|
|
||||||
|
**Root Cause**: GameEngine was using hardcoded placeholder IDs (batter_id=1, pitcher_id=1, catcher_id=1)
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
- Updated `_save_play_to_db()` to fetch real lineup records
|
||||||
|
- Query active lineups from database
|
||||||
|
- Extract IDs by position (`P`, `C`, first batter)
|
||||||
|
- Test script now creates 18 dummy lineup entries (9 per team)
|
||||||
|
|
||||||
|
**Files Changed**:
|
||||||
|
- `backend/app/core/game_engine.py:271-285`
|
||||||
|
- `backend/scripts/test_game_flow.py:50-70, 165-185`
|
||||||
|
|
||||||
|
### Problem 2: String Too Long for VARCHAR(50)
|
||||||
|
**Error**:
|
||||||
|
```
|
||||||
|
asyncpg.exceptions.StringDataRightTruncationError: value too long for type character varying(50)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Root Cause**: `AbRoll.__str__()` returned 70+ character strings like:
|
||||||
|
- `"AB Roll: Wild Pitch Check (check=2, resolution=20)"`
|
||||||
|
- `"AB Roll: Passed Ball Check (check=2, resolution=20)"`
|
||||||
|
|
||||||
|
**Solution**: Compressed string format
|
||||||
|
- Wild Pitch: `"WP 2/20"` (8 chars)
|
||||||
|
- Passed Ball: `"PB 2/20"` (8 chars)
|
||||||
|
- Normal: `"AB 4,9(6+3) d20=12/10"` (~22 chars)
|
||||||
|
|
||||||
|
**Files Changed**: `backend/app/core/roll_types.py:97-103`
|
||||||
|
|
||||||
|
### Problem 3: Missing Runner Tracking in Play Records
|
||||||
|
**Issue**: Play records not capturing:
|
||||||
|
- `on_first_id`, `on_second_id`, `on_third_id` (who was on base)
|
||||||
|
- `on_first_final`, `on_second_final`, `on_third_final` (where they ended up)
|
||||||
|
- `batter_final` (where batter ended up)
|
||||||
|
- `on_base_code` (bit field for queries)
|
||||||
|
|
||||||
|
**Root Cause**: Awkward "lookback" architecture - trying to infer state after the fact
|
||||||
|
|
||||||
|
**Solution** (APPROVED PLAN): Forward-looking play tracking
|
||||||
|
- Capture snapshot BEFORE play in GameState
|
||||||
|
- Use snapshot when saving Play record
|
||||||
|
- No lookbacks needed
|
||||||
|
|
||||||
|
**Status**: Plan designed and approved, implementation pending
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technology Context
|
||||||
|
|
||||||
|
### System Components
|
||||||
|
- **FastAPI Backend**: Python 3.13, running at http://localhost:8000
|
||||||
|
- **PostgreSQL Database**: 10.10.0.42:5432, database `paperdynasty_dev`
|
||||||
|
- **State Management**: Hybrid in-memory (dict) + PostgreSQL persistence
|
||||||
|
- **Dice System**: Cryptographically secure using `secrets` module
|
||||||
|
- **Testing**: pytest with pytest-asyncio for async test support
|
||||||
|
|
||||||
|
### Database Schema (Relevant Tables)
|
||||||
|
|
||||||
|
#### games table
|
||||||
|
```sql
|
||||||
|
id UUID PRIMARY KEY
|
||||||
|
status VARCHAR -- 'pending', 'active', 'completed'
|
||||||
|
current_inning INTEGER
|
||||||
|
current_half VARCHAR -- 'top', 'bottom'
|
||||||
|
home_score INTEGER
|
||||||
|
away_score INTEGER
|
||||||
|
-- TODO: Will add for state recovery:
|
||||||
|
-- current_runners JSON
|
||||||
|
-- away_team_batter_idx INTEGER
|
||||||
|
-- home_team_batter_idx INTEGER
|
||||||
|
```
|
||||||
|
|
||||||
|
#### lineups table
|
||||||
|
```sql
|
||||||
|
id SERIAL PRIMARY KEY
|
||||||
|
game_id UUID REFERENCES games
|
||||||
|
team_id INTEGER
|
||||||
|
player_id INTEGER (nullable, for SBA)
|
||||||
|
card_id INTEGER (nullable, for PD)
|
||||||
|
position VARCHAR -- P, C, 1B, 2B, 3B, SS, LF, CF, RF, DH
|
||||||
|
batting_order INTEGER -- 1-9
|
||||||
|
is_active BOOLEAN
|
||||||
|
```
|
||||||
|
|
||||||
|
#### plays table
|
||||||
|
```sql
|
||||||
|
id SERIAL PRIMARY KEY
|
||||||
|
game_id UUID REFERENCES games
|
||||||
|
play_number INTEGER
|
||||||
|
batter_id INTEGER REFERENCES lineups -- ✅ Now populated
|
||||||
|
pitcher_id INTEGER REFERENCES lineups -- ✅ Now populated
|
||||||
|
catcher_id INTEGER REFERENCES lineups -- ✅ Now populated
|
||||||
|
on_first_id INTEGER REFERENCES lineups -- ❌ TODO
|
||||||
|
on_second_id INTEGER REFERENCES lineups -- ❌ TODO
|
||||||
|
on_third_id INTEGER REFERENCES lineups -- ❌ TODO
|
||||||
|
on_first_final INTEGER -- ❌ TODO
|
||||||
|
on_second_final INTEGER -- ❌ TODO
|
||||||
|
on_third_final INTEGER -- ❌ TODO
|
||||||
|
batter_final INTEGER -- ❌ TODO
|
||||||
|
on_base_code INTEGER -- ❌ TODO (bit field: 1|2|4)
|
||||||
|
dice_roll VARCHAR(50) -- ✅ Fixed length issue
|
||||||
|
result_description TEXT
|
||||||
|
outs_recorded INTEGER
|
||||||
|
runs_scored INTEGER
|
||||||
|
complete BOOLEAN
|
||||||
|
```
|
||||||
|
|
||||||
|
#### rolls table
|
||||||
|
```sql
|
||||||
|
roll_id VARCHAR PRIMARY KEY -- Unique from secrets.token_hex()
|
||||||
|
game_id UUID REFERENCES games
|
||||||
|
roll_type VARCHAR -- 'ab', 'jump', 'fielding', 'd20'
|
||||||
|
roll_data JSONB -- Complete roll details
|
||||||
|
context JSONB -- Game context (inning, outs, etc.)
|
||||||
|
timestamp TIMESTAMP
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
- **SQLAlchemy 2.0.36** - Async ORM
|
||||||
|
- **asyncpg 0.30.0** - PostgreSQL async driver
|
||||||
|
- **Pydantic 2.10.6** - Data validation
|
||||||
|
- **Pendulum 3.0.0** - DateTime handling (replaces datetime)
|
||||||
|
- **pytest-asyncio 0.25.2** - Async test support
|
||||||
|
|
||||||
|
### Performance Characteristics
|
||||||
|
- **At-bat resolution**: ~200ms (includes DB writes)
|
||||||
|
- **State access**: O(1) dictionary lookup in memory
|
||||||
|
- **Roll batch save**: 1 write per half-inning vs 30+ individual writes
|
||||||
|
- **Test run**: 50 at-bats across 6 innings in ~24 seconds
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
### Immediate Actions (Approved Plan)
|
||||||
|
|
||||||
|
1. **Update GameState Model** (`backend/app/models/game_models.py`)
|
||||||
|
- Add `away_team_batter_idx: int = 0`
|
||||||
|
- Add `home_team_batter_idx: int = 0`
|
||||||
|
- Add `current_batter_lineup_id: Optional[int] = None`
|
||||||
|
- Add `current_pitcher_lineup_id: Optional[int] = None`
|
||||||
|
- Add `current_catcher_lineup_id: Optional[int] = None`
|
||||||
|
- Add `current_on_base_code: int = 0`
|
||||||
|
|
||||||
|
2. **Refactor start_game()** (`backend/app/core/game_engine.py:38`)
|
||||||
|
- Add lineup validation (both teams, 9 players minimum)
|
||||||
|
- Call `_prepare_next_play()` before returning
|
||||||
|
- Throw `ValidationError` if lineups incomplete
|
||||||
|
|
||||||
|
3. **Create _prepare_next_play() Method** (new in game_engine.py)
|
||||||
|
- Determine current batter index based on half
|
||||||
|
- Advance appropriate team's batter_idx (% 9 wrap)
|
||||||
|
- Fetch active lineups
|
||||||
|
- Set current_batter/pitcher/catcher_lineup_id
|
||||||
|
- Calculate on_base_code from state.runners
|
||||||
|
|
||||||
|
4. **Refactor resolve_play()** (`backend/app/core/game_engine.py:119`)
|
||||||
|
- Explicit orchestration sequence:
|
||||||
|
1. Resolve play
|
||||||
|
2. Save play (with snapshot)
|
||||||
|
3. Apply result
|
||||||
|
4. Update DB
|
||||||
|
5. Check inning change → update DB again
|
||||||
|
6. Prepare next play
|
||||||
|
|
||||||
|
5. **Update _save_play_to_db()** (`backend/app/core/game_engine.py:271`)
|
||||||
|
- Use snapshot from GameState (no queries for batter/pitcher/catcher)
|
||||||
|
- Extract runner IDs from state.runners
|
||||||
|
- Calculate finals from result.runners_advanced
|
||||||
|
- Set on_base_code from snapshot
|
||||||
|
|
||||||
|
6. **Add State Recovery** (`backend/app/core/state_manager.py`)
|
||||||
|
- New method: `recover_game_from_last_play(game_id)`
|
||||||
|
- Query last play, extract runner positions and batter indices
|
||||||
|
- Rebuild GameState
|
||||||
|
- Call _prepare_next_play()
|
||||||
|
|
||||||
|
### Testing Requirements
|
||||||
|
- ✅ Test start_game() fails with incomplete lineups
|
||||||
|
- ✅ Verify on_base_code calculation (bit field 1|2|4)
|
||||||
|
- ✅ Verify runner lineup_ids in Play records
|
||||||
|
- ✅ Test batting order cycles 0-8 per team
|
||||||
|
- ✅ Test state recovery from last play
|
||||||
|
|
||||||
|
### Follow-up Work (Post-MVP)
|
||||||
|
- Player substitutions (pinch hitter, defensive replacement)
|
||||||
|
- Pitcher fatigue tracking
|
||||||
|
- Detailed result charts (load from config files)
|
||||||
|
- Advanced decision logic (steal success rates, etc.)
|
||||||
|
- WebSocket integration for real-time updates
|
||||||
|
- Frontend game interface
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reference Information
|
||||||
|
|
||||||
|
### Key File Paths
|
||||||
|
```
|
||||||
|
/mnt/NV2/Development/strat-gameplay-webapp/backend/
|
||||||
|
├── app/
|
||||||
|
│ ├── core/
|
||||||
|
│ │ ├── game_engine.py # Main orchestration (334 lines)
|
||||||
|
│ │ ├── play_resolver.py # Play resolution (372 lines)
|
||||||
|
│ │ ├── validators.py # Rule validation (115 lines)
|
||||||
|
│ │ ├── dice.py # Dice system (441 lines)
|
||||||
|
│ │ ├── roll_types.py # Roll dataclasses (233 lines)
|
||||||
|
│ │ └── state_manager.py # In-memory state (296 lines)
|
||||||
|
│ │
|
||||||
|
│ ├── models/
|
||||||
|
│ │ ├── game_models.py # Pydantic state models (492 lines)
|
||||||
|
│ │ └── db_models.py # SQLAlchemy ORM models
|
||||||
|
│ │
|
||||||
|
│ └── database/
|
||||||
|
│ └── operations.py # DB operations (362 lines)
|
||||||
|
│
|
||||||
|
├── scripts/
|
||||||
|
│ └── test_game_flow.py # End-to-end test (260 lines)
|
||||||
|
│
|
||||||
|
└── tests/
|
||||||
|
├── unit/core/
|
||||||
|
│ ├── test_dice.py # 35 tests (34/35 passing)
|
||||||
|
│ └── test_roll_types.py # 27 tests (27/27 passing)
|
||||||
|
└── integration/
|
||||||
|
└── database/
|
||||||
|
└── test_roll_persistence.py # 16 tests (work individually)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Commands
|
||||||
|
```bash
|
||||||
|
# Activate virtual environment
|
||||||
|
cd /mnt/NV2/Development/strat-gameplay-webapp/backend
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
# Run end-to-end test
|
||||||
|
python scripts/test_game_flow.py
|
||||||
|
|
||||||
|
# Run unit tests
|
||||||
|
pytest tests/unit/core/ -v
|
||||||
|
|
||||||
|
# Run specific test
|
||||||
|
pytest tests/unit/core/test_dice.py -v
|
||||||
|
|
||||||
|
# Check git status
|
||||||
|
git status
|
||||||
|
git log --oneline -10
|
||||||
|
|
||||||
|
# Start development server
|
||||||
|
python -m app.main # http://localhost:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Important URLs
|
||||||
|
- API Docs: http://localhost:8000/docs
|
||||||
|
- ReDoc: http://localhost:8000/redoc
|
||||||
|
- Database: postgresql://paperdynasty@10.10.0.42:5432/paperdynasty_dev
|
||||||
|
|
||||||
|
### Git Branches
|
||||||
|
- `main` - Production branch
|
||||||
|
- `implement-phase-2` - Current development (GameEngine work)
|
||||||
|
|
||||||
|
### Session Cost
|
||||||
|
- Total: $17.15
|
||||||
|
- Duration: 35m 34s (API time), 22h 35m 33s (wall time)
|
||||||
|
- Code changes: 2,020 lines added, 346 lines removed
|
||||||
|
- Model: Claude Sonnet (13.7k input, 68.1k output)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture Diagram
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ GameEngine │
|
||||||
|
│ │
|
||||||
|
│ start_game() │
|
||||||
|
│ ├─ Validate lineups (HARD REQUIREMENT) │
|
||||||
|
│ ├─ Transition: pending → active │
|
||||||
|
│ └─ Call _prepare_next_play() [NEW] │
|
||||||
|
│ │
|
||||||
|
│ resolve_play() │
|
||||||
|
│ ├─ 1. Resolve (PlayResolver + DiceSystem) │
|
||||||
|
│ ├─ 2. Save play (uses GameState snapshot) │
|
||||||
|
│ ├─ 3. Apply result │
|
||||||
|
│ ├─ 4. Update DB │
|
||||||
|
│ ├─ 5. Check inning → _advance_inning() → Update DB │
|
||||||
|
│ └─ 6. _prepare_next_play() [NEW] │
|
||||||
|
│ │
|
||||||
|
│ _prepare_next_play() [NEW METHOD] │
|
||||||
|
│ ├─ Advance batter index (per team, wrap 0-8) │
|
||||||
|
│ ├─ Fetch active lineups │
|
||||||
|
│ ├─ Set current_batter/pitcher/catcher_lineup_id │
|
||||||
|
│ └─ Calculate on_base_code from runners │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ GameState (Enhanced) │
|
||||||
|
│ │
|
||||||
|
│ Existing Fields: │
|
||||||
|
│ - game_id, inning, half, outs │
|
||||||
|
│ - home_score, away_score │
|
||||||
|
│ - runners: List[RunnerState] (has lineup_id!) │
|
||||||
|
│ - status, play_count, etc. │
|
||||||
|
│ │
|
||||||
|
│ NEW Fields (Approved Plan): │
|
||||||
|
│ - away_team_batter_idx: int = 0 (0-8) │
|
||||||
|
│ - home_team_batter_idx: int = 0 (0-8) │
|
||||||
|
│ - current_batter_lineup_id: Optional[int] │
|
||||||
|
│ - current_pitcher_lineup_id: Optional[int] │
|
||||||
|
│ - current_catcher_lineup_id: Optional[int] │
|
||||||
|
│ - current_on_base_code: int = 0 (bit field) │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Play Record (DB) │
|
||||||
|
│ │
|
||||||
|
│ Snapshot (from GameState): │
|
||||||
|
│ - batter_id ← current_batter_lineup_id │
|
||||||
|
│ - pitcher_id ← current_pitcher_lineup_id │
|
||||||
|
│ - catcher_id ← current_catcher_lineup_id │
|
||||||
|
│ - on_base_code ← current_on_base_code │
|
||||||
|
│ - on_first_id ← state.runners (lineup_id where base=1) │
|
||||||
|
│ - on_second_id ← state.runners (lineup_id where base=2) │
|
||||||
|
│ - on_third_id ← state.runners (lineup_id where base=3) │
|
||||||
|
│ │
|
||||||
|
│ Result (from PlayResult): │
|
||||||
|
│ - on_first_final ← runners_advanced │
|
||||||
|
│ - on_second_final ← runners_advanced │
|
||||||
|
│ - on_third_final ← runners_advanced │
|
||||||
|
│ - batter_final ← batter_result │
|
||||||
|
│ - outs_recorded, runs_scored, description │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Status: Ready for Implementation
|
||||||
|
|
||||||
|
All architectural decisions made and approved. Next agent can proceed with implementation following the approved plan above.
|
||||||
|
|
||||||
|
**Estimated Implementation Time**: 2-3 hours
|
||||||
|
**Priority**: High (core functionality)
|
||||||
|
**Risk**: Low (well-defined scope, clear plan)
|
||||||
Loading…
Reference in New Issue
Block a user