Critical fixes in game_engine.py: - Fix silent error swallowing in _batch_save_inning_rolls (re-raise) - Add per-game asyncio.Lock for race condition prevention - Add _cleanup_game_resources() for memory leak prevention - All 739 tests passing Documentation refactoring: - Created CODE_REVIEW_GAME_ENGINE.md documenting 24 identified issues - Trimmed backend/app/core/CLAUDE.md from 1371 to 143 lines - Trimmed frontend-sba/CLAUDE.md from 696 to 110 lines - Created focused subdirectory CLAUDE.md files: - frontend-sba/components/CLAUDE.md (105 lines) - frontend-sba/composables/CLAUDE.md (79 lines) - frontend-sba/store/CLAUDE.md (116 lines) - frontend-sba/types/CLAUDE.md (95 lines) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
143 lines
4.3 KiB
Markdown
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**: 739/739 passing | **Last Updated**: 2025-01-19
|