/** * 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; } 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 1–16 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[]; } /** All seed numbers in a standard 16-team regional bracket (1–16) */ 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 }, ], }; /** * 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): * [0–15] East seeds 1–16 * [16–30] South seeds 1–15 * [31–45] West seeds 1–10, 12–16 * [46–59] Midwest seeds 1–10, 12–15 * [60–61] South 16-seed play-in (2 teams) * [62–63] West 11-seed play-in (2 teams) * [64–65] Midwest 11-seed play-in (2 teams) * [66–67] 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 1–15 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 1–10 and 12–16 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 1–10 and 12–15 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, }, { name: "Finals", matchCount: 1, feedsInto: null, isScoring: true, }, ], }; /** * All available bracket templates */ export const BRACKET_TEMPLATES: Record = { 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, }; /** * 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(); for (const m of matches) { roundMatchCounts.set(m.round, (roundMatchCounts.get(m.round) || 0) + 1); } 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; }