brackt/app/services/simulations/config-access.ts

44 lines
2.1 KiB
TypeScript
Raw Normal View History

Honor engine knobs across simulators, de-dupe odds, unify config UI 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
2026-06-30 22:00:33 +00:00
/**
* 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;
}