# Database Schema Reference Detailed database schema for the Paper Dynasty game engine. Based on proven Discord game implementation with enhancements for web real-time gameplay. --- ## Core Tables ### Game (`games`) Primary game container with state tracking. **Key Fields:** - `id` (UUID): Primary key, prevents ID collisions across distributed systems - `league_id` (String): 'sba' or 'pd', determines league-specific behavior - `status` (String): 'pending', 'active', 'completed' - `game_mode` (String): 'ranked', 'friendly', 'practice' - `visibility` (String): 'public', 'private' - `current_inning`, `current_half`: Current game state - `home_score`, `away_score`: Running scores **AI Support:** - `home_team_is_ai` (Boolean): Home team controlled by AI - `away_team_is_ai` (Boolean): Away team controlled by AI - `ai_difficulty` (String): 'balanced', 'yolo', 'safe' **Relationships:** - `plays`: All plays in the game (cascade delete) - `lineups`: All lineup entries (cascade delete) - `cardset_links`: PD only - approved cardsets (cascade delete) - `roster_links`: Roster tracking - cards (PD) or players (SBA) (cascade delete) - `session`: Real-time WebSocket session (cascade delete) --- ### Play (`plays`) Records every at-bat with full statistics and game state. **Game State Snapshot:** - `play_number`: Sequential play counter - `inning`, `half`: Inning state - `outs_before`: Outs at start of play - `batting_order`: Current spot in order - `away_score`, `home_score`: Score at play start **Player References (FKs to Lineup):** - `batter_id`, `pitcher_id`, `catcher_id`: Required players - `defender_id`, `runner_id`: Optional for specific plays - `on_first_id`, `on_second_id`, `on_third_id`: Base runners **Runner Outcomes:** - `on_first_final`, `on_second_final`, `on_third_final`: Final base (None = out, 1-4 = base) - `batter_final`: Where batter ended up - `on_base_code` (Integer): Bit field for efficient queries (1=1st, 2=2nd, 4=3rd, 7=loaded) **Strategic Decisions:** - `defensive_choices` (JSON): Alignment, holds, shifts - `offensive_choices` (JSON): Steal attempts, bunts, hit-and-run **Play Result:** - `dice_roll` (String): Dice notation (e.g., "14+6") - `hit_type` (String): GB, FB, LD, etc. - `result_description` (Text): Human-readable result - `outs_recorded`, `runs_scored`: Play outcome - `check_pos` (String): Defensive position for X-check **Batting Statistics (25+ fields):** - `pa`, `ab`, `hit`, `double`, `triple`, `homerun` - `bb`, `so`, `hbp`, `rbi`, `sac`, `ibb`, `gidp` - `sb`, `cs`: Base stealing - `wild_pitch`, `passed_ball`, `pick_off`, `balk` - `bphr`, `bpfo`, `bp1b`, `bplo`: Ballpark power events - `run`, `e_run`: Earned/unearned runs **Advanced Analytics:** - `wpa` (Float): Win Probability Added - `re24` (Float): Run Expectancy 24 base-out states **Game Situation Flags:** - `is_tied`, `is_go_ahead`, `is_new_inning`: Context flags - `in_pow`: Pitcher over workload - `complete`, `locked`: Workflow state **Helper Properties:** ```python @property def ai_is_batting(self) -> bool: """True if batting team is AI-controlled""" return (self.half == 'top' and self.game.away_team_is_ai) or \ (self.half == 'bot' and self.game.home_team_is_ai) @property def ai_is_fielding(self) -> bool: """True if fielding team is AI-controlled""" return not self.ai_is_batting ``` --- ### Lineup (`lineups`) Tracks player assignments and substitutions. **Key Fields:** - `game_id`, `team_id`, `card_id`: Links to game/team/card - `position` (String): P, C, 1B, 2B, 3B, SS, LF, CF, RF, DH - `batting_order` (Integer): 1-9 **Substitution Tracking:** - `is_starter` (Boolean): Original lineup vs substitute - `is_active` (Boolean): Currently in game - `entered_inning` (Integer): When player entered - `replacing_id` (Integer): Lineup ID of replaced player - `after_play` (Integer): Exact play number of substitution **Pitcher Management:** - `is_fatigued` (Boolean): Triggers bullpen decisions --- ### GameCardsetLink (`game_cardset_links`) PD league only - defines legal cardsets for a game. **Key Fields:** - `game_id`, `cardset_id`: Composite primary key - `priority` (Integer): 1 = primary, 2+ = backup **Usage:** - SBA games: Empty (no cardset restrictions) - PD games: Required (validates card eligibility) --- ### RosterLink (`roster_links`) Tracks eligible cards (PD) or players (SBA) for a game. **Polymorphic Design**: Single table supporting both leagues with application-layer type safety. **Key Fields:** - `id` (Integer): Surrogate primary key (auto-increment) - `game_id` (UUID): Foreign key to games table - `card_id` (Integer, nullable): PD league - card identifier - `player_id` (Integer, nullable): SBA league - player identifier - `team_id` (Integer): Which team owns this entity in this game **Constraints:** - `roster_link_one_id_required`: CHECK constraint ensures exactly one of `card_id` or `player_id` is populated (XOR logic) - `uq_game_card`: UNIQUE constraint on (game_id, card_id) for PD - `uq_game_player`: UNIQUE constraint on (game_id, player_id) for SBA **Usage Pattern:** ```python # PD league - add card to roster roster_data = await db_ops.add_pd_roster_card( game_id=game_id, card_id=123, team_id=1 ) # SBA league - add player to roster roster_data = await db_ops.add_sba_roster_player( game_id=game_id, player_id=456, team_id=2 ) # Get roster (league-specific) pd_roster = await db_ops.get_pd_roster(game_id, team_id=1) sba_roster = await db_ops.get_sba_roster(game_id, team_id=2) ``` **Design Rationale:** - Single table avoids complex joins and simplifies queries - Nullable columns with CHECK constraint ensures data integrity at database level - Pydantic models (`PdRosterLinkData`, `SbaRosterLinkData`) provide type safety at application layer - Surrogate key allows nullable columns (can't use nullable columns in composite PK) --- ### GameSession (`game_sessions`) Real-time WebSocket state tracking. **Key Fields:** - `game_id` (UUID): Primary key, one-to-one with Game - `connected_users` (JSON): Active WebSocket connections - `last_action_at` (DateTime): Last activity timestamp - `state_snapshot` (JSON): In-memory game state cache --- ## Common Query Patterns ### Async Session Usage ```python from app.database.session import get_session async def some_function(): async with get_session() as session: result = await session.execute(query) # session.commit() happens automatically ``` ### Relationship Loading ```python from sqlalchemy.orm import joinedload # Efficient loading for broadcasting play = await session.execute( select(Play) .options( joinedload(Play.batter), joinedload(Play.pitcher), joinedload(Play.on_first), joinedload(Play.on_second), joinedload(Play.on_third) ) .where(Play.id == play_id) ) ``` ### Find Plays with Bases Loaded ```python # Using on_base_code bit field bases_loaded_plays = await session.execute( select(Play).where(Play.on_base_code == 7) # 1+2+4 = 7 ) ``` ### Get Active Pitcher ```python pitcher = await session.execute( select(Lineup) .where( Lineup.game_id == game_id, Lineup.team_id == team_id, Lineup.position == 'P', Lineup.is_active == True ) ) ``` ### Calculate Box Score Stats ```python stats = await session.execute( select( func.sum(Play.ab).label('ab'), func.sum(Play.hit).label('hits'), func.sum(Play.homerun).label('hr'), func.sum(Play.rbi).label('rbi') ) .where( Play.game_id == game_id, Play.batter_id == lineup_id ) ) ``` --- **Updated**: 2025-01-19