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.baseEloPriority).toEqual(DEFAULT_BASE_ELO_PRIORITY);
expect(defaults.oddsWeight).toBe(0.3); expect(defaults.oddsWeight).toBe(0.3);
// oddsWeight can be sourced from the top-level config or the input policy, // oddsWeight lives under inputPolicy and is clamped to [0, 1]; a top-level
// and is clamped to [0, 1]; junk base-priority entries are dropped. // config.oddsWeight is ignored (single home); junk base-priority entries drop.
expect(getSimulatorInputPolicy({ oddsWeight: 0.6 }).oddsWeight).toBe(0.6); 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: 5 } }).oddsWeight).toBe(1);
expect(getSimulatorInputPolicy({ inputPolicy: { oddsWeight: -2 } }).oddsWeight).toBe(0); expect(getSimulatorInputPolicy({ inputPolicy: { oddsWeight: -2 } }).oddsWeight).toBe(0);
expect( expect(

View file

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

View file

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