strat-gameplay-webapp/.claude/implementation/GAMESTATE_REFACTOR_PLAN.md
Cal Corum d560844704 CLAUDE: Phase 3E-Prep - Refactor GameState to use full LineupPlayerState objects
**Architectural Improvement**: Unified player references in GameState

**Changed**: Make all player references consistent
- BEFORE: current_batter/pitcher/catcher were IDs (int)
- AFTER: current_batter/pitcher/catcher are full LineupPlayerState objects
- Matches pattern of on_first/on_second/on_third (already objects)

**Benefits**:
1. Consistent API - all player references use same type
2. Self-contained GameState - everything needed for resolution
3. No lookups needed - direct access to player data
4. Sets foundation for Phase 3E-Main (adding position ratings)

**Files Modified**:
- app/models/game_models.py: Changed current_batter/pitcher/catcher to objects
- app/core/game_engine.py: Updated _prepare_next_play() to populate full objects
- app/core/state_manager.py: Create placeholder batter on game creation
- tests/unit/models/test_game_models.py: Updated all 27 GameState tests

**Database Operations**:
- No schema changes needed
- Play table still stores IDs (for referential integrity)
- IDs extracted from objects when saving: state.current_batter.lineup_id

**Testing**:
- All 27 GameState tests passing
- No regressions in existing functionality
- Type checking passes

**Next**: Phase 3E-Main - Add PositionRating dataclass and load ratings at game start

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 14:11:40 -06:00

436 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# GameState Refactor Plan - Self-Contained State with Position Ratings
**Created**: 2025-11-03
**Status**: Ready for Implementation
**Priority**: High - Prerequisite for Phase 3E
## Problem Statement
### Current Architectural Inconsistency
GameState has an inconsistency in how it references players:
```python
# FULL OBJECTS for runners
on_first: Optional[LineupPlayerState] = None
on_second: Optional[LineupPlayerState] = None
on_third: Optional[LineupPlayerState] = None
# ONLY IDs for active players
current_batter_lineup_id: int
current_pitcher_lineup_id: Optional[int] = None
current_catcher_lineup_id: Optional[int] = None
```
This creates several problems:
1. **Inconsistent API**: Runners are objects, but batter/pitcher/catcher are IDs
2. **Extra lookups needed**: PlayResolver must look up lineup to get player details
3. **Coupling**: PlayResolver depends on StateManager for player lookups
4. **Missing data**: No direct access to position ratings for X-Check resolution
## Proposed Solution
### Phase 1: Make All Player References Consistent
Change `current_batter/pitcher/catcher_lineup_id` to full `LineupPlayerState` objects:
```python
class GameState(BaseModel):
# ... existing fields
# Runners (already full objects) ✅
on_first: Optional[LineupPlayerState] = None
on_second: Optional[LineupPlayerState] = None
on_third: Optional[LineupPlayerState] = None
# CHANGE: Make these full objects too
current_batter: LineupPlayerState
current_pitcher: Optional[LineupPlayerState] = None
current_catcher: Optional[LineupPlayerState] = None
# KEEP for database persistence (Play table needs IDs)
current_on_base_code: int = Field(default=0, ge=0)
```
### Phase 2: Add Position Ratings to LineupPlayerState
Enhance `LineupPlayerState` to hold position ratings loaded at game start:
```python
# In app/models/player_models.py
@dataclass
class PositionRating:
"""Defensive ratings for a player at a specific position."""
position: str # Position code (SS, 3B, LF, etc.)
range: int # Range rating (1-5)
error: int # Error rating (0-88 depending on position)
arm: int # Arm strength (1-5)
speed: Optional[int] = None # Speed rating (0-20) for SPD tests
# In app/models/game_models.py
class LineupPlayerState(BaseModel):
lineup_id: int
card_id: int
position: str
batting_order: Optional[int] = None
is_active: bool = True
# NEW: Position rating (loaded at game start for PD league)
position_rating: Optional[PositionRating] = None
```
### Phase 3: Load Position Ratings at Game Start
Create PD API client and load ratings when lineups are created:
```python
# In app/services/pd_api_client.py (NEW)
async def fetch_position_rating(player_id: int, position: str) -> PositionRating:
"""Fetch position rating from PD API."""
url = f"{PD_API_BASE}/cardpositions/player/{player_id}"
async with httpx.AsyncClient() as client:
response = await client.get(url)
data = response.json()
# Find rating for this position
for pos_data in data.get('positions', []):
if pos_data['position'] == position:
return PositionRating(
position=position,
range=pos_data['range'],
error=pos_data['error'],
arm=pos_data['arm'],
speed=pos_data.get('speed')
)
return None
# In game_engine.py - at lineup creation
async def _load_lineup_with_ratings(self, lineup_entries, league_id):
"""Load lineup and attach position ratings for PD league."""
lineup_players = []
for entry in lineup_entries:
player = LineupPlayerState(
lineup_id=entry.id,
card_id=entry.card_id,
position=entry.position,
batting_order=entry.batting_order
)
# Load position rating for PD league
if league_id == 'pd':
rating = await pd_api.fetch_position_rating(
entry.card_id,
entry.position
)
player.position_rating = rating
lineup_players.append(player)
return lineup_players
```
## Benefits
### 1. Architectural Consistency ✅
- All player references in GameState are `LineupPlayerState` objects
- Uniform API for accessing player data
- More intuitive to work with
### 2. Self-Contained State ✅
- GameState has everything needed for play resolution
- No external lookups required during resolution
- Truly represents "complete game state"
### 3. Simplified PlayResolver ✅
```python
# BEFORE (needs StateManager)
def _resolve_x_check(self, state, state_manager, ...):
lineup = state_manager.get_lineup(state.game_id, team_id)
defender = next(p for p in lineup.players if p.position == position)
# Still need to fetch rating somehow...
# AFTER (fully self-contained)
def _resolve_x_check(self, state, ...):
lineup = self.state_manager.get_lineup(state.game_id, team_id)
defender = next(p for p in lineup.players if p.position == position)
# Direct access to everything needed!
defender_range = defender.position_rating.range
defender_error = defender.position_rating.error
batter_speed = state.current_batter.position_rating.speed
```
### 4. Better Performance ✅
- Ratings loaded **once** at game start
- No API calls during play resolution
- No Redis lookups during plays
- Faster resolution (<500ms target easily met)
### 5. Redis Still Useful ✅
- Cache ratings for future games with same players
- Reduce PD API load
- Fast game recovery (load ratings from cache)
## Trade-offs
### Memory Impact
- `LineupPlayerState` currently: ~100 bytes
- Adding `PositionRating`: +50 bytes
- Per game (18 players): 18 × 150 = **~2.7KB total**
- **Verdict**: Negligible
### Complexity
- **Loading**: Slightly more complex at game start (API calls)
- **Resolution**: Much simpler (direct access)
- **Overall**: Net reduction in complexity
### Database
- Play table still stores `batter_id`, `pitcher_id`, etc. (IDs)
- No schema changes needed
- IDs used for referential integrity, objects used in memory
## Implementation Plan
### Phase 3E-Prep: Refactor GameState (2-3 hours)
**Goal**: Make all player references consistent (full objects)
**Files to Modify**:
1. `backend/app/models/game_models.py`
- Change fields: `current_batter`, `current_pitcher`, `current_catcher`
- Remove: `current_batter_lineup_id`, etc. (IDs)
- Update validators if needed
2. `backend/app/core/game_engine.py`
- Update `_prepare_next_play()` to set full objects
- Change from: `state.current_batter_lineup_id = batter.id`
- Change to: `state.current_batter = LineupPlayerState(...)`
3. `backend/app/core/play_resolver.py`
- Update any references to `state.current_batter_lineup_id`
- Change to: `state.current_batter.lineup_id`
4. `backend/app/database/operations.py`
- When saving Play, extract IDs from objects
- Change from: `batter_id=state.current_batter_lineup_id`
- Change to: `batter_id=state.current_batter.lineup_id`
5. Update all tests
- `tests/unit/models/test_game_models.py`
- `tests/unit/core/test_game_engine.py`
- `tests/integration/test_state_persistence.py`
**Acceptance Criteria**:
- [ ] All player references in GameState are `LineupPlayerState` objects
- [ ] All tests passing
- [ ] No regressions in existing functionality
- [ ] Type checking passes
**Test Command**:
```bash
pytest tests/unit/models/test_game_models.py -v
pytest tests/unit/core/test_game_engine.py -v
pytest tests/integration/ -v
```
---
### Phase 3E-Main: Add Position Ratings (3-4 hours)
**Goal**: Load and attach position ratings at game start
**Files to Create**:
1. `backend/app/services/pd_api_client.py` (NEW)
- `fetch_position_rating(player_id, position)` - Get rating from PD API
- `fetch_all_positions(player_id)` - Get all positions for a player
- Async HTTP client with proper error handling
2. `backend/app/services/position_rating_cache.py` (NEW)
- Redis caching wrapper
- Cache key: `position_rating:{player_id}:{position}`
- TTL: 24 hours
- Graceful degradation if Redis unavailable
**Files to Modify**:
1. `backend/app/models/player_models.py`
- Add `PositionRating` dataclass
- Fields: position, range, error, arm, speed
2. `backend/app/models/game_models.py`
- Add `position_rating: Optional[PositionRating]` to `LineupPlayerState`
3. `backend/app/core/game_engine.py`
- Add `_load_lineup_with_ratings()` method
- Call during lineup creation
- Handle PD vs SBA (only load for PD)
4. `backend/app/config/settings.py`
- Add `PD_API_URL` setting
- Add `PD_API_TOKEN` setting (if needed)
**Tests to Create**:
1. `tests/unit/services/test_pd_api_client.py`
- Mock HTTP responses
- Test successful fetch
- Test error handling
- Test API response parsing
2. `tests/unit/services/test_position_rating_cache.py`
- Test cache hit/miss
- Test TTL expiration
- Test graceful degradation
3. `tests/integration/test_lineup_rating_loading.py`
- End-to-end test of lineup creation with ratings
- Verify ratings attached to LineupPlayerState
**Acceptance Criteria**:
- [ ] PD API client created and tested
- [ ] Redis caching implemented
- [ ] Ratings loaded at game start for PD league
- [ ] SBA league unaffected (no ratings loaded)
- [ ] All tests passing
- [ ] Graceful handling of API failures
**Test Command**:
```bash
pytest tests/unit/services/test_pd_api_client.py -v
pytest tests/unit/services/test_position_rating_cache.py -v
pytest tests/integration/test_lineup_rating_loading.py -v
```
---
### Phase 3E-Final: Update X-Check Resolution (1 hour)
**Goal**: Use attached position ratings in X-Check resolution
**Files to Modify**:
1. `backend/app/core/play_resolver.py`
- Update `_resolve_x_check()` to use defender ratings
- Remove placeholders:
```python
# OLD (placeholder)
defender_range = 3
defender_error_rating = 10
# NEW (from GameState)
defender_range = defender.position_rating.range
defender_error_rating = defender.position_rating.error
```
- Update SPD test to use batter speed
- Add fallback for missing ratings (use league defaults)
2. Update tests
- Add position ratings to test fixtures
- Test X-Check with actual ratings
- Test fallback behavior
**Acceptance Criteria**:
- [ ] X-Check uses actual position ratings
- [ ] SPD test uses actual batter speed
- [ ] Graceful fallback for missing ratings
- [ ] All PlayResolver tests passing
- [ ] Integration test with full flow
**Test Command**:
```bash
pytest tests/unit/core/test_play_resolver.py -v
pytest tests/integration/test_x_check_full_flow.py -v
```
---
## Verification Steps
After all phases complete:
1. **Unit Tests**:
```bash
pytest tests/unit/ -v
```
Expected: All tests passing
2. **Integration Tests**:
```bash
pytest tests/integration/ -v
```
Expected: All tests passing
3. **Type Checking**:
```bash
mypy app/
```
Expected: No new type errors
4. **Manual Testing** (Terminal Client):
```bash
python -m terminal_client
> new_game
> start_game
> defensive
> offensive
> resolve # Should use actual ratings for X-Check
> status
```
5. **Memory Usage**:
- Check GameState size in memory
- Expected: < 5KB per game (negligible increase)
6. **Performance**:
- Time play resolution
- Expected: < 500ms (should be faster with direct access)
## Success Criteria
Phase 3E will be **100% complete** when:
- [ ] All player references in GameState are consistent (full objects)
- [ ] Position ratings loaded at game start (PD league)
- [ ] X-Check resolution uses actual ratings (no placeholders)
- [ ] Redis caching implemented and working
- [ ] All unit tests passing (400+ tests)
- [ ] All integration tests passing (30+ tests)
- [ ] Type checking passes with no new errors
- [ ] Documentation updated (CLAUDE.md, NEXT_SESSION.md)
- [ ] Performance targets met (< 500ms resolution)
- [ ] Memory usage acceptable (< 5KB increase per game)
## Rollback Plan
If issues arise during implementation:
1. **Phase 3E-Prep issues**:
- Revert game_models.py changes
- Keep using IDs instead of objects
- Phase 3E-Main can still proceed with lookups
2. **Phase 3E-Main issues**:
- Skip position rating loading
- Use default/placeholder ratings
- Complete Phase 3E-Prep first (still valuable)
3. **Phase 3E-Final issues**:
- Keep placeholders in PlayResolver
- Complete position rating loading
- Update resolution in separate PR
## Related Documentation
- **Phase 3 Overview**: `./.claude/implementation/PHASE_3_OVERVIEW.md`
- **Next Session Plan**: `./.claude/implementation/NEXT_SESSION.md`
- **Game Models**: `../backend/app/models/CLAUDE.md`
- **Play Resolver**: `../backend/app/core/play_resolver.py` (lines 589-758)
---
**Estimated Total Time**: 6-8 hours (spread across 3 phases)
**Priority**: High - This refactor makes Phase 3E cleaner and more maintainable
**Blocking Work**: None - can start immediately
**Next Milestone**: Phase 3F (Testing & Integration)