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
This commit is contained in:
Claude 2026-03-31 18:31:32 +00:00
parent a92cd6da97
commit 585dc9e112
No known key found for this signature in database
2 changed files with 26 additions and 50 deletions

View file

@ -30,6 +30,7 @@ import {
} from '~/components/ui/card'; } from '~/components/ui/card';
import { useState } from 'react'; import { useState } from 'react';
import { Loader2, CheckCircle2, AlertCircle } from 'lucide-react'; import { Loader2, CheckCircle2, AlertCircle } from 'lucide-react';
import { normalizeName } from '~/lib/fuzzy-match';
export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors { export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors {
return [{ title: `Darts Elo & Rankings — ${data?.sportsSeason?.name ?? "Sports Season"} - Brackt Admin` }]; 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`); 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() { export default function AdminSportsSeasonDartsElo() {
const { sportsSeason, participants, existingData } = useLoaderData<typeof loader>(); const { sportsSeason, participants, existingData } = useLoaderData<typeof loader>();
const actionData = useActionData<ActionData>(); const actionData = useActionData<ActionData>();

View file

@ -84,37 +84,6 @@ const SETS_TO_WIN = [2, 3, 3, 4, 4, 6, 7] as const;
*/ */
const TOP_SEEDS = 32; 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 33128 are randomly drawn.
* We model this by randomly pairing all 96 unseeded players in R1.
*/
// ─── Math helpers ────────────────────────────────────────────────────────────── // ─── Math helpers ──────────────────────────────────────────────────────────────
/** /**
@ -158,14 +127,12 @@ function binomialCoeff(n: number, k: number): number {
* shuffled and assigned to the remaining slots. * shuffled and assigned to the remaining slots.
* *
* Structure: * Structure:
* - 16 seeded-vs-unseeded matches (SEEDED_R1_PAIRS, one per seed 116) * - 32 seeded-vs-unseeded matches (seeds 132 each face a randomly drawn unseeded opponent)
* - 16 seeded-vs-unseeded matches (seeds 1732 each also get an unseeded opponent) * - 32 unseeded-vs-unseeded matches (remaining 64 unseeded players paired randomly)
* - 32 unseeded-vs-unseeded matches (remaining 64 unseeded players) * Total: 64 R1 matches (128 players)
* *
* Wait in the PDC World Championship, seeds 132 are all seeded and each plays * Note: in the hot simulation loop, seeded positions are pre-computed via getSeededMatchOrder
* an unseeded opponent in R1. That gives 32 seeded matches. The remaining 64 * and inlined directly this function is used for testing and bracket-draw path only.
* unseeded players fill 32 R1 matches among themselves.
* Total: 32 + 32 = 64 R1 matches (128 players).
* *
* Exported for unit testing. * Exported for unit testing.
*/ */
@ -527,17 +494,29 @@ export class DartsSimulator implements Simulator {
return Math.random() < winProb ? p1Id : p2Id; return Math.random() < winProb ? p1Id : p2Id;
}; };
for (let s = 0; s < NUM_SIMULATIONS; s++) { // Pre-compute fixed seeded bracket positions once — only the unseeded draw changes per sim.
// Draw: shuffle the unseeded pool and build R1 bracket. const seededMatchOrder = getSeededMatchOrder(TOP_SEEDS);
const drawnUnseeded = shuffle([...unseeded]); const seededSlots = seededMatchOrder.map(seed => topSeeds[seed - 1]);
// If fewer than 128 players total, pad with ghost entries using a low fallback Elo. // Pad unseeded pool to 96 once before the loop.
// (In practice the admin should always load 128 players.) // In practice the admin should always load 128 players; this guards against edge cases.
while (drawnUnseeded.length + topSeeds.length < 128) { const unseededPool = [...unseeded];
drawnUnseeded.push(`__bye_${drawnUnseeded.length}`); while (unseededPool.length + topSeeds.length < 128) {
unseededPool.push(`__bye_${unseededPool.length}`);
} }
const r1Pairs = buildR1Bracket(topSeeds, drawnUnseeded); for (let s = 0; s < NUM_SIMULATIONS; s++) {
// Draw: shuffle the unseeded pool — seeded positions are pre-computed.
const drawnUnseeded = shuffle([...unseededPool]);
// 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]]);
}
// R1 (64 matches) // R1 (64 matches)
const r1Winners: string[] = []; const r1Winners: string[] = [];