* 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>
639 lines
18 KiB
TypeScript
639 lines
18 KiB
TypeScript
/**
|
||
* 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<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,
|
||
};
|
||
|
||
/**
|
||
* 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);
|
||
}
|
||
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;
|
||
}
|