strat-gameplay-webapp/backend/app/models/db_models.py
Cal Corum d8a43faa2e CLAUDE: Complete Phase 1 - Frontend Infrastructure Setup
Initialize both Nuxt 3 frontends (SBA and PD) with full configuration:

Frontend Setup:
- Initialized Nuxt 3 projects for both leagues (SBA and PD)
- Installed dependencies: Tailwind CSS, Pinia, Socket.io-client, Axios
- Configured league-specific settings in nuxt.config.ts
- Created WebSocket plugins for real-time communication
- Set up TypeScript with strict mode and type checking
- Configured Tailwind CSS for styling

Backend Updates:
- Updated database models documentation in backend/CLAUDE.md
- Enhanced db_models.py with additional relationship patterns

Documentation:
- Updated Phase 1 completion checklist (12/12 items - 100% complete)
- Marked all infrastructure objectives as complete

Running Services:
- Backend (FastAPI + Socket.io): http://localhost:8000
- Frontend SBA: http://localhost:3000
- Frontend PD: http://localhost:3001
- Redis: port 6379
- PostgreSQL: Connected to remote server

Phase 1 is now complete. Ready to proceed to Phase 2 (Game Engine Core).

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-22 00:24:00 -05:00

246 lines
9.2 KiB
Python

from sqlalchemy import Column, Integer, String, Boolean, DateTime, JSON, Text, ForeignKey, Float
from sqlalchemy.orm import relationship
from sqlalchemy.dialects.postgresql import UUID
import uuid
import pendulum
from app.database.session import Base
class GameCardsetLink(Base):
"""Link table for PD games - tracks which cardsets are allowed"""
__tablename__ = "game_cardset_links"
game_id = Column(UUID(as_uuid=True), ForeignKey("games.id", ondelete="CASCADE"), primary_key=True)
cardset_id = Column(Integer, primary_key=True)
priority = Column(Integer, default=1, index=True)
# Relationships
game = relationship("Game", back_populates="cardset_links")
class RosterLink(Base):
"""Tracks which cards each team is using in a game - PD only"""
__tablename__ = "roster_links"
game_id = Column(UUID(as_uuid=True), ForeignKey("games.id", ondelete="CASCADE"), primary_key=True)
card_id = Column(Integer, primary_key=True)
team_id = Column(Integer, nullable=False, index=True)
# Relationships
game = relationship("Game", back_populates="roster_links")
class Game(Base):
"""Game model"""
__tablename__ = "games"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
league_id = Column(String(50), nullable=False, index=True)
home_team_id = Column(Integer, nullable=False)
away_team_id = Column(Integer, nullable=False)
status = Column(String(20), nullable=False, default="pending", index=True)
game_mode = Column(String(20), nullable=False)
visibility = Column(String(20), nullable=False)
current_inning = Column(Integer)
current_half = Column(String(10))
home_score = Column(Integer, default=0)
away_score = Column(Integer, default=0)
# AI opponent configuration
home_team_is_ai = Column(Boolean, default=False)
away_team_is_ai = Column(Boolean, default=False)
ai_difficulty = Column(String(20), nullable=True)
# Timestamps
created_at = Column(DateTime, default=lambda: pendulum.now('UTC'), index=True)
started_at = Column(DateTime)
completed_at = Column(DateTime)
# Results
winner_team_id = Column(Integer)
game_metadata = Column(JSON, default=dict)
# Relationships
plays = relationship("Play", back_populates="game", cascade="all, delete-orphan")
lineups = relationship("Lineup", back_populates="game", cascade="all, delete-orphan")
cardset_links = relationship("GameCardsetLink", back_populates="game", cascade="all, delete-orphan")
roster_links = relationship("RosterLink", back_populates="game", cascade="all, delete-orphan")
session = relationship("GameSession", back_populates="game", uselist=False, cascade="all, delete-orphan")
class Play(Base):
"""Play model - tracks individual plays/at-bats"""
__tablename__ = "plays"
id = Column(Integer, primary_key=True, autoincrement=True)
game_id = Column(UUID(as_uuid=True), ForeignKey("games.id", ondelete="CASCADE"), nullable=False, index=True)
play_number = Column(Integer, nullable=False)
# Game state at start of play
inning = Column(Integer, nullable=False, default=1)
half = Column(String(10), nullable=False)
outs_before = Column(Integer, nullable=False, default=0)
batting_order = Column(Integer, nullable=False, default=1)
away_score = Column(Integer, default=0)
home_score = Column(Integer, default=0)
# Players involved (ForeignKeys to Lineup)
batter_id = Column(Integer, ForeignKey("lineups.id"), nullable=False)
pitcher_id = Column(Integer, ForeignKey("lineups.id"), nullable=False)
catcher_id = Column(Integer, ForeignKey("lineups.id"), nullable=False)
defender_id = Column(Integer, ForeignKey("lineups.id"), nullable=True)
runner_id = Column(Integer, ForeignKey("lineups.id"), nullable=True)
# Base runners (ForeignKeys to Lineup for who's on base)
on_first_id = Column(Integer, ForeignKey("lineups.id"), nullable=True)
on_second_id = Column(Integer, ForeignKey("lineups.id"), nullable=True)
on_third_id = Column(Integer, ForeignKey("lineups.id"), nullable=True)
# Runner final positions (None = out, 1-4 = base)
on_first_final = Column(Integer, nullable=True)
on_second_final = Column(Integer, nullable=True)
on_third_final = Column(Integer, nullable=True)
batter_final = Column(Integer, nullable=True)
# Base state code for efficient queries (bit field: 1=1st, 2=2nd, 4=3rd, 7=loaded)
on_base_code = Column(Integer, default=0)
# Strategic decisions
defensive_choices = Column(JSON, default=dict)
offensive_choices = Column(JSON, default=dict)
# Play result
dice_roll = Column(String(50))
hit_type = Column(String(50))
result_description = Column(Text)
outs_recorded = Column(Integer, nullable=False, default=0)
runs_scored = Column(Integer, default=0)
# Defensive details
check_pos = Column(String(10), nullable=True)
error = Column(Integer, default=0)
# Batting statistics
pa = Column(Integer, default=0)
ab = Column(Integer, default=0)
hit = Column(Integer, default=0)
double = Column(Integer, default=0)
triple = Column(Integer, default=0)
homerun = Column(Integer, default=0)
bb = Column(Integer, default=0)
so = Column(Integer, default=0)
hbp = Column(Integer, default=0)
rbi = Column(Integer, default=0)
sac = Column(Integer, default=0)
ibb = Column(Integer, default=0)
gidp = Column(Integer, default=0)
# Baserunning statistics
sb = Column(Integer, default=0)
cs = Column(Integer, default=0)
# Pitching events
wild_pitch = Column(Integer, default=0)
passed_ball = Column(Integer, default=0)
pick_off = Column(Integer, default=0)
balk = Column(Integer, default=0)
# Ballpark power events
bphr = Column(Integer, default=0)
bpfo = Column(Integer, default=0)
bp1b = Column(Integer, default=0)
bplo = Column(Integer, default=0)
# Advanced analytics
wpa = Column(Float, default=0.0)
re24 = Column(Float, default=0.0)
# Earned/unearned runs
run = Column(Integer, default=0)
e_run = Column(Integer, default=0)
# Game situation flags
is_tied = Column(Boolean, default=False)
is_go_ahead = Column(Boolean, default=False)
is_new_inning = Column(Boolean, default=False)
in_pow = Column(Boolean, default=False)
# Play workflow
complete = Column(Boolean, default=False, index=True)
locked = Column(Boolean, default=False)
# Timestamps
created_at = Column(DateTime, default=lambda: pendulum.now('UTC'), index=True)
# Extensibility (use for custom runner data like jump status, etc.)
play_metadata = Column(JSON, default=dict)
# Relationships
game = relationship("Game", back_populates="plays")
batter = relationship("Lineup", foreign_keys=[batter_id], lazy="joined")
pitcher = relationship("Lineup", foreign_keys=[pitcher_id], lazy="joined")
catcher = relationship("Lineup", foreign_keys=[catcher_id], lazy="joined")
defender = relationship("Lineup", foreign_keys=[defender_id])
runner = relationship("Lineup", foreign_keys=[runner_id])
on_first = relationship("Lineup", foreign_keys=[on_first_id])
on_second = relationship("Lineup", foreign_keys=[on_second_id])
on_third = relationship("Lineup", foreign_keys=[on_third_id])
@property
def ai_is_batting(self) -> bool:
"""Determine if current batting team is AI-controlled"""
if self.half == 'top':
return self.game.away_team_is_ai
else:
return self.game.home_team_is_ai
@property
def ai_is_fielding(self) -> bool:
"""Determine if current fielding team is AI-controlled"""
if self.half == 'top':
return self.game.home_team_is_ai
else:
return self.game.away_team_is_ai
class Lineup(Base):
"""Lineup model - tracks player assignments in a game"""
__tablename__ = "lineups"
id = Column(Integer, primary_key=True, autoincrement=True)
game_id = Column(UUID(as_uuid=True), ForeignKey("games.id", ondelete="CASCADE"), nullable=False, index=True)
team_id = Column(Integer, nullable=False, index=True)
card_id = Column(Integer, nullable=False)
position = Column(String(10), nullable=False)
batting_order = Column(Integer)
# Substitution tracking
is_starter = Column(Boolean, default=True)
is_active = Column(Boolean, default=True, index=True)
entered_inning = Column(Integer, default=1)
replacing_id = Column(Integer, nullable=True) # Lineup ID of player being replaced
after_play = Column(Integer, nullable=True) # Play number when substitution occurred
# Pitcher fatigue
is_fatigued = Column(Boolean, nullable=True)
# Extensibility
lineup_metadata = Column(JSON, default=dict)
# Relationships
game = relationship("Game", back_populates="lineups")
class GameSession(Base):
"""Game session tracking - real-time WebSocket state"""
__tablename__ = "game_sessions"
game_id = Column(UUID(as_uuid=True), ForeignKey("games.id", ondelete="CASCADE"), primary_key=True)
connected_users = Column(JSON, default=dict)
last_action_at = Column(DateTime, default=lambda: pendulum.now('UTC'), index=True)
state_snapshot = Column(JSON, default=dict)
# Relationships
game = relationship("Game", back_populates="session")