import { describe, it, expect } from "vitest"; import { americanToImplied, getMajorOddsKey, resolveSkill, simulateMajor, } from "../golf-simulator"; import type { GolfSkillsRecord } from "~/models/golf-skills"; // ─── americanToImplied ──────────────────────────────────────────────────────── describe("americanToImplied", () => { it("converts positive (underdog) American odds correctly", () => { // +400 → 100 / (400 + 100) = 0.2 expect(americanToImplied(400)).toBeCloseTo(0.2, 5); // +100 → 100 / 200 = 0.5 expect(americanToImplied(100)).toBeCloseTo(0.5, 5); }); it("converts negative (favorite) American odds correctly", () => { // -200 → 200 / 300 ≈ 0.6667 expect(americanToImplied(-200)).toBeCloseTo(0.6667, 3); }); it("returns null for odds = 0", () => { expect(americanToImplied(0)).toBeNull(); }); it("returns probability in (0, 1] for valid odds", () => { const p = americanToImplied(200); expect(p).not.toBeNull(); expect(p).toBeGreaterThan(0); expect(p).toBeLessThanOrEqual(1); }); }); // ─── getMajorOddsKey ────────────────────────────────────────────────────────── describe("getMajorOddsKey", () => { it("maps Masters names to mastersOdds", () => { expect(getMajorOddsKey("The Masters")).toBe("mastersOdds"); expect(getMajorOddsKey("masters tournament")).toBe("mastersOdds"); }); it("maps PGA Championship to pgaChampionshipOdds", () => { expect(getMajorOddsKey("PGA Championship")).toBe("pgaChampionshipOdds"); expect(getMajorOddsKey("pga championship 2025")).toBe("pgaChampionshipOdds"); }); it("maps US Open to usOpenOdds", () => { expect(getMajorOddsKey("US Open")).toBe("usOpenOdds"); expect(getMajorOddsKey("U.S. Open Golf")).toBe("usOpenOdds"); expect(getMajorOddsKey("2025 US Open")).toBe("usOpenOdds"); }); it("maps Open Championship / British Open to openChampionshipOdds", () => { expect(getMajorOddsKey("The Open Championship")).toBe("openChampionshipOdds"); expect(getMajorOddsKey("British Open")).toBe("openChampionshipOdds"); }); it("returns null for unrecognized names", () => { expect(getMajorOddsKey("Ryder Cup")).toBeNull(); expect(getMajorOddsKey("Travelers Championship")).toBeNull(); }); }); // ─── resolveSkill ───────────────────────────────────────────────────────────── function makeSkills(overrides: Partial = {}): GolfSkillsRecord { return { id: "skill-1", participantId: "p1", sportsSeasonId: "s1", sgTotal: null, datagolfRank: null, mastersOdds: null, usOpenOdds: null, openChampionshipOdds: null, pgaChampionshipOdds: null, updatedAt: new Date(), ...overrides, }; } describe("resolveSkill", () => { it("returns sgTotal when set, ignoring odds", () => { const skills = makeSkills({ sgTotal: 2.5, mastersOdds: 400 }); expect(resolveSkill(skills, "mastersOdds")).toBe(2.5); }); it("falls back to odds-derived skill when sgTotal is null", () => { const skills = makeSkills({ sgTotal: null, mastersOdds: 400 }); const skill = resolveSkill(skills, "mastersOdds"); // +400 = 20% win prob; skill > 0 because 20% > 1/156 baseline expect(skill).toBeGreaterThan(0); }); it("returns 0 when no skills record", () => { expect(resolveSkill(undefined, "mastersOdds")).toBe(0); expect(resolveSkill(undefined, null)).toBe(0); }); it("returns 0 when sgTotal is null and no matching odds key", () => { const skills = makeSkills({ sgTotal: null, mastersOdds: 400 }); // oddsKey = null → no odds available for this major expect(resolveSkill(skills, null)).toBe(0); }); it("returns 0 when sgTotal is null and the odds column is null", () => { const skills = makeSkills({ sgTotal: null, mastersOdds: null }); expect(resolveSkill(skills, "mastersOdds")).toBe(0); }); it("better American odds → higher resolved skill", () => { const skillGood = makeSkills({ sgTotal: null, mastersOdds: 200 }); // +200 = 33% win prob const skillPoor = makeSkills({ sgTotal: null, mastersOdds: 2000 }); // +2000 = 4.8% win prob const sg1 = resolveSkill(skillGood, "mastersOdds"); const sg2 = resolveSkill(skillPoor, "mastersOdds"); expect(sg1).toBeGreaterThan(sg2); }); }); // ─── simulateMajor ──────────────────────────────────────────────────────────── function makeQPConfig(maxPlacement = 16): Map { // Default QP: 20, 14, 10, 8, 5, 5, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1 const values = [20, 14, 10, 8, 5, 5, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1]; const map = new Map(); for (let i = 0; i < maxPlacement; i++) { map.set(i + 1, values[i] ?? 0); } return map; } describe("simulateMajor", () => { const qpConfig = makeQPConfig(); it("returns an entry for every tracked player", () => { const players = [ { id: "p1", strength: 2.0 }, { id: "p2", strength: 1.0 }, ]; const result = simulateMajor(players, 154, 1.0, qpConfig); expect(result.has("p1")).toBe(true); expect(result.has("p2")).toBe(true); }); it("total QP awarded does not exceed sum of top-16 QP config", () => { const players = Array.from({ length: 10 }, (_, i) => ({ id: `p${i}`, strength: 1.0 })); const result = simulateMajor(players, 146, 1.0, qpConfig); const totalQP = [...result.values()].reduce((s, v) => s + v, 0); const maxQP = [...qpConfig.values()].reduce((s, v) => s + v, 0); expect(totalQP).toBeLessThanOrEqual(maxQP); }); it("all QP values are valid (>= 0 and in the config set or 0)", () => { const players = [{ id: "p1", strength: 8.0 }, { id: "p2", strength: 1.0 }]; const result = simulateMajor(players, 154, 1.0, qpConfig); const validQP = new Set([0, ...qpConfig.values()]); for (const qp of result.values()) { expect(validQP.has(qp)).toBe(true); } }); it("a stronger player wins more often than a weaker one (statistical)", () => { const TRIALS = 5_000; let eliteWins = 0; const players = [ { id: "elite", strength: 50.0 }, // very high strength { id: "average", strength: 1.0 }, ]; const singleQP = new Map([[1, 20], [2, 14]]); for (let i = 0; i < TRIALS; i++) { const result = simulateMajor(players, 0, 1.0, singleQP); if (result.get("elite") === 20) eliteWins++; } // Elite player should win at least 80% of the time with strength 50x average expect(eliteWins / TRIALS).toBeGreaterThan(0.8); }); it("works when tracked players outnumber field size (restCount = 0)", () => { const players = Array.from({ length: 200 }, (_, i) => ({ id: `p${i}`, strength: 1.0 })); const result = simulateMajor(players, 0, 1.0, qpConfig); // All players should have an entry expect(result.size).toBe(200); // Only 16 can get QP const scorers = [...result.values()].filter((v) => v > 0); expect(scorers.length).toBeLessThanOrEqual(16); }); it("when all players have equal strength, each wins approximately equally (statistical)", () => { const TRIALS = 10_000; const N = 3; const wins: Record = {}; const players = Array.from({ length: N }, (_, i) => { const id = `p${i}`; wins[id] = 0; return { id, strength: 1.0 }; }); const singleQP = new Map([[1, 20]]); for (let i = 0; i < TRIALS; i++) { const result = simulateMajor(players, 0, 1.0, singleQP); for (const [id, qp] of result) { if (qp === 20) wins[id]++; } } // Each of 3 equal players should win ~33% ± 5% for (const id of Object.keys(wins)) { expect(wins[id] / TRIALS).toBeGreaterThan(0.27); expect(wins[id] / TRIALS).toBeLessThan(0.39); } }); }); // ─── Monte Carlo property test ───────────────────────────────────────────────── describe("simulateMajor Monte Carlo properties", () => { it("better SG player wins the head-to-head more often (no rest-of-field)", () => { // With no rest-of-field, the win rate is purely determined by strength ratio: // P(elite wins) = exp(0.7 * 3.0) / (exp(0.7 * 3.0) + exp(0.7 * 1.5)) // = 8.17 / (8.17 + 2.86) ≈ 74% const TRIALS = 3_000; const qpConfig = new Map([[1, 20], [2, 14]]); let eliteFirst = 0; for (let i = 0; i < TRIALS; i++) { const players = [ { id: "elite", strength: Math.exp(0.7 * 3.0) }, // SG = 3.0 { id: "good", strength: Math.exp(0.7 * 1.5) }, // SG = 1.5 ]; const result = simulateMajor(players, 0, 1.0, qpConfig); if (result.get("elite") === 20) eliteFirst++; } const eliteWinRate = eliteFirst / TRIALS; // Elite player wins ~74% in a head-to-head; allow generous margin for randomness expect(eliteWinRate).toBeGreaterThan(0.65); }); it("in a full 156-player field, a player with strength 8 wins ~5% of the time", () => { // Calibration check: strength 8 vs 155 opponents at strength 1 → 8 / (8 + 155) ≈ 4.9% const TRIALS = 5_000; const qpConfig = new Map([[1, 20]]); let eliteWins = 0; for (let i = 0; i < TRIALS; i++) { const players = [{ id: "elite", strength: 8 }]; const result = simulateMajor(players, 155, 1.0, qpConfig); if (result.get("elite") === 20) eliteWins++; } const winRate = eliteWins / TRIALS; // Should be approximately 4.9%; allow ±3% tolerance expect(winRate).toBeGreaterThan(0.02); expect(winRate).toBeLessThan(0.09); }); });