Closes #75. New file app/services/evolution_init.py: - _determine_card_type(player): pure fn mapping pos_1 to 'batter'/'sp'/'rp' - initialize_card_evolution(player_id, team_id, card_type): get_or_create EvolutionCardState with current_tier=0, current_value=0.0, fully_evolved=False - Safe failure: all exceptions caught and logged, never raises - Idempotent: duplicate calls for same (player_id, team_id) are no-ops and do NOT reset existing evolution progress Modified app/routers_v2/cards.py: - Add WP-10 hook after Card.bulk_create in the POST endpoint - For each card posted, call _determine_card_type + initialize_card_evolution - Wrapped in try/except so evolution failures cannot block pack opening - Fix pre-existing lint violations (unused lc_id, bare f-string, unused e) New file tests/test_evolution_init.py (16 tests, all passing): - Unit: track assignment for batter / SP / RP / CP positions - Integration: first card creates state with zeroed fields - Integration: duplicate card is a no-op (progress not reset) - Integration: different players on same team get separate states - Integration: card_type routes to correct EvolutionTrack - Integration: missing track returns None gracefully Fix tests/test_evolution_models.py: correct PlayerSeasonStats import/usage Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
242 lines
9.6 KiB
PL/PgSQL
242 lines
9.6 KiB
PL/PgSQL
-- Migration: Add card evolution tables and column extensions
|
|
-- Date: 2026-03-17
|
|
-- Issue: WP-04
|
|
-- Purpose: Support the Card Evolution system — creates batting_season_stats
|
|
-- and pitching_season_stats for per-player stat accumulation, plus
|
|
-- evolution tracks with tier thresholds, per-card evolution state,
|
|
-- tier-based stat boosts, and cosmetic unlocks. Also extends the
|
|
-- card, battingcard, and pitchingcard tables with variant and
|
|
-- image_url columns required by the evolution display layer.
|
|
--
|
|
-- Run on dev first, verify with:
|
|
-- SELECT count(*) FROM batting_season_stats;
|
|
-- SELECT count(*) FROM pitching_season_stats;
|
|
-- SELECT count(*) FROM evolution_track;
|
|
-- SELECT count(*) FROM evolution_card_state;
|
|
-- SELECT count(*) FROM evolution_tier_boost;
|
|
-- SELECT count(*) FROM evolution_cosmetic;
|
|
-- SELECT column_name FROM information_schema.columns
|
|
-- WHERE table_name IN ('card', 'battingcard', 'pitchingcard')
|
|
-- AND column_name IN ('variant', 'image_url')
|
|
-- ORDER BY table_name, column_name;
|
|
--
|
|
-- Rollback: See DROP/ALTER statements at bottom of file
|
|
|
|
-- ============================================
|
|
-- FORWARD MIGRATION
|
|
-- ============================================
|
|
|
|
BEGIN;
|
|
|
|
-- --------------------------------------------
|
|
-- Table 1: batting_season_stats
|
|
-- Accumulates per-player per-team per-season
|
|
-- batting totals for evolution formula evaluation
|
|
-- and leaderboard queries.
|
|
-- --------------------------------------------
|
|
CREATE TABLE IF NOT EXISTS batting_season_stats (
|
|
id SERIAL PRIMARY KEY,
|
|
player_id INTEGER NOT NULL REFERENCES player(player_id) ON DELETE CASCADE,
|
|
team_id INTEGER NOT NULL REFERENCES team(id) ON DELETE CASCADE,
|
|
season INTEGER NOT NULL,
|
|
games INTEGER NOT NULL DEFAULT 0,
|
|
pa INTEGER NOT NULL DEFAULT 0,
|
|
ab INTEGER NOT NULL DEFAULT 0,
|
|
hits INTEGER NOT NULL DEFAULT 0,
|
|
doubles INTEGER NOT NULL DEFAULT 0,
|
|
triples INTEGER NOT NULL DEFAULT 0,
|
|
hr INTEGER NOT NULL DEFAULT 0,
|
|
rbi INTEGER NOT NULL DEFAULT 0,
|
|
runs INTEGER NOT NULL DEFAULT 0,
|
|
bb INTEGER NOT NULL DEFAULT 0,
|
|
strikeouts INTEGER NOT NULL DEFAULT 0,
|
|
hbp INTEGER NOT NULL DEFAULT 0,
|
|
sac INTEGER NOT NULL DEFAULT 0,
|
|
ibb INTEGER NOT NULL DEFAULT 0,
|
|
gidp INTEGER NOT NULL DEFAULT 0,
|
|
sb INTEGER NOT NULL DEFAULT 0,
|
|
cs INTEGER NOT NULL DEFAULT 0,
|
|
last_game_id INTEGER REFERENCES stratgame(id) ON DELETE SET NULL,
|
|
last_updated_at TIMESTAMP
|
|
);
|
|
|
|
-- One row per player per team per season
|
|
CREATE UNIQUE INDEX IF NOT EXISTS batting_season_stats_player_team_season_uniq
|
|
ON batting_season_stats (player_id, team_id, season);
|
|
|
|
-- Fast lookup by team + season (e.g. leaderboard queries)
|
|
CREATE INDEX IF NOT EXISTS batting_season_stats_team_season_idx
|
|
ON batting_season_stats (team_id, season);
|
|
|
|
-- Fast lookup by player across seasons
|
|
CREATE INDEX IF NOT EXISTS batting_season_stats_player_season_idx
|
|
ON batting_season_stats (player_id, season);
|
|
|
|
-- --------------------------------------------
|
|
-- Table 2: pitching_season_stats
|
|
-- Accumulates per-player per-team per-season
|
|
-- pitching totals for evolution formula evaluation
|
|
-- and leaderboard queries.
|
|
-- --------------------------------------------
|
|
CREATE TABLE IF NOT EXISTS pitching_season_stats (
|
|
id SERIAL PRIMARY KEY,
|
|
player_id INTEGER NOT NULL REFERENCES player(player_id) ON DELETE CASCADE,
|
|
team_id INTEGER NOT NULL REFERENCES team(id) ON DELETE CASCADE,
|
|
season INTEGER NOT NULL,
|
|
games INTEGER NOT NULL DEFAULT 0,
|
|
games_started INTEGER NOT NULL DEFAULT 0,
|
|
outs INTEGER NOT NULL DEFAULT 0,
|
|
strikeouts INTEGER NOT NULL DEFAULT 0,
|
|
bb INTEGER NOT NULL DEFAULT 0,
|
|
hits_allowed INTEGER NOT NULL DEFAULT 0,
|
|
runs_allowed INTEGER NOT NULL DEFAULT 0,
|
|
earned_runs INTEGER NOT NULL DEFAULT 0,
|
|
hr_allowed INTEGER NOT NULL DEFAULT 0,
|
|
hbp INTEGER NOT NULL DEFAULT 0,
|
|
wild_pitches INTEGER NOT NULL DEFAULT 0,
|
|
balks INTEGER NOT NULL DEFAULT 0,
|
|
wins INTEGER NOT NULL DEFAULT 0,
|
|
losses INTEGER NOT NULL DEFAULT 0,
|
|
holds INTEGER NOT NULL DEFAULT 0,
|
|
saves INTEGER NOT NULL DEFAULT 0,
|
|
blown_saves INTEGER NOT NULL DEFAULT 0,
|
|
last_game_id INTEGER REFERENCES stratgame(id) ON DELETE SET NULL,
|
|
last_updated_at TIMESTAMP
|
|
);
|
|
|
|
-- One row per player per team per season
|
|
CREATE UNIQUE INDEX IF NOT EXISTS pitching_season_stats_player_team_season_uniq
|
|
ON pitching_season_stats (player_id, team_id, season);
|
|
|
|
-- Fast lookup by team + season (e.g. leaderboard queries)
|
|
CREATE INDEX IF NOT EXISTS pitching_season_stats_team_season_idx
|
|
ON pitching_season_stats (team_id, season);
|
|
|
|
-- Fast lookup by player across seasons
|
|
CREATE INDEX IF NOT EXISTS pitching_season_stats_player_season_idx
|
|
ON pitching_season_stats (player_id, season);
|
|
|
|
-- --------------------------------------------
|
|
-- Table 3: evolution_track
|
|
-- Defines the available evolution tracks
|
|
-- (e.g. "HR Mastery", "Ace SP"), their
|
|
-- metric formula, and the four tier thresholds.
|
|
-- --------------------------------------------
|
|
CREATE TABLE IF NOT EXISTS evolution_track (
|
|
id SERIAL PRIMARY KEY,
|
|
name VARCHAR(255) UNIQUE NOT NULL,
|
|
card_type VARCHAR(50) NOT NULL, -- 'batter', 'sp', or 'rp'
|
|
formula VARCHAR(255) NOT NULL, -- e.g. 'hr', 'k_per_9', 'ops'
|
|
t1_threshold INTEGER NOT NULL,
|
|
t2_threshold INTEGER NOT NULL,
|
|
t3_threshold INTEGER NOT NULL,
|
|
t4_threshold INTEGER NOT NULL
|
|
);
|
|
|
|
-- --------------------------------------------
|
|
-- Table 4: evolution_card_state
|
|
-- Records each card's current evolution tier,
|
|
-- running metric value, and the track it
|
|
-- belongs to. One state row per card (player
|
|
-- + team combination uniquely identifies a
|
|
-- card in a given season).
|
|
-- --------------------------------------------
|
|
CREATE TABLE IF NOT EXISTS evolution_card_state (
|
|
id SERIAL PRIMARY KEY,
|
|
player_id INTEGER NOT NULL REFERENCES player(player_id) ON DELETE CASCADE,
|
|
team_id INTEGER NOT NULL REFERENCES team(id) ON DELETE CASCADE,
|
|
track_id INTEGER NOT NULL REFERENCES evolution_track(id) ON DELETE CASCADE,
|
|
current_tier INTEGER NOT NULL DEFAULT 0,
|
|
current_value DOUBLE PRECISION NOT NULL DEFAULT 0.0,
|
|
fully_evolved BOOLEAN NOT NULL DEFAULT FALSE,
|
|
last_evaluated_at TIMESTAMP
|
|
);
|
|
|
|
-- One evolution state per card (player + team)
|
|
CREATE UNIQUE INDEX IF NOT EXISTS evolution_card_state_player_team_uniq
|
|
ON evolution_card_state (player_id, team_id);
|
|
|
|
-- --------------------------------------------
|
|
-- Table 5: evolution_tier_boost
|
|
-- Defines the stat boosts unlocked at each
|
|
-- tier within a track. A single tier may
|
|
-- grant multiple boosts (e.g. +1 HR and
|
|
-- +1 power rating).
|
|
-- --------------------------------------------
|
|
CREATE TABLE IF NOT EXISTS evolution_tier_boost (
|
|
id SERIAL PRIMARY KEY,
|
|
track_id INTEGER NOT NULL REFERENCES evolution_track(id) ON DELETE CASCADE,
|
|
tier INTEGER NOT NULL, -- 1-4
|
|
boost_type VARCHAR(50) NOT NULL, -- e.g. 'rating_bump', 'display_only'
|
|
boost_target VARCHAR(50) NOT NULL, -- e.g. 'hr_rating', 'contact_rating'
|
|
boost_value DOUBLE PRECISION NOT NULL DEFAULT 0.0
|
|
);
|
|
|
|
-- Prevent duplicate boost definitions for the same track/tier/type/target
|
|
CREATE UNIQUE INDEX IF NOT EXISTS evolution_tier_boost_track_tier_type_target_uniq
|
|
ON evolution_tier_boost (track_id, tier, boost_type, boost_target);
|
|
|
|
-- --------------------------------------------
|
|
-- Table 6: evolution_cosmetic
|
|
-- Catalogue of unlockable visual treatments
|
|
-- (borders, foils, badges, etc.) tied to
|
|
-- minimum tier requirements.
|
|
-- --------------------------------------------
|
|
CREATE TABLE IF NOT EXISTS evolution_cosmetic (
|
|
id SERIAL PRIMARY KEY,
|
|
name VARCHAR(255) UNIQUE NOT NULL,
|
|
tier_required INTEGER NOT NULL DEFAULT 0,
|
|
cosmetic_type VARCHAR(50) NOT NULL, -- e.g. 'border', 'foil', 'badge'
|
|
css_class VARCHAR(255),
|
|
asset_url VARCHAR(500)
|
|
);
|
|
|
|
-- --------------------------------------------
|
|
-- Column extensions for existing tables
|
|
-- --------------------------------------------
|
|
|
|
-- Track which visual variant a card is displaying
|
|
-- (NULL = base card, 1+ = evolved variants)
|
|
ALTER TABLE card ADD COLUMN IF NOT EXISTS variant INTEGER DEFAULT NULL;
|
|
|
|
-- Store pre-rendered or externally-hosted card image URLs
|
|
ALTER TABLE battingcard ADD COLUMN IF NOT EXISTS image_url VARCHAR(500);
|
|
ALTER TABLE pitchingcard ADD COLUMN IF NOT EXISTS image_url VARCHAR(500);
|
|
|
|
COMMIT;
|
|
|
|
-- ============================================
|
|
-- VERIFICATION QUERIES
|
|
-- ============================================
|
|
-- \d batting_season_stats
|
|
-- \d pitching_season_stats
|
|
-- \d evolution_track
|
|
-- \d evolution_card_state
|
|
-- \d evolution_tier_boost
|
|
-- \d evolution_cosmetic
|
|
-- SELECT indexname FROM pg_indexes
|
|
-- WHERE tablename IN (
|
|
-- 'batting_season_stats',
|
|
-- 'pitching_season_stats',
|
|
-- 'evolution_card_state',
|
|
-- 'evolution_tier_boost'
|
|
-- )
|
|
-- ORDER BY tablename, indexname;
|
|
-- SELECT column_name, data_type FROM information_schema.columns
|
|
-- WHERE table_name IN ('card', 'battingcard', 'pitchingcard')
|
|
-- AND column_name IN ('variant', 'image_url')
|
|
-- ORDER BY table_name, column_name;
|
|
|
|
-- ============================================
|
|
-- ROLLBACK (if needed)
|
|
-- ============================================
|
|
-- ALTER TABLE pitchingcard DROP COLUMN IF EXISTS image_url;
|
|
-- ALTER TABLE battingcard DROP COLUMN IF EXISTS image_url;
|
|
-- ALTER TABLE card DROP COLUMN IF EXISTS variant;
|
|
-- DROP TABLE IF EXISTS evolution_cosmetic CASCADE;
|
|
-- DROP TABLE IF EXISTS evolution_tier_boost CASCADE;
|
|
-- DROP TABLE IF EXISTS evolution_card_state CASCADE;
|
|
-- DROP TABLE IF EXISTS evolution_track CASCADE;
|
|
-- DROP TABLE IF EXISTS pitching_season_stats CASCADE;
|
|
-- DROP TABLE IF EXISTS batting_season_stats CASCADE;
|