brackt/app/services/simulations/registry.ts
Chris Parsons 6f5ea35952
Add NHL playoff simulator and fix simulator dropdown (#166)
Closes #115

- Add `NHLSimulator` using NHL's divisional bracket format (top 3 per
  division + 2 wildcards per conference, best-of-7 series, parity
  factor 800). Elo ratings and seeding probabilities sourced from
  elo.harvitronix.com/nhl/2025-2026 as of March 18, 2026.
- Add `nhl_bracket` to `simulatorTypeEnum` with migration.
- Register `NHLSimulator` in the simulator registry.
- Fix simulator type dropdown in admin to auto-generate from the
  registry instead of a hardcoded list, so new simulators appear
  automatically without a separate UI change.

Code review fixes applied to the simulator:
- Narrow `weightedPick` key type to the five seeding probability fields
  (was `keyof NhlTeamData`, allowing `"elo"` which would silently
  corrupt results)
- Remove uniform-random fallback in `weightedPick` — zero-weight draws
  now return `undefined` and are skipped, preventing eliminated teams
  from becoming wildcards
- Exclude unrecognized participants entirely (with a console warning)
  instead of silently routing them into the Eastern wildcard pool
- Replace the confusing for-of loop over both conferences (with unused
  `champ` variable and `!`-suppressed unitialized vars) with two
  explicit sequential East/West blocks
- Track `effectiveN` for skipped degenerate draws so probability
  denominators stay accurate; throw if all iterations are skipped

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 01:23:53 -07:00

105 lines
4.2 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 { 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";
export const SIMULATOR_TYPES = [
"f1_standings",
"indycar_standings",
"golf_qualifying_points",
"playoff_bracket",
"ucl_bracket",
"ncaam_bracket",
"ncaaw_bracket",
"nba_bracket",
"nhl_bracket",
] as const;
export type SimulatorType = typeof SIMULATOR_TYPES[number];
// ─── Race points tables ───────────────────────────────────────────────────────
/** F1 points: positions 110. */
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 126. */
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;
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: "Qualifying Points Model", description: "Simulates remaining majors using current QP standings and futures odds" },
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 the NCAA Men's Basketball Tournament 64-team bracket using KenPom net ratings" },
create: () => new NCAAMSimulator(),
},
ncaaw_bracket: {
info: { name: "NCAAW Bracket Monte Carlo", description: "Simulates the NCAA Women's Basketball Tournament 64-team bracket using Barttorvik Barthag 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(),
},
};
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;
}