diff --git a/app/services/simulations/__tests__/epl-simulator.test.ts b/app/services/simulations/__tests__/epl-simulator.test.ts index 546b9fa..9e89ee6 100644 --- a/app/services/simulations/__tests__/epl-simulator.test.ts +++ b/app/services/simulations/__tests__/epl-simulator.test.ts @@ -75,7 +75,7 @@ describe("EPLSimulator.simulate()", () => { (database as unknown as MockInstance).mockReturnValue(mockDb); (getRegularSeasonStandings as unknown as MockInstance).mockResolvedValue([]); (getSportsSeasonSimulatorConfig as unknown as MockInstance).mockResolvedValue({ - config: { iterations: 10_000, matchParityFactor: 400 }, + config: { iterations: 500, matchParityFactor: 400 }, }); }); diff --git a/app/services/simulations/__tests__/world-cup-simulator.test.ts b/app/services/simulations/__tests__/world-cup-simulator.test.ts index ab18632..bf2e9e4 100644 --- a/app/services/simulations/__tests__/world-cup-simulator.test.ts +++ b/app/services/simulations/__tests__/world-cup-simulator.test.ts @@ -114,7 +114,7 @@ describe("WorldCupSimulator", () => { mockDb.query.tournamentGroups.findMany.mockResolvedValue([]); mockDb.query.playoffMatches.findMany.mockResolvedValue([]); - const sim = new WorldCupSimulator(500); + const sim = new WorldCupSimulator(100); await expect(sim.simulate("season-1")).rejects.toThrow("No participants found"); }); @@ -125,7 +125,7 @@ describe("WorldCupSimulator", () => { mockDb.query.tournamentGroups.findMany.mockResolvedValue([]); mockDb.query.playoffMatches.findMany.mockResolvedValue([]); - const sim = new WorldCupSimulator(500); + const sim = new WorldCupSimulator(100); const results = await sim.simulate("season-1"); expect(results).toHaveLength(48); const ids = new Set(results.map((r) => r.participantId)); @@ -141,7 +141,7 @@ describe("WorldCupSimulator", () => { mockDb.query.tournamentGroups.findMany.mockResolvedValue([]); mockDb.query.playoffMatches.findMany.mockResolvedValue([]); - const sim = new WorldCupSimulator(500); + const sim = new WorldCupSimulator(100); const results = await sim.simulate("season-1"); const sumFirst = results.reduce((s, r) => s + r.probabilities.probFirst, 0); @@ -163,7 +163,7 @@ describe("WorldCupSimulator", () => { mockDb.query.tournamentGroups.findMany.mockResolvedValue([]); mockDb.query.playoffMatches.findMany.mockResolvedValue([]); - const sim = new WorldCupSimulator(500); + const sim = new WorldCupSimulator(100); const results = await sim.simulate("season-1"); // probFirst + probSecond + probThird + probFourth should cover all probability mass @@ -219,7 +219,7 @@ describe("WorldCupSimulator", () => { mockDb.query.tournamentGroups.findMany.mockResolvedValue([group, ...remainingGroups]); mockDb.query.playoffMatches.findMany.mockResolvedValue([]); - const sim = new WorldCupSimulator(500); + const sim = new WorldCupSimulator(100); const results = await sim.simulate("season-1"); const p3Result = results.find((r) => r.participantId === "p3"); @@ -239,7 +239,7 @@ describe("WorldCupSimulator", () => { mockDb.query.tournamentGroups.findMany.mockResolvedValue([]); mockDb.query.playoffMatches.findMany.mockResolvedValue([]); - const sim = new WorldCupSimulator(500); + const sim = new WorldCupSimulator(100); const results = await sim.simulate("season-1"); for (const r of results) { diff --git a/app/services/simulations/epl-simulator.ts b/app/services/simulations/epl-simulator.ts index f2b4f49..b0f08d8 100644 --- a/app/services/simulations/epl-simulator.ts +++ b/app/services/simulations/epl-simulator.ts @@ -127,10 +127,10 @@ export class EPLSimulator implements Simulator { } } - if (dbEloMap.size === 0) { - const oddsInput = evRows - .filter((row) => row.sourceOdds !== null && row.sourceOdds !== undefined) - .map((row) => ({ participantId: row.participantId, odds: row.sourceOdds as number })); + const oddsInput = evRows + .filter((row) => !dbEloMap.has(row.participantId) && row.sourceOdds !== null && row.sourceOdds !== undefined) + .map((row) => ({ participantId: row.participantId, odds: row.sourceOdds as number })); + if (oddsInput.length > 0) { const converted = convertFuturesToElo(oddsInput); for (const [participantId, elo] of converted) dbEloMap.set(participantId, elo); } diff --git a/app/services/simulations/manifest.ts b/app/services/simulations/manifest.ts index 589b6ec..7558cda 100644 --- a/app/services/simulations/manifest.ts +++ b/app/services/simulations/manifest.ts @@ -116,6 +116,7 @@ const PROFILES: Record -): void { - for (const key of positionKeys) { - const colSum = results.reduce((s, r) => s + r.probabilities[key], 0); - const residual = 1.0 - colSum; - if (residual !== 0 && Math.abs(residual) < 1e-9) { - const maxResult = results.reduce((best, r) => - r.probabilities[key] > best.probabilities[key] ? r : best - ); - maxResult.probabilities[key] += residual; - } - } -} - // ─── Main simulator ─────────────────────────────────────────────────────────── export class WorldCupSimulator implements Simulator { @@ -468,6 +453,31 @@ export class WorldCupSimulator implements Simulator { counts.qfLoser.set(id, 0); } + function runKnockoutRound( + teams: string[], + roundName: string + ): { winners: string[]; losers: string[] } { + const winners: string[] = []; + const losers: string[] = []; + for (let i = 0; i < teams.length; i += 2) { + const a = teams[i]; + const b = teams[i + 1]; + if (!a || !b) continue; + + const matchNum = Math.floor(i / 2) + 1; + const fixed = completedByRoundAndNumber.get(`${roundName}:${matchNum}`); + if (fixed) { + winners.push(fixed.winnerId); + losers.push(fixed.loserId); + } else { + const { winner, loser } = simKnockout(a, b, eloFn, normalizedProb); + winners.push(winner); + losers.push(loser); + } + } + return { winners, losers }; + } + for (let sim = 0; sim < this.numSimulations; sim++) { // ── Group stage ────────────────────────────────────────────── const advancingFromGroup: string[] = []; // group winners + runners-up (24 teams) @@ -504,31 +514,6 @@ export class WorldCupSimulator implements Simulator { // Completed knockout matches are honoured by round+matchNumber, so as the real // bracket plays out the simulation locks in actual results automatically. - function runKnockoutRound( - teams: string[], - roundName: string - ): { winners: string[]; losers: string[] } { - const winners: string[] = []; - const losers: string[] = []; - for (let i = 0; i < teams.length; i += 2) { - const a = teams[i]; - const b = teams[i + 1]; - if (!a || !b) continue; - - const matchNum = Math.floor(i / 2) + 1; - const fixed = completedByRoundAndNumber.get(`${roundName}:${matchNum}`); - if (fixed) { - winners.push(fixed.winnerId); - losers.push(fixed.loserId); - } else { - const { winner, loser } = simKnockout(a, b, eloFn, normalizedProb); - winners.push(winner); - losers.push(loser); - } - } - return { winners, losers }; - } - const r32 = runKnockoutRound(r32Pool, "Round of 32"); const r16 = runKnockoutRound(r32.winners, "Round of 16"); const qf = runKnockoutRound(r16.winners, "Quarterfinals"); @@ -595,11 +580,7 @@ export class WorldCupSimulator implements Simulator { }; }); - // Normalise floating-point residuals per position - const positionKeys: Array = [ - "probFirst", "probSecond", "probThird", "probFourth", - ]; - normalizeProbabilities(results, positionKeys); + normalizeSimulationResultColumns(results); return results; }