brackt/app/models/__tests__/cfp-12-bracket.test.ts
Chris Parsons 4d5fea05ab
Add NCAA Football CFP simulator (12-team bracket) (#278)
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>
2026-04-08 09:48:32 -04:00

93 lines
3.1 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 { 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 5th8th)", () => {
expect(getScoringRoundType("Quarterfinals", CFP_12)).toBe("quarterfinals");
});
it("classifies Semifinals as semifinals (losers share 3rd4th)", () => {
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
});
});
});