diff --git a/app/routes/admin.sports-seasons.$id.darts-elo.tsx b/app/routes/admin.sports-seasons.$id.darts-elo.tsx index d7329c9..890cfd3 100644 --- a/app/routes/admin.sports-seasons.$id.darts-elo.tsx +++ b/app/routes/admin.sports-seasons.$id.darts-elo.tsx @@ -30,6 +30,7 @@ import { } from '~/components/ui/card'; import { useState } from 'react'; import { Loader2, CheckCircle2, AlertCircle } from 'lucide-react'; +import { normalizeName } from '~/lib/fuzzy-match'; export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors { return [{ title: `Darts Elo & Rankings — ${data?.sportsSeason?.name ?? "Sports Season"} - Brackt Admin` }]; @@ -185,10 +186,6 @@ export async function action({ request, params }: Route.ActionArgs) { return redirect(`/admin/sports-seasons/${sportsSeasonId}/expected-values`); } -function normalizeName(name: string): string { - return name.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, ' ').trim(); -} - export default function AdminSportsSeasonDartsElo() { const { sportsSeason, participants, existingData } = useLoaderData(); const actionData = useActionData(); diff --git a/app/services/simulations/darts-simulator.ts b/app/services/simulations/darts-simulator.ts index 73dd89f..96af36b 100644 --- a/app/services/simulations/darts-simulator.ts +++ b/app/services/simulations/darts-simulator.ts @@ -84,37 +84,6 @@ const SETS_TO_WIN = [2, 3, 3, 4, 4, 6, 7] as const; */ const TOP_SEEDS = 32; -/** - * Standard seeded R128 bracket for the top 32 seeds. - * Each entry is [topSeedSlot, unseededOpponentIndex] where: - * - topSeedSlot is 1-indexed seed number (1 = world #1) - * - unseededOpponentIndex is a placeholder (0..31) indicating which randomly-drawn - * unseeded player fills this slot; the actual opponent is drawn at runtime. - * - * Structure preserves the property that seeds 1 and 2 can only meet in the Final, - * seeds 1/2/3/4 can only meet in the SFs or later, etc. - * - * Top half: 1v?, 16v?, 9v?, 8v?, 5v?, 12v?, 13v?, 4v? - * Bottom half: 3v?, 14v?, 11v?, 6v?, 7v?, 10v?, 15v?, 2v? - * (Each seeded position plays an unseeded opponent in R1) - */ -const SEEDED_R1_PAIRS: Array<[number, number]> = [ - [1, 0], [16, 1], - [9, 2], [8, 3], - [5, 4], [12, 5], - [13, 6], [4, 7], - [3, 8], [14, 9], - [11, 10],[6, 11], - [7, 12], [10, 13], - [15, 14],[2, 15], -]; - -/** - * Unseeded R1 matchups: the 96 unseeded players pair off in 48 matches among - * themselves for the remaining 32 R1 slots. Seeds 33–128 are randomly drawn. - * We model this by randomly pairing all 96 unseeded players in R1. - */ - // ─── Math helpers ────────────────────────────────────────────────────────────── /** @@ -158,14 +127,12 @@ function binomialCoeff(n: number, k: number): number { * shuffled and assigned to the remaining slots. * * Structure: - * - 16 seeded-vs-unseeded matches (SEEDED_R1_PAIRS, one per seed 1–16) - * - 16 seeded-vs-unseeded matches (seeds 17–32 each also get an unseeded opponent) - * - 32 unseeded-vs-unseeded matches (remaining 64 unseeded players) + * - 32 seeded-vs-unseeded matches (seeds 1–32 each face a randomly drawn unseeded opponent) + * - 32 unseeded-vs-unseeded matches (remaining 64 unseeded players paired randomly) + * Total: 64 R1 matches ✓ (128 players) * - * Wait — in the PDC World Championship, seeds 1–32 are all seeded and each plays - * an unseeded opponent in R1. That gives 32 seeded matches. The remaining 64 - * unseeded players fill 32 R1 matches among themselves. - * Total: 32 + 32 = 64 R1 matches ✓ (128 players). + * Note: in the hot simulation loop, seeded positions are pre-computed via getSeededMatchOrder + * and inlined directly — this function is used for testing and bracket-draw path only. * * Exported for unit testing. */ @@ -527,17 +494,29 @@ export class DartsSimulator implements Simulator { return Math.random() < winProb ? p1Id : p2Id; }; + // Pre-compute fixed seeded bracket positions once — only the unseeded draw changes per sim. + const seededMatchOrder = getSeededMatchOrder(TOP_SEEDS); + const seededSlots = seededMatchOrder.map(seed => topSeeds[seed - 1]); + + // Pad unseeded pool to 96 once before the loop. + // In practice the admin should always load 128 players; this guards against edge cases. + const unseededPool = [...unseeded]; + while (unseededPool.length + topSeeds.length < 128) { + unseededPool.push(`__bye_${unseededPool.length}`); + } + for (let s = 0; s < NUM_SIMULATIONS; s++) { - // Draw: shuffle the unseeded pool and build R1 bracket. - const drawnUnseeded = shuffle([...unseeded]); + // Draw: shuffle the unseeded pool — seeded positions are pre-computed. + const drawnUnseeded = shuffle([...unseededPool]); - // If fewer than 128 players total, pad with ghost entries using a low fallback Elo. - // (In practice the admin should always load 128 players.) - while (drawnUnseeded.length + topSeeds.length < 128) { - drawnUnseeded.push(`__bye_${drawnUnseeded.length}`); + // Build R1 pairs inline using pre-computed seeded slots. + const r1Pairs: Array<[string, string]> = []; + for (let i = 0; i < TOP_SEEDS; i++) { + r1Pairs.push([seededSlots[i], drawnUnseeded[i]]); + } + for (let i = TOP_SEEDS; i < drawnUnseeded.length; i += 2) { + r1Pairs.push([drawnUnseeded[i], drawnUnseeded[i + 1]]); } - - const r1Pairs = buildR1Bracket(topSeeds, drawnUnseeded); // R1 (64 matches) const r1Winners: string[] = [];