2026-06-15 03:34:44 +00:00
|
|
|
|
import { describe, it, expect, vi, beforeEach, type MockInstance } from "vitest";
|
|
|
|
|
|
import { AutoRacingSimulator } from "../auto-racing-simulator";
|
Fix auto racing sims ignoring championship standings
The IndyCar simulator gave a near-clinched championship leader ~60% to win
the title. It was reporting the futures odds and nothing else.
`event_type` has no race value, so a season_standings calendar (F1, IndyCar)
is stored as `schedule_event` rows — the admin default for that scoring
pattern. The simulator skipped exactly those rows when counting races, so it
saw zero remaining races, took the branch labelled "pre-season", and never
looked at `participant_season_results`. Since `sourceOdds` is an admin input
that is never auto-refreshed, the output was whatever the market said months
ago. With a realistic late-season field (leader on 601 pts vs 480, 2 races
left, stale futures at -300) the old path returns ~55%; counting the races
returns 100.0%.
- Add `countSeasonRaces` in a new leaf model. A race is every event except
`final_standings`, and completion is inferred from the event date, since
nobody marks rows labelled "Non-Scoring" complete. Its own module because
`scoring-event.ts` reaches back into `simulator.ts` through
`scoring-calculator`.
- Split "season over" from "pre-season". Both had `remainingRaces === 0`, so
a finished season reverted to an odds draw instead of reporting the final
standings.
- Warn when a season has championship points but no calendar, in the
simulator and as a non-blocking readiness warning on the setup page.
- Replace proportional vig removal with a power devig. Dividing every runner
by the same book sum guts the favourite in a 27-driver market: a 75%
implied favourite came out at 55%, a -20000 near-lock at 95%. Power devig
gives 69.5% and 99.3%. Unpriced drivers now floor at the bottom of the
market instead of being handed 1/N.
- Move the race points tables to their own module so tests can read them
without tripping the manifest/registry import cycle.
The existing tests missed all of this because their event fixtures used
`eventType: "race"`, which is not a value the enum has. Rebuilt on real enum
values, plus a regression test for the reported case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUcV7KenckF893zQ46EDXt
2026-08-17 00:36:20 +00:00
|
|
|
|
import { F1_RACE_POINTS, INDYCAR_RACE_POINTS } from "../race-points";
|
2026-06-15 03:34:44 +00:00
|
|
|
|
|
|
|
|
|
|
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(),
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
Fix auto racing sims ignoring championship standings
The IndyCar simulator gave a near-clinched championship leader ~60% to win
the title. It was reporting the futures odds and nothing else.
`event_type` has no race value, so a season_standings calendar (F1, IndyCar)
is stored as `schedule_event` rows — the admin default for that scoring
pattern. The simulator skipped exactly those rows when counting races, so it
saw zero remaining races, took the branch labelled "pre-season", and never
looked at `participant_season_results`. Since `sourceOdds` is an admin input
that is never auto-refreshed, the output was whatever the market said months
ago. With a realistic late-season field (leader on 601 pts vs 480, 2 races
left, stale futures at -300) the old path returns ~55%; counting the races
returns 100.0%.
- Add `countSeasonRaces` in a new leaf model. A race is every event except
`final_standings`, and completion is inferred from the event date, since
nobody marks rows labelled "Non-Scoring" complete. Its own module because
`scoring-event.ts` reaches back into `simulator.ts` through
`scoring-calculator`.
- Split "season over" from "pre-season". Both had `remainingRaces === 0`, so
a finished season reverted to an odds draw instead of reporting the final
standings.
- Warn when a season has championship points but no calendar, in the
simulator and as a non-blocking readiness warning on the setup page.
- Replace proportional vig removal with a power devig. Dividing every runner
by the same book sum guts the favourite in a 27-driver market: a 75%
implied favourite came out at 55%, a -20000 near-lock at 95%. Power devig
gives 69.5% and 99.3%. Unpriced drivers now floor at the bottom of the
market instead of being handed 1/N.
- Move the race points tables to their own module so tests can read them
without tripping the manifest/registry import cycle.
The existing tests missed all of this because their event fixtures used
`eventType: "race"`, which is not a value the enum has. Rebuilt on real enum
values, plus a regression test for the reported case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUcV7KenckF893zQ46EDXt
2026-08-17 00:36:20 +00:00
|
|
|
|
vi.mock("~/models/season-races", () => ({
|
|
|
|
|
|
countSeasonRaces: vi.fn(),
|
|
|
|
|
|
}));
|
2026-06-15 03:34:44 +00:00
|
|
|
|
|
|
|
|
|
|
// ─── Fixtures ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
const DRIVERS = ["d1", "d2", "d3", "d4", "d5"].map((id) => ({ id }));
|
|
|
|
|
|
|
Fix auto racing sims ignoring championship standings
The IndyCar simulator gave a near-clinched championship leader ~60% to win
the title. It was reporting the futures odds and nothing else.
`event_type` has no race value, so a season_standings calendar (F1, IndyCar)
is stored as `schedule_event` rows — the admin default for that scoring
pattern. The simulator skipped exactly those rows when counting races, so it
saw zero remaining races, took the branch labelled "pre-season", and never
looked at `participant_season_results`. Since `sourceOdds` is an admin input
that is never auto-refreshed, the output was whatever the market said months
ago. With a realistic late-season field (leader on 601 pts vs 480, 2 races
left, stale futures at -300) the old path returns ~55%; counting the races
returns 100.0%.
- Add `countSeasonRaces` in a new leaf model. A race is every event except
`final_standings`, and completion is inferred from the event date, since
nobody marks rows labelled "Non-Scoring" complete. Its own module because
`scoring-event.ts` reaches back into `simulator.ts` through
`scoring-calculator`.
- Split "season over" from "pre-season". Both had `remainingRaces === 0`, so
a finished season reverted to an odds draw instead of reporting the final
standings.
- Warn when a season has championship points but no calendar, in the
simulator and as a non-blocking readiness warning on the setup page.
- Replace proportional vig removal with a power devig. Dividing every runner
by the same book sum guts the favourite in a 27-driver market: a 75%
implied favourite came out at 55%, a -20000 near-lock at 95%. Power devig
gives 69.5% and 99.3%. Unpriced drivers now floor at the bottom of the
market instead of being handed 1/N.
- Move the race points tables to their own module so tests can read them
without tripping the manifest/registry import cycle.
The existing tests missed all of this because their event fixtures used
`eventType: "race"`, which is not a value the enum has. Rebuilt on real enum
values, plus a regression test for the reported case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUcV7KenckF893zQ46EDXt
2026-08-17 00:36:20 +00:00
|
|
|
|
const PROB_KEYS = [
|
|
|
|
|
|
"probFirst", "probSecond", "probThird", "probFourth",
|
|
|
|
|
|
"probFifth", "probSixth", "probSeventh", "probEighth",
|
|
|
|
|
|
] as const;
|
2026-06-15 03:34:44 +00:00
|
|
|
|
|
|
|
|
|
|
function makeSeasonResult(participantId: string, currentPoints: string) {
|
|
|
|
|
|
return { participant: { id: participantId }, currentPoints };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function makeEv(participantId: string, sourceOdds: number | null) {
|
|
|
|
|
|
return { participantId, sourceOdds };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Fix auto racing sims ignoring championship standings
The IndyCar simulator gave a near-clinched championship leader ~60% to win
the title. It was reporting the futures odds and nothing else.
`event_type` has no race value, so a season_standings calendar (F1, IndyCar)
is stored as `schedule_event` rows — the admin default for that scoring
pattern. The simulator skipped exactly those rows when counting races, so it
saw zero remaining races, took the branch labelled "pre-season", and never
looked at `participant_season_results`. Since `sourceOdds` is an admin input
that is never auto-refreshed, the output was whatever the market said months
ago. With a realistic late-season field (leader on 601 pts vs 480, 2 races
left, stale futures at -300) the old path returns ~55%; counting the races
returns 100.0%.
- Add `countSeasonRaces` in a new leaf model. A race is every event except
`final_standings`, and completion is inferred from the event date, since
nobody marks rows labelled "Non-Scoring" complete. Its own module because
`scoring-event.ts` reaches back into `simulator.ts` through
`scoring-calculator`.
- Split "season over" from "pre-season". Both had `remainingRaces === 0`, so
a finished season reverted to an odds draw instead of reporting the final
standings.
- Warn when a season has championship points but no calendar, in the
simulator and as a non-blocking readiness warning on the setup page.
- Replace proportional vig removal with a power devig. Dividing every runner
by the same book sum guts the favourite in a 27-driver market: a 75%
implied favourite came out at 55%, a -20000 near-lock at 95%. Power devig
gives 69.5% and 99.3%. Unpriced drivers now floor at the bottom of the
market instead of being handed 1/N.
- Move the race points tables to their own module so tests can read them
without tripping the manifest/registry import cycle.
The existing tests missed all of this because their event fixtures used
`eventType: "race"`, which is not a value the enum has. Rebuilt on real enum
values, plus a regression test for the reported case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUcV7KenckF893zQ46EDXt
2026-08-17 00:36:20 +00:00
|
|
|
|
function mockDb(drivers: { id: string }[] = DRIVERS) {
|
2026-06-15 03:34:44 +00:00
|
|
|
|
return {
|
|
|
|
|
|
query: {
|
|
|
|
|
|
seasonParticipants: {
|
Fix auto racing sims ignoring championship standings
The IndyCar simulator gave a near-clinched championship leader ~60% to win
the title. It was reporting the futures odds and nothing else.
`event_type` has no race value, so a season_standings calendar (F1, IndyCar)
is stored as `schedule_event` rows — the admin default for that scoring
pattern. The simulator skipped exactly those rows when counting races, so it
saw zero remaining races, took the branch labelled "pre-season", and never
looked at `participant_season_results`. Since `sourceOdds` is an admin input
that is never auto-refreshed, the output was whatever the market said months
ago. With a realistic late-season field (leader on 601 pts vs 480, 2 races
left, stale futures at -300) the old path returns ~55%; counting the races
returns 100.0%.
- Add `countSeasonRaces` in a new leaf model. A race is every event except
`final_standings`, and completion is inferred from the event date, since
nobody marks rows labelled "Non-Scoring" complete. Its own module because
`scoring-event.ts` reaches back into `simulator.ts` through
`scoring-calculator`.
- Split "season over" from "pre-season". Both had `remainingRaces === 0`, so
a finished season reverted to an odds draw instead of reporting the final
standings.
- Warn when a season has championship points but no calendar, in the
simulator and as a non-blocking readiness warning on the setup page.
- Replace proportional vig removal with a power devig. Dividing every runner
by the same book sum guts the favourite in a 27-driver market: a 75%
implied favourite came out at 55%, a -20000 near-lock at 95%. Power devig
gives 69.5% and 99.3%. Unpriced drivers now floor at the bottom of the
market instead of being handed 1/N.
- Move the race points tables to their own module so tests can read them
without tripping the manifest/registry import cycle.
The existing tests missed all of this because their event fixtures used
`eventType: "race"`, which is not a value the enum has. Rebuilt on real enum
values, plus a regression test for the reported case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUcV7KenckF893zQ46EDXt
2026-08-17 00:36:20 +00:00
|
|
|
|
findMany: vi.fn().mockResolvedValue(drivers),
|
2026-06-15 03:34:44 +00:00
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Fix auto racing sims ignoring championship standings
The IndyCar simulator gave a near-clinched championship leader ~60% to win
the title. It was reporting the futures odds and nothing else.
`event_type` has no race value, so a season_standings calendar (F1, IndyCar)
is stored as `schedule_event` rows — the admin default for that scoring
pattern. The simulator skipped exactly those rows when counting races, so it
saw zero remaining races, took the branch labelled "pre-season", and never
looked at `participant_season_results`. Since `sourceOdds` is an admin input
that is never auto-refreshed, the output was whatever the market said months
ago. With a realistic late-season field (leader on 601 pts vs 480, 2 races
left, stale futures at -300) the old path returns ~55%; counting the races
returns 100.0%.
- Add `countSeasonRaces` in a new leaf model. A race is every event except
`final_standings`, and completion is inferred from the event date, since
nobody marks rows labelled "Non-Scoring" complete. Its own module because
`scoring-event.ts` reaches back into `simulator.ts` through
`scoring-calculator`.
- Split "season over" from "pre-season". Both had `remainingRaces === 0`, so
a finished season reverted to an odds draw instead of reporting the final
standings.
- Warn when a season has championship points but no calendar, in the
simulator and as a non-blocking readiness warning on the setup page.
- Replace proportional vig removal with a power devig. Dividing every runner
by the same book sum guts the favourite in a 27-driver market: a 75%
implied favourite came out at 55%, a -20000 near-lock at 95%. Power devig
gives 69.5% and 99.3%. Unpriced drivers now floor at the bottom of the
market instead of being handed 1/N.
- Move the race points tables to their own module so tests can read them
without tripping the manifest/registry import cycle.
The existing tests missed all of this because their event fixtures used
`eventType: "race"`, which is not a value the enum has. Rebuilt on real enum
values, plus a regression test for the reported case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUcV7KenckF893zQ46EDXt
2026-08-17 00:36:20 +00:00
|
|
|
|
/** Set the race counts the simulator reads from the calendar. */
|
|
|
|
|
|
async function setRaceCounts(completed: number, remaining: number) {
|
|
|
|
|
|
const { countSeasonRaces } = await import("~/models/season-races");
|
|
|
|
|
|
(countSeasonRaces as unknown as MockInstance).mockResolvedValue({
|
|
|
|
|
|
completed,
|
|
|
|
|
|
remaining,
|
|
|
|
|
|
total: completed + remaining,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-06-15 03:34:44 +00:00
|
|
|
|
|
Fix auto racing sims ignoring championship standings
The IndyCar simulator gave a near-clinched championship leader ~60% to win
the title. It was reporting the futures odds and nothing else.
`event_type` has no race value, so a season_standings calendar (F1, IndyCar)
is stored as `schedule_event` rows — the admin default for that scoring
pattern. The simulator skipped exactly those rows when counting races, so it
saw zero remaining races, took the branch labelled "pre-season", and never
looked at `participant_season_results`. Since `sourceOdds` is an admin input
that is never auto-refreshed, the output was whatever the market said months
ago. With a realistic late-season field (leader on 601 pts vs 480, 2 races
left, stale futures at -300) the old path returns ~55%; counting the races
returns 100.0%.
- Add `countSeasonRaces` in a new leaf model. A race is every event except
`final_standings`, and completion is inferred from the event date, since
nobody marks rows labelled "Non-Scoring" complete. Its own module because
`scoring-event.ts` reaches back into `simulator.ts` through
`scoring-calculator`.
- Split "season over" from "pre-season". Both had `remainingRaces === 0`, so
a finished season reverted to an odds draw instead of reporting the final
standings.
- Warn when a season has championship points but no calendar, in the
simulator and as a non-blocking readiness warning on the setup page.
- Replace proportional vig removal with a power devig. Dividing every runner
by the same book sum guts the favourite in a 27-driver market: a 75%
implied favourite came out at 55%, a -20000 near-lock at 95%. Power devig
gives 69.5% and 99.3%. Unpriced drivers now floor at the bottom of the
market instead of being handed 1/N.
- Move the race points tables to their own module so tests can read them
without tripping the manifest/registry import cycle.
The existing tests missed all of this because their event fixtures used
`eventType: "race"`, which is not a value the enum has. Rebuilt on real enum
values, plus a regression test for the reported case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUcV7KenckF893zQ46EDXt
2026-08-17 00:36:20 +00:00
|
|
|
|
async function setStandings(results: ReturnType<typeof makeSeasonResult>[]) {
|
2026-06-15 03:34:44 +00:00
|
|
|
|
const { getSeasonResults } = await import("~/models/participant-season-result");
|
Fix auto racing sims ignoring championship standings
The IndyCar simulator gave a near-clinched championship leader ~60% to win
the title. It was reporting the futures odds and nothing else.
`event_type` has no race value, so a season_standings calendar (F1, IndyCar)
is stored as `schedule_event` rows — the admin default for that scoring
pattern. The simulator skipped exactly those rows when counting races, so it
saw zero remaining races, took the branch labelled "pre-season", and never
looked at `participant_season_results`. Since `sourceOdds` is an admin input
that is never auto-refreshed, the output was whatever the market said months
ago. With a realistic late-season field (leader on 601 pts vs 480, 2 races
left, stale futures at -300) the old path returns ~55%; counting the races
returns 100.0%.
- Add `countSeasonRaces` in a new leaf model. A race is every event except
`final_standings`, and completion is inferred from the event date, since
nobody marks rows labelled "Non-Scoring" complete. Its own module because
`scoring-event.ts` reaches back into `simulator.ts` through
`scoring-calculator`.
- Split "season over" from "pre-season". Both had `remainingRaces === 0`, so
a finished season reverted to an odds draw instead of reporting the final
standings.
- Warn when a season has championship points but no calendar, in the
simulator and as a non-blocking readiness warning on the setup page.
- Replace proportional vig removal with a power devig. Dividing every runner
by the same book sum guts the favourite in a 27-driver market: a 75%
implied favourite came out at 55%, a -20000 near-lock at 95%. Power devig
gives 69.5% and 99.3%. Unpriced drivers now floor at the bottom of the
market instead of being handed 1/N.
- Move the race points tables to their own module so tests can read them
without tripping the manifest/registry import cycle.
The existing tests missed all of this because their event fixtures used
`eventType: "race"`, which is not a value the enum has. Rebuilt on real enum
values, plus a regression test for the reported case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUcV7KenckF893zQ46EDXt
2026-08-17 00:36:20 +00:00
|
|
|
|
(getSeasonResults as unknown as MockInstance).mockResolvedValue(results);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function setOdds(evs: ReturnType<typeof makeEv>[]) {
|
2026-06-15 03:34:44 +00:00
|
|
|
|
const { getAllParticipantEVsForSeason } = await import("~/models/participant-expected-value");
|
Fix auto racing sims ignoring championship standings
The IndyCar simulator gave a near-clinched championship leader ~60% to win
the title. It was reporting the futures odds and nothing else.
`event_type` has no race value, so a season_standings calendar (F1, IndyCar)
is stored as `schedule_event` rows — the admin default for that scoring
pattern. The simulator skipped exactly those rows when counting races, so it
saw zero remaining races, took the branch labelled "pre-season", and never
looked at `participant_season_results`. Since `sourceOdds` is an admin input
that is never auto-refreshed, the output was whatever the market said months
ago. With a realistic late-season field (leader on 601 pts vs 480, 2 races
left, stale futures at -300) the old path returns ~55%; counting the races
returns 100.0%.
- Add `countSeasonRaces` in a new leaf model. A race is every event except
`final_standings`, and completion is inferred from the event date, since
nobody marks rows labelled "Non-Scoring" complete. Its own module because
`scoring-event.ts` reaches back into `simulator.ts` through
`scoring-calculator`.
- Split "season over" from "pre-season". Both had `remainingRaces === 0`, so
a finished season reverted to an odds draw instead of reporting the final
standings.
- Warn when a season has championship points but no calendar, in the
simulator and as a non-blocking readiness warning on the setup page.
- Replace proportional vig removal with a power devig. Dividing every runner
by the same book sum guts the favourite in a 27-driver market: a 75%
implied favourite came out at 55%, a -20000 near-lock at 95%. Power devig
gives 69.5% and 99.3%. Unpriced drivers now floor at the bottom of the
market instead of being handed 1/N.
- Move the race points tables to their own module so tests can read them
without tripping the manifest/registry import cycle.
The existing tests missed all of this because their event fixtures used
`eventType: "race"`, which is not a value the enum has. Rebuilt on real enum
values, plus a regression test for the reported case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUcV7KenckF893zQ46EDXt
2026-08-17 00:36:20 +00:00
|
|
|
|
(getAllParticipantEVsForSeason as unknown as MockInstance).mockResolvedValue(evs);
|
|
|
|
|
|
}
|
2026-06-15 03:34:44 +00:00
|
|
|
|
|
Fix auto racing sims ignoring championship standings
The IndyCar simulator gave a near-clinched championship leader ~60% to win
the title. It was reporting the futures odds and nothing else.
`event_type` has no race value, so a season_standings calendar (F1, IndyCar)
is stored as `schedule_event` rows — the admin default for that scoring
pattern. The simulator skipped exactly those rows when counting races, so it
saw zero remaining races, took the branch labelled "pre-season", and never
looked at `participant_season_results`. Since `sourceOdds` is an admin input
that is never auto-refreshed, the output was whatever the market said months
ago. With a realistic late-season field (leader on 601 pts vs 480, 2 races
left, stale futures at -300) the old path returns ~55%; counting the races
returns 100.0%.
- Add `countSeasonRaces` in a new leaf model. A race is every event except
`final_standings`, and completion is inferred from the event date, since
nobody marks rows labelled "Non-Scoring" complete. Its own module because
`scoring-event.ts` reaches back into `simulator.ts` through
`scoring-calculator`.
- Split "season over" from "pre-season". Both had `remainingRaces === 0`, so
a finished season reverted to an odds draw instead of reporting the final
standings.
- Warn when a season has championship points but no calendar, in the
simulator and as a non-blocking readiness warning on the setup page.
- Replace proportional vig removal with a power devig. Dividing every runner
by the same book sum guts the favourite in a 27-driver market: a 75%
implied favourite came out at 55%, a -20000 near-lock at 95%. Power devig
gives 69.5% and 99.3%. Unpriced drivers now floor at the bottom of the
market instead of being handed 1/N.
- Move the race points tables to their own module so tests can read them
without tripping the manifest/registry import cycle.
The existing tests missed all of this because their event fixtures used
`eventType: "race"`, which is not a value the enum has. Rebuilt on real enum
values, plus a regression test for the reported case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUcV7KenckF893zQ46EDXt
2026-08-17 00:36:20 +00:00
|
|
|
|
async function useDrivers(drivers: { id: string }[]) {
|
|
|
|
|
|
const { database } = await import("~/database/context");
|
|
|
|
|
|
(database as unknown as MockInstance).mockReturnValue(mockDb(drivers));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ─── Setup ────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
beforeEach(async () => {
|
|
|
|
|
|
const { database } = await import("~/database/context");
|
|
|
|
|
|
(database as unknown as MockInstance).mockReturnValue(mockDb());
|
|
|
|
|
|
await setStandings([]);
|
|
|
|
|
|
await setOdds([]);
|
|
|
|
|
|
await setRaceCounts(0, 0);
|
2026-06-15 03:34:44 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ─── Tests ────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
describe("AutoRacingSimulator", () => {
|
|
|
|
|
|
it("throws when no participants are found", async () => {
|
Fix auto racing sims ignoring championship standings
The IndyCar simulator gave a near-clinched championship leader ~60% to win
the title. It was reporting the futures odds and nothing else.
`event_type` has no race value, so a season_standings calendar (F1, IndyCar)
is stored as `schedule_event` rows — the admin default for that scoring
pattern. The simulator skipped exactly those rows when counting races, so it
saw zero remaining races, took the branch labelled "pre-season", and never
looked at `participant_season_results`. Since `sourceOdds` is an admin input
that is never auto-refreshed, the output was whatever the market said months
ago. With a realistic late-season field (leader on 601 pts vs 480, 2 races
left, stale futures at -300) the old path returns ~55%; counting the races
returns 100.0%.
- Add `countSeasonRaces` in a new leaf model. A race is every event except
`final_standings`, and completion is inferred from the event date, since
nobody marks rows labelled "Non-Scoring" complete. Its own module because
`scoring-event.ts` reaches back into `simulator.ts` through
`scoring-calculator`.
- Split "season over" from "pre-season". Both had `remainingRaces === 0`, so
a finished season reverted to an odds draw instead of reporting the final
standings.
- Warn when a season has championship points but no calendar, in the
simulator and as a non-blocking readiness warning on the setup page.
- Replace proportional vig removal with a power devig. Dividing every runner
by the same book sum guts the favourite in a 27-driver market: a 75%
implied favourite came out at 55%, a -20000 near-lock at 95%. Power devig
gives 69.5% and 99.3%. Unpriced drivers now floor at the bottom of the
market instead of being handed 1/N.
- Move the race points tables to their own module so tests can read them
without tripping the manifest/registry import cycle.
The existing tests missed all of this because their event fixtures used
`eventType: "race"`, which is not a value the enum has. Rebuilt on real enum
values, plus a regression test for the reported case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUcV7KenckF893zQ46EDXt
2026-08-17 00:36:20 +00:00
|
|
|
|
await useDrivers([]);
|
2026-06-15 03:34:44 +00:00
|
|
|
|
await expect(
|
|
|
|
|
|
new AutoRacingSimulator(F1_RACE_POINTS, "f1").simulate("s1")
|
|
|
|
|
|
).rejects.toThrow(/No participants found/);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
Fix auto racing sims ignoring championship standings
The IndyCar simulator gave a near-clinched championship leader ~60% to win
the title. It was reporting the futures odds and nothing else.
`event_type` has no race value, so a season_standings calendar (F1, IndyCar)
is stored as `schedule_event` rows — the admin default for that scoring
pattern. The simulator skipped exactly those rows when counting races, so it
saw zero remaining races, took the branch labelled "pre-season", and never
looked at `participant_season_results`. Since `sourceOdds` is an admin input
that is never auto-refreshed, the output was whatever the market said months
ago. With a realistic late-season field (leader on 601 pts vs 480, 2 races
left, stale futures at -300) the old path returns ~55%; counting the races
returns 100.0%.
- Add `countSeasonRaces` in a new leaf model. A race is every event except
`final_standings`, and completion is inferred from the event date, since
nobody marks rows labelled "Non-Scoring" complete. Its own module because
`scoring-event.ts` reaches back into `simulator.ts` through
`scoring-calculator`.
- Split "season over" from "pre-season". Both had `remainingRaces === 0`, so
a finished season reverted to an odds draw instead of reporting the final
standings.
- Warn when a season has championship points but no calendar, in the
simulator and as a non-blocking readiness warning on the setup page.
- Replace proportional vig removal with a power devig. Dividing every runner
by the same book sum guts the favourite in a 27-driver market: a 75%
implied favourite came out at 55%, a -20000 near-lock at 95%. Power devig
gives 69.5% and 99.3%. Unpriced drivers now floor at the bottom of the
market instead of being handed 1/N.
- Move the race points tables to their own module so tests can read them
without tripping the manifest/registry import cycle.
The existing tests missed all of this because their event fixtures used
`eventType: "race"`, which is not a value the enum has. Rebuilt on real enum
values, plus a regression test for the reported case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUcV7KenckF893zQ46EDXt
2026-08-17 00:36:20 +00:00
|
|
|
|
describe("pre-season path (no races run, none remaining)", () => {
|
2026-06-15 03:34:44 +00:00
|
|
|
|
beforeEach(async () => {
|
Fix auto racing sims ignoring championship standings
The IndyCar simulator gave a near-clinched championship leader ~60% to win
the title. It was reporting the futures odds and nothing else.
`event_type` has no race value, so a season_standings calendar (F1, IndyCar)
is stored as `schedule_event` rows — the admin default for that scoring
pattern. The simulator skipped exactly those rows when counting races, so it
saw zero remaining races, took the branch labelled "pre-season", and never
looked at `participant_season_results`. Since `sourceOdds` is an admin input
that is never auto-refreshed, the output was whatever the market said months
ago. With a realistic late-season field (leader on 601 pts vs 480, 2 races
left, stale futures at -300) the old path returns ~55%; counting the races
returns 100.0%.
- Add `countSeasonRaces` in a new leaf model. A race is every event except
`final_standings`, and completion is inferred from the event date, since
nobody marks rows labelled "Non-Scoring" complete. Its own module because
`scoring-event.ts` reaches back into `simulator.ts` through
`scoring-calculator`.
- Split "season over" from "pre-season". Both had `remainingRaces === 0`, so
a finished season reverted to an odds draw instead of reporting the final
standings.
- Warn when a season has championship points but no calendar, in the
simulator and as a non-blocking readiness warning on the setup page.
- Replace proportional vig removal with a power devig. Dividing every runner
by the same book sum guts the favourite in a 27-driver market: a 75%
implied favourite came out at 55%, a -20000 near-lock at 95%. Power devig
gives 69.5% and 99.3%. Unpriced drivers now floor at the bottom of the
market instead of being handed 1/N.
- Move the race points tables to their own module so tests can read them
without tripping the manifest/registry import cycle.
The existing tests missed all of this because their event fixtures used
`eventType: "race"`, which is not a value the enum has. Rebuilt on real enum
values, plus a regression test for the reported case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUcV7KenckF893zQ46EDXt
2026-08-17 00:36:20 +00:00
|
|
|
|
await setRaceCounts(0, 0);
|
2026-06-15 03:34:44 +00:00
|
|
|
|
// Heavy favourite: d1 at −500, all others at +1000
|
Fix auto racing sims ignoring championship standings
The IndyCar simulator gave a near-clinched championship leader ~60% to win
the title. It was reporting the futures odds and nothing else.
`event_type` has no race value, so a season_standings calendar (F1, IndyCar)
is stored as `schedule_event` rows — the admin default for that scoring
pattern. The simulator skipped exactly those rows when counting races, so it
saw zero remaining races, took the branch labelled "pre-season", and never
looked at `participant_season_results`. Since `sourceOdds` is an admin input
that is never auto-refreshed, the output was whatever the market said months
ago. With a realistic late-season field (leader on 601 pts vs 480, 2 races
left, stale futures at -300) the old path returns ~55%; counting the races
returns 100.0%.
- Add `countSeasonRaces` in a new leaf model. A race is every event except
`final_standings`, and completion is inferred from the event date, since
nobody marks rows labelled "Non-Scoring" complete. Its own module because
`scoring-event.ts` reaches back into `simulator.ts` through
`scoring-calculator`.
- Split "season over" from "pre-season". Both had `remainingRaces === 0`, so
a finished season reverted to an odds draw instead of reporting the final
standings.
- Warn when a season has championship points but no calendar, in the
simulator and as a non-blocking readiness warning on the setup page.
- Replace proportional vig removal with a power devig. Dividing every runner
by the same book sum guts the favourite in a 27-driver market: a 75%
implied favourite came out at 55%, a -20000 near-lock at 95%. Power devig
gives 69.5% and 99.3%. Unpriced drivers now floor at the bottom of the
market instead of being handed 1/N.
- Move the race points tables to their own module so tests can read them
without tripping the manifest/registry import cycle.
The existing tests missed all of this because their event fixtures used
`eventType: "race"`, which is not a value the enum has. Rebuilt on real enum
values, plus a regression test for the reported case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUcV7KenckF893zQ46EDXt
2026-08-17 00:36:20 +00:00
|
|
|
|
await setOdds([
|
2026-06-15 03:34:44 +00:00
|
|
|
|
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");
|
Fix auto racing sims ignoring championship standings
The IndyCar simulator gave a near-clinched championship leader ~60% to win
the title. It was reporting the futures odds and nothing else.
`event_type` has no race value, so a season_standings calendar (F1, IndyCar)
is stored as `schedule_event` rows — the admin default for that scoring
pattern. The simulator skipped exactly those rows when counting races, so it
saw zero remaining races, took the branch labelled "pre-season", and never
looked at `participant_season_results`. Since `sourceOdds` is an admin input
that is never auto-refreshed, the output was whatever the market said months
ago. With a realistic late-season field (leader on 601 pts vs 480, 2 races
left, stale futures at -300) the old path returns ~55%; counting the races
returns 100.0%.
- Add `countSeasonRaces` in a new leaf model. A race is every event except
`final_standings`, and completion is inferred from the event date, since
nobody marks rows labelled "Non-Scoring" complete. Its own module because
`scoring-event.ts` reaches back into `simulator.ts` through
`scoring-calculator`.
- Split "season over" from "pre-season". Both had `remainingRaces === 0`, so
a finished season reverted to an odds draw instead of reporting the final
standings.
- Warn when a season has championship points but no calendar, in the
simulator and as a non-blocking readiness warning on the setup page.
- Replace proportional vig removal with a power devig. Dividing every runner
by the same book sum guts the favourite in a 27-driver market: a 75%
implied favourite came out at 55%, a -20000 near-lock at 95%. Power devig
gives 69.5% and 99.3%. Unpriced drivers now floor at the bottom of the
market instead of being handed 1/N.
- Move the race points tables to their own module so tests can read them
without tripping the manifest/registry import cycle.
The existing tests missed all of this because their event fixtures used
`eventType: "race"`, which is not a value the enum has. Rebuilt on real enum
values, plus a regression test for the reported case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUcV7KenckF893zQ46EDXt
2026-08-17 00:36:20 +00:00
|
|
|
|
for (const key of PROB_KEYS) {
|
2026-06-15 03:34:44 +00:00
|
|
|
|
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).toBeDefined();
|
|
|
|
|
|
expect(longShot).toBeDefined();
|
|
|
|
|
|
if (!favourite || !longShot) return;
|
|
|
|
|
|
expect(favourite.probabilities.probFirst).toBeGreaterThan(longShot.probabilities.probFirst);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("drivers without odds get equal fallback probability", async () => {
|
Fix auto racing sims ignoring championship standings
The IndyCar simulator gave a near-clinched championship leader ~60% to win
the title. It was reporting the futures odds and nothing else.
`event_type` has no race value, so a season_standings calendar (F1, IndyCar)
is stored as `schedule_event` rows — the admin default for that scoring
pattern. The simulator skipped exactly those rows when counting races, so it
saw zero remaining races, took the branch labelled "pre-season", and never
looked at `participant_season_results`. Since `sourceOdds` is an admin input
that is never auto-refreshed, the output was whatever the market said months
ago. With a realistic late-season field (leader on 601 pts vs 480, 2 races
left, stale futures at -300) the old path returns ~55%; counting the races
returns 100.0%.
- Add `countSeasonRaces` in a new leaf model. A race is every event except
`final_standings`, and completion is inferred from the event date, since
nobody marks rows labelled "Non-Scoring" complete. Its own module because
`scoring-event.ts` reaches back into `simulator.ts` through
`scoring-calculator`.
- Split "season over" from "pre-season". Both had `remainingRaces === 0`, so
a finished season reverted to an odds draw instead of reporting the final
standings.
- Warn when a season has championship points but no calendar, in the
simulator and as a non-blocking readiness warning on the setup page.
- Replace proportional vig removal with a power devig. Dividing every runner
by the same book sum guts the favourite in a 27-driver market: a 75%
implied favourite came out at 55%, a -20000 near-lock at 95%. Power devig
gives 69.5% and 99.3%. Unpriced drivers now floor at the bottom of the
market instead of being handed 1/N.
- Move the race points tables to their own module so tests can read them
without tripping the manifest/registry import cycle.
The existing tests missed all of this because their event fixtures used
`eventType: "race"`, which is not a value the enum has. Rebuilt on real enum
values, plus a regression test for the reported case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUcV7KenckF893zQ46EDXt
2026-08-17 00:36:20 +00:00
|
|
|
|
await setOdds([]);
|
2026-06-15 03:34:44 +00:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
Fix auto racing sims ignoring championship standings
The IndyCar simulator gave a near-clinched championship leader ~60% to win
the title. It was reporting the futures odds and nothing else.
`event_type` has no race value, so a season_standings calendar (F1, IndyCar)
is stored as `schedule_event` rows — the admin default for that scoring
pattern. The simulator skipped exactly those rows when counting races, so it
saw zero remaining races, took the branch labelled "pre-season", and never
looked at `participant_season_results`. Since `sourceOdds` is an admin input
that is never auto-refreshed, the output was whatever the market said months
ago. With a realistic late-season field (leader on 601 pts vs 480, 2 races
left, stale futures at -300) the old path returns ~55%; counting the races
returns 100.0%.
- Add `countSeasonRaces` in a new leaf model. A race is every event except
`final_standings`, and completion is inferred from the event date, since
nobody marks rows labelled "Non-Scoring" complete. Its own module because
`scoring-event.ts` reaches back into `simulator.ts` through
`scoring-calculator`.
- Split "season over" from "pre-season". Both had `remainingRaces === 0`, so
a finished season reverted to an odds draw instead of reporting the final
standings.
- Warn when a season has championship points but no calendar, in the
simulator and as a non-blocking readiness warning on the setup page.
- Replace proportional vig removal with a power devig. Dividing every runner
by the same book sum guts the favourite in a 27-driver market: a 75%
implied favourite came out at 55%, a -20000 near-lock at 95%. Power devig
gives 69.5% and 99.3%. Unpriced drivers now floor at the bottom of the
market instead of being handed 1/N.
- Move the race points tables to their own module so tests can read them
without tripping the manifest/registry import cycle.
The existing tests missed all of this because their event fixtures used
`eventType: "race"`, which is not a value the enum has. Rebuilt on real enum
values, plus a regression test for the reported case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUcV7KenckF893zQ46EDXt
2026-08-17 00:36:20 +00:00
|
|
|
|
|
|
|
|
|
|
it("floors an unpriced driver at the bottom of the priced market", async () => {
|
|
|
|
|
|
// d5 has no odds at all; d2–d4 are +1000 long shots. Before power devig
|
|
|
|
|
|
// an unpriced driver was handed 1/N, which rated them above the field.
|
|
|
|
|
|
await setOdds([
|
|
|
|
|
|
makeEv("d1", -500),
|
|
|
|
|
|
makeEv("d2", 1000),
|
|
|
|
|
|
makeEv("d3", 1000),
|
|
|
|
|
|
makeEv("d4", 1000),
|
|
|
|
|
|
]);
|
|
|
|
|
|
const results = await new AutoRacingSimulator(F1_RACE_POINTS, "f1").simulate("s1");
|
|
|
|
|
|
const unpriced = results.find((r) => r.participantId === "d5");
|
|
|
|
|
|
const longShot = results.find((r) => r.participantId === "d2");
|
|
|
|
|
|
expect(unpriced).toBeDefined();
|
|
|
|
|
|
expect(longShot).toBeDefined();
|
|
|
|
|
|
if (!unpriced || !longShot) return;
|
|
|
|
|
|
expect(unpriced.probabilities.probFirst).toBeLessThanOrEqual(
|
|
|
|
|
|
longShot.probabilities.probFirst + 0.02
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
2026-06-15 03:34:44 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
Fix auto racing sims ignoring championship standings
The IndyCar simulator gave a near-clinched championship leader ~60% to win
the title. It was reporting the futures odds and nothing else.
`event_type` has no race value, so a season_standings calendar (F1, IndyCar)
is stored as `schedule_event` rows — the admin default for that scoring
pattern. The simulator skipped exactly those rows when counting races, so it
saw zero remaining races, took the branch labelled "pre-season", and never
looked at `participant_season_results`. Since `sourceOdds` is an admin input
that is never auto-refreshed, the output was whatever the market said months
ago. With a realistic late-season field (leader on 601 pts vs 480, 2 races
left, stale futures at -300) the old path returns ~55%; counting the races
returns 100.0%.
- Add `countSeasonRaces` in a new leaf model. A race is every event except
`final_standings`, and completion is inferred from the event date, since
nobody marks rows labelled "Non-Scoring" complete. Its own module because
`scoring-event.ts` reaches back into `simulator.ts` through
`scoring-calculator`.
- Split "season over" from "pre-season". Both had `remainingRaces === 0`, so
a finished season reverted to an odds draw instead of reporting the final
standings.
- Warn when a season has championship points but no calendar, in the
simulator and as a non-blocking readiness warning on the setup page.
- Replace proportional vig removal with a power devig. Dividing every runner
by the same book sum guts the favourite in a 27-driver market: a 75%
implied favourite came out at 55%, a -20000 near-lock at 95%. Power devig
gives 69.5% and 99.3%. Unpriced drivers now floor at the bottom of the
market instead of being handed 1/N.
- Move the race points tables to their own module so tests can read them
without tripping the manifest/registry import cycle.
The existing tests missed all of this because their event fixtures used
`eventType: "race"`, which is not a value the enum has. Rebuilt on real enum
values, plus a regression test for the reported case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUcV7KenckF893zQ46EDXt
2026-08-17 00:36:20 +00:00
|
|
|
|
describe("season complete (races run, none remaining)", () => {
|
|
|
|
|
|
it("returns the final standings order deterministically", async () => {
|
|
|
|
|
|
await setRaceCounts(17, 0);
|
|
|
|
|
|
// getSeasonResults returns rows already sorted by championship position.
|
|
|
|
|
|
await setStandings([
|
|
|
|
|
|
makeSeasonResult("d3", "601"),
|
|
|
|
|
|
makeSeasonResult("d1", "480"),
|
|
|
|
|
|
makeSeasonResult("d5", "446"),
|
|
|
|
|
|
makeSeasonResult("d2", "420"),
|
|
|
|
|
|
makeSeasonResult("d4", "398"),
|
2026-06-15 03:34:44 +00:00
|
|
|
|
]);
|
Fix auto racing sims ignoring championship standings
The IndyCar simulator gave a near-clinched championship leader ~60% to win
the title. It was reporting the futures odds and nothing else.
`event_type` has no race value, so a season_standings calendar (F1, IndyCar)
is stored as `schedule_event` rows — the admin default for that scoring
pattern. The simulator skipped exactly those rows when counting races, so it
saw zero remaining races, took the branch labelled "pre-season", and never
looked at `participant_season_results`. Since `sourceOdds` is an admin input
that is never auto-refreshed, the output was whatever the market said months
ago. With a realistic late-season field (leader on 601 pts vs 480, 2 races
left, stale futures at -300) the old path returns ~55%; counting the races
returns 100.0%.
- Add `countSeasonRaces` in a new leaf model. A race is every event except
`final_standings`, and completion is inferred from the event date, since
nobody marks rows labelled "Non-Scoring" complete. Its own module because
`scoring-event.ts` reaches back into `simulator.ts` through
`scoring-calculator`.
- Split "season over" from "pre-season". Both had `remainingRaces === 0`, so
a finished season reverted to an odds draw instead of reporting the final
standings.
- Warn when a season has championship points but no calendar, in the
simulator and as a non-blocking readiness warning on the setup page.
- Replace proportional vig removal with a power devig. Dividing every runner
by the same book sum guts the favourite in a 27-driver market: a 75%
implied favourite came out at 55%, a -20000 near-lock at 95%. Power devig
gives 69.5% and 99.3%. Unpriced drivers now floor at the bottom of the
market instead of being handed 1/N.
- Move the race points tables to their own module so tests can read them
without tripping the manifest/registry import cycle.
The existing tests missed all of this because their event fixtures used
`eventType: "race"`, which is not a value the enum has. Rebuilt on real enum
values, plus a regression test for the reported case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUcV7KenckF893zQ46EDXt
2026-08-17 00:36:20 +00:00
|
|
|
|
// Futures odds disagree entirely — they must be ignored once it is over.
|
|
|
|
|
|
await setOdds([makeEv("d1", -10000), makeEv("d3", 20000)]);
|
|
|
|
|
|
|
|
|
|
|
|
const results = await new AutoRacingSimulator(F1_RACE_POINTS, "f1").simulate("s1");
|
|
|
|
|
|
const byId = new Map(results.map((r) => [r.participantId, r.probabilities]));
|
|
|
|
|
|
|
|
|
|
|
|
expect(byId.get("d3")?.probFirst).toBe(1);
|
|
|
|
|
|
expect(byId.get("d1")?.probFirst).toBe(0);
|
|
|
|
|
|
expect(byId.get("d1")?.probSecond).toBe(1);
|
|
|
|
|
|
expect(byId.get("d5")?.probThird).toBe(1);
|
|
|
|
|
|
expect(byId.get("d2")?.probFourth).toBe(1);
|
|
|
|
|
|
expect(byId.get("d4")?.probFifth).toBe(1);
|
2026-06-15 03:34:44 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
Fix auto racing sims ignoring championship standings
The IndyCar simulator gave a near-clinched championship leader ~60% to win
the title. It was reporting the futures odds and nothing else.
`event_type` has no race value, so a season_standings calendar (F1, IndyCar)
is stored as `schedule_event` rows — the admin default for that scoring
pattern. The simulator skipped exactly those rows when counting races, so it
saw zero remaining races, took the branch labelled "pre-season", and never
looked at `participant_season_results`. Since `sourceOdds` is an admin input
that is never auto-refreshed, the output was whatever the market said months
ago. With a realistic late-season field (leader on 601 pts vs 480, 2 races
left, stale futures at -300) the old path returns ~55%; counting the races
returns 100.0%.
- Add `countSeasonRaces` in a new leaf model. A race is every event except
`final_standings`, and completion is inferred from the event date, since
nobody marks rows labelled "Non-Scoring" complete. Its own module because
`scoring-event.ts` reaches back into `simulator.ts` through
`scoring-calculator`.
- Split "season over" from "pre-season". Both had `remainingRaces === 0`, so
a finished season reverted to an odds draw instead of reporting the final
standings.
- Warn when a season has championship points but no calendar, in the
simulator and as a non-blocking readiness warning on the setup page.
- Replace proportional vig removal with a power devig. Dividing every runner
by the same book sum guts the favourite in a 27-driver market: a 75%
implied favourite came out at 55%, a -20000 near-lock at 95%. Power devig
gives 69.5% and 99.3%. Unpriced drivers now floor at the bottom of the
market instead of being handed 1/N.
- Move the race points tables to their own module so tests can read them
without tripping the manifest/registry import cycle.
The existing tests missed all of this because their event fixtures used
`eventType: "race"`, which is not a value the enum has. Rebuilt on real enum
values, plus a regression test for the reported case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUcV7KenckF893zQ46EDXt
2026-08-17 00:36:20 +00:00
|
|
|
|
it("warns and falls back to odds when there are no standings rows", async () => {
|
|
|
|
|
|
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
|
|
|
|
await setRaceCounts(17, 0);
|
|
|
|
|
|
await setStandings([]);
|
|
|
|
|
|
await setOdds([makeEv("d1", -500), makeEv("d2", 1000)]);
|
|
|
|
|
|
|
|
|
|
|
|
const results = await new AutoRacingSimulator(F1_RACE_POINTS, "f1").simulate("s1");
|
|
|
|
|
|
|
|
|
|
|
|
expect(warnSpy).toHaveBeenCalledWith(
|
|
|
|
|
|
expect.stringContaining("no races left but no standings rows")
|
|
|
|
|
|
);
|
|
|
|
|
|
// Still produces a usable distribution rather than all zeroes.
|
|
|
|
|
|
const total = results.reduce((s, r) => s + r.probabilities.probFirst, 0);
|
|
|
|
|
|
expect(total).toBeCloseTo(1.0, 6);
|
|
|
|
|
|
warnSpy.mockRestore();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe("no race calendar", () => {
|
|
|
|
|
|
it("warns when the season has championship points but no events", async () => {
|
|
|
|
|
|
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
|
|
|
|
await setRaceCounts(0, 0);
|
|
|
|
|
|
await setStandings([makeSeasonResult("d1", "400"), makeSeasonResult("d2", "300")]);
|
|
|
|
|
|
|
|
|
|
|
|
await new AutoRacingSimulator(F1_RACE_POINTS, "f1").simulate("s1");
|
|
|
|
|
|
|
|
|
|
|
|
expect(warnSpy).toHaveBeenCalledWith(
|
|
|
|
|
|
expect.stringContaining("championship points but no race calendar")
|
2026-06-15 03:34:44 +00:00
|
|
|
|
);
|
Fix auto racing sims ignoring championship standings
The IndyCar simulator gave a near-clinched championship leader ~60% to win
the title. It was reporting the futures odds and nothing else.
`event_type` has no race value, so a season_standings calendar (F1, IndyCar)
is stored as `schedule_event` rows — the admin default for that scoring
pattern. The simulator skipped exactly those rows when counting races, so it
saw zero remaining races, took the branch labelled "pre-season", and never
looked at `participant_season_results`. Since `sourceOdds` is an admin input
that is never auto-refreshed, the output was whatever the market said months
ago. With a realistic late-season field (leader on 601 pts vs 480, 2 races
left, stale futures at -300) the old path returns ~55%; counting the races
returns 100.0%.
- Add `countSeasonRaces` in a new leaf model. A race is every event except
`final_standings`, and completion is inferred from the event date, since
nobody marks rows labelled "Non-Scoring" complete. Its own module because
`scoring-event.ts` reaches back into `simulator.ts` through
`scoring-calculator`.
- Split "season over" from "pre-season". Both had `remainingRaces === 0`, so
a finished season reverted to an odds draw instead of reporting the final
standings.
- Warn when a season has championship points but no calendar, in the
simulator and as a non-blocking readiness warning on the setup page.
- Replace proportional vig removal with a power devig. Dividing every runner
by the same book sum guts the favourite in a 27-driver market: a 75%
implied favourite came out at 55%, a -20000 near-lock at 95%. Power devig
gives 69.5% and 99.3%. Unpriced drivers now floor at the bottom of the
market instead of being handed 1/N.
- Move the race points tables to their own module so tests can read them
without tripping the manifest/registry import cycle.
The existing tests missed all of this because their event fixtures used
`eventType: "race"`, which is not a value the enum has. Rebuilt on real enum
values, plus a regression test for the reported case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUcV7KenckF893zQ46EDXt
2026-08-17 00:36:20 +00:00
|
|
|
|
warnSpy.mockRestore();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("stays quiet for a genuine pre-season with no points yet", async () => {
|
|
|
|
|
|
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
|
|
|
|
await setRaceCounts(0, 0);
|
|
|
|
|
|
await setStandings([]);
|
|
|
|
|
|
|
|
|
|
|
|
await new AutoRacingSimulator(F1_RACE_POINTS, "f1").simulate("s1");
|
|
|
|
|
|
|
|
|
|
|
|
expect(warnSpy).not.toHaveBeenCalled();
|
|
|
|
|
|
warnSpy.mockRestore();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe("in-season path (races remaining)", () => {
|
|
|
|
|
|
beforeEach(async () => {
|
|
|
|
|
|
await setRaceCounts(10, 5);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("normalizes each position column to sum to 1.0", async () => {
|
|
|
|
|
|
await setStandings(DRIVERS.map((d, i) => makeSeasonResult(d.id, String((5 - i) * 50))));
|
2026-06-15 03:34:44 +00:00
|
|
|
|
const results = await new AutoRacingSimulator(F1_RACE_POINTS, "f1").simulate("s1");
|
Fix auto racing sims ignoring championship standings
The IndyCar simulator gave a near-clinched championship leader ~60% to win
the title. It was reporting the futures odds and nothing else.
`event_type` has no race value, so a season_standings calendar (F1, IndyCar)
is stored as `schedule_event` rows — the admin default for that scoring
pattern. The simulator skipped exactly those rows when counting races, so it
saw zero remaining races, took the branch labelled "pre-season", and never
looked at `participant_season_results`. Since `sourceOdds` is an admin input
that is never auto-refreshed, the output was whatever the market said months
ago. With a realistic late-season field (leader on 601 pts vs 480, 2 races
left, stale futures at -300) the old path returns ~55%; counting the races
returns 100.0%.
- Add `countSeasonRaces` in a new leaf model. A race is every event except
`final_standings`, and completion is inferred from the event date, since
nobody marks rows labelled "Non-Scoring" complete. Its own module because
`scoring-event.ts` reaches back into `simulator.ts` through
`scoring-calculator`.
- Split "season over" from "pre-season". Both had `remainingRaces === 0`, so
a finished season reverted to an odds draw instead of reporting the final
standings.
- Warn when a season has championship points but no calendar, in the
simulator and as a non-blocking readiness warning on the setup page.
- Replace proportional vig removal with a power devig. Dividing every runner
by the same book sum guts the favourite in a 27-driver market: a 75%
implied favourite came out at 55%, a -20000 near-lock at 95%. Power devig
gives 69.5% and 99.3%. Unpriced drivers now floor at the bottom of the
market instead of being handed 1/N.
- Move the race points tables to their own module so tests can read them
without tripping the manifest/registry import cycle.
The existing tests missed all of this because their event fixtures used
`eventType: "race"`, which is not a value the enum has. Rebuilt on real enum
values, plus a regression test for the reported case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUcV7KenckF893zQ46EDXt
2026-08-17 00:36:20 +00:00
|
|
|
|
for (const key of PROB_KEYS) {
|
2026-06-15 03:34:44 +00:00
|
|
|
|
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%
|
Fix auto racing sims ignoring championship standings
The IndyCar simulator gave a near-clinched championship leader ~60% to win
the title. It was reporting the futures odds and nothing else.
`event_type` has no race value, so a season_standings calendar (F1, IndyCar)
is stored as `schedule_event` rows — the admin default for that scoring
pattern. The simulator skipped exactly those rows when counting races, so it
saw zero remaining races, took the branch labelled "pre-season", and never
looked at `participant_season_results`. Since `sourceOdds` is an admin input
that is never auto-refreshed, the output was whatever the market said months
ago. With a realistic late-season field (leader on 601 pts vs 480, 2 races
left, stale futures at -300) the old path returns ~55%; counting the races
returns 100.0%.
- Add `countSeasonRaces` in a new leaf model. A race is every event except
`final_standings`, and completion is inferred from the event date, since
nobody marks rows labelled "Non-Scoring" complete. Its own module because
`scoring-event.ts` reaches back into `simulator.ts` through
`scoring-calculator`.
- Split "season over" from "pre-season". Both had `remainingRaces === 0`, so
a finished season reverted to an odds draw instead of reporting the final
standings.
- Warn when a season has championship points but no calendar, in the
simulator and as a non-blocking readiness warning on the setup page.
- Replace proportional vig removal with a power devig. Dividing every runner
by the same book sum guts the favourite in a 27-driver market: a 75%
implied favourite came out at 55%, a -20000 near-lock at 95%. Power devig
gives 69.5% and 99.3%. Unpriced drivers now floor at the bottom of the
market instead of being handed 1/N.
- Move the race points tables to their own module so tests can read them
without tripping the manifest/registry import cycle.
The existing tests missed all of this because their event fixtures used
`eventType: "race"`, which is not a value the enum has. Rebuilt on real enum
values, plus a regression test for the reported case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUcV7KenckF893zQ46EDXt
2026-08-17 00:36:20 +00:00
|
|
|
|
await setRaceCounts(20, 5);
|
2026-06-15 03:34:44 +00:00
|
|
|
|
// d1 leads with 400 pts; d2 is a distant 2nd with 50 pts
|
Fix auto racing sims ignoring championship standings
The IndyCar simulator gave a near-clinched championship leader ~60% to win
the title. It was reporting the futures odds and nothing else.
`event_type` has no race value, so a season_standings calendar (F1, IndyCar)
is stored as `schedule_event` rows — the admin default for that scoring
pattern. The simulator skipped exactly those rows when counting races, so it
saw zero remaining races, took the branch labelled "pre-season", and never
looked at `participant_season_results`. Since `sourceOdds` is an admin input
that is never auto-refreshed, the output was whatever the market said months
ago. With a realistic late-season field (leader on 601 pts vs 480, 2 races
left, stale futures at -300) the old path returns ~55%; counting the races
returns 100.0%.
- Add `countSeasonRaces` in a new leaf model. A race is every event except
`final_standings`, and completion is inferred from the event date, since
nobody marks rows labelled "Non-Scoring" complete. Its own module because
`scoring-event.ts` reaches back into `simulator.ts` through
`scoring-calculator`.
- Split "season over" from "pre-season". Both had `remainingRaces === 0`, so
a finished season reverted to an odds draw instead of reporting the final
standings.
- Warn when a season has championship points but no calendar, in the
simulator and as a non-blocking readiness warning on the setup page.
- Replace proportional vig removal with a power devig. Dividing every runner
by the same book sum guts the favourite in a 27-driver market: a 75%
implied favourite came out at 55%, a -20000 near-lock at 95%. Power devig
gives 69.5% and 99.3%. Unpriced drivers now floor at the bottom of the
market instead of being handed 1/N.
- Move the race points tables to their own module so tests can read them
without tripping the manifest/registry import cycle.
The existing tests missed all of this because their event fixtures used
`eventType: "race"`, which is not a value the enum has. Rebuilt on real enum
values, plus a regression test for the reported case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUcV7KenckF893zQ46EDXt
2026-08-17 00:36:20 +00:00
|
|
|
|
await setStandings([
|
2026-06-15 03:34:44 +00:00
|
|
|
|
makeSeasonResult("d1", "400"),
|
|
|
|
|
|
makeSeasonResult("d2", "50"),
|
|
|
|
|
|
makeSeasonResult("d3", "40"),
|
|
|
|
|
|
makeSeasonResult("d4", "30"),
|
|
|
|
|
|
makeSeasonResult("d5", "20"),
|
|
|
|
|
|
]);
|
|
|
|
|
|
// Futures odds heavily favour d2 (pretend markets disagree)
|
Fix auto racing sims ignoring championship standings
The IndyCar simulator gave a near-clinched championship leader ~60% to win
the title. It was reporting the futures odds and nothing else.
`event_type` has no race value, so a season_standings calendar (F1, IndyCar)
is stored as `schedule_event` rows — the admin default for that scoring
pattern. The simulator skipped exactly those rows when counting races, so it
saw zero remaining races, took the branch labelled "pre-season", and never
looked at `participant_season_results`. Since `sourceOdds` is an admin input
that is never auto-refreshed, the output was whatever the market said months
ago. With a realistic late-season field (leader on 601 pts vs 480, 2 races
left, stale futures at -300) the old path returns ~55%; counting the races
returns 100.0%.
- Add `countSeasonRaces` in a new leaf model. A race is every event except
`final_standings`, and completion is inferred from the event date, since
nobody marks rows labelled "Non-Scoring" complete. Its own module because
`scoring-event.ts` reaches back into `simulator.ts` through
`scoring-calculator`.
- Split "season over" from "pre-season". Both had `remainingRaces === 0`, so
a finished season reverted to an odds draw instead of reporting the final
standings.
- Warn when a season has championship points but no calendar, in the
simulator and as a non-blocking readiness warning on the setup page.
- Replace proportional vig removal with a power devig. Dividing every runner
by the same book sum guts the favourite in a 27-driver market: a 75%
implied favourite came out at 55%, a -20000 near-lock at 95%. Power devig
gives 69.5% and 99.3%. Unpriced drivers now floor at the bottom of the
market instead of being handed 1/N.
- Move the race points tables to their own module so tests can read them
without tripping the manifest/registry import cycle.
The existing tests missed all of this because their event fixtures used
`eventType: "race"`, which is not a value the enum has. Rebuilt on real enum
values, plus a regression test for the reported case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUcV7KenckF893zQ46EDXt
2026-08-17 00:36:20 +00:00
|
|
|
|
await setOdds([
|
2026-06-15 03:34:44 +00:00
|
|
|
|
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");
|
|
|
|
|
|
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);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("falls back to odds for all drivers when no standings data exists", async () => {
|
|
|
|
|
|
// totalCurrentPoints = 0 → standings signal disabled, odds take over
|
Fix auto racing sims ignoring championship standings
The IndyCar simulator gave a near-clinched championship leader ~60% to win
the title. It was reporting the futures odds and nothing else.
`event_type` has no race value, so a season_standings calendar (F1, IndyCar)
is stored as `schedule_event` rows — the admin default for that scoring
pattern. The simulator skipped exactly those rows when counting races, so it
saw zero remaining races, took the branch labelled "pre-season", and never
looked at `participant_season_results`. Since `sourceOdds` is an admin input
that is never auto-refreshed, the output was whatever the market said months
ago. With a realistic late-season field (leader on 601 pts vs 480, 2 races
left, stale futures at -300) the old path returns ~55%; counting the races
returns 100.0%.
- Add `countSeasonRaces` in a new leaf model. A race is every event except
`final_standings`, and completion is inferred from the event date, since
nobody marks rows labelled "Non-Scoring" complete. Its own module because
`scoring-event.ts` reaches back into `simulator.ts` through
`scoring-calculator`.
- Split "season over" from "pre-season". Both had `remainingRaces === 0`, so
a finished season reverted to an odds draw instead of reporting the final
standings.
- Warn when a season has championship points but no calendar, in the
simulator and as a non-blocking readiness warning on the setup page.
- Replace proportional vig removal with a power devig. Dividing every runner
by the same book sum guts the favourite in a 27-driver market: a 75%
implied favourite came out at 55%, a -20000 near-lock at 95%. Power devig
gives 69.5% and 99.3%. Unpriced drivers now floor at the bottom of the
market instead of being handed 1/N.
- Move the race points tables to their own module so tests can read them
without tripping the manifest/registry import cycle.
The existing tests missed all of this because their event fixtures used
`eventType: "race"`, which is not a value the enum has. Rebuilt on real enum
values, plus a regression test for the reported case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUcV7KenckF893zQ46EDXt
2026-08-17 00:36:20 +00:00
|
|
|
|
await setOdds([makeEv("d1", -500), makeEv("d2", 1000)]);
|
2026-06-15 03:34:44 +00:00
|
|
|
|
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");
|
|
|
|
|
|
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);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("a driver with 0 points mid-season is not penalized beyond their odds weight", async () => {
|
Fix auto racing sims ignoring championship standings
The IndyCar simulator gave a near-clinched championship leader ~60% to win
the title. It was reporting the futures odds and nothing else.
`event_type` has no race value, so a season_standings calendar (F1, IndyCar)
is stored as `schedule_event` rows — the admin default for that scoring
pattern. The simulator skipped exactly those rows when counting races, so it
saw zero remaining races, took the branch labelled "pre-season", and never
looked at `participant_season_results`. Since `sourceOdds` is an admin input
that is never auto-refreshed, the output was whatever the market said months
ago. With a realistic late-season field (leader on 601 pts vs 480, 2 races
left, stale futures at -300) the old path returns ~55%; counting the races
returns 100.0%.
- Add `countSeasonRaces` in a new leaf model. A race is every event except
`final_standings`, and completion is inferred from the event date, since
nobody marks rows labelled "Non-Scoring" complete. Its own module because
`scoring-event.ts` reaches back into `simulator.ts` through
`scoring-calculator`.
- Split "season over" from "pre-season". Both had `remainingRaces === 0`, so
a finished season reverted to an odds draw instead of reporting the final
standings.
- Warn when a season has championship points but no calendar, in the
simulator and as a non-blocking readiness warning on the setup page.
- Replace proportional vig removal with a power devig. Dividing every runner
by the same book sum guts the favourite in a 27-driver market: a 75%
implied favourite came out at 55%, a -20000 near-lock at 95%. Power devig
gives 69.5% and 99.3%. Unpriced drivers now floor at the bottom of the
market instead of being handed 1/N.
- Move the race points tables to their own module so tests can read them
without tripping the manifest/registry import cycle.
The existing tests missed all of this because their event fixtures used
`eventType: "race"`, which is not a value the enum has. Rebuilt on real enum
values, plus a regression test for the reported case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUcV7KenckF893zQ46EDXt
2026-08-17 00:36:20 +00:00
|
|
|
|
// Early season → standings gap is small
|
|
|
|
|
|
await setRaceCounts(2, 20);
|
2026-06-15 03:34:44 +00:00
|
|
|
|
// d1-d4 have a modest lead; d5 is absent (0 pts, new entry)
|
Fix auto racing sims ignoring championship standings
The IndyCar simulator gave a near-clinched championship leader ~60% to win
the title. It was reporting the futures odds and nothing else.
`event_type` has no race value, so a season_standings calendar (F1, IndyCar)
is stored as `schedule_event` rows — the admin default for that scoring
pattern. The simulator skipped exactly those rows when counting races, so it
saw zero remaining races, took the branch labelled "pre-season", and never
looked at `participant_season_results`. Since `sourceOdds` is an admin input
that is never auto-refreshed, the output was whatever the market said months
ago. With a realistic late-season field (leader on 601 pts vs 480, 2 races
left, stale futures at -300) the old path returns ~55%; counting the races
returns 100.0%.
- Add `countSeasonRaces` in a new leaf model. A race is every event except
`final_standings`, and completion is inferred from the event date, since
nobody marks rows labelled "Non-Scoring" complete. Its own module because
`scoring-event.ts` reaches back into `simulator.ts` through
`scoring-calculator`.
- Split "season over" from "pre-season". Both had `remainingRaces === 0`, so
a finished season reverted to an odds draw instead of reporting the final
standings.
- Warn when a season has championship points but no calendar, in the
simulator and as a non-blocking readiness warning on the setup page.
- Replace proportional vig removal with a power devig. Dividing every runner
by the same book sum guts the favourite in a 27-driver market: a 75%
implied favourite came out at 55%, a -20000 near-lock at 95%. Power devig
gives 69.5% and 99.3%. Unpriced drivers now floor at the bottom of the
market instead of being handed 1/N.
- Move the race points tables to their own module so tests can read them
without tripping the manifest/registry import cycle.
The existing tests missed all of this because their event fixtures used
`eventType: "race"`, which is not a value the enum has. Rebuilt on real enum
values, plus a regression test for the reported case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUcV7KenckF893zQ46EDXt
2026-08-17 00:36:20 +00:00
|
|
|
|
await setStandings([
|
2026-06-15 03:34:44 +00:00
|
|
|
|
makeSeasonResult("d1", "10"),
|
|
|
|
|
|
makeSeasonResult("d2", "8"),
|
|
|
|
|
|
makeSeasonResult("d3", "6"),
|
|
|
|
|
|
makeSeasonResult("d4", "4"),
|
|
|
|
|
|
// d5 intentionally absent → falls back to odds weight
|
|
|
|
|
|
]);
|
Fix auto racing sims ignoring championship standings
The IndyCar simulator gave a near-clinched championship leader ~60% to win
the title. It was reporting the futures odds and nothing else.
`event_type` has no race value, so a season_standings calendar (F1, IndyCar)
is stored as `schedule_event` rows — the admin default for that scoring
pattern. The simulator skipped exactly those rows when counting races, so it
saw zero remaining races, took the branch labelled "pre-season", and never
looked at `participant_season_results`. Since `sourceOdds` is an admin input
that is never auto-refreshed, the output was whatever the market said months
ago. With a realistic late-season field (leader on 601 pts vs 480, 2 races
left, stale futures at -300) the old path returns ~55%; counting the races
returns 100.0%.
- Add `countSeasonRaces` in a new leaf model. A race is every event except
`final_standings`, and completion is inferred from the event date, since
nobody marks rows labelled "Non-Scoring" complete. Its own module because
`scoring-event.ts` reaches back into `simulator.ts` through
`scoring-calculator`.
- Split "season over" from "pre-season". Both had `remainingRaces === 0`, so
a finished season reverted to an odds draw instead of reporting the final
standings.
- Warn when a season has championship points but no calendar, in the
simulator and as a non-blocking readiness warning on the setup page.
- Replace proportional vig removal with a power devig. Dividing every runner
by the same book sum guts the favourite in a 27-driver market: a 75%
implied favourite came out at 55%, a -20000 near-lock at 95%. Power devig
gives 69.5% and 99.3%. Unpriced drivers now floor at the bottom of the
market instead of being handed 1/N.
- Move the race points tables to their own module so tests can read them
without tripping the manifest/registry import cycle.
The existing tests missed all of this because their event fixtures used
`eventType: "race"`, which is not a value the enum has. Rebuilt on real enum
values, plus a regression test for the reported case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUcV7KenckF893zQ46EDXt
2026-08-17 00:36:20 +00:00
|
|
|
|
await setOdds([makeEv("d5", -500)]); // strong odds favourite despite 0 pts
|
2026-06-15 03:34:44 +00:00
|
|
|
|
|
|
|
|
|
|
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).toBeDefined();
|
|
|
|
|
|
if (!d5) return;
|
|
|
|
|
|
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(() => {});
|
|
|
|
|
|
// Only 3 of 5 drivers have standings rows
|
Fix auto racing sims ignoring championship standings
The IndyCar simulator gave a near-clinched championship leader ~60% to win
the title. It was reporting the futures odds and nothing else.
`event_type` has no race value, so a season_standings calendar (F1, IndyCar)
is stored as `schedule_event` rows — the admin default for that scoring
pattern. The simulator skipped exactly those rows when counting races, so it
saw zero remaining races, took the branch labelled "pre-season", and never
looked at `participant_season_results`. Since `sourceOdds` is an admin input
that is never auto-refreshed, the output was whatever the market said months
ago. With a realistic late-season field (leader on 601 pts vs 480, 2 races
left, stale futures at -300) the old path returns ~55%; counting the races
returns 100.0%.
- Add `countSeasonRaces` in a new leaf model. A race is every event except
`final_standings`, and completion is inferred from the event date, since
nobody marks rows labelled "Non-Scoring" complete. Its own module because
`scoring-event.ts` reaches back into `simulator.ts` through
`scoring-calculator`.
- Split "season over" from "pre-season". Both had `remainingRaces === 0`, so
a finished season reverted to an odds draw instead of reporting the final
standings.
- Warn when a season has championship points but no calendar, in the
simulator and as a non-blocking readiness warning on the setup page.
- Replace proportional vig removal with a power devig. Dividing every runner
by the same book sum guts the favourite in a 27-driver market: a 75%
implied favourite came out at 55%, a -20000 near-lock at 95%. Power devig
gives 69.5% and 99.3%. Unpriced drivers now floor at the bottom of the
market instead of being handed 1/N.
- Move the race points tables to their own module so tests can read them
without tripping the manifest/registry import cycle.
The existing tests missed all of this because their event fixtures used
`eventType: "race"`, which is not a value the enum has. Rebuilt on real enum
values, plus a regression test for the reported case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUcV7KenckF893zQ46EDXt
2026-08-17 00:36:20 +00:00
|
|
|
|
await setStandings([
|
2026-06-15 03:34:44 +00:00
|
|
|
|
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();
|
|
|
|
|
|
});
|
Fix auto racing sims ignoring championship standings
The IndyCar simulator gave a near-clinched championship leader ~60% to win
the title. It was reporting the futures odds and nothing else.
`event_type` has no race value, so a season_standings calendar (F1, IndyCar)
is stored as `schedule_event` rows — the admin default for that scoring
pattern. The simulator skipped exactly those rows when counting races, so it
saw zero remaining races, took the branch labelled "pre-season", and never
looked at `participant_season_results`. Since `sourceOdds` is an admin input
that is never auto-refreshed, the output was whatever the market said months
ago. With a realistic late-season field (leader on 601 pts vs 480, 2 races
left, stale futures at -300) the old path returns ~55%; counting the races
returns 100.0%.
- Add `countSeasonRaces` in a new leaf model. A race is every event except
`final_standings`, and completion is inferred from the event date, since
nobody marks rows labelled "Non-Scoring" complete. Its own module because
`scoring-event.ts` reaches back into `simulator.ts` through
`scoring-calculator`.
- Split "season over" from "pre-season". Both had `remainingRaces === 0`, so
a finished season reverted to an odds draw instead of reporting the final
standings.
- Warn when a season has championship points but no calendar, in the
simulator and as a non-blocking readiness warning on the setup page.
- Replace proportional vig removal with a power devig. Dividing every runner
by the same book sum guts the favourite in a 27-driver market: a 75%
implied favourite came out at 55%, a -20000 near-lock at 95%. Power devig
gives 69.5% and 99.3%. Unpriced drivers now floor at the bottom of the
market instead of being handed 1/N.
- Move the race points tables to their own module so tests can read them
without tripping the manifest/registry import cycle.
The existing tests missed all of this because their event fixtures used
`eventType: "race"`, which is not a value the enum has. Rebuilt on real enum
values, plus a regression test for the reported case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUcV7KenckF893zQ46EDXt
2026-08-17 00:36:20 +00:00
|
|
|
|
});
|
2026-06-15 03:34:44 +00:00
|
|
|
|
|
Fix auto racing sims ignoring championship standings
The IndyCar simulator gave a near-clinched championship leader ~60% to win
the title. It was reporting the futures odds and nothing else.
`event_type` has no race value, so a season_standings calendar (F1, IndyCar)
is stored as `schedule_event` rows — the admin default for that scoring
pattern. The simulator skipped exactly those rows when counting races, so it
saw zero remaining races, took the branch labelled "pre-season", and never
looked at `participant_season_results`. Since `sourceOdds` is an admin input
that is never auto-refreshed, the output was whatever the market said months
ago. With a realistic late-season field (leader on 601 pts vs 480, 2 races
left, stale futures at -300) the old path returns ~55%; counting the races
returns 100.0%.
- Add `countSeasonRaces` in a new leaf model. A race is every event except
`final_standings`, and completion is inferred from the event date, since
nobody marks rows labelled "Non-Scoring" complete. Its own module because
`scoring-event.ts` reaches back into `simulator.ts` through
`scoring-calculator`.
- Split "season over" from "pre-season". Both had `remainingRaces === 0`, so
a finished season reverted to an odds draw instead of reporting the final
standings.
- Warn when a season has championship points but no calendar, in the
simulator and as a non-blocking readiness warning on the setup page.
- Replace proportional vig removal with a power devig. Dividing every runner
by the same book sum guts the favourite in a 27-driver market: a 75%
implied favourite came out at 55%, a -20000 near-lock at 95%. Power devig
gives 69.5% and 99.3%. Unpriced drivers now floor at the bottom of the
market instead of being handed 1/N.
- Move the race points tables to their own module so tests can read them
without tripping the manifest/registry import cycle.
The existing tests missed all of this because their event fixtures used
`eventType: "race"`, which is not a value the enum has. Rebuilt on real enum
values, plus a regression test for the reported case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUcV7KenckF893zQ46EDXt
2026-08-17 00:36:20 +00:00
|
|
|
|
describe("IndyCar regression: near-clinched championship leader", () => {
|
|
|
|
|
|
// The reported bug. A 121-point lead with 2 races left is arithmetically
|
|
|
|
|
|
// unassailable (max 100 available, and the leader banks at least 10), but
|
|
|
|
|
|
// the simulator skipped `schedule_event` rows, saw zero remaining races,
|
|
|
|
|
|
// took the pre-season branch and echoed stale futures odds at ~55%.
|
|
|
|
|
|
const POINTS = [
|
|
|
|
|
|
601, 480, 446, 420, 398, 372, 350, 331, 315, 300, 288, 270, 255, 240,
|
|
|
|
|
|
228, 215, 200, 188, 175, 160, 148, 135, 120, 105, 90, 70, 55,
|
|
|
|
|
|
];
|
|
|
|
|
|
const ODDS = [
|
|
|
|
|
|
-300, 450, 700, 1200, 1800, 2500, 4000, 5000, 6000, 8000, 10000, 12000,
|
|
|
|
|
|
15000, 20000, 25000, 30000, 40000, 50000, 50000, 50000, 50000, 50000,
|
|
|
|
|
|
50000, 50000, 50000, 50000, 50000,
|
|
|
|
|
|
];
|
|
|
|
|
|
const FIELD = POINTS.map((_, i) => ({ id: `driver${i}` }));
|
|
|
|
|
|
|
|
|
|
|
|
beforeEach(async () => {
|
|
|
|
|
|
await useDrivers(FIELD);
|
|
|
|
|
|
await setStandings(FIELD.map((d, i) => makeSeasonResult(d.id, String(POINTS[i]))));
|
|
|
|
|
|
await setOdds(FIELD.map((d, i) => makeEv(d.id, ODDS[i])));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("gives the leader ~100% with 2 of 17 races left", async () => {
|
|
|
|
|
|
await setRaceCounts(15, 2);
|
|
|
|
|
|
const results = await new AutoRacingSimulator(INDYCAR_RACE_POINTS, "indycar").simulate("s1", {
|
|
|
|
|
|
iterations: 2000,
|
|
|
|
|
|
});
|
|
|
|
|
|
const leader = results.find((r) => r.participantId === "driver0");
|
|
|
|
|
|
expect(leader).toBeDefined();
|
|
|
|
|
|
if (!leader) return;
|
|
|
|
|
|
expect(leader.probabilities.probFirst).toBeGreaterThan(0.99);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("without a calendar it can only echo the stale odds — the shape of the bug", async () => {
|
|
|
|
|
|
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
|
|
|
|
await setRaceCounts(0, 0);
|
|
|
|
|
|
const results = await new AutoRacingSimulator(INDYCAR_RACE_POINTS, "indycar").simulate("s1", {
|
|
|
|
|
|
iterations: 2000,
|
|
|
|
|
|
});
|
|
|
|
|
|
const leader = results.find((r) => r.participantId === "driver0");
|
|
|
|
|
|
expect(leader).toBeDefined();
|
|
|
|
|
|
if (!leader) return;
|
|
|
|
|
|
// Nowhere near the truth, which is exactly why the no-calendar warning
|
|
|
|
|
|
// above exists. Power devig keeps the -300 favourite well clear of the
|
|
|
|
|
|
// 55% that proportional devig produced, but odds alone cannot see a
|
|
|
|
|
|
// 121-point lead.
|
|
|
|
|
|
expect(leader.probabilities.probFirst).toBeLessThan(0.9);
|
|
|
|
|
|
expect(warnSpy).toHaveBeenCalledWith(
|
|
|
|
|
|
expect.stringContaining("championship points but no race calendar")
|
|
|
|
|
|
);
|
|
|
|
|
|
warnSpy.mockRestore();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("still gives the leader a commanding lead with 5 races left", async () => {
|
|
|
|
|
|
await setRaceCounts(12, 5);
|
|
|
|
|
|
const results = await new AutoRacingSimulator(INDYCAR_RACE_POINTS, "indycar").simulate("s1", {
|
|
|
|
|
|
iterations: 2000,
|
|
|
|
|
|
});
|
|
|
|
|
|
const leader = results.find((r) => r.participantId === "driver0");
|
|
|
|
|
|
expect(leader).toBeDefined();
|
|
|
|
|
|
if (!leader) return;
|
|
|
|
|
|
expect(leader.probabilities.probFirst).toBeGreaterThan(0.9);
|
2026-06-15 03:34:44 +00:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
Fix auto racing sims ignoring championship standings
The IndyCar simulator gave a near-clinched championship leader ~60% to win
the title. It was reporting the futures odds and nothing else.
`event_type` has no race value, so a season_standings calendar (F1, IndyCar)
is stored as `schedule_event` rows — the admin default for that scoring
pattern. The simulator skipped exactly those rows when counting races, so it
saw zero remaining races, took the branch labelled "pre-season", and never
looked at `participant_season_results`. Since `sourceOdds` is an admin input
that is never auto-refreshed, the output was whatever the market said months
ago. With a realistic late-season field (leader on 601 pts vs 480, 2 races
left, stale futures at -300) the old path returns ~55%; counting the races
returns 100.0%.
- Add `countSeasonRaces` in a new leaf model. A race is every event except
`final_standings`, and completion is inferred from the event date, since
nobody marks rows labelled "Non-Scoring" complete. Its own module because
`scoring-event.ts` reaches back into `simulator.ts` through
`scoring-calculator`.
- Split "season over" from "pre-season". Both had `remainingRaces === 0`, so
a finished season reverted to an odds draw instead of reporting the final
standings.
- Warn when a season has championship points but no calendar, in the
simulator and as a non-blocking readiness warning on the setup page.
- Replace proportional vig removal with a power devig. Dividing every runner
by the same book sum guts the favourite in a 27-driver market: a 75%
implied favourite came out at 55%, a -20000 near-lock at 95%. Power devig
gives 69.5% and 99.3%. Unpriced drivers now floor at the bottom of the
market instead of being handed 1/N.
- Move the race points tables to their own module so tests can read them
without tripping the manifest/registry import cycle.
The existing tests missed all of this because their event fixtures used
`eventType: "race"`, which is not a value the enum has. Rebuilt on real enum
values, plus a regression test for the reported case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUcV7KenckF893zQ46EDXt
2026-08-17 00:36:20 +00:00
|
|
|
|
|
|
|
|
|
|
describe("race points tables", () => {
|
|
|
|
|
|
it("IndyCar pays 50 for a win and scores down to P26", () => {
|
|
|
|
|
|
expect(INDYCAR_RACE_POINTS[1]).toBe(50);
|
|
|
|
|
|
expect(INDYCAR_RACE_POINTS[2]).toBe(40);
|
|
|
|
|
|
expect(INDYCAR_RACE_POINTS[25]).toBe(5);
|
|
|
|
|
|
expect(INDYCAR_RACE_POINTS[26]).toBe(5);
|
|
|
|
|
|
expect(INDYCAR_RACE_POINTS[27]).toBeUndefined();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("F1 pays 25 for a win and scores down to P10", () => {
|
|
|
|
|
|
expect(F1_RACE_POINTS[1]).toBe(25);
|
|
|
|
|
|
expect(F1_RACE_POINTS[10]).toBe(1);
|
|
|
|
|
|
expect(F1_RACE_POINTS[11]).toBeUndefined();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("both tables decrease monotonically so the points loop never truncates early", () => {
|
|
|
|
|
|
for (const table of [F1_RACE_POINTS, INDYCAR_RACE_POINTS]) {
|
|
|
|
|
|
const positions = Object.keys(table).map(Number).toSorted((a, b) => a - b);
|
|
|
|
|
|
// Contiguous from P1, no gaps — the award loop breaks at the first 0.
|
|
|
|
|
|
positions.forEach((pos, i) => expect(pos).toBe(i + 1));
|
|
|
|
|
|
for (let i = 1; i < positions.length; i++) {
|
|
|
|
|
|
expect(table[positions[i]]).toBeLessThanOrEqual(table[positions[i - 1]]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|