brackt/app/services/simulations/registry.ts
Claude adceda3aca
Fix auto racing sims ignoring championship standings
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
2026-08-17 00:36:20 +00:00

205 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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 916) 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 14 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;
}