import { test } from 'node:test'; import assert from 'node:assert/strict'; import { Tournament, validateSet } from '../engine/tournament.js'; import { CourtEngine } from '../engine/courts.js'; import { roundRobin, makePools, singleElimination, doubleElimination, seedOrder, resetIds } from '../engine/formats.js'; const setup = (n, opts = {}) => { resetIds(); const t = new Tournament({ name: 'test', ...opts }); for (let i = 1; i <= n; i++) t.registerTeam({ name: `T${i}`, players: 2 }); return t; }; const ids = t => [...t.teams.keys()]; test('round robin: every pair once, each team once per round', () => { resetIds(); const ms = roundRobin(['a', 'b', 'c', 'd', 'e']); assert.equal(ms.length, 10); const pairs = new Set(ms.map(m => [m.teamA, m.teamB].sort().join())); assert.equal(pairs.size, 10); for (let r = 1; r <= 5; r++) { const teams = ms.filter(m => m.round === r).flatMap(m => [m.teamA, m.teamB]); assert.equal(new Set(teams).size, teams.length); } }); test('pools: snake seeding of 12 into 3 pools', () => { const p = makePools(Array.from({ length: 12 }, (_, i) => `s${i + 1}`), 4); assert.deepEqual(p.map(x => x.teams), [['s1', 's6', 's7', 's12'], ['s2', 's5', 's8', 's11'], ['s3', 's4', 's9', 's10']]); }); test('seed order for 8 is the standard 1-8, 4-5, 2-7, 3-6', () => { assert.deepEqual(seedOrder(8), [1, 8, 4, 5, 2, 7, 3, 6]); }); test('single elimination with 6 teams gives top two seeds byes into round 2', () => { resetIds(); const { matches, size } = singleElimination(['s1', 's2', 's3', 's4', 's5', 's6']); assert.equal(size, 8); const r2 = matches.filter(m => m.round === 2); assert.deepEqual(r2.map(m => [m.teamA, m.teamB]).flat().filter(Boolean).sort(), ['s1', 's2']); assert.equal(matches.filter(m => m.status === 'bye').length, 2); }); test('scores are validated against rally rules', () => { const r = { pointsTo: 21, winBy: 2, cap: 25 }; assert.ok(validateSet(21, 19, r)); assert.ok(validateSet(25, 24, r)); assert.ok(validateSet(23, 21, r)); assert.throws(() => validateSet(21, 20, r)); assert.throws(() => validateSet(24, 20, r)); assert.throws(() => validateSet(26, 24, r)); assert.throws(() => validateSet(19, 17, r)); }); test('withdrawal in forfeit mode: remaining games lost, standings recompute', () => { const t = setup(4); t.closeRegistration(); t.generatePools(4); const [a, b, c, d] = ids(t); const m = t.matches.find(x => [x.teamA, x.teamB].includes(a) && [x.teamA, x.teamB].includes(b)); t.recordResult(m.id, [[21, 10]].map(s => m.teamA === a ? s : s.reverse())); t.withdrawTeam(a, { mode: 'forfeit' }); const rows = t.standings('A'); assert.equal(rows.at(-1).teamId, a); assert.equal(rows.at(-1).status, 'withdrawn'); const forfeits = t.matches.filter(x => x.status === 'forfeit'); assert.equal(forfeits.length, 2); assert.ok(forfeits.every(x => x.winner !== a)); }); test('withdrawal in void mode removes the team\'s played games from standings', () => { const t = setup(4); t.closeRegistration(); t.generatePools(4); const [a, b] = ids(t); const m = t.matches.find(x => [x.teamA, x.teamB].includes(a) && [x.teamA, x.teamB].includes(b)); t.recordResult(m.id, m.teamA === a ? [[21, 10]] : [[10, 21]]); t.withdrawTeam(a, { mode: 'void' }); const rowB = t.standings('A').find(r => r.teamId === b); assert.equal(rowB.played, 0); assert.equal(t.matches.filter(x => x.status === 'void').length, 3); }); test('three-way circular head-to-head falls through to the next tiebreak', () => { const t = setup(3, { rules: { tiebreaks: ['headToHead', 'pointDiff'] } }); t.closeRegistration(); t.generatePools(3); const [a, b, c] = ids(t); const find = (x, y) => t.matches.find(m => [m.teamA, m.teamB].includes(x) && [m.teamA, m.teamB].includes(y)); const win = (x, y, s) => { const m = find(x, y); t.recordResult(m.id, m.teamA === x ? [s] : [[s[1], s[0]]]); }; win(a, b, [21, 10]); win(b, c, [21, 15]); win(c, a, [21, 19]); const rows = t.standings('A'); assert.deepEqual(rows.map(r => r.w), [1, 1, 1]); assert.equal(rows[0].teamId, a); // best point diff (+9) assert.equal(rows[0].decidedBy, 'pointDiff'); }); test('court engine: two courts, no team on two courts, alerts once per match', () => { let clock = 0; const t = setup(8, { stages: ['pool'] }); t.closeRegistration(); t.generatePools(4); const eng = new CourtEngine(t, { now: () => clock }); eng.start(); assert.equal(eng.courts.filter(c => c.matchId).length, 2); let guard = 0; while (t.phase === 'live' && guard++ < 100) { const busy = eng.courts.filter(c => c.matchId).flatMap(c => { const m = t.match(c.matchId); return [m.teamA, m.teamB]; }); assert.equal(new Set(busy).size, busy.length, 'a team is on two courts'); const c = eng.courts.find(c => c.matchId); clock += 12 * 60_000; eng.recordResult(c.matchId, [[21, 15]]); } assert.equal(t.phase, 'final'); const upNow = eng.alerts.filter(a => a.kind === 'up_now'); assert.equal(upNow.length, 12); assert.equal(new Set(upNow.map(a => a.match)).size, 12); }); test('court engine: paused court is skipped, resume picks up the queue', () => { let clock = 0; const t = setup(4, { stages: ['pool'] }); t.closeRegistration(); t.generatePools(4); const eng = new CourtEngine(t, { now: () => clock }); eng.start(); eng.pauseCourt(2, 'rain'); clock += 10 * 60_000; eng.recordResult(eng.courts[0].matchId, [[21, 5]]); eng.recordResult(eng.courts[1].matchId, [[21, 5]]); assert.equal(eng.courts[1].matchId, null); assert.ok(eng.courts[0].matchId); eng.resumeCourt(2); assert.ok(eng.courts[1].matchId); }); test('double elimination with 6 teams (byes) plays to a champion', () => { const t = setup(6, { 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++ < 100) { const c = eng.courts.find(c => c.matchId); assert.ok(c, 'engine stalled'); clock += 10 * 60_000; const m = t.match(c.matchId); // lower seed (later id) always loses, except grand final where LB side wins to force the reset const lbWins = m.label === 'Grand final'; eng.recordResult(m.id, lbWins ? [[10, 21]] : [[21, 10]]); } assert.equal(t.phase, 'final'); const reset = t.matches.find(m => m.label === 'Bracket reset'); assert.equal(reset.status, 'final'); assert.equal(t.champion(), t.teamName(reset.winner)); });