import { describe, it, expect, vi, beforeEach, type MockInstance } from "vitest"; import { LLWSSimulator, makePlayGame, playCrossoverGame, readBracketSlots, } from "../llws-simulator"; import { convertAmericanOddsToProbability } from "~/services/probability-engine"; import type { SimulationResult } from "../types"; vi.mock("~/database/context", () => ({ database: vi.fn(), })); vi.mock("~/services/probability-engine", async (importOriginal) => { const actual = await importOriginal() as Record; return { ...actual }; }); // ─── Fixtures ───────────────────────────────────────────────────────────────── const US_IDS = Array.from({ length: 10 }, (_, i) => `us-${i + 1}`); const INTL_IDS = Array.from({ length: 10 }, (_, i) => `intl-${i + 1}`); const ALL_IDS = [...US_IDS, ...INTL_IDS]; /** * Build EV rows with descending odds favouring the first team per side. * ids[0] is the strongest (best odds → lowest American number for favorites). */ function makeEvRows(ids: string[], opts: { includeOdds?: boolean } = {}) { return ids.map((participantId, i) => ({ participantId, sourceOdds: opts.includeOdds ? (i === 0 ? -300 : 200 + i * 100) : null, })); } // ─── Bracket fixtures ───────────────────────────────────────────────────────── /** The subset of playoff_matches columns the simulator reads. */ type PlayoffMatchRow = { round: string; matchNumber: number; participant1Id: string | null; participant2Id: string | null; winnerId: string | null; loserId: string | null; isComplete: boolean; }; const EMPTY_MATCH: PlayoffMatchRow = { round: "", matchNumber: 0, participant1Id: null, participant2Id: null, winnerId: null, loserId: null, isComplete: false, }; /** * A freshly generated, fully seeded llws_20 bracket with no results recorded. * * Mirrors generateLLWS20Bracket: U.S. matches take the low match numbers * (Opening Round 1–4, Winners Round 2 1–2), International the high ones * (Opening Round 5–8, Winners Round 2 3–4). Byes sit at participant1 of * Winners Round 2. Slot order per side is ids[0..7] opening, ids[8..9] byes. */ function seededBracket(): PlayoffMatchRow[] { const matches: PlayoffMatchRow[] = []; const sides = [ { ids: US_IDS, openingOffset: 0, wr2Offset: 0 }, { ids: INTL_IDS, openingOffset: 4, wr2Offset: 2 }, ]; for (const { ids, openingOffset, wr2Offset } of sides) { for (let local = 1; local <= 4; local++) { matches.push({ ...EMPTY_MATCH, round: "Opening Round", matchNumber: local + openingOffset, participant1Id: ids[(local - 1) * 2], participant2Id: ids[(local - 1) * 2 + 1], }); } for (let local = 1; local <= 2; local++) { matches.push({ ...EMPTY_MATCH, round: "Winners Round 2", matchNumber: local + wr2Offset, participant1Id: ids[8 + (local - 1)], }); } } return matches; } /** * Mark a bracket match complete, the way the scoring flow would once the game is * played. `loserId` is passed explicitly for matches whose second slot is filled by * advancement rather than by the initial seeding. */ function completeMatch( matches: PlayoffMatchRow[], round: string, matchNumber: number, winnerId: string, loserId: string ): PlayoffMatchRow[] { const existing = matches.find((m) => m.round === round && m.matchNumber === matchNumber); const filled: PlayoffMatchRow = { ...(existing ?? { ...EMPTY_MATCH, round, matchNumber }), participant1Id: existing?.participant1Id ?? winnerId, participant2Id: existing?.participant2Id ?? loserId, winnerId, loserId, isComplete: true, }; return [...matches.filter((m) => m !== existing), filled]; } /** Normalized (vig-removed) market probability for each team in an odds board. */ function marketProbabilities(odds: number[]): number[] { const raw = odds.map(convertAmericanOddsToProbability); const sum = raw.reduce((a, b) => a + b, 0); return raw.map((p) => p / sum); } /** Look up one participant's simulated probabilities, failing loudly if absent. */ function probsFor(results: SimulationResult[], participantId: string) { const match = results.find((r) => r.participantId === participantId); if (!match) throw new Error(`No simulation result for ${participantId}`); return match.probabilities; } /** Equal-strength Team records for direct (non-Monte-Carlo) helper tests. */ const TEST_TEAMS = new Map( ALL_IDS.map((id) => [ id, { participantId: id, side: id.startsWith("us") ? ("US" as const) : ("Intl" as const), elo: 1500, }, ]) ); function team(participantId: string) { const found = TEST_TEAMS.get(participantId); if (!found) throw new Error(`No test team for ${participantId}`); return found; } // ─── Tests ──────────────────────────────────────────────────────────────────── describe("LLWSSimulator", () => { let mockDb: { select: MockInstance; query: { scoringEvents: { findFirst: MockInstance }; playoffMatches: { findMany: MockInstance }; }; }; let selectCallCount: number; beforeEach(async () => { selectCallCount = 0; const { database } = await import("~/database/context"); mockDb = { select: vi.fn(), query: { scoringEvents: { findFirst: vi.fn().mockResolvedValue(undefined) }, playoffMatches: { findMany: vi.fn().mockResolvedValue([]) }, }, }; (database as unknown as MockInstance).mockReturnValue(mockDb); }); function setupMockDb( participants: { id: string; name?: string; externalId: string | null }[], evRows: { participantId: string; sourceOdds: number | null }[], bracketMatches?: Partial[] ) { selectCallCount = 0; mockDb.select.mockImplementation(() => { const callIndex = selectCallCount++; const data = callIndex === 0 ? participants : evRows; return { from: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue(data) }) }; }); if (bracketMatches) { mockDb.query.scoringEvents.findFirst.mockResolvedValue({ id: "event-1" }); mockDb.query.playoffMatches.findMany.mockResolvedValue( bracketMatches.map((m) => ({ ...EMPTY_MATCH, ...m })) ); } } function defaultParticipants(mode: "randomized" | "fixed" = "randomized") { if (mode === "fixed") { const usA = US_IDS.slice(0, 5).map((id) => ({ id, name: `US Team ${id}`, externalId: "US:A" })); const usB = US_IDS.slice(5).map((id) => ({ id, name: `US Team ${id}`, externalId: "US:B" })); const intlA = INTL_IDS.slice(0, 5).map((id) => ({ id, name: `Team ${id}`, externalId: "Intl:A" })); const intlB = INTL_IDS.slice(5).map((id) => ({ id, name: `Team ${id}`, externalId: "Intl:B" })); return [...usA, ...usB, ...intlA, ...intlB]; } return [ ...US_IDS.map((id) => ({ id, name: `US Team ${id}`, externalId: "US" })), ...INTL_IDS.map((id) => ({ id, name: `Team ${id}`, externalId: "Intl" })), ]; } // ── Core output structure ───────────────────────────────────────────────── describe("output structure", () => { it("returns one result per participant (20 total)", async () => { setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS)); const results = await new LLWSSimulator(1_000).simulate("season-1"); expect(results).toHaveLength(20); }); it("every result has source 'llws_monte_carlo'", async () => { setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS)); const results = await new LLWSSimulator(1_000).simulate("season-1"); for (const r of results) { expect(r.source).toBe("llws_monte_carlo"); } }); it("all probability values are between 0 and 1", async () => { setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS)); const results = await new LLWSSimulator(1_000).simulate("season-1"); for (const r of results) { const p = r.probabilities; for (const v of Object.values(p)) { expect(v).toBeGreaterThanOrEqual(0); expect(v).toBeLessThanOrEqual(1); } } }); }); // ── Probability conservation ────────────────────────────────────────────── describe("probability conservation (one winner per sim)", () => { it("probFirst sums to ~1.0 across all participants", async () => { setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS)); const results = await new LLWSSimulator(1_000).simulate("season-1"); const total = results.reduce((s, r) => s + r.probabilities.probFirst, 0); expect(total).toBeCloseTo(1.0, 1); }); it("probSecond sums to ~1.0 across all participants", async () => { setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS)); const results = await new LLWSSimulator(1_000).simulate("season-1"); const total = results.reduce((s, r) => s + r.probabilities.probSecond, 0); expect(total).toBeCloseTo(1.0, 1); }); it("probThird sums to ~1.0 across all participants", async () => { setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS)); const results = await new LLWSSimulator(1_000).simulate("season-1"); const total = results.reduce((s, r) => s + r.probabilities.probThird, 0); expect(total).toBeCloseTo(1.0, 1); }); it("probFourth sums to ~1.0 across all participants", async () => { setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS)); const results = await new LLWSSimulator(1_000).simulate("season-1"); const total = results.reduce((s, r) => s + r.probabilities.probFourth, 0); expect(total).toBeCloseTo(1.0, 1); }); it("probFifth sums to ~1.0 (2 Elimination Final losers per sim, split over 5th/6th)", async () => { setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS)); const results = await new LLWSSimulator(1_000).simulate("season-1"); const total = results.reduce((s, r) => s + r.probabilities.probFifth, 0); expect(total).toBeCloseTo(1.0, 1); }); it("probSeventh sums to ~1.0 (2 Elimination Round 4 losers per sim, split over 7th/8th)", async () => { setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS)); const results = await new LLWSSimulator(1_000).simulate("season-1"); const total = results.reduce((s, r) => s + r.probabilities.probSeventh, 0); expect(total).toBeCloseTo(1.0, 1); }); it("ties 5th with 6th and 7th with 8th, but keeps the two tiers separate", async () => { setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS, { includeOdds: true })); const results = await new LLWSSimulator(2_000).simulate("season-1"); for (const r of results) { const p = r.probabilities; // Within a tier the two positions are tied. expect(p.probFifth).toBeCloseTo(p.probSixth, 10); expect(p.probSeventh).toBeCloseTo(p.probEighth, 10); } // The tiers are distinct outcomes (losing the Elimination Final vs losing // Elimination Round 4), so they must not be forced equal across the field. const differs = results.some( (r) => Math.abs(r.probabilities.probFifth - r.probabilities.probSeventh) > 1e-9 ); expect(differs).toBe(true); }); it("gives every team a total placement probability of at most 1", async () => { setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS)); const results = await new LLWSSimulator(1_000).simulate("season-1"); for (const r of results) { const p = r.probabilities; // Each sim assigns a team at most one placement, so summing the distinct // tiers (5th/6th and 7th/8th each count once) cannot exceed 1. const total = p.probFirst + p.probSecond + p.probThird + p.probFourth + p.probFifth * 2 + p.probSeventh * 2; expect(total).toBeLessThanOrEqual(1 + 1e-9); } }); }); // ── Odds-driven probability ─────────────────────────────────────────────── describe("odds-driven win probability", () => { it("strong favourite (us-1) has higher probFirst than a weak team", async () => { setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS, { includeOdds: true })); const results = await new LLWSSimulator(1_000).simulate("season-1"); const byId = new Map(results.map((r) => [r.participantId, r])); const us1prob = byId.get("us-1")?.probabilities.probFirst ?? 0; const us10prob = byId.get("us-10")?.probabilities.probFirst ?? 0; expect(us1prob).toBeGreaterThan(us10prob); }); it("works when no odds are entered (all 50/50 fallback)", async () => { setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS)); // no odds const results = await new LLWSSimulator(1_000).simulate("season-1"); const total = results.reduce((s, r) => s + r.probabilities.probFirst, 0); expect(total).toBeCloseTo(1.0, 1); }); it("with equal odds, each team wins the championship roughly equally", async () => { // Equal positive odds (+5000 for every team) → vig-removed prob ≈ 1/20 each. const eqOddsRows = ALL_IDS.map((id) => ({ participantId: id, sourceOdds: 5000 })); setupMockDb(defaultParticipants(), eqOddsRows); const results = await new LLWSSimulator(1_000).simulate("season-1"); for (const r of results) { // With equal odds and random pools, each team should win ~5% of the time. // Allow a generous band given Monte Carlo variance. expect(r.probabilities.probFirst).toBeGreaterThan(0.01); expect(r.probabilities.probFirst).toBeLessThan(0.15); } }); }); // ── Legacy externalId formats ───────────────────────────────────────────── // // The tournament no longer has pool play, but seasons configured for the old // format still carry pool suffixes. Those must keep loading, read as the side alone. describe("legacy pool-suffix externalIds", () => { it("accepts US:A / US:B / Intl:A / Intl:B, ignoring the pool part", async () => { setupMockDb(defaultParticipants("fixed"), makeEvRows(ALL_IDS)); const results = await new LLWSSimulator(1_000).simulate("season-1"); expect(results).toHaveLength(20); const total = results.reduce((s, r) => s + r.probabilities.probFirst, 0); expect(total).toBeCloseTo(1.0, 1); }); it("accepts a mix of suffixed and bare side ids", async () => { const participants = [ ...US_IDS.slice(0, 5).map((id) => ({ id, name: `US Team ${id}`, externalId: "US:A" })), ...US_IDS.slice(5).map((id) => ({ id, name: `US Team ${id}`, externalId: "US" })), ...INTL_IDS.map((id) => ({ id, name: `Team ${id}`, externalId: "Intl" })), ]; setupMockDb(participants, makeEvRows(ALL_IDS)); const results = await new LLWSSimulator(1_000).simulate("season-1"); expect(results).toHaveLength(20); const total = results.reduce((s, r) => s + r.probabilities.probFirst, 0); expect(total).toBeCloseTo(1.0, 1); }); it("accepts an uneven suffix split (pools no longer constrain anything)", async () => { const participants = [ ...US_IDS.slice(0, 6).map((id) => ({ id, name: `US Team ${id}`, externalId: "US:A" })), ...US_IDS.slice(6).map((id) => ({ id, name: `US Team ${id}`, externalId: "US:B" })), ...INTL_IDS.map((id) => ({ id, name: `Team ${id}`, externalId: "Intl" })), ]; setupMockDb(participants, makeEvRows(ALL_IDS)); const results = await new LLWSSimulator(1_000).simulate("season-1"); expect(results).toHaveLength(20); }); }); // ── Error cases ─────────────────────────────────────────────────────────── describe("error cases", () => { it("throws when participant count is not 20", async () => { const nineteen = [...US_IDS, ...INTL_IDS.slice(0, 9)]; const participants = nineteen.map((id, i) => ({ id, name: i < 10 ? `US Team ${id}` : `Team ${id}`, externalId: i < 10 ? "US" : "Intl", })); setupMockDb(participants, makeEvRows(nineteen)); await expect(new LLWSSimulator(1_000).simulate("season-1")).rejects.toThrow(/exactly 20/); }); it("infers US side from name prefix when externalId is null", async () => { const participants = [ ...US_IDS.map((id) => ({ id, name: `US Team ${id}`, externalId: null })), ...INTL_IDS.map((id) => ({ id, name: `Japan ${id}`, externalId: null })), ]; setupMockDb(participants, makeEvRows(ALL_IDS)); const results = await new LLWSSimulator(1_000).simulate("season-1"); expect(results).toHaveLength(20); const total = results.reduce((s, r) => s + r.probabilities.probFirst, 0); expect(total).toBeCloseTo(1.0, 1); }); it("infers US side from exact name 'US' when externalId is null", async () => { const participants = [ ...US_IDS.map((id) => ({ id, name: "US", externalId: null })), ...INTL_IDS.map((id) => ({ id, name: `Team ${id}`, externalId: null })), ]; setupMockDb(participants, makeEvRows(ALL_IDS)); const results = await new LLWSSimulator(1_000).simulate("season-1"); expect(results).toHaveLength(20); const total = results.reduce((s, r) => s + r.probabilities.probFirst, 0); expect(total).toBeCloseTo(1.0, 1); }); it("throws when a participant has an unrecognized non-null externalId", async () => { const participants = [ ...US_IDS.map((id) => ({ id, name: `US Team ${id}`, externalId: "US" })), ...INTL_IDS.slice(0, 9).map((id) => ({ id, name: `Team ${id}`, externalId: "Intl" })), { id: "intl-10", name: "Team intl-10", externalId: "CANADA" }, // unrecognized ]; setupMockDb(participants, makeEvRows(ALL_IDS)); await expect(new LLWSSimulator(1_000).simulate("season-1")).rejects.toThrow(/invalid externalId/); }); it("throws when US team count is not 10", async () => { // 11 US teams, 9 International const participants = [ ...Array.from({ length: 11 }, (_, i) => ({ id: `us-${i + 1}`, name: `US Team ${i + 1}`, externalId: "US" })), ...Array.from({ length: 9 }, (_, i) => ({ id: `intl-${i + 1}`, name: `Team ${i + 1}`, externalId: "Intl" })), ]; setupMockDb(participants, makeEvRows(ALL_IDS)); await expect(new LLWSSimulator(1_000).simulate("season-1")).rejects.toThrow(/10 US teams/); }); it("throws when International team count is not 10", async () => { const participants = [ ...Array.from({ length: 9 }, (_, i) => ({ id: `us-${i + 1}`, name: `US Team ${i + 1}`, externalId: "US" })), ...Array.from({ length: 11 }, (_, i) => ({ id: `intl-${i + 1}`, name: `Team ${i + 1}`, externalId: "Intl" })), ]; setupMockDb(participants, makeEvRows(ALL_IDS)); await expect(new LLWSSimulator(1_000).simulate("season-1")).rejects.toThrow(/10 US teams/); }); }); // ── Futures calibration ─────────────────────────────────────────────────── // // A championship future already contains the ~6 wins needed to lift the trophy. // Feeding it straight into a single game (p1 / (p1 + p2)) makes every game as // lopsided as the whole tournament and compounds the favorite's edge round after // round, which inflated favorites badly. The simulator decompresses futures to Elo // first, so re-simulating a random draw should hand back roughly the prices it was // given rather than a much more extreme distribution. describe("futures calibration", () => { // A representative LLWS board: a clear favorite, a long tail. const BOARD = [ 200, 750, 900, 1200, 1600, 2000, 2500, 3000, 4000, 6000, 350, 800, 1000, 1400, 1800, 2200, 2800, 3500, 5000, 8000, ]; function boardEvRows() { return ALL_IDS.map((participantId, i) => ({ participantId, sourceOdds: BOARD[i] })); } it("reproduces the favorite's championship price instead of inflating it", async () => { setupMockDb(defaultParticipants(), boardEvRows()); const results = await new LLWSSimulator(20_000).simulate("season-1"); const market = marketProbabilities(BOARD); const simulated = probsFor(results, "us-1").probFirst; // The favorite prices around 22%. The old raw-futures model simulated ~45%. expect(simulated).toBeCloseTo(market[0], 1); expect(simulated).toBeLessThan(market[0] + 0.06); }); it("keeps the whole field close to its priced championship probability", async () => { setupMockDb(defaultParticipants(), boardEvRows()); const results = await new LLWSSimulator(20_000).simulate("season-1"); const market = marketProbabilities(BOARD); const errors = ALL_IDS.map((id, i) => probsFor(results, id).probFirst - market[i]); const rmse = Math.sqrt(errors.reduce((s, e) => s + e * e, 0) / errors.length); // Calibrated RMSE is ~0.003; the old model sat around 0.06. expect(rmse).toBeLessThan(0.02); }); it("does not starve longshots of championship probability", async () => { setupMockDb(defaultParticipants(), boardEvRows()); const results = await new LLWSSimulator(20_000).simulate("season-1"); // The longest shot on the board prices near 0.8%. Compounding raw futures drove // teams like this to essentially zero. const longshot = probsFor(results, "intl-10").probFirst; expect(longshot).toBeGreaterThan(0.002); }); }); // ── Bracket-aware mode ──────────────────────────────────────────────────── describe("bracket-aware mode", () => { it("uses the real draw rather than shuffling when a bracket is seeded", async () => { // With no odds every team is equally strong, so the only edge is structural: // the two bye teams skip the Opening Round. Under a randomized draw every team // gets a bye equally often and this difference disappears. setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS), seededBracket()); const results = await new LLWSSimulator(20_000).simulate("season-1"); const byeTeam = probsFor(results, "us-9").probFirst; const openingTeam = probsFor(results, "us-1").probFirst; expect(byeTeam).toBeGreaterThan(openingTeam); }); it("still returns a full, normalized distribution in bracket mode", async () => { setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS), seededBracket()); const results = await new LLWSSimulator(5_000).simulate("season-1"); expect(results).toHaveLength(20); expect(results.reduce((s, r) => s + r.probabilities.probFirst, 0)).toBeCloseTo(1.0, 1); expect(results.reduce((s, r) => s + r.probabilities.probThird, 0)).toBeCloseTo(1.0, 1); }); it("falls back to a randomized draw when the bracket has no participants seeded", async () => { const unseeded = seededBracket().map((m) => ({ ...m, participant1Id: null, participant2Id: null, })); setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS), unseeded); const results = await new LLWSSimulator(5_000).simulate("season-1"); expect(results).toHaveLength(20); expect(results.reduce((s, r) => s + r.probabilities.probFirst, 0)).toBeCloseTo(1.0, 1); }); it("throws when the bracket seeds the same team into two slots", async () => { const duplicated = seededBracket().map((m) => m.round === "Opening Round" && m.matchNumber === 2 ? { ...m, participant1Id: "us-1" } // us-1 already opens match 1 : m ); setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS), duplicated); await expect(new LLWSSimulator(100).simulate("season-1")).rejects.toThrow( /more than one slot/ ); }); it("takes sides from the bracket, not externalId, once a bracket is seeded", async () => { // The bracket is authoritative about the draw, so an externalId the pre-bracket // path would reject must not block a season that already has a real bracket. const participants = [ ...US_IDS.map((id) => ({ id, name: `US Team ${id}`, externalId: "US" })), ...INTL_IDS.slice(0, 9).map((id) => ({ id, name: `Team ${id}`, externalId: "Intl" })), { id: "intl-10", name: "Team intl-10", externalId: "CANADA" }, ]; setupMockDb(participants, makeEvRows(ALL_IDS), seededBracket()); const results = await new LLWSSimulator(5_000).simulate("season-1"); expect(results).toHaveLength(20); expect(results.reduce((s, r) => s + r.probabilities.probFirst, 0)).toBeCloseTo(1.0, 1); }); it("throws when the bracket is seeded with a participant outside the season", async () => { const foreign = seededBracket().map((m) => m.round === "Opening Round" && m.matchNumber === 1 ? { ...m, participant1Id: "stranger-1" } : m ); setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS), foreign); await expect(new LLWSSimulator(100).simulate("season-1")).rejects.toThrow( /not in this sports season/ ); }); }); // ── Completed results ───────────────────────────────────────────────────── // // The core of the fix: games already played must stick across every iteration // instead of being re-simulated from scratch. describe("completed results", () => { // us-1 is a strong favorite, so a recorded loss should visibly move its number. const favouredEvRows = ALL_IDS.map((participantId, i) => ({ participantId, sourceOdds: participantId === "us-1" ? 200 : 1000 + i * 200, })); async function probFirstFor(id: string, matches: PlayoffMatchRow[]): Promise { setupMockDb(defaultParticipants(), favouredEvRows, matches); const results = await new LLWSSimulator(20_000).simulate("season-1"); return probsFor(results, id).probFirst; } it("drops a favorite's championship probability after a recorded loss", async () => { const before = await probFirstFor("us-1", seededBracket()); // us-1 loses its Opening Round game. In double elimination that is not an // elimination — it drops to the elimination bracket — but it now needs a much // longer path, so its title probability must fall. const afterLoss = completeMatch(seededBracket(), "Opening Round", 1, "us-2", "us-1"); const after = await probFirstFor("us-1", afterLoss); expect(after).toBeLessThan(before); // Not merely noise: a first-round loss is a real blow to a favorite. expect(after).toBeLessThan(before * 0.8); // But not elimination either — the elimination bracket still reaches the final. expect(after).toBeGreaterThan(0); }); it("raises the opponent's championship probability after that same win", async () => { const before = await probFirstFor("us-2", seededBracket()); const afterWin = completeMatch(seededBracket(), "Opening Round", 1, "us-2", "us-1"); const after = await probFirstFor("us-2", afterWin); expect(after).toBeGreaterThan(before); }); it("zeroes out a team that has been eliminated (two recorded losses)", async () => { // Fill the elimination-bracket game the way advancement would: the Opening // Round 1 and Opening Round 4 losers meet in Elimination Round 1 match 2. let matches = seededBracket(); matches = completeMatch(matches, "Opening Round", 1, "us-2", "us-1"); matches = completeMatch(matches, "Opening Round", 4, "us-7", "us-8"); matches = completeMatch(matches, "Elimination Round 1", 2, "us-8", "us-1"); setupMockDb(defaultParticipants(), favouredEvRows, matches); const results = await new LLWSSimulator(5_000).simulate("season-1"); const eliminated = probsFor(results, "us-1"); // A second loss is final — every placement tier must be exactly zero. for (const value of Object.values(eliminated)) { expect(value).toBe(0); } }); it("keeps the distribution normalized once results have been recorded", async () => { let matches = seededBracket(); matches = completeMatch(matches, "Opening Round", 1, "us-2", "us-1"); matches = completeMatch(matches, "Opening Round", 5, "intl-2", "intl-1"); setupMockDb(defaultParticipants(), favouredEvRows, matches); const results = await new LLWSSimulator(5_000).simulate("season-1"); expect(results.reduce((s, r) => s + r.probabilities.probFirst, 0)).toBeCloseTo(1.0, 1); expect(results.reduce((s, r) => s + r.probabilities.probSecond, 0)).toBeCloseTo(1.0, 1); expect(results.reduce((s, r) => s + r.probabilities.probFifth, 0)).toBeCloseTo(1.0, 1); }); it("ignores a completed result whose participants never reach that game", async () => { // A corrupt row: Elimination Round 1 match 2 takes the Opening Round 1 and 4 // losers, so a team from Opening Round 3 can never appear there. The game must // be simulated instead of desynchronising the rest of the bracket. const matches = completeMatch( seededBracket(), "Elimination Round 1", 2, "us-5", "us-6" ); setupMockDb(defaultParticipants(), favouredEvRows, matches); const results = await new LLWSSimulator(5_000).simulate("season-1"); expect(results).toHaveLength(20); expect(results.reduce((s, r) => s + r.probabilities.probFirst, 0)).toBeCloseTo(1.0, 1); }); }); // ── Result-honoring rules ───────────────────────────────────────────────── // // Tested directly rather than through the Monte Carlo output: the aggregate only // shows these effects diluted by how often a given pairing occurs, which is too // noisy to assert on. describe("result-honoring rules", () => { function bracketOf(matches: PlayoffMatchRow[]) { const bracket = readBracketSlots(matches, TEST_TEAMS); if (!bracket) throw new Error("Expected the seeded bracket to be readable"); return bracket; } const us1 = team("us-1"); const us2 = team("us-2"); const us5 = team("us-5"); it("replays a completed game from its recorded result", () => { const bracket = bracketOf( completeMatch(seededBracket(), "Opening Round", 1, "us-2", "us-1") ); const play = makePlayGame(0, bracket, 1_000); // Deterministic across repeats — no coin flip is involved any more. for (let i = 0; i < 25; i++) { const result = play("Opening Round", 1, us1, us2); expect(result.winner.participantId).toBe("us-2"); expect(result.loser.participantId).toBe("us-1"); } }); it("returns the recorded winner regardless of which slot it arrives in", () => { const bracket = bracketOf( completeMatch(seededBracket(), "Opening Round", 1, "us-1", "us-2") ); const play = makePlayGame(0, bracket, 1_000); // Same game, arguments swapped. expect(play("Opening Round", 1, us2, us1).winner.participantId).toBe("us-1"); }); it("simulates a game that has not been played yet", () => { const play = makePlayGame(0, bracketOf(seededBracket()), 1_000); const winners = new Set( Array.from({ length: 200 }, () => play("Opening Round", 1, us1, us2).winner.participantId) ); // Equal Elo, so both outcomes must show up. expect(winners).toEqual(new Set(["us-1", "us-2"])); }); it("ignores a recorded result between teams that did not arrive at the game", () => { const bracket = bracketOf( completeMatch(seededBracket(), "Opening Round", 1, "us-5", "us-2") ); const play = makePlayGame(0, bracket, 1_000); // us-5 belongs to a different Opening Round game, so this row cannot apply to // the us-1 v us-2 pairing — it must be simulated instead. const winners = new Set( Array.from({ length: 200 }, () => play("Opening Round", 1, us1, us2).winner.participantId) ); expect(winners).toEqual(new Set(["us-1", "us-2"])); }); it("reads U.S. and International games from their own match numbers", () => { // The same side-local game number maps to different global matches per side: // U.S. Opening Round 1 is match 1, International Opening Round 1 is match 5. const bracket = bracketOf( completeMatch(seededBracket(), "Opening Round", 5, "intl-2", "intl-1") ); const intl1 = team("intl-1"); const intl2 = team("intl-2"); expect(makePlayGame(1, bracket, 1_000)("Opening Round", 1, intl1, intl2).winner.participantId) .toBe("intl-2"); // The U.S. side's Opening Round 1 is untouched by that result. const usWinners = new Set( Array.from({ length: 200 }, () => makePlayGame(0, bracket, 1_000)("Opening Round", 1, us1, us2).winner.participantId ) ); expect(usWinners).toEqual(new Set(["us-1", "us-2"])); }); it("honors a completed World Championship", () => { // The two crossover games are single shared matches, numbered 1. const bracket = bracketOf( completeMatch(seededBracket(), "World Championship", 1, "us-3", "intl-4") ); const us3 = team("us-3"); const intl4 = team("intl-4"); const result = playCrossoverGame("World Championship", bracket, 1_000, us3, intl4); expect(result.winner.participantId).toBe("us-3"); expect(result.loser.participantId).toBe("intl-4"); }); it("simulates the crossover game when different finalists arrive", () => { const bracket = bracketOf( completeMatch(seededBracket(), "World Championship", 1, "us-3", "intl-4") ); const intl5 = team("intl-5"); const winners = new Set( Array.from({ length: 200 }, () => playCrossoverGame("World Championship", bracket, 1_000, us5, intl5).winner.participantId ) ); expect(winners).toEqual(new Set(["us-5", "intl-5"])); }); }); });