2025-11-03 13:16:37 -08:00
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-14 22:30:12 -08:00
|
|
|
export interface GroupStageConfig {
|
|
|
|
|
/** Number of groups */
|
|
|
|
|
groupCount: number;
|
|
|
|
|
/** Teams per group */
|
|
|
|
|
teamsPerGroup: number;
|
|
|
|
|
/** Group labels (e.g., ["A", "B", ..., "L"]) */
|
|
|
|
|
groupLabels: string[];
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-03 13:16:37 -08:00
|
|
|
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;
|
2026-02-14 22:30:12 -08:00
|
|
|
/** Optional group stage configuration (e.g., FIFA World Cup) */
|
|
|
|
|
groupStage?: GroupStageConfig;
|
|
|
|
|
/** Number of teams advancing to knockout stage (when groupStage is defined) */
|
|
|
|
|
knockoutTeams?: number;
|
2025-11-03 13:16:37 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
*/
|
|
|
|
|
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
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-12 23:21:22 -08:00
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-14 22:30:12 -08:00
|
|
|
/**
|
|
|
|
|
* 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,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-03 13:16:37 -08:00
|
|
|
/**
|
|
|
|
|
* 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,
|
2025-11-12 23:21:22 -08:00
|
|
|
afl_10: AFL_10,
|
2026-02-14 22:30:12 -08:00
|
|
|
fifa_48: FIFA_48,
|
2025-11-03 13:16:37 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Helper to determine scoring round type based on match count in scoring rounds
|
|
|
|
|
* Used for determining placement sharing (5-8th, 3-4th, 1-2nd)
|
2025-11-12 23:21:22 -08:00
|
|
|
*
|
|
|
|
|
* Special handling for AFL finals system which has specific placement rules
|
2025-11-03 13:16:37 -08:00
|
|
|
*/
|
|
|
|
|
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;
|
|
|
|
|
|
2025-11-12 23:21:22 -08:00
|
|
|
// 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
|
2025-11-03 13:16:37 -08:00
|
|
|
// 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;
|
|
|
|
|
}
|