# Fantasy Game Core Patterns — Backend (FastAPI + SQLite) ## Database schema ### users - id (int PK), email (unique), phone, display_name, password_hash, created_at ### leagues - id (int PK), name, invite_code (unique 8-char), status (draft/active/closed), season_start (date), season_end (date/null), created_by (FK users), created_at ### league_members - id (int PK), league_id (FK), user_id (FK), draft_order (int), is_commissioner (bool) - Unique: (league_id, user_id) ### regions - id (int PK), name, emoji, description, prev_year_sightings (int), prev_year_bites (int), prev_year_fatalities (int) ### draft_picks - id (int PK), league_id (FK), user_id (FK), region_id (FK), round (int), pick_number (int), created_at - Unique: (league_id, region_id) — no duplicates ### scores - id (int PK), region_id (FK), event_type (sighting/bite/fatality), description, source_url, points (int: 2/5/10), event_date (date), verified (bool default false), created_at ### user_scores (materialized) - user_id (FK), league_id (FK), total_points (int) ### league_excluded_regions - id (int PK), league_id (FK), region_id (FK) - Unique: (league_id, region_id) — a region can only be excluded once per league - Used when 32 % member_count > 0 at draft start: lowest-performing regions are removed so remaining pool is evenly divisible ## Points mapping sighting=2, bite=5, fatality=10 ## Snake draft implementation ```python members = sorted(league_members, key=lambda m: m['draft_order']) for round_num in range(1, total_rounds + 1): if round_num % 2 == 0: order = reversed(members) # Reverse order on even rounds else: order = members for member in order: # member picks ``` ## Turn enforcement ```python current_pick_count = len(draft_picks) current_turn_order = (current_pick_count % member_count) + 1 current_picker = member with draft_order == current_turn_order ``` ## Score recalculation (on event) ```python INSERT OR REPLACE INTO user_scores (user_id, league_id, total_points) SELECT dp.user_id, dp.league_id, COALESCE(SUM(s.points), 0) FROM draft_picks dp LEFT JOIN scores s ON s.region_id = dp.region_id AND s.verified = 1 WHERE dp.league_id = ? GROUP BY dp.user_id ``` ## Excluded region filtering When querying available regions for the draft board, filter against BOTH draft_picks AND league_excluded_regions: ```python excluded_rows = conn.execute( "SELECT region_id FROM league_excluded_regions WHERE league_id = ?", (league_id,) ).fetchall() excluded_ids = {r["region_id"] for r in excluded_rows} forbidden = drafted_ids | excluded_ids ``` ## Exclusion pick rejection ```python excluded = conn.execute( "SELECT id FROM league_excluded_regions WHERE league_id = ? AND region_id = ?", (league_id, req.region_id), ).fetchone() if excluded: raise HTTPException(status_code=400, detail="This region has been removed from the draft pool") ``` ## Performance score for exclusion ordering ```sql ORDER BY (prev_year_sightings + prev_year_bites * 5 + prev_year_fatalities * 10) ASC, id ASC ``` Lower scores = worse performing regions = first to be removed. ## League invite code generation ```python def generate_invite_code(): return secrets.token_hex(4).upper()[:8] ``` ## Region seeding Use `INSERT OR IGNORE` so repeated runs don't error. Seed at least 20+ regions for meaningful draft depth.