Add comprehensive project documentation and Docker infrastructure for Paper Dynasty Real-Time Game Engine - a web-based multiplayer baseball simulation platform replacing the legacy Google Sheets system. Documentation Added: - Complete PRD (Product Requirements Document) - Project README with dual development workflows - Implementation guide with 5-phase roadmap - Architecture docs (backend, frontend, database, WebSocket) - CLAUDE.md context files for each major directory Infrastructure Added: - Root docker-compose.yml for full stack orchestration - Dockerfiles for backend and both frontends (multi-stage builds) - .dockerignore files for optimal build context - .env.example with all required configuration - Updated .gitignore for Python, Node, Nuxt, and Docker Project Structure: - backend/ - FastAPI + Socket.io game engine (Python 3.11+) - frontend-sba/ - SBA League Nuxt 3 frontend - frontend-pd/ - PD League Nuxt 3 frontend - .claude/implementation/ - Detailed implementation guides Supports two development workflows: 1. Local dev (recommended): Services run natively with hot-reload 2. Full Docker: One-command stack orchestration for testing/demos Next: Phase 1 implementation (backend/frontend foundations)
15 KiB
15 KiB
Backend Architecture
Overview
FastAPI-based game backend serving as the central game engine for both SBA and PD leagues. Handles real-time WebSocket communication, game state management, and database persistence.
Directory Structure
backend/
├── app/
│ ├── main.py # FastAPI app initialization, CORS, middleware
│ ├── config.py # Environment variables, settings
│ │
│ ├── core/ # Core game logic
│ │ ├── __init__.py
│ │ ├── game_engine.py # Main game simulation engine
│ │ ├── state_manager.py # In-memory state management
│ │ ├── play_resolver.py # Play outcome resolution logic
│ │ ├── dice.py # Cryptographic random roll generation
│ │ └── validators.py # Game rule validation
│ │
│ ├── config/ # League configurations
│ │ ├── __init__.py
│ │ ├── base_config.py # Shared BaseGameConfig
│ │ ├── league_configs.py # SBA/PD specific configs
│ │ ├── result_charts.py # d20 outcome tables
│ │ └── loader.py # Config loading utilities
│ │
│ ├── models/ # Data models
│ │ ├── __init__.py
│ │ ├── game_models.py # Pydantic models for game state
│ │ ├── player_models.py # Polymorphic player models
│ │ ├── db_models.py # SQLAlchemy ORM models
│ │ └── api_schemas.py # Request/response schemas
│ │
│ ├── websocket/ # WebSocket handling
│ │ ├── __init__.py
│ │ ├── connection_manager.py # Connection lifecycle
│ │ ├── events.py # Event definitions
│ │ ├── handlers.py # Action handlers
│ │ └── rooms.py # Socket.io room management
│ │
│ ├── api/ # REST API endpoints
│ │ ├── __init__.py
│ │ ├── routes/
│ │ │ ├── games.py # Game CRUD operations
│ │ │ ├── auth.py # Discord OAuth endpoints
│ │ │ └── health.py # Health check endpoints
│ │ └── dependencies.py # FastAPI dependencies
│ │
│ ├── database/ # Database operations
│ │ ├── __init__.py
│ │ ├── session.py # DB connection management
│ │ ├── operations.py # Async DB operations
│ │ └── migrations/ # Alembic migrations
│ │
│ ├── data/ # External data integration
│ │ ├── __init__.py
│ │ ├── api_client.py # League REST API client
│ │ ├── cache.py # Optional caching layer
│ │ └── models.py # API response models
│ │
│ └── utils/ # Utilities
│ ├── __init__.py
│ ├── logging.py # Logging configuration
│ ├── auth.py # JWT token handling
│ └── errors.py # Custom exceptions
│
├── tests/
│ ├── unit/ # Unit tests
│ │ ├── test_game_engine.py
│ │ ├── test_dice.py
│ │ ├── test_play_resolver.py
│ │ └── test_player_models.py
│ ├── integration/ # Integration tests
│ │ ├── test_websocket.py
│ │ ├── test_database.py
│ │ └── test_api_client.py
│ └── e2e/ # End-to-end tests
│ └── test_full_game.py
│
├── alembic.ini # Alembic configuration
├── requirements.txt # Python dependencies
├── requirements-dev.txt # Development dependencies
├── docker-compose.yml # Local development setup
├── Dockerfile # Production container
└── pytest.ini # Pytest configuration
Core Components
1. Game Engine (core/game_engine.py)
Responsibilities:
- Manage in-memory game state
- Process player actions
- Coordinate play resolution
- Emit events to WebSocket clients
Key Methods:
class GameEngine:
def __init__(self, game_id: str, config: LeagueConfig)
async def start_game(self) -> None
async def process_defensive_positioning(self, user_id: str, positioning: str) -> PlayOutcome
async def process_stolen_base_attempt(self, runner_positions: List[str]) -> PlayOutcome
async def process_offensive_approach(self, approach: str) -> PlayOutcome
async def make_substitution(self, card_id: int, position: str) -> None
async def change_pitcher(self, card_id: int) -> None
def get_current_state(self) -> GameState
async def persist_play(self, play: Play) -> None
2. State Manager (core/state_manager.py)
Responsibilities:
- Hold active game states in memory
- Provide fast read/write access
- Handle state recovery from database
- Manage state lifecycle (creation, updates, cleanup)
Key Methods:
class StateManager:
def create_game_state(self, game_id: str, config: LeagueConfig) -> GameState
def get_game_state(self, game_id: str) -> Optional[GameState]
def update_state(self, game_id: str, updates: dict) -> None
async def recover_state(self, game_id: str) -> GameState
def remove_state(self, game_id: str) -> None
def get_active_games(self) -> List[str]
Data Structure:
@dataclass
class GameState:
game_id: str
league_id: str
inning: int
half: str # 'top' or 'bottom'
outs: int
balls: int
strikes: int
home_score: int
away_score: int
runners: Dict[str, Optional[int]] # {'first': card_id, 'second': None, 'third': card_id}
current_batter_idx: int
lineups: Dict[str, List[Lineup]] # {'home': [...], 'away': [...]}
current_decisions: Dict[str, Any] # Pending decisions
play_history: List[int] # Play IDs
metadata: dict
3. Play Resolver (core/play_resolver.py)
Responsibilities:
- Roll dice and determine outcomes
- Apply league-specific result charts
- Calculate runner advancement
- Update game state based on outcome
Key Methods:
class PlayResolver:
def __init__(self, config: LeagueConfig)
def roll_dice(self) -> int
def resolve_at_bat(
self,
batter: PdPlayer | SbaPlayer,
pitcher: PdPlayer | SbaPlayer,
defensive_positioning: str,
offensive_approach: str,
count: Dict[str, int]
) -> PlayOutcome
def advance_runners(
self,
hit_type: str,
runners_before: Dict[str, Optional[int]],
hit_location: Optional[str] = None
) -> Tuple[Dict[str, Optional[int]], int] # (runners_after, runs_scored)
4. Polymorphic Player Models (models/player_models.py)
Class Hierarchy:
class BasePlayer(BaseModel, ABC):
id: int
name: str
@abstractmethod
def get_image_url(self) -> str:
pass
@abstractmethod
def get_display_data(self) -> dict:
pass
class SbaPlayer(BasePlayer):
image: str
team: Optional[str] = None
manager: Optional[str] = None
def get_image_url(self) -> str:
return self.image
class PdPlayer(BasePlayer):
image: str
scouting_data: dict
ratings: dict
probabilities: dict
def get_image_url(self) -> str:
return self.image
class Lineup(BaseModel):
id: int
game_id: str
card_id: int
position: str
batting_order: Optional[int] = None
player: Union[SbaPlayer, PdPlayer]
@classmethod
def from_api_data(cls, game_config: LeagueConfig, api_data: dict) -> 'Lineup':
player_data = api_data.pop('player')
if game_config.league_id == "sba":
player = SbaPlayer(**player_data)
elif game_config.league_id == "pd":
player = PdPlayer(**player_data)
else:
raise ValueError(f"Unknown league: {game_config.league_id}")
return cls(player=player, **api_data)
5. WebSocket Connection Manager (websocket/connection_manager.py)
Responsibilities:
- Manage Socket.io connections
- Handle room assignments (game rooms)
- Broadcast events to participants
- Handle disconnections and reconnections
Key Methods:
class ConnectionManager:
def __init__(self, sio: socketio.AsyncServer)
async def connect(self, sid: str, user_id: str) -> None
async def disconnect(self, sid: str) -> None
async def join_game(self, sid: str, game_id: str, role: str) -> None
async def leave_game(self, sid: str, game_id: str) -> None
async def broadcast_to_game(self, game_id: str, event: str, data: dict) -> None
async def emit_to_user(self, sid: str, event: str, data: dict) -> None
def get_game_participants(self, game_id: str) -> List[str]
Data Flow
Game Action Processing
1. Client sends WebSocket event
↓
2. handlers.py receives and validates
↓
3. Verify user authorization
↓
4. Load game state from StateManager
↓
5. GameEngine processes action
↓
6. PlayResolver determines outcome (if dice roll needed)
↓
7. StateManager updates in-memory state
↓
8. Database operations (async, non-blocking)
↓
9. ConnectionManager broadcasts update to all clients
↓
10. Clients update UI
State Recovery on Reconnect
1. Client reconnects to WebSocket
↓
2. Client sends join_game event
↓
3. Check if state exists in StateManager
↓
4. If not in memory:
a. Load game metadata from database
b. Load all plays from database
c. Replay plays through GameEngine
d. Store reconstructed state in StateManager
↓
5. Send current state to client
↓
6. Client resumes gameplay
League Configuration System
Config Loading
# config/base_config.py
@dataclass
class BaseGameConfig:
innings: int = 9
outs_per_inning: int = 3
strikes_for_out: int = 3
balls_for_walk: int = 4
roster_size: int = 26
positions: List[str] = field(default_factory=lambda: ['C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF'])
# config/league_configs.py
@dataclass
class SbaConfig(BaseGameConfig):
league_id: str = "sba"
league_name: str = "Super Baseball Alliance"
api_url: str = field(default_factory=lambda: os.getenv("SBA_API_URL"))
result_charts: dict = field(default_factory=lambda: load_sba_charts())
special_rules: dict = field(default_factory=dict)
@dataclass
class PdConfig(BaseGameConfig):
league_id: str = "pd"
league_name: str = "Paper Dynasty"
api_url: str = field(default_factory=lambda: os.getenv("PD_API_URL"))
result_charts: dict = field(default_factory=lambda: load_pd_charts())
special_rules: dict = field(default_factory=dict)
# config/loader.py
def get_league_config(league_id: str) -> BaseGameConfig:
configs = {
"sba": SbaConfig,
"pd": PdConfig
}
if league_id not in configs:
raise ValueError(f"Unknown league: {league_id}")
return configs[league_id]()
Database Integration
Async Operations
All database operations use async patterns to avoid blocking game logic:
# database/operations.py
async def save_play(play: Play) -> int:
"""Save play to database asynchronously"""
async with get_session() as session:
db_play = DbPlay(**play.dict())
session.add(db_play)
await session.commit()
await session.refresh(db_play)
return db_play.id
async def load_game_plays(game_id: str) -> List[Play]:
"""Load all plays for a game"""
async with get_session() as session:
result = await session.execute(
select(DbPlay)
.where(DbPlay.game_id == game_id)
.order_by(DbPlay.play_number)
)
db_plays = result.scalars().all()
return [Play.from_orm(p) for p in db_plays]
Security Considerations
Authentication
- Discord OAuth tokens validated on REST endpoints
- WebSocket connections require valid JWT token
- Token expiration and refresh handled
Authorization
- Verify user owns team before allowing actions
- Spectator permissions enforced (read-only)
- Rate limiting on API endpoints
Game Integrity
- All rule validation server-side
- Cryptographically secure dice rolls
- Audit trail of all plays
- State validation before each action
Performance Optimizations
In-Memory State
- Active games kept in memory for fast access
- Idle games evicted after timeout (configurable)
- State recovery only when needed
Async Database Writes
- Play persistence doesn't block game logic
- Connection pooling for efficiency
- Batch writes where appropriate
Caching Layer (Optional)
- Cache team/roster data from league APIs
- Redis for distributed caching (if multi-server)
- TTL-based cache invalidation
Logging Strategy
Log Levels
- DEBUG: State transitions, action processing details
- INFO: Game started/completed, player joins/leaves
- WARNING: Invalid actions, timeouts, retry attempts
- ERROR: Database errors, API failures, state corruption
- CRITICAL: System failures requiring immediate attention
Log Format
import logging
logger = logging.getLogger(f'{__name__}.GameEngine')
# Usage
logger.info(f"Game {game_id} started", extra={
"game_id": game_id,
"league_id": league_id,
"home_team": home_team_id,
"away_team": away_team_id
})
Error Handling
Custom Exceptions
class GameEngineError(Exception):
"""Base exception for game engine errors"""
pass
class InvalidActionError(GameEngineError):
"""Raised when action violates game rules"""
pass
class StateRecoveryError(GameEngineError):
"""Raised when state cannot be recovered"""
pass
class UnauthorizedActionError(GameEngineError):
"""Raised when user not authorized for action"""
pass
Error Recovery
- Automatic reconnection for database issues
- State recovery from last known good state
- Graceful degradation when league API unavailable
- Transaction rollback on database errors
Testing Strategy
Unit Tests
- Test each core component in isolation
- Mock external dependencies (DB, APIs)
- Verify dice roll randomness and distribution
- Test all play resolution scenarios
Integration Tests
- Test WebSocket event flow
- Test database persistence and recovery
- Test league config loading
- Test API client interactions
Performance Tests
- Measure action processing time
- Test concurrent game handling
- Monitor memory usage under load
- Test state recovery speed
Next Steps: See 02-game-engine.md for implementation details.