import { describe, it, expect } from "vitest"; import { srsToElo, simSeriesN, resolveElo } from "../wnba-simulator"; import { eloWinProbability } from "~/services/probability-engine"; // ─── srsToElo ───────────────────────────────────────────────────────────────── describe("srsToElo", () => { it("maps SRS 0 to league-average Elo 1500", () => { expect(srsToElo(0)).toBe(1500); }); it("maps positive SRS above 1500", () => { expect(srsToElo(5)).toBe(1600); expect(srsToElo(10)).toBe(1700); }); it("maps negative SRS below 1500", () => { expect(srsToElo(-5)).toBe(1400); expect(srsToElo(-10)).toBe(1300); }); it("is linear with scale 20", () => { expect(srsToElo(3)).toBe(srsToElo(1) + 40); }); }); // ─── eloWinProbability ──────────────────────────────────────────────────────── describe("eloWinProbability", () => { it("returns 0.5 for equal Elo ratings", () => { expect(eloWinProbability(1500, 1500)).toBeCloseTo(0.5, 6); }); it("favors the higher-rated team", () => { expect(eloWinProbability(1600, 1500)).toBeGreaterThan(0.5); expect(eloWinProbability(1500, 1600)).toBeLessThan(0.5); }); it("is anti-symmetric: P(A>B) + P(B>A) = 1", () => { const p = eloWinProbability(1640, 1430); expect(p + eloWinProbability(1430, 1640)).toBeCloseTo(1.0, 10); }); it("SRS +10 vs SRS -10 gives ~76% win probability", () => { // srsToElo(+10) = 1700, srsToElo(-10) = 1300 → gap 400 → P = 1/(1+10^(-1)) ≈ 0.909 // Actually gap of 400 → P ≈ 0.909. Let's verify the ±10 SRS case: // srsToElo(+10) = 1700, srsToElo(-10) = 1300 → gap = 400 → P ≈ 0.909 const p = eloWinProbability(srsToElo(10), srsToElo(-10)); expect(p).toBeGreaterThan(0.9); expect(p).toBeLessThan(0.92); }); it("SRS +5 vs SRS 0 gives ~64% win probability", () => { // srsToElo(+5) = 1600, srsToElo(0) = 1500 → gap = 100 → P = 1/(1+10^(-0.25)) ≈ 0.640 const p = eloWinProbability(srsToElo(5), srsToElo(0)); expect(p).toBeGreaterThan(0.63); expect(p).toBeLessThan(0.66); }); }); // ─── resolveElo ─────────────────────────────────────────────────────────────── describe("resolveElo", () => { it("SRS mode with SRS present: uses SRS-derived Elo", () => { expect(resolveElo(5, 1600, true)).toBe(srsToElo(5)); // 1600 }); it("SRS mode with no SRS: falls back to futures Elo", () => { expect(resolveElo(null, 1620, true)).toBe(1620); }); it("SRS mode with no SRS and no futures: falls back to 1500", () => { expect(resolveElo(null, null, true)).toBe(1500); }); it("futures mode: uses futures Elo regardless of SRS", () => { expect(resolveElo(8, 1650, false)).toBe(1650); }); it("futures mode with no futures: falls back to 1500", () => { expect(resolveElo(8, null, false)).toBe(1500); }); it("sourceElo overrides SRS when in SRS mode", () => { expect(resolveElo(5, 1600, true, 1750)).toBe(1750); }); it("sourceElo overrides futures when not in SRS mode", () => { expect(resolveElo(null, 1600, false, 1750)).toBe(1750); }); it("sourceElo overrides 1500 fallback when all other signals are absent", () => { expect(resolveElo(null, null, false, 1700)).toBe(1700); }); it("null sourceElo falls through to normal priority chain", () => { expect(resolveElo(5, 1600, true, null)).toBe(srsToElo(5)); }); it("undefined sourceElo falls through to normal priority chain", () => { expect(resolveElo(null, 1620, false, undefined)).toBe(1620); }); }); // ─── simSeriesN ─────────────────────────────────────────────────────────────── const makeTeam = (id: string, elo: number) => ({ id, name: id, elo, currentWins: 0, remainingGames: 0, winProb: 0.5, }); describe("simSeriesN", () => { it("best-of-3: winner reaches exactly 2 wins", () => { const a = makeTeam("A", 1600); const b = makeTeam("B", 1400); for (let i = 0; i < 100; i++) { const { winner, loser } = simSeriesN(a, b, 2); expect(winner === a || winner === b).toBe(true); expect(loser === a || loser === b).toBe(true); expect(winner).not.toBe(loser); } }); it("best-of-5: winner and loser are always different teams", () => { const a = makeTeam("A", 1500); const b = makeTeam("B", 1500); for (let i = 0; i < 100; i++) { const { winner, loser } = simSeriesN(a, b, 3); expect(winner).not.toBe(loser); } }); it("best-of-7: returns the input team objects (referential identity)", () => { const a = makeTeam("TeamA", 1550); const b = makeTeam("TeamB", 1450); const { winner, loser } = simSeriesN(a, b, 4); expect(winner === a || winner === b).toBe(true); expect(loser === a || loser === b).toBe(true); }); it("heavily favored team wins most best-of-3 series", () => { const strong = makeTeam("Strong", 1800); const weak = makeTeam("Weak", 1200); let strongWins = 0; const N = 1000; for (let i = 0; i < N; i++) { if (simSeriesN(strong, weak, 2).winner === strong) strongWins++; } // P(game) ≈ 0.994, P(series) should be > 0.99 expect(strongWins / N).toBeGreaterThan(0.97); }); it("even matchup produces roughly 50% win rate in a large sample", () => { const a = makeTeam("A", 1500); const b = makeTeam("B", 1500); let aWins = 0; const N = 2000; for (let i = 0; i < N; i++) { if (simSeriesN(a, b, 3).winner === a) aWins++; } // Should be close to 50% — allow ±5% expect(aWins / N).toBeGreaterThan(0.45); expect(aWins / N).toBeLessThan(0.55); }); });