All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 2m53s
🚀 Deploy / ʦ🔍 Typecheck & Lint (pull_request) Successful in 1m19s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
🚀 Deploy / 🧪 Test (push) Successful in 3m6s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m22s
🚀 Deploy / 🐳 Build (push) Successful in 1m13s
🚀 Deploy / 🚀 Deploy (push) Successful in 13s
The World Cup Monte Carlo simulator ignored the admin-populated knockout draw and re-paired its own simulated group output sequentially, so bracket paths from the Round of 32 onward did not match the real tournament. It also silently invented 12 synthetic groups when none were configured, discarding real group-stage results. Rework the simulator around three regimes: - draw set → simulate the real R32 matchups, advancing through the bracket tree via the canonical ceil(matchNumber/2) rule (matching advanceWinnerTemplate) so sim paths match the admin tooling. - groups only → simulate remaining group matches, then seed the R32 with the official 2026 group-position template + FIFA's exact Annex C best-third-place allocation (495-row published table). - neither → futures-seeded bracket fallback, flagged via result source. Completed group and knockout matches still lock in real results at every round. New app/lib/fifa-2026-bracket.ts encodes the R32 group-position template (official FIFA matches 73-88 mapped onto DB matchNumbers 1-16 so the tree reproduces the real halves) and assignThirdPlaceSlots(). The Annex C table is generated deterministically by scripts/gen-fifa-third-place.mjs into app/lib/fifa-2026-third-place-allocation.ts. Tests assert exact Annex C values, cross-check the template's eligible groups against all 495 combinations, and cover each regime including the partial-draw dedup and fully-drawn fast path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
133 lines
5.3 KiB
TypeScript
133 lines
5.3 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
||
import {
|
||
FIFA_2026_R32_TEMPLATE,
|
||
THIRD_PLACE_SLOTS,
|
||
assignThirdPlaceSlots,
|
||
type R32Slot,
|
||
} from "../fifa-2026-bracket";
|
||
|
||
const GROUPS = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L"];
|
||
|
||
describe("FIFA_2026_R32_TEMPLATE", () => {
|
||
it("has 16 matches with unique DB match numbers 1–16", () => {
|
||
expect(FIFA_2026_R32_TEMPLATE).toHaveLength(16);
|
||
const dbNums = FIFA_2026_R32_TEMPLATE.map((m) => m.dbMatchNumber).toSorted((a, b) => a - b);
|
||
expect(dbNums).toEqual(Array.from({ length: 16 }, (_, i) => i + 1));
|
||
});
|
||
|
||
it("maps onto the official FIFA match numbers 73–88", () => {
|
||
const fifaNums = FIFA_2026_R32_TEMPLATE.map((m) => m.fifaMatchNumber).toSorted((a, b) => a - b);
|
||
expect(fifaNums).toEqual(Array.from({ length: 16 }, (_, i) => i + 73));
|
||
});
|
||
|
||
it("uses each group winner and runner-up exactly once across the 32 slots", () => {
|
||
const winners: string[] = [];
|
||
const runnersUp: string[] = [];
|
||
for (const m of FIFA_2026_R32_TEMPLATE) {
|
||
for (const slot of [m.slot1, m.slot2] as R32Slot[]) {
|
||
if (slot.kind === "winner") winners.push(slot.group);
|
||
if (slot.kind === "runnerUp") runnersUp.push(slot.group);
|
||
}
|
||
}
|
||
expect(winners.toSorted()).toEqual(GROUPS);
|
||
expect(runnersUp.toSorted()).toEqual(GROUPS);
|
||
});
|
||
|
||
it("has exactly 8 third-place slots", () => {
|
||
expect(THIRD_PLACE_SLOTS).toHaveLength(8);
|
||
});
|
||
});
|
||
|
||
describe("assignThirdPlaceSlots", () => {
|
||
it("assigns all 8 qualifying thirds to distinct, eligible slots", () => {
|
||
// Best-8 thirds come from groups A–H (a plausible qualifying set).
|
||
const qualifying = ["A", "B", "C", "D", "E", "F", "G", "H"];
|
||
const assignment = assignThirdPlaceSlots(qualifying);
|
||
|
||
expect(assignment.size).toBe(8);
|
||
|
||
// Every slot filled, each by an eligible & qualifying group, no group reused.
|
||
const usedGroups = new Set<string>();
|
||
for (const slot of THIRD_PLACE_SLOTS) {
|
||
const group = assignment.get(slot.thirdSlotId) ?? "";
|
||
expect(group).not.toBe("");
|
||
expect(slot.eligibleGroups).toContain(group);
|
||
expect(qualifying).toContain(group);
|
||
expect(usedGroups.has(group)).toBe(false);
|
||
usedGroups.add(group);
|
||
}
|
||
expect(usedGroups.size).toBe(8);
|
||
});
|
||
|
||
it("produces a valid matching for every 8-of-12 qualifying combination", () => {
|
||
// Exhaustively check all C(12,8) = 495 combinations resolve to a complete,
|
||
// eligibility-respecting, conflict-free assignment.
|
||
let combinations = 0;
|
||
const combos = (start: number, chosen: string[]) => {
|
||
if (chosen.length === 8) {
|
||
combinations++;
|
||
const assignment = assignThirdPlaceSlots(chosen);
|
||
expect(assignment.size).toBe(8);
|
||
const used = new Set(assignment.values());
|
||
expect(used.size).toBe(8);
|
||
for (const slot of THIRD_PLACE_SLOTS) {
|
||
const group = assignment.get(slot.thirdSlotId);
|
||
expect(slot.eligibleGroups).toContain(group);
|
||
expect(chosen).toContain(group);
|
||
}
|
||
return;
|
||
}
|
||
for (let i = start; i < GROUPS.length; i++) {
|
||
combos(i + 1, [...chosen, GROUPS[i]]);
|
||
}
|
||
};
|
||
combos(0, []);
|
||
expect(combinations).toBe(495);
|
||
});
|
||
|
||
it("is deterministic for a given combination", () => {
|
||
const qualifying = ["B", "D", "E", "F", "H", "I", "J", "L"];
|
||
const a = assignThirdPlaceSlots(qualifying);
|
||
const b = assignThirdPlaceSlots(qualifying.toReversed());
|
||
expect([...a.entries()].toSorted()).toEqual([...b.entries()].toSorted());
|
||
});
|
||
|
||
it("matches FIFA's exact Annex C allocation for option 1 (groups E–L qualify)", () => {
|
||
// Published row 1: 1A vs 3E, 1B vs 3J, 1D vs 3I, 1E vs 3F,
|
||
// 1G vs 3H, 1I vs 3G, 1K vs 3L, 1L vs 3K.
|
||
// Mapped to DB third-slot ids (the match each winner plays in):
|
||
const assignment = assignThirdPlaceSlots(["E", "F", "G", "H", "I", "J", "K", "L"]);
|
||
expect(assignment.get(11)).toBe("E"); // winner A's match
|
||
expect(assignment.get(15)).toBe("J"); // winner B's match
|
||
expect(assignment.get(7)).toBe("I"); // winner D's match
|
||
expect(assignment.get(1)).toBe("F"); // winner E's match
|
||
expect(assignment.get(8)).toBe("H"); // winner G's match
|
||
expect(assignment.get(2)).toBe("G"); // winner I's match
|
||
expect(assignment.get(16)).toBe("L"); // winner K's match
|
||
expect(assignment.get(12)).toBe("K"); // winner L's match
|
||
});
|
||
|
||
it("template eligible-group lists exactly match the Annex C table", () => {
|
||
// For every slot, collect the set of groups the published table ever assigns
|
||
// to it across all 495 combinations, and assert it equals the hand-written
|
||
// eligibleGroups list in the template.
|
||
const observed = new Map<number, Set<string>>(
|
||
THIRD_PLACE_SLOTS.map((s) => [s.thirdSlotId, new Set<string>()])
|
||
);
|
||
const combos = (start: number, chosen: string[]) => {
|
||
if (chosen.length === 8) {
|
||
const assignment = assignThirdPlaceSlots(chosen);
|
||
for (const [slotId, group] of assignment) observed.get(slotId)?.add(group);
|
||
return;
|
||
}
|
||
for (let i = start; i < GROUPS.length; i++) combos(i + 1, [...chosen, GROUPS[i]]);
|
||
};
|
||
combos(0, []);
|
||
|
||
for (const slot of THIRD_PLACE_SLOTS) {
|
||
expect([...(observed.get(slot.thirdSlotId) ?? [])].toSorted()).toEqual(
|
||
[...slot.eligibleGroups].toSorted()
|
||
);
|
||
}
|
||
});
|
||
});
|