From e507f82508372ce23999462251e8b58be399bd38 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 01:02:46 +0000 Subject: [PATCH 1/3] Blend standings-based strength into F1/IndyCar EV simulation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Championship futures odds rate mid-season drivers by likelihood to win the title, which understates a driver like Hamilton (2nd in standings) because the odds reflect the difficulty of closing a gap — not where he'll likely finish. The simulator was using those odds as-is for all remaining race win probabilities, making the current standings irrelevant to EV. Fix: blend each driver's odds-derived strength with their share of total current championship points, weighted by season progress (completedRaces / totalRaces). Pre-season remains pure futures-odds; mid-season is a 50/50 blend; late-season standings dominate. Additionally scale PARTICIPANT_VOLATILITY down by up to 70% as the season progresses to prevent unrealistic swings when few races remain. Applies equally to F1 and IndyCar via AutoRacingSimulator. https://claude.ai/code/session_01FyG28zxsjSrgr8aKYuy6xY --- .../simulations/auto-racing-simulator.ts | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/app/services/simulations/auto-racing-simulator.ts b/app/services/simulations/auto-racing-simulator.ts index 64072ee..8120702 100644 --- a/app/services/simulations/auto-racing-simulator.ts +++ b/app/services/simulations/auto-racing-simulator.ts @@ -117,13 +117,19 @@ export class AutoRacingSimulator implements Simulator { seasonResults.map((r) => [r.participant.id, parseFloat(r.currentPoints ?? "0")]) ); - // 3. Count remaining races: incomplete scoring events, excluding schedule_event entries + // 3. Count remaining and completed races (exclude schedule_event entries) const allEvents = await db.query.scoringEvents.findMany({ where: eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId), }); const remainingRaces = allEvents.filter( (e) => !e.isComplete && e.eventType !== "schedule_event" ).length; + const completedRaces = allEvents.filter( + (e) => e.isComplete && e.eventType !== "schedule_event" + ).length; + // 0.0 = pre-season, 1.0 = all races done + const totalRaces = completedRaces + remainingRaces; + const seasonProgress = totalRaces > 0 ? completedRaces / totalRaces : 0; // 4. Load EV data for championship win probabilities const evs = await getAllParticipantEVsForSeason(sportsSeasonId); @@ -160,7 +166,28 @@ export class AutoRacingSimulator implements Simulator { } } - // 7. Accumulate finish counts across simulations + // 7. Build blended probability weights that combine futures-odds strength + // with standings-based strength, weighted by season progress. + // - Pre-season (seasonProgress=0): pure futures odds + // - Mid/late season: standings dominate, reducing the distortion from + // championship futures (which penalize 2nd-place drivers whose odds of + // *winning* the title are weak, even though they'll likely finish top 3) + const totalCurrentPoints = [...currentPointsMap.values()].reduce((a, b) => a + b, 0); + const blendedProbs = new Map(); + for (const id of ids) { + const oddsW = baseProbs.get(id) ?? fallbackProb; + let standingsW: number; + if (totalCurrentPoints > 0) { + const pts = currentPointsMap.get(id) ?? 0; + // Drivers with 0 pts (new entry, early DNF) fall back to odds strength + standingsW = pts > 0 ? pts / totalCurrentPoints : oddsW; + } else { + standingsW = oddsW; + } + blendedProbs.set(id, (1 - seasonProgress) * oddsW + seasonProgress * standingsW); + } + + // Accumulate finish counts across simulations // rankCounts[id][0..7] = number of times driver finished 1st..8th const rankCounts = new Map(); for (const id of ids) { @@ -180,14 +207,17 @@ export class AutoRacingSimulator implements Simulator { } } else { // In-season: simulate remaining races from current standings. + // Volatility shrinks as the season progresses — late-season standings are + // much more predictive than early-season odds. + const effectiveVolatility = PARTICIPANT_VOLATILITY * (1 - seasonProgress * 0.7); for (let sim = 0; sim < NUM_SIMULATIONS; sim++) { - // 7a. Season-long performance multiplier per driver + // 7a. Season-long performance multiplier per driver (uses blended strength) const seasonWeights = new Map(); for (const id of ids) { - const base = baseProbs.get(id) ?? fallbackProb; + const base = blendedProbs.get(id) ?? fallbackProb; const mult = Math.max( 0.05, - 1 - PARTICIPANT_VOLATILITY + Math.random() * PARTICIPANT_VOLATILITY * 2 + 1 - effectiveVolatility + Math.random() * effectiveVolatility * 2 ); seasonWeights.set(id, base * mult); } -- 2.45.3 From 06a011f0ed534d423e12672c31f8ccc51883a366 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 01:31:43 +0000 Subject: [PATCH 2/3] Address code review findings in AutoRacingSimulator - Extract VOLATILITY_DECAY_FACTOR named constant (was magic 0.7) with docstring - Replace two complementary allEvents.filter() calls with a single for-loop pass - Move blendedProbs construction inside the in-season else branch (was built unconditionally even in pre-season where it was never read) - Flatten nested totalCurrentPoints/pts conditions to a single ternary - Emit a console.warn when participants are missing from standings data, so degraded blending is visible rather than silent - Add AutoRacingSimulator test suite covering pre-season path, in-season standings-dominant blending, zero-points fallback, missing-standings warning, schedule_event exclusion, and column normalization https://claude.ai/code/session_01FyG28zxsjSrgr8aKYuy6xY --- .../__tests__/auto-racing-simulator.test.ts | 260 ++++++++++++++++++ .../simulations/auto-racing-simulator.ts | 72 +++-- 2 files changed, 303 insertions(+), 29 deletions(-) create mode 100644 app/services/simulations/__tests__/auto-racing-simulator.test.ts diff --git a/app/services/simulations/__tests__/auto-racing-simulator.test.ts b/app/services/simulations/__tests__/auto-racing-simulator.test.ts new file mode 100644 index 0000000..c42fba6 --- /dev/null +++ b/app/services/simulations/__tests__/auto-racing-simulator.test.ts @@ -0,0 +1,260 @@ +import { describe, it, expect, vi, beforeEach, type MockInstance } from "vitest"; +import { AutoRacingSimulator } from "../auto-racing-simulator"; + +vi.mock("~/database/context", () => ({ + database: vi.fn(), +})); + +vi.mock("~/models/participant-season-result", () => ({ + getSeasonResults: vi.fn(), +})); + +vi.mock("~/models/participant-expected-value", () => ({ + getAllParticipantEVsForSeason: vi.fn(), +})); + +// ─── F1 race points (positions 1–10) ───────────────────────────────────────── +const F1_RACE_POINTS: Record = { + 1: 25, 2: 18, 3: 15, 4: 12, 5: 10, 6: 8, 7: 6, 8: 4, 9: 2, 10: 1, +}; + +// ─── Fixtures ───────────────────────────────────────────────────────────────── + +const DRIVERS = ["d1", "d2", "d3", "d4", "d5"].map((id) => ({ id })); + +function makeEvent(isComplete: boolean, eventType = "race") { + return { isComplete, eventType }; +} + +function makeSeasonResult(participantId: string, currentPoints: string) { + return { participant: { id: participantId }, currentPoints }; +} + +function makeEv(participantId: string, sourceOdds: number | null) { + return { participantId, sourceOdds }; +} + +function mockDb(events: ReturnType[]) { + return { + query: { + seasonParticipants: { + findMany: vi.fn().mockResolvedValue(DRIVERS), + }, + scoringEvents: { + findMany: vi.fn().mockResolvedValue(events), + }, + }, + }; +} + +// ─── Setup ──────────────────────────────────────────────────────────────────── + +let db: ReturnType; + +beforeEach(async () => { + const { database } = await import("~/database/context"); + const { getSeasonResults } = await import("~/models/participant-season-result"); + const { getAllParticipantEVsForSeason } = await import("~/models/participant-expected-value"); + + db = mockDb([]); + (database as unknown as MockInstance).mockReturnValue(db); + (getSeasonResults as unknown as MockInstance).mockResolvedValue([]); + (getAllParticipantEVsForSeason as unknown as MockInstance).mockResolvedValue([]); +}); + +// ─── Tests ──────────────────────────────────────────────────────────────────── + +describe("AutoRacingSimulator", () => { + it("throws when no participants are found", async () => { + db.query.seasonParticipants.findMany.mockResolvedValue([]); + await expect( + new AutoRacingSimulator(F1_RACE_POINTS, "f1").simulate("s1") + ).rejects.toThrow(/No participants found/); + }); + + describe("pre-season path (remainingRaces === 0)", () => { + beforeEach(async () => { + const { getAllParticipantEVsForSeason } = await import("~/models/participant-expected-value"); + // No scoring events → remainingRaces = 0 + db = mockDb([]); + const { database } = await import("~/database/context"); + (database as unknown as MockInstance).mockReturnValue(db); + // Heavy favourite: d1 at −500, all others at +1000 + (getAllParticipantEVsForSeason as unknown as MockInstance).mockResolvedValue([ + makeEv("d1", -500), + makeEv("d2", 1000), + makeEv("d3", 1000), + makeEv("d4", 1000), + makeEv("d5", 1000), + ]); + }); + + it("returns one result per driver", async () => { + const results = await new AutoRacingSimulator(F1_RACE_POINTS, "f1").simulate("s1"); + expect(results).toHaveLength(5); + }); + + it("normalizes each position column to sum to 1.0", async () => { + const results = await new AutoRacingSimulator(F1_RACE_POINTS, "f1").simulate("s1"); + const keys = [ + "probFirst", "probSecond", "probThird", "probFourth", + "probFifth", "probSixth", "probSeventh", "probEighth", + ] as const; + for (const key of keys) { + const sum = results.reduce((s, r) => s + r.probabilities[key], 0); + expect(sum, `${key} column sum`).toBeCloseTo(1.0, 6); + } + }); + + it("heavy favourite ranks first more often than long shots", async () => { + const results = await new AutoRacingSimulator(F1_RACE_POINTS, "f1").simulate("s1"); + const favourite = results.find((r) => r.participantId === "d1")!; + const longShot = results.find((r) => r.participantId === "d2")!; + expect(favourite.probabilities.probFirst).toBeGreaterThan(longShot.probabilities.probFirst); + }); + + it("drivers without odds get equal fallback probability", async () => { + const { getAllParticipantEVsForSeason } = await import("~/models/participant-expected-value"); + (getAllParticipantEVsForSeason as unknown as MockInstance).mockResolvedValue([]); + const results = await new AutoRacingSimulator(F1_RACE_POINTS, "f1").simulate("s1"); + // With equal weights all 5 drivers should finish 1st roughly equally + for (const r of results) { + expect(r.probabilities.probFirst).toBeGreaterThan(0.1); + expect(r.probabilities.probFirst).toBeLessThan(0.3); + } + }); + }); + + describe("in-season path (remainingRaces > 0)", () => { + beforeEach(async () => { + const { database } = await import("~/database/context"); + // 10 completed races, 5 remaining + db = mockDb([ + ...Array.from({ length: 10 }, () => makeEvent(true)), + ...Array.from({ length: 5 }, () => makeEvent(false)), + ]); + (database as unknown as MockInstance).mockReturnValue(db); + }); + + it("normalizes each position column to sum to 1.0", async () => { + const { getSeasonResults } = await import("~/models/participant-season-result"); + (getSeasonResults as unknown as MockInstance).mockResolvedValue( + DRIVERS.map((d, i) => makeSeasonResult(d.id, String((5 - i) * 50))) + ); + const results = await new AutoRacingSimulator(F1_RACE_POINTS, "f1").simulate("s1"); + const keys = [ + "probFirst", "probSecond", "probThird", "probFourth", + "probFifth", "probSixth", "probSeventh", "probEighth", + ] as const; + for (const key of keys) { + const sum = results.reduce((s, r) => s + r.probabilities[key], 0); + expect(sum, `${key} column sum`).toBeCloseTo(1.0, 6); + } + }); + + it("standings leader ranks higher than a driver far behind when standings dominate", async () => { + // 20/25 races done → seasonProgress = 0.8 → standings weighted 80% + const { database } = await import("~/database/context"); + db = mockDb([ + ...Array.from({ length: 20 }, () => makeEvent(true)), + ...Array.from({ length: 5 }, () => makeEvent(false)), + ]); + (database as unknown as MockInstance).mockReturnValue(db); + + const { getSeasonResults } = await import("~/models/participant-season-result"); + const { getAllParticipantEVsForSeason } = await import("~/models/participant-expected-value"); + // d1 leads with 400 pts; d2 is a distant 2nd with 50 pts + (getSeasonResults as unknown as MockInstance).mockResolvedValue([ + makeSeasonResult("d1", "400"), + makeSeasonResult("d2", "50"), + makeSeasonResult("d3", "40"), + makeSeasonResult("d4", "30"), + makeSeasonResult("d5", "20"), + ]); + // Futures odds heavily favour d2 (pretend markets disagree) + (getAllParticipantEVsForSeason as unknown as MockInstance).mockResolvedValue([ + makeEv("d1", 5000), // very long shot per futures + makeEv("d2", -500), // heavy favourite per futures + ]); + + const results = await new AutoRacingSimulator(F1_RACE_POINTS, "f1").simulate("s1"); + const leader = results.find((r) => r.participantId === "d1")!; + const distant = results.find((r) => r.participantId === "d2")!; + // Standings signal should dominate: d1's massive points lead wins out + expect(leader.probabilities.probFirst).toBeGreaterThan(distant.probabilities.probFirst); + }); + + it("falls back to odds for all drivers when no standings data exists", async () => { + // totalCurrentPoints = 0 → standings signal disabled, odds take over + const { getAllParticipantEVsForSeason } = await import("~/models/participant-expected-value"); + (getAllParticipantEVsForSeason as unknown as MockInstance).mockResolvedValue([ + makeEv("d1", -500), + makeEv("d2", 1000), + ]); + const results = await new AutoRacingSimulator(F1_RACE_POINTS, "f1").simulate("s1"); + const fav = results.find((r) => r.participantId === "d1")!; + const longShot = results.find((r) => r.participantId === "d2")!; + // Without standings, odds-favoured driver should still rank higher + expect(fav.probabilities.probFirst).toBeGreaterThan(longShot.probabilities.probFirst); + }); + + it("a driver with 0 points mid-season is not penalized beyond their odds weight", async () => { + // Use 2 completed / 20 remaining → early season, standings gap is small + const { database } = await import("~/database/context"); + db = mockDb([ + ...Array.from({ length: 2 }, () => makeEvent(true)), + ...Array.from({ length: 20 }, () => makeEvent(false)), + ]); + (database as unknown as MockInstance).mockReturnValue(db); + + const { getSeasonResults } = await import("~/models/participant-season-result"); + // d1-d4 have a modest lead; d5 is absent (0 pts, new entry) + (getSeasonResults as unknown as MockInstance).mockResolvedValue([ + makeSeasonResult("d1", "10"), + makeSeasonResult("d2", "8"), + makeSeasonResult("d3", "6"), + makeSeasonResult("d4", "4"), + // d5 intentionally absent → falls back to odds weight + ]); + const { getAllParticipantEVsForSeason } = await import("~/models/participant-expected-value"); + (getAllParticipantEVsForSeason as unknown as MockInstance).mockResolvedValue([ + makeEv("d5", -500), // strong odds favourite despite 0 pts + ]); + + const results = await new AutoRacingSimulator(F1_RACE_POINTS, "f1").simulate("s1"); + // d5 should win championships at a non-trivial rate given their strong odds weight + const d5 = results.find((r) => r.participantId === "d5")!; + expect(d5.probabilities.probFirst).toBeGreaterThan(0.1); + }); + + it("emits a warning when participants are missing from standings", async () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const { getSeasonResults } = await import("~/models/participant-season-result"); + // Only 3 of 5 drivers have standings rows + (getSeasonResults as unknown as MockInstance).mockResolvedValue([ + makeSeasonResult("d1", "100"), + makeSeasonResult("d2", "80"), + makeSeasonResult("d3", "60"), + ]); + await new AutoRacingSimulator(F1_RACE_POINTS, "f1").simulate("s1"); + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining("2 participant(s) missing from standings") + ); + warnSpy.mockRestore(); + }); + + it("schedule_event entries are excluded from race counts", async () => { + const { database } = await import("~/database/context"); + // 5 real races + 3 schedule_events (should be ignored) + db = mockDb([ + ...Array.from({ length: 5 }, () => makeEvent(true)), + ...Array.from({ length: 3 }, () => makeEvent(false, "schedule_event")), + makeEvent(false), // 1 real remaining + ]); + (database as unknown as MockInstance).mockReturnValue(db); + // Should not throw and should use seasonProgress = 5/6 + const results = await new AutoRacingSimulator(F1_RACE_POINTS, "f1").simulate("s1"); + expect(results).toHaveLength(5); + }); + }); +}); diff --git a/app/services/simulations/auto-racing-simulator.ts b/app/services/simulations/auto-racing-simulator.ts index 8120702..151ea14 100644 --- a/app/services/simulations/auto-racing-simulator.ts +++ b/app/services/simulations/auto-racing-simulator.ts @@ -41,6 +41,14 @@ const RACE_NOISE = 0.50; */ const PARTICIPANT_VOLATILITY = 1.5; +/** + * How much PARTICIPANT_VOLATILITY shrinks as the season progresses. + * effectiveVolatility = PARTICIPANT_VOLATILITY * (1 - progress * VOLATILITY_DECAY_FACTOR). + * At 0.7, effective volatility reaches 30% of its baseline with one race left, + * preventing large standing swings when the championship is nearly decided. + */ +const VOLATILITY_DECAY_FACTOR = 0.7; + /** * Optional smoothing toward the mean after vig removal. * 0.0 = use vig-removed market odds exactly (recommended). @@ -117,16 +125,17 @@ export class AutoRacingSimulator implements Simulator { seasonResults.map((r) => [r.participant.id, parseFloat(r.currentPoints ?? "0")]) ); - // 3. Count remaining and completed races (exclude schedule_event entries) + // 3. Count remaining and completed races in a single pass (exclude schedule_event entries) const allEvents = await db.query.scoringEvents.findMany({ where: eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId), }); - const remainingRaces = allEvents.filter( - (e) => !e.isComplete && e.eventType !== "schedule_event" - ).length; - const completedRaces = allEvents.filter( - (e) => e.isComplete && e.eventType !== "schedule_event" - ).length; + let remainingRaces = 0; + let completedRaces = 0; + for (const e of allEvents) { + if (e.eventType === "schedule_event") continue; + if (e.isComplete) completedRaces++; + else remainingRaces++; + } // 0.0 = pre-season, 1.0 = all races done const totalRaces = completedRaces + remainingRaces; const seasonProgress = totalRaces > 0 ? completedRaces / totalRaces : 0; @@ -166,27 +175,6 @@ export class AutoRacingSimulator implements Simulator { } } - // 7. Build blended probability weights that combine futures-odds strength - // with standings-based strength, weighted by season progress. - // - Pre-season (seasonProgress=0): pure futures odds - // - Mid/late season: standings dominate, reducing the distortion from - // championship futures (which penalize 2nd-place drivers whose odds of - // *winning* the title are weak, even though they'll likely finish top 3) - const totalCurrentPoints = [...currentPointsMap.values()].reduce((a, b) => a + b, 0); - const blendedProbs = new Map(); - for (const id of ids) { - const oddsW = baseProbs.get(id) ?? fallbackProb; - let standingsW: number; - if (totalCurrentPoints > 0) { - const pts = currentPointsMap.get(id) ?? 0; - // Drivers with 0 pts (new entry, early DNF) fall back to odds strength - standingsW = pts > 0 ? pts / totalCurrentPoints : oddsW; - } else { - standingsW = oddsW; - } - blendedProbs.set(id, (1 - seasonProgress) * oddsW + seasonProgress * standingsW); - } - // Accumulate finish counts across simulations // rankCounts[id][0..7] = number of times driver finished 1st..8th const rankCounts = new Map(); @@ -207,9 +195,35 @@ export class AutoRacingSimulator implements Simulator { } } else { // In-season: simulate remaining races from current standings. + + // Warn if standings data is incomplete — missing rows distort each driver's + // share-of-points weight and silently degrade the blending accuracy. + const participantsWithPoints = ids.filter((id) => currentPointsMap.has(id)); + if (participantsWithPoints.length < ids.length) { + console.warn( + `[AutoRacingSimulator] ${ids.length - participantsWithPoints.length} participant(s) missing from standings for season ${sportsSeasonId} — blending may be inaccurate` + ); + } + + // Build blended probability weights that combine futures-odds strength + // with standings-based strength, weighted by season progress. + // - Early season (low seasonProgress): mostly futures odds + // - Mid/late season: standings dominate, reducing the distortion from + // championship futures (which penalize 2nd-place drivers whose odds of + // *winning* the title are weak, even though they'll likely finish top 3) + const totalCurrentPoints = [...currentPointsMap.values()].reduce((a, b) => a + b, 0); + const blendedProbs = new Map(); + for (const id of ids) { + const oddsW = baseProbs.get(id) ?? fallbackProb; + const pts = currentPointsMap.get(id) ?? 0; + // Drivers with 0 pts (new entry, early DNF) fall back to odds strength + const standingsW = totalCurrentPoints > 0 && pts > 0 ? pts / totalCurrentPoints : oddsW; + blendedProbs.set(id, (1 - seasonProgress) * oddsW + seasonProgress * standingsW); + } + // Volatility shrinks as the season progresses — late-season standings are // much more predictive than early-season odds. - const effectiveVolatility = PARTICIPANT_VOLATILITY * (1 - seasonProgress * 0.7); + const effectiveVolatility = PARTICIPANT_VOLATILITY * (1 - seasonProgress * VOLATILITY_DECAY_FACTOR); for (let sim = 0; sim < NUM_SIMULATIONS; sim++) { // 7a. Season-long performance multiplier per driver (uses blended strength) const seasonWeights = new Map(); -- 2.45.3 From 1bb70a06c3b4bfcd488a8deb5152dd176f51e49c Mon Sep 17 00:00:00 2001 From: Chris Parsons Date: Sun, 14 Jun 2026 20:26:43 -0700 Subject: [PATCH 3/3] Fix lint errors in AutoRacingSimulator: no-non-null-assertion + no-console Co-Authored-By: Claude Sonnet 4.6 --- .../__tests__/auto-racing-simulator.test.ts | 25 +++++++++++++------ .../simulations/auto-racing-simulator.ts | 1 + 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/app/services/simulations/__tests__/auto-racing-simulator.test.ts b/app/services/simulations/__tests__/auto-racing-simulator.test.ts index c42fba6..9b193c5 100644 --- a/app/services/simulations/__tests__/auto-racing-simulator.test.ts +++ b/app/services/simulations/__tests__/auto-racing-simulator.test.ts @@ -108,8 +108,11 @@ describe("AutoRacingSimulator", () => { it("heavy favourite ranks first more often than long shots", async () => { const results = await new AutoRacingSimulator(F1_RACE_POINTS, "f1").simulate("s1"); - const favourite = results.find((r) => r.participantId === "d1")!; - const longShot = results.find((r) => r.participantId === "d2")!; + const favourite = results.find((r) => r.participantId === "d1"); + const longShot = results.find((r) => r.participantId === "d2"); + expect(favourite).toBeDefined(); + expect(longShot).toBeDefined(); + if (!favourite || !longShot) return; expect(favourite.probabilities.probFirst).toBeGreaterThan(longShot.probabilities.probFirst); }); @@ -178,8 +181,11 @@ describe("AutoRacingSimulator", () => { ]); const results = await new AutoRacingSimulator(F1_RACE_POINTS, "f1").simulate("s1"); - const leader = results.find((r) => r.participantId === "d1")!; - const distant = results.find((r) => r.participantId === "d2")!; + const leader = results.find((r) => r.participantId === "d1"); + const distant = results.find((r) => r.participantId === "d2"); + expect(leader).toBeDefined(); + expect(distant).toBeDefined(); + if (!leader || !distant) return; // Standings signal should dominate: d1's massive points lead wins out expect(leader.probabilities.probFirst).toBeGreaterThan(distant.probabilities.probFirst); }); @@ -192,8 +198,11 @@ describe("AutoRacingSimulator", () => { makeEv("d2", 1000), ]); const results = await new AutoRacingSimulator(F1_RACE_POINTS, "f1").simulate("s1"); - const fav = results.find((r) => r.participantId === "d1")!; - const longShot = results.find((r) => r.participantId === "d2")!; + const fav = results.find((r) => r.participantId === "d1"); + const longShot = results.find((r) => r.participantId === "d2"); + expect(fav).toBeDefined(); + expect(longShot).toBeDefined(); + if (!fav || !longShot) return; // Without standings, odds-favoured driver should still rank higher expect(fav.probabilities.probFirst).toBeGreaterThan(longShot.probabilities.probFirst); }); @@ -223,7 +232,9 @@ describe("AutoRacingSimulator", () => { const results = await new AutoRacingSimulator(F1_RACE_POINTS, "f1").simulate("s1"); // d5 should win championships at a non-trivial rate given their strong odds weight - const d5 = results.find((r) => r.participantId === "d5")!; + const d5 = results.find((r) => r.participantId === "d5"); + expect(d5).toBeDefined(); + if (!d5) return; expect(d5.probabilities.probFirst).toBeGreaterThan(0.1); }); diff --git a/app/services/simulations/auto-racing-simulator.ts b/app/services/simulations/auto-racing-simulator.ts index 151ea14..dc8933e 100644 --- a/app/services/simulations/auto-racing-simulator.ts +++ b/app/services/simulations/auto-racing-simulator.ts @@ -200,6 +200,7 @@ export class AutoRacingSimulator implements Simulator { // share-of-points weight and silently degrade the blending accuracy. const participantsWithPoints = ids.filter((id) => currentPointsMap.has(id)); if (participantsWithPoints.length < ids.length) { + // eslint-disable-next-line no-console console.warn( `[AutoRacingSimulator] ${ids.length - participantsWithPoints.length} participant(s) missing from standings for season ${sportsSeasonId} — blending may be inaccurate` ); -- 2.45.3