claude/clever-archimedes-dvye42 #110

Merged
chrisp merged 4 commits from claude/clever-archimedes-dvye42 into main 2026-06-26 05:16:54 +00:00
3 changed files with 65 additions and 77 deletions
Showing only changes of commit a77485d5a5 - Show all commits

View file

@ -280,9 +280,10 @@ describe("simulator input policy", () => {
expect(defaults.baseEloPriority).toEqual(DEFAULT_BASE_ELO_PRIORITY);
expect(defaults.oddsWeight).toBe(0.3);
// oddsWeight can be sourced from the top-level config or the input policy,
// and is clamped to [0, 1]; junk base-priority entries are dropped.
expect(getSimulatorInputPolicy({ oddsWeight: 0.6 }).oddsWeight).toBe(0.6);
// oddsWeight lives under inputPolicy and is clamped to [0, 1]; a top-level
// config.oddsWeight is ignored (single home); junk base-priority entries drop.
expect(getSimulatorInputPolicy({ inputPolicy: { oddsWeight: 0.6 } }).oddsWeight).toBe(0.6);
expect(getSimulatorInputPolicy({ oddsWeight: 0.6 }).oddsWeight).toBe(0.3);
expect(getSimulatorInputPolicy({ inputPolicy: { oddsWeight: 5 } }).oddsWeight).toBe(1);
expect(getSimulatorInputPolicy({ inputPolicy: { oddsWeight: -2 } }).oddsWeight).toBe(0);
expect(

View file

@ -142,13 +142,8 @@ export function getSimulatorInputPolicy(config: Record<string, unknown>): Simula
? ratingStrategy
: "block",
baseEloPriority: parseBaseEloPriority(rawPolicy.baseEloPriority, DEFAULT_POLICY.baseEloPriority),
// Allow oddsWeight to live either in inputPolicy (the unified knob) or at the
// top level of config (legacy per-profile defaultConfig), preferring the policy.
oddsWeight: clamp(
optionalNumber(rawPolicy.oddsWeight, optionalNumber(config.oddsWeight, DEFAULT_POLICY.oddsWeight)),
0,
1
),
// The futures blend weight lives only under inputPolicy (single home).
oddsWeight: clamp(optionalNumber(rawPolicy.oddsWeight, DEFAULT_POLICY.oddsWeight), 0, 1),
fallbackElo: optionalNumber(rawPolicy.fallbackElo, DEFAULT_POLICY.fallbackElo),
fallbackEloDelta: optionalNumber(rawPolicy.fallbackEloDelta, DEFAULT_POLICY.fallbackEloDelta),
eloMin: optionalNumber(rawPolicy.eloMin, DEFAULT_POLICY.eloMin),
@ -173,6 +168,32 @@ export function ratingRequirementLabel(profile: SimulatorManifestProfile): strin
return ["rating", ...alternatives.map(simulatorInputLabel)].join(" / ");
}
/**
* Combine a participant's base strength value and its odds-derived value into the
* single value that feeds the simulator, by `oddsWeight`: 0 = base only,
* 1 = odds only, in between = weighted blend. Odds-derived and blended results
* pass through `clampDerived` (they are mapped onto the configured band by
* design); a base value is returned untouched (a directly entered Elo/rating is
* trusted as-is, a projection-derived Elo was already clamped upstream). Returns
* undefined when neither source is present (the caller falls back to its
* missing-input strategy). Shared by Elo and rating resolution.
*/
function blendBaseAndOdds<M extends string>(
base: { value: number; method: M } | undefined,
odds: number | undefined,
oddsWeight: number,
clampDerived: (value: number) => number
): { value: number; method: M | "sourceOdds" | "blend" } | undefined {
if (base !== undefined && odds !== undefined) {
if (oddsWeight <= 0) return { value: base.value, method: base.method };
if (oddsWeight >= 1) return { value: clampDerived(odds), method: "sourceOdds" };
return { value: clampDerived((1 - oddsWeight) * base.value + oddsWeight * odds), method: "blend" };
}
if (base !== undefined) return { value: base.value, method: base.method };
if (odds !== undefined) return { value: clampDerived(odds), method: "sourceOdds" };
return undefined;
}
export function resolveSourceElos(
inputs: ParticipantInputForPolicy[],
profile: Pick<SimulatorManifestProfile, "derivableInputs">,
@ -240,42 +261,23 @@ export function resolveSourceElos(
}
// 3. Blend base + odds into the single Elo that feeds the simulator.
// Only odds-derived / blended / fallback values are clamped+rounded onto the
// [eloMin, eloMax] band (they are mapped onto it by design). A base Elo is
// passed through untouched: a directly entered Elo is trusted as-is (some
// sports use ratings above the default ceiling, e.g. snooker ~2450) and a
// projection-derived Elo was already clamped by projectionToElo.
const w = policy.oddsWeight;
// Directly entered Elos (some sports use ratings above the default ceiling,
// e.g. snooker ~2450) pass through unclamped; only derived values are
// clamped onto [eloMin, eloMax].
const clampDerived = (elo: number): number => clamp(Math.round(elo), policy.eloMin, policy.eloMax);
for (const input of inputs) {
const base = baseElo.get(input.participantId);
const odds = oddsElo.get(input.participantId);
let sourceElo: number;
let method: ResolvedSourceElo["method"];
if (base !== undefined && odds !== undefined) {
if (w <= 0) {
sourceElo = base.elo;
method = base.method;
} else if (w >= 1) {
sourceElo = clampDerived(odds);
method = "sourceOdds";
} else {
sourceElo = clampDerived((1 - w) * base.elo + w * odds);
method = "blend";
}
} else if (base !== undefined) {
sourceElo = base.elo;
method = base.method;
} else if (odds !== undefined) {
sourceElo = clampDerived(odds);
method = "sourceOdds";
} else {
continue;
}
const blended = blendBaseAndOdds(
base ? { value: base.elo, method: base.method } : undefined,
oddsElo.get(input.participantId),
policy.oddsWeight,
clampDerived
);
if (!blended) continue;
resolved.set(input.participantId, {
participantId: input.participantId,
sourceElo,
method,
sourceElo: blended.value,
method: blended.method,
});
}
@ -349,39 +351,22 @@ export function resolveRatings(
}
}
// As with Elo: only odds-derived / blended values are clamped onto the rating
// band; a directly entered rating is trusted as-is (matching prior behavior).
const w = policy.oddsWeight;
// As with Elo: a directly entered rating passes through untouched; only
// derived values are clamped onto the rating band.
const clampDerived = (rating: number): number => clamp(rating, policy.ratingMin, policy.ratingMax);
for (const input of inputs) {
const base = baseRating.get(input.participantId);
const odds = oddsRating.get(input.participantId);
let rating: number;
let method: ResolvedRating["method"];
if (base !== undefined && odds !== undefined) {
if (w <= 0) {
rating = base;
method = "direct";
} else if (w >= 1) {
rating = clampDerived(odds);
method = "sourceOdds";
} else {
rating = clampDerived((1 - w) * base + w * odds);
method = "blend";
}
} else if (base !== undefined) {
rating = base;
method = "direct";
} else if (odds !== undefined) {
rating = clampDerived(odds);
method = "sourceOdds";
} else {
continue;
}
const blended = blendBaseAndOdds(
base !== undefined ? { value: base, method: "direct" as const } : undefined,
oddsRating.get(input.participantId),
policy.oddsWeight,
clampDerived
);
if (!blended) continue;
resolved.set(input.participantId, {
participantId: input.participantId,
rating,
method,
rating: blended.value,
method: blended.method,
});
}

View file

@ -66,7 +66,7 @@ const PROFILES: Record<SimulatorType, Omit<SimulatorManifestProfile, "simulatorT
setupSections: ["participants", "futuresOdds", "bracket"],
},
ucl_bracket: {
defaultConfig: { ...BASE_CONFIG, oddsWeight: 0.3 },
defaultConfig: { ...BASE_CONFIG, inputPolicy: { oddsWeight: 0.3 } },
requiredInputs: ["sourceElo"],
optionalInputs: ["sourceOdds"],
derivableInputs: { sourceElo: ["sourceOdds"] },
@ -143,7 +143,7 @@ const PROFILES: Record<SimulatorType, Omit<SimulatorManifestProfile, "simulatorT
setupSections: ["participants", "surfaceElo", "events"],
},
mlb_bracket: {
defaultConfig: { ...BASE_CONFIG, rDiffWeight: 0.7, oddsWeight: 0.3, seasonGames: 162 },
defaultConfig: { ...BASE_CONFIG, seasonGames: 162, inputPolicy: { oddsWeight: 0.3 } },
requiredInputs: ["sourceElo"],
optionalInputs: ["sourceOdds", "projectedWins"],
derivableInputs: { sourceElo: ["projectedWins", "sourceOdds"] },
@ -157,7 +157,7 @@ const PROFILES: Record<SimulatorType, Omit<SimulatorManifestProfile, "simulatorT
setupSections: ["participants", "eloRatings", "regularStandings"],
},
world_cup: {
defaultConfig: { ...BASE_CONFIG, iterations: 10_000, oddsWeight: 0.3 },
defaultConfig: { ...BASE_CONFIG, iterations: 10_000, inputPolicy: { oddsWeight: 0.3 } },
requiredInputs: ["sourceElo"],
optionalInputs: ["sourceOdds", "worldRanking"],
derivableInputs: { sourceElo: ["sourceOdds"] },
@ -176,7 +176,7 @@ const PROFILES: Record<SimulatorType, Omit<SimulatorManifestProfile, "simulatorT
setupSections: ["participants", "eloRatings", "rankings", "cs2Setup", "events"],
},
ncaa_football_bracket: {
defaultConfig: { ...BASE_CONFIG, bracketSize: 12, oddsWeight: 0.4 },
defaultConfig: { ...BASE_CONFIG, bracketSize: 12, inputPolicy: { oddsWeight: 0.4 } },
requiredInputs: ["sourceElo"],
optionalInputs: ["sourceOdds", "worldRanking"],
derivableInputs: { sourceElo: ["sourceOdds"] },
@ -189,10 +189,12 @@ const PROFILES: Record<SimulatorType, Omit<SimulatorManifestProfile, "simulatorT
setupSections: ["participants", "futuresOdds"],
},
college_hockey_bracket: {
// College hockey already blends odds into Elo internally (and also uses NPI
// rank, which the central resolver can't), so the central blend is disabled
// (oddsWeight 0) to avoid double-counting the odds.
defaultConfig: { ...BASE_CONFIG, bracketSize: 16, parityFactor: 850, oddsWeight: 0 },
// College hockey blends odds into Elo internally (and also uses NPI rank,
// which the central resolver can't represent). oddsWeight 0 makes the central
// resolver leave a present base Elo untouched (no double-count), while still
// letting odds resolve a participant's Elo when they are the only source — so
// readiness and the sim's own internal odds blend both keep working.
defaultConfig: { ...BASE_CONFIG, bracketSize: 16, parityFactor: 850, inputPolicy: { oddsWeight: 0 } },
requiredInputs: ["sourceElo"],
optionalInputs: ["sourceOdds", "worldRanking"],
derivableInputs: { sourceElo: ["sourceOdds"] },