Fixes #124 * Add NCAA Football CFP simulator (12-team bracket) Implements a Monte Carlo simulator for the College Football Playoff using the 2024-present 12-team format. Elo/FPI ratings are entered manually via the existing admin Elo Ratings page; championship futures odds can optionally be blended in (60% Elo / 40% odds). - Add CFP_12 bracket template (First Round not scoring, QFs onward score) - Add generateCFP12Bracket() with correct seeding: 5v12, 6v11, 7v10, 8v9 in First Round; seeds 1–4 receive QF byes - Add NCAAFootballSimulator: 50k Monte Carlo sims, seeds teams by blended Elo+odds strength, tracks champion/finalist/SF/QF placement tiers - Register ncaa_football_bracket simulator type in registry and schema enum - Add migration 0071: ALTER TYPE simulator_type ADD VALUE 'ncaa_football_bracket' - Add tests: 30 tests covering bracket template structure and simulator probability distributions, seeding, edge cases, futures blending Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix lint errors in NCAA Football CFP simulator Replace non-null assertions with optional chaining, change let to const, use toSorted() instead of sort(), and add a bump() helper to avoid repeated map lookups with non-null assertions in simulateBracket. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix TS2345 in NCAA football simulator test Add ?? 0 fallback so optional-chained probFirst is number, not number | undefined. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
93 lines
3.1 KiB
TypeScript
93 lines
3.1 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
||
import { CFP_12, BRACKET_TEMPLATES, getScoringRoundType } from "~/lib/bracket-templates";
|
||
|
||
describe("CFP_12 Bracket Template", () => {
|
||
describe("Template structure", () => {
|
||
it("has correct total teams", () => {
|
||
expect(CFP_12.totalTeams).toBe(12);
|
||
});
|
||
|
||
it("has 4 rounds", () => {
|
||
expect(CFP_12.rounds).toHaveLength(4);
|
||
});
|
||
|
||
it("has correct round names", () => {
|
||
expect(CFP_12.rounds[0].name).toBe("First Round");
|
||
expect(CFP_12.rounds[1].name).toBe("Quarterfinals");
|
||
expect(CFP_12.rounds[2].name).toBe("Semifinals");
|
||
expect(CFP_12.rounds[3].name).toBe("National Championship");
|
||
});
|
||
|
||
it("has correct match counts per round", () => {
|
||
expect(CFP_12.rounds[0].matchCount).toBe(4); // 8 seeds play, 4 byes
|
||
expect(CFP_12.rounds[1].matchCount).toBe(4); // 8 → 4
|
||
expect(CFP_12.rounds[2].matchCount).toBe(2); // 4 → 2
|
||
expect(CFP_12.rounds[3].matchCount).toBe(1); // 2 → 1
|
||
});
|
||
|
||
it("has correct round advancement chain", () => {
|
||
expect(CFP_12.rounds[0].feedsInto).toBe("Quarterfinals");
|
||
expect(CFP_12.rounds[1].feedsInto).toBe("Semifinals");
|
||
expect(CFP_12.rounds[2].feedsInto).toBe("National Championship");
|
||
expect(CFP_12.rounds[3].feedsInto).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe("Scoring configuration", () => {
|
||
it("scoring starts at Quarterfinals", () => {
|
||
expect(CFP_12.scoringStartsAtRound).toBe("Quarterfinals");
|
||
});
|
||
|
||
it("First Round is not scoring", () => {
|
||
expect(CFP_12.rounds[0].isScoring).toBe(false);
|
||
});
|
||
|
||
it("Quarterfinals is scoring", () => {
|
||
expect(CFP_12.rounds[1].isScoring).toBe(true);
|
||
});
|
||
|
||
it("Semifinals is scoring", () => {
|
||
expect(CFP_12.rounds[2].isScoring).toBe(true);
|
||
});
|
||
|
||
it("National Championship is scoring", () => {
|
||
expect(CFP_12.rounds[3].isScoring).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe("Template ID and registry", () => {
|
||
it("has id cfp_12", () => {
|
||
expect(CFP_12.id).toBe("cfp_12");
|
||
});
|
||
|
||
it("is registered in BRACKET_TEMPLATES", () => {
|
||
expect(BRACKET_TEMPLATES["cfp_12"]).toBeDefined();
|
||
expect(BRACKET_TEMPLATES["cfp_12"]).toBe(CFP_12);
|
||
});
|
||
});
|
||
|
||
describe("getScoringRoundType", () => {
|
||
it("classifies Quarterfinals as quarterfinals (losers share 5th–8th)", () => {
|
||
expect(getScoringRoundType("Quarterfinals", CFP_12)).toBe("quarterfinals");
|
||
});
|
||
|
||
it("classifies Semifinals as semifinals (losers share 3rd–4th)", () => {
|
||
expect(getScoringRoundType("Semifinals", CFP_12)).toBe("semifinals");
|
||
});
|
||
|
||
it("classifies National Championship as finals", () => {
|
||
expect(getScoringRoundType("National Championship", CFP_12)).toBe("finals");
|
||
});
|
||
|
||
it("returns null for First Round (non-scoring)", () => {
|
||
expect(getScoringRoundType("First Round", CFP_12)).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe("Total matches", () => {
|
||
it("has 11 total matches across all rounds", () => {
|
||
const total = CFP_12.rounds.reduce((sum, r) => sum + r.matchCount, 0);
|
||
expect(total).toBe(11); // 4 + 4 + 2 + 1
|
||
});
|
||
});
|
||
});
|