strat-gameplay-webapp/.claude/implementation/GAMESTATE_REFACTOR_PLAN.md
Cal Corum e147ab17f1 CLAUDE: Phase 3F - Substitution System WebSocket Events
Implemented complete WebSocket integration for real-time player substitutions.
System is now 80% complete - only tests remain.

## WebSocket Events Implemented (600 lines)

### Event Handlers (backend/app/websocket/handlers.py):
1. request_pinch_hitter - Pinch hitter substitution
   - Validates: game_id, player_out_lineup_id, player_in_card_id, team_id
   - Executes: SubstitutionManager.pinch_hit()
   - Broadcasts: player_substituted (all clients), substitution_confirmed (requester)
   - Error codes: MISSING_FIELD, INVALID_FORMAT, NOT_CURRENT_BATTER, etc.

2. request_defensive_replacement - Defensive replacement
   - Additional field: new_position (P, C, 1B, 2B, 3B, SS, LF, CF, RF)
   - Executes: SubstitutionManager.defensive_replace()
   - Same broadcast pattern as pinch hitter

3. request_pitching_change - Pitching change
   - Validates minimum batters faced (handled in SubstitutionManager)
   - Executes: SubstitutionManager.change_pitcher()
   - Broadcasts new pitcher to all clients

4. get_lineup - Get active lineup for team
   - Returns: lineup_data with all active players
   - Uses: StateManager cache (O(1)) or database fallback
   - Purpose: UI refresh after substitutions

### Event Pattern (follows existing handlers):
- Validate inputs (UUID format, required fields, game exists)
- Create SubstitutionManager instance with DatabaseOperations
- Execute substitution (validate → DB → state)
- Broadcast player_substituted to game room
- Send substitution_confirmed to requester
- Error handling with specific error codes

### Events Emitted:
- player_substituted (broadcast) - Includes: type, lineup IDs, position, batting_order
- substitution_confirmed (requester) - Success confirmation with new_lineup_id
- substitution_error (requester) - Validation error with error code
- lineup_data (requester) - Active lineup response
- error (requester) - Generic error

## Documentation Updates (350 lines)

### backend/app/websocket/CLAUDE.md:
- Complete handler documentation with examples
- Event data structures and response formats
- Error code reference (MISSING_FIELD, INVALID_FORMAT, NOT_CURRENT_BATTER, etc.)
- Client integration examples (JavaScript)
- Complete workflow diagrams
- Updated event summary table (+8 events)
- Updated Common Imports section

### .claude/implementation/ updates:
- NEXT_SESSION.md: Marked Task 1 complete, updated to 80% done
- SUBSTITUTION_SYSTEM_SUMMARY.md: Added WebSocket section, updated status
- GAMESTATE_REFACTOR_PLAN.md: Marked complete
- PHASE_3_OVERVIEW.md: Updated all phases to reflect completion
- phase-3e-COMPLETED.md: Created comprehensive completion doc

## Architecture

### DB-First Pattern (maintained):
```
Client Request → WebSocket Handler
    ↓
SubstitutionManager
    ├─ SubstitutionRules.validate_*()
    ├─ DatabaseOperations.create_substitution() (DB first!)
    ├─ StateManager.update_lineup_cache()
    └─ Update GameState if applicable
    ↓
Success Responses
    ├─ player_substituted (broadcast to room)
    └─ substitution_confirmed (to requester)
```

### Error Handling:
- Three-tier: ValidationError, GameValidationError, Exception
- Specific error codes for each failure type
- User-friendly error messages
- Comprehensive logging at each step

## Status Update

**Phase 3F Substitution System**: 80% Complete
-  Core logic (SubstitutionRules, SubstitutionManager) - 1,027 lines
-  Database operations (create_substitution, get_eligible_substitutes)
-  WebSocket events (4 handlers) - 600 lines
-  Documentation (350 lines)
-  Unit tests (20% remaining) - ~300 lines needed
-  Integration tests - ~400 lines needed

**Phase 3 Overall**: ~97% Complete
- Phase 3A-D (X-Check Core): 100%
- Phase 3E (GameState, Ratings, Redis, Testing): 100%
- Phase 3F (Substitutions): 80%

## Files Modified

backend/app/websocket/handlers.py (+600 lines)
backend/app/websocket/CLAUDE.md (+350 lines)
.claude/implementation/NEXT_SESSION.md (updated progress)
.claude/implementation/SUBSTITUTION_SYSTEM_SUMMARY.md (added WebSocket section)
.claude/implementation/GAMESTATE_REFACTOR_PLAN.md (marked complete)
.claude/implementation/PHASE_3_OVERVIEW.md (updated all phases)
.claude/implementation/phase-3e-COMPLETED.md (new file, 400+ lines)

## Next Steps

Remaining work (2-3 hours):
1. Unit tests for SubstitutionRules (~300 lines)
   - 15+ pinch hitter tests
   - 12+ defensive replacement tests
   - 10+ pitching change tests
2. Integration tests for SubstitutionManager (~400 lines)
   - Full DB + state sync flow
   - State recovery verification
   - Error handling and rollback

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 21:24:43 -06:00

464 lines
14 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
**Completed**: 2025-11-04
**Status**: ✅ COMPLETE - All phases implemented and tested
**Priority**: High - Prerequisite for Phase 3E (SATISFIED)
## 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**:
- [x] All player references in GameState are `LineupPlayerState` objects
- [x] All tests passing (609/609 unit tests)
- [x] No regressions in existing functionality
- [x] Type checking passes
**Completion Notes** (2025-11-04):
- Commits: cf7cc23, 76e0142, bb78de2, e6bd66e, c7b376d
- 7 files modified (game_models, game_engine, play_resolver, runner_advancement, state_manager, display, repl)
- 34 runner advancement tests passing
- DO3 bug fixed, game recovery working
- Documentation updated in CLAUDE.md files
**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**:
- [x] PD API client created and tested
- [x] Redis caching implemented (760x speedup achieved)
- [x] Ratings loaded at game start for PD league
- [x] SBA league unaffected (no ratings loaded)
- [x] All tests passing
- [x] Graceful handling of API failures
**Completion Notes** (2025-11-03):
- Commits: 02e816a, 7d15018, adf7c76
- Files created: pd_api_client.py, position_rating_service.py, redis_client.py
- Performance: 0.274s API 0.000361s Redis (760x speedup)
- Live Redis integration test validated
- WebSocket events enhanced with X-Check details
**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] X-Check uses actual position ratings
- [x] SPD test uses actual batter speed
- [x] Graceful fallback for missing ratings
- [x] All PlayResolver tests passing
- [x] Integration test with full flow (terminal client testing)
**Completion Notes** (2025-11-04):
- Commits: bb78de2, 8fb740f
- Terminal client integration with `resolve_with x-check <position>` command
- Complete X-Check resolution with defense tables and error charts
- All resolution steps shown with audit trail
- Works with actual player ratings from PD API
**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 ✅ ALL COMPLETE
Phase 3E is **100% complete** as of 2025-11-04:
- [x] All player references in GameState are consistent (full objects)
- [x] Position ratings loaded at game start (PD league)
- [x] X-Check resolution uses actual ratings (no placeholders)
- [x] Redis caching implemented and working (760x speedup)
- [x] All unit tests passing (609/609 tests)
- [x] All integration tests passing (with documented asyncpg issues)
- [x] Type checking passes with no new errors
- [x] Documentation updated (CLAUDE.md, NEXT_SESSION.md)
- [x] Performance targets met (< 500ms resolution)
- [x] Memory usage acceptable (< 5KB increase per game)
**Additional Achievements**:
- Terminal client X-Check testing (`resolve_with x-check <position>`)
- 100% test requirement policy with git hooks
- DO3 bug fixed, game recovery working
- 679 total tests in test suite
## 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)