134 lines
5.3 KiB
TypeScript
134 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()
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|