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:
parent
7f9b564a5d
commit
84787c9a4b
5 changed files with 51 additions and 30 deletions
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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 1–10 ──────────────────────────────
|
||||
|
||||
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
|
||||
|
|
@ -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 1–10. */
|
||||
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 1–26. */
|
||||
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" },
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue