diff --git a/app/lib/date-utils.ts b/app/lib/date-utils.ts index 48b9267..3aeddcf 100644 --- a/app/lib/date-utils.ts +++ b/app/lib/date-utils.ts @@ -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. * diff --git a/app/models/scoring-calculator.ts b/app/models/scoring-calculator.ts index 7d2eaf1..2e74fd1 100644 --- a/app/models/scoring-calculator.ts +++ b/app/models/scoring-calculator.ts @@ -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. */ diff --git a/app/routes/admin.sports.$id.tsx b/app/routes/admin.sports.$id.tsx index a68392b..b68416a 100644 --- a/app/routes/admin.sports.$id.tsx +++ b/app/routes/admin.sports.$id.tsx @@ -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; diff --git a/app/services/simulations/f1-simulator.ts b/app/services/simulations/auto-racing-simulator.ts similarity index 92% rename from app/services/simulations/f1-simulator.ts rename to app/services/simulations/auto-racing-simulator.ts index 70763b2..da7a1fe 100644 --- a/app/services/simulations/f1-simulator.ts +++ b/app/services/simulations/auto-racing-simulator.ts @@ -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 = { - 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, 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, + private readonly source: string, + ) {} + async simulate(sportsSeasonId: string): Promise { 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 diff --git a/app/services/simulations/registry.ts b/app/services/simulations/registry.ts index 3ea29a7..3c74abf 100644 --- a/app/services/simulations/registry.ts +++ b/app/services/simulations/registry.ts @@ -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 = { + 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 = { + 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 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" },