// Simulated tournament day: 12 teams, 3 pools of 4, 2 courts, one withdrawal mid-pool, // then an 8-team single-elimination bracket with a 3rd-place match. import { Tournament } from './engine/tournament.js'; import { CourtEngine } from './engine/courts.js'; import { resetIds } from './engine/formats.js'; const args = Object.fromEntries(process.argv.slice(2).map(a => a.replace(/^--/, '').split('='))); const SEED = Number(args.seed ?? 7); const BRACKET = args.bracket ?? 'single'; const VERBOSE = args.quiet === undefined; // deterministic PRNG so a run is reproducible let s = SEED >>> 0; const rnd = () => ((s = (s * 1664525 + 1013904223) >>> 0) / 2 ** 32); const pick = arr => arr[Math.floor(rnd() * arr.length)]; // simulated clock: starts 9:00, advances as matches finish let clock = new Date('2026-09-07T09:00:00-05:00').getTime(); const now = () => clock; const hhmm = ms => new Date(ms).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', timeZone: 'America/Chicago' }); resetIds(); const t = new Tournament({ name: 'HOA Labor Day 2s', rules: { teamSize: 2, pointsTo: 21, winBy: 2, cap: 25 }, courtCount: 2 }); const eng = new CourtEngine(t, { now, defaultMatchMinutes: 15, changeoverMinutes: 3 }); const names = ['Net Gains', 'Block Party', 'Sunburnt', 'Dig It', 'Kiss My Ace', 'Setting Ducks', 'The Spikers', 'Sandbaggers', 'Serves You Right', 'Bump Chumps', 'Ace Holes', 'Beach Please']; // hidden "true strength" so results look plausible rather than uniformly random const strength = new Map(); const printed = []; const out = (...a) => { if (VERBOSE) console.log(...a); printed.push(a.join(' ')); }; t.on((e) => { const at = hhmm(now()); switch (e.type) { case 'team_registered': return; case 'phase': return out(`${at} PHASE โ†’ ${e.phase}${e.champion ? ` ๐Ÿ† Champion: ${e.champion}` : ''}`); case 'pools_generated': return out(`${at} Pools: ` + e.pools.map(p => `${p.id}[${p.teams.join(', ')}]`).join(' ')); case 'bracket_generated': return out(`${at} Bracket (${e.bracket}, ${e.size}): ` + e.teams.map((n, i) => `${i + 1}.${n}`).join(' ')); case 'match_started': return out(`${at} โ–ถ Court ${e.court}: ${e.a} vs ${e.b} (${e.match})`); case 'result': { const m = t.match(e.match); return out(`${at} โœ“ ${m.id} ${t.teamName(m.teamA)} vs ${t.teamName(m.teamB)} โ†’ ${t.teamName(e.winner)} ${e.score}`); } case 'alert': return out(`${at} ๐Ÿ“ฑ ${e.kind.toUpperCase().padEnd(9)} ${e.text}`); case 'team_withdrawn': return out(`${at} โœ— WITHDRAWN: ${e.name} (${e.mode})`); case 'court_paused': return out(`${at} โธ Court ${e.court} paused: ${e.reason}`); case 'court_resumed': return out(`${at} โ–ถ Court ${e.court} resumed`); default: return out(`${at} ยท ${e.type} ${JSON.stringify(e)}`); } }); // ---------- Phase 1: check-in ---------- for (const n of names) { const team = t.registerTeam({ name: n, players: pick([2, 2, 2, 3]), captain: 'Captain', phone: '+1555' + String(Math.floor(rnd() * 1e7)).padStart(7, '0') }); strength.set(team.id, 0.35 + rnd() * 0.5); } out(`Registered ${t.teams.size} teams. Team codes e.g. ${[...t.teams.values()].slice(0, 3).map(x => `${x.name}=${x.code}`).join(', ')}`); try { t.registerTeam({ name: 'net gains', players: 2 }); } catch (e) { out(` (rejected duplicate: ${e.message})`); } try { t.registerTeam({ name: 'Solo', players: 1 }); } catch (e) { out(` (rejected: ${e.message})`); } // ---------- Phase 2: closed, generate ---------- t.closeRegistration(); t.generatePools(4); // ---------- Phase 3: live ---------- eng.start(); function playScore(m) { // simulate a set to 21, win by 2, cap 25 with the stronger team favoured const pa = strength.get(m.teamA), pb = strength.get(m.teamB); let a = 0, b = 0; const { pointsTo, winBy, cap } = t.rules; while (true) { if (rnd() < pa / (pa + pb)) a++; else b++; const hi = Math.max(a, b), lo = Math.min(a, b); if (hi === cap) break; if (hi >= pointsTo && hi - lo >= winBy) break; } return [[a, b]]; } let withdrawn = false; let paused = false; let step = 0; while (t.phase === 'live' && step++ < 200) { // pick the court whose match will finish first const live = eng.courts.filter(c => c.matchId); if (!live.length) { // nothing running: maybe everything is waiting on a paused court if (paused) { clock += 5 * 60_000; eng.resumeCourt(2); paused = false; continue; } out('!! stalled: no live matches and nothing assignable'); break; } const durations = live.map(c => ({ c, end: c.startedAt + (11 + rnd() * 7) * 60_000 })); durations.sort((x, y) => x.end - y.end); const { c, end } = durations[0]; clock = Math.max(clock, end); const m = t.match(c.matchId); eng.recordResult(m.id, playScore(m)); const poolDone = t.matches.filter(x => x.stage === 'pool' && ['final','forfeit','void'].includes(x.status)).length; const poolTotal = t.matches.filter(x => x.stage === 'pool').length; // mid-pool: a team leaves (kid's soccer game) if (!withdrawn && poolDone >= 7) { withdrawn = true; const victim = t.activeTeams().find(x => x.name === 'Sunburnt'); clock += 60_000; eng.withdrawTeam(victim.id, { mode: 'forfeit' }); eng.broadcast('Lunch is out at the shelter. Court 2 keeps running.'); } // rain delay on court 2 for a bit if (!paused && poolDone === 12) { paused = true; eng.pauseCourt(2, 'net repair'); } if (paused && poolDone === 14) { paused = false; eng.resumeCourt(2); } // pool play over -> bracket if (poolDone === poolTotal && !t.matches.some(x => x.stage !== 'pool')) { out(''); for (const p of t.pools) { out(` Pool ${p.id} standings`); for (const r of t.standings(p.id)) out(` ${r.name.padEnd(18)} ${r.w}-${r.l} pts ${String(r.pf).padStart(3)}-${String(r.pa).padStart(3)} diff ${String(r.pointDiff).padStart(4)}${r.decidedBy ? ` (${r.decidedBy})` : ''}${r.status === 'withdrawn' ? ' WITHDRAWN' : ''}`); } const seeded = t.advanceFromPools({ perPool: 2, wildcards: 2 }); out(''); t.generateBracket(seeded, { type: BRACKET, thirdPlace: BRACKET === 'single' }); eng.tick(); const b = eng.board(); out(` Board: ` + b.courts.map(c => `C${c.court}: ${c.match ? `${c.match.a} vs ${c.match.b}` : c.status}`).join(' | ') + ` โ€– up next: ` + b.upNext.map(u => `${u.a} vs ${u.b} (C${u.court}, ~${u.etaMin}m)`).join('; ')); out(''); } } out(''); out(`Day finished at ${hhmm(now())}. ${t.matches.filter(m => m.status === 'final').length} matches played, ${t.matches.filter(m => m.status === 'forfeit').length} forfeits, ${eng.alerts.length} alerts sent, avg match ${eng.board().avgMatchMin} min.`); const finals = t.matches.filter(m => m.stage !== 'pool').map(m => `${m.label ?? `${m.stage} R${m.round}`}: ${t.teamName(m.teamA)} vs ${t.teamName(m.teamB)} โ†’ ${m.winner ? t.teamName(m.winner) : m.status}`); out(finals.join('\n')); // ---------- invariants ---------- const problems = []; for (const m of t.matches) { if (m.status === 'final' && !m.winner) problems.push(`${m.id} final without winner`); if (m.status === 'live') problems.push(`${m.id} still live at end`); } const teamsPlayingTwice = eng.courts.filter(c => c.matchId).length; if (teamsPlayingTwice) problems.push('courts still occupied'); const sunburnt = [...t.teams.values()].find(x => x.name === 'Sunburnt'); if (t.matches.some(m => m.stage !== 'pool' && (m.teamA === sunburnt.id || m.teamB === sunburnt.id))) problems.push('withdrawn team reached bracket'); const upNowPerMatch = eng.alerts.filter(a => a.kind === 'up_now').map(a => a.match); if (new Set(upNowPerMatch).size !== upNowPerMatch.length) problems.push('duplicate up_now alert'); console.log(problems.length ? `\nINVARIANT FAILURES:\n - ${problems.join('\n - ')}` : `\nAll invariants hold.`); process.exitCode = problems.length ? 1 : 0;