Fix no-non-null-assertion lint violations and promote to error
Eliminates all 208 no-non-null-assertion warnings across 38 files. Promotes typescript/no-non-null-assertion from warn to error in .oxlintrc.json. Fix patterns applied: - Map.get(key)! after .has() check → extract with get() + null guard - Map.get(key)! on pre-populated count maps → ?? 0 default - .set(id, map.get(id)! + 1) increment → ?? 0 before adding - participant1Id!/participant2Id! on DB matches → ?? "" fallback - array.find()! in tests → guard + throw or expect().toBeDefined() - bracketTemplateCache.get(id)! → null guard extract - Various nullable field accesses → optional chain or ?? default Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4de6cb23a0
commit
552fe28b99
38 changed files with 289 additions and 222 deletions
|
|
@ -23,7 +23,7 @@
|
||||||
{ "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }
|
{ "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }
|
||||||
],
|
],
|
||||||
"typescript/no-explicit-any": "error",
|
"typescript/no-explicit-any": "error",
|
||||||
"typescript/no-non-null-assertion": "warn",
|
"typescript/no-non-null-assertion": "error",
|
||||||
"typescript/consistent-type-imports": [
|
"typescript/consistent-type-imports": [
|
||||||
"error",
|
"error",
|
||||||
{ "prefer": "type-imports" }
|
{ "prefer": "type-imports" }
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,7 @@ describe('AutodraftSettings Component', () => {
|
||||||
|
|
||||||
describe('Optimistic UI', () => {
|
describe('Optimistic UI', () => {
|
||||||
it('does not disable buttons while a fetch is in flight', async () => {
|
it('does not disable buttons while a fetch is in flight', async () => {
|
||||||
let resolve: (v: any) => void;
|
let resolve: ((v: any) => void) | undefined;
|
||||||
(global.fetch as any).mockReturnValueOnce(new Promise((r) => { resolve = r; }));
|
(global.fetch as any).mockReturnValueOnce(new Promise((r) => { resolve = r; }));
|
||||||
|
|
||||||
render(<AutodraftSettings {...defaultProps} />);
|
render(<AutodraftSettings {...defaultProps} />);
|
||||||
|
|
@ -154,7 +154,7 @@ describe('AutodraftSettings Component', () => {
|
||||||
// Buttons stay enabled — optimistic UI does not block interaction
|
// Buttons stay enabled — optimistic UI does not block interaction
|
||||||
expect(screen.getByRole('button', { name: 'Off' })).not.toBeDisabled();
|
expect(screen.getByRole('button', { name: 'Off' })).not.toBeDisabled();
|
||||||
|
|
||||||
resolve!({ ok: true, json: async () => ({ success: true }) });
|
resolve?.({ ok: true, json: async () => ({ success: true }) });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fires a fetch for every rapid click, aborting previous in-flight requests', async () => {
|
it('fires a fetch for every rapid click, aborting previous in-flight requests', async () => {
|
||||||
|
|
|
||||||
|
|
@ -49,11 +49,12 @@ export const TeamsDraftedGrid = memo(function TeamsDraftedGrid({
|
||||||
if (!map.has(pick.team.id)) {
|
if (!map.has(pick.team.id)) {
|
||||||
map.set(pick.team.id, new Map());
|
map.set(pick.team.id, new Map());
|
||||||
}
|
}
|
||||||
const teamMap = map.get(pick.team.id)!;
|
const teamMap = map.get(pick.team.id) ?? new Map<string, typeof picks>();
|
||||||
|
if (!map.has(pick.team.id)) map.set(pick.team.id, teamMap);
|
||||||
if (!teamMap.has(pick.sport.id)) {
|
if (!teamMap.has(pick.sport.id)) {
|
||||||
teamMap.set(pick.sport.id, []);
|
teamMap.set(pick.sport.id, []);
|
||||||
}
|
}
|
||||||
teamMap.get(pick.sport.id)!.push(pick);
|
teamMap.get(pick.sport.id)?.push(pick);
|
||||||
});
|
});
|
||||||
|
|
||||||
return map;
|
return map;
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,10 @@ export function groupPicksByTeamAndSport(
|
||||||
const map = new Map<string, Map<string, DraftPick[]>>();
|
const map = new Map<string, Map<string, DraftPick[]>>();
|
||||||
picks.forEach((pick) => {
|
picks.forEach((pick) => {
|
||||||
if (!map.has(pick.team.id)) map.set(pick.team.id, new Map());
|
if (!map.has(pick.team.id)) map.set(pick.team.id, new Map());
|
||||||
const teamMap = map.get(pick.team.id)!;
|
const teamMap = map.get(pick.team.id) ?? new Map<string, DraftPick[]>();
|
||||||
|
if (!map.has(pick.team.id)) map.set(pick.team.id, teamMap);
|
||||||
if (!teamMap.has(pick.sport.id)) teamMap.set(pick.sport.id, []);
|
if (!teamMap.has(pick.sport.id)) teamMap.set(pick.sport.id, []);
|
||||||
teamMap.get(pick.sport.id)!.push(pick);
|
teamMap.get(pick.sport.id)?.push(pick);
|
||||||
});
|
});
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ export function groupMatchesByRound(matches: Match[]): Map<string, Match[]> {
|
||||||
const byRound = new Map<string, Match[]>();
|
const byRound = new Map<string, Match[]>();
|
||||||
for (const match of matches) {
|
for (const match of matches) {
|
||||||
if (!byRound.has(match.round)) byRound.set(match.round, []);
|
if (!byRound.has(match.round)) byRound.set(match.round, []);
|
||||||
byRound.get(match.round)!.push(match);
|
byRound.get(match.round)?.push(match);
|
||||||
}
|
}
|
||||||
for (const group of byRound.values()) {
|
for (const group of byRound.values()) {
|
||||||
group.sort((a, b) => a.matchNumber - b.matchNumber);
|
group.sort((a, b) => a.matchNumber - b.matchNumber);
|
||||||
|
|
@ -153,7 +153,7 @@ export function computeEliminatedByRound(
|
||||||
// Double-chance survivor: participant appeared in a LATER round — not yet eliminated here.
|
// Double-chance survivor: participant appeared in a LATER round — not yet eliminated here.
|
||||||
if (loserLatestAppear > lossRoundIndex) continue;
|
if (loserLatestAppear > lossRoundIndex) continue;
|
||||||
if (!result.has(match.round)) result.set(match.round, []);
|
if (!result.has(match.round)) result.set(match.round, []);
|
||||||
result.get(match.round)!.push(match.loser.id);
|
result.get(match.round)?.push(match.loser.id);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
@ -211,7 +211,7 @@ export function PlayoffBracket({
|
||||||
: match.participant2Score;
|
: match.participant2Score;
|
||||||
if (loserScore) _hasScore = true;
|
if (loserScore) _hasScore = true;
|
||||||
if (!losersByRound.has(match.round)) losersByRound.set(match.round, []);
|
if (!losersByRound.has(match.round)) losersByRound.set(match.round, []);
|
||||||
losersByRound.get(match.round)!.push({
|
losersByRound.get(match.round)?.push({
|
||||||
participant: match.loser,
|
participant: match.loser,
|
||||||
score: loserScore,
|
score: loserScore,
|
||||||
ownership: ownershipMap.get(match.loser.id) || null,
|
ownership: ownershipMap.get(match.loser.id) || null,
|
||||||
|
|
@ -331,8 +331,8 @@ export function PlayoffBracket({
|
||||||
const p1IsTbd = !match.participant1Id;
|
const p1IsTbd = !match.participant1Id;
|
||||||
const p2IsTbd = !match.participant2Id;
|
const p2IsTbd = !match.participant2Id;
|
||||||
|
|
||||||
const p1IsOwned = !p1IsTbd && userParticipantSet.has(match.participant1Id!);
|
const p1IsOwned = !p1IsTbd && userParticipantSet.has(match.participant1Id ?? "");
|
||||||
const p2IsOwned = !p2IsTbd && userParticipantSet.has(match.participant2Id!);
|
const p2IsOwned = !p2IsTbd && userParticipantSet.has(match.participant2Id ?? "");
|
||||||
const matchHasOwned = p1IsOwned || p2IsOwned;
|
const matchHasOwned = p1IsOwned || p2IsOwned;
|
||||||
|
|
||||||
const p1Ownership =
|
const p1Ownership =
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ function buildFlatSections(
|
||||||
for (const row of standings) {
|
for (const row of standings) {
|
||||||
const conf = row.conference ?? "Other";
|
const conf = row.conference ?? "Other";
|
||||||
if (!confMap.has(conf)) confMap.set(conf, []);
|
if (!confMap.has(conf)) confMap.set(conf, []);
|
||||||
confMap.get(conf)!.push(row);
|
confMap.get(conf)?.push(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.from(confMap.entries()).map(([conference, rows]) => ({
|
return Array.from(confMap.entries()).map(([conference, rows]) => ({
|
||||||
|
|
@ -138,7 +138,7 @@ function buildNhlSections(
|
||||||
for (const row of standings) {
|
for (const row of standings) {
|
||||||
const conf = row.conference ?? "Other";
|
const conf = row.conference ?? "Other";
|
||||||
if (!confMap.has(conf)) confMap.set(conf, []);
|
if (!confMap.has(conf)) confMap.set(conf, []);
|
||||||
confMap.get(conf)!.push(row);
|
confMap.get(conf)?.push(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.from(confMap.entries()).map(([conference, rows]) => {
|
return Array.from(confMap.entries()).map(([conference, rows]) => {
|
||||||
|
|
@ -146,7 +146,7 @@ function buildNhlSections(
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const div = row.division ?? "";
|
const div = row.division ?? "";
|
||||||
if (!divMap.has(div)) divMap.set(div, []);
|
if (!divMap.has(div)) divMap.set(div, []);
|
||||||
divMap.get(div)!.push(row);
|
divMap.get(div)?.push(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
const divisions = Array.from(divMap.entries())
|
const divisions = Array.from(divMap.entries())
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,7 @@ export function TeamScoreBreakdown({
|
||||||
(pick.finalPosition ?? 0) === 0 ? (
|
(pick.finalPosition ?? 0) === 0 ? (
|
||||||
<Badge variant="secondary">Did Not Score</Badge>
|
<Badge variant="secondary">Did Not Score</Badge>
|
||||||
) : (
|
) : (
|
||||||
<PlacementBadge position={pick.finalPosition!} />
|
<PlacementBadge position={pick.finalPosition ?? 0} />
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<Badge variant="outline">Pending</Badge>
|
<Badge variant="outline">Pending</Badge>
|
||||||
|
|
|
||||||
|
|
@ -26,13 +26,13 @@ describe("localDateTimeToUtcIso", () => {
|
||||||
const input = "2026-03-11T17:00";
|
const input = "2026-03-11T17:00";
|
||||||
const result = localDateTimeToUtcIso(input);
|
const result = localDateTimeToUtcIso(input);
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
expect(new Date(result!).getTime()).toBe(new Date(input).getTime());
|
expect(new Date(result ?? "").getTime()).toBe(new Date(input).getTime());
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns a string ending with 'Z' (UTC designator)", () => {
|
it("returns a string ending with 'Z' (UTC designator)", () => {
|
||||||
const result = localDateTimeToUtcIso("2026-03-11T10:00");
|
const result = localDateTimeToUtcIso("2026-03-11T10:00");
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
expect(result!.endsWith("Z")).toBe(true);
|
expect((result ?? "").endsWith("Z")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns a full ISO-8601 string with milliseconds", () => {
|
it("returns a full ISO-8601 string with milliseconds", () => {
|
||||||
|
|
|
||||||
|
|
@ -157,7 +157,7 @@ describe("NCAA 68 Bracket Template - Phase 2.8", () => {
|
||||||
|
|
||||||
it("has 8 teams in Elite Eight (top 8 scoring)", () => {
|
it("has 8 teams in Elite Eight (top 8 scoring)", () => {
|
||||||
const eliteEightRound = NCAA_68.rounds.find((r) => r.name === "Elite Eight");
|
const eliteEightRound = NCAA_68.rounds.find((r) => r.name === "Elite Eight");
|
||||||
const teamsInEliteEight = eliteEightRound!.matchCount * 2;
|
const teamsInEliteEight = (eliteEightRound?.matchCount ?? 0) * 2;
|
||||||
expect(teamsInEliteEight).toBe(8);
|
expect(teamsInEliteEight).toBe(8);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -216,32 +216,32 @@ describe("NCAA 68 Bracket Template - Phase 2.8", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("has regions in order: East, South, West, Midwest", () => {
|
it("has regions in order: East, South, West, Midwest", () => {
|
||||||
const names = NCAA_68.regions!.map((r) => r.name);
|
const names = (NCAA_68.regions ?? []).map((r) => r.name);
|
||||||
expect(names).toEqual(["East", "South", "West", "Midwest"]);
|
expect(names).toEqual(["East", "South", "West", "Midwest"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("East has 16 direct seeds and no play-ins", () => {
|
it("East has 16 direct seeds and no play-ins", () => {
|
||||||
const east = NCAA_68.regions![0];
|
const east = (NCAA_68.regions ?? [])[0];
|
||||||
expect(east.directSeeds).toHaveLength(16);
|
expect(east.directSeeds).toHaveLength(16);
|
||||||
expect(east.playIns).toHaveLength(0);
|
expect(east.playIns).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("South has 15 direct seeds and one 16-seed play-in", () => {
|
it("South has 15 direct seeds and one 16-seed play-in", () => {
|
||||||
const south = NCAA_68.regions![1];
|
const south = (NCAA_68.regions ?? [])[1];
|
||||||
expect(south.directSeeds).toHaveLength(15);
|
expect(south.directSeeds).toHaveLength(15);
|
||||||
expect(south.playIns).toHaveLength(1);
|
expect(south.playIns).toHaveLength(1);
|
||||||
expect(south.playIns[0].seedSlot).toBe(16);
|
expect(south.playIns[0].seedSlot).toBe(16);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("West has 15 direct seeds and one 11-seed play-in", () => {
|
it("West has 15 direct seeds and one 11-seed play-in", () => {
|
||||||
const west = NCAA_68.regions![2];
|
const west = (NCAA_68.regions ?? [])[2];
|
||||||
expect(west.directSeeds).toHaveLength(15);
|
expect(west.directSeeds).toHaveLength(15);
|
||||||
expect(west.playIns).toHaveLength(1);
|
expect(west.playIns).toHaveLength(1);
|
||||||
expect(west.playIns[0].seedSlot).toBe(11);
|
expect(west.playIns[0].seedSlot).toBe(11);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Midwest has 14 direct seeds and two play-ins (11 and 16)", () => {
|
it("Midwest has 14 direct seeds and two play-ins (11 and 16)", () => {
|
||||||
const midwest = NCAA_68.regions![3];
|
const midwest = (NCAA_68.regions ?? [])[3];
|
||||||
expect(midwest.directSeeds).toHaveLength(14);
|
expect(midwest.directSeeds).toHaveLength(14);
|
||||||
expect(midwest.playIns).toHaveLength(2);
|
expect(midwest.playIns).toHaveLength(2);
|
||||||
expect(midwest.playIns[0].seedSlot).toBe(11);
|
expect(midwest.playIns[0].seedSlot).toBe(11);
|
||||||
|
|
@ -249,12 +249,12 @@ describe("NCAA 68 Bracket Template - Phase 2.8", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("total direct seeds across all regions equals 60", () => {
|
it("total direct seeds across all regions equals 60", () => {
|
||||||
const total = NCAA_68.regions!.reduce((sum, r) => sum + r.directSeeds.length, 0);
|
const total = (NCAA_68.regions ?? []).reduce((sum, r) => sum + r.directSeeds.length, 0);
|
||||||
expect(total).toBe(60);
|
expect(total).toBe(60);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("total play-in teams across all regions equals 8", () => {
|
it("total play-in teams across all regions equals 8", () => {
|
||||||
const total = NCAA_68.regions!.reduce(
|
const total = (NCAA_68.regions ?? []).reduce(
|
||||||
(sum, r) => sum + r.playIns.reduce((s, pi) => s + pi.teams, 0),
|
(sum, r) => sum + r.playIns.reduce((s, pi) => s + pi.teams, 0),
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
@ -262,8 +262,8 @@ describe("NCAA 68 Bracket Template - Phase 2.8", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("direct + play-in teams sum to 68", () => {
|
it("direct + play-in teams sum to 68", () => {
|
||||||
const direct = NCAA_68.regions!.reduce((sum, r) => sum + r.directSeeds.length, 0);
|
const direct = (NCAA_68.regions ?? []).reduce((sum, r) => sum + r.directSeeds.length, 0);
|
||||||
const playIn = NCAA_68.regions!.reduce(
|
const playIn = (NCAA_68.regions ?? []).reduce(
|
||||||
(sum, r) => sum + r.playIns.reduce((s, pi) => s + pi.teams, 0),
|
(sum, r) => sum + r.playIns.reduce((s, pi) => s + pi.teams, 0),
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
@ -272,7 +272,7 @@ describe("NCAA 68 Bracket Template - Phase 2.8", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("buildNCAA68SlotMap", () => {
|
describe("buildNCAA68SlotMap", () => {
|
||||||
const slotMap = buildNCAA68SlotMap(NCAA_68.regions!);
|
const slotMap = buildNCAA68SlotMap(NCAA_68.regions ?? []);
|
||||||
|
|
||||||
it("East starts at index 0", () => {
|
it("East starts at index 0", () => {
|
||||||
expect(slotMap.directOffsets[0]).toBe(0);
|
expect(slotMap.directOffsets[0]).toBe(0);
|
||||||
|
|
@ -357,7 +357,7 @@ describe("NCAA 68 Bracket Template - Phase 2.8", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("First Four → Round of 64 match number derivation", () => {
|
describe("First Four → Round of 64 match number derivation", () => {
|
||||||
const slotMap = buildNCAA68SlotMap(NCAA_68.regions!);
|
const slotMap = buildNCAA68SlotMap(NCAA_68.regions ?? []);
|
||||||
|
|
||||||
// r64MatchNumber = regionIndex * 8 + matchIndexForSeedSlot(seedSlot) + 1
|
// r64MatchNumber = regionIndex * 8 + matchIndexForSeedSlot(seedSlot) + 1
|
||||||
it("FF1 (South region index 1, seed 16) → R64 match 9", () => {
|
it("FF1 (South region index 1, seed 16) → R64 match 9", () => {
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ describe("NFL 14 Bracket Template - Phase 2.9", () => {
|
||||||
expect(wildCard?.matchCount).toBe(6); // 6 matches × 2 teams = 12 teams
|
expect(wildCard?.matchCount).toBe(6); // 6 matches × 2 teams = 12 teams
|
||||||
|
|
||||||
// Total teams (14) - Wild Card teams (12) = 2 bye teams
|
// Total teams (14) - Wild Card teams (12) = 2 bye teams
|
||||||
const byeTeams = NFL_14.totalTeams - (wildCard!.matchCount * 2);
|
const byeTeams = NFL_14.totalTeams - ((wildCard?.matchCount ?? 0) * 2);
|
||||||
expect(byeTeams).toBe(2);
|
expect(byeTeams).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -104,9 +104,9 @@ describe("NFL 14 Bracket Template - Phase 2.9", () => {
|
||||||
const wildCard = NFL_14.rounds.find((r) => r.name === "Wild Card");
|
const wildCard = NFL_14.rounds.find((r) => r.name === "Wild Card");
|
||||||
const divisional = NFL_14.rounds.find((r) => r.name === "Divisional");
|
const divisional = NFL_14.rounds.find((r) => r.name === "Divisional");
|
||||||
|
|
||||||
const wildCardWinners = wildCard!.matchCount; // 6 winners
|
const wildCardWinners = wildCard?.matchCount ?? 0; // 6 winners
|
||||||
const byeTeams = 2;
|
const byeTeams = 2;
|
||||||
const divisionalTeams = divisional!.matchCount * 2; // 4 matches × 2 = 8 teams
|
const divisionalTeams = (divisional?.matchCount ?? 0) * 2; // 4 matches × 2 = 8 teams
|
||||||
|
|
||||||
expect(wildCardWinners + byeTeams).toBe(divisionalTeams);
|
expect(wildCardWinners + byeTeams).toBe(divisionalTeams);
|
||||||
});
|
});
|
||||||
|
|
@ -131,7 +131,7 @@ describe("NFL 14 Bracket Template - Phase 2.9", () => {
|
||||||
|
|
||||||
it("verifies only 8 teams score fantasy points (Divisional and beyond)", () => {
|
it("verifies only 8 teams score fantasy points (Divisional and beyond)", () => {
|
||||||
const divisional = NFL_14.rounds.find((r) => r.name === "Divisional");
|
const divisional = NFL_14.rounds.find((r) => r.name === "Divisional");
|
||||||
const divisionalTeams = divisional!.matchCount * 2; // 8 teams
|
const divisionalTeams = (divisional?.matchCount ?? 0) * 2; // 8 teams
|
||||||
|
|
||||||
expect(divisionalTeams).toBe(8); // Matches requirement from Q18
|
expect(divisionalTeams).toBe(8); // Matches requirement from Q18
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ export async function getDraftedParticipantsBySportsSeason(
|
||||||
if (!map.has(row.sportsSeasonId)) {
|
if (!map.has(row.sportsSeasonId)) {
|
||||||
map.set(row.sportsSeasonId, []);
|
map.set(row.sportsSeasonId, []);
|
||||||
}
|
}
|
||||||
map.get(row.sportsSeasonId)!.push({ id: row.participantId, name: row.participantName });
|
map.get(row.sportsSeasonId)?.push({ id: row.participantId, name: row.participantName });
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -422,7 +422,8 @@ async function generateNCAA68Bracket(
|
||||||
for (let i = 0; i < slotMap.playInOffsets.length; i++) {
|
for (let i = 0; i < slotMap.playInOffsets.length; i++) {
|
||||||
const { regionIndex, playInIndex } = slotMap.playInOffsets[i];
|
const { regionIndex, playInIndex } = slotMap.playInOffsets[i];
|
||||||
if (!regionFFLabels.has(regionIndex)) regionFFLabels.set(regionIndex, []);
|
if (!regionFFLabels.has(regionIndex)) regionFFLabels.set(regionIndex, []);
|
||||||
regionFFLabels.get(regionIndex)![playInIndex] = `FF${i + 1}`;
|
const labels = regionFFLabels.get(regionIndex);
|
||||||
|
if (labels) labels[playInIndex] = `FF${i + 1}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── First Four ────────────────────────────────────────────────────────────
|
// ── First Four ────────────────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -298,7 +298,7 @@ export async function updateFinalRankings(
|
||||||
if (!groupedByPoints.has(points)) {
|
if (!groupedByPoints.has(points)) {
|
||||||
groupedByPoints.set(points, []);
|
groupedByPoints.set(points, []);
|
||||||
}
|
}
|
||||||
groupedByPoints.get(points)!.push(standing);
|
groupedByPoints.get(points)?.push(standing);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign rankings, handling ties
|
// Assign rankings, handling ties
|
||||||
|
|
|
||||||
|
|
@ -593,7 +593,7 @@ export async function finalizeQualifyingPoints(
|
||||||
if (!groupedByQP.has(qp)) {
|
if (!groupedByQP.has(qp)) {
|
||||||
groupedByQP.set(qp, []);
|
groupedByQP.set(qp, []);
|
||||||
}
|
}
|
||||||
groupedByQP.get(qp)!.push(standing);
|
groupedByQP.get(qp)?.push(standing);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort groups by QP (descending)
|
// Sort groups by QP (descending)
|
||||||
|
|
@ -858,7 +858,7 @@ export async function calculateTeamScore(
|
||||||
const bracketTemplateCache = new Map<string, string | null>();
|
const bracketTemplateCache = new Map<string, string | null>();
|
||||||
async function getBracketTemplate(sportsSeasonId: string): Promise<string | null> {
|
async function getBracketTemplate(sportsSeasonId: string): Promise<string | null> {
|
||||||
if (bracketTemplateCache.has(sportsSeasonId)) {
|
if (bracketTemplateCache.has(sportsSeasonId)) {
|
||||||
return bracketTemplateCache.get(sportsSeasonId)!;
|
return bracketTemplateCache.get(sportsSeasonId) ?? null;
|
||||||
}
|
}
|
||||||
const event = await db.query.scoringEvents.findFirst({
|
const event = await db.query.scoringEvents.findFirst({
|
||||||
where: eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId),
|
where: eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId),
|
||||||
|
|
@ -952,7 +952,7 @@ export async function calculateTeamProjectedScore(
|
||||||
const bracketTemplateCache = new Map<string, string | null>();
|
const bracketTemplateCache = new Map<string, string | null>();
|
||||||
async function getBracketTemplate(sportsSeasonId: string): Promise<string | null> {
|
async function getBracketTemplate(sportsSeasonId: string): Promise<string | null> {
|
||||||
if (bracketTemplateCache.has(sportsSeasonId)) {
|
if (bracketTemplateCache.has(sportsSeasonId)) {
|
||||||
return bracketTemplateCache.get(sportsSeasonId)!;
|
return bracketTemplateCache.get(sportsSeasonId) ?? null;
|
||||||
}
|
}
|
||||||
const event = await db.query.scoringEvents.findFirst({
|
const event = await db.query.scoringEvents.findFirst({
|
||||||
where: eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId),
|
where: eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId),
|
||||||
|
|
@ -1378,8 +1378,8 @@ export async function recalculateAffectedLeagues(
|
||||||
scoredMatches = relevant
|
scoredMatches = relevant
|
||||||
.filter((m) => m.winnerName && m.loserName)
|
.filter((m) => m.winnerName && m.loserName)
|
||||||
.map((m) => ({
|
.map((m) => ({
|
||||||
winnerName: m.winnerName!,
|
winnerName: m.winnerName ?? "",
|
||||||
loserName: m.loserName!,
|
loserName: m.loserName ?? "",
|
||||||
winnerUsername: usernameForParticipant(m.winnerId, true),
|
winnerUsername: usernameForParticipant(m.winnerId, true),
|
||||||
loserUsername: usernameForParticipant(m.loserId),
|
loserUsername: usernameForParticipant(m.loserId),
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -496,16 +496,18 @@ export async function getUpcomingEventsForDraftedParticipants(
|
||||||
gameKeysByEvent.set(row.id, new Set());
|
gameKeysByEvent.set(row.id, new Set());
|
||||||
}
|
}
|
||||||
|
|
||||||
const pMap = participantsByEvent.get(row.id)!;
|
const pMap = participantsByEvent.get(row.id);
|
||||||
|
if (pMap) {
|
||||||
if (row.participant1Id && draftedMap.has(row.participant1Id)) {
|
if (row.participant1Id && draftedMap.has(row.participant1Id)) {
|
||||||
pMap.set(row.participant1Id, draftedMap.get(row.participant1Id)!);
|
pMap.set(row.participant1Id, draftedMap.get(row.participant1Id) ?? "");
|
||||||
}
|
}
|
||||||
if (row.participant2Id && draftedMap.has(row.participant2Id)) {
|
if (row.participant2Id && draftedMap.has(row.participant2Id)) {
|
||||||
pMap.set(row.participant2Id, draftedMap.get(row.participant2Id)!);
|
pMap.set(row.participant2Id, draftedMap.get(row.participant2Id) ?? "");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (row.round && row.gameNumber !== null) {
|
if (row.round && row.gameNumber !== null) {
|
||||||
gameKeysByEvent.get(row.id)!.add(`${row.round}|${row.gameNumber}`);
|
gameKeysByEvent.get(row.id)?.add(`${row.round}|${row.gameNumber}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (row.scheduledAt) {
|
if (row.scheduledAt) {
|
||||||
|
|
@ -517,13 +519,13 @@ export async function getUpcomingEventsForDraftedParticipants(
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [eventId, event] of eventMap) {
|
for (const [eventId, event] of eventMap) {
|
||||||
event.relevantParticipants = Array.from(participantsByEvent.get(eventId)!.values());
|
event.relevantParticipants = Array.from(participantsByEvent.get(eventId)?.values() ?? []);
|
||||||
|
|
||||||
const earliest = earliestTimeByEvent.get(eventId);
|
const earliest = earliestTimeByEvent.get(eventId);
|
||||||
event.earliestGameTime = earliest ? earliest.toISOString() : null;
|
event.earliestGameTime = earliest ? earliest.toISOString() : null;
|
||||||
|
|
||||||
// Build matchLabel from game keys
|
// Build matchLabel from game keys
|
||||||
const gameKeys = gameKeysByEvent.get(eventId)!;
|
const gameKeys = gameKeysByEvent.get(eventId) ?? new Set<string>();
|
||||||
if (gameKeys.size === 1) {
|
if (gameKeys.size === 1) {
|
||||||
const [round, gameNumberStr] = [...gameKeys][0].split("|");
|
const [round, gameNumberStr] = [...gameKeys][0].split("|");
|
||||||
// When round name duplicates the event name, omit the round to avoid
|
// When round name duplicates the event name, omit the round to avoid
|
||||||
|
|
|
||||||
|
|
@ -158,7 +158,7 @@ export async function getTeamScoreBreakdown(
|
||||||
const bracketTemplateCache = new Map<string, string | null>();
|
const bracketTemplateCache = new Map<string, string | null>();
|
||||||
async function getBracketTemplate(sportsSeasonId: string): Promise<string | null> {
|
async function getBracketTemplate(sportsSeasonId: string): Promise<string | null> {
|
||||||
if (bracketTemplateCache.has(sportsSeasonId)) {
|
if (bracketTemplateCache.has(sportsSeasonId)) {
|
||||||
return bracketTemplateCache.get(sportsSeasonId)!;
|
return bracketTemplateCache.get(sportsSeasonId) ?? null;
|
||||||
}
|
}
|
||||||
const event = await db.query.scoringEvents.findFirst({
|
const event = await db.query.scoringEvents.findFirst({
|
||||||
where: eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId),
|
where: eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId),
|
||||||
|
|
@ -318,7 +318,7 @@ export async function getSevenDayStandingsChange(
|
||||||
return current.map((standing) => ({
|
return current.map((standing) => ({
|
||||||
...standing,
|
...standing,
|
||||||
sevenDayRankChange: oldRanks.has(standing.teamId)
|
sevenDayRankChange: oldRanks.has(standing.teamId)
|
||||||
? oldRanks.get(standing.teamId)! - standing.currentRank
|
? (oldRanks.get(standing.teamId) ?? 0) - standing.currentRank
|
||||||
: 0,
|
: 0,
|
||||||
sevenDayOldRank: oldRanks.get(standing.teamId) || null,
|
sevenDayOldRank: oldRanks.get(standing.teamId) || null,
|
||||||
}));
|
}));
|
||||||
|
|
@ -411,7 +411,8 @@ export async function getSeasonPointProgression(
|
||||||
dateMap.set(date, { date });
|
dateMap.set(date, { date });
|
||||||
}
|
}
|
||||||
|
|
||||||
const dateData = dateMap.get(date)!;
|
const dateData = dateMap.get(date);
|
||||||
|
if (!dateData) continue;
|
||||||
dateData[snapshot.team.name] = parseFloat(snapshot.totalPoints);
|
dateData[snapshot.team.name] = parseFloat(snapshot.totalPoints);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -414,8 +414,8 @@ export default function EventBracket({
|
||||||
<CardTitle className="text-base">Group {label}</CardTitle>
|
<CardTitle className="text-base">Group {label}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="px-4 pb-4 space-y-2">
|
<CardContent className="px-4 pb-4 space-y-2">
|
||||||
{[...Array(template.groupStage!.teamsPerGroup)].map((_, teamIndex) => {
|
{[...Array(template.groupStage?.teamsPerGroup ?? 0)].map((_, teamIndex) => {
|
||||||
const seedIndex = groupIndex * template.groupStage!.teamsPerGroup + teamIndex;
|
const seedIndex = groupIndex * (template.groupStage?.teamsPerGroup ?? 0) + teamIndex;
|
||||||
const availableParticipants = getAvailableParticipants(seedIndex);
|
const availableParticipants = getAvailableParticipants(seedIndex);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -1051,7 +1051,7 @@ export default function EventBracket({
|
||||||
<Form key={participant.id} method="post" className="flex gap-2 items-end mb-2">
|
<Form key={participant.id} method="post" className="flex gap-2 items-end mb-2">
|
||||||
<input type="hidden" name="intent" value="upsert-odds" />
|
<input type="hidden" name="intent" value="upsert-odds" />
|
||||||
<input type="hidden" name="matchId" value={match.id} />
|
<input type="hidden" name="matchId" value={match.id} />
|
||||||
<input type="hidden" name="participantId" value={participant.id!} />
|
<input type="hidden" name="participantId" value={participant.id ?? ""} />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Label className="text-xs">{participant.name ?? "TBD"}</Label>
|
<Label className="text-xs">{participant.name ?? "TBD"}</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|
|
||||||
|
|
@ -65,8 +65,10 @@ export default function ExpectedValuesPage({ loaderData }: Route.ComponentProps)
|
||||||
// Sum only over participants shown in the table — excludes orphan EV records
|
// Sum only over participants shown in the table — excludes orphan EV records
|
||||||
// from prior simulation runs for participants no longer in this season.
|
// from prior simulation runs for participants no longer in this season.
|
||||||
const sorted = [...participants].toSorted((a, b) => {
|
const sorted = [...participants].toSorted((a, b) => {
|
||||||
const evA = existingEVs.has(a.id) ? evFromProbs(existingEVs.get(a.id)!) : 0;
|
const evDataA = existingEVs.get(a.id);
|
||||||
const evB = existingEVs.has(b.id) ? evFromProbs(existingEVs.get(b.id)!) : 0;
|
const evA = evDataA ? evFromProbs(evDataA) : 0;
|
||||||
|
const evDataB = existingEVs.get(b.id);
|
||||||
|
const evB = evDataB ? evFromProbs(evDataB) : 0;
|
||||||
return evB - evA;
|
return evB - evA;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -110,8 +110,8 @@ export async function loader(args: Route.LoaderArgs) {
|
||||||
const calendarDateFrom = today;
|
const calendarDateFrom = today;
|
||||||
const calendarDateTo = addDays(today, 30);
|
const calendarDateTo = addDays(today, 30);
|
||||||
|
|
||||||
const participantsBySportsSeason = myTeam
|
const participantsBySportsSeason = myTeam && season
|
||||||
? await getDraftedParticipantsBySportsSeason(myTeam.id, season!.id)
|
? await getDraftedParticipantsBySportsSeason(myTeam.id, season.id)
|
||||||
: new Map<string, Array<{ id: string; name: string }>>();
|
: new Map<string, Array<{ id: string; name: string }>>();
|
||||||
|
|
||||||
const sportsSeasons = await Promise.all(
|
const sportsSeasons = await Promise.all(
|
||||||
|
|
|
||||||
|
|
@ -218,7 +218,7 @@ export async function loader(args: Route.LoaderArgs) {
|
||||||
})
|
})
|
||||||
.map((r) => ({
|
.map((r) => ({
|
||||||
participantId: r.participantId,
|
participantId: r.participantId,
|
||||||
points: calculateBracketPoints(r.finalPosition!, scoringRules, templateId),
|
points: calculateBracketPoints(r.finalPosition ?? 0, scoringRules, templateId),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Track which participants have provisional (floor) scores — still competing
|
// Track which participants have provisional (floor) scores — still competing
|
||||||
|
|
|
||||||
|
|
@ -628,7 +628,8 @@ describe("full mobile reconnection scenario", () => {
|
||||||
const serverPicks = Array.from({ length: 24 }, (_, i) => {
|
const serverPicks = Array.from({ length: 24 }, (_, i) => {
|
||||||
const pickNum = i + 1;
|
const pickNum = i + 1;
|
||||||
const teamIdx = getTeamForPick(pickNum, draftSlots);
|
const teamIdx = getTeamForPick(pickNum, draftSlots);
|
||||||
return makePick(pickNum, teamIdx!.team.id, `player-${pickNum}`);
|
if (!teamIdx) throw new Error(`No team for pick ${pickNum}`);
|
||||||
|
return makePick(pickNum, teamIdx.team.id, `player-${pickNum}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Simulate draft-state-sync handler
|
// Simulate draft-state-sync handler
|
||||||
|
|
|
||||||
|
|
@ -51,8 +51,9 @@ describe('bracket-simulator', () => {
|
||||||
|
|
||||||
const results = simulateBracketSync(teams, 'nhl-8', 10000);
|
const results = simulateBracketSync(teams, 'nhl-8', 10000);
|
||||||
|
|
||||||
const strongDist = results.get('strong')!;
|
const strongDist = results.get('strong');
|
||||||
const weakDist = results.get('weak')!;
|
const weakDist = results.get('weak');
|
||||||
|
if (!strongDist || !weakDist) throw new Error('strong or weak result not found');
|
||||||
|
|
||||||
// Strong team should have higher championship probability
|
// Strong team should have higher championship probability
|
||||||
expect(strongDist[0]).toBeGreaterThan(weakDist[0]);
|
expect(strongDist[0]).toBeGreaterThan(weakDist[0]);
|
||||||
|
|
@ -76,7 +77,8 @@ describe('bracket-simulator', () => {
|
||||||
|
|
||||||
const results = simulateBracketSync(teams, 'nhl-8', 10000);
|
const results = simulateBracketSync(teams, 'nhl-8', 10000);
|
||||||
|
|
||||||
const championDist = results.get('champion')!;
|
const championDist = results.get('champion');
|
||||||
|
if (!championDist) throw new Error('champion result not found');
|
||||||
|
|
||||||
// Dominant team should win very often
|
// Dominant team should win very often
|
||||||
expect(championDist[0]).toBeGreaterThan(0.6); // >60% championship probability
|
expect(championDist[0]).toBeGreaterThan(0.6); // >60% championship probability
|
||||||
|
|
@ -139,8 +141,9 @@ describe('bracket-simulator', () => {
|
||||||
const results2 = simulateBracketSync(teams, 'nhl-8', 1000);
|
const results2 = simulateBracketSync(teams, 'nhl-8', 1000);
|
||||||
|
|
||||||
// Results won't be identical due to randomness, but should be in same ballpark
|
// Results won't be identical due to randomness, but should be in same ballpark
|
||||||
const dist1 = results1.get('1')!;
|
const dist1 = results1.get('1');
|
||||||
const dist2 = results2.get('1')!;
|
const dist2 = results2.get('1');
|
||||||
|
if (!dist1 || !dist2) throw new Error('Result for participant 1 not found');
|
||||||
|
|
||||||
// Championship probabilities should be within 10% of each other
|
// Championship probabilities should be within 10% of each other
|
||||||
expect(Math.abs(dist1[0] - dist2[0])).toBeLessThan(0.1);
|
expect(Math.abs(dist1[0] - dist2[0])).toBeLessThan(0.1);
|
||||||
|
|
@ -257,8 +260,9 @@ describe('bracket-simulator', () => {
|
||||||
|
|
||||||
const results = await simulateBracket(teams, 'nhl-8', 10000);
|
const results = await simulateBracket(teams, 'nhl-8', 10000);
|
||||||
|
|
||||||
const colDist = results.get('COL')!;
|
const colDist = results.get('COL');
|
||||||
const nyiDist = results.get('NYI')!;
|
const nyiDist = results.get('NYI');
|
||||||
|
if (!colDist || !nyiDist) throw new Error('COL or NYI result not found');
|
||||||
|
|
||||||
// Colorado should have highest championship probability
|
// Colorado should have highest championship probability
|
||||||
expect(colDist[0]).toBeGreaterThan(0.15); // >15%
|
expect(colDist[0]).toBeGreaterThan(0.15); // >15%
|
||||||
|
|
|
||||||
|
|
@ -45,8 +45,11 @@ describe('icm-calculator', () => {
|
||||||
|
|
||||||
const results = calculateICM(participants);
|
const results = calculateICM(participants);
|
||||||
|
|
||||||
const strongProbs = icmResultToArray(results.get('strong')!);
|
const strongResult = results.get('strong');
|
||||||
const weakProbs = icmResultToArray(results.get('weak')!);
|
const weakResult = results.get('weak');
|
||||||
|
if (!strongResult || !weakResult) throw new Error('Expected results not found');
|
||||||
|
const strongProbs = icmResultToArray(strongResult);
|
||||||
|
const weakProbs = icmResultToArray(weakResult);
|
||||||
|
|
||||||
// Strong team should have higher P(1st) than weak team
|
// Strong team should have higher P(1st) than weak team
|
||||||
expect(strongProbs[0]).toBeGreaterThan(weakProbs[0]);
|
expect(strongProbs[0]).toBeGreaterThan(weakProbs[0]);
|
||||||
|
|
@ -81,11 +84,15 @@ describe('icm-calculator', () => {
|
||||||
expect(results.size).toBe(32);
|
expect(results.size).toBe(32);
|
||||||
|
|
||||||
// Colorado (favorite) should have highest P(1st)
|
// Colorado (favorite) should have highest P(1st)
|
||||||
const colProbs = icmResultToArray(results.get('COL')!);
|
const colResult32 = results.get('COL');
|
||||||
|
if (!colResult32) throw new Error('COL result not found');
|
||||||
|
const colProbs = icmResultToArray(colResult32);
|
||||||
expect(colProbs[0]).toBeGreaterThan(0.05); // Should have >5% chance of 1st
|
expect(colProbs[0]).toBeGreaterThan(0.05); // Should have >5% chance of 1st
|
||||||
|
|
||||||
// Even the worst team should have some probability for all placements
|
// Even the worst team should have some probability for all placements
|
||||||
const worstProbs = icmResultToArray(results.get('TEAM32')!);
|
const worstResult = results.get('TEAM32');
|
||||||
|
if (!worstResult) throw new Error('TEAM32 result not found');
|
||||||
|
const worstProbs = icmResultToArray(worstResult);
|
||||||
worstProbs.forEach(p => {
|
worstProbs.forEach(p => {
|
||||||
expect(p).toBeGreaterThan(0); // Not zero
|
expect(p).toBeGreaterThan(0); // Not zero
|
||||||
expect(p).toBeLessThan(1); // Valid probability
|
expect(p).toBeLessThan(1); // Valid probability
|
||||||
|
|
@ -111,7 +118,9 @@ describe('icm-calculator', () => {
|
||||||
|
|
||||||
expect(results.size).toBe(1);
|
expect(results.size).toBe(1);
|
||||||
|
|
||||||
const probs = icmResultToArray(results.get('1')!);
|
const singleResult = results.get('1');
|
||||||
|
if (!singleResult) throw new Error('Result for participant 1 not found');
|
||||||
|
const probs = icmResultToArray(singleResult);
|
||||||
// With 1 participant, only 1st place is filled (removed from pool after)
|
// With 1 participant, only 1st place is filled (removed from pool after)
|
||||||
expect(probs[0]).toBeCloseTo(1.0, 1); // 100% chance of 1st
|
expect(probs[0]).toBeCloseTo(1.0, 1); // 100% chance of 1st
|
||||||
const sum = probs.reduce((acc, p) => acc + p, 0);
|
const sum = probs.reduce((acc, p) => acc + p, 0);
|
||||||
|
|
@ -167,8 +176,11 @@ describe('icm-calculator', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Participant 1 (higher prob) should have higher P(1st)
|
// Participant 1 (higher prob) should have higher P(1st)
|
||||||
const probs1 = icmResultToArray(results.get('1')!);
|
const r1 = results.get('1');
|
||||||
const probs2 = icmResultToArray(results.get('2')!);
|
const r2 = results.get('2');
|
||||||
|
if (!r1 || !r2) throw new Error('Results for participants 1 and 2 not found');
|
||||||
|
const probs1 = icmResultToArray(r1);
|
||||||
|
const probs2 = icmResultToArray(r2);
|
||||||
expect(probs1[0]).toBeGreaterThan(probs2[0]);
|
expect(probs1[0]).toBeGreaterThan(probs2[0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -187,10 +199,15 @@ describe('icm-calculator', () => {
|
||||||
|
|
||||||
const results = calculateICM(participants);
|
const results = calculateICM(participants);
|
||||||
|
|
||||||
const first = icmResultToArray(results.get('1st')!);
|
const rFirst = results.get('1st');
|
||||||
const second = icmResultToArray(results.get('2nd')!);
|
const rSecond = results.get('2nd');
|
||||||
const third = icmResultToArray(results.get('3rd')!);
|
const rThird = results.get('3rd');
|
||||||
const fourth = icmResultToArray(results.get('4th')!);
|
const rFourth = results.get('4th');
|
||||||
|
if (!rFirst || !rSecond || !rThird || !rFourth) throw new Error('Expected results not found');
|
||||||
|
const first = icmResultToArray(rFirst);
|
||||||
|
const second = icmResultToArray(rSecond);
|
||||||
|
const third = icmResultToArray(rThird);
|
||||||
|
const fourth = icmResultToArray(rFourth);
|
||||||
|
|
||||||
// P(1st place) should be ordered
|
// P(1st place) should be ordered
|
||||||
expect(first[0]).toBeGreaterThan(second[0]);
|
expect(first[0]).toBeGreaterThan(second[0]);
|
||||||
|
|
@ -212,8 +229,11 @@ describe('icm-calculator', () => {
|
||||||
expect(results.size).toBe(3);
|
expect(results.size).toBe(3);
|
||||||
|
|
||||||
// Colorado should have better odds than Arizona
|
// Colorado should have better odds than Arizona
|
||||||
const colProbs = icmResultToArray(results.get('COL')!);
|
const colResultOdds = results.get('COL');
|
||||||
const ariProbs = icmResultToArray(results.get('ARI')!);
|
const ariResult = results.get('ARI');
|
||||||
|
if (!colResultOdds || !ariResult) throw new Error('COL or ARI result not found');
|
||||||
|
const colProbs = icmResultToArray(colResultOdds);
|
||||||
|
const ariProbs = icmResultToArray(ariResult);
|
||||||
|
|
||||||
expect(colProbs[0]).toBeGreaterThan(ariProbs[0]);
|
expect(colProbs[0]).toBeGreaterThan(ariProbs[0]);
|
||||||
|
|
||||||
|
|
@ -229,8 +249,11 @@ describe('icm-calculator', () => {
|
||||||
|
|
||||||
const results = calculateICMFromOdds(odds);
|
const results = calculateICMFromOdds(odds);
|
||||||
|
|
||||||
const favProbs = icmResultToArray(results.get('FAV')!);
|
const favResult = results.get('FAV');
|
||||||
const dogProbs = icmResultToArray(results.get('DOG')!);
|
const dogResult = results.get('DOG');
|
||||||
|
if (!favResult || !dogResult) throw new Error('FAV or DOG result not found');
|
||||||
|
const favProbs = icmResultToArray(favResult);
|
||||||
|
const dogProbs = icmResultToArray(dogResult);
|
||||||
|
|
||||||
// Favorite should have higher P(1st)
|
// Favorite should have higher P(1st)
|
||||||
expect(favProbs[0]).toBeGreaterThan(dogProbs[0]);
|
expect(favProbs[0]).toBeGreaterThan(dogProbs[0]);
|
||||||
|
|
@ -303,17 +326,21 @@ describe('icm-calculator', () => {
|
||||||
expect(results.size).toBe(32);
|
expect(results.size).toBe(32);
|
||||||
|
|
||||||
// Favorite (Colorado) should have reasonable championship probability
|
// Favorite (Colorado) should have reasonable championship probability
|
||||||
const colProbs = icmResultToArray(results.get('COL')!);
|
const colResultInteg = results.get('COL');
|
||||||
|
const detResult = results.get('DET');
|
||||||
|
const longResult = results.get('TEAM32');
|
||||||
|
if (!colResultInteg || !detResult || !longResult) throw new Error('Expected results not found');
|
||||||
|
const colProbs = icmResultToArray(colResultInteg);
|
||||||
expect(colProbs[0]).toBeGreaterThan(0.05); // >5% for 1st
|
expect(colProbs[0]).toBeGreaterThan(0.05); // >5% for 1st
|
||||||
expect(colProbs[0]).toBeLessThan(0.35); // <35% for 1st (not guaranteed)
|
expect(colProbs[0]).toBeLessThan(0.35); // <35% for 1st (not guaranteed)
|
||||||
|
|
||||||
// Middle team (Detroit) should have middling probabilities
|
// Middle team (Detroit) should have middling probabilities
|
||||||
const detProbs = icmResultToArray(results.get('DET')!);
|
const detProbs = icmResultToArray(detResult);
|
||||||
expect(detProbs[0]).toBeGreaterThan(0); // Some chance
|
expect(detProbs[0]).toBeGreaterThan(0); // Some chance
|
||||||
expect(detProbs[0]).toBeLessThan(0.10); // But not high
|
expect(detProbs[0]).toBeLessThan(0.10); // But not high
|
||||||
|
|
||||||
// Longshot should have very small but non-zero probabilities
|
// Longshot should have very small but non-zero probabilities
|
||||||
const longProbs = icmResultToArray(results.get('TEAM32')!);
|
const longProbs = icmResultToArray(longResult);
|
||||||
expect(longProbs[0]).toBeGreaterThan(0); // Not impossible
|
expect(longProbs[0]).toBeGreaterThan(0); // Not impossible
|
||||||
expect(longProbs[0]).toBeLessThan(0.05); // But unlikely (with 32 teams, even worst has ~3% uniform)
|
expect(longProbs[0]).toBeLessThan(0.05); // But unlikely (with 32 teams, even worst has ~3% uniform)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -200,8 +200,8 @@ describe('probability-engine', () => {
|
||||||
const eloRatings = convertFuturesToElo(odds, 'american');
|
const eloRatings = convertFuturesToElo(odds, 'american');
|
||||||
|
|
||||||
expect(eloRatings.size).toBe(3);
|
expect(eloRatings.size).toBe(3);
|
||||||
expect(eloRatings.get('1')).toBeGreaterThan(eloRatings.get('2')!);
|
expect(eloRatings.get('1')).toBeGreaterThan(eloRatings.get('2') ?? 0);
|
||||||
expect(eloRatings.get('2')).toBeGreaterThan(eloRatings.get('3')!);
|
expect(eloRatings.get('2')).toBeGreaterThan(eloRatings.get('3') ?? 0);
|
||||||
|
|
||||||
// Strongest team should be near max Elo
|
// Strongest team should be near max Elo
|
||||||
expect(eloRatings.get('1')).toBeGreaterThan(1600);
|
expect(eloRatings.get('1')).toBeGreaterThan(1600);
|
||||||
|
|
@ -220,8 +220,8 @@ describe('probability-engine', () => {
|
||||||
const eloRatings = convertFuturesToElo(odds, 'decimal');
|
const eloRatings = convertFuturesToElo(odds, 'decimal');
|
||||||
|
|
||||||
expect(eloRatings.size).toBe(3);
|
expect(eloRatings.size).toBe(3);
|
||||||
expect(eloRatings.get('1')).toBeGreaterThan(eloRatings.get('2')!);
|
expect(eloRatings.get('1')).toBeGreaterThan(eloRatings.get('2') ?? 0);
|
||||||
expect(eloRatings.get('2')).toBeGreaterThan(eloRatings.get('3')!);
|
expect(eloRatings.get('2')).toBeGreaterThan(eloRatings.get('3') ?? 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses custom calibration parameters', () => {
|
it('uses custom calibration parameters', () => {
|
||||||
|
|
@ -302,8 +302,8 @@ describe('probability-engine', () => {
|
||||||
];
|
];
|
||||||
|
|
||||||
const eloRatings = convertFuturesToElo(odds, 'american');
|
const eloRatings = convertFuturesToElo(odds, 'american');
|
||||||
const colElo = eloRatings.get('col')!;
|
const colElo = eloRatings.get('col') ?? 1500;
|
||||||
const nyiElo = eloRatings.get('nyi')!;
|
const nyiElo = eloRatings.get('nyi') ?? 1500;
|
||||||
|
|
||||||
// Calculate predicted game line
|
// Calculate predicted game line
|
||||||
const predicted = eloWinProbability(colElo, nyiElo);
|
const predicted = eloWinProbability(colElo, nyiElo);
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,8 @@ function simulate8TeamBracket(teams: TeamForSimulation[]): SimulationResult {
|
||||||
|
|
||||||
// Finals: 2 → 1
|
// Finals: 2 → 1
|
||||||
const champion = simulateMatchup(finalists[0], finalists[1]);
|
const champion = simulateMatchup(finalists[0], finalists[1]);
|
||||||
const runnerUp = finalists.find(t => t.participantId !== champion.participantId)!;
|
const runnerUp = finalists.find(t => t.participantId !== champion.participantId);
|
||||||
|
if (!runnerUp) throw new Error("Runner-up not found in finalists");
|
||||||
|
|
||||||
placements.set(champion.participantId, 1);
|
placements.set(champion.participantId, 1);
|
||||||
placements.set(runnerUp.participantId, 2);
|
placements.set(runnerUp.participantId, 2);
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,8 @@ export function calculateICM(
|
||||||
|
|
||||||
// Record results
|
// Record results
|
||||||
for (let pos = 0; pos < placements.length; pos++) {
|
for (let pos = 0; pos < placements.length; pos++) {
|
||||||
counts.get(placements[pos])![pos]++;
|
const participantCounts = counts.get(placements[pos]);
|
||||||
|
if (participantCounts) participantCounts[pos]++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Progress logging every 10,000 iterations
|
// Progress logging every 10,000 iterations
|
||||||
|
|
@ -149,7 +150,7 @@ export function calculateICM(
|
||||||
const results = new Map<string, ICMResult>();
|
const results = new Map<string, ICMResult>();
|
||||||
|
|
||||||
for (const p of normalized) {
|
for (const p of normalized) {
|
||||||
const participantCounts = counts.get(p.participantId)!;
|
const participantCounts = counts.get(p.participantId) ?? Array(scoringPlaces).fill(0);
|
||||||
const probabilities = participantCounts.map(count => count / iterations);
|
const probabilities = participantCounts.map(count => count / iterations);
|
||||||
|
|
||||||
results.set(p.participantId, {
|
results.set(p.participantId, {
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,7 @@ export async function updateProbabilitiesAfterResult(
|
||||||
const finishedMap = new Map(
|
const finishedMap = new Map(
|
||||||
results
|
results
|
||||||
.filter(r => r.finalPosition !== null)
|
.filter(r => r.finalPosition !== null)
|
||||||
.map(r => [r.participantId, r.finalPosition!])
|
.map(r => [r.participantId, r.finalPosition ?? 0])
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update finished participants
|
// Update finished participants
|
||||||
|
|
@ -263,7 +263,7 @@ export async function previewProbabilityUpdate(
|
||||||
const finishedMap = new Map(
|
const finishedMap = new Map(
|
||||||
results
|
results
|
||||||
.filter(r => r.finalPosition !== null)
|
.filter(r => r.finalPosition !== null)
|
||||||
.map(r => [r.participantId, r.finalPosition!])
|
.map(r => [r.participantId, r.finalPosition ?? 0])
|
||||||
);
|
);
|
||||||
|
|
||||||
const comparisons: ProbabilityComparison[] = [];
|
const comparisons: ProbabilityComparison[] = [];
|
||||||
|
|
|
||||||
|
|
@ -306,18 +306,18 @@ describe("UCLSimulator.simulate()", () => {
|
||||||
// Actually: QF1: team-1 beats team-4; QF2: team-6 beats team-8; etc.
|
// Actually: QF1: team-1 beats team-4; QF2: team-6 beats team-8; etc.
|
||||||
const qfMatches = Array.from({ length: 4 }, (_, i) => {
|
const qfMatches = Array.from({ length: 4 }, (_, i) => {
|
||||||
const matchNum = i + 1;
|
const matchNum = i + 1;
|
||||||
const r16w1 = r16Matches[(matchNum - 1) * 2].winnerId!;
|
const r16w1 = r16Matches[(matchNum - 1) * 2].winnerId ?? "";
|
||||||
const r16w2 = r16Matches[(matchNum - 1) * 2 + 1].winnerId!;
|
const r16w2 = r16Matches[(matchNum - 1) * 2 + 1].winnerId ?? "";
|
||||||
const winner = matchNum === 1 ? r16w1 : r16w2;
|
const winner = matchNum === 1 ? r16w1 : r16w2;
|
||||||
const loser = matchNum === 1 ? r16w2 : r16w1;
|
const loser = matchNum === 1 ? r16w2 : r16w1;
|
||||||
return makeQFMatch(matchNum, { winnerId: winner, loserId: loser, isComplete: true });
|
return makeQFMatch(matchNum, { winnerId: winner, loserId: loser, isComplete: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
// SF: QF1 winner (team-1) + QF2 winner → SF1: team-1 wins
|
// SF: QF1 winner (team-1) + QF2 winner → SF1: team-1 wins
|
||||||
const sf1Winner = qfMatches[0].winnerId!;
|
const sf1Winner = qfMatches[0].winnerId ?? "";
|
||||||
const sf1Loser = qfMatches[1].winnerId!;
|
const sf1Loser = qfMatches[1].winnerId ?? "";
|
||||||
const sf2Winner = qfMatches[2].winnerId!;
|
const sf2Winner = qfMatches[2].winnerId ?? "";
|
||||||
const sf2Loser = qfMatches[3].winnerId!;
|
const sf2Loser = qfMatches[3].winnerId ?? "";
|
||||||
const sfMatches = [
|
const sfMatches = [
|
||||||
makeSFMatch(1, { winnerId: sf1Winner, loserId: sf1Loser, isComplete: true }),
|
makeSFMatch(1, { winnerId: sf1Winner, loserId: sf1Loser, isComplete: true }),
|
||||||
makeSFMatch(2, { winnerId: sf2Winner, loserId: sf2Loser, isComplete: true }),
|
makeSFMatch(2, { winnerId: sf2Winner, loserId: sf2Loser, isComplete: true }),
|
||||||
|
|
@ -335,8 +335,9 @@ describe("UCLSimulator.simulate()", () => {
|
||||||
const sim = new UCLSimulator();
|
const sim = new UCLSimulator();
|
||||||
const results = await sim.simulate("season-1");
|
const results = await sim.simulate("season-1");
|
||||||
|
|
||||||
const championResult = results.find((r) => r.participantId === actualChampion)!;
|
const championResult = results.find((r) => r.participantId === actualChampion);
|
||||||
const finalistResult = results.find((r) => r.participantId === actualFinalist)!;
|
const finalistResult = results.find((r) => r.participantId === actualFinalist);
|
||||||
|
if (!championResult || !finalistResult) throw new Error('Champion or finalist result not found');
|
||||||
|
|
||||||
// Champion must have probFirst = 1.0
|
// Champion must have probFirst = 1.0
|
||||||
expect(championResult.probabilities.probFirst).toBeCloseTo(1.0, 3);
|
expect(championResult.probabilities.probFirst).toBeCloseTo(1.0, 3);
|
||||||
|
|
@ -347,8 +348,9 @@ describe("UCLSimulator.simulate()", () => {
|
||||||
expect(finalistResult.probabilities.probSecond).toBeCloseTo(1.0, 3);
|
expect(finalistResult.probabilities.probSecond).toBeCloseTo(1.0, 3);
|
||||||
|
|
||||||
// R16 losers (teams that lost in R16) must have all probs = 0
|
// R16 losers (teams that lost in R16) must have all probs = 0
|
||||||
const r16LoserId = r16Matches[0].loserId!; // team-2 lost in R16 match 1
|
const r16LoserId = r16Matches[0].loserId ?? ""; // team-2 lost in R16 match 1
|
||||||
const r16LoserResult = results.find((r) => r.participantId === r16LoserId)!;
|
const r16LoserResult = results.find((r) => r.participantId === r16LoserId);
|
||||||
|
if (!r16LoserResult) throw new Error('R16 loser result not found');
|
||||||
const r16Sum = Object.values(r16LoserResult.probabilities).reduce((a, b) => a + b, 0);
|
const r16Sum = Object.values(r16LoserResult.probabilities).reduce((a, b) => a + b, 0);
|
||||||
expect(r16Sum).toBeCloseTo(0, 5);
|
expect(r16Sum).toBeCloseTo(0, 5);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -174,7 +174,8 @@ export class AutoRacingSimulator implements Simulator {
|
||||||
for (let sim = 0; sim < NUM_SIMULATIONS; sim++) {
|
for (let sim = 0; sim < NUM_SIMULATIONS; sim++) {
|
||||||
const finishOrder = weightedDrawWithoutReplacement(ids, weights);
|
const finishOrder = weightedDrawWithoutReplacement(ids, weights);
|
||||||
for (let rank = 0; rank < Math.min(8, finishOrder.length); rank++) {
|
for (let rank = 0; rank < Math.min(8, finishOrder.length); rank++) {
|
||||||
rankCounts.get(finishOrder[rank])![rank]++;
|
const counts = rankCounts.get(finishOrder[rank]);
|
||||||
|
if (counts) counts[rank]++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -219,26 +220,30 @@ export class AutoRacingSimulator implements Simulator {
|
||||||
.map(([id]) => id);
|
.map(([id]) => id);
|
||||||
|
|
||||||
for (let rank = 0; rank < Math.min(8, finalOrder.length); rank++) {
|
for (let rank = 0; rank < Math.min(8, finalOrder.length); rank++) {
|
||||||
rankCounts.get(finalOrder[rank])![rank]++;
|
const counts = rankCounts.get(finalOrder[rank]);
|
||||||
|
if (counts) counts[rank]++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8. Convert counts → probability distributions
|
// 8. Convert counts → probability distributions
|
||||||
const results: SimulationResult[] = participants.map((p) => ({
|
const results: SimulationResult[] = participants.map((p) => {
|
||||||
|
const counts = rankCounts.get(p.id) ?? [0,0,0,0,0,0,0,0];
|
||||||
|
return {
|
||||||
participantId: p.id,
|
participantId: p.id,
|
||||||
probabilities: {
|
probabilities: {
|
||||||
probFirst: rankCounts.get(p.id)![0] / NUM_SIMULATIONS,
|
probFirst: counts[0] / NUM_SIMULATIONS,
|
||||||
probSecond: rankCounts.get(p.id)![1] / NUM_SIMULATIONS,
|
probSecond: counts[1] / NUM_SIMULATIONS,
|
||||||
probThird: rankCounts.get(p.id)![2] / NUM_SIMULATIONS,
|
probThird: counts[2] / NUM_SIMULATIONS,
|
||||||
probFourth: rankCounts.get(p.id)![3] / NUM_SIMULATIONS,
|
probFourth: counts[3] / NUM_SIMULATIONS,
|
||||||
probFifth: rankCounts.get(p.id)![4] / NUM_SIMULATIONS,
|
probFifth: counts[4] / NUM_SIMULATIONS,
|
||||||
probSixth: rankCounts.get(p.id)![5] / NUM_SIMULATIONS,
|
probSixth: counts[5] / NUM_SIMULATIONS,
|
||||||
probSeventh: rankCounts.get(p.id)![6] / NUM_SIMULATIONS,
|
probSeventh: counts[6] / NUM_SIMULATIONS,
|
||||||
probEighth: rankCounts.get(p.id)![7] / NUM_SIMULATIONS,
|
probEighth: counts[7] / NUM_SIMULATIONS,
|
||||||
},
|
},
|
||||||
source: this.source,
|
source: this.source,
|
||||||
}));
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// 9. Per-position normalization: each column should sum to exactly 1.0 but
|
// 9. Per-position normalization: each column should sum to exactly 1.0 but
|
||||||
// floating-point division (count / 10000) accumulates small errors across
|
// floating-point division (count / 10000) accumulates small errors across
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ export class BracketSimulator implements Simulator {
|
||||||
if (hasOdds) {
|
if (hasOdds) {
|
||||||
const oddsInput = evRows
|
const oddsInput = evRows
|
||||||
.filter((r) => r.sourceOdds !== null)
|
.filter((r) => r.sourceOdds !== null)
|
||||||
.map((r) => ({ participantId: r.participantId, odds: r.sourceOdds! }));
|
.map((r) => ({ participantId: r.participantId, odds: r.sourceOdds ?? 0 }));
|
||||||
eloMap = convertFuturesToElo(oddsInput);
|
eloMap = convertFuturesToElo(oddsInput);
|
||||||
} else {
|
} else {
|
||||||
// Fall back: treat probFirst (as %) as championship win probability,
|
// Fall back: treat probFirst (as %) as championship win probability,
|
||||||
|
|
@ -71,7 +71,7 @@ export class BracketSimulator implements Simulator {
|
||||||
.filter((r) => eloMap.has(r.participantId))
|
.filter((r) => eloMap.has(r.participantId))
|
||||||
.map((r) => ({
|
.map((r) => ({
|
||||||
participantId: r.participantId,
|
participantId: r.participantId,
|
||||||
elo: eloMap.get(r.participantId)!,
|
elo: eloMap.get(r.participantId) ?? 1500,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (teamsForSimulation.length === 0) {
|
if (teamsForSimulation.length === 0) {
|
||||||
|
|
|
||||||
|
|
@ -385,12 +385,12 @@ export class NBASimulator implements Simulator {
|
||||||
const { winner: champion, loser: finalist } = simSeries(eastChamp, westChamp);
|
const { winner: champion, loser: finalist } = simSeries(eastChamp, westChamp);
|
||||||
|
|
||||||
// ── Record counts (maps are pre-populated so .get() is always defined) ───
|
// ── Record counts (maps are pre-populated so .get() is always defined) ───
|
||||||
championCounts.set(champion.id, championCounts.get(champion.id)! + 1);
|
championCounts.set(champion.id, (championCounts.get(champion.id) ?? 0) + 1);
|
||||||
finalistCounts.set(finalist.id, finalistCounts.get(finalist.id)! + 1);
|
finalistCounts.set(finalist.id, (finalistCounts.get(finalist.id) ?? 0) + 1);
|
||||||
confFinalLoserCounts.set(eastCFLoser.id, confFinalLoserCounts.get(eastCFLoser.id)! + 1);
|
confFinalLoserCounts.set(eastCFLoser.id, (confFinalLoserCounts.get(eastCFLoser.id) ?? 0) + 1);
|
||||||
confFinalLoserCounts.set(westCFLoser.id, confFinalLoserCounts.get(westCFLoser.id)! + 1);
|
confFinalLoserCounts.set(westCFLoser.id, (confFinalLoserCounts.get(westCFLoser.id) ?? 0) + 1);
|
||||||
for (const loser of [...eastR2Losers, ...westR2Losers]) {
|
for (const loser of [...eastR2Losers, ...westR2Losers]) {
|
||||||
confSemiLoserCounts.set(loser.id, confSemiLoserCounts.get(loser.id)! + 1);
|
confSemiLoserCounts.set(loser.id, (confSemiLoserCounts.get(loser.id) ?? 0) + 1);
|
||||||
}
|
}
|
||||||
// Round 1 losers are not counted (0 points per scoring rules).
|
// Round 1 losers are not counted (0 points per scoring rules).
|
||||||
}
|
}
|
||||||
|
|
@ -402,10 +402,10 @@ export class NBASimulator implements Simulator {
|
||||||
// probFifth–Eighth → confSemiLoserCounts / (4*N) — 4 conf semi losers per sim
|
// probFifth–Eighth → confSemiLoserCounts / (4*N) — 4 conf semi losers per sim
|
||||||
const N = NUM_SIMULATIONS;
|
const N = NUM_SIMULATIONS;
|
||||||
const results: SimulationResult[] = participantIds.map((participantId) => {
|
const results: SimulationResult[] = participantIds.map((participantId) => {
|
||||||
const c = championCounts.get(participantId)!;
|
const c = championCounts.get(participantId) ?? 0;
|
||||||
const f = finalistCounts.get(participantId)!;
|
const f = finalistCounts.get(participantId) ?? 0;
|
||||||
const cf = confFinalLoserCounts.get(participantId)!;
|
const cf = confFinalLoserCounts.get(participantId) ?? 0;
|
||||||
const cs = confSemiLoserCounts.get(participantId)!;
|
const cs = confSemiLoserCounts.get(participantId) ?? 0;
|
||||||
return {
|
return {
|
||||||
participantId,
|
participantId,
|
||||||
probabilities: {
|
probabilities: {
|
||||||
|
|
|
||||||
|
|
@ -343,16 +343,24 @@ export class NCAAMSimulator implements Simulator {
|
||||||
const byRound = new Map<string, typeof allMatches>();
|
const byRound = new Map<string, typeof allMatches>();
|
||||||
for (const m of allMatches) {
|
for (const m of allMatches) {
|
||||||
if (!byRound.has(m.round)) byRound.set(m.round, []);
|
if (!byRound.has(m.round)) byRound.set(m.round, []);
|
||||||
byRound.get(m.round)!.push(m);
|
byRound.get(m.round)?.push(m);
|
||||||
}
|
}
|
||||||
|
|
||||||
const firstFourMatches = byRound.has("First Four") ? sortByMatchNumber(byRound.get("First Four")!) : [];
|
const rawFirstFour = byRound.get("First Four");
|
||||||
const r64Matches = byRound.has("Round of 64") ? sortByMatchNumber(byRound.get("Round of 64")!) : null;
|
const rawR64 = byRound.get("Round of 64");
|
||||||
const r32Matches = byRound.has("Round of 32") ? sortByMatchNumber(byRound.get("Round of 32")!) : null;
|
const rawR32 = byRound.get("Round of 32");
|
||||||
const s16Matches = byRound.has("Sweet Sixteen") ? sortByMatchNumber(byRound.get("Sweet Sixteen")!) : null;
|
const rawS16 = byRound.get("Sweet Sixteen");
|
||||||
const e8Matches = byRound.has("Elite Eight") ? sortByMatchNumber(byRound.get("Elite Eight")!) : null;
|
const rawE8 = byRound.get("Elite Eight");
|
||||||
const ffMatches = byRound.has("Final Four") ? sortByMatchNumber(byRound.get("Final Four")!) : null;
|
const rawFF = byRound.get("Final Four");
|
||||||
const champMatches = byRound.has("Championship") ? sortByMatchNumber(byRound.get("Championship")!) : null;
|
const rawChamp = byRound.get("Championship");
|
||||||
|
|
||||||
|
const firstFourMatches = rawFirstFour ? sortByMatchNumber(rawFirstFour) : [];
|
||||||
|
const r64Matches = rawR64 ? sortByMatchNumber(rawR64) : null;
|
||||||
|
const r32Matches = rawR32 ? sortByMatchNumber(rawR32) : null;
|
||||||
|
const s16Matches = rawS16 ? sortByMatchNumber(rawS16) : null;
|
||||||
|
const e8Matches = rawE8 ? sortByMatchNumber(rawE8) : null;
|
||||||
|
const ffMatches = rawFF ? sortByMatchNumber(rawFF) : null;
|
||||||
|
const champMatches = rawChamp ? sortByMatchNumber(rawChamp) : null;
|
||||||
|
|
||||||
if (!r64Matches || !r32Matches || !s16Matches || !e8Matches || !ffMatches || !champMatches) {
|
if (!r64Matches || !r32Matches || !s16Matches || !e8Matches || !ffMatches || !champMatches) {
|
||||||
const found = [...byRound.keys()].join(", ");
|
const found = [...byRound.keys()].join(", ");
|
||||||
|
|
@ -404,7 +412,7 @@ export class NCAAMSimulator implements Simulator {
|
||||||
const regions =
|
const regions =
|
||||||
(bracketEvent.bracketRegionConfig as BracketRegion[] | null) ??
|
(bracketEvent.bracketRegionConfig as BracketRegion[] | null) ??
|
||||||
// Fall back to the template's built-in regions — single source of truth
|
// Fall back to the template's built-in regions — single source of truth
|
||||||
NCAA_68.regions!;
|
NCAA_68.regions ?? [];
|
||||||
|
|
||||||
const slotMap = buildNCAA68SlotMap(regions);
|
const slotMap = buildNCAA68SlotMap(regions);
|
||||||
for (let i = 0; i < slotMap.playInOffsets.length; i++) {
|
for (let i = 0; i < slotMap.playInOffsets.length; i++) {
|
||||||
|
|
@ -533,23 +541,25 @@ export class NCAAMSimulator implements Simulator {
|
||||||
// ffSimWinners: Map<r64MatchNumber, winnerId> for the null participant2Id slots
|
// ffSimWinners: Map<r64MatchNumber, winnerId> for the null participant2Id slots
|
||||||
const ffSimWinners = new Map<number, string>();
|
const ffSimWinners = new Map<number, string>();
|
||||||
for (const [ffMatchNum, r64MatchNum] of ffToR64) {
|
for (const [ffMatchNum, r64MatchNum] of ffToR64) {
|
||||||
const m = firstFourByNum.get(ffMatchNum)!;
|
const m = firstFourByNum.get(ffMatchNum);
|
||||||
|
if (!m) continue;
|
||||||
const winner = m.isComplete && m.winnerId
|
const winner = m.isComplete && m.winnerId
|
||||||
? m.winnerId
|
? m.winnerId
|
||||||
: simMatch(m.participant1Id!, m.participant2Id!).winner;
|
: simMatch(m.participant1Id ?? "", m.participant2Id ?? "").winner;
|
||||||
ffSimWinners.set(r64MatchNum, winner);
|
ffSimWinners.set(r64MatchNum, winner);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Round of 64 (32 matches → 32 winners) ────────────────────────────
|
// ── Round of 64 (32 matches → 32 winners) ────────────────────────────
|
||||||
const r64Winners: string[] = [];
|
const r64Winners: string[] = [];
|
||||||
for (let i = 1; i <= 32; i++) {
|
for (let i = 1; i <= 32; i++) {
|
||||||
const m = r64ByNum.get(i)!;
|
const m = r64ByNum.get(i);
|
||||||
|
if (!m) continue;
|
||||||
// participant2Id may be null if this slot is filled by a First Four winner
|
// participant2Id may be null if this slot is filled by a First Four winner
|
||||||
const p2 = m.participant2Id ?? ffSimWinners.get(i) ?? null;
|
const p2 = m.participant2Id ?? ffSimWinners.get(i) ?? null;
|
||||||
if (m.isComplete && m.winnerId) {
|
if (m.isComplete && m.winnerId) {
|
||||||
r64Winners.push(m.winnerId);
|
r64Winners.push(m.winnerId);
|
||||||
} else {
|
} else {
|
||||||
r64Winners.push(simMatch(m.participant1Id!, p2!).winner);
|
r64Winners.push(simMatch(m.participant1Id ?? "", p2 ?? "").winner);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -591,8 +601,8 @@ export class NCAAMSimulator implements Simulator {
|
||||||
// Derive loser from stored participants if loserId is missing
|
// Derive loser from stored participants if loserId is missing
|
||||||
loser = dbMatch.loserId ??
|
loser = dbMatch.loserId ??
|
||||||
(dbMatch.participant1Id === dbMatch.winnerId
|
(dbMatch.participant1Id === dbMatch.winnerId
|
||||||
? dbMatch.participant2Id!
|
? dbMatch.participant2Id ?? ""
|
||||||
: dbMatch.participant1Id!);
|
: dbMatch.participant1Id ?? "");
|
||||||
} else {
|
} else {
|
||||||
const p1 = s16Winners[(i - 1) * 2];
|
const p1 = s16Winners[(i - 1) * 2];
|
||||||
const p2 = s16Winners[(i - 1) * 2 + 1];
|
const p2 = s16Winners[(i - 1) * 2 + 1];
|
||||||
|
|
@ -612,8 +622,8 @@ export class NCAAMSimulator implements Simulator {
|
||||||
winner = dbMatch.winnerId;
|
winner = dbMatch.winnerId;
|
||||||
loser = dbMatch.loserId ??
|
loser = dbMatch.loserId ??
|
||||||
(dbMatch.participant1Id === dbMatch.winnerId
|
(dbMatch.participant1Id === dbMatch.winnerId
|
||||||
? dbMatch.participant2Id!
|
? dbMatch.participant2Id ?? ""
|
||||||
: dbMatch.participant1Id!);
|
: dbMatch.participant1Id ?? "");
|
||||||
} else {
|
} else {
|
||||||
const p1 = e8Winners[(i - 1) * 2];
|
const p1 = e8Winners[(i - 1) * 2];
|
||||||
const p2 = e8Winners[(i - 1) * 2 + 1];
|
const p2 = e8Winners[(i - 1) * 2 + 1];
|
||||||
|
|
@ -630,8 +640,8 @@ export class NCAAMSimulator implements Simulator {
|
||||||
champion = champMatch.winnerId;
|
champion = champMatch.winnerId;
|
||||||
finalist = champMatch.loserId ??
|
finalist = champMatch.loserId ??
|
||||||
(champMatch.participant1Id === champMatch.winnerId
|
(champMatch.participant1Id === champMatch.winnerId
|
||||||
? champMatch.participant2Id!
|
? champMatch.participant2Id ?? ""
|
||||||
: champMatch.participant1Id!);
|
: champMatch.participant1Id ?? "");
|
||||||
} else {
|
} else {
|
||||||
({ winner: champion, loser: finalist } = simMatch(ffWinners[0], ffWinners[1]));
|
({ winner: champion, loser: finalist } = simMatch(ffWinners[0], ffWinners[1]));
|
||||||
}
|
}
|
||||||
|
|
@ -646,10 +656,10 @@ export class NCAAMSimulator implements Simulator {
|
||||||
// probFifth–Eighth → e8LoserCounts / (4*N) → 4 losers * 1/(4N) = 1
|
// probFifth–Eighth → e8LoserCounts / (4*N) → 4 losers * 1/(4N) = 1
|
||||||
const N = NUM_SIMULATIONS;
|
const N = NUM_SIMULATIONS;
|
||||||
const results: SimulationResult[] = participantIds.map((participantId) => {
|
const results: SimulationResult[] = participantIds.map((participantId) => {
|
||||||
const c = championCounts.get(participantId)!;
|
const c = championCounts.get(participantId) ?? 0;
|
||||||
const f = finalistCounts.get(participantId)!;
|
const f = finalistCounts.get(participantId) ?? 0;
|
||||||
const ff = ffLoserCounts.get(participantId)!;
|
const ff = ffLoserCounts.get(participantId) ?? 0;
|
||||||
const e8 = e8LoserCounts.get(participantId)!;
|
const e8 = e8LoserCounts.get(participantId) ?? 0;
|
||||||
return {
|
return {
|
||||||
participantId,
|
participantId,
|
||||||
probabilities: {
|
probabilities: {
|
||||||
|
|
|
||||||
|
|
@ -320,16 +320,24 @@ export class NCAAWSimulator implements Simulator {
|
||||||
const byRound = new Map<string, typeof allMatches>();
|
const byRound = new Map<string, typeof allMatches>();
|
||||||
for (const m of allMatches) {
|
for (const m of allMatches) {
|
||||||
if (!byRound.has(m.round)) byRound.set(m.round, []);
|
if (!byRound.has(m.round)) byRound.set(m.round, []);
|
||||||
byRound.get(m.round)!.push(m);
|
byRound.get(m.round)?.push(m);
|
||||||
}
|
}
|
||||||
|
|
||||||
const firstFourMatches = byRound.has("First Four") ? sortByMatchNumber(byRound.get("First Four")!) : [];
|
const rawFirstFour = byRound.get("First Four");
|
||||||
const r64Matches = byRound.has("Round of 64") ? sortByMatchNumber(byRound.get("Round of 64")!) : null;
|
const rawR64 = byRound.get("Round of 64");
|
||||||
const r32Matches = byRound.has("Round of 32") ? sortByMatchNumber(byRound.get("Round of 32")!) : null;
|
const rawR32 = byRound.get("Round of 32");
|
||||||
const s16Matches = byRound.has("Sweet Sixteen") ? sortByMatchNumber(byRound.get("Sweet Sixteen")!) : null;
|
const rawS16 = byRound.get("Sweet Sixteen");
|
||||||
const e8Matches = byRound.has("Elite Eight") ? sortByMatchNumber(byRound.get("Elite Eight")!) : null;
|
const rawE8 = byRound.get("Elite Eight");
|
||||||
const ffMatches = byRound.has("Final Four") ? sortByMatchNumber(byRound.get("Final Four")!) : null;
|
const rawFF = byRound.get("Final Four");
|
||||||
const champMatches = byRound.has("Championship") ? sortByMatchNumber(byRound.get("Championship")!) : null;
|
const rawChamp = byRound.get("Championship");
|
||||||
|
|
||||||
|
const firstFourMatches = rawFirstFour ? sortByMatchNumber(rawFirstFour) : [];
|
||||||
|
const r64Matches = rawR64 ? sortByMatchNumber(rawR64) : null;
|
||||||
|
const r32Matches = rawR32 ? sortByMatchNumber(rawR32) : null;
|
||||||
|
const s16Matches = rawS16 ? sortByMatchNumber(rawS16) : null;
|
||||||
|
const e8Matches = rawE8 ? sortByMatchNumber(rawE8) : null;
|
||||||
|
const ffMatches = rawFF ? sortByMatchNumber(rawFF) : null;
|
||||||
|
const champMatches = rawChamp ? sortByMatchNumber(rawChamp) : null;
|
||||||
|
|
||||||
if (!r64Matches || !r32Matches || !s16Matches || !e8Matches || !ffMatches || !champMatches) {
|
if (!r64Matches || !r32Matches || !s16Matches || !e8Matches || !ffMatches || !champMatches) {
|
||||||
const found = [...byRound.keys()].join(", ");
|
const found = [...byRound.keys()].join(", ");
|
||||||
|
|
@ -371,7 +379,7 @@ export class NCAAWSimulator implements Simulator {
|
||||||
const regions =
|
const regions =
|
||||||
(bracketEvent.bracketRegionConfig as BracketRegion[] | null) ??
|
(bracketEvent.bracketRegionConfig as BracketRegion[] | null) ??
|
||||||
// Fall back to the template's built-in regions — single source of truth
|
// Fall back to the template's built-in regions — single source of truth
|
||||||
NCAA_68.regions!;
|
NCAA_68.regions ?? [];
|
||||||
|
|
||||||
const slotMap = buildNCAA68SlotMap(regions);
|
const slotMap = buildNCAA68SlotMap(regions);
|
||||||
for (let i = 0; i < slotMap.playInOffsets.length; i++) {
|
for (let i = 0; i < slotMap.playInOffsets.length; i++) {
|
||||||
|
|
@ -495,22 +503,24 @@ export class NCAAWSimulator implements Simulator {
|
||||||
// ── First Four ────────────────────────────────────────────────────────
|
// ── First Four ────────────────────────────────────────────────────────
|
||||||
const ffSimWinners = new Map<number, string>();
|
const ffSimWinners = new Map<number, string>();
|
||||||
for (const [ffMatchNum, r64MatchNum] of ffToR64) {
|
for (const [ffMatchNum, r64MatchNum] of ffToR64) {
|
||||||
const m = firstFourByNum.get(ffMatchNum)!;
|
const m = firstFourByNum.get(ffMatchNum);
|
||||||
|
if (!m) continue;
|
||||||
const winner = m.isComplete && m.winnerId
|
const winner = m.isComplete && m.winnerId
|
||||||
? m.winnerId
|
? m.winnerId
|
||||||
: simMatch(m.participant1Id!, m.participant2Id!).winner;
|
: simMatch(m.participant1Id ?? "", m.participant2Id ?? "").winner;
|
||||||
ffSimWinners.set(r64MatchNum, winner);
|
ffSimWinners.set(r64MatchNum, winner);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Round of 64 ───────────────────────────────────────────────────────
|
// ── Round of 64 ───────────────────────────────────────────────────────
|
||||||
const r64Winners: string[] = [];
|
const r64Winners: string[] = [];
|
||||||
for (let i = 1; i <= 32; i++) {
|
for (let i = 1; i <= 32; i++) {
|
||||||
const m = r64ByNum.get(i)!;
|
const m = r64ByNum.get(i);
|
||||||
|
if (!m) continue;
|
||||||
const p2 = m.participant2Id ?? ffSimWinners.get(i) ?? null;
|
const p2 = m.participant2Id ?? ffSimWinners.get(i) ?? null;
|
||||||
if (m.isComplete && m.winnerId) {
|
if (m.isComplete && m.winnerId) {
|
||||||
r64Winners.push(m.winnerId);
|
r64Winners.push(m.winnerId);
|
||||||
} else {
|
} else {
|
||||||
r64Winners.push(simMatch(m.participant1Id!, p2!).winner);
|
r64Winners.push(simMatch(m.participant1Id ?? "", p2 ?? "").winner);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -551,8 +561,8 @@ export class NCAAWSimulator implements Simulator {
|
||||||
// Derive loser from stored participants if loserId is missing
|
// Derive loser from stored participants if loserId is missing
|
||||||
loser = dbMatch.loserId ??
|
loser = dbMatch.loserId ??
|
||||||
(dbMatch.participant1Id === dbMatch.winnerId
|
(dbMatch.participant1Id === dbMatch.winnerId
|
||||||
? dbMatch.participant2Id!
|
? dbMatch.participant2Id ?? ""
|
||||||
: dbMatch.participant1Id!);
|
: dbMatch.participant1Id ?? "");
|
||||||
} else {
|
} else {
|
||||||
const p1 = s16Winners[(i - 1) * 2];
|
const p1 = s16Winners[(i - 1) * 2];
|
||||||
const p2 = s16Winners[(i - 1) * 2 + 1];
|
const p2 = s16Winners[(i - 1) * 2 + 1];
|
||||||
|
|
@ -572,8 +582,8 @@ export class NCAAWSimulator implements Simulator {
|
||||||
winner = dbMatch.winnerId;
|
winner = dbMatch.winnerId;
|
||||||
loser = dbMatch.loserId ??
|
loser = dbMatch.loserId ??
|
||||||
(dbMatch.participant1Id === dbMatch.winnerId
|
(dbMatch.participant1Id === dbMatch.winnerId
|
||||||
? dbMatch.participant2Id!
|
? dbMatch.participant2Id ?? ""
|
||||||
: dbMatch.participant1Id!);
|
: dbMatch.participant1Id ?? "");
|
||||||
} else {
|
} else {
|
||||||
const p1 = e8Winners[(i - 1) * 2];
|
const p1 = e8Winners[(i - 1) * 2];
|
||||||
const p2 = e8Winners[(i - 1) * 2 + 1];
|
const p2 = e8Winners[(i - 1) * 2 + 1];
|
||||||
|
|
@ -590,8 +600,8 @@ export class NCAAWSimulator implements Simulator {
|
||||||
champion = champMatch.winnerId;
|
champion = champMatch.winnerId;
|
||||||
finalist = champMatch.loserId ??
|
finalist = champMatch.loserId ??
|
||||||
(champMatch.participant1Id === champMatch.winnerId
|
(champMatch.participant1Id === champMatch.winnerId
|
||||||
? champMatch.participant2Id!
|
? champMatch.participant2Id ?? ""
|
||||||
: champMatch.participant1Id!);
|
: champMatch.participant1Id ?? "");
|
||||||
} else {
|
} else {
|
||||||
({ winner: champion, loser: finalist } = simMatch(ffWinners[0], ffWinners[1]));
|
({ winner: champion, loser: finalist } = simMatch(ffWinners[0], ffWinners[1]));
|
||||||
}
|
}
|
||||||
|
|
@ -602,10 +612,10 @@ export class NCAAWSimulator implements Simulator {
|
||||||
// 10. Convert integer counts to probability distributions.
|
// 10. Convert integer counts to probability distributions.
|
||||||
const N = NUM_SIMULATIONS;
|
const N = NUM_SIMULATIONS;
|
||||||
const results: SimulationResult[] = participantIds.map((participantId) => {
|
const results: SimulationResult[] = participantIds.map((participantId) => {
|
||||||
const c = championCounts.get(participantId)!;
|
const c = championCounts.get(participantId) ?? 0;
|
||||||
const f = finalistCounts.get(participantId)!;
|
const f = finalistCounts.get(participantId) ?? 0;
|
||||||
const ff = ffLoserCounts.get(participantId)!;
|
const ff = ffLoserCounts.get(participantId) ?? 0;
|
||||||
const e8 = e8LoserCounts.get(participantId)!;
|
const e8 = e8LoserCounts.get(participantId) ?? 0;
|
||||||
return {
|
return {
|
||||||
participantId,
|
participantId,
|
||||||
probabilities: {
|
probabilities: {
|
||||||
|
|
|
||||||
|
|
@ -493,7 +493,7 @@ export class NHLSimulator implements Simulator {
|
||||||
// Conference Final.
|
// Conference Final.
|
||||||
const eastCF = simSeries(eastR2m1.winner, eastR2m2.winner);
|
const eastCF = simSeries(eastR2m1.winner, eastR2m2.winner);
|
||||||
const eastChamp = eastCF.winner;
|
const eastChamp = eastCF.winner;
|
||||||
confFinalLoserCounts.set(eastCF.loser.id, confFinalLoserCounts.get(eastCF.loser.id)! + 1);
|
confFinalLoserCounts.set(eastCF.loser.id, (confFinalLoserCounts.get(eastCF.loser.id) ?? 0) + 1);
|
||||||
|
|
||||||
// ── Western Conference ────────────────────────────────────────────────────
|
// ── Western Conference ────────────────────────────────────────────────────
|
||||||
const westR1Winners = westBracket.map(([a, b]) => simSeries(a, b).winner);
|
const westR1Winners = westBracket.map(([a, b]) => simSeries(a, b).winner);
|
||||||
|
|
@ -501,16 +501,16 @@ export class NHLSimulator implements Simulator {
|
||||||
const westR2m2 = simSeries(westR1Winners[1], westR1Winners[3]);
|
const westR2m2 = simSeries(westR1Winners[1], westR1Winners[3]);
|
||||||
const westCF = simSeries(westR2m1.winner, westR2m2.winner);
|
const westCF = simSeries(westR2m1.winner, westR2m2.winner);
|
||||||
const westChamp = westCF.winner;
|
const westChamp = westCF.winner;
|
||||||
confFinalLoserCounts.set(westCF.loser.id, confFinalLoserCounts.get(westCF.loser.id)! + 1);
|
confFinalLoserCounts.set(westCF.loser.id, (confFinalLoserCounts.get(westCF.loser.id) ?? 0) + 1);
|
||||||
|
|
||||||
// ── Stanley Cup Final ─────────────────────────────────────────────────────
|
// ── Stanley Cup Final ─────────────────────────────────────────────────────
|
||||||
const final = simSeries(eastChamp, westChamp);
|
const final = simSeries(eastChamp, westChamp);
|
||||||
championCounts.set(final.winner.id, championCounts.get(final.winner.id)! + 1);
|
championCounts.set(final.winner.id, (championCounts.get(final.winner.id) ?? 0) + 1);
|
||||||
finalistCounts.set(final.loser.id, finalistCounts.get(final.loser.id)! + 1);
|
finalistCounts.set(final.loser.id, (finalistCounts.get(final.loser.id) ?? 0) + 1);
|
||||||
|
|
||||||
// Round 2 losers (4 per sim). Round 1 losers are not counted (0 points).
|
// Round 2 losers (4 per sim). Round 1 losers are not counted (0 points).
|
||||||
for (const loser of [eastR2m1.loser, eastR2m2.loser, westR2m1.loser, westR2m2.loser]) {
|
for (const loser of [eastR2m1.loser, eastR2m2.loser, westR2m1.loser, westR2m2.loser]) {
|
||||||
r2LoserCounts.set(loser.id, r2LoserCounts.get(loser.id)! + 1);
|
r2LoserCounts.set(loser.id, (r2LoserCounts.get(loser.id) ?? 0) + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -529,10 +529,10 @@ export class NHLSimulator implements Simulator {
|
||||||
//
|
//
|
||||||
const N = effectiveN;
|
const N = effectiveN;
|
||||||
const results: SimulationResult[] = participantIds.map((participantId) => {
|
const results: SimulationResult[] = participantIds.map((participantId) => {
|
||||||
const c = championCounts.get(participantId)!;
|
const c = championCounts.get(participantId) ?? 0;
|
||||||
const f = finalistCounts.get(participantId)!;
|
const f = finalistCounts.get(participantId) ?? 0;
|
||||||
const cf = confFinalLoserCounts.get(participantId)!;
|
const cf = confFinalLoserCounts.get(participantId) ?? 0;
|
||||||
const r2 = r2LoserCounts.get(participantId)!;
|
const r2 = r2LoserCounts.get(participantId) ?? 0;
|
||||||
return {
|
return {
|
||||||
participantId,
|
participantId,
|
||||||
probabilities: {
|
probabilities: {
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ export class UCLSimulator implements Simulator {
|
||||||
const byRound = new Map<string, typeof allMatches>();
|
const byRound = new Map<string, typeof allMatches>();
|
||||||
for (const m of allMatches) {
|
for (const m of allMatches) {
|
||||||
if (!byRound.has(m.round)) byRound.set(m.round, []);
|
if (!byRound.has(m.round)) byRound.set(m.round, []);
|
||||||
byRound.get(m.round)!.push(m);
|
byRound.get(m.round)?.push(m);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortedRoundMatches = [...byRound.values()]
|
const sortedRoundMatches = [...byRound.values()]
|
||||||
|
|
@ -139,7 +139,7 @@ export class UCLSimulator implements Simulator {
|
||||||
// r16Matches is sorted by matchNumber, so participantIds[0..1] = match 1, [2..3] = match 2, …
|
// r16Matches is sorted by matchNumber, so participantIds[0..1] = match 1, [2..3] = match 2, …
|
||||||
const participantIds: string[] = [];
|
const participantIds: string[] = [];
|
||||||
for (const m of r16Matches) {
|
for (const m of r16Matches) {
|
||||||
participantIds.push(m.participant1Id!, m.participant2Id!);
|
participantIds.push(m.participant1Id ?? "", m.participant2Id ?? "");
|
||||||
}
|
}
|
||||||
const participantSet = new Set(participantIds);
|
const participantSet = new Set(participantIds);
|
||||||
const fallbackProb = 1 / participantIds.length;
|
const fallbackProb = 1 / participantIds.length;
|
||||||
|
|
@ -164,7 +164,7 @@ export class UCLSimulator implements Simulator {
|
||||||
if (hasOdds) {
|
if (hasOdds) {
|
||||||
const oddsInput = evRows
|
const oddsInput = evRows
|
||||||
.filter((r) => r.sourceOdds !== null && participantSet.has(r.participantId))
|
.filter((r) => r.sourceOdds !== null && participantSet.has(r.participantId))
|
||||||
.map((r) => ({ participantId: r.participantId, odds: r.sourceOdds! }));
|
.map((r) => ({ participantId: r.participantId, odds: r.sourceOdds ?? 0 }));
|
||||||
eloMap = convertFuturesToElo(oddsInput, "american");
|
eloMap = convertFuturesToElo(oddsInput, "american");
|
||||||
} else {
|
} else {
|
||||||
// No odds stored — all teams get equal Elo (coin-flip bracket)
|
// No odds stored — all teams get equal Elo (coin-flip bracket)
|
||||||
|
|
@ -227,11 +227,12 @@ export class UCLSimulator implements Simulator {
|
||||||
// R16 losers: no count added (0 points per scoring rules)
|
// R16 losers: no count added (0 points per scoring rules)
|
||||||
const r16Winners: string[] = [];
|
const r16Winners: string[] = [];
|
||||||
for (let i = 1; i <= 8; i++) {
|
for (let i = 1; i <= 8; i++) {
|
||||||
const m = r16ByNum.get(i)!;
|
const m = r16ByNum.get(i);
|
||||||
|
if (!m) continue;
|
||||||
if (m.isComplete && m.winnerId) {
|
if (m.isComplete && m.winnerId) {
|
||||||
r16Winners.push(m.winnerId);
|
r16Winners.push(m.winnerId);
|
||||||
} else {
|
} else {
|
||||||
const { winner } = simMatch(m.participant1Id!, m.participant2Id!);
|
const { winner } = simMatch(m.participant1Id ?? "", m.participant2Id ?? "");
|
||||||
r16Winners.push(winner);
|
r16Winners.push(winner);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -255,9 +256,7 @@ export class UCLSimulator implements Simulator {
|
||||||
}
|
}
|
||||||
|
|
||||||
qfWinners.push(winner);
|
qfWinners.push(winner);
|
||||||
if (qfLoserCounts.has(loser)) {
|
qfLoserCounts.set(loser, (qfLoserCounts.get(loser) ?? 0) + 1);
|
||||||
qfLoserCounts.set(loser, qfLoserCounts.get(loser)! + 1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Semifinals ───────────────────────────────────────────────────────
|
// ── Semifinals ───────────────────────────────────────────────────────
|
||||||
|
|
@ -278,9 +277,7 @@ export class UCLSimulator implements Simulator {
|
||||||
}
|
}
|
||||||
|
|
||||||
sfWinners.push(winner);
|
sfWinners.push(winner);
|
||||||
if (sfLoserCounts.has(loser)) {
|
sfLoserCounts.set(loser, (sfLoserCounts.get(loser) ?? 0) + 1);
|
||||||
sfLoserCounts.set(loser, sfLoserCounts.get(loser)! + 1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Final ─────────────────────────────────────────────────────────────
|
// ── Final ─────────────────────────────────────────────────────────────
|
||||||
|
|
@ -294,12 +291,8 @@ export class UCLSimulator implements Simulator {
|
||||||
({ winner: champion, loser: finalist } = simMatch(sfWinners[0], sfWinners[1]));
|
({ winner: champion, loser: finalist } = simMatch(sfWinners[0], sfWinners[1]));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (championCounts.has(champion)) {
|
championCounts.set(champion, (championCounts.get(champion) ?? 0) + 1);
|
||||||
championCounts.set(champion, championCounts.get(champion)! + 1);
|
finalistCounts.set(finalist, (finalistCounts.get(finalist) ?? 0) + 1);
|
||||||
}
|
|
||||||
if (finalistCounts.has(finalist)) {
|
|
||||||
finalistCounts.set(finalist, finalistCounts.get(finalist)! + 1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 11. Convert counts to probability distributions.
|
// 11. Convert counts to probability distributions.
|
||||||
|
|
@ -309,10 +302,10 @@ export class UCLSimulator implements Simulator {
|
||||||
// probFifth–Eighth → qfLoserCounts / (4*N) — 4 QF losers per sim
|
// probFifth–Eighth → qfLoserCounts / (4*N) — 4 QF losers per sim
|
||||||
const N = NUM_SIMULATIONS;
|
const N = NUM_SIMULATIONS;
|
||||||
const results: SimulationResult[] = participantIds.map((participantId) => {
|
const results: SimulationResult[] = participantIds.map((participantId) => {
|
||||||
const c = championCounts.get(participantId)!;
|
const c = championCounts.get(participantId) ?? 0;
|
||||||
const f = finalistCounts.get(participantId)!;
|
const f = finalistCounts.get(participantId) ?? 0;
|
||||||
const sf = sfLoserCounts.get(participantId)!;
|
const sf = sfLoserCounts.get(participantId) ?? 0;
|
||||||
const qf = qfLoserCounts.get(participantId)!;
|
const qf = qfLoserCounts.get(participantId) ?? 0;
|
||||||
return {
|
return {
|
||||||
participantId,
|
participantId,
|
||||||
probabilities: {
|
probabilities: {
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,8 @@ describe("NbaStandingsAdapter", () => {
|
||||||
|
|
||||||
expect(records).toHaveLength(3);
|
expect(records).toHaveLength(3);
|
||||||
|
|
||||||
const okc = records.find((r) => r.teamName === "Oklahoma City Thunder")!;
|
const okc = records.find((r) => r.teamName === "Oklahoma City Thunder");
|
||||||
|
if (!okc) throw new Error("Oklahoma City Thunder record not found");
|
||||||
expect(okc).toBeDefined();
|
expect(okc).toBeDefined();
|
||||||
expect(okc.wins).toBe(58);
|
expect(okc.wins).toBe(58);
|
||||||
expect(okc.losses).toBe(10);
|
expect(okc.losses).toBe(10);
|
||||||
|
|
@ -113,7 +114,8 @@ describe("NbaStandingsAdapter", () => {
|
||||||
const adapter = new NbaStandingsAdapter();
|
const adapter = new NbaStandingsAdapter();
|
||||||
const records = await adapter.fetchStandings();
|
const records = await adapter.fetchStandings();
|
||||||
|
|
||||||
const bos = records.find((r) => r.teamName === "Boston Celtics")!;
|
const bos = records.find((r) => r.teamName === "Boston Celtics");
|
||||||
|
if (!bos) throw new Error("Boston Celtics record not found");
|
||||||
expect(bos.conference).toBe("Eastern Conference");
|
expect(bos.conference).toBe("Eastern Conference");
|
||||||
expect(bos.division).toBe("Atlantic Division");
|
expect(bos.division).toBe("Atlantic Division");
|
||||||
});
|
});
|
||||||
|
|
@ -127,10 +129,12 @@ describe("NbaStandingsAdapter", () => {
|
||||||
const adapter = new NbaStandingsAdapter();
|
const adapter = new NbaStandingsAdapter();
|
||||||
const records = await adapter.fetchStandings();
|
const records = await adapter.fetchStandings();
|
||||||
|
|
||||||
const bos = records.find((r) => r.teamName === "Boston Celtics")!;
|
const bos = records.find((r) => r.teamName === "Boston Celtics");
|
||||||
|
if (!bos) throw new Error("Boston Celtics record not found");
|
||||||
expect(bos.streak).toBe("W5");
|
expect(bos.streak).toBe("W5");
|
||||||
|
|
||||||
const tor = records.find((r) => r.teamName === "Toronto Raptors")!;
|
const tor = records.find((r) => r.teamName === "Toronto Raptors");
|
||||||
|
if (!tor) throw new Error("Toronto Raptors record not found");
|
||||||
expect(tor.streak).toBe("L2");
|
expect(tor.streak).toBe("L2");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ export async function syncStandings(sportsSeasonId: string): Promise<SyncResult>
|
||||||
const participantByName = new Map(participants.map((p) => [p.name, p]));
|
const participantByName = new Map(participants.map((p) => [p.name, p]));
|
||||||
// Build a map of externalId → participant for ID-first matching
|
// Build a map of externalId → participant for ID-first matching
|
||||||
const participantByExternalId = new Map(
|
const participantByExternalId = new Map(
|
||||||
participants.filter((p) => p.externalId).map((p) => [p.externalId!, p])
|
participants.filter((p) => p.externalId).map((p) => [p.externalId ?? "", p])
|
||||||
);
|
);
|
||||||
|
|
||||||
const toUpsert: Parameters<typeof upsertRegularSeasonStandings>[0] = [];
|
const toUpsert: Parameters<typeof upsertRegularSeasonStandings>[0] = [];
|
||||||
|
|
@ -84,7 +84,7 @@ export async function syncStandings(sportsSeasonId: string): Promise<SyncResult>
|
||||||
// Fall back to name matching
|
// Fall back to name matching
|
||||||
const matchedName = findMatchingTeamName(record.teamName, participantNames);
|
const matchedName = findMatchingTeamName(record.teamName, participantNames);
|
||||||
if (matchedName) {
|
if (matchedName) {
|
||||||
participant = participantByName.get(matchedName)!;
|
participant = participantByName.get(matchedName) ?? null;
|
||||||
// Write-back: persist externalId so future syncs skip name matching for this team
|
// Write-back: persist externalId so future syncs skip name matching for this team
|
||||||
if (!participant.externalId) {
|
if (!participant.externalId) {
|
||||||
await updateParticipant(participant.id, { externalId: record.externalTeamId });
|
await updateParticipant(participant.id, { externalId: record.externalTeamId });
|
||||||
|
|
|
||||||
|
|
@ -163,7 +163,8 @@ export function initializeSocketIO(httpServer: HTTPServer): SocketIOServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current connected teams for this season
|
// Get current connected teams for this season
|
||||||
const seasonConnectedTeams = connectedTeams.get(seasonId)!;
|
const seasonConnectedTeams = connectedTeams.get(seasonId) ?? new Set<string>();
|
||||||
|
if (!connectedTeams.has(seasonId)) connectedTeams.set(seasonId, seasonConnectedTeams);
|
||||||
|
|
||||||
// Send the list of already-connected teams to this socket
|
// Send the list of already-connected teams to this socket
|
||||||
socket.emit("connected-teams-list", { teamIds: Array.from(seasonConnectedTeams) });
|
socket.emit("connected-teams-list", { teamIds: Array.from(seasonConnectedTeams) });
|
||||||
|
|
@ -172,7 +173,7 @@ export function initializeSocketIO(httpServer: HTTPServer): SocketIOServer {
|
||||||
seasonConnectedTeams.add(teamId);
|
seasonConnectedTeams.add(teamId);
|
||||||
|
|
||||||
// Broadcast to everyone else that this team connected
|
// Broadcast to everyone else that this team connected
|
||||||
io!.to(`draft-${seasonId}`).emit("team-connected", { teamId });
|
io?.to(`draft-${seasonId}`).emit("team-connected", { teamId });
|
||||||
console.log(`Team ${teamId} connected to draft-${seasonId}. Total connected: ${seasonConnectedTeams.size}`);
|
console.log(`Team ${teamId} connected to draft-${seasonId}. Total connected: ${seasonConnectedTeams.size}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -269,7 +270,7 @@ export function initializeSocketIO(httpServer: HTTPServer): SocketIOServer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
io!.to(`draft-${seasonId}`).emit("team-disconnected", { teamId: currentTeamId });
|
io?.to(`draft-${seasonId}`).emit("team-disconnected", { teamId: currentTeamId });
|
||||||
console.log(`Team ${currentTeamId} disconnected from draft-${seasonId}`);
|
console.log(`Team ${currentTeamId} disconnected from draft-${seasonId}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -304,7 +305,7 @@ export function initializeSocketIO(httpServer: HTTPServer): SocketIOServer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
io!.to(`draft-${currentSeasonId}`).emit("team-disconnected", { teamId: currentTeamId });
|
io?.to(`draft-${currentSeasonId}`).emit("team-disconnected", { teamId: currentTeamId });
|
||||||
console.log(`Team ${currentTeamId} disconnected from draft-${currentSeasonId}`);
|
console.log(`Team ${currentTeamId} disconnected from draft-${currentSeasonId}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue