diff --git a/app/services/simulations/__tests__/llws-simulator.test.ts b/app/services/simulations/__tests__/llws-simulator.test.ts index bfb84ba..b22fb25 100644 --- a/app/services/simulations/__tests__/llws-simulator.test.ts +++ b/app/services/simulations/__tests__/llws-simulator.test.ts @@ -153,9 +153,9 @@ describe("LLWSSimulator", () => { setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS, { includeOdds: true })); const results = await new LLWSSimulator().simulate("season-1"); const byId = new Map(results.map((r) => [r.participantId, r])); - expect(byId.get("us-1")!.probabilities.probFirst).toBeGreaterThan( - byId.get("us-10")!.probabilities.probFirst - ); + const us1prob = byId.get("us-1")?.probabilities.probFirst ?? 0; + const us10prob = byId.get("us-10")?.probabilities.probFirst ?? 0; + expect(us1prob).toBeGreaterThan(us10prob); }); it("works when no odds are entered (all 50/50 fallback)", async () => { diff --git a/app/services/simulations/llws-simulator.ts b/app/services/simulations/llws-simulator.ts index 1974873..212f811 100644 --- a/app/services/simulations/llws-simulator.ts +++ b/app/services/simulations/llws-simulator.ts @@ -147,7 +147,7 @@ function simulatePoolPlay(pool: Team[]): [Team, Team] { // the comparator is consistent (Math.random() inside a comparator is a bug — the // engine may call it multiple times per pair and get contradictory results). const tiebreaker = new Map(pool.map((t) => [t.participantId, Math.random()])); - const ranked = [...pool].sort((a, b) => { + const ranked = pool.toSorted((a, b) => { const diff = (wins.get(b.participantId) ?? 0) - (wins.get(a.participantId) ?? 0); return diff !== 0 ? diff : (tiebreaker.get(a.participantId) ?? 0) - (tiebreaker.get(b.participantId) ?? 0); }); @@ -178,12 +178,8 @@ function simulateSideBracket( poolA2: Team, poolB1: Team, poolB2: Team, - counts: Map + bump: (id: string, key: keyof PlacementCounts) => void ): { sideChampion: Team; sideLoser: Team } { - const bump = (id: string, key: keyof PlacementCounts) => { - const entry = counts.get(id); - if (entry) entry[key]++; - }; // Winners bracket const g1 = simGame(poolA1, poolB2); @@ -331,6 +327,10 @@ export class LLWSSimulator implements Simulator { const counts = new Map( allIds.map((id) => [id, { champion: 0, finalist: 0, thirdPlace: 0, fourthPlace: 0, bracketLoser: 0 }]) ); + const bump = (id: string, key: keyof PlacementCounts) => { + const entry = counts.get(id); + if (entry) entry[key]++; + }; // 6. Run Monte Carlo simulations. for (let s = 0; s < NUM_SIMULATIONS; s++) { @@ -346,19 +346,19 @@ export class LLWSSimulator implements Simulator { // Double-elimination bracket per side. const { sideChampion: usChamp, sideLoser: usLose } = - simulateSideBracket(usA1, usA2, usB1, usB2, counts); + simulateSideBracket(usA1, usA2, usB1, usB2, bump); const { sideChampion: intlChamp, sideLoser: intlLose } = - simulateSideBracket(intlA1, intlA2, intlB1, intlB2, counts); + simulateSideBracket(intlA1, intlA2, intlB1, intlB2, bump); // Consolation game: 3rd / 4th place. const consolation = simGame(usLose, intlLose); - counts.get(consolation.winner.participantId)!.thirdPlace++; - counts.get(consolation.loser.participantId)!.fourthPlace++; + bump(consolation.winner.participantId, "thirdPlace"); + bump(consolation.loser.participantId, "fourthPlace"); // World Series: 1st / 2nd place. const ws = simGame(usChamp, intlChamp); - counts.get(ws.winner.participantId)!.champion++; - counts.get(ws.loser.participantId)!.finalist++; + bump(ws.winner.participantId, "champion"); + bump(ws.loser.participantId, "finalist"); } // 7. Convert counts to probability distributions. @@ -366,8 +366,9 @@ export class LLWSSimulator implements Simulator { const bracketLosersPerSim = 4; const bracketDivisor = bracketLosersPerSim * NUM_SIMULATIONS; + const zeroCounts: PlacementCounts = { champion: 0, finalist: 0, thirdPlace: 0, fourthPlace: 0, bracketLoser: 0 }; return allIds.map((id) => { - const c = counts.get(id)!; + const c = counts.get(id) ?? zeroCounts; const bracketProb = c.bracketLoser / bracketDivisor; return { participantId: id,