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:
1 parent
a5222953dd
commit
a4bee22d96
13 files changed
+214
-25
No files matched your search
@@ -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/);
|
||||
});
|
||||
Reference in new issue
Block a user