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;
|
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;
|
Add LLWS 20-team double-elimination bracket
The Little League Baseball World Series runs two independent 10-team
double-elimination brackets — United States and International — each
producing a side champion, then a World Championship game and a
Consolation Third Place game between the side runners-up. 38 games in
all. No existing template could express it: every one is single
elimination, at most with a bolted-on third-place game.
Adds the llws_20 template plus dedicated generation and advancement,
following the same bespoke-routing pattern afl_10 and nba_20 use rather
than the generic ceil(matchNumber / 2) advancement.
The core of the change is loser routing. In the winners bracket a loss
is not an elimination — it drops the team into the elimination bracket
at a specific slot, including the deliberate cross-overs the official
bracket uses (Elimination Round 1 pairs L4/L6 and L2/L8; Elimination
Round 3 pairs each semifinal loser with the winner from the opposite
half). In the elimination bracket a loss is final. Matching the official
modified double-elimination format, there is no "if necessary" game: the
winners-bracket champion is eliminated if it loses the side
championship, dropping to the consolation game.
Rounds are shared across both sides, U.S. taking the low match numbers
and International the high ones, so the scoring config stays one entry
per stage. The existing phases/groups display machinery splits them back
apart into United States / International / Championship tabs.
Scoring lands on exactly 8 point-earning teams, which is the field size
when Elimination Round 4 begins: the two finals decide 1st–4th,
Elimination Final losers take 5th–6th, and Elimination Round 4 losers
7th–8th. 3rd and 4th are distinct because the consolation game is real,
and 5–8 splits into two two-team tiers so surviving Elimination Round 4
is worth more than losing it.
Also:
- Adds an optional nonScoringWinnerFloor to BracketRound. The engine
hardcoded a 5th-place floor for winners of non-scoring rounds feeding
a scoring one, which is wrong inside a losers bracket where a win can
guarantee only 7th. Opt-in, so no existing template changes behavior.
- Fixes TabbedBracketLayout's mobile path, which built its match map
unfiltered and so would have merged U.S. and International games into
one column. No-op for NCAA and NBA, whose groups already cover every
match in their phases.
- Rewrites the LLWS Monte Carlo simulator, which still modelled the
retired pool-play format (5 teams per pool, then a 4-team bracket per
side) and no longer described the tournament being scored. It now runs
the real 10-team double elimination and splits the 5–8 probabilities
into the correct tiers instead of one even four-way split. Legacy
"US:A"/"Intl:B" externalIds are still accepted, read as the side
alone, so seasons configured for the old format keep loading.
Tests replay all 38 games through the pure advancement resolver and
assert each one against the feed labels printed on the official bracket.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EAxqBVzfmKJe6WQ6VF9guj
2026-08-03 18:06:56 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* Floor position banked by the WINNER of a *non-scoring* round.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Omit for the default behavior: winners entering the first scoring round bank a
|
|
|
|
|
|
* T5–T8 floor (position 5), everyone else banks nothing. Set an explicit number when
|
|
|
|
|
|
* that default is wrong — in a double-elimination losers bracket a win can guarantee
|
|
|
|
|
|
* a worse finish than 5th (llws_20 "Elimination Round 3" → 7). Set null to bank no
|
|
|
|
|
|
* floor even though the next round scores.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Has no effect on scoring rounds, which use RoundScoringConfig.winnerFloor instead.
|
|
|
|
|
|
*/
|
|
|
|
|
|
nonScoringWinnerFloor?: number | null;
|
2025-11-03 13:16:37 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
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[];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-15 21:52:47 -07:00
|
|
|
|
/**
|
|
|
|
|
|
* 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[];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
New design (#309)
* Redesign home page with new layout and component system
- Two-column layout (My Leagues 2/3, Upcoming Events 1/3) with mobile stack
- LeagueRow: square avatar, gradient draft highlight, rank/points display, progress bar
- MyLeaguesCard, CreateLeagueCard with shared SectionCardHeader
- UpcomingEventsCard: vertical timeline with grouped multi-league events
- Shared gradient system: BracktGradients SVG defs, GradientIcon wrapper, brand.ts constants
- Button default variant updated to green→cyan gradient
- Navbar: plain nav links with gradient hover, support/admin icon buttons
- Accessibility fixes: semantic h2 headings, aria-label on LeagueAvatar and nav elements
- Storybook stories for all new components
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Responsive league row layout and mobile polish
- League rows stack avatar+name on top, stats full-width below on mobile
- Stats spread to right side on sm+ screens with border separator on mobile
- Tighter padding on mobile (px-3/py-3), full padding on sm+
- Card headers and content use px-3 sm:px-6 to reduce mobile gutters
- Two-column home layout deferred to lg breakpoint (tablet gets stacked)
- Active leagues sorted by completion percentage descending
- Default rank 1 / 0 points for active leagues with no scoring events yet
- Fix ordinal bug for 11th/12th/13th; add aria-labels to rank change indicators
- Remove dead StatDivider className prop
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Improve claude file.
* Add StandingsPreview card component with podium row styling
- New StandingsPreview component with gold/silver/bronze row tints for
top 3, team avatar, and LeagueRow-style stat columns (Ranking + Points)
with rank and 7-day point change indicators
- Fix GradientIcon in Storybook by adding BracktGradients decorator to
preview.tsx (renamed from .ts to support JSX)
- Fix degenerate SVG gradient on horizontal strokes by switching
BracktGradients to gradientUnits="userSpaceOnUse" with Lucide-space
coordinates (0→24)
- Revert erroneous fill: url(#gradient) from GradientIcon; stroke-only
fix was sufficient once gradientUnits was corrected
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Update components on league homepage.
* Finish up league page styling.
* Work on standings page.
* Add story for RecentScoresCard
* Update Point Progression Chart.
* Sort point progression legend by ranking and add team links to standings rows
* Fix standings discrepancy on change.
* Create draft cell component.
* Update draft board page
* Draft room improvements.
* Update some draft room styling.
* Fix context menu missing.
* Move tab navigation and autodraft to header row, narrow sidebar
* Virtualize available participants list, memoize draft room props
Adds @tanstack/react-virtual to replace separate mobile/desktop lists
with a single unified virtual scroll loop. Also memoizes miniDraftGrid
and availableParticipantsSectionProps, and switches pick lookup from
Array.find to a Map for O(1) access.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Update draft room UI.
* More draft room fixes.
* Draft room tweaks.
* Fix Rosters page.
* Queue Section fixes.
* Mobile Draft fixes.
* Fix draft board page.
* Create bracket look.
* Bracket work.
* Finish bracket page.
* Homepage initial styling
* homepage copy
* Add privacy policy. Fixes #88.
* how to play copy
* rules copy
* Fix brackets on homepage.
* Add footer to website.
* Glow on dots.
* Landing page copy.
* Fix sidebar.
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 13:14:55 -07:00
|
|
|
|
/**
|
|
|
|
|
|
* Defines a named conference or sub-bracket group within a bracket.
|
|
|
|
|
|
* Used to split matches into East/West (NBA) or regional groups (NCAA) for display.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface ConferenceGroup {
|
|
|
|
|
|
/** Display name, e.g. "Eastern Conference" */
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Maps round name → the match numbers (1-based) that belong to this conference.
|
|
|
|
|
|
* Rounds not listed here are either shared (Finals) or not applicable.
|
|
|
|
|
|
*/
|
|
|
|
|
|
roundMatchNumbers: Record<string, number[]>;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* A named tab/section within a bracket display.
|
|
|
|
|
|
* Either shows a simple list of rounds, or a set of conference/regional sub-brackets.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface BracketPhase {
|
|
|
|
|
|
/** Tab label */
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
/** Simple ordered list of round names to show in this tab */
|
|
|
|
|
|
rounds?: string[];
|
|
|
|
|
|
/** Regional/conference sub-groups to show stacked in this tab */
|
|
|
|
|
|
groups?: ConferenceGroup[];
|
|
|
|
|
|
/** Rounds rendered after groups (e.g. Final Four, Championship) */
|
|
|
|
|
|
sharedRounds?: string[];
|
|
|
|
|
|
/** Custom rendering layout for special phases */
|
|
|
|
|
|
layout?: "play-in";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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;
|
2026-03-15 21:52:47 -07:00
|
|
|
|
/**
|
|
|
|
|
|
* 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[];
|
2026-04-13 00:51:53 -04:00
|
|
|
|
/**
|
|
|
|
|
|
* 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[];
|
New design (#309)
* Redesign home page with new layout and component system
- Two-column layout (My Leagues 2/3, Upcoming Events 1/3) with mobile stack
- LeagueRow: square avatar, gradient draft highlight, rank/points display, progress bar
- MyLeaguesCard, CreateLeagueCard with shared SectionCardHeader
- UpcomingEventsCard: vertical timeline with grouped multi-league events
- Shared gradient system: BracktGradients SVG defs, GradientIcon wrapper, brand.ts constants
- Button default variant updated to green→cyan gradient
- Navbar: plain nav links with gradient hover, support/admin icon buttons
- Accessibility fixes: semantic h2 headings, aria-label on LeagueAvatar and nav elements
- Storybook stories for all new components
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Responsive league row layout and mobile polish
- League rows stack avatar+name on top, stats full-width below on mobile
- Stats spread to right side on sm+ screens with border separator on mobile
- Tighter padding on mobile (px-3/py-3), full padding on sm+
- Card headers and content use px-3 sm:px-6 to reduce mobile gutters
- Two-column home layout deferred to lg breakpoint (tablet gets stacked)
- Active leagues sorted by completion percentage descending
- Default rank 1 / 0 points for active leagues with no scoring events yet
- Fix ordinal bug for 11th/12th/13th; add aria-labels to rank change indicators
- Remove dead StatDivider className prop
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Improve claude file.
* Add StandingsPreview card component with podium row styling
- New StandingsPreview component with gold/silver/bronze row tints for
top 3, team avatar, and LeagueRow-style stat columns (Ranking + Points)
with rank and 7-day point change indicators
- Fix GradientIcon in Storybook by adding BracktGradients decorator to
preview.tsx (renamed from .ts to support JSX)
- Fix degenerate SVG gradient on horizontal strokes by switching
BracktGradients to gradientUnits="userSpaceOnUse" with Lucide-space
coordinates (0→24)
- Revert erroneous fill: url(#gradient) from GradientIcon; stroke-only
fix was sufficient once gradientUnits was corrected
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Update components on league homepage.
* Finish up league page styling.
* Work on standings page.
* Add story for RecentScoresCard
* Update Point Progression Chart.
* Sort point progression legend by ranking and add team links to standings rows
* Fix standings discrepancy on change.
* Create draft cell component.
* Update draft board page
* Draft room improvements.
* Update some draft room styling.
* Fix context menu missing.
* Move tab navigation and autodraft to header row, narrow sidebar
* Virtualize available participants list, memoize draft room props
Adds @tanstack/react-virtual to replace separate mobile/desktop lists
with a single unified virtual scroll loop. Also memoizes miniDraftGrid
and availableParticipantsSectionProps, and switches pick lookup from
Array.find to a Map for O(1) access.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Update draft room UI.
* More draft room fixes.
* Draft room tweaks.
* Fix Rosters page.
* Queue Section fixes.
* Mobile Draft fixes.
* Fix draft board page.
* Create bracket look.
* Bracket work.
* Finish bracket page.
* Homepage initial styling
* homepage copy
* Add privacy policy. Fixes #88.
* how to play copy
* rules copy
* Fix brackets on homepage.
* Add footer to website.
* Glow on dots.
* Landing page copy.
* Fix sidebar.
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 13:14:55 -07:00
|
|
|
|
/**
|
|
|
|
|
|
* Optional conference/group split for brackets with parallel sub-brackets (NBA).
|
|
|
|
|
|
* When present, the UI renders each group as a separate sub-bracket.
|
|
|
|
|
|
* Rounds not listed in any group's roundMatchNumbers are treated as shared (e.g., Finals).
|
|
|
|
|
|
*/
|
|
|
|
|
|
conferenceGroups?: ConferenceGroup[];
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Optional tabbed phase display (NCAA).
|
|
|
|
|
|
* When present, the UI renders a tab switcher with each phase as a section.
|
|
|
|
|
|
*/
|
|
|
|
|
|
phases?: BracketPhase[];
|
2026-03-15 21:52:47 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 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 };
|
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
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-08 12:29:39 -07:00
|
|
|
|
/**
|
|
|
|
|
|
* NCAA Men's College Hockey Tournament (16 teams)
|
|
|
|
|
|
* Regional Semifinals → Regional Finals → Frozen Four Semifinals → National Championship
|
|
|
|
|
|
* Only Regional Finals and beyond score points (top 8).
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const COLLEGE_HOCKEY_16: BracketTemplate = {
|
|
|
|
|
|
id: "college_hockey_16",
|
|
|
|
|
|
name: "NCAA Men's Hockey Tournament (16 teams)",
|
|
|
|
|
|
totalTeams: 16,
|
|
|
|
|
|
scoringStartsAtRound: "Regional Finals",
|
|
|
|
|
|
rounds: [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "Regional Semifinals",
|
|
|
|
|
|
matchCount: 8,
|
|
|
|
|
|
feedsInto: "Regional Finals",
|
|
|
|
|
|
isScoring: false,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "Regional Finals",
|
|
|
|
|
|
matchCount: 4,
|
|
|
|
|
|
feedsInto: "Frozen Four Semifinals",
|
|
|
|
|
|
isScoring: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "Frozen Four Semifinals",
|
|
|
|
|
|
matchCount: 2,
|
|
|
|
|
|
feedsInto: "National Championship",
|
|
|
|
|
|
isScoring: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "National Championship",
|
|
|
|
|
|
matchCount: 1,
|
|
|
|
|
|
feedsInto: null,
|
|
|
|
|
|
isScoring: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
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 5th–8th
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "Semi-Finals",
|
|
|
|
|
|
matchCount: 2,
|
|
|
|
|
|
feedsInto: "Final",
|
|
|
|
|
|
isScoring: true, // SF losers share 3rd–4th
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "Final",
|
|
|
|
|
|
matchCount: 1,
|
|
|
|
|
|
feedsInto: null,
|
|
|
|
|
|
isScoring: true, // Winner 1st, Loser 2nd
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
Unify majors: score once, fan out across windows + tennis bracket EV
Make a "major" (golf/tennis/CS2) scored once on its canonical tournament
and fan out to every linked sports_season window and league.
Fan-out & completion (app/services/sync-tournament-results.ts):
- syncTournamentResults now marks each synced window's event complete
(gated by markComplete), recalculates affected leagues, and counts
recalc failures so a stale league can't hide behind a "completed" badge
- syncMajorFromPrimaryEvent promotes a primary window's derived results to
canonical tournament_results (deleting rows for dropped placements) and
fans out to siblings; fanOutMajorIfPrimary guards on the primary
- placement removals now propagate (stale rows reset to filler)
Primary-event model (scoring_events.is_primary, migration 0122):
- getPrimaryEventForTournament / isReadOnlySibling / ensurePrimaryEvent /
setPrimaryEvent; event creation auto-seeds a primary for bracket majors;
"Make primary" button on the tournament page
- per-window event/bracket/cs2 pages are read-only for non-primary linked
events (not-participating stays editable)
Tennis Grand Slam bracket (tennis_128 template + TEMPLATE_ROUND_CONFIG):
- bracket-scored qualifying major via the existing bracket pipeline
- simulator conditions in-progress EV on the real bracket (honoring
completed matches, walkover for withdrawals), QP derived from config,
round structure read from the template; CS2 + tennis share resolveStructureSource
Backfill (scripts/backfill-major-linking.ts): one-time idempotent reconcile
of existing majors (link orphans, designate primary, promote canonical, sync).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 20:32:22 -07:00
|
|
|
|
/**
|
|
|
|
|
|
* Tennis Grand Slam (128-player single-elimination draw)
|
|
|
|
|
|
* R128 → R64 → R32 → Round of 16 → Quarterfinals → Semifinals → Final
|
|
|
|
|
|
*
|
|
|
|
|
|
* Qualifying-points sport: QP is derived from how far each player advances
|
|
|
|
|
|
* (see TEMPLATE_ROUND_CONFIG["tennis_128"] in scoring-calculator.ts). Scoring
|
|
|
|
|
|
* begins at the Round of 16 — R16 losers share placements 9–16; earlier rounds
|
|
|
|
|
|
* award nothing. Seeding: top 32 fixed, remaining 96 randomly drawn.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const TENNIS_128: BracketTemplate = {
|
|
|
|
|
|
id: "tennis_128",
|
|
|
|
|
|
name: "Tennis Grand Slam (128 Players)",
|
|
|
|
|
|
totalTeams: 128,
|
|
|
|
|
|
scoringStartsAtRound: "Round of 16",
|
|
|
|
|
|
rounds: [
|
|
|
|
|
|
{ name: "Round of 128", matchCount: 64, feedsInto: "Round of 64", isScoring: false },
|
|
|
|
|
|
{ name: "Round of 64", matchCount: 32, feedsInto: "Round of 32", isScoring: false },
|
|
|
|
|
|
{ name: "Round of 32", matchCount: 16, feedsInto: "Round of 16", isScoring: false },
|
|
|
|
|
|
{ name: "Round of 16", matchCount: 8, feedsInto: "Quarterfinals", isScoring: true }, // losers share 9th–16th
|
|
|
|
|
|
{ name: "Quarterfinals", matchCount: 4, feedsInto: "Semifinals", isScoring: true }, // losers share 5th–8th
|
|
|
|
|
|
{ name: "Semifinals", matchCount: 2, feedsInto: "Final", isScoring: true }, // losers share 3rd–4th
|
|
|
|
|
|
{ name: "Final", matchCount: 1, feedsInto: null, isScoring: true }, // winner 1st, loser 2nd
|
|
|
|
|
|
],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-03 13:16:37 -08:00
|
|
|
|
/**
|
|
|
|
|
|
* 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-03-15 21:52:47 -07:00
|
|
|
|
*
|
New design (#309)
* Redesign home page with new layout and component system
- Two-column layout (My Leagues 2/3, Upcoming Events 1/3) with mobile stack
- LeagueRow: square avatar, gradient draft highlight, rank/points display, progress bar
- MyLeaguesCard, CreateLeagueCard with shared SectionCardHeader
- UpcomingEventsCard: vertical timeline with grouped multi-league events
- Shared gradient system: BracktGradients SVG defs, GradientIcon wrapper, brand.ts constants
- Button default variant updated to green→cyan gradient
- Navbar: plain nav links with gradient hover, support/admin icon buttons
- Accessibility fixes: semantic h2 headings, aria-label on LeagueAvatar and nav elements
- Storybook stories for all new components
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Responsive league row layout and mobile polish
- League rows stack avatar+name on top, stats full-width below on mobile
- Stats spread to right side on sm+ screens with border separator on mobile
- Tighter padding on mobile (px-3/py-3), full padding on sm+
- Card headers and content use px-3 sm:px-6 to reduce mobile gutters
- Two-column home layout deferred to lg breakpoint (tablet gets stacked)
- Active leagues sorted by completion percentage descending
- Default rank 1 / 0 points for active leagues with no scoring events yet
- Fix ordinal bug for 11th/12th/13th; add aria-labels to rank change indicators
- Remove dead StatDivider className prop
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Improve claude file.
* Add StandingsPreview card component with podium row styling
- New StandingsPreview component with gold/silver/bronze row tints for
top 3, team avatar, and LeagueRow-style stat columns (Ranking + Points)
with rank and 7-day point change indicators
- Fix GradientIcon in Storybook by adding BracktGradients decorator to
preview.tsx (renamed from .ts to support JSX)
- Fix degenerate SVG gradient on horizontal strokes by switching
BracktGradients to gradientUnits="userSpaceOnUse" with Lucide-space
coordinates (0→24)
- Revert erroneous fill: url(#gradient) from GradientIcon; stroke-only
fix was sufficient once gradientUnits was corrected
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Update components on league homepage.
* Finish up league page styling.
* Work on standings page.
* Add story for RecentScoresCard
* Update Point Progression Chart.
* Sort point progression legend by ranking and add team links to standings rows
* Fix standings discrepancy on change.
* Create draft cell component.
* Update draft board page
* Draft room improvements.
* Update some draft room styling.
* Fix context menu missing.
* Move tab navigation and autodraft to header row, narrow sidebar
* Virtualize available participants list, memoize draft room props
Adds @tanstack/react-virtual to replace separate mobile/desktop lists
with a single unified virtual scroll loop. Also memoizes miniDraftGrid
and availableParticipantsSectionProps, and switches pick lookup from
Array.find to a Map for O(1) access.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Update draft room UI.
* More draft room fixes.
* Draft room tweaks.
* Fix Rosters page.
* Queue Section fixes.
* Mobile Draft fixes.
* Fix draft board page.
* Create bracket look.
* Bracket work.
* Finish bracket page.
* Homepage initial styling
* homepage copy
* Add privacy policy. Fixes #88.
* how to play copy
* rules copy
* Fix brackets on homepage.
* Add footer to website.
* Glow on dots.
* Landing page copy.
* Fix sidebar.
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 13:14:55 -07:00
|
|
|
|
* Region config (year-specific — update each March when the First Four matchups are announced):
|
2026-03-15 21:52:47 -07:00
|
|
|
|
* 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)
|
2025-11-03 13:16:37 -08:00
|
|
|
|
*/
|
|
|
|
|
|
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
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
2026-03-15 21:52:47 -07:00
|
|
|
|
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 },
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
New design (#309)
* Redesign home page with new layout and component system
- Two-column layout (My Leagues 2/3, Upcoming Events 1/3) with mobile stack
- LeagueRow: square avatar, gradient draft highlight, rank/points display, progress bar
- MyLeaguesCard, CreateLeagueCard with shared SectionCardHeader
- UpcomingEventsCard: vertical timeline with grouped multi-league events
- Shared gradient system: BracktGradients SVG defs, GradientIcon wrapper, brand.ts constants
- Button default variant updated to green→cyan gradient
- Navbar: plain nav links with gradient hover, support/admin icon buttons
- Accessibility fixes: semantic h2 headings, aria-label on LeagueAvatar and nav elements
- Storybook stories for all new components
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Responsive league row layout and mobile polish
- League rows stack avatar+name on top, stats full-width below on mobile
- Stats spread to right side on sm+ screens with border separator on mobile
- Tighter padding on mobile (px-3/py-3), full padding on sm+
- Card headers and content use px-3 sm:px-6 to reduce mobile gutters
- Two-column home layout deferred to lg breakpoint (tablet gets stacked)
- Active leagues sorted by completion percentage descending
- Default rank 1 / 0 points for active leagues with no scoring events yet
- Fix ordinal bug for 11th/12th/13th; add aria-labels to rank change indicators
- Remove dead StatDivider className prop
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Improve claude file.
* Add StandingsPreview card component with podium row styling
- New StandingsPreview component with gold/silver/bronze row tints for
top 3, team avatar, and LeagueRow-style stat columns (Ranking + Points)
with rank and 7-day point change indicators
- Fix GradientIcon in Storybook by adding BracktGradients decorator to
preview.tsx (renamed from .ts to support JSX)
- Fix degenerate SVG gradient on horizontal strokes by switching
BracktGradients to gradientUnits="userSpaceOnUse" with Lucide-space
coordinates (0→24)
- Revert erroneous fill: url(#gradient) from GradientIcon; stroke-only
fix was sufficient once gradientUnits was corrected
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Update components on league homepage.
* Finish up league page styling.
* Work on standings page.
* Add story for RecentScoresCard
* Update Point Progression Chart.
* Sort point progression legend by ranking and add team links to standings rows
* Fix standings discrepancy on change.
* Create draft cell component.
* Update draft board page
* Draft room improvements.
* Update some draft room styling.
* Fix context menu missing.
* Move tab navigation and autodraft to header row, narrow sidebar
* Virtualize available participants list, memoize draft room props
Adds @tanstack/react-virtual to replace separate mobile/desktop lists
with a single unified virtual scroll loop. Also memoizes miniDraftGrid
and availableParticipantsSectionProps, and switches pick lookup from
Array.find to a Map for O(1) access.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Update draft room UI.
* More draft room fixes.
* Draft room tweaks.
* Fix Rosters page.
* Queue Section fixes.
* Mobile Draft fixes.
* Fix draft board page.
* Create bracket look.
* Bracket work.
* Finish bracket page.
* Homepage initial styling
* homepage copy
* Add privacy policy. Fixes #88.
* how to play copy
* rules copy
* Fix brackets on homepage.
* Add footer to website.
* Glow on dots.
* Landing page copy.
* Fix sidebar.
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 13:14:55 -07:00
|
|
|
|
// Two-tab display: First Four → Bracket (4 regional sub-brackets + Final Four)
|
|
|
|
|
|
phases: [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "First Four",
|
|
|
|
|
|
rounds: ["First Four"],
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "Bracket",
|
|
|
|
|
|
groups: [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "East Region",
|
|
|
|
|
|
roundMatchNumbers: {
|
|
|
|
|
|
"Round of 64": [1, 2, 3, 4, 5, 6, 7, 8],
|
|
|
|
|
|
"Round of 32": [1, 2, 3, 4],
|
|
|
|
|
|
"Sweet Sixteen": [1, 2],
|
|
|
|
|
|
"Elite Eight": [1],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "South Region",
|
|
|
|
|
|
roundMatchNumbers: {
|
|
|
|
|
|
"Round of 64": [9, 10, 11, 12, 13, 14, 15, 16],
|
|
|
|
|
|
"Round of 32": [5, 6, 7, 8],
|
|
|
|
|
|
"Sweet Sixteen": [3, 4],
|
|
|
|
|
|
"Elite Eight": [2],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "West Region",
|
|
|
|
|
|
roundMatchNumbers: {
|
|
|
|
|
|
"Round of 64": [17, 18, 19, 20, 21, 22, 23, 24],
|
|
|
|
|
|
"Round of 32": [9, 10, 11, 12],
|
|
|
|
|
|
"Sweet Sixteen": [5, 6],
|
|
|
|
|
|
"Elite Eight": [3],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "Midwest Region",
|
|
|
|
|
|
roundMatchNumbers: {
|
|
|
|
|
|
"Round of 64": [25, 26, 27, 28, 29, 30, 31, 32],
|
|
|
|
|
|
"Round of 32": [13, 14, 15, 16],
|
|
|
|
|
|
"Sweet Sixteen": [7, 8],
|
|
|
|
|
|
"Elite Eight": [4],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
sharedRounds: ["Final Four", "Championship"],
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
2025-11-03 13:16:37 -08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 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,
|
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,
|
2026-02-14 22:30:12 -08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
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, 2024–present format)
|
|
|
|
|
|
* First Round: 5v12, 6v11, 7v10, 8v9 (campus sites, no fantasy points)
|
|
|
|
|
|
* Quarterfinals: Seeds 1–4 (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 5th–8th
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "Semifinals",
|
|
|
|
|
|
matchCount: 2,
|
|
|
|
|
|
feedsInto: "National Championship",
|
|
|
|
|
|
isScoring: true, // Losers share 3rd–4th
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "National Championship",
|
|
|
|
|
|
matchCount: 1,
|
|
|
|
|
|
feedsInto: null,
|
|
|
|
|
|
isScoring: true, // Winner 1st, loser 2nd
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-13 00:51:53 -04:00
|
|
|
|
/**
|
|
|
|
|
|
* 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):
|
|
|
|
|
|
* [0–9] East seeds 1–10
|
|
|
|
|
|
* [10–19] West seeds 1–10
|
|
|
|
|
|
*
|
|
|
|
|
|
* 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",
|
|
|
|
|
|
],
|
New design (#309)
* Redesign home page with new layout and component system
- Two-column layout (My Leagues 2/3, Upcoming Events 1/3) with mobile stack
- LeagueRow: square avatar, gradient draft highlight, rank/points display, progress bar
- MyLeaguesCard, CreateLeagueCard with shared SectionCardHeader
- UpcomingEventsCard: vertical timeline with grouped multi-league events
- Shared gradient system: BracktGradients SVG defs, GradientIcon wrapper, brand.ts constants
- Button default variant updated to green→cyan gradient
- Navbar: plain nav links with gradient hover, support/admin icon buttons
- Accessibility fixes: semantic h2 headings, aria-label on LeagueAvatar and nav elements
- Storybook stories for all new components
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Responsive league row layout and mobile polish
- League rows stack avatar+name on top, stats full-width below on mobile
- Stats spread to right side on sm+ screens with border separator on mobile
- Tighter padding on mobile (px-3/py-3), full padding on sm+
- Card headers and content use px-3 sm:px-6 to reduce mobile gutters
- Two-column home layout deferred to lg breakpoint (tablet gets stacked)
- Active leagues sorted by completion percentage descending
- Default rank 1 / 0 points for active leagues with no scoring events yet
- Fix ordinal bug for 11th/12th/13th; add aria-labels to rank change indicators
- Remove dead StatDivider className prop
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Improve claude file.
* Add StandingsPreview card component with podium row styling
- New StandingsPreview component with gold/silver/bronze row tints for
top 3, team avatar, and LeagueRow-style stat columns (Ranking + Points)
with rank and 7-day point change indicators
- Fix GradientIcon in Storybook by adding BracktGradients decorator to
preview.tsx (renamed from .ts to support JSX)
- Fix degenerate SVG gradient on horizontal strokes by switching
BracktGradients to gradientUnits="userSpaceOnUse" with Lucide-space
coordinates (0→24)
- Revert erroneous fill: url(#gradient) from GradientIcon; stroke-only
fix was sufficient once gradientUnits was corrected
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Update components on league homepage.
* Finish up league page styling.
* Work on standings page.
* Add story for RecentScoresCard
* Update Point Progression Chart.
* Sort point progression legend by ranking and add team links to standings rows
* Fix standings discrepancy on change.
* Create draft cell component.
* Update draft board page
* Draft room improvements.
* Update some draft room styling.
* Fix context menu missing.
* Move tab navigation and autodraft to header row, narrow sidebar
* Virtualize available participants list, memoize draft room props
Adds @tanstack/react-virtual to replace separate mobile/desktop lists
with a single unified virtual scroll loop. Also memoizes miniDraftGrid
and availableParticipantsSectionProps, and switches pick lookup from
Array.find to a Map for O(1) access.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Update draft room UI.
* More draft room fixes.
* Draft room tweaks.
* Fix Rosters page.
* Queue Section fixes.
* Mobile Draft fixes.
* Fix draft board page.
* Create bracket look.
* Bracket work.
* Finish bracket page.
* Homepage initial styling
* homepage copy
* Add privacy policy. Fixes #88.
* how to play copy
* rules copy
* Fix brackets on homepage.
* Add footer to website.
* Glow on dots.
* Landing page copy.
* Fix sidebar.
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 13:14:55 -07:00
|
|
|
|
phases: [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "Play-In",
|
|
|
|
|
|
rounds: ["Play-In Round 1", "Play-In Round 2"],
|
|
|
|
|
|
layout: "play-in" as const,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "Playoffs",
|
|
|
|
|
|
rounds: ["First Round", "Conference Semifinals", "Conference Finals", "NBA Finals"],
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
2026-04-13 00:51:53 -04:00
|
|
|
|
};
|
|
|
|
|
|
|
Add LLWS 20-team double-elimination bracket
The Little League Baseball World Series runs two independent 10-team
double-elimination brackets — United States and International — each
producing a side champion, then a World Championship game and a
Consolation Third Place game between the side runners-up. 38 games in
all. No existing template could express it: every one is single
elimination, at most with a bolted-on third-place game.
Adds the llws_20 template plus dedicated generation and advancement,
following the same bespoke-routing pattern afl_10 and nba_20 use rather
than the generic ceil(matchNumber / 2) advancement.
The core of the change is loser routing. In the winners bracket a loss
is not an elimination — it drops the team into the elimination bracket
at a specific slot, including the deliberate cross-overs the official
bracket uses (Elimination Round 1 pairs L4/L6 and L2/L8; Elimination
Round 3 pairs each semifinal loser with the winner from the opposite
half). In the elimination bracket a loss is final. Matching the official
modified double-elimination format, there is no "if necessary" game: the
winners-bracket champion is eliminated if it loses the side
championship, dropping to the consolation game.
Rounds are shared across both sides, U.S. taking the low match numbers
and International the high ones, so the scoring config stays one entry
per stage. The existing phases/groups display machinery splits them back
apart into United States / International / Championship tabs.
Scoring lands on exactly 8 point-earning teams, which is the field size
when Elimination Round 4 begins: the two finals decide 1st–4th,
Elimination Final losers take 5th–6th, and Elimination Round 4 losers
7th–8th. 3rd and 4th are distinct because the consolation game is real,
and 5–8 splits into two two-team tiers so surviving Elimination Round 4
is worth more than losing it.
Also:
- Adds an optional nonScoringWinnerFloor to BracketRound. The engine
hardcoded a 5th-place floor for winners of non-scoring rounds feeding
a scoring one, which is wrong inside a losers bracket where a win can
guarantee only 7th. Opt-in, so no existing template changes behavior.
- Fixes TabbedBracketLayout's mobile path, which built its match map
unfiltered and so would have merged U.S. and International games into
one column. No-op for NCAA and NBA, whose groups already cover every
match in their phases.
- Rewrites the LLWS Monte Carlo simulator, which still modelled the
retired pool-play format (5 teams per pool, then a 4-team bracket per
side) and no longer described the tournament being scored. It now runs
the real 10-team double elimination and splits the 5–8 probabilities
into the correct tiers instead of one even four-way split. Legacy
"US:A"/"Intl:B" externalIds are still accepted, read as the side
alone, so seasons configured for the old format keep loading.
Tests replay all 38 games through the pure advancement resolver and
assert each one against the feed labels printed on the official bracket.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EAxqBVzfmKJe6WQ6VF9guj
2026-08-03 18:06:56 +00:00
|
|
|
|
// ── LLWS 20 ───────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/** Side-local match numbers → global match numbers, per round shape. */
|
|
|
|
|
|
const LLWS_OPENING_OFFSET = 4; // Opening Round: US M1–4, Intl M5–8
|
|
|
|
|
|
const LLWS_PAIR_OFFSET = 2; // 4-match rounds: US M1–2, Intl M3–4
|
|
|
|
|
|
const LLWS_SOLO_OFFSET = 1; // 2-match rounds: US M1, Intl M2
|
|
|
|
|
|
|
|
|
|
|
|
/** Rounds with 4 matches (2 per side). Opening Round has 8; the rest have 2. */
|
|
|
|
|
|
export const LLWS_FOUR_MATCH_ROUNDS = new Set([
|
|
|
|
|
|
"Winners Round 2",
|
|
|
|
|
|
"Elimination Round 1",
|
|
|
|
|
|
"Winners Semifinals",
|
|
|
|
|
|
"Elimination Round 2",
|
|
|
|
|
|
"Elimination Round 3",
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Returns the global match number for a side-local match in an LLWS round.
|
|
|
|
|
|
* side 0 = United States, side 1 = International.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function llwsMatchNumber(round: string, side: 0 | 1, localMatch: number): number {
|
|
|
|
|
|
const offset =
|
|
|
|
|
|
round === "Opening Round"
|
|
|
|
|
|
? LLWS_OPENING_OFFSET
|
|
|
|
|
|
: LLWS_FOUR_MATCH_ROUNDS.has(round)
|
|
|
|
|
|
? LLWS_PAIR_OFFSET
|
|
|
|
|
|
: LLWS_SOLO_OFFSET;
|
|
|
|
|
|
return localMatch + side * offset;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Inverse of llwsMatchNumber: global match number → { side, localMatch }.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function llwsSideAndLocal(
|
|
|
|
|
|
round: string,
|
|
|
|
|
|
matchNumber: number
|
|
|
|
|
|
): { side: 0 | 1; localMatch: number } {
|
|
|
|
|
|
const offset =
|
|
|
|
|
|
round === "Opening Round"
|
|
|
|
|
|
? LLWS_OPENING_OFFSET
|
|
|
|
|
|
: LLWS_FOUR_MATCH_ROUNDS.has(round)
|
|
|
|
|
|
? LLWS_PAIR_OFFSET
|
|
|
|
|
|
: LLWS_SOLO_OFFSET;
|
|
|
|
|
|
const side: 0 | 1 = matchNumber > offset ? 1 : 0;
|
|
|
|
|
|
return { side, localMatch: matchNumber - side * offset };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Little League Baseball World Series (20 teams, 2025+ double-elimination format)
|
|
|
|
|
|
*
|
|
|
|
|
|
* Two independent 10-team double-elimination brackets — United States and
|
|
|
|
|
|
* International — each producing a side champion, then a World Championship game and
|
|
|
|
|
|
* a Consolation Third Place game between the two side runners-up.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Rounds are shared across both sides: U.S. matches take the low match numbers and
|
|
|
|
|
|
* International the high ones (see llwsMatchNumber). The phases/groups config splits
|
|
|
|
|
|
* them back apart for display.
|
|
|
|
|
|
*
|
|
|
|
|
|
* A loss in the winners bracket is NOT an elimination — it drops the team into the
|
|
|
|
|
|
* elimination bracket at a specific slot (see advanceLLWSWinner in models/playoff-match).
|
|
|
|
|
|
* A loss in the elimination bracket is final.
|
|
|
|
|
|
*
|
|
|
|
|
|
* There is deliberately NO "if necessary" game: the winners-bracket champion is out if
|
|
|
|
|
|
* it loses the Bracket Championship, dropping to the Consolation game rather than
|
|
|
|
|
|
* forcing a rematch. This is the official LLWS modified double-elimination format.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Placement tiers (only 8 teams score — the field is exactly 8 when Elim R4 begins):
|
|
|
|
|
|
* 1st / 2nd World Championship
|
|
|
|
|
|
* 3rd / 4th Consolation Third Place (real game, so positions are distinct)
|
|
|
|
|
|
* 5th / 6th Elimination Final losers
|
|
|
|
|
|
* 7th / 8th Elimination Round 4 losers
|
|
|
|
|
|
* 0 pts the 12 teams eliminated in Elimination Rounds 1–3
|
|
|
|
|
|
*
|
|
|
|
|
|
* Participant array layout (20 slots):
|
|
|
|
|
|
* [0–7] U.S. Opening Round teams, two per game (M1..M4)
|
|
|
|
|
|
* [8, 9] U.S. bye teams, entering Winners Round 2 M1 / M2 at participant1
|
|
|
|
|
|
* [10–17] International Opening Round teams, two per game (M5..M8)
|
|
|
|
|
|
* [18,19] International bye teams, entering Winners Round 2 M3 / M4 at participant1
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const LLWS_20: BracketTemplate = {
|
|
|
|
|
|
id: "llws_20",
|
|
|
|
|
|
name: "Little League World Series (20 teams)",
|
|
|
|
|
|
totalTeams: 20,
|
|
|
|
|
|
scoringStartsAtRound: "Winners Final",
|
|
|
|
|
|
// Ordered by the real schedule so non-phased views read chronologically.
|
|
|
|
|
|
rounds: [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "Opening Round",
|
|
|
|
|
|
matchCount: 8,
|
|
|
|
|
|
feedsInto: "Winners Round 2",
|
|
|
|
|
|
isScoring: false,
|
|
|
|
|
|
loserFeedsInto: "Elimination Round 1",
|
|
|
|
|
|
nonScoringWinnerFloor: null, // 16 teams still alive — nothing guaranteed
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "Winners Round 2",
|
|
|
|
|
|
matchCount: 4,
|
|
|
|
|
|
feedsInto: "Winners Semifinals",
|
|
|
|
|
|
isScoring: false,
|
|
|
|
|
|
loserFeedsInto: "Elimination Round 2",
|
|
|
|
|
|
nonScoringWinnerFloor: null,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "Elimination Round 1",
|
|
|
|
|
|
matchCount: 4,
|
|
|
|
|
|
feedsInto: "Elimination Round 2",
|
|
|
|
|
|
isScoring: false, // losers finish 13th–16th
|
|
|
|
|
|
nonScoringWinnerFloor: null,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "Winners Semifinals",
|
|
|
|
|
|
matchCount: 4,
|
|
|
|
|
|
feedsInto: "Winners Final",
|
|
|
|
|
|
isScoring: false,
|
|
|
|
|
|
loserFeedsInto: "Elimination Round 3",
|
|
|
|
|
|
// Reaching the Winners Final guarantees at worst 5th (lose it, then lose the
|
|
|
|
|
|
// Elimination Final). Same value as the engine default, stated explicitly.
|
|
|
|
|
|
nonScoringWinnerFloor: 5,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "Elimination Round 2",
|
|
|
|
|
|
matchCount: 4,
|
|
|
|
|
|
feedsInto: "Elimination Round 3",
|
|
|
|
|
|
isScoring: false, // losers finish 11th–12th
|
|
|
|
|
|
nonScoringWinnerFloor: null,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "Elimination Round 3",
|
|
|
|
|
|
matchCount: 4,
|
|
|
|
|
|
feedsInto: "Elimination Round 4",
|
|
|
|
|
|
isScoring: false, // losers finish 9th–10th
|
|
|
|
|
|
// Winners reach Elimination Round 4, where a loss is 7th — not 5th.
|
|
|
|
|
|
nonScoringWinnerFloor: 7,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "Winners Final",
|
|
|
|
|
|
matchCount: 2,
|
|
|
|
|
|
feedsInto: "Bracket Championship",
|
|
|
|
|
|
isScoring: true, // loser drops to the Elimination Final (provisional 5th)
|
|
|
|
|
|
loserFeedsInto: "Elimination Final",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "Elimination Round 4",
|
|
|
|
|
|
matchCount: 2,
|
|
|
|
|
|
feedsInto: "Elimination Final",
|
|
|
|
|
|
isScoring: true, // losers share 7th–8th
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "Elimination Final",
|
|
|
|
|
|
matchCount: 2,
|
|
|
|
|
|
feedsInto: "Bracket Championship",
|
|
|
|
|
|
isScoring: true, // losers share 5th–6th
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "Bracket Championship",
|
|
|
|
|
|
matchCount: 2,
|
|
|
|
|
|
feedsInto: "World Championship",
|
|
|
|
|
|
isScoring: true, // loser drops to the Consolation game (provisional 4th)
|
|
|
|
|
|
loserFeedsInto: "Consolation Third Place",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "Consolation Third Place",
|
|
|
|
|
|
matchCount: 1,
|
|
|
|
|
|
feedsInto: null,
|
|
|
|
|
|
isScoring: true, // winner 3rd, loser 4th
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "World Championship",
|
|
|
|
|
|
matchCount: 1,
|
|
|
|
|
|
feedsInto: null,
|
|
|
|
|
|
isScoring: true, // winner 1st, loser 2nd
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
// Region assignments rotate year to year (which region draws the bye changes), so
|
|
|
|
|
|
// these are positional slot labels rather than region names.
|
|
|
|
|
|
participantLabels: [
|
|
|
|
|
|
"U.S. Opening 1 — Home", "U.S. Opening 1 — Away",
|
|
|
|
|
|
"U.S. Opening 2 — Home", "U.S. Opening 2 — Away",
|
|
|
|
|
|
"U.S. Opening 3 — Home", "U.S. Opening 3 — Away",
|
|
|
|
|
|
"U.S. Opening 4 — Home", "U.S. Opening 4 — Away",
|
|
|
|
|
|
"U.S. Bye — Winners R2 G1", "U.S. Bye — Winners R2 G2",
|
|
|
|
|
|
"Intl Opening 1 — Home", "Intl Opening 1 — Away",
|
|
|
|
|
|
"Intl Opening 2 — Home", "Intl Opening 2 — Away",
|
|
|
|
|
|
"Intl Opening 3 — Home", "Intl Opening 3 — Away",
|
|
|
|
|
|
"Intl Opening 4 — Home", "Intl Opening 4 — Away",
|
|
|
|
|
|
"Intl Bye — Winners R2 G1", "Intl Bye — Winners R2 G2",
|
|
|
|
|
|
],
|
|
|
|
|
|
phases: [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "United States",
|
|
|
|
|
|
groups: [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "U.S. Winner's Bracket",
|
|
|
|
|
|
roundMatchNumbers: {
|
|
|
|
|
|
"Opening Round": [1, 2, 3, 4],
|
|
|
|
|
|
"Winners Round 2": [1, 2],
|
|
|
|
|
|
"Winners Semifinals": [1, 2],
|
|
|
|
|
|
"Winners Final": [1],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "U.S. Elimination Bracket",
|
|
|
|
|
|
roundMatchNumbers: {
|
|
|
|
|
|
"Elimination Round 1": [1, 2],
|
|
|
|
|
|
"Elimination Round 2": [1, 2],
|
|
|
|
|
|
"Elimination Round 3": [1, 2],
|
|
|
|
|
|
"Elimination Round 4": [1],
|
|
|
|
|
|
"Elimination Final": [1],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "U.S. Championship",
|
|
|
|
|
|
roundMatchNumbers: { "Bracket Championship": [1] },
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "International",
|
|
|
|
|
|
groups: [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "International Winner's Bracket",
|
|
|
|
|
|
roundMatchNumbers: {
|
|
|
|
|
|
"Opening Round": [5, 6, 7, 8],
|
|
|
|
|
|
"Winners Round 2": [3, 4],
|
|
|
|
|
|
"Winners Semifinals": [3, 4],
|
|
|
|
|
|
"Winners Final": [2],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "International Elimination Bracket",
|
|
|
|
|
|
roundMatchNumbers: {
|
|
|
|
|
|
"Elimination Round 1": [3, 4],
|
|
|
|
|
|
"Elimination Round 2": [3, 4],
|
|
|
|
|
|
"Elimination Round 3": [3, 4],
|
|
|
|
|
|
"Elimination Round 4": [2],
|
|
|
|
|
|
"Elimination Final": [2],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "International Championship",
|
|
|
|
|
|
roundMatchNumbers: { "Bracket Championship": [2] },
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "Championship",
|
|
|
|
|
|
rounds: ["Consolation Third Place", "World Championship"],
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
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,
|
2026-05-08 12:29:39 -07:00
|
|
|
|
college_hockey_16: COLLEGE_HOCKEY_16,
|
2025-11-03 13:16:37 -08:00
|
|
|
|
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,
|
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,
|
Unify majors: score once, fan out across windows + tennis bracket EV
Make a "major" (golf/tennis/CS2) scored once on its canonical tournament
and fan out to every linked sports_season window and league.
Fan-out & completion (app/services/sync-tournament-results.ts):
- syncTournamentResults now marks each synced window's event complete
(gated by markComplete), recalculates affected leagues, and counts
recalc failures so a stale league can't hide behind a "completed" badge
- syncMajorFromPrimaryEvent promotes a primary window's derived results to
canonical tournament_results (deleting rows for dropped placements) and
fans out to siblings; fanOutMajorIfPrimary guards on the primary
- placement removals now propagate (stale rows reset to filler)
Primary-event model (scoring_events.is_primary, migration 0122):
- getPrimaryEventForTournament / isReadOnlySibling / ensurePrimaryEvent /
setPrimaryEvent; event creation auto-seeds a primary for bracket majors;
"Make primary" button on the tournament page
- per-window event/bracket/cs2 pages are read-only for non-primary linked
events (not-participating stays editable)
Tennis Grand Slam bracket (tennis_128 template + TEMPLATE_ROUND_CONFIG):
- bracket-scored qualifying major via the existing bracket pipeline
- simulator conditions in-progress EV on the real bracket (honoring
completed matches, walkover for withdrawals), QP derived from config,
round structure read from the template; CS2 + tennis share resolveStructureSource
Backfill (scripts/backfill-major-linking.ts): one-time idempotent reconcile
of existing majors (link orphans, designate primary, promote canonical, sync).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 20:32:22 -07:00
|
|
|
|
tennis_128: TENNIS_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,
|
2026-04-13 00:51:53 -04:00
|
|
|
|
nba_20: NBA_20,
|
Add LLWS 20-team double-elimination bracket
The Little League Baseball World Series runs two independent 10-team
double-elimination brackets — United States and International — each
producing a side champion, then a World Championship game and a
Consolation Third Place game between the side runners-up. 38 games in
all. No existing template could express it: every one is single
elimination, at most with a bolted-on third-place game.
Adds the llws_20 template plus dedicated generation and advancement,
following the same bespoke-routing pattern afl_10 and nba_20 use rather
than the generic ceil(matchNumber / 2) advancement.
The core of the change is loser routing. In the winners bracket a loss
is not an elimination — it drops the team into the elimination bracket
at a specific slot, including the deliberate cross-overs the official
bracket uses (Elimination Round 1 pairs L4/L6 and L2/L8; Elimination
Round 3 pairs each semifinal loser with the winner from the opposite
half). In the elimination bracket a loss is final. Matching the official
modified double-elimination format, there is no "if necessary" game: the
winners-bracket champion is eliminated if it loses the side
championship, dropping to the consolation game.
Rounds are shared across both sides, U.S. taking the low match numbers
and International the high ones, so the scoring config stays one entry
per stage. The existing phases/groups display machinery splits them back
apart into United States / International / Championship tabs.
Scoring lands on exactly 8 point-earning teams, which is the field size
when Elimination Round 4 begins: the two finals decide 1st–4th,
Elimination Final losers take 5th–6th, and Elimination Round 4 losers
7th–8th. 3rd and 4th are distinct because the consolation game is real,
and 5–8 splits into two two-team tiers so surviving Elimination Round 4
is worth more than losing it.
Also:
- Adds an optional nonScoringWinnerFloor to BracketRound. The engine
hardcoded a 5th-place floor for winners of non-scoring rounds feeding
a scoring one, which is wrong inside a losers bracket where a win can
guarantee only 7th. Opt-in, so no existing template changes behavior.
- Fixes TabbedBracketLayout's mobile path, which built its match map
unfiltered and so would have merged U.S. and International games into
one column. No-op for NCAA and NBA, whose groups already cover every
match in their phases.
- Rewrites the LLWS Monte Carlo simulator, which still modelled the
retired pool-play format (5 teams per pool, then a 4-team bracket per
side) and no longer described the tournament being scored. It now runs
the real 10-team double elimination and splits the 5–8 probabilities
into the correct tiers instead of one even four-way split. Legacy
"US:A"/"Intl:B" externalIds are still accepted, read as the side
alone, so seasons configured for the old format keep loading.
Tests replay all 38 games through the pure advancement resolver and
assert each one against the feed labels printed on the official bracket.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EAxqBVzfmKJe6WQ6VF9guj
2026-08-03 18:06:56 +00:00
|
|
|
|
llws_20: LLWS_20,
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-16 12:27:43 -07:00
|
|
|
|
/**
|
|
|
|
|
|
* 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);
|
|
|
|
|
|
}
|
2026-03-21 09:44:05 -07:00
|
|
|
|
return Array.from(roundMatchCounts.keys()).toSorted(
|
2026-03-16 12:27:43 -07:00
|
|
|
|
(a, b) => (roundMatchCounts.get(b) || 0) - (roundMatchCounts.get(a) || 0)
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-03 13:16:37 -08:00
|
|
|
|
/**
|
|
|
|
|
|
* 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;
|
|
|
|
|
|
|
Add LLWS 20-team double-elimination bracket
The Little League Baseball World Series runs two independent 10-team
double-elimination brackets — United States and International — each
producing a side champion, then a World Championship game and a
Consolation Third Place game between the side runners-up. 38 games in
all. No existing template could express it: every one is single
elimination, at most with a bolted-on third-place game.
Adds the llws_20 template plus dedicated generation and advancement,
following the same bespoke-routing pattern afl_10 and nba_20 use rather
than the generic ceil(matchNumber / 2) advancement.
The core of the change is loser routing. In the winners bracket a loss
is not an elimination — it drops the team into the elimination bracket
at a specific slot, including the deliberate cross-overs the official
bracket uses (Elimination Round 1 pairs L4/L6 and L2/L8; Elimination
Round 3 pairs each semifinal loser with the winner from the opposite
half). In the elimination bracket a loss is final. Matching the official
modified double-elimination format, there is no "if necessary" game: the
winners-bracket champion is eliminated if it loses the side
championship, dropping to the consolation game.
Rounds are shared across both sides, U.S. taking the low match numbers
and International the high ones, so the scoring config stays one entry
per stage. The existing phases/groups display machinery splits them back
apart into United States / International / Championship tabs.
Scoring lands on exactly 8 point-earning teams, which is the field size
when Elimination Round 4 begins: the two finals decide 1st–4th,
Elimination Final losers take 5th–6th, and Elimination Round 4 losers
7th–8th. 3rd and 4th are distinct because the consolation game is real,
and 5–8 splits into two two-team tiers so surviving Elimination Round 4
is worth more than losing it.
Also:
- Adds an optional nonScoringWinnerFloor to BracketRound. The engine
hardcoded a 5th-place floor for winners of non-scoring rounds feeding
a scoring one, which is wrong inside a losers bracket where a win can
guarantee only 7th. Opt-in, so no existing template changes behavior.
- Fixes TabbedBracketLayout's mobile path, which built its match map
unfiltered and so would have merged U.S. and International games into
one column. No-op for NCAA and NBA, whose groups already cover every
match in their phases.
- Rewrites the LLWS Monte Carlo simulator, which still modelled the
retired pool-play format (5 teams per pool, then a 4-team bracket per
side) and no longer described the tournament being scored. It now runs
the real 10-team double elimination and splits the 5–8 probabilities
into the correct tiers instead of one even four-way split. Legacy
"US:A"/"Intl:B" externalIds are still accepted, read as the side
alone, so seasons configured for the old format keep loading.
Tests replay all 38 games through the pure advancement resolver and
assert each one against the feed labels printed on the official bracket.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EAxqBVzfmKJe6WQ6VF9guj
2026-08-03 18:06:56 +00:00
|
|
|
|
// Special handling for LLWS double elimination: match counts don't identify the
|
|
|
|
|
|
// tier (Elimination Round 4 and the Elimination Final both have 2 matches), and
|
|
|
|
|
|
// the Winners Final eliminates nobody.
|
|
|
|
|
|
if (template.id === "llws_20") {
|
|
|
|
|
|
if (roundName === "Elimination Round 4") return "quarterfinals"; // losers share 7-8th
|
|
|
|
|
|
if (roundName === "Elimination Final") return "quarterfinals"; // losers share 5-6th
|
|
|
|
|
|
if (roundName === "Bracket Championship") return "semifinals"; // losers play for 3-4th
|
|
|
|
|
|
if (roundName === "Consolation Third Place") return "semifinals"; // finalizes 3rd/4th
|
|
|
|
|
|
if (roundName === "World Championship") return "finals"; // 1st and 2nd
|
|
|
|
|
|
return null; // Winners Final: loser drops to the elimination bracket, nobody is out
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|