brackt/app/services/simulations/simulator-config.ts
Chris Parsons 7fa88fc0ef
Add projected-wins input mode to admin Elo Ratings page (#306)
* Add projected-wins input mode to admin Elo Ratings page

Admins can now enter projected season win totals instead of raw Elo
numbers on the Elo Ratings page. Wins are auto-converted to Elo using
the inverse formula and stored as sourceElo, keeping the rest of the
simulation pipeline unchanged.

- Add `projectedWinsToElo` / `eloToProjectedWins` to probability-engine
- Add `simulator-config.ts` centralising per-sport season length, parity
  factor, and average opponent Elo (AFL, NFL, NBA, NHL, MLB, WNBA)
- Admin Elo Ratings page: toggle between Elo and Projected Wins input
  modes; bulk import parses wins format; existing sourceElo back-fills
  the wins field on load
- AFL simulator now reads sourceElo from participantExpectedValues first,
  falling back to hardcoded TEAMS_DATA then 1400

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix lint errors in simulator-config tests and probability-engine

- Replace non-null assertions (`!`) with optional chaining (`?.`) in simulator-config tests
- Remove redundant type annotations on default parameters in probability-engine

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 12:40:08 -07:00

89 lines
2.6 KiB
TypeScript

/**
* Simulator Configuration
*
* Per-sport parameters used by simulators and admin UI tools.
* Centralizes season length, parity factors, and other sport-specific
* constants so they can be shared between simulators and the Elo Ratings
* admin page (e.g., for projected wins → Elo conversion).
*/
import type { SimulatorType } from "./registry";
export interface SimulatorConfig {
/** Total regular season games per team in this sport. */
seasonGames: number;
/** Elo parity factor for single-game win probability.
* P(A) = 1 / (1 + 10^((eloB - eloA) / parityFactor))
* Higher = more unpredictable per game. */
parityFactor: number;
/** Average opponent Elo — used for season projections against a generic opponent. */
averageOpponentElo: number;
}
const CONFIG: Record<SimulatorType, SimulatorConfig | null> = {
afl_bracket: {
seasonGames: 23,
parityFactor: 450,
averageOpponentElo: 1500,
},
nfl_bracket: {
seasonGames: 17,
parityFactor: 400,
averageOpponentElo: 1500,
},
nba_bracket: {
seasonGames: 82,
parityFactor: 400,
averageOpponentElo: 1500,
},
nhl_bracket: {
seasonGames: 82,
// NHL is the highest-parity major North American league — roughly 1 in 3 games
// goes to overtime/shootout and favourites win far less reliably than in NBA/NFL.
// 1000 was calibrated so that a team projected at 55 wins (67%) maps to ~1570 Elo,
// which keeps the spread tight and reflects the empirical upset rate.
parityFactor: 1000,
averageOpponentElo: 1500,
},
mlb_bracket: {
seasonGames: 162,
parityFactor: 400,
averageOpponentElo: 1500,
},
wnba_bracket: {
seasonGames: 44,
parityFactor: 400,
averageOpponentElo: 1500,
},
// Simulators below don't use projected-wins → Elo conversion.
// They can be populated later if needed.
f1_standings: null,
indycar_standings: null,
golf_qualifying_points: null,
playoff_bracket: null,
ucl_bracket: null,
ncaam_bracket: null,
ncaaw_bracket: null,
snooker_bracket: null,
tennis_qualifying_points: null,
world_cup: null,
darts_bracket: null,
cs2_major_qualifying_points: null,
ncaa_football_bracket: null,
llws_bracket: null,
};
/**
* Get the simulator config for a given simulator type.
* Returns null for simulator types that don't have season-game / Elo config.
*/
export function getSimulatorConfig(simulatorType: SimulatorType): SimulatorConfig | null {
return CONFIG[simulatorType];
}
/**
* Check if a simulator type supports projected-wins → Elo conversion.
*/
export function supportsProjectedWins(simulatorType: SimulatorType): boolean {
return CONFIG[simulatorType] !== null;
}