import { describe, it, expect } from "vitest"; import { normalizeTeamName, getTeamData, eloWinProbability, simulateProjectedPoints, } from "../nhl-simulator"; // ─── normalizeTeamName ──────────────────────────────────────────────────────── describe("normalizeTeamName", () => { it("lowercases and trims", () => { expect(normalizeTeamName(" Colorado Avalanche ")).toBe("colorado avalanche"); }); it("collapses internal whitespace", () => { expect(normalizeTeamName("Tampa Bay Lightning")).toBe("tampa bay lightning"); }); it("is already-normalized identity", () => { expect(normalizeTeamName("dallas stars")).toBe("dallas stars"); }); }); // ─── getTeamData ────────────────────────────────────────────────────────────── describe("getTeamData", () => { it("returns data for an exact match", () => { const d = getTeamData("Colorado Avalanche"); expect(d).toBeDefined(); expect(d?.conference).toBe("Western"); expect(d?.division).toBe("Central"); expect(d?.elo).toBeGreaterThan(1500); }); it("is case-insensitive", () => { expect(getTeamData("colorado avalanche")).toEqual(getTeamData("Colorado Avalanche")); }); it("returns undefined for an unknown team", () => { expect(getTeamData("Springfield Ice Hounds")).toBeUndefined(); }); it("all 32 teams are present", () => { const allTeams = [ // Eastern — Atlantic "Tampa Bay Lightning", "Buffalo Sabres", "Montreal Canadiens", "Ottawa Senators", "Detroit Red Wings", "Boston Bruins", "Florida Panthers", "Toronto Maple Leafs", // Eastern — Metropolitan "Carolina Hurricanes", "Columbus Blue Jackets", "Pittsburgh Penguins", "New York Islanders", "Philadelphia Flyers", "Washington Capitals", "New Jersey Devils", "New York Rangers", // Western — Central "Colorado Avalanche", "Dallas Stars", "Minnesota Wild", "Utah Mammoth", "Nashville Predators", "Winnipeg Jets", "St. Louis Blues", "Chicago Blackhawks", // Western — Pacific "Anaheim Ducks", "Edmonton Oilers", "Vegas Golden Knights", "Los Angeles Kings", "San Jose Sharks", "Seattle Kraken", "Calgary Flames", "Vancouver Canucks", ]; for (const name of allTeams) { expect(getTeamData(name), `missing team: ${name}`).toBeDefined(); } }); it("all 32 teams have a valid conference and division", () => { const validConferences = new Set(["Eastern", "Western"]); const validDivisions = new Set(["Atlantic", "Metropolitan", "Central", "Pacific"]); const allTeams = [ "Tampa Bay Lightning", "Buffalo Sabres", "Montreal Canadiens", "Ottawa Senators", "Detroit Red Wings", "Boston Bruins", "Florida Panthers", "Toronto Maple Leafs", "Carolina Hurricanes", "Columbus Blue Jackets", "Pittsburgh Penguins", "New York Islanders", "Philadelphia Flyers", "Washington Capitals", "New Jersey Devils", "New York Rangers", "Colorado Avalanche", "Dallas Stars", "Minnesota Wild", "Utah Mammoth", "Nashville Predators", "Winnipeg Jets", "St. Louis Blues", "Chicago Blackhawks", "Anaheim Ducks", "Edmonton Oilers", "Vegas Golden Knights", "Los Angeles Kings", "San Jose Sharks", "Seattle Kraken", "Calgary Flames", "Vancouver Canucks", ]; for (const name of allTeams) { const data = getTeamData(name); expect(validConferences.has(data?.conference ?? ""), `${name}: invalid conference`).toBe(true); expect(validDivisions.has(data?.division ?? ""), `${name}: invalid division`).toBe(true); } }); it("conference/division assignments are correct for spot-checked teams", () => { expect(getTeamData("Carolina Hurricanes")?.division).toBe("Metropolitan"); expect(getTeamData("Colorado Avalanche")?.division).toBe("Central"); expect(getTeamData("Vegas Golden Knights")?.division).toBe("Pacific"); expect(getTeamData("Buffalo Sabres")?.conference).toBe("Eastern"); expect(getTeamData("Edmonton Oilers")?.conference).toBe("Western"); }); }); // ─── eloWinProbability ──────────────────────────────────────────────────────── describe("eloWinProbability (PARITY_FACTOR = 1000)", () => { it("returns 0.5 for equal Elo ratings", () => { expect(eloWinProbability(1500, 1500)).toBeCloseTo(0.5, 6); }); it("favors the higher-rated team", () => { expect(eloWinProbability(1594, 1500)).toBeGreaterThan(0.5); expect(eloWinProbability(1500, 1594)).toBeLessThan(0.5); }); it("is anti-symmetric: P(A>B) + P(B>A) = 1", () => { const p = eloWinProbability(1580, 1520); expect(p + eloWinProbability(1520, 1580)).toBeCloseTo(1.0, 10); }); it("a 100-pt gap gives ~55.7% win prob per game", () => { const p = eloWinProbability(1600, 1500); expect(p).toBeCloseTo(0.557, 2); }); it("a 200-pt gap gives ~61.3% win prob per game", () => { const p = eloWinProbability(1700, 1500); expect(p).toBeCloseTo(0.613, 2); }); }); // ─── simulateProjectedPoints ────────────────────────────────────────────────── const makeEntry = (overrides: Partial<{ currentPoints: number; remainingGames: number; winProb: number; }> = {}) => ({ id: "test", name: "Test Team", data: undefined, conference: "Western" as const, division: "Central", currentPoints: overrides.currentPoints ?? 80, remainingGames: overrides.remainingGames ?? 10, winProb: overrides.winProb ?? 0.55, }); describe("simulateProjectedPoints", () => { it("returns exactly currentPoints when no games remain", () => { const entry = makeEntry({ currentPoints: 95, remainingGames: 0 }); expect(simulateProjectedPoints(entry)).toBe(95); }); it("returns at least currentPoints (points never decrease)", () => { const entry = makeEntry({ currentPoints: 70, remainingGames: 20, winProb: 0.5 }); for (let i = 0; i < 100; i++) { expect(simulateProjectedPoints(entry)).toBeGreaterThanOrEqual(70); } }); it("returns at most currentPoints + remainingGames * 2 (can't exceed max)", () => { const entry = makeEntry({ currentPoints: 60, remainingGames: 15, winProb: 0.9 }); for (let i = 0; i < 100; i++) { expect(simulateProjectedPoints(entry)).toBeLessThanOrEqual(60 + 15 * 2); } }); it("higher winProb produces higher projected points on average", () => { const base = makeEntry({ currentPoints: 0, remainingGames: 82 }); const runs = 2000; const avg = (winProb: number) => { let total = 0; for (let i = 0; i < runs; i++) { total += simulateProjectedPoints({ ...base, winProb }); } return total / runs; }; expect(avg(0.7)).toBeGreaterThan(avg(0.4)); }); });