import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { buildSimulator } from './monteCarlo'; import type { RatingsModel } from './predict'; import type { FixturesFile } from '../types'; const DATA = join(process.cwd(), 'public', 'data'); const fixtures = (JSON.parse(readFileSync(join(DATA, 'fixtures.json'), 'utf8')) as FixturesFile).fixtures; const model = JSON.parse(readFileSync(join(DATA, 'ratings.json'), 'utf8')) as RatingsModel; /** Deterministic RNG so the test is stable. */ function mulberry32(seed: number): () => number { let a = seed; return () => { a |= 0; a = (a + 0x6d2b79f5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } describe('monte carlo tournament', () => { const sim = buildSimulator(fixtures, model); const res = sim.run(3000, mulberry32(12345)); const byTeam = new Map(res.teams.map((t) => [t.team, t])); const sum = (k: keyof (typeof res.teams)[number]) => res.teams.reduce((s, t) => s + (t[k] as number), 0); it('simulates all 48 teams', () => { expect(res.teams.length).toBe(48); }); it('conserves the exact per-round counts (12/32/16/8/4/2/1)', () => { expect(sum('winGroup')).toBeCloseTo(12, 5); expect(sum('qualify')).toBeCloseTo(32, 5); expect(sum('reachR16')).toBeCloseTo(16, 5); expect(sum('reachQF')).toBeCloseTo(8, 5); expect(sum('reachSF')).toBeCloseTo(4, 5); expect(sum('reachFinal')).toBeCloseTo(2, 5); expect(sum('champion')).toBeCloseTo(1, 5); }); it('every team has monotonically non-increasing deep-run odds', () => { for (const t of res.teams) { expect(t.qualify).toBeGreaterThanOrEqual(t.reachR16 - 1e-9); expect(t.reachR16).toBeGreaterThanOrEqual(t.reachQF - 1e-9); expect(t.reachQF).toBeGreaterThanOrEqual(t.reachSF - 1e-9); expect(t.reachSF).toBeGreaterThanOrEqual(t.reachFinal - 1e-9); expect(t.reachFinal).toBeGreaterThanOrEqual(t.champion - 1e-9); expect(t.winGroup).toBeGreaterThanOrEqual(t.champion - 1e-9); } }); it('all probabilities are within [0, 1]', () => { for (const t of res.teams) { for (const k of ['winGroup', 'qualify', 'reachR16', 'reachQF', 'reachSF', 'reachFinal', 'champion'] as const) { expect(t[k]).toBeGreaterThanOrEqual(0); expect(t[k]).toBeLessThanOrEqual(1); } } }); it('ranks a strong team well above a weak one', () => { const strong = byTeam.get('Spain')!; const weak = byTeam.get('CuraƧao')!; expect(strong.champion).toBeGreaterThan(weak.champion); expect(strong.qualify).toBeGreaterThan(weak.qualify); // the favourite should have a non-trivial title chance expect(res.teams[0]!.champion).toBeGreaterThan(0.04); }); });