brackt/app/lib/bracket-templates.ts

858 lines
24 KiB
TypeScript
Raw Normal View History

/**
* Bracket Template System
*
* Defines pre-configured tournament bracket structures for various sports.
* Templates specify rounds, match counts, and which rounds contribute to fantasy scoring.
*/
export interface BracketRound {
/** Display name for this round (e.g., "First Four", "Elite Eight") */
name: string;
/** Number of matches in this round */
matchCount: number;
/** Name of the round that winners advance to (null for championship) */
feedsInto: string | null;
/** Whether this round affects fantasy points (false = 0 points per Q20) */
isScoring: boolean;
Add FIFA World Cup 2026 support with group stage display and Monte Carlo simulator (#242) * Add FIFA World Cup 2026 support with group stage display and Monte Carlo simulator, fixes #127 - New `groupStageMatches` table for recording group play results (W/D/L, scores, matchday, schedule) - `computeGroupStandings()` model function: pts → GD → GF → name tiebreaker ordering - `GroupStageStandings` component showing all 12 groups with standings table and manager column - Admin bracket UI: group match score entry, per-group standings, "Recalculate Floors" action - `WorldCupSimulator`: 50k Monte Carlo covering group stage + best-8 3rd-place + knockout + 3rd place game - Fuzzy name matching for national team Elo lookup (exact → substring → word-overlap), warns on miss - Partial group completion: completed matches replayed with real scores, remaining matches simulated - Elo priority: admin-entered sourceElo > futures odds converted to Elo > hardcoded national team ratings - `fifa_48` bracket template: added Third Place Game round with `loserFeedsInto` on Semifinals - Scoring rules: distinct 3rd/4th place for `fifa_48` (not averaged), QF losers share 5th–8th equally - Floor scoring: SF participants guaranteed 4th (provisional), finalized after 3rd place game - `recalculate-floors` admin action deletes and replays all results from scratch (fixes stale guard bug) - Unique index on `(tournamentGroupId, participant1Id, participant2Id)` to prevent duplicate pairings - Batch `findMatchesByGroupIds()` replacing N sequential queries in the sport season loader - League home mini-standings now shows `actualPoints` (includes floor) instead of `totalPoints` only - Elo ratings admin page supports World Cup (same bulk-import flow as snooker) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix TypeScript errors: update GroupStandingData type to use findMatchesByGroupIds Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Increase Node heap to 4GB for unit tests in CI to prevent OOM Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix OOM in CI: make WorldCupSimulator simulation count configurable for tests Tests now pass numSimulations=500 instead of the production default of 50,000. Six simulator tests × 50k iterations each was exhausting the 4GB heap on GitHub Actions runners. Also reduce simGroupMatch stat tests from 50k to 5k iterations. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 10:27:47 -07:00
/**
* Name of the round that *losers* advance to (e.g., "Third Place Game" for SF losers).
* When set, the loser of each match in this round is placed into the target round.
*/
loserFeedsInto?: string | null;
}
export interface GroupStageConfig {
/** Number of groups */
groupCount: number;
/** Teams per group */
teamsPerGroup: number;
/** Group labels (e.g., ["A", "B", ..., "L"]) */
groupLabels: string[];
}
/**
* A play-in game within a bracket region (e.g., NCAA First Four).
* Two teams compete; the winner fills a specific seed slot in the main bracket.
*/
export interface BracketPlayIn {
/** Which seed slot the winner fills (e.g., 11 or 16) */
seedSlot: number;
/** Number of teams in this play-in game (always 2) */
teams: 2;
}
/**
* A named region within a bracket (e.g., NCAA "East", "South").
* Each region contributes exactly 16 teams to the Round of 64.
*/
export interface BracketRegion {
/** Display name (e.g., "East", "South") */
name: string;
/**
* Seed numbers that enter the region directly (no play-in).
* Must be ascending and contain every seed 116 except those covered by playIns.
* Length = 16 - playIns.length.
*/
directSeeds: number[];
/**
* Play-in games for this region. Order here determines the order their teams
* appear in the participant array (after all direct-seed teams across all regions).
*/
playIns: BracketPlayIn[];
}
export interface BracketTemplate {
/** Unique identifier for this template */
id: string;
/** Human-readable name */
name: string;
/** Total number of teams in the bracket */
totalTeams: number;
/** Ordered list of rounds from earliest to championship */
rounds: BracketRound[];
/** Round name where fantasy scoring begins */
scoringStartsAtRound: string;
/** Optional group stage configuration (e.g., FIFA World Cup) */
groupStage?: GroupStageConfig;
/** Number of teams advancing to knockout stage (when groupStage is defined) */
knockoutTeams?: number;
/**
* Optional named region config. When present, the participant array for bracket
* generation is structured as: all direct seeds region-by-region in order, then
* all play-in teams grouped by region (and play-in order within each region).
*/
regions?: BracketRegion[];
/**
* Optional human-readable labels for each participant slot (0-indexed).
* When provided, the admin bracket UI shows these labels instead of "Seed N".
* Length must equal totalTeams.
*/
participantLabels?: string[];
}
/** All seed numbers in a standard 16-team regional bracket (116) */
export const ALL_16_SEEDS: number[] = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16];
// Standard seeding order for a 16-team regional bracket
// Each entry is [higherSeed, lowerSeed] (lower number = better seed)
export const STANDARD_BRACKET_SEEDING: [number, number][] = [
[1, 16], [8, 9], [5, 12], [4, 13], [6, 11], [3, 14], [7, 10], [2, 15],
];
/**
* Returns the 0-indexed position within the standard 8-match bracket order
* for a given seed slot. Used to compute which Round of 64 match a play-in
* winner advances to.
*
* Standard order: 1v16, 8v9, 5v12, 4v13, 6v11, 3v14, 7v10, 2v15
*/
export function matchIndexForSeedSlot(seedSlot: number): number {
for (let i = 0; i < STANDARD_BRACKET_SEEDING.length; i++) {
if (STANDARD_BRACKET_SEEDING[i].includes(seedSlot)) return i;
}
throw new Error(`Seed slot ${seedSlot} not found in standard bracket seeding`);
}
export interface NCAA68SlotMap {
/** Start index in participantIds for each region's direct seeds */
directOffsets: number[];
/** Ordered list of all play-ins, with participant index info and region context */
playInOffsets: Array<{
regionIndex: number;
playInIndex: number;
/** Index of the first team in this play-in within participantIds */
startIndex: number;
seedSlot: number;
}>;
totalDirect: number;
totalPlayIn: number;
}
/**
* Computes the participant array slot mapping for an NCAA-style bracket with regions.
*
* Participant array layout:
* [region 0 direct seeds] [region 1 direct seeds] ... [region N direct seeds]
* [region 0 play-in 0 team A, team B] [region 0 play-in 1 team A, team B] ...
* [region 1 play-in 0 team A, team B] ...
*
* This layout is shared between the server (bracket generation) and the client
* (form rendering) so both sides agree on which participant{i} maps where.
*/
export function buildNCAA68SlotMap(regions: BracketRegion[]): NCAA68SlotMap {
let cursor = 0;
const directOffsets: number[] = [];
for (const region of regions) {
directOffsets.push(cursor);
cursor += region.directSeeds.length;
}
const totalDirect = cursor;
const playInOffsets: NCAA68SlotMap["playInOffsets"] = [];
for (let r = 0; r < regions.length; r++) {
for (let p = 0; p < regions[r].playIns.length; p++) {
playInOffsets.push({
regionIndex: r,
playInIndex: p,
startIndex: cursor,
seedSlot: regions[r].playIns[p].seedSlot,
});
cursor += 2;
}
}
const totalPlayIn = cursor - totalDirect;
return { directOffsets, playInOffsets, totalDirect, totalPlayIn };
}
/**
* Simple 4-team bracket
* Semifinals Finals
* All rounds score fantasy points
*/
export const SIMPLE_4: BracketTemplate = {
id: "simple_4",
name: "Simple 4-Team Bracket",
totalTeams: 4,
scoringStartsAtRound: "Semifinals",
rounds: [
{
name: "Semifinals",
matchCount: 2,
feedsInto: "Finals",
isScoring: true, // Losers share 3rd-4th
},
{
name: "Finals",
matchCount: 1,
feedsInto: null,
isScoring: true, // Winner 1st, Loser 2nd
},
],
};
/**
* Simple 8-team bracket
* Quarterfinals Semifinals Finals
* All rounds score fantasy points
*/
export const SIMPLE_8: BracketTemplate = {
id: "simple_8",
name: "Simple 8-Team Bracket",
totalTeams: 8,
scoringStartsAtRound: "Quarterfinals",
rounds: [
{
name: "Quarterfinals",
matchCount: 4,
feedsInto: "Semifinals",
isScoring: true, // Losers share 5th-8th
},
{
name: "Semifinals",
matchCount: 2,
feedsInto: "Finals",
isScoring: true, // Losers share 3rd-4th
},
{
name: "Finals",
matchCount: 1,
feedsInto: null,
isScoring: true, // Winner 1st, Loser 2nd
},
],
};
/**
* Simple 16-team bracket
* Round of 16 Quarterfinals Semifinals Finals
* Only Quarterfinals and beyond score points (top 8)
*/
export const SIMPLE_16: BracketTemplate = {
id: "simple_16",
name: "Simple 16-Team Bracket",
totalTeams: 16,
scoringStartsAtRound: "Quarterfinals",
rounds: [
{
name: "Round of 16",
matchCount: 8,
feedsInto: "Quarterfinals",
isScoring: false, // Early elimination = 0 points
},
{
name: "Quarterfinals",
matchCount: 4,
feedsInto: "Semifinals",
isScoring: true, // Losers share 5th-8th
},
{
name: "Semifinals",
matchCount: 2,
feedsInto: "Finals",
isScoring: true, // Losers share 3rd-4th
},
{
name: "Finals",
matchCount: 1,
feedsInto: null,
isScoring: true, // Winner 1st, Loser 2nd
},
],
};
/**
* Simple 32-team bracket
* Round of 32 Round of 16 Quarterfinals Semifinals Finals
* Only Quarterfinals and beyond score points (top 8)
*/
export const SIMPLE_32: BracketTemplate = {
id: "simple_32",
name: "Simple 32-Team Bracket",
totalTeams: 32,
scoringStartsAtRound: "Quarterfinals",
rounds: [
{
name: "Round of 32",
matchCount: 16,
feedsInto: "Round of 16",
isScoring: false, // Early elimination = 0 points
},
{
name: "Round of 16",
matchCount: 8,
feedsInto: "Quarterfinals",
isScoring: false, // Early elimination = 0 points
},
{
name: "Quarterfinals",
matchCount: 4,
feedsInto: "Semifinals",
isScoring: true, // Losers share 5th-8th
},
{
name: "Semifinals",
matchCount: 2,
feedsInto: "Finals",
isScoring: true, // Losers share 3rd-4th
},
{
name: "Finals",
matchCount: 1,
feedsInto: null,
isScoring: true, // Winner 1st, Loser 2nd
},
],
};
Add PDC World Darts Championship simulator with Elo-based bracket (#248) Fixes #123 * Add PDC World Darts Championship simulator (128-player bracket) - New DartsSimulator: 128-player single-elimination, 7 rounds with PDC-accurate best-of-sets formats (bo3/bo5/bo5/bo7/bo7/bo11/bo13). ELO_DIVISOR=500 via set-level Bernoulli model. Two simulation paths: Path A (bracket drawn) simulates from actual DB matches; Path B (pre-bracket) seeds top 32 by world ranking in fixed positions and randomly draws the remaining 96 per simulation run (50,000 iterations). - darts_bracket added to simulatorTypeEnum and simulator registry. - world_ranking nullable integer column added to participant_expected_values (migration 0067); batchSaveSourceElos now accepts and persists it. - Admin route /admin/sports-seasons/:id/darts-elo: bulk import with format "Player Name, 2099, 1" (name, Elo, optional world ranking), fuzzy name matching, auto-runs simulation and updates EV/snapshots on save. - DARTS_128 bracket template added (scoring starts at Quarterfinals). - 30 unit tests: math helpers, bracket seeding structure, Path A/B integration. https://claude.ai/code/session_01WZ6FjMmC2eBeZ1564M9Ruz * Review fixes: pre-compute hot-loop invariants, import normalizeName - Pre-compute seededSlots and getSeededMatchOrder(32) before the 50k simulation loop — was being recomputed every iteration - Pad unseeded pool to 96 once before the loop instead of inside it - Inline bracket-building in the hot loop using pre-computed seededSlots; removes the per-iteration call to buildR1Bracket/getSeededMatchOrder - Remove dead SEEDED_R1_PAIRS constant (was never referenced) - Import normalizeName from ~/lib/fuzzy-match instead of redefining it locally https://claude.ai/code/session_01WZ6FjMmC2eBeZ1564M9Ruz * Fix lint failures: unused vars, eqeqeq, toSorted - Remove unused seededSet/unseededSet variables in test - Replace != null with !== null && !== undefined (eqeqeq rule) - Replace .sort() with .toSorted() in simulator and route (unicorn/no-array-sort) https://claude.ai/code/session_01WZ6FjMmC2eBeZ1564M9Ruz * Fix TS2552: restore seededSet declaration removed during lint fix https://claude.ai/code/session_01WZ6FjMmC2eBeZ1564M9Ruz --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-03-31 15:37:28 -07:00
/**
* PDC World Darts Championship (128 players)
* R1 (128) R2 (64) R3 (32) R4 (16) Quarterfinals Semifinals Final
* Only Quarterfinals and beyond score points.
*
* Match formats:
* R1: best-of-3 sets (first to 2)
* R2: best-of-5 sets (first to 3)
* R3: best-of-5 sets (first to 3)
* R4: best-of-7 sets (first to 4)
* QF: best-of-7 sets (first to 4)
* SF: best-of-11 sets (first to 6)
* Final: best-of-13 sets (first to 7)
*
* Seeding: top 32 seeds placed in fixed positions; remaining 96 randomly drawn.
*/
export const DARTS_128: BracketTemplate = {
id: "darts_128",
name: "PDC World Darts Championship (128 Players)",
totalTeams: 128,
scoringStartsAtRound: "Quarterfinals",
rounds: [
{
name: "Round 1",
matchCount: 64,
feedsInto: "Round 2",
isScoring: false,
},
{
name: "Round 2",
matchCount: 32,
feedsInto: "Round 3",
isScoring: false,
},
{
name: "Round 3",
matchCount: 16,
feedsInto: "Round 4",
isScoring: false,
},
{
name: "Round 4",
matchCount: 8,
feedsInto: "Quarterfinals",
isScoring: false,
},
{
name: "Quarterfinals",
matchCount: 4,
feedsInto: "Semi-Finals",
isScoring: true, // QF losers share 5th8th
},
{
name: "Semi-Finals",
matchCount: 2,
feedsInto: "Final",
isScoring: true, // SF losers share 3rd4th
},
{
name: "Final",
matchCount: 1,
feedsInto: null,
isScoring: true, // Winner 1st, Loser 2nd
},
],
};
/**
* NCAA March Madness (68 teams)
* First Four (play-in) Round of 64 Round of 32 Sweet Sixteen Elite Eight Final Four Championship
* Only Elite Eight and beyond score points per Q18
*
* 2026 region config:
* East 16 direct seeds, no play-ins
* South 15 direct seeds, 16-seed play-in
* West 15 direct seeds, 11-seed play-in
* Midwest 14 direct seeds, 11-seed play-in + 16-seed play-in
*
* Participant array layout (68 slots):
* [015] East seeds 116
* [1630] South seeds 115
* [3145] West seeds 110, 1216
* [4659] Midwest seeds 110, 1215
* [6061] South 16-seed play-in (2 teams)
* [6263] West 11-seed play-in (2 teams)
* [6465] Midwest 11-seed play-in (2 teams)
* [6667] Midwest 16-seed play-in (2 teams)
*/
export const NCAA_68: BracketTemplate = {
id: "ncaa_68",
name: "NCAA March Madness (68 teams)",
totalTeams: 68,
scoringStartsAtRound: "Elite Eight",
rounds: [
{
name: "First Four",
matchCount: 4,
feedsInto: "Round of 64",
isScoring: false, // Play-in games don't score
},
{
name: "Round of 64",
matchCount: 32,
feedsInto: "Round of 32",
isScoring: false,
},
{
name: "Round of 32",
matchCount: 16,
feedsInto: "Sweet Sixteen",
isScoring: false,
},
{
name: "Sweet Sixteen",
matchCount: 8,
feedsInto: "Elite Eight",
isScoring: false,
},
{
name: "Elite Eight",
matchCount: 4,
feedsInto: "Final Four",
isScoring: true, // Losers share 5th-8th
},
{
name: "Final Four",
matchCount: 2,
feedsInto: "Championship",
isScoring: true, // Losers share 3rd-4th
},
{
name: "Championship",
matchCount: 1,
feedsInto: null,
isScoring: true, // Winner 1st, Loser 2nd
},
],
regions: [
{
// East: all 16 seeds enter directly — no play-in games
name: "East",
directSeeds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
playIns: [],
},
{
// South: seeds 115 direct; 16-seed determined by First Four play-in
name: "South",
directSeeds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
playIns: [{ seedSlot: 16, teams: 2 }],
},
{
// West: seeds 110 and 1216 direct; 11-seed determined by First Four play-in
name: "West",
directSeeds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 13, 14, 15, 16],
playIns: [{ seedSlot: 11, teams: 2 }],
},
{
// Midwest: seeds 110 and 1215 direct; both 11-seed and 16-seed via play-ins
name: "Midwest",
directSeeds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 13, 14, 15],
playIns: [
{ seedSlot: 11, teams: 2 },
{ seedSlot: 16, teams: 2 },
],
},
],
};
/**
* NFL Playoffs (14 teams)
* Wild Card Divisional Conference Championship Super Bowl
* Top 2 seeds get byes (skip Wild Card)
* Scoring starts at Divisional (top 8)
*/
export const NFL_14: BracketTemplate = {
id: "nfl_14",
name: "NFL Playoffs (14 teams)",
totalTeams: 14,
scoringStartsAtRound: "Divisional",
rounds: [
{
name: "Wild Card",
matchCount: 6,
feedsInto: "Divisional",
isScoring: false, // 12 teams play, 2 have byes
},
{
name: "Divisional",
matchCount: 4,
feedsInto: "Conference Championship",
isScoring: true, // Quarterfinals - Losers share 5th-8th
},
{
name: "Conference Championship",
matchCount: 2,
feedsInto: "Super Bowl",
isScoring: true, // Semifinals - Losers share 3rd-4th
},
{
name: "Super Bowl",
matchCount: 1,
feedsInto: null,
isScoring: true, // Finals - Winner 1st, Loser 2nd
},
],
};
/**
* AFL Finals (10 teams with Wildcard Round from 2026)
* Complex bracket with double-chance system for top 6 teams
*
* Structure:
* - Wildcard Round: 7v10, 8v9 (losers eliminated with 0 points)
* - Week 1 Finals:
* - Qualifying Finals: 1v4, 2v3 (losers get second chance)
* - Elimination Finals: 5v8(wildcard winner), 6v7(wildcard winner) (losers share 7th-8th)
* - Week 2: Semi-Finals (QF losers vs EF winners, losers share 5th-6th)
* - Week 3: Preliminary Finals (QF winners vs SF winners, losers share 3rd-4th)
* - Week 4: Grand Final (1st vs 2nd)
*
* Note: This is NOT a simple single-elimination tree - it has double-chance paths
*/
export const AFL_10: BracketTemplate = {
id: "afl_10",
name: "AFL Finals (10 teams with Wildcard)",
totalTeams: 10,
scoringStartsAtRound: "Elimination Finals",
rounds: [
{
name: "Wildcard Round",
matchCount: 2,
feedsInto: "Elimination Finals",
isScoring: false, // Losers get 0 points (9th-10th)
},
{
name: "Qualifying Finals",
matchCount: 2,
feedsInto: "Preliminary Finals", // Winners get bye
isScoring: false, // Losers get second chance (go to Semi-Finals)
},
{
name: "Elimination Finals",
matchCount: 2,
feedsInto: "Semi-Finals",
isScoring: true, // Losers share 7th-8th
},
{
name: "Semi-Finals",
matchCount: 2,
feedsInto: "Preliminary Finals",
isScoring: true, // Losers share 5th-6th
},
{
name: "Preliminary Finals",
matchCount: 2,
feedsInto: "Grand Final",
isScoring: true, // Losers share 3rd-4th
},
{
name: "Grand Final",
matchCount: 1,
feedsInto: null,
isScoring: true, // Winner 1st, Loser 2nd
},
],
};
/**
* FIFA World Cup 48-team tournament (2026+)
* Group stage: 12 groups of 4 teams play round-robin
* Knockout stage: 32 teams advance to single-elimination bracket
*
* Group-to-knockout advancement is fully manual.
* Admin marks teams as eliminated from groups, then assigns 32 advancing
* teams into knockout bracket slots.
*
* Only knockout stage awards fantasy points.
* Teams eliminated in groups get finalPosition = 0.
*/
export const FIFA_48: BracketTemplate = {
id: "fifa_48",
name: "FIFA World Cup (48 teams)",
totalTeams: 48,
knockoutTeams: 32,
scoringStartsAtRound: "Quarterfinals",
groupStage: {
groupCount: 12,
teamsPerGroup: 4,
groupLabels: ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L"],
},
rounds: [
{
name: "Round of 32",
matchCount: 16,
feedsInto: "Round of 16",
isScoring: false,
},
{
name: "Round of 16",
matchCount: 8,
feedsInto: "Quarterfinals",
isScoring: false,
},
{
name: "Quarterfinals",
matchCount: 4,
feedsInto: "Semifinals",
isScoring: true,
},
{
name: "Semifinals",
matchCount: 2,
feedsInto: "Finals",
isScoring: true,
Add FIFA World Cup 2026 support with group stage display and Monte Carlo simulator (#242) * Add FIFA World Cup 2026 support with group stage display and Monte Carlo simulator, fixes #127 - New `groupStageMatches` table for recording group play results (W/D/L, scores, matchday, schedule) - `computeGroupStandings()` model function: pts → GD → GF → name tiebreaker ordering - `GroupStageStandings` component showing all 12 groups with standings table and manager column - Admin bracket UI: group match score entry, per-group standings, "Recalculate Floors" action - `WorldCupSimulator`: 50k Monte Carlo covering group stage + best-8 3rd-place + knockout + 3rd place game - Fuzzy name matching for national team Elo lookup (exact → substring → word-overlap), warns on miss - Partial group completion: completed matches replayed with real scores, remaining matches simulated - Elo priority: admin-entered sourceElo > futures odds converted to Elo > hardcoded national team ratings - `fifa_48` bracket template: added Third Place Game round with `loserFeedsInto` on Semifinals - Scoring rules: distinct 3rd/4th place for `fifa_48` (not averaged), QF losers share 5th–8th equally - Floor scoring: SF participants guaranteed 4th (provisional), finalized after 3rd place game - `recalculate-floors` admin action deletes and replays all results from scratch (fixes stale guard bug) - Unique index on `(tournamentGroupId, participant1Id, participant2Id)` to prevent duplicate pairings - Batch `findMatchesByGroupIds()` replacing N sequential queries in the sport season loader - League home mini-standings now shows `actualPoints` (includes floor) instead of `totalPoints` only - Elo ratings admin page supports World Cup (same bulk-import flow as snooker) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix TypeScript errors: update GroupStandingData type to use findMatchesByGroupIds Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Increase Node heap to 4GB for unit tests in CI to prevent OOM Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix OOM in CI: make WorldCupSimulator simulation count configurable for tests Tests now pass numSimulations=500 instead of the production default of 50,000. Six simulator tests × 50k iterations each was exhausting the 4GB heap on GitHub Actions runners. Also reduce simGroupMatch stat tests from 50k to 5k iterations. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 10:27:47 -07:00
loserFeedsInto: "Third Place Game",
},
{
name: "Third Place Game",
matchCount: 1,
feedsInto: null,
isScoring: true,
},
{
name: "Finals",
matchCount: 1,
feedsInto: null,
isScoring: true,
},
],
};
Add NCAA Football CFP simulator (12-team bracket) (#278) Fixes #124 * Add NCAA Football CFP simulator (12-team bracket) Implements a Monte Carlo simulator for the College Football Playoff using the 2024-present 12-team format. Elo/FPI ratings are entered manually via the existing admin Elo Ratings page; championship futures odds can optionally be blended in (60% Elo / 40% odds). - Add CFP_12 bracket template (First Round not scoring, QFs onward score) - Add generateCFP12Bracket() with correct seeding: 5v12, 6v11, 7v10, 8v9 in First Round; seeds 1–4 receive QF byes - Add NCAAFootballSimulator: 50k Monte Carlo sims, seeds teams by blended Elo+odds strength, tracks champion/finalist/SF/QF placement tiers - Register ncaa_football_bracket simulator type in registry and schema enum - Add migration 0071: ALTER TYPE simulator_type ADD VALUE 'ncaa_football_bracket' - Add tests: 30 tests covering bracket template structure and simulator probability distributions, seeding, edge cases, futures blending Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix lint errors in NCAA Football CFP simulator Replace non-null assertions with optional chaining, change let to const, use toSorted() instead of sort(), and add a bump() helper to avoid repeated map lookups with non-null assertions in simulateBracket. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix TS2345 in NCAA football simulator test Add ?? 0 fallback so optional-chained probFirst is number, not number | undefined. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 09:48:32 -04:00
/**
* College Football Playoff (12 teams, 2024present format)
* First Round: 5v12, 6v11, 7v10, 8v9 (campus sites, no fantasy points)
* Quarterfinals: Seeds 14 (bye) vs first-round winners (scoring starts here)
* Semifinals: 4 2
* National Championship: 1 game
*/
export const CFP_12: BracketTemplate = {
id: "cfp_12",
name: "College Football Playoff (12 teams)",
totalTeams: 12,
scoringStartsAtRound: "Quarterfinals",
rounds: [
{
name: "First Round",
matchCount: 4,
feedsInto: "Quarterfinals",
isScoring: false, // 8 seeds play, top 4 have byes
},
{
name: "Quarterfinals",
matchCount: 4,
feedsInto: "Semifinals",
isScoring: true, // Losers share 5th8th
},
{
name: "Semifinals",
matchCount: 2,
feedsInto: "National Championship",
isScoring: true, // Losers share 3rd4th
},
{
name: "National Championship",
matchCount: 1,
feedsInto: null,
isScoring: true, // Winner 1st, loser 2nd
},
],
};
/**
* NBA Playoffs with Play-In Tournament (20 teams)
*
* Structure:
* Play-In Round 1 (4 games, not scoring):
* East: 7 vs 8 (winner = E7 seed), 9 vs 10 (winner advances; loser eliminated)
* West: 7 vs 8 (winner = W7 seed), 9 vs 10 (winner advances; loser eliminated)
* Play-In Round 2 (2 games, not scoring):
* East: loser of 7/8 vs winner of 9/10 winner = E8 seed; loser eliminated
* West: loser of 7/8 vs winner of 9/10 winner = W8 seed; loser eliminated
* First Round (8 series, not scoring):
* East: 1v8, 4v5, 2v7, 3v6
* West: 1v8, 4v5, 2v7, 3v6
* Conference Semifinals (4 series, scoring starts 5th-8th for losers)
* Conference Finals (2 series, scoring)
* NBA Finals (1 series, scoring)
*
* Participant array layout (20 slots, indices 0-19):
* [09] East seeds 110
* [1019] West seeds 110
*
* Play-in loser paths (custom advancement logic):
* PIR1 M1 (E7v8): winner First Round M3 p1; loser PIR2 M1 p1
* PIR1 M2 (E9v10): winner PIR2 M1 p2; loser eliminated
* PIR1 M3 (W7v8): winner First Round M7 p1; loser PIR2 M2 p1
* PIR1 M4 (W9v10): winner PIR2 M2 p2; loser eliminated
* PIR2 M1: winner First Round M1 p2; loser eliminated
* PIR2 M2: winner First Round M5 p2; loser eliminated
*
* First Round bracket order (enables correct Conference Semis matchups via standard ceil logic):
* M1: E1 vs E8 (play-in winner) CS M1 p1
* M2: E4 vs E5 CS M1 p2
* M3: E2 vs E7 (play-in winner) CS M2 p1
* M4: E3 vs E6 CS M2 p2
* M5: W1 vs W8 (play-in winner) CS M3 p1
* M6: W4 vs W5 CS M3 p2
* M7: W2 vs W7 (play-in winner) CS M4 p1
* M8: W3 vs W6 CS M4 p2
*/
export const NBA_20: BracketTemplate = {
id: "nba_20",
name: "NBA Playoffs with Play-In (20 teams)",
totalTeams: 20,
scoringStartsAtRound: "Conference Semifinals",
rounds: [
{
name: "Play-In Round 1",
matchCount: 4,
feedsInto: "Play-In Round 2",
isScoring: false, // Play-in games don't score
},
{
name: "Play-In Round 2",
matchCount: 2,
feedsInto: "First Round",
isScoring: false, // Still determining playoff field
},
{
name: "First Round",
matchCount: 8,
feedsInto: "Conference Semifinals",
isScoring: false, // First Round losers score 0 points
},
{
name: "Conference Semifinals",
matchCount: 4,
feedsInto: "Conference Finals",
isScoring: true, // Losers share 5th-8th
},
{
name: "Conference Finals",
matchCount: 2,
feedsInto: "NBA Finals",
isScoring: true, // Losers share 3rd-4th
},
{
name: "NBA Finals",
matchCount: 1,
feedsInto: null,
isScoring: true, // Winner 1st, Loser 2nd
},
],
participantLabels: [
"East 1", "East 2", "East 3", "East 4", "East 5",
"East 6", "East 7", "East 8", "East 9", "East 10",
"West 1", "West 2", "West 3", "West 4", "West 5",
"West 6", "West 7", "West 8", "West 9", "West 10",
],
};
/**
* All available bracket templates
*/
export const BRACKET_TEMPLATES: Record<string, BracketTemplate> = {
simple_4: SIMPLE_4,
simple_8: SIMPLE_8,
simple_16: SIMPLE_16,
simple_32: SIMPLE_32,
ncaa_68: NCAA_68,
nfl_14: NFL_14,
afl_10: AFL_10,
fifa_48: FIFA_48,
Add PDC World Darts Championship simulator with Elo-based bracket (#248) Fixes #123 * Add PDC World Darts Championship simulator (128-player bracket) - New DartsSimulator: 128-player single-elimination, 7 rounds with PDC-accurate best-of-sets formats (bo3/bo5/bo5/bo7/bo7/bo11/bo13). ELO_DIVISOR=500 via set-level Bernoulli model. Two simulation paths: Path A (bracket drawn) simulates from actual DB matches; Path B (pre-bracket) seeds top 32 by world ranking in fixed positions and randomly draws the remaining 96 per simulation run (50,000 iterations). - darts_bracket added to simulatorTypeEnum and simulator registry. - world_ranking nullable integer column added to participant_expected_values (migration 0067); batchSaveSourceElos now accepts and persists it. - Admin route /admin/sports-seasons/:id/darts-elo: bulk import with format "Player Name, 2099, 1" (name, Elo, optional world ranking), fuzzy name matching, auto-runs simulation and updates EV/snapshots on save. - DARTS_128 bracket template added (scoring starts at Quarterfinals). - 30 unit tests: math helpers, bracket seeding structure, Path A/B integration. https://claude.ai/code/session_01WZ6FjMmC2eBeZ1564M9Ruz * Review fixes: pre-compute hot-loop invariants, import normalizeName - Pre-compute seededSlots and getSeededMatchOrder(32) before the 50k simulation loop — was being recomputed every iteration - Pad unseeded pool to 96 once before the loop instead of inside it - Inline bracket-building in the hot loop using pre-computed seededSlots; removes the per-iteration call to buildR1Bracket/getSeededMatchOrder - Remove dead SEEDED_R1_PAIRS constant (was never referenced) - Import normalizeName from ~/lib/fuzzy-match instead of redefining it locally https://claude.ai/code/session_01WZ6FjMmC2eBeZ1564M9Ruz * Fix lint failures: unused vars, eqeqeq, toSorted - Remove unused seededSet/unseededSet variables in test - Replace != null with !== null && !== undefined (eqeqeq rule) - Replace .sort() with .toSorted() in simulator and route (unicorn/no-array-sort) https://claude.ai/code/session_01WZ6FjMmC2eBeZ1564M9Ruz * Fix TS2552: restore seededSet declaration removed during lint fix https://claude.ai/code/session_01WZ6FjMmC2eBeZ1564M9Ruz --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-03-31 15:37:28 -07:00
darts_128: DARTS_128,
Add NCAA Football CFP simulator (12-team bracket) (#278) Fixes #124 * Add NCAA Football CFP simulator (12-team bracket) Implements a Monte Carlo simulator for the College Football Playoff using the 2024-present 12-team format. Elo/FPI ratings are entered manually via the existing admin Elo Ratings page; championship futures odds can optionally be blended in (60% Elo / 40% odds). - Add CFP_12 bracket template (First Round not scoring, QFs onward score) - Add generateCFP12Bracket() with correct seeding: 5v12, 6v11, 7v10, 8v9 in First Round; seeds 1–4 receive QF byes - Add NCAAFootballSimulator: 50k Monte Carlo sims, seeds teams by blended Elo+odds strength, tracks champion/finalist/SF/QF placement tiers - Register ncaa_football_bracket simulator type in registry and schema enum - Add migration 0071: ALTER TYPE simulator_type ADD VALUE 'ncaa_football_bracket' - Add tests: 30 tests covering bracket template structure and simulator probability distributions, seeding, edge cases, futures blending Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix lint errors in NCAA Football CFP simulator Replace non-null assertions with optional chaining, change let to const, use toSorted() instead of sort(), and add a bump() helper to avoid repeated map lookups with non-null assertions in simulateBracket. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix TS2345 in NCAA football simulator test Add ?? 0 fallback so optional-chained probFirst is number, not number | undefined. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 09:48:32 -04:00
cfp_12: CFP_12,
nba_20: NBA_20,
};
/**
* Get a bracket template by ID
*/
export function getBracketTemplate(id: string): BracketTemplate | undefined {
return BRACKET_TEMPLATES[id];
}
/**
* Get all bracket templates as an array
*/
export function getAllBracketTemplates(): BracketTemplate[] {
return Object.values(BRACKET_TEMPLATES);
}
/**
* Returns round names in chronological order (earliest first) for the given matches.
* When a template is provided its defined order is used; otherwise rounds are sorted
* by descending match count (more matches = earlier round).
*/
export function getOrderedRoundsFromMatches(
matches: Array<{ round: string }>,
template?: BracketTemplate
): string[] {
const roundsInMatches = new Set(matches.map((m) => m.round));
if (template) {
return template.rounds
.map((r) => r.name)
.filter((name) => roundsInMatches.has(name));
}
const roundMatchCounts = new Map<string, number>();
for (const m of matches) {
roundMatchCounts.set(m.round, (roundMatchCounts.get(m.round) || 0) + 1);
}
Add oxlint linting setup with zero errors (#194) * Add oxlint and fix all lint errors - Install oxlint, add .oxlintrc.json with rules for TypeScript/React - Add npm run lint / lint:fix scripts - Add Claude PostToolUse hook to run oxlint on every edited file - Fix 101 errors: unused vars/imports, eqeqeq, prefer-const, no-new-array - Fix no-array-index-key (use stable keys or suppress positional cases) - Fix exhaustive-deps missing dependency in useEffect - Promote exhaustive-deps and no-array-index-key to errors - Fix Map.get() !== null bug in $leagueId.server.ts (should be !== undefined) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix no-explicit-any warnings and upgrade tsconfig to ES2023 - Replace all `any` types with proper types or `unknown` across ~20 files - Add typed socket payload interfaces in draft route and useDraftSocket - Use any[] with eslint-disable for socket.io callbacks (legitimate escape hatch) - Bump all tsconfigs from ES2022 → ES2023 to support toSorted/toReversed - Fix cascading type errors uncovered by removing any: Map.get narrowing, participant relation types, ChartDataPoint, Partial<NewSeason> indexing - Add ParticipantResultWithParticipant type to participant-result model - Fix test fixtures to match updated interfaces (DraftCell, ParticipantResult) - Fix duplicate getQPStandings import in sportsSeasonId.server.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Promote no-explicit-any to error Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 09:44:05 -07:00
return Array.from(roundMatchCounts.keys()).toSorted(
(a, b) => (roundMatchCounts.get(b) || 0) - (roundMatchCounts.get(a) || 0)
);
}
/**
* Helper to determine scoring round type based on match count in scoring rounds
* Used for determining placement sharing (5-8th, 3-4th, 1-2nd)
*
* Special handling for AFL finals system which has specific placement rules
*/
export function getScoringRoundType(
roundName: string,
template: BracketTemplate
): "quarterfinals" | "semifinals" | "finals" | null {
const round = template.rounds.find((r) => r.name === roundName);
if (!round || !round.isScoring) return null;
// Special handling for AFL finals
if (template.id === "afl_10") {
if (roundName === "Elimination Finals") return "quarterfinals"; // Losers share 7-8th
if (roundName === "Semi-Finals") return "quarterfinals"; // Losers share 5-6th (custom mapping)
if (roundName === "Preliminary Finals") return "semifinals"; // Losers share 3-4th
if (roundName === "Grand Final") return "finals"; // 1st and 2nd
return null;
}
// Standard logic for other templates
// Determine type by match count in scoring round
if (round.matchCount === 4) return "quarterfinals"; // 8 teams, losers share 5-8th
if (round.matchCount === 2) return "semifinals"; // 4 teams, losers share 3-4th
if (round.matchCount === 1) return "finals"; // 2 teams, 1st and 2nd
return null;
}