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.
|
* Produces a stable sort key for upcoming calendar events.
|
||||||
*
|
*
|
||||||
* Bracket events carry a full ISO game timestamp in `earliestGameTime`; all-compete
|
* 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
|
* 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.
|
* 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
|
* Reads from participant_season_results (championship points) and converts
|
||||||
* final standings positions to fantasy placements. Ties share placements.
|
* final standings positions to fantasy placements. Ties share placements.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import {
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "~/components/ui/select";
|
} from "~/components/ui/select";
|
||||||
|
import { SIMULATOR_TYPES } from "~/services/simulations/registry";
|
||||||
|
|
||||||
export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors {
|
export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors {
|
||||||
return [{ title: `${data?.sport?.name ?? "Sport"} - Brackt Admin` }];
|
return [{ title: `${data?.sport?.name ?? "Sport"} - Brackt Admin` }];
|
||||||
|
|
@ -101,10 +102,9 @@ export async function action({ request, params }: Route.ActionArgs) {
|
||||||
iconUrl = null;
|
iconUrl = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const validSimulatorTypes = ["f1_standings", "indycar_standings", "golf_qualifying_points", "playoff_bracket", "ucl_bracket", "ncaam_bracket", "ncaaw_bracket"] as const;
|
type ValidSimulatorType = typeof SIMULATOR_TYPES[number];
|
||||||
type ValidSimulatorType = typeof validSimulatorTypes[number];
|
|
||||||
const parsedSimulatorType: ValidSimulatorType | null =
|
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)
|
? (simulatorType as ValidSimulatorType)
|
||||||
: null;
|
: 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:
|
* Algorithm:
|
||||||
* 1. Load participants + current championship points from DB
|
* 1. Load participants + current championship points from DB
|
||||||
|
|
@ -8,7 +12,7 @@
|
||||||
* 4. Two simulation paths:
|
* 4. Two simulation paths:
|
||||||
* a. remainingRaces === 0 (pre-season): pure weighted draws from odds
|
* a. remainingRaces === 0 (pre-season): pure weighted draws from odds
|
||||||
* b. remainingRaces > 0 (in-season): simulate each remaining race,
|
* 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
|
* 5. Convert finish counts → probability distributions + normalize columns
|
||||||
*
|
*
|
||||||
* Notes:
|
* Notes:
|
||||||
|
|
@ -44,14 +48,9 @@ const PARTICIPANT_VOLATILITY = 1.5;
|
||||||
*/
|
*/
|
||||||
const UNCERTAINTY_FACTOR = 0.0;
|
const UNCERTAINTY_FACTOR = 0.0;
|
||||||
|
|
||||||
// ─── F1 race points for finishing positions 1–10 ──────────────────────────────
|
/** Lookup points for a finishing position; returns 0 for unscored positions. */
|
||||||
|
function getRacePoints(racePoints: Record<number, number>, position: number): number {
|
||||||
const RACE_POINTS: Record<number, number> = {
|
return racePoints[position] ?? 0;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Odds helpers ─────────────────────────────────────────────────────────────
|
// ─── Odds helpers ─────────────────────────────────────────────────────────────
|
||||||
|
|
@ -94,7 +93,12 @@ function weightedDrawWithoutReplacement(ids: string[], weights: number[]): strin
|
||||||
|
|
||||||
// ─── Simulator ────────────────────────────────────────────────────────────────
|
// ─── 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[]> {
|
async simulate(sportsSeasonId: string): Promise<SimulationResult[]> {
|
||||||
const db = database();
|
const db = database();
|
||||||
|
|
||||||
|
|
@ -203,8 +207,8 @@ export class F1Simulator implements Simulator {
|
||||||
const finishOrder = weightedDrawWithoutReplacement(ids, raceWeights);
|
const finishOrder = weightedDrawWithoutReplacement(ids, raceWeights);
|
||||||
|
|
||||||
for (let pos = 0; pos < finishOrder.length; pos++) {
|
for (let pos = 0; pos < finishOrder.length; pos++) {
|
||||||
const pts = getRacePoints(pos + 1);
|
const pts = getRacePoints(this.racePoints, pos + 1);
|
||||||
if (pts === 0) break; // positions 11+ earn no F1 points
|
if (pts === 0) break; // unscored positions earn no points
|
||||||
simPoints.set(finishOrder[pos], (simPoints.get(finishOrder[pos]) ?? 0) + pts);
|
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,
|
probSeventh: rankCounts.get(p.id)![6] / NUM_SIMULATIONS,
|
||||||
probEighth: rankCounts.get(p.id)![7] / 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
|
// 9. Per-position normalization: each column should sum to exactly 1.0 but
|
||||||
|
|
@ -8,20 +8,37 @@
|
||||||
|
|
||||||
import type { Simulator } from "./types";
|
import type { Simulator } from "./types";
|
||||||
import { BracketSimulator } from "./bracket-simulator";
|
import { BracketSimulator } from "./bracket-simulator";
|
||||||
import { F1Simulator } from "./f1-simulator";
|
import { AutoRacingSimulator } from "./auto-racing-simulator";
|
||||||
import { GolfSimulator } from "./golf-simulator";
|
import { GolfSimulator } from "./golf-simulator";
|
||||||
import { UCLSimulator } from "./ucl-simulator";
|
import { UCLSimulator } from "./ucl-simulator";
|
||||||
import { NCAAMSimulator } from "./ncaam-simulator";
|
import { NCAAMSimulator } from "./ncaam-simulator";
|
||||||
import { NCAAWSimulator } from "./ncaaw-simulator";
|
import { NCAAWSimulator } from "./ncaaw-simulator";
|
||||||
|
|
||||||
export type SimulatorType =
|
export const SIMULATOR_TYPES = [
|
||||||
| "f1_standings"
|
"f1_standings",
|
||||||
| "indycar_standings"
|
"indycar_standings",
|
||||||
| "golf_qualifying_points"
|
"golf_qualifying_points",
|
||||||
| "playoff_bracket"
|
"playoff_bracket",
|
||||||
| "ucl_bracket"
|
"ucl_bracket",
|
||||||
| "ncaam_bracket"
|
"ncaam_bracket",
|
||||||
| "ncaaw_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 {
|
export interface SimulatorInfo {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -31,11 +48,11 @@ export interface SimulatorInfo {
|
||||||
const REGISTRY: Record<SimulatorType, { info: SimulatorInfo; create: () => Simulator }> = {
|
const REGISTRY: Record<SimulatorType, { info: SimulatorInfo; create: () => Simulator }> = {
|
||||||
f1_standings: {
|
f1_standings: {
|
||||||
info: { name: "F1 Standings Model", description: "Simulates remaining races using current F1 standings and futures odds" },
|
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: {
|
indycar_standings: {
|
||||||
info: { name: "IndyCar Standings Model", description: "Simulates remaining races using current IndyCar standings and futures odds" },
|
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: {
|
golf_qualifying_points: {
|
||||||
info: { name: "Qualifying Points Model", description: "Simulates remaining majors using current QP standings and futures odds" },
|
info: { name: "Qualifying Points Model", description: "Simulates remaining majors using current QP standings and futures odds" },
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue