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>
4.3 KiB
4.3 KiB
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)
_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:
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)
- Resolve play with dice rolls
- Save play to DB (uses snapshot)
- Apply result to state
- Update game state in DB (conditional)
- Check for inning change, batch save rolls
- Prepare next play OR cleanup if completed
StateManager Storage
_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
# 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_rollsre-raises on failure (audit data is critical)
Integration Points
With WebSocket Handlers
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
from app.models.game_models import GameState, DefensiveDecision, OffensiveDecision
from app.config import PlayOutcome
Testing
# 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