brackt/app/services/simulations/config-access.ts
Claude 4142b21c55
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

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;
}