brackt/app/lib/bracket-templates.ts

383 lines
9.8 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;
}
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;
}
/**
* 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
},
],
};
/**
* 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
},
],
};
/**
* 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,
};
/**
* 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)
*
* 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;
}