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>
This commit is contained in:
parent
88a5207c2c
commit
9546d2a370
258
backend/.claude/DATABASE_SCHEMA.md
Normal file
258
backend/.claude/DATABASE_SCHEMA.md
Normal file
@ -0,0 +1,258 @@
|
||||
# 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
|
||||
@ -122,6 +122,7 @@ DATABASE_URL=postgresql+asyncpg://user:pass@10.10.0.42:5432/paperdynasty_dev
|
||||
|
||||
## References
|
||||
|
||||
- **Database Schema**: See `../../.claude/DATABASE_SCHEMA.md` for complete table details
|
||||
- **Models**: See `../models/CLAUDE.md`
|
||||
- **State Recovery**: See `../core/state_manager.py`
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user