fix: interleave seeded and unseeded R1 pairs in darts bracket
Previously, all 32 seeded-vs-unseeded matches were added to r1Pairs
first (indices 0–31) and all 32 unseeded-vs-unseeded matches last
(indices 32–63). Since R2 pairs adjacent R1 winners, this created two
completely separate sub-brackets that only converged at the Final:
- Seeded sub-bracket: top players eliminating each other early
- Unseeded sub-bracket: high-Elo unseeded players (e.g. Doets 1818,
Zonneveld 1806) steamrolling weak opponents and making the Final
~20% of the time
Fix: interleave each seeded match with its adjacent unseeded-vs-unseeded
match. Seeded pair i is at r1Pairs[2i], unseeded pair i is at r1Pairs[2i+1].
From R2 onwards, winners from seeded and unseeded regions now merge
correctly as in a real single-elimination bracket.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
715f61d1c7
commit
e3052748e5
2 changed files with 22 additions and 14 deletions
|
|
@ -160,21 +160,21 @@ describe("buildR1Bracket", () => {
|
||||||
const pairs = buildR1Bracket(seeded, unseeded);
|
const pairs = buildR1Bracket(seeded, unseeded);
|
||||||
const seededSet = new Set(seeded);
|
const seededSet = new Set(seeded);
|
||||||
|
|
||||||
// First 32 pairs: seeded vs unseeded
|
// Even-indexed pairs (0, 2, 4, ...) are seeded vs unseeded
|
||||||
for (let i = 0; i < 32; i++) {
|
for (let i = 0; i < 64; i += 2) {
|
||||||
const [a, b] = pairs[i];
|
const [a, b] = pairs[i];
|
||||||
const aSeeded = seededSet.has(a);
|
const aSeeded = seededSet.has(a);
|
||||||
const bSeeded = seededSet.has(b);
|
const bSeeded = seededSet.has(b);
|
||||||
// One must be seeded, the other unseeded
|
|
||||||
expect(aSeeded !== bSeeded).toBe(true);
|
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 pairs = buildR1Bracket(seeded, unseeded);
|
||||||
const seededSet = new Set(seeded);
|
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];
|
const [a, b] = pairs[i];
|
||||||
expect(seededSet.has(a)).toBe(false);
|
expect(seededSet.has(a)).toBe(false);
|
||||||
expect(seededSet.has(b)).toBe(false);
|
expect(seededSet.has(b)).toBe(false);
|
||||||
|
|
|
||||||
|
|
@ -150,15 +150,15 @@ export function buildR1Bracket(
|
||||||
|
|
||||||
const pairs: Array<[string, string]> = [];
|
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++) {
|
for (let i = 0; i < 32; i++) {
|
||||||
const seedPos = seededMatchOrder[i] - 1; // 0-indexed
|
const seedPos = seededMatchOrder[i] - 1; // 0-indexed
|
||||||
|
// Even slot: seed vs. unseeded[i]
|
||||||
pairs.push([seededIds[seedPos], unseededIds[i]]);
|
pairs.push([seededIds[seedPos], unseededIds[i]]);
|
||||||
}
|
// Odd slot: unseeded vs. unseeded (indices 32 + 2i and 32 + 2i + 1)
|
||||||
|
pairs.push([unseededIds[32 + i * 2], unseededIds[32 + i * 2 + 1]]);
|
||||||
// 32 unseeded-vs-unseeded R1 matches (players 32–95)
|
|
||||||
for (let i = 32; i < 96; i += 2) {
|
|
||||||
pairs.push([unseededIds[i], unseededIds[i + 1]]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return pairs;
|
return pairs;
|
||||||
|
|
@ -519,12 +519,20 @@ export class DartsSimulator implements Simulator {
|
||||||
const drawnUnseeded = shuffle([...unseededPool]);
|
const drawnUnseeded = shuffle([...unseededPool]);
|
||||||
|
|
||||||
// Build R1 pairs inline using pre-computed seeded slots.
|
// 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]> = [];
|
const r1Pairs: Array<[string, string]> = [];
|
||||||
for (let i = 0; i < TOP_SEEDS; i++) {
|
for (let i = 0; i < TOP_SEEDS; i++) {
|
||||||
|
// Even slot: seeded player vs. their randomly drawn unseeded opponent
|
||||||
r1Pairs.push([seededSlots[i], drawnUnseeded[i]]);
|
r1Pairs.push([seededSlots[i], drawnUnseeded[i]]);
|
||||||
}
|
// Odd slot (adjacent): unseeded vs. unseeded pair that feeds into the
|
||||||
for (let i = TOP_SEEDS; i < drawnUnseeded.length; i += 2) {
|
// same R2 match as the seeded slot above
|
||||||
r1Pairs.push([drawnUnseeded[i], drawnUnseeded[i + 1]]);
|
r1Pairs.push([drawnUnseeded[TOP_SEEDS + i * 2], drawnUnseeded[TOP_SEEDS + i * 2 + 1]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// R1 (64 matches)
|
// R1 (64 matches)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue