Consolidate F1/IndyCar simulators into shared AutoRacingSimulator class (#151)

Replaces the duplicated F1Simulator (which was copy-pasted for IndyCar) with a
single AutoRacingSimulator that accepts a race points table at construction time.
Registry entries for f1_standings and indycar_standings remain separate, each
wired to the correct scoring system (F1: top-10, IndyCar: top-26 positions).

Also imports SIMULATOR_TYPES from registry into the admin route to eliminate the
duplicate hardcoded array, and updates stale comments mentioning F1/IndyCar.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Chris Parsons 2026-03-16 12:22:08 -07:00 committed by GitHub
parent 7f9b564a5d
commit 84787c9a4b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 51 additions and 30 deletions

View file

@ -35,7 +35,7 @@ export function formatEventDate(dateStr: string | null | undefined): string | nu
* Produces a stable sort key for upcoming calendar events.
*
* Bracket events carry a full ISO game timestamp in `earliestGameTime`; all-compete
* events (F1, IndyCar, golf) use the ISO timestamp in `earliestGameTime` when a start
* events (auto racing, golf) use the ISO timestamp in `earliestGameTime` when a start
* time has been set, or fall back to the date-only `eventDate` string. Events with
* neither sort to the far future so they appear last.
*

View file

@ -636,7 +636,7 @@ export async function finalizeQualifyingPoints(
}
/**
* Process season standings (F1/IndyCar) and assign final placements.
* Process season standings (auto racing: F1, IndyCar, etc.) and assign final placements.
* Reads from participant_season_results (championship points) and converts
* final standings positions to fantasy placements. Ties share placements.
*/

View file

@ -22,6 +22,7 @@ import {
SelectTrigger,
SelectValue,
} from "~/components/ui/select";
import { SIMULATOR_TYPES } from "~/services/simulations/registry";
export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors {
return [{ title: `${data?.sport?.name ?? "Sport"} - Brackt Admin` }];
@ -101,10 +102,9 @@ export async function action({ request, params }: Route.ActionArgs) {
iconUrl = null;
}
const validSimulatorTypes = ["f1_standings", "indycar_standings", "golf_qualifying_points", "playoff_bracket", "ucl_bracket", "ncaam_bracket", "ncaaw_bracket"] as const;
type ValidSimulatorType = typeof validSimulatorTypes[number];
type ValidSimulatorType = typeof SIMULATOR_TYPES[number];
const parsedSimulatorType: ValidSimulatorType | null =
typeof simulatorType === "string" && validSimulatorTypes.includes(simulatorType as ValidSimulatorType)
typeof simulatorType === "string" && (SIMULATOR_TYPES as readonly string[]).includes(simulatorType)
? (simulatorType as ValidSimulatorType)
: null;

View file

@ -1,5 +1,9 @@
/**
* F1 / Season Standings Simulator
* Auto Racing Season Standings Simulator
*
* Generic simulator for points-based auto racing championships (F1, IndyCar, etc.).
* The race points table is injected at construction time so different series can
* use their own scoring systems.
*
* Algorithm:
* 1. Load participants + current championship points from DB
@ -8,7 +12,7 @@
* 4. Two simulation paths:
* a. remainingRaces === 0 (pre-season): pure weighted draws from odds
* b. remainingRaces > 0 (in-season): simulate each remaining race,
* starting from real standings, awarding F1 race points per finish
* starting from real standings, awarding series-specific points per finish
* 5. Convert finish counts probability distributions + normalize columns
*
* Notes:
@ -44,14 +48,9 @@ const PARTICIPANT_VOLATILITY = 1.5;
*/
const UNCERTAINTY_FACTOR = 0.0;
// ─── F1 race points for finishing positions 110 ──────────────────────────────
const RACE_POINTS: Record<number, number> = {
1: 25, 2: 18, 3: 15, 4: 12, 5: 10, 6: 8, 7: 6, 8: 4, 9: 2, 10: 1,
};
function getRacePoints(position: number): number {
return RACE_POINTS[position] ?? 0;
/** Lookup points for a finishing position; returns 0 for unscored positions. */
function getRacePoints(racePoints: Record<number, number>, position: number): number {
return racePoints[position] ?? 0;
}
// ─── Odds helpers ─────────────────────────────────────────────────────────────
@ -94,7 +93,12 @@ function weightedDrawWithoutReplacement(ids: string[], weights: number[]): strin
// ─── Simulator ────────────────────────────────────────────────────────────────
export class F1Simulator implements Simulator {
export class AutoRacingSimulator implements Simulator {
constructor(
private readonly racePoints: Record<number, number>,
private readonly source: string,
) {}
async simulate(sportsSeasonId: string): Promise<SimulationResult[]> {
const db = database();
@ -203,8 +207,8 @@ export class F1Simulator implements Simulator {
const finishOrder = weightedDrawWithoutReplacement(ids, raceWeights);
for (let pos = 0; pos < finishOrder.length; pos++) {
const pts = getRacePoints(pos + 1);
if (pts === 0) break; // positions 11+ earn no F1 points
const pts = getRacePoints(this.racePoints, pos + 1);
if (pts === 0) break; // unscored positions earn no points
simPoints.set(finishOrder[pos], (simPoints.get(finishOrder[pos]) ?? 0) + pts);
}
}
@ -233,7 +237,7 @@ export class F1Simulator implements Simulator {
probSeventh: rankCounts.get(p.id)![6] / NUM_SIMULATIONS,
probEighth: rankCounts.get(p.id)![7] / NUM_SIMULATIONS,
},
source: "f1_standings_model",
source: this.source,
}));
// 9. Per-position normalization: each column should sum to exactly 1.0 but

View file

@ -8,20 +8,37 @@
import type { Simulator } from "./types";
import { BracketSimulator } from "./bracket-simulator";
import { F1Simulator } from "./f1-simulator";
import { AutoRacingSimulator } from "./auto-racing-simulator";
import { GolfSimulator } from "./golf-simulator";
import { UCLSimulator } from "./ucl-simulator";
import { NCAAMSimulator } from "./ncaam-simulator";
import { NCAAWSimulator } from "./ncaaw-simulator";
export type SimulatorType =
| "f1_standings"
| "indycar_standings"
| "golf_qualifying_points"
| "playoff_bracket"
| "ucl_bracket"
| "ncaam_bracket"
| "ncaaw_bracket";
export const SIMULATOR_TYPES = [
"f1_standings",
"indycar_standings",
"golf_qualifying_points",
"playoff_bracket",
"ucl_bracket",
"ncaam_bracket",
"ncaaw_bracket",
] as const;
export type SimulatorType = typeof SIMULATOR_TYPES[number];
// ─── Race points tables ───────────────────────────────────────────────────────
/** F1 points: positions 110. */
const F1_RACE_POINTS: Record<number, number> = {
1: 25, 2: 18, 3: 15, 4: 12, 5: 10, 6: 8, 7: 6, 8: 4, 9: 2, 10: 1,
};
/** IndyCar standard race points: positions 126. */
const INDYCAR_RACE_POINTS: Record<number, number> = {
1: 50, 2: 40, 3: 35, 4: 32, 5: 30, 6: 28, 7: 26, 8: 24, 9: 22, 10: 20,
11: 19, 12: 18, 13: 17, 14: 16, 15: 15, 16: 14, 17: 13, 18: 12, 19: 11, 20: 10,
21: 9, 22: 8, 23: 7, 24: 6, 25: 5, 26: 5,
};
export interface SimulatorInfo {
name: string;
@ -31,11 +48,11 @@ export interface SimulatorInfo {
const REGISTRY: Record<SimulatorType, { info: SimulatorInfo; create: () => Simulator }> = {
f1_standings: {
info: { name: "F1 Standings Model", description: "Simulates remaining races using current F1 standings and futures odds" },
create: () => new F1Simulator(),
create: () => new AutoRacingSimulator(F1_RACE_POINTS, "f1_standings_model"),
},
indycar_standings: {
info: { name: "IndyCar Standings Model", description: "Simulates remaining races using current IndyCar standings and futures odds" },
create: () => new F1Simulator(),
create: () => new AutoRacingSimulator(INDYCAR_RACE_POINTS, "indycar_standings_model"),
},
golf_qualifying_points: {
info: { name: "Qualifying Points Model", description: "Simulates remaining majors using current QP standings and futures odds" },