diff --git a/app/services/simulations/__tests__/darts-simulator.test.ts b/app/services/simulations/__tests__/darts-simulator.test.ts index cf85093..6ad9b5e 100644 --- a/app/services/simulations/__tests__/darts-simulator.test.ts +++ b/app/services/simulations/__tests__/darts-simulator.test.ts @@ -160,21 +160,21 @@ describe("buildR1Bracket", () => { const pairs = buildR1Bracket(seeded, unseeded); const seededSet = new Set(seeded); - // First 32 pairs: seeded vs unseeded - for (let i = 0; i < 32; i++) { + // Even-indexed pairs (0, 2, 4, ...) are seeded vs unseeded + for (let i = 0; i < 64; i += 2) { const [a, b] = pairs[i]; const aSeeded = seededSet.has(a); const bSeeded = seededSet.has(b); - // One must be seeded, the other unseeded expect(aSeeded !== bSeeded).toBe(true); } }); - it("last 32 pairs: all unseeded vs unseeded", () => { + it("odd-indexed pairs are all unseeded vs unseeded (interleaved bracket structure)", () => { const pairs = buildR1Bracket(seeded, unseeded); const seededSet = new Set(seeded); - for (let i = 32; i < 64; i++) { + // Odd-indexed pairs (1, 3, 5, ...) are unseeded vs unseeded + for (let i = 1; i < 64; i += 2) { const [a, b] = pairs[i]; expect(seededSet.has(a)).toBe(false); expect(seededSet.has(b)).toBe(false); diff --git a/app/services/simulations/darts-simulator.ts b/app/services/simulations/darts-simulator.ts index b8759e6..207d4e0 100644 --- a/app/services/simulations/darts-simulator.ts +++ b/app/services/simulations/darts-simulator.ts @@ -150,15 +150,15 @@ export function buildR1Bracket( const pairs: Array<[string, string]> = []; - // 32 seeded-vs-unseeded R1 matches + // Interleave seeded and unseeded pairs so each seed's R1 match is immediately + // followed by an unseeded-vs-unseeded match. This ensures the two types of + // match converge in R2 rather than running as separate sub-brackets until the Final. for (let i = 0; i < 32; i++) { const seedPos = seededMatchOrder[i] - 1; // 0-indexed + // Even slot: seed vs. unseeded[i] pairs.push([seededIds[seedPos], unseededIds[i]]); - } - - // 32 unseeded-vs-unseeded R1 matches (players 32–95) - for (let i = 32; i < 96; i += 2) { - pairs.push([unseededIds[i], unseededIds[i + 1]]); + // Odd slot: unseeded vs. unseeded (indices 32 + 2i and 32 + 2i + 1) + pairs.push([unseededIds[32 + i * 2], unseededIds[32 + i * 2 + 1]]); } return pairs; @@ -519,12 +519,20 @@ export class DartsSimulator implements Simulator { const drawnUnseeded = shuffle([...unseededPool]); // Build R1 pairs inline using pre-computed seeded slots. + // IMPORTANT: interleave each seeded match with its adjacent unseeded match. + // Without interleaving, all 32 seeded matches come first (pairs 0–31) and + // all 32 unseeded matches come last (pairs 32–63). Because R2 pairs adjacent + // R1 winners, this creates two completely separate sub-brackets (seeded vs. + // unseeded) that only converge at the Final — producing absurd results like + // unseeded 1800-Elo players having a 20% finalist probability. + // Interleaving ensures seeded and unseeded regions mix from R2 onwards. const r1Pairs: Array<[string, string]> = []; for (let i = 0; i < TOP_SEEDS; i++) { + // Even slot: seeded player vs. their randomly drawn unseeded opponent r1Pairs.push([seededSlots[i], drawnUnseeded[i]]); - } - for (let i = TOP_SEEDS; i < drawnUnseeded.length; i += 2) { - r1Pairs.push([drawnUnseeded[i], drawnUnseeded[i + 1]]); + // Odd slot (adjacent): unseeded vs. unseeded pair that feeds into the + // same R2 match as the seeded slot above + r1Pairs.push([drawnUnseeded[TOP_SEEDS + i * 2], drawnUnseeded[TOP_SEEDS + i * 2 + 1]]); } // R1 (64 matches)