# 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**: ```python 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**: ```python 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**: ```python @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**: ```python 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**: ```python 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**: ```python 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 ```python # 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: ```python # 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 ```python 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 ```python 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](./02-game-engine.md) for implementation details.