Features: - Lineup Builder screen: set batting order, assign positions, save/load lineups - Gameday screen: integrated matchup scout + lineup builder side-by-side - Matchup Scout: analyze batters vs opposing pitchers with standardized scoring - Standardized scoring system with league AVG/STDEV calculations - Score caching for fast matchup lookups Lineup Builder (press 'l'): - Dual-panel UI with available batters and 9-slot lineup - Keyboard controls: a=add, r=remove, k/j=reorder, p=change position - Save/load named lineups, delete saved lineups with 'd' Gameday screen (press 'g'): - Left panel: team/pitcher selection with matchup ratings - Right panel: lineup builder with live matchup ratings per batter - Players in lineup marked with * in matchup list - Click highlighted row to toggle selection for screenshots Other changes: - Dynamic season configuration (removed hardcoded season=13) - Added delete_lineup query function - StandardizedScoreCache model for pre-computed scores - Auto-rebuild score cache after card imports
473 lines
16 KiB
Python
473 lines
16 KiB
Python
"""
|
|
SQLAlchemy models for SBA Scout database.
|
|
|
|
This module defines the local database schema for:
|
|
- Synced data from the league API (players, teams, transactions)
|
|
- Locally managed card data (batter/pitcher Strat-o-Matic card values)
|
|
- User preferences (lineups, roster assignments)
|
|
"""
|
|
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
|
|
from sqlalchemy import (
|
|
JSON,
|
|
Boolean,
|
|
Column,
|
|
DateTime,
|
|
Float,
|
|
ForeignKey,
|
|
Integer,
|
|
String,
|
|
Text,
|
|
UniqueConstraint,
|
|
)
|
|
from sqlalchemy.orm import DeclarativeBase, relationship
|
|
|
|
|
|
class Base(DeclarativeBase):
|
|
"""Base class for all models."""
|
|
|
|
pass
|
|
|
|
|
|
# =============================================================================
|
|
# Core Entities (synced from league API)
|
|
# =============================================================================
|
|
|
|
|
|
class Team(Base):
|
|
"""
|
|
Team entity - synced from league API.
|
|
|
|
Teams include main roster teams, IL teams (e.g., WVIL), and minor league teams (e.g., WVMiL).
|
|
"""
|
|
|
|
__tablename__ = "teams"
|
|
|
|
id = Column(Integer, primary_key=True) # Matches league API team_id
|
|
abbrev = Column(String(10), nullable=False)
|
|
short_name = Column(String(50), nullable=False)
|
|
long_name = Column(String(100), nullable=False)
|
|
season = Column(Integer, nullable=False)
|
|
|
|
# Manager info
|
|
manager1_name = Column(String(100), nullable=True)
|
|
manager2_name = Column(String(100), nullable=True)
|
|
gm_discord_id = Column(String(20), nullable=True)
|
|
gm2_discord_id = Column(String(20), nullable=True)
|
|
|
|
# Division/League
|
|
division_id = Column(Integer, nullable=True)
|
|
division_name = Column(String(50), nullable=True)
|
|
league_abbrev = Column(String(10), nullable=True)
|
|
|
|
# Visual
|
|
thumbnail = Column(String(500), nullable=True)
|
|
color = Column(String(10), nullable=True)
|
|
dice_color = Column(String(10), nullable=True)
|
|
stadium = Column(String(200), nullable=True)
|
|
|
|
# Roster limits
|
|
salary_cap = Column(Float, nullable=True)
|
|
|
|
# Sync metadata
|
|
synced_at = Column(DateTime, default=datetime.utcnow)
|
|
|
|
# Relationships
|
|
players = relationship("Player", back_populates="team")
|
|
|
|
__table_args__ = (UniqueConstraint("abbrev", "season", name="uq_team_season"),)
|
|
|
|
|
|
class Player(Base):
|
|
"""
|
|
Player entity - synced from league API.
|
|
|
|
Contains player metadata. Card stats are stored in BatterCard/PitcherCard.
|
|
"""
|
|
|
|
__tablename__ = "players"
|
|
|
|
id = Column(Integer, primary_key=True) # Matches league API player_id
|
|
name = Column(String(200), nullable=False)
|
|
season = Column(Integer, nullable=False)
|
|
|
|
# Team assignment
|
|
team_id = Column(Integer, ForeignKey("teams.id"), nullable=True)
|
|
team = relationship("Team", back_populates="players")
|
|
|
|
# sWAR (salary/value metric)
|
|
swar = Column(Float, default=0.0)
|
|
|
|
# Card images
|
|
card_image = Column(String(500), nullable=True)
|
|
card_image_alt = Column(String(500), nullable=True)
|
|
headshot = Column(String(500), nullable=True)
|
|
vanity_card = Column(String(500), nullable=True)
|
|
|
|
# Positions (up to 8)
|
|
pos_1 = Column(String(5), nullable=True)
|
|
pos_2 = Column(String(5), nullable=True)
|
|
pos_3 = Column(String(5), nullable=True)
|
|
pos_4 = Column(String(5), nullable=True)
|
|
pos_5 = Column(String(5), nullable=True)
|
|
pos_6 = Column(String(5), nullable=True)
|
|
pos_7 = Column(String(5), nullable=True)
|
|
pos_8 = Column(String(5), nullable=True)
|
|
|
|
# Hand (L/R/S for switch)
|
|
hand = Column(String(1), nullable=True)
|
|
|
|
# Injury info
|
|
injury_rating = Column(String(20), nullable=True)
|
|
il_return = Column(String(20), nullable=True)
|
|
|
|
# Demotion tracking
|
|
demotion_week = Column(Integer, nullable=True)
|
|
|
|
# External references
|
|
strat_code = Column(String(100), nullable=True)
|
|
bbref_id = Column(String(50), nullable=True)
|
|
sbaplayer_id = Column(Integer, nullable=True)
|
|
|
|
# Last game appearances
|
|
last_game = Column(String(20), nullable=True)
|
|
last_game2 = Column(String(20), nullable=True)
|
|
|
|
# Sync metadata
|
|
synced_at = Column(DateTime, default=datetime.utcnow)
|
|
|
|
# Relationships
|
|
batter_card = relationship("BatterCard", back_populates="player", uselist=False)
|
|
pitcher_card = relationship("PitcherCard", back_populates="player", uselist=False)
|
|
|
|
@property
|
|
def positions(self) -> list[str]:
|
|
"""Return list of all positions this player can play."""
|
|
return [
|
|
p
|
|
for p in [
|
|
self.pos_1,
|
|
self.pos_2,
|
|
self.pos_3,
|
|
self.pos_4,
|
|
self.pos_5,
|
|
self.pos_6,
|
|
self.pos_7,
|
|
self.pos_8,
|
|
]
|
|
if p
|
|
]
|
|
|
|
@property
|
|
def is_pitcher(self) -> bool:
|
|
"""Check if player is a pitcher."""
|
|
pitcher_positions = {"SP", "RP", "CP"}
|
|
return bool(pitcher_positions & set(self.positions))
|
|
|
|
@property
|
|
def is_batter(self) -> bool:
|
|
"""Check if player can bat (has non-pitcher positions or is DH)."""
|
|
batter_positions = {"C", "1B", "2B", "3B", "SS", "LF", "CF", "RF", "DH"}
|
|
return bool(batter_positions & set(self.positions))
|
|
|
|
|
|
# =============================================================================
|
|
# Card Data (locally managed, imported from Strat-o-Matic)
|
|
# =============================================================================
|
|
|
|
|
|
class BatterCard(Base):
|
|
"""
|
|
Strat-o-Matic batter card data.
|
|
|
|
Contains all the probability values from the physical/digital card.
|
|
This data is imported manually (CSV, Excel, or future API).
|
|
"""
|
|
|
|
__tablename__ = "batter_cards"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
player_id = Column(Integer, ForeignKey("players.id"), nullable=False, unique=True)
|
|
player = relationship("Player", back_populates="batter_card")
|
|
|
|
# vs Left-Handed Pitchers
|
|
so_vlhp = Column(Float, default=0) # Strikeout chance
|
|
bb_vlhp = Column(Float, default=0) # Walk chance
|
|
hit_vlhp = Column(Float, default=0) # Hit chance
|
|
ob_vlhp = Column(Float, default=0) # On-base chance
|
|
tb_vlhp = Column(Float, default=0) # Total bases
|
|
hr_vlhp = Column(Float, default=0) # Home run chance
|
|
dp_vlhp = Column(Float, default=0) # Double play chance
|
|
|
|
# vs Right-Handed Pitchers
|
|
so_vrhp = Column(Float, default=0)
|
|
bb_vrhp = Column(Float, default=0)
|
|
hit_vrhp = Column(Float, default=0)
|
|
ob_vrhp = Column(Float, default=0)
|
|
tb_vrhp = Column(Float, default=0)
|
|
hr_vrhp = Column(Float, default=0)
|
|
dp_vrhp = Column(Float, default=0)
|
|
|
|
# Ballpark modifiers
|
|
bphr_vlhp = Column(Float, default=0) # Ballpark HR modifier vs LHP
|
|
bphr_vrhp = Column(Float, default=0) # Ballpark HR modifier vs RHP
|
|
bp1b_vlhp = Column(Float, default=0) # Ballpark single modifier vs LHP
|
|
bp1b_vrhp = Column(Float, default=0) # Ballpark single modifier vs RHP
|
|
|
|
# Running game
|
|
stealing = Column(String(50), nullable=True) # e.g., "*4,5/- (19-12)"
|
|
steal_rating = Column(String(5), nullable=True) # e.g., "A", "B", "C"
|
|
speed = Column(Integer, default=10) # Speed rating 1-20
|
|
|
|
# Batting extras
|
|
bunt = Column(String(5), nullable=True) # Bunt rating
|
|
hit_run = Column(String(5), nullable=True) # Hit and run rating
|
|
|
|
# Fielding data - raw string from CSV (e.g., "cf-3(-1)e5 rf-2e5 lf-2e5")
|
|
fielding = Column(String(500), nullable=True)
|
|
|
|
# Catcher-specific
|
|
catcher_arm = Column(Integer, nullable=True)
|
|
catcher_pb = Column(Integer, nullable=True) # Passed ball rating
|
|
catcher_t = Column(Integer, nullable=True) # T-rating for throwing
|
|
|
|
# Computed ratings (updated when weights change)
|
|
rating_vl = Column(Float, nullable=True) # Composite rating vs LHP
|
|
rating_vr = Column(Float, nullable=True) # Composite rating vs RHP
|
|
rating_overall = Column(Float, nullable=True) # Combined rating
|
|
|
|
# Import metadata
|
|
imported_at = Column(DateTime, default=datetime.utcnow)
|
|
source = Column(String(100), nullable=True) # Where the data came from
|
|
|
|
|
|
class PitcherCard(Base):
|
|
"""
|
|
Strat-o-Matic pitcher card data.
|
|
|
|
Contains all the probability values from the physical/digital card.
|
|
This data is imported manually (CSV, Excel, or future API).
|
|
"""
|
|
|
|
__tablename__ = "pitcher_cards"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
player_id = Column(Integer, ForeignKey("players.id"), nullable=False, unique=True)
|
|
player = relationship("Player", back_populates="pitcher_card")
|
|
|
|
# vs Left-Handed Batters
|
|
so_vlhb = Column(Float, default=0) # Strikeout chance
|
|
bb_vlhb = Column(Float, default=0) # Walk chance
|
|
hit_vlhb = Column(Float, default=0) # Hit chance
|
|
ob_vlhb = Column(Float, default=0) # On-base chance
|
|
tb_vlhb = Column(Float, default=0) # Total bases
|
|
hr_vlhb = Column(Float, default=0) # Home run chance
|
|
dp_vlhb = Column(Float, default=0) # Double play chance
|
|
|
|
# Ballpark columns for pitcher
|
|
bphr_vlhb = Column(Float, default=0)
|
|
bp1b_vlhb = Column(Float, default=0)
|
|
|
|
# vs Right-Handed Batters
|
|
so_vrhb = Column(Float, default=0)
|
|
bb_vrhb = Column(Float, default=0)
|
|
hit_vrhb = Column(Float, default=0)
|
|
ob_vrhb = Column(Float, default=0)
|
|
tb_vrhb = Column(Float, default=0)
|
|
hr_vrhb = Column(Float, default=0)
|
|
dp_vrhb = Column(Float, default=0)
|
|
bphr_vrhb = Column(Float, default=0)
|
|
bp1b_vrhb = Column(Float, default=0)
|
|
|
|
# Hold rating (pitcher's card reading probability)
|
|
hold_rating = Column(Integer, default=0) # -3 to +3 typical range
|
|
|
|
# Endurance
|
|
endurance_start = Column(Integer, nullable=True) # SP endurance
|
|
endurance_relief = Column(Integer, nullable=True) # RP endurance
|
|
endurance_close = Column(Integer, nullable=True) # CP endurance
|
|
|
|
# Fielding
|
|
fielding_range = Column(Integer, nullable=True)
|
|
fielding_error = Column(Integer, nullable=True)
|
|
|
|
# Wild pitch / Balk
|
|
wild_pitch = Column(Integer, default=0)
|
|
balk = Column(Integer, default=0)
|
|
|
|
# Batting (for NL/interleague)
|
|
batting_rating = Column(String(10), nullable=True)
|
|
|
|
# Computed ratings (updated when weights change)
|
|
rating_vlhb = Column(Float, nullable=True) # Rating vs LH batters
|
|
rating_vrhb = Column(Float, nullable=True) # Rating vs RH batters
|
|
rating_overall = Column(Float, nullable=True)
|
|
|
|
# Import metadata
|
|
imported_at = Column(DateTime, default=datetime.utcnow)
|
|
source = Column(String(100), nullable=True)
|
|
|
|
|
|
# =============================================================================
|
|
# Transactions (synced from league API)
|
|
# =============================================================================
|
|
|
|
|
|
class Transaction(Base):
|
|
"""
|
|
Transaction record - synced from league API.
|
|
|
|
Tracks player moves between teams.
|
|
"""
|
|
|
|
__tablename__ = "transactions"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
season = Column(Integer, nullable=False)
|
|
week = Column(Integer, nullable=False)
|
|
move_id = Column(String(50), nullable=False) # Unique identifier for the move
|
|
|
|
player_id = Column(Integer, ForeignKey("players.id"), nullable=False)
|
|
from_team_id = Column(Integer, ForeignKey("teams.id"), nullable=False)
|
|
to_team_id = Column(Integer, ForeignKey("teams.id"), nullable=False)
|
|
|
|
# Status
|
|
cancelled = Column(Boolean, default=False)
|
|
frozen = Column(Boolean, default=False)
|
|
|
|
# Sync metadata
|
|
synced_at = Column(DateTime, default=datetime.utcnow)
|
|
|
|
__table_args__ = (UniqueConstraint("move_id", "player_id", name="uq_transaction_move_player"),)
|
|
|
|
|
|
# =============================================================================
|
|
# User Data (local only)
|
|
# =============================================================================
|
|
|
|
|
|
class Lineup(Base):
|
|
"""
|
|
Saved lineup configuration.
|
|
|
|
Users can save multiple lineups (e.g., "vs LHP", "vs RHP", "Standard").
|
|
"""
|
|
|
|
__tablename__ = "lineups"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
name = Column(String(100), nullable=False)
|
|
description = Column(Text, nullable=True)
|
|
|
|
# Lineup type
|
|
lineup_type = Column(String(20), default="standard") # standard, vs_lhp, vs_rhp
|
|
|
|
# Batting order (JSON array of player_ids)
|
|
# Format: [player_id_1, player_id_2, ..., player_id_9]
|
|
batting_order = Column(JSON, nullable=True)
|
|
|
|
# Position assignments (JSON object)
|
|
# Format: {"C": player_id, "1B": player_id, "2B": player_id, ...}
|
|
positions = Column(JSON, nullable=True)
|
|
|
|
# Pitcher
|
|
starting_pitcher_id = Column(Integer, ForeignKey("players.id"), nullable=True)
|
|
|
|
# Metadata
|
|
created_at = Column(DateTime, default=datetime.utcnow)
|
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
|
|
|
|
|
class MatchupCache(Base):
|
|
"""
|
|
Cached matchup calculations.
|
|
|
|
Stores pre-computed matchup results for quick lookup during games.
|
|
Invalidated when card data or weights change.
|
|
"""
|
|
|
|
__tablename__ = "matchup_cache"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
batter_id = Column(Integer, ForeignKey("players.id"), nullable=False)
|
|
pitcher_id = Column(Integer, ForeignKey("players.id"), nullable=False)
|
|
|
|
# Calculated values
|
|
rating = Column(Float, nullable=False)
|
|
tier = Column(String(1), nullable=True) # A, B, C, D, F
|
|
|
|
# Detailed breakdown (JSON)
|
|
details = Column(JSON, nullable=True)
|
|
|
|
# Cache validity
|
|
computed_at = Column(DateTime, default=datetime.utcnow)
|
|
weights_hash = Column(String(64), nullable=True) # Hash of weights used
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint("batter_id", "pitcher_id", name="uq_matchup_batter_pitcher"),
|
|
)
|
|
|
|
|
|
class StandardizedScoreCache(Base):
|
|
"""
|
|
Cached standardized scores for card stats.
|
|
|
|
Pre-computes the standardized score (-3 to +3) and weighted score for each
|
|
stat on each card, based on league averages and standard deviations.
|
|
|
|
Invalidated and recalculated when:
|
|
- Card data is imported/updated
|
|
- Weight values are changed
|
|
- League stats change significantly (new cards added)
|
|
"""
|
|
|
|
__tablename__ = "standardized_score_cache"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
|
|
# Card reference (either batter or pitcher, not both)
|
|
batter_card_id = Column(Integer, ForeignKey("batter_cards.id"), nullable=True)
|
|
pitcher_card_id = Column(Integer, ForeignKey("pitcher_cards.id"), nullable=True)
|
|
|
|
# Which split this score is for
|
|
split = Column(String(10), nullable=False) # "vlhp", "vrhp", "vlhb", "vrhb"
|
|
|
|
# Pre-computed total weighted score for this card/split
|
|
total_score = Column(Float, nullable=False)
|
|
|
|
# Individual stat scores (JSON for flexibility)
|
|
# Format: {"so": {"raw": 15.0, "std": 1, "weighted": 1}, "bb": {...}, ...}
|
|
stat_scores = Column(JSON, nullable=False)
|
|
|
|
# Cache validity
|
|
computed_at = Column(DateTime, default=datetime.utcnow)
|
|
weights_hash = Column(String(64), nullable=True) # Hash of weights used
|
|
league_stats_hash = Column(String(64), nullable=True) # Hash of league avg/stdev
|
|
|
|
# Relationships
|
|
batter_card = relationship("BatterCard", foreign_keys=[batter_card_id])
|
|
pitcher_card = relationship("PitcherCard", foreign_keys=[pitcher_card_id])
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint("batter_card_id", "split", name="uq_batter_score_split"),
|
|
UniqueConstraint("pitcher_card_id", "split", name="uq_pitcher_score_split"),
|
|
)
|
|
|
|
|
|
class SyncStatus(Base):
|
|
"""
|
|
Tracks sync status with the league API.
|
|
|
|
Helps determine when data needs refreshing.
|
|
"""
|
|
|
|
__tablename__ = "sync_status"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
entity_type = Column(String(50), nullable=False, unique=True) # players, teams, etc.
|
|
last_sync = Column(DateTime, nullable=True)
|
|
last_sync_count = Column(Integer, default=0)
|
|
last_error = Column(Text, nullable=True)
|