Two problems addressed:
1. Favorites' P(1st) was too sharp (e.g. NHL top teams ~20% vs ~12% implied).
- The NHL simulator hardcoded its parity factor (1000) and ignored the
season config's parityFactor, so the knob meant to flatten the
distribution did nothing. It also re-blended raw futures odds into every
game on top of the odds->Elo conversion, double-counting the same signal.
- NHL now reads parityFactor/iterations/seasonGames/overtimeRate from config
and no longer re-blends odds per game (odds enter once, via the central
odds->Elo resolver). Honoring parity 2500 flattens a top team from ~29% to
~13% title odds.
2. "Season Config" and "Input Policy" were two forms over the same stored
object that didn't reflect each other, and the engine-knob half was inert
for many simulators.
- Every simulator now reads its engine knobs (iterations everywhere;
parityFactor for all Elo-based sims) from the merged config, passed in by
the runner via the Simulator interface. Defaults equal the former
hardcoded constants, so behavior is unchanged unless a season overrides.
- The admin simulator page is now a single "Simulator Configuration" card
with structured Engine and Input-derivation sections (profile-driven, so
each sport shows only the knobs it honors) plus an Advanced raw-JSON
escape hatch — all writing the same config.
Also: centralized the duplicated configNumber helpers into config-access.ts;
the central odds->Elo resolver now maps onto the configured Elo floor/ceiling
so those bounds set the odds-derived spread (a real flattening dial).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PAFMogMkFJf52YpHyCDvuf
43 lines
2.1 KiB
TypeScript
43 lines
2.1 KiB
TypeScript
/**
|
|
* Shared accessors for reading numeric knobs out of a merged simulator config
|
|
* (`sports_season_simulator_configs.config` merged over the profile defaults).
|
|
*
|
|
* These were previously duplicated inside individual simulators (EPL, MLS,
|
|
* NCAA-M). Centralizing them keeps every simulator reading engine knobs the same
|
|
* way, so values set in the unified config screen reliably drive the math.
|
|
*
|
|
* Two zero-handling variants exist deliberately:
|
|
* - `configNumber` accepts 0 (`>= 0`) — for knobs where 0 is meaningful
|
|
* (e.g. a draw decay or blend weight of 0).
|
|
* - `positiveConfigNumber` requires `> 0` — for counts/divisors where 0 is
|
|
* nonsensical and should fall back to the default.
|
|
*/
|
|
|
|
type Config = Record<string, unknown> | undefined;
|
|
|
|
/** Read a finite, non-negative number from config, else the fallback. */
|
|
export function configNumber(config: Config, key: string, fallback: number): number {
|
|
const value = config?.[key];
|
|
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
}
|
|
|
|
/** Read a finite, strictly-positive number from config, else the fallback. */
|
|
export function positiveConfigNumber(config: Config, key: string, fallback: number): number {
|
|
const value = config?.[key];
|
|
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
}
|
|
|
|
/**
|
|
* Read a required finite, strictly-positive number from config. Throws when the
|
|
* key is missing or non-positive — use only for knobs a profile always seeds.
|
|
*/
|
|
export function requiredConfigNumber(config: Config, key: string, label = "simulator"): number {
|
|
const value = config?.[key];
|
|
if (typeof value === "number" && Number.isFinite(value) && value > 0) return value;
|
|
throw new Error(`${label} config is missing positive numeric ${key}.`);
|
|
}
|
|
|
|
/** Coerce a raw value (already extracted from config) to a positive number. */
|
|
export function optionalPositiveNumber(value: unknown, fallback: number): number {
|
|
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
}
|