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")