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
|
## References
|
||||||
|
|
||||||
|
- **Database Schema**: See `../../.claude/DATABASE_SCHEMA.md` for complete table details
|
||||||
- **Models**: See `../models/CLAUDE.md`
|
- **Models**: See `../models/CLAUDE.md`
|
||||||
- **State Recovery**: See `../core/state_manager.py`
|
- **State Recovery**: See `../core/state_manager.py`
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user