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
205 lines
11 KiB
TypeScript
205 lines
11 KiB
TypeScript
/**
|
||
* Simulator Registry
|
||
*
|
||
* Maps a sport's simulatorType to the appropriate Simulator implementation.
|
||
* Set simulatorType on the sport record in the admin UI.
|
||
* Add new simulators here as new sports are supported.
|
||
*/
|
||
|
||
import type { Simulator } from "./types";
|
||
import { BracketSimulator } from "./bracket-simulator";
|
||
import { AutoRacingSimulator } from "./auto-racing-simulator";
|
||
import { F1_RACE_POINTS, INDYCAR_RACE_POINTS } from "./race-points";
|
||
import { GolfSimulator } from "./golf-simulator";
|
||
import { UCLSimulator } from "./ucl-simulator";
|
||
import { NCAAMSimulator } from "./ncaam-simulator";
|
||
import { NCAAWSimulator } from "./ncaaw-simulator";
|
||
import { NBASimulator } from "./nba-simulator";
|
||
import { NHLSimulator } from "./nhl-simulator";
|
||
import { AFLSimulator } from "./afl-simulator";
|
||
import { EPLSimulator } from "./epl-simulator";
|
||
import { SnookerSimulator } from "./snooker-simulator";
|
||
import { TennisSimulator } from "./tennis-simulator";
|
||
import { DartsSimulator } from "./darts-simulator";
|
||
import { CSMajorSimulator } from "./cs-major-simulator";
|
||
import { MLBSimulator } from "./mlb-simulator";
|
||
import { NFLSimulator } from "./nfl-simulator";
|
||
import { WNBASimulator } from "./wnba-simulator";
|
||
import { WorldCupSimulator } from "./world-cup-simulator";
|
||
import { NCAAFootballSimulator } from "./ncaa-football-simulator";
|
||
import { LLWSSimulator } from "./llws-simulator";
|
||
import { CollegeHockeySimulator } from "./college-hockey-simulator";
|
||
import { BracktSimulator } from "./brackt-simulator";
|
||
import { NLLSimulator } from "./nll-simulator";
|
||
import { MLSSimulator } from "./mls-simulator";
|
||
|
||
export const SIMULATOR_TYPES = [
|
||
"f1_standings",
|
||
"indycar_standings",
|
||
"golf_qualifying_points",
|
||
"playoff_bracket",
|
||
"ucl_bracket",
|
||
"ncaam_bracket",
|
||
"ncaaw_bracket",
|
||
"nba_bracket",
|
||
"nhl_bracket",
|
||
"nfl_bracket",
|
||
"afl_bracket",
|
||
"epl_standings",
|
||
"snooker_bracket",
|
||
"tennis_qualifying_points",
|
||
"mlb_bracket",
|
||
"wnba_bracket",
|
||
"world_cup",
|
||
"darts_bracket",
|
||
"cs2_major_qualifying_points",
|
||
"ncaa_football_bracket",
|
||
"llws_bracket",
|
||
"college_hockey_bracket",
|
||
"brackt",
|
||
"nll_bracket",
|
||
"mls_bracket",
|
||
] as const;
|
||
|
||
export type SimulatorType = typeof SIMULATOR_TYPES[number];
|
||
|
||
export interface SimulatorInfo {
|
||
name: string;
|
||
description: string;
|
||
}
|
||
|
||
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 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 AutoRacingSimulator(INDYCAR_RACE_POINTS, "indycar_standings_model"),
|
||
},
|
||
golf_qualifying_points: {
|
||
info: { name: "Golf Qualifying Points Monte Carlo", description: "Simulates remaining majors using a Plackett-Luce model with SG: Total ratings. Awards QP by finishing position; ranks players by total QP across all 4 majors." },
|
||
create: () => new GolfSimulator(),
|
||
},
|
||
playoff_bracket: {
|
||
info: { name: "Bracket Monte Carlo", description: "Simulates playoff bracket outcomes using Elo ratings" },
|
||
create: () => new BracketSimulator(),
|
||
},
|
||
ucl_bracket: {
|
||
info: { name: "UCL Bracket Monte Carlo", description: "Simulates the UEFA Champions League 16-team knockout bracket using blended Elo + futures odds" },
|
||
create: () => new UCLSimulator(),
|
||
},
|
||
ncaam_bracket: {
|
||
info: { name: "NCAAM Bracket Monte Carlo", description: "Simulates preseason field selection or the NCAA Men's Basketball Tournament bracket using season-scoped KenPom-like ratings" },
|
||
create: () => new NCAAMSimulator(),
|
||
},
|
||
ncaaw_bracket: {
|
||
info: { name: "NCAAW Bracket Monte Carlo", description: "Simulates preseason field selection or the NCAA Women's Basketball Tournament bracket using season-scoped Barthag-like ratings" },
|
||
create: () => new NCAAWSimulator(),
|
||
},
|
||
nba_bracket: {
|
||
info: { name: "NBA Playoff Monte Carlo", description: "Simulates NBA playoff seedings (via seed probabilities) and full bracket (best-of-7 series) using Elo ratings" },
|
||
create: () => new NBASimulator(),
|
||
},
|
||
nhl_bracket: {
|
||
info: { name: "NHL Playoff Monte Carlo", description: "Simulates NHL playoff seedings (divisional format, via seed probabilities) and full bracket (best-of-7 series) using Elo ratings" },
|
||
create: () => new NHLSimulator(),
|
||
},
|
||
nfl_bracket: {
|
||
info: {
|
||
name: "NFL Season + Playoffs Monte Carlo",
|
||
description: "Projects NFL regular season standings (via nfelo Elo ratings) to determine the 14-team playoff field, then simulates the full bracket (Wild Card → Divisional → Conference Championship → Super Bowl). Elo ratings maintained via admin sourceElo field.",
|
||
},
|
||
create: () => new NFLSimulator(),
|
||
},
|
||
afl_bracket: {
|
||
info: { name: "AFL Season + Finals Monte Carlo", description: "Projects AFL regular season standings via Elo, then simulates the 10-team finals series (Wildcard → QF/EF → SF → PF → Grand Final). Reads current standings from DB; falls back to full-season projection pre-season." },
|
||
create: () => new AFLSimulator(),
|
||
},
|
||
epl_standings: {
|
||
info: { name: "EPL Season Standings Monte Carlo", description: "Projects the English Premier League table using current standings and Elo/projected-points inputs. Simulates draws correctly and returns top-eight placement probabilities." },
|
||
create: () => new EPLSimulator(),
|
||
},
|
||
snooker_bracket: {
|
||
info: { name: "Snooker World Championship Monte Carlo", description: "Simulates the 32-player World Championship bracket using frame-by-frame Bernoulli win probability and direct Elo ratings. Pre-bracket path simulates qualifying (ranks 17-48) and randomly draws qualifiers vs top 16 seeds." },
|
||
create: () => new SnookerSimulator(),
|
||
},
|
||
tennis_qualifying_points: {
|
||
info: { name: "Tennis Grand Slam Monte Carlo", description: "Simulates all 4 Grand Slam majors (128-player seeded bracket) using surface-specific Elo ratings. Accumulates qualifying points per round (tie-split applied) and ranks players by total QP across the season." },
|
||
create: () => new TennisSimulator(),
|
||
},
|
||
mlb_bracket: {
|
||
info: { name: "MLB Playoff Monte Carlo", description: "Simulates MLB division races + full playoff bracket (WC best-of-3, DS best-of-5, LCS/WS best-of-7) using Elo ratings calibrated from FanGraphs projected wins" },
|
||
create: () => new MLBSimulator(),
|
||
},
|
||
wnba_bracket: {
|
||
info: { name: "WNBA Playoff Monte Carlo", description: "Projects WNBA regular season seedings and simulates full playoff bracket (R1 best-of-3, Semis best-of-5, Finals best-of-7) using SRS-derived Elo ratings. SRS values sourced from basketball-reference.com." },
|
||
create: () => new WNBASimulator(),
|
||
},
|
||
world_cup: {
|
||
info: { name: "FIFA World Cup 2026 Monte Carlo", description: "Simulates the full 48-team World Cup: round-robin group stage (12 groups), best-8 third-place selection, R32→R16→QF→SF→3rd place game→Final. Uses blended Elo + futures odds." },
|
||
create: () => new WorldCupSimulator(),
|
||
},
|
||
darts_bracket: {
|
||
info: { name: "PDC World Darts Championship Monte Carlo", description: "Simulates the 128-player World Darts Championship bracket using set-level Bernoulli win probability and Elo ratings. Top 32 seeds placed in fixed positions; remaining 96 players randomly drawn per simulation." },
|
||
create: () => new DartsSimulator(),
|
||
},
|
||
cs2_major_qualifying_points: {
|
||
info: { name: "CS2 Major Qualifying Points Monte Carlo", description: "Simulates 2 CS2 Majors per season (3 Swiss stages: Opening Bo1, Elimination Bo1, Decider Bo3, then Champions Stage single-elimination). Awards QP by final placement; ranks teams by total QP across both majors. Stage 3 exits (placements 9–16) are sub-ranked by W-L record." },
|
||
create: () => new CSMajorSimulator(),
|
||
},
|
||
ncaa_football_bracket: {
|
||
info: { name: "NCAA Football CFP Monte Carlo", description: "Simulates the 12-team College Football Playoff bracket using Elo/FPI ratings entered via Admin → Elo Ratings. Optionally blends with championship futures odds (60% Elo / 40% odds). Seeds 1–4 receive first-round byes; First Round losers score 0 points." },
|
||
create: () => new NCAAFootballSimulator(),
|
||
},
|
||
llws_bracket: {
|
||
info: {
|
||
name: "LLWS Bracket Monte Carlo",
|
||
description: "Simulates the 20-team Little League World Series: a 10-team double-elimination bracket per side (US & International), each producing a side champion, then the consolation game (3rd/4th) and the World Championship (1st/2nd). Uses championship futures odds for all win probabilities. Set externalId to 'US' or 'Intl'.",
|
||
},
|
||
create: () => new LLWSSimulator(),
|
||
},
|
||
college_hockey_bracket: {
|
||
info: {
|
||
name: "Men's College Hockey Monte Carlo",
|
||
description: "Simulates the 16-team NCAA men's hockey tournament. Pre-bracket mode samples the field using admin-entered futures odds, NPI rank, and/or Elo; bracket mode honors the existing Frozen Four bracket and completed results.",
|
||
},
|
||
create: () => new CollegeHockeySimulator(),
|
||
},
|
||
brackt: {
|
||
info: {
|
||
name: "Brackt Harville Model",
|
||
description: "Projects league manager standings from drafted non-Brackt EVs and simulates final league placement probabilities.",
|
||
},
|
||
create: () => new BracktSimulator(),
|
||
},
|
||
nll_bracket: {
|
||
info: {
|
||
name: "NLL Season + Playoffs Monte Carlo",
|
||
description: "Projects NLL regular season standings via Elo (14 teams, 18 games) to determine the top-8 playoff field, then simulates the bracket (QF single-game; SF and Finals best-of-3). Reads current standings from DB; falls back to full-season projection pre-season.",
|
||
},
|
||
create: () => new NLLSimulator(),
|
||
},
|
||
mls_bracket: {
|
||
info: {
|
||
name: "MLS Season + Playoffs Monte Carlo",
|
||
description: "Projects MLS regular season standings per conference via Elo (30 teams, 34 games) to determine the 18-team playoff field (top 9 per conference), then simulates the full bracket: Wild Card (single game + PKs) → Round 1 (best-of-3) → Conference Semis (single game) → Conference Finals (single game) → MLS Cup.",
|
||
},
|
||
create: () => new MLSSimulator(),
|
||
},
|
||
};
|
||
|
||
export function getSimulator(simulatorType: SimulatorType): Simulator {
|
||
const entry = REGISTRY[simulatorType];
|
||
if (!entry) {
|
||
throw new Error(
|
||
`No simulator registered for type: "${simulatorType}". ` +
|
||
`Add a simulator to app/services/simulations/registry.ts.`
|
||
);
|
||
}
|
||
return entry.create();
|
||
}
|
||
|
||
export function getSimulatorInfo(simulatorType: SimulatorType): SimulatorInfo | null {
|
||
return REGISTRY[simulatorType]?.info ?? null;
|
||
}
|