brackt/app/lib/__tests__/fifa-2026-bracket.test.ts
Chris Parsons 6772079e86
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
Reflect group stage in World Cup sim; seed R32 from real 2026 bracket
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>
2026-06-23 12:15:45 -07:00

133 lines
5.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 116", () => {
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 7388", () => {
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 AH (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 EL 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()
);
}
});
});