* fix: NBA play-in advancement writes E7/W7 to wrong participant slot advanceNBAPlayInWinner was writing the PIR1 7v8 winner to participant1Id of First Round M3/M7, but those slots already hold E2/W2 (seeded at generation time). The "already filled" guard fired, silently swallowing both the winner advancement and the loser's placement into Play-In Round 2 — so the 7/8 loser never appeared in the second play-in game. Fix: write the 7v8 winner to participant2Id (the correct null slot) and update the guard, doc comment, and load-bearing comment accordingly. Also swap the p2Override arguments in the simulator and test fixture to match the corrected slot layout (FR M3: E2=p1, E7=p2). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: hasGroupStage check for templates without groupStage property undefined !== null is true, so templates that omit groupStage (like nba_20) were incorrectly treated as group-stage templates, triggering the generate-groups action and the "Invalid template or template has no group stage" error. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
497 lines
19 KiB
TypeScript
497 lines
19 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, type MockInstance } from "vitest";
|
|
import { getTeamData, eloWinProbability, NBASimulator } from "../nba-simulator";
|
|
import { NBA_20, BRACKET_TEMPLATES } from "~/lib/bracket-templates";
|
|
|
|
// ─── Pure math / team data ────────────────────────────────────────────────────
|
|
|
|
describe("eloWinProbability", () => {
|
|
it("returns 0.5 for equal Elo ratings", () => {
|
|
expect(eloWinProbability(1500, 1500)).toBeCloseTo(0.5, 10);
|
|
});
|
|
|
|
it("favors the higher-rated team", () => {
|
|
expect(eloWinProbability(1600, 1400)).toBeGreaterThan(0.5);
|
|
expect(eloWinProbability(1400, 1600)).toBeLessThan(0.5);
|
|
});
|
|
|
|
it("is symmetric: P(A>B) + P(B>A) = 1", () => {
|
|
const a = 1650;
|
|
const b = 1480;
|
|
expect(eloWinProbability(a, b) + eloWinProbability(b, a)).toBeCloseTo(1.0, 10);
|
|
});
|
|
|
|
it("approaches 1 for a very large Elo advantage", () => {
|
|
expect(eloWinProbability(2000, 1000)).toBeGreaterThan(0.99);
|
|
});
|
|
});
|
|
|
|
describe("getTeamData", () => {
|
|
it("finds teams by full name", () => {
|
|
expect(getTeamData("Boston Celtics")).toMatchObject({ conference: "Eastern" });
|
|
expect(getTeamData("Oklahoma City Thunder")).toMatchObject({ conference: "Western" });
|
|
});
|
|
|
|
it("is case-insensitive", () => {
|
|
expect(getTeamData("boston celtics")).toBeDefined();
|
|
expect(getTeamData("OKLAHOMA CITY THUNDER")).toBeDefined();
|
|
});
|
|
|
|
it("returns undefined for unknown teams", () => {
|
|
expect(getTeamData("Springfield Nuclears")).toBeUndefined();
|
|
});
|
|
|
|
it("covers all 30 NBA teams", () => {
|
|
const allTeams = [
|
|
// Eastern Conference
|
|
"Detroit Pistons", "Boston Celtics", "New York Knicks", "Cleveland Cavaliers",
|
|
"Orlando Magic", "Miami Heat", "Toronto Raptors", "Atlanta Hawks",
|
|
"Philadelphia 76ers", "Charlotte Hornets", "Milwaukee Bucks", "Chicago Bulls",
|
|
"Brooklyn Nets", "Indiana Pacers", "Washington Wizards",
|
|
// Western Conference
|
|
"Oklahoma City Thunder", "San Antonio Spurs", "Houston Rockets", "Denver Nuggets",
|
|
"LA Lakers", "Minnesota Timberwolves", "Phoenix Suns", "LA Clippers",
|
|
"Golden State Warriors", "Portland Trail Blazers", "Dallas Mavericks",
|
|
"Memphis Grizzlies", "New Orleans Pelicans", "Sacramento Kings", "Utah Jazz",
|
|
];
|
|
|
|
for (const name of allTeams) {
|
|
expect(getTeamData(name), `${name} not found`).toBeDefined();
|
|
}
|
|
expect(allTeams).toHaveLength(30);
|
|
});
|
|
|
|
it("has exactly 15 Eastern and 15 Western teams", () => {
|
|
const allTeams = [
|
|
"Detroit Pistons", "Boston Celtics", "New York Knicks", "Cleveland Cavaliers",
|
|
"Orlando Magic", "Miami Heat", "Toronto Raptors", "Atlanta Hawks",
|
|
"Philadelphia 76ers", "Charlotte Hornets", "Milwaukee Bucks", "Chicago Bulls",
|
|
"Brooklyn Nets", "Indiana Pacers", "Washington Wizards",
|
|
"Oklahoma City Thunder", "San Antonio Spurs", "Houston Rockets", "Denver Nuggets",
|
|
"LA Lakers", "Minnesota Timberwolves", "Phoenix Suns", "LA Clippers",
|
|
"Golden State Warriors", "Portland Trail Blazers", "Dallas Mavericks",
|
|
"Memphis Grizzlies", "New Orleans Pelicans", "Sacramento Kings", "Utah Jazz",
|
|
];
|
|
const east = allTeams.filter((n) => getTeamData(n)?.conference === "Eastern");
|
|
const west = allTeams.filter((n) => getTeamData(n)?.conference === "Western");
|
|
expect(east).toHaveLength(15);
|
|
expect(west).toHaveLength(15);
|
|
});
|
|
});
|
|
|
|
// ─── NBA_20 bracket template structure ───────────────────────────────────────
|
|
|
|
describe("NBA_20 bracket template", () => {
|
|
it("is registered in BRACKET_TEMPLATES", () => {
|
|
expect(BRACKET_TEMPLATES.nba_20).toBe(NBA_20);
|
|
});
|
|
|
|
it("has correct totalTeams and id", () => {
|
|
expect(NBA_20.id).toBe("nba_20");
|
|
expect(NBA_20.totalTeams).toBe(20);
|
|
expect(NBA_20.scoringStartsAtRound).toBe("Conference Semifinals");
|
|
});
|
|
|
|
it("has 6 rounds in the correct order", () => {
|
|
const names = NBA_20.rounds.map((r) => r.name);
|
|
expect(names).toEqual([
|
|
"Play-In Round 1",
|
|
"Play-In Round 2",
|
|
"First Round",
|
|
"Conference Semifinals",
|
|
"Conference Finals",
|
|
"NBA Finals",
|
|
]);
|
|
});
|
|
|
|
it("has correct match counts per round", () => {
|
|
const counts = NBA_20.rounds.map((r) => r.matchCount);
|
|
expect(counts).toEqual([4, 2, 8, 4, 2, 1]);
|
|
});
|
|
|
|
it("non-scoring rounds have isScoring=false", () => {
|
|
const nonScoring = ["Play-In Round 1", "Play-In Round 2", "First Round"];
|
|
for (const name of nonScoring) {
|
|
const round = NBA_20.rounds.find((r) => r.name === name);
|
|
expect(round?.isScoring, `${name} should be non-scoring`).toBe(false);
|
|
}
|
|
});
|
|
|
|
it("scoring rounds have isScoring=true", () => {
|
|
const scoring = ["Conference Semifinals", "Conference Finals", "NBA Finals"];
|
|
for (const name of scoring) {
|
|
const round = NBA_20.rounds.find((r) => r.name === name);
|
|
expect(round?.isScoring, `${name} should be scoring`).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("feedsInto forms a valid chain ending at null", () => {
|
|
const roundByName = new Map(NBA_20.rounds.map((r) => [r.name, r]));
|
|
let current = NBA_20.rounds[0];
|
|
const visited: string[] = [];
|
|
while (current.feedsInto !== null) {
|
|
visited.push(current.name);
|
|
const next = roundByName.get(current.feedsInto);
|
|
expect(next, `${current.feedsInto} not found in rounds`).toBeDefined();
|
|
if (!next) break;
|
|
current = next;
|
|
}
|
|
visited.push(current.name);
|
|
expect(visited).toHaveLength(NBA_20.rounds.length);
|
|
expect(current.name).toBe("NBA Finals");
|
|
});
|
|
|
|
it("has 20 participantLabels with correct East/West structure", () => {
|
|
const labels = NBA_20.participantLabels ?? [];
|
|
expect(labels).toHaveLength(20);
|
|
// East seeds 1-10 at indices 0-9
|
|
for (let i = 0; i < 10; i++) {
|
|
expect(labels[i]).toMatch(/^East \d+$/);
|
|
}
|
|
// West seeds 1-10 at indices 10-19
|
|
for (let i = 10; i < 20; i++) {
|
|
expect(labels[i]).toMatch(/^West \d+$/);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── Simulator integration tests (mocked DB) ─────────────────────────────────
|
|
|
|
const TEAM_IDS = Array.from({ length: 20 }, (_, i) => `team-${i + 1}`);
|
|
|
|
/** Build a minimal DB match object */
|
|
function makeMatch(
|
|
round: string,
|
|
matchNumber: number,
|
|
opts: {
|
|
p1?: string | null;
|
|
p2?: string | null;
|
|
winnerId?: string | null;
|
|
loserId?: string | null;
|
|
isComplete?: boolean;
|
|
isScoring?: boolean;
|
|
} = {}
|
|
) {
|
|
return {
|
|
id: `${round.replace(/\s/g, "-").toLowerCase()}-m${matchNumber}`,
|
|
scoringEventId: "event-1",
|
|
round,
|
|
matchNumber,
|
|
participant1Id: opts.p1 ?? null,
|
|
participant2Id: opts.p2 ?? null,
|
|
winnerId: opts.winnerId ?? null,
|
|
loserId: opts.loserId ?? null,
|
|
isComplete: opts.isComplete ?? false,
|
|
isScoring: opts.isScoring ?? false,
|
|
templateRound: round,
|
|
seedInfo: null,
|
|
participant1Score: null,
|
|
participant2Score: null,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Builds a full 20-team NBA bracket with all Play-In and First Round slots
|
|
* filled so the simulator has valid participants on every match.
|
|
*
|
|
* Team layout matches generateNBA20Bracket:
|
|
* PIR1 M1: idx6(E7) vs idx7(E8) PIR1 M2: idx8(E9) vs idx9(E10)
|
|
* PIR1 M3: idx16(W7) vs idx17(W8) PIR1 M4: idx18(W9) vs idx19(W10)
|
|
* PIR2 M1: e8Cand vs e9e10W PIR2 M2: w8Cand vs w9w10W
|
|
* FR M1: idx0(E1) vs e8 FR M2: idx3(E4) vs idx4(E5)
|
|
* FR M3: e7 vs idx1(E2) FR M4: idx2(E3) vs idx5(E6)
|
|
* FR M5: idx10(W1) vs w8 FR M6: idx13(W4) vs idx14(W5)
|
|
* FR M7: w7 vs idx11(W2) FR M8: idx12(W3) vs idx15(W6)
|
|
*/
|
|
function buildFullBracket(
|
|
pir1Results: { m1Winner: string; m2Winner: string; m3Winner: string; m4Winner: string },
|
|
pir2Results: { m1Winner: string; m2Winner: string }
|
|
) {
|
|
const t = TEAM_IDS;
|
|
|
|
const e7 = pir1Results.m1Winner;
|
|
const e8Cand = t[7]; // E8 candidate = PIR1 M1 loser (for simplicity, hard-code)
|
|
const e9e10W = pir1Results.m2Winner;
|
|
const w7 = pir1Results.m3Winner;
|
|
const w8Cand = t[17];
|
|
const w9w10W = pir1Results.m4Winner;
|
|
|
|
const e8Seed = pir2Results.m1Winner;
|
|
const w8Seed = pir2Results.m2Winner;
|
|
|
|
return [
|
|
// Play-In Round 1
|
|
makeMatch("Play-In Round 1", 1, { p1: t[6], p2: t[7], winnerId: pir1Results.m1Winner, loserId: pir1Results.m1Winner === t[6] ? t[7] : t[6], isComplete: true }),
|
|
makeMatch("Play-In Round 1", 2, { p1: t[8], p2: t[9], winnerId: pir1Results.m2Winner, loserId: pir1Results.m2Winner === t[8] ? t[9] : t[8], isComplete: true }),
|
|
makeMatch("Play-In Round 1", 3, { p1: t[16], p2: t[17], winnerId: pir1Results.m3Winner, loserId: pir1Results.m3Winner === t[16] ? t[17] : t[16], isComplete: true }),
|
|
makeMatch("Play-In Round 1", 4, { p1: t[18], p2: t[19], winnerId: pir1Results.m4Winner, loserId: pir1Results.m4Winner === t[18] ? t[19] : t[18], isComplete: true }),
|
|
// Play-In Round 2
|
|
makeMatch("Play-In Round 2", 1, { p1: e8Cand, p2: e9e10W, winnerId: pir2Results.m1Winner, loserId: pir2Results.m1Winner === e8Cand ? e9e10W : e8Cand, isComplete: true }),
|
|
makeMatch("Play-In Round 2", 2, { p1: w8Cand, p2: w9w10W, winnerId: pir2Results.m2Winner, loserId: pir2Results.m2Winner === w8Cand ? w9w10W : w8Cand, isComplete: true }),
|
|
// First Round (play-in slots filled)
|
|
makeMatch("First Round", 1, { p1: t[0], p2: e8Seed, isScoring: false }),
|
|
makeMatch("First Round", 2, { p1: t[3], p2: t[4], isScoring: false }),
|
|
makeMatch("First Round", 3, { p1: t[1], p2: e7, isScoring: false }),
|
|
makeMatch("First Round", 4, { p1: t[2], p2: t[5], isScoring: false }),
|
|
makeMatch("First Round", 5, { p1: t[10], p2: w8Seed, isScoring: false }),
|
|
makeMatch("First Round", 6, { p1: t[13], p2: t[14], isScoring: false }),
|
|
makeMatch("First Round", 7, { p1: t[11], p2: w7, isScoring: false }),
|
|
makeMatch("First Round", 8, { p1: t[12], p2: t[15], isScoring: false }),
|
|
// Conference Semifinals (TBD)
|
|
makeMatch("Conference Semifinals", 1, { isScoring: true }),
|
|
makeMatch("Conference Semifinals", 2, { isScoring: true }),
|
|
makeMatch("Conference Semifinals", 3, { isScoring: true }),
|
|
makeMatch("Conference Semifinals", 4, { isScoring: true }),
|
|
// Conference Finals (TBD)
|
|
makeMatch("Conference Finals", 1, { isScoring: true }),
|
|
makeMatch("Conference Finals", 2, { isScoring: true }),
|
|
// NBA Finals (TBD)
|
|
makeMatch("NBA Finals", 1, { isScoring: true }),
|
|
];
|
|
}
|
|
|
|
vi.mock("~/database/context", () => ({
|
|
database: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("~/models/regular-season-standings", () => ({
|
|
getRegularSeasonStandings: vi.fn().mockResolvedValue([]),
|
|
}));
|
|
|
|
describe("NBASimulator — bracket-aware mode", () => {
|
|
let mockDb: {
|
|
query: {
|
|
scoringEvents: { findFirst: MockInstance };
|
|
playoffMatches: { findMany: MockInstance };
|
|
};
|
|
select: MockInstance;
|
|
};
|
|
|
|
beforeEach(async () => {
|
|
const { database } = await import("~/database/context");
|
|
|
|
mockDb = {
|
|
query: {
|
|
scoringEvents: { findFirst: vi.fn() },
|
|
playoffMatches: { findMany: vi.fn() },
|
|
},
|
|
select: vi.fn().mockReturnValue({
|
|
from: vi.fn().mockReturnValue({
|
|
where: vi.fn().mockResolvedValue(
|
|
TEAM_IDS.map((id) => ({ id, name: id })) // name = id (no real team name → fallback Elo 1400)
|
|
),
|
|
}),
|
|
}),
|
|
};
|
|
|
|
(database as unknown as MockInstance).mockReturnValue(mockDb);
|
|
|
|
mockDb.query.scoringEvents.findFirst.mockResolvedValue({
|
|
id: "event-1",
|
|
sportsSeasonId: "season-1",
|
|
eventType: "playoff_game",
|
|
});
|
|
});
|
|
|
|
it("falls back to season-projection when no playoff matches exist", async () => {
|
|
mockDb.query.playoffMatches.findMany.mockResolvedValue([]);
|
|
|
|
// Season projection needs standings — mock getRegularSeasonStandings via the model import
|
|
// Since season-projection throws when conference split < 10, we just verify the right path:
|
|
const sim = new NBASimulator();
|
|
// select().from().where() returns 20 teams, but no conference data → East assigned via TEAMS_DATA
|
|
// All teams have unknown names so conference defaults to "Eastern" → <10 Western → error
|
|
await expect(sim.simulate("season-1")).rejects.toThrow(/Each conference needs at least 10/);
|
|
});
|
|
|
|
it("throws if bracket structure is wrong (wrong match count)", async () => {
|
|
// Only return PIR1 matches — missing rest of bracket
|
|
const pir1Only = [1, 2, 3, 4].map((n) =>
|
|
makeMatch("Play-In Round 1", n, { p1: TEAM_IDS[n - 1], p2: TEAM_IDS[n + 3] })
|
|
);
|
|
mockDb.query.playoffMatches.findMany.mockResolvedValue(pir1Only);
|
|
|
|
const sim = new NBASimulator();
|
|
await expect(sim.simulate("season-1")).rejects.toThrow(/unexpected structure/);
|
|
});
|
|
|
|
it("throws if a participant slot is missing (no override and DB null)", async () => {
|
|
const t = TEAM_IDS;
|
|
// Build bracket but leave FR M2 with null p2 (no override provided)
|
|
const matches = buildFullBracket(
|
|
{ m1Winner: t[6], m2Winner: t[8], m3Winner: t[16], m4Winner: t[18] },
|
|
{ m1Winner: t[7], m2Winner: t[17] }
|
|
);
|
|
// Null out FR M2 p2
|
|
const frM2 = matches.find((m) => m.round === "First Round" && m.matchNumber === 2);
|
|
if (!frM2) throw new Error("FR M2 not found in test fixture");
|
|
frM2.participant2Id = null;
|
|
|
|
mockDb.query.playoffMatches.findMany.mockResolvedValue(matches);
|
|
|
|
const sim = new NBASimulator();
|
|
await expect(sim.simulate("season-1")).rejects.toThrow(/Cannot resolve series/);
|
|
});
|
|
|
|
it("returns results for all 20 participants", async () => {
|
|
const t = TEAM_IDS;
|
|
const matches = buildFullBracket(
|
|
{ m1Winner: t[6], m2Winner: t[8], m3Winner: t[16], m4Winner: t[18] },
|
|
{ m1Winner: t[7], m2Winner: t[17] }
|
|
);
|
|
mockDb.query.playoffMatches.findMany.mockResolvedValue(matches);
|
|
|
|
const sim = new NBASimulator();
|
|
const results = await sim.simulate("season-1");
|
|
|
|
expect(results).toHaveLength(20);
|
|
});
|
|
|
|
it("each probability column sums to 1.0 across all 20 participants", async () => {
|
|
const t = TEAM_IDS;
|
|
const matches = buildFullBracket(
|
|
{ m1Winner: t[6], m2Winner: t[8], m3Winner: t[16], m4Winner: t[18] },
|
|
{ m1Winner: t[7], m2Winner: t[17] }
|
|
);
|
|
mockDb.query.playoffMatches.findMany.mockResolvedValue(matches);
|
|
|
|
const sim = new NBASimulator();
|
|
const results = await sim.simulate("season-1");
|
|
|
|
const keys = [
|
|
"probFirst", "probSecond", "probThird", "probFourth",
|
|
"probFifth", "probSixth", "probSeventh", "probEighth",
|
|
] as const;
|
|
|
|
for (const key of keys) {
|
|
const colSum = results.reduce((s, r) => s + r.probabilities[key], 0);
|
|
expect(colSum, `column ${key} should sum to 1`).toBeCloseTo(1.0, 1);
|
|
}
|
|
});
|
|
|
|
it("all probabilities are non-negative", async () => {
|
|
const t = TEAM_IDS;
|
|
const matches = buildFullBracket(
|
|
{ m1Winner: t[6], m2Winner: t[8], m3Winner: t[16], m4Winner: t[18] },
|
|
{ m1Winner: t[7], m2Winner: t[17] }
|
|
);
|
|
mockDb.query.playoffMatches.findMany.mockResolvedValue(matches);
|
|
|
|
const sim = new NBASimulator();
|
|
const results = await sim.simulate("season-1");
|
|
|
|
for (const r of results) {
|
|
for (const v of Object.values(r.probabilities)) {
|
|
expect(v).toBeGreaterThanOrEqual(0);
|
|
}
|
|
}
|
|
});
|
|
|
|
it("completed tournament: champion has probFirst=1, all others probFirst≈0", async () => {
|
|
const t = TEAM_IDS;
|
|
const champion = t[0]; // E1
|
|
|
|
const matches = buildFullBracket(
|
|
{ m1Winner: t[6], m2Winner: t[8], m3Winner: t[16], m4Winner: t[18] },
|
|
{ m1Winner: t[7], m2Winner: t[17] }
|
|
);
|
|
|
|
// Mark all bracket matches complete with t[0] winning everything on the East side
|
|
// and t[10] winning West side, then t[0] beats t[10] in finals
|
|
const frMatches = matches.filter((m) => m.round === "First Round");
|
|
const csMatches = matches.filter((m) => m.round === "Conference Semifinals");
|
|
const cfMatches = matches.filter((m) => m.round === "Conference Finals");
|
|
const finalMatch = matches.find((m) => m.round === "NBA Finals");
|
|
if (!finalMatch) throw new Error("Finals match not found in test fixture");;
|
|
|
|
// Fully script the bracket:
|
|
// FR: top seed wins each series
|
|
for (const m of frMatches) {
|
|
const w = m.participant1Id ?? "";
|
|
m.winnerId = w;
|
|
m.loserId = m.participant2Id;
|
|
m.isComplete = true;
|
|
}
|
|
|
|
// CS M1: FR M1 winner vs FR M2 winner → FR M1 winner (t[0]) wins
|
|
csMatches[0].participant1Id = frMatches[0].winnerId;
|
|
csMatches[0].participant2Id = frMatches[1].winnerId;
|
|
csMatches[0].winnerId = frMatches[0].winnerId; // t[0]
|
|
csMatches[0].loserId = frMatches[1].winnerId;
|
|
csMatches[0].isComplete = true;
|
|
|
|
// CS M2: FR M3 winner vs FR M4 winner
|
|
csMatches[1].participant1Id = frMatches[2].winnerId;
|
|
csMatches[1].participant2Id = frMatches[3].winnerId;
|
|
csMatches[1].winnerId = frMatches[2].winnerId;
|
|
csMatches[1].loserId = frMatches[3].winnerId;
|
|
csMatches[1].isComplete = true;
|
|
|
|
// CS M3: FR M5 winner vs FR M6 winner
|
|
csMatches[2].participant1Id = frMatches[4].winnerId;
|
|
csMatches[2].participant2Id = frMatches[5].winnerId;
|
|
csMatches[2].winnerId = frMatches[4].winnerId; // t[10]
|
|
csMatches[2].loserId = frMatches[5].winnerId;
|
|
csMatches[2].isComplete = true;
|
|
|
|
// CS M4: FR M7 winner vs FR M8 winner
|
|
csMatches[3].participant1Id = frMatches[6].winnerId;
|
|
csMatches[3].participant2Id = frMatches[7].winnerId;
|
|
csMatches[3].winnerId = frMatches[6].winnerId;
|
|
csMatches[3].loserId = frMatches[7].winnerId;
|
|
csMatches[3].isComplete = true;
|
|
|
|
// CF M1: East champion (CS M1 winner = t[0])
|
|
cfMatches[0].participant1Id = csMatches[0].winnerId;
|
|
cfMatches[0].participant2Id = csMatches[1].winnerId;
|
|
cfMatches[0].winnerId = csMatches[0].winnerId; // t[0]
|
|
cfMatches[0].loserId = csMatches[1].winnerId;
|
|
cfMatches[0].isComplete = true;
|
|
|
|
// CF M2: West champion (CS M3 winner = t[10])
|
|
cfMatches[1].participant1Id = csMatches[2].winnerId;
|
|
cfMatches[1].participant2Id = csMatches[3].winnerId;
|
|
cfMatches[1].winnerId = csMatches[2].winnerId; // t[10]
|
|
cfMatches[1].loserId = csMatches[3].winnerId;
|
|
cfMatches[1].isComplete = true;
|
|
|
|
// Finals: t[0] beats t[10]
|
|
finalMatch.participant1Id = cfMatches[0].winnerId;
|
|
finalMatch.participant2Id = cfMatches[1].winnerId;
|
|
finalMatch.winnerId = champion;
|
|
finalMatch.loserId = t[10];
|
|
finalMatch.isComplete = true;
|
|
|
|
mockDb.query.playoffMatches.findMany.mockResolvedValue(matches);
|
|
|
|
const sim = new NBASimulator();
|
|
const results = await sim.simulate("season-1");
|
|
|
|
const champResult = results.find((r) => r.participantId === champion);
|
|
expect(champResult, "champion result should exist").toBeDefined();
|
|
expect(champResult?.probabilities.probFirst).toBeCloseTo(1.0, 3);
|
|
|
|
for (const r of results) {
|
|
if (r.participantId !== champion) {
|
|
expect(r.probabilities.probFirst).toBeCloseTo(0, 3);
|
|
}
|
|
}
|
|
});
|
|
|
|
it("uses source: 'nba_bracket_monte_carlo' on all results", async () => {
|
|
const t = TEAM_IDS;
|
|
const matches = buildFullBracket(
|
|
{ m1Winner: t[6], m2Winner: t[8], m3Winner: t[16], m4Winner: t[18] },
|
|
{ m1Winner: t[7], m2Winner: t[17] }
|
|
);
|
|
mockDb.query.playoffMatches.findMany.mockResolvedValue(matches);
|
|
|
|
const sim = new NBASimulator();
|
|
const results = await sim.simulate("season-1");
|
|
|
|
for (const r of results) {
|
|
expect(r.source).toBe("nba_bracket_monte_carlo");
|
|
}
|
|
});
|
|
});
|