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:
parent
a92cd6da97
commit
585dc9e112
2 changed files with 26 additions and 50 deletions
|
|
@ -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>();
|
||||||
|
|
|
||||||
|
|
@ -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 33–128 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 1–16)
|
* - 32 seeded-vs-unseeded matches (seeds 1–32 each face a randomly drawn unseeded opponent)
|
||||||
* - 16 seeded-vs-unseeded matches (seeds 17–32 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 1–32 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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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++) {
|
for (let s = 0; s < NUM_SIMULATIONS; s++) {
|
||||||
// Draw: shuffle the unseeded pool and build R1 bracket.
|
// Draw: shuffle the unseeded pool — seeded positions are pre-computed.
|
||||||
const drawnUnseeded = shuffle([...unseeded]);
|
const drawnUnseeded = shuffle([...unseededPool]);
|
||||||
|
|
||||||
// If fewer than 128 players total, pad with ghost entries using a low fallback Elo.
|
// Build R1 pairs inline using pre-computed seeded slots.
|
||||||
// (In practice the admin should always load 128 players.)
|
const r1Pairs: Array<[string, string]> = [];
|
||||||
while (drawnUnseeded.length + topSeeds.length < 128) {
|
for (let i = 0; i < TOP_SEEDS; i++) {
|
||||||
drawnUnseeded.push(`__bye_${drawnUnseeded.length}`);
|
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)
|
// R1 (64 matches)
|
||||||
const r1Winners: string[] = [];
|
const r1Winners: string[] = [];
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue