Fix double elimination with byes never finishing; hand-built brackets

Byes now cascade through the whole bracket (a slot nobody can fill is a bye or
a void), re-run on server start so stuck tournaments recover. New manual
bracket builder on the desk: pick round-1 pairings per match. Tests at 5-23
teams for both bracket types. Release 0.5.0.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MB7nCCAscYsb3zzkT6LHZi
This commit is contained in:
bkvargyasandClaude Fable 5.1 committed 2026-09-05 15:33:36 +00:00
1 parent a5222953dd
commit a4bee22d96
13 files changed
+214 -25

No files matched your search

+64
View File
@@ -152,3 +152,67 @@ test('double elimination with 6 teams (byes) plays to a champion', () => {
assert.equal(reset.status, 'final');
assert.equal(t.champion(), t.teamName(reset.winner));
});
for (const n of [5, 9, 12, 17, 18, 23]) {
test(`double elimination with ${n} teams (many byes) plays to a champion with no stuck matches`, () => {
const t = setup(n, { stages: ['bracket'] });
t.closeRegistration();
t.generateBracket(ids(t), { type: 'double' });
let clock = 0;
const eng = new CourtEngine(t, { now: () => clock });
eng.start();
let guard = 0;
while (t.phase === 'live' && guard++ < 400) {
const c = eng.courts.find(c => c.matchId);
assert.ok(c, `engine stalled with ${t.matches.filter(m => m.status === 'pending').length} pending matches`);
clock += 10 * 60_000;
eng.recordResult(c.matchId, [[21, 10]]);
}
assert.equal(t.phase, 'final');
assert.ok(t.champion());
assert.equal(t.matches.filter(m => m.status === 'pending').length, 0);
});
test(`single elimination with ${n} teams plays to a champion`, () => {
const t = setup(n, { stages: ['bracket'] });
t.closeRegistration();
t.generateBracket(ids(t), { type: 'single', thirdPlace: true });
let clock = 0;
const eng = new CourtEngine(t, { now: () => clock });
eng.start();
let guard = 0;
while (t.phase === 'live' && guard++ < 200) {
const c = eng.courts.find(c => c.matchId);
assert.ok(c, 'engine stalled');
clock += 10 * 60_000;
eng.recordResult(c.matchId, [[21, 10]]);
}
assert.equal(t.phase, 'final');
});
}
test('withdrawal from a bracket before the opponent is known becomes a bye for the opponent', () => {
const t = setup(4, { stages: ['bracket'] });
t.closeRegistration();
const [a, b, c, d] = ids(t);
t.generateBracket([a, b, c, d], { type: 'single' });
const r1 = t.matches.filter(m => m.round === 1);
const mA = r1.find(m => [m.teamA, m.teamB].includes(a));
t.recordResult(mA.id, mA.teamA === a ? [[21, 5]] : [[5, 21]]); // a advances to final
t.withdrawTeam(a, { mode: 'forfeit' }); // a leaves before the final is set
const other = r1.find(m => m !== mA);
t.recordResult(other.id, [[21, 5]]);
assert.equal(t.phase, 'final');
assert.equal(t.champion(), t.teamName(other.teamA));
});
test('manual bracket: hand-picked round-1 pairs land in the chosen matches', () => {
const t = setup(6, { stages: ['bracket'] });
t.closeRegistration();
const [a, b, c, d, e, f] = ids(t);
t.generateBracketFromPairs([[a, f], [b, null], [c, d], [null, e]], { type: 'single' });
const r1 = t.matches.filter(m => m.round === 1).sort((x, y) => x.slot - y.slot);
assert.deepEqual(r1.map(m => [m.teamA, m.teamB]), [[a, f], [b, null], [c, d], [null, e]]);
assert.equal(r1[1].status, 'bye'); assert.equal(r1[3].status, 'bye');
assert.throws(() => t.generateBracketFromPairs([[a, a], [b, c], [d, e], [f, null]]), /placed twice/);
assert.throws(() => t.generateBracketFromPairs([[a, b], [c, d], [e, null], [null, null]]), /Not placed/);
});