strat-gameplay-webapp/backend/.claude/DATABASE_SCHEMA.md
Cal Corum 9546d2a370 CLAUDE: Extract database schema to reference document
- Created backend/.claude/DATABASE_SCHEMA.md with full table details
- Updated database/CLAUDE.md to reference the new schema doc
- Preserves valuable reference material while keeping CLAUDE.md concise

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 16:27:27 -06:00

7.5 KiB

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:

@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

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)

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:

# 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

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

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

# 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

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

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