strat-gameplay-webapp/backend/app/core/CLAUDE.md
Cal Corum 529c5b1b99 CLAUDE: Implement uncapped hit interactive decision tree (Issue #6)
Add full multi-step decision workflow for SINGLE_UNCAPPED and DOUBLE_UNCAPPED
outcomes, replacing the previous stub that fell through to basic single/double
advancement. The decision tree follows the same interactive pattern as X-Check
resolution with 5 phases: lead runner advance, defensive throw, trail runner
advance, throw target selection, and safe/out speed check.

- game_models.py: PendingUncappedHit model, 5 new decision phases
- game_engine.py: initiate_uncapped_hit(), 5 submit methods, 3 result builders
- handlers.py: 5 new WebSocket event handlers
- ai_opponent.py: 5 AI decision stubs (conservative defaults)
- play_resolver.py: Updated TODO comments for fallback paths
- 80 new backend tests (2481 total): workflow (49), handlers (23), truth tables (8)
- Fix GameplayPanel.spec.ts: add missing Pinia setup, fix component references

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 09:33:58 -06:00

143 lines
4.3 KiB
Markdown

# Core - Game Engine & Logic
## Purpose
The `core` directory contains the baseball simulation engine - game orchestration, dice rolling, play resolution, and state management for real-time performance (<500ms target).
## Architecture Overview
```
WebSocket → GameEngine → PlayResolver → RunnerAdvancement
↓ ↓
StateManager DiceSystem
Database
```
## Core Modules
| Module | Purpose | Singleton |
|--------|---------|-----------|
| `state_manager.py` | In-memory game state with O(1) lookups | Yes |
| `game_engine.py` | Main orchestrator, workflow coordination | Yes |
| `play_resolver.py` | Outcome resolution (manual/auto modes) | No |
| `runner_advancement.py` | Groundball/flyball runner logic | No |
| `dice.py` | Cryptographic dice rolling | Yes |
| `validators.py` | Baseball rule enforcement | Static |
| `ai_opponent.py` | AI decision generation | Yes |
## GameEngine Key Features
### Storage (per-game tracking)
```python
_rolls_this_inning: dict[UUID, List] # Batch saved at inning boundary
_game_locks: dict[UUID, asyncio.Lock] # Prevents concurrent decision race conditions
```
### Thread Safety
Decision submissions (`submit_defensive_decision`, `submit_offensive_decision`) use per-game locks:
```python
async with self._get_game_lock(game_id):
# decision logic
```
### Resource Cleanup
When games complete (natural or manual), `_cleanup_game_resources(game_id)` releases:
- `_rolls_this_inning[game_id]`
- `_game_locks[game_id]`
### Core Workflow (6 steps)
1. Resolve play with dice rolls
2. Save play to DB (uses snapshot)
3. Apply result to state
4. Update game state in DB (conditional)
5. Check for inning change, batch save rolls
6. Prepare next play OR cleanup if completed
## StateManager Storage
```python
_states: Dict[UUID, GameState] # O(1) state lookup
_lineups: Dict[UUID, Dict[int, TeamLineupState]] # Cached lineups
_last_access: Dict[UUID, pendulum.DateTime] # For idle eviction
_pending_decisions: Dict[tuple, asyncio.Future] # Async decision futures
```
## Common Patterns
### Two Resolution Modes
- **Manual** (primary): Player submits outcome from physical card
- **Auto** (rare, PD only): System generates from digitized ratings
### Lineup Caching
```python
# First access - fetch from DB
lineup_state = await lineup_service.load_team_lineup_with_player_data(...)
state_manager.set_lineup(game_id, team_id, lineup_state)
# Subsequent - cache hit (no DB query)
lineup_state = state_manager.get_lineup(game_id, team_id)
```
### Error Handling
- "Raise or Return" pattern - exceptions propagate, no silent failures
- `_batch_save_inning_rolls` re-raises on failure (audit data is critical)
## Integration Points
### With WebSocket Handlers
```python
from app.core.game_engine import game_engine
# Start game
state = await game_engine.start_game(game_id)
# Submit decisions
await game_engine.submit_defensive_decision(game_id, decision)
await game_engine.submit_offensive_decision(game_id, decision)
# Resolve play (manual mode)
result = await game_engine.resolve_manual_play(game_id, ab_roll, outcome, hit_location)
```
### With Models
```python
from app.models.game_models import GameState, DefensiveDecision, OffensiveDecision
from app.config import PlayOutcome
```
## Testing
```bash
# All core tests
uv run pytest tests/unit/core/ -v
# Terminal client (interactive testing)
uv run python -m terminal_client
```
## Performance
- State access: O(1) (~1μs)
- Play resolution: 50-100ms
- Query reduction: 60% vs naive implementation
## Troubleshooting
| Issue | Cause | Solution |
|-------|-------|----------|
| Game not found | Evicted or never created | `await state_manager.recover_game(game_id)` |
| Batter/pitcher None | `_prepare_next_play` not called | Ensure proper orchestration sequence |
| Lineup out of sync | DB changed but cache stale | `state_manager.set_lineup()` after changes |
## References
- **Code Review**: `../.claude/CODE_REVIEW_GAME_ENGINE.md` - Detailed issues and fixes
- **Terminal Client**: `../terminal_client/CLAUDE.md` - Interactive testing guide
- **Database Operations**: `../database/operations.py` - Persistence layer
- **Main CLAUDE**: `../../CLAUDE.md` - Backend overview
---
**Tests**: 2481/2481 passing | **Last Updated**: 2026-02-11