- 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>
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 systemsleague_id(String): 'sba' or 'pd', determines league-specific behaviorstatus(String): 'pending', 'active', 'completed'game_mode(String): 'ranked', 'friendly', 'practice'visibility(String): 'public', 'private'current_inning,current_half: Current game statehome_score,away_score: Running scores
AI Support:
home_team_is_ai(Boolean): Home team controlled by AIaway_team_is_ai(Boolean): Away team controlled by AIai_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 counterinning,half: Inning stateouts_before: Outs at start of playbatting_order: Current spot in orderaway_score,home_score: Score at play start
Player References (FKs to Lineup):
batter_id,pitcher_id,catcher_id: Required playersdefender_id,runner_id: Optional for specific playson_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 upon_base_code(Integer): Bit field for efficient queries (1=1st, 2=2nd, 4=3rd, 7=loaded)
Strategic Decisions:
defensive_choices(JSON): Alignment, holds, shiftsoffensive_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 resultouts_recorded,runs_scored: Play outcomecheck_pos(String): Defensive position for X-check
Batting Statistics (25+ fields):
pa,ab,hit,double,triple,homerunbb,so,hbp,rbi,sac,ibb,gidpsb,cs: Base stealingwild_pitch,passed_ball,pick_off,balkbphr,bpfo,bp1b,bplo: Ballpark power eventsrun,e_run: Earned/unearned runs
Advanced Analytics:
wpa(Float): Win Probability Addedre24(Float): Run Expectancy 24 base-out states
Game Situation Flags:
is_tied,is_go_ahead,is_new_inning: Context flagsin_pow: Pitcher over workloadcomplete,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/cardposition(String): P, C, 1B, 2B, 3B, SS, LF, CF, RF, DHbatting_order(Integer): 1-9
Substitution Tracking:
is_starter(Boolean): Original lineup vs substituteis_active(Boolean): Currently in gameentered_inning(Integer): When player enteredreplacing_id(Integer): Lineup ID of replaced playerafter_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 keypriority(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 tablecard_id(Integer, nullable): PD league - card identifierplayer_id(Integer, nullable): SBA league - player identifierteam_id(Integer): Which team owns this entity in this game
Constraints:
roster_link_one_id_required: CHECK constraint ensures exactly one ofcard_idorplayer_idis populated (XOR logic)uq_game_card: UNIQUE constraint on (game_id, card_id) for PDuq_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 Gameconnected_users(JSON): Active WebSocket connectionslast_action_at(DateTime): Last activity timestampstate_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