**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>
13 KiB
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:
# 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:
- Inconsistent API: Runners are objects, but batter/pitcher/catcher are IDs
- Extra lookups needed: PlayResolver must look up lineup to get player details
- Coupling: PlayResolver depends on StateManager for player lookups
- 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:
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:
# 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:
# 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
LineupPlayerStateobjects - 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 ✅
# 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
LineupPlayerStatecurrently: ~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:
-
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
- Change fields:
-
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(...)
- Update
-
backend/app/core/play_resolver.py- Update any references to
state.current_batter_lineup_id - Change to:
state.current_batter.lineup_id
- Update any references to
-
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
-
Update all tests
tests/unit/models/test_game_models.pytests/unit/core/test_game_engine.pytests/integration/test_state_persistence.py
Acceptance Criteria:
- All player references in GameState are
LineupPlayerStateobjects - All tests passing
- No regressions in existing functionality
- Type checking passes
Test Command:
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:
-
backend/app/services/pd_api_client.py(NEW)fetch_position_rating(player_id, position)- Get rating from PD APIfetch_all_positions(player_id)- Get all positions for a player- Async HTTP client with proper error handling
-
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:
-
backend/app/models/player_models.py- Add
PositionRatingdataclass - Fields: position, range, error, arm, speed
- Add
-
backend/app/models/game_models.py- Add
position_rating: Optional[PositionRating]toLineupPlayerState
- Add
-
backend/app/core/game_engine.py- Add
_load_lineup_with_ratings()method - Call during lineup creation
- Handle PD vs SBA (only load for PD)
- Add
-
backend/app/config/settings.py- Add
PD_API_URLsetting - Add
PD_API_TOKENsetting (if needed)
- Add
Tests to Create:
-
tests/unit/services/test_pd_api_client.py- Mock HTTP responses
- Test successful fetch
- Test error handling
- Test API response parsing
-
tests/unit/services/test_position_rating_cache.py- Test cache hit/miss
- Test TTL expiration
- Test graceful degradation
-
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:
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:
-
backend/app/core/play_resolver.py- Update
_resolve_x_check()to use defender ratings - Remove placeholders:
# 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)
- Update
-
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:
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:
-
Unit Tests:
pytest tests/unit/ -vExpected: All tests passing
-
Integration Tests:
pytest tests/integration/ -vExpected: All tests passing
-
Type Checking:
mypy app/Expected: No new type errors
-
Manual Testing (Terminal Client):
python -m terminal_client > new_game > start_game > defensive > offensive > resolve # Should use actual ratings for X-Check > status -
Memory Usage:
- Check GameState size in memory
- Expected: < 5KB per game (negligible increase)
-
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:
-
Phase 3E-Prep issues:
- Revert game_models.py changes
- Keep using IDs instead of objects
- Phase 3E-Main can still proceed with lookups
-
Phase 3E-Main issues:
- Skip position rating loading
- Use default/placeholder ratings
- Complete Phase 3E-Prep first (still valuable)
-
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)