brackt/app/services/simulations/manifest.ts
Claude eefe407e7f
Route every bracket-aware sport away from ICM, not just AFL
The previous commit gated the simulator re-run on a `bracketAware` flag set only
on afl_bracket and llws_bracket, on the claim that every other simulator was
bracket-blind and would resurrect eliminated teams if re-run. That claim was
wrong. Eleven more read playoff_matches and honor isComplete/winnerId already:
ucl, ncaam, ncaaw (both via ncaa-basketball), nba, nhl, snooker, world_cup,
darts, cs2_major, college_hockey and nll. All thirteen are now flagged, so any
sport with a bracket the simulator can read absorbs a result by re-running that
simulator rather than through ICM.

Two simulators are deliberately left off. playoff_bracket and
ncaa_football_bracket declare a "bracket" setup section but never read
playoff_matches, so re-running them really would re-draw the field. That
mismatch runs the other way too — world_cup, darts_bracket and
cs2_major_qualifying_points read the bracket without declaring the section — so
setupSections is not a usable signal here and the flag stays separate from it,
with both facts written down on the flag.

The EV-source condition is also gone. It only asked whether the existing EVs
came from a simulation, which protected nothing: the alternative to re-running
was never leaving them alone, it was the ICM branch overwriting them anyway.
Given two overwrites, the bracket-aware one wins regardless of what wrote them.

Tests: a bracket-blind simulator still goes through ICM, futures-odds EVs no
longer divert a bracket-aware season away from the re-run, and the bracket-aware
set is pinned in the manifest test so a new simulator is an explicit decision
rather than a default.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VPxDnSEKVoKx9HFcQ6gmcS
2026-08-29 02:49:17 +00:00

317 lines
12 KiB
TypeScript

import { getSimulatorInfo, SIMULATOR_TYPES, type SimulatorType } from "./registry";
export type SimulatorInputKey =
| "sourceOdds"
| "sourceElo"
| "worldRanking"
| "rating"
| "projectedWins"
| "projectedTablePoints"
| "seed"
| "region"
| "metadata";
export type SimulatorSetupSection =
| "futuresOdds"
| "eloRatings"
| "rankings"
| "ratings"
| "regularStandings"
| "bracket"
| "events"
| "golfSkills"
| "surfaceElo"
| "cs2Setup"
| "participants";
export interface SimulatorManifestProfile {
simulatorType: SimulatorType;
displayName: string;
description: string;
defaultConfig: Record<string, unknown>;
requiredInputs: SimulatorInputKey[];
optionalInputs: SimulatorInputKey[];
derivableInputs?: Partial<Record<SimulatorInputKey, SimulatorInputKey[]>>;
setupSections: SimulatorSetupSection[];
minParticipantInputs?: number;
/**
* The simulator reads the season's generated bracket: it seeds from the real draw and
* replays completed matches from their recorded result, rather than re-drawing the field
* and re-playing decided games every iteration.
*
* updateProbabilitiesAfterResult reads this to decide whether a result should be absorbed
* by re-running the simulator or by the generic ICM recalculation. Re-running is both more
* accurate and the only option that respects a banked placement floor, but it is only safe
* here: re-running a bracket-blind simulator would re-draw the field and hand equity back
* to teams already knocked out.
*
* Both halves are required. A simulator that reads the draw but re-simulates games already
* played is NOT bracket-aware for this purpose — it resurrects eliminated teams just the
* same. Check for an `isComplete`/`winnerId` replay before setting this on a new simulator.
*
* This is deliberately separate from `setupSections: ["bracket"]`, which only drives admin
* links and a readiness warning and does not track this accurately in either direction.
*/
bracketAware?: boolean;
}
const BASE_CONFIG = {
iterations: 50_000,
};
const PROFILES: Record<SimulatorType, Omit<SimulatorManifestProfile, "simulatorType" | "displayName" | "description">> = {
f1_standings: {
defaultConfig: { iterations: 10_000, raceNoise: 0.5, participantVolatility: 1.5 },
requiredInputs: ["sourceOdds"],
optionalInputs: [],
setupSections: ["participants", "futuresOdds", "events"],
},
indycar_standings: {
defaultConfig: { iterations: 10_000, raceNoise: 0.5, participantVolatility: 1.5 },
requiredInputs: ["sourceOdds"],
optionalInputs: [],
setupSections: ["participants", "futuresOdds", "events"],
},
golf_qualifying_points: {
defaultConfig: { iterations: 10_000, fieldSize: 156, plBeta: 1.5 },
requiredInputs: [],
optionalInputs: ["sourceOdds", "rating"],
setupSections: ["participants", "events", "golfSkills"],
},
playoff_bracket: {
defaultConfig: { ...BASE_CONFIG },
requiredInputs: ["sourceOdds"],
optionalInputs: ["sourceElo"],
setupSections: ["participants", "futuresOdds", "bracket"],
},
ucl_bracket: {
defaultConfig: { ...BASE_CONFIG, parityFactor: 400, inputPolicy: { oddsWeight: 0.3 } },
requiredInputs: ["sourceElo"],
optionalInputs: ["sourceOdds"],
derivableInputs: { sourceElo: ["sourceOdds"] },
setupSections: ["participants", "futuresOdds", "bracket"],
bracketAware: true,
},
ncaam_bracket: {
defaultConfig: { ...BASE_CONFIG, ratingScaleFactor: 7.5, inputPolicy: { ratingMin: -10, ratingMax: 35, fallbackRatingDelta: 5 } },
requiredInputs: ["rating"],
optionalInputs: ["sourceOdds", "sourceElo", "seed", "region"],
derivableInputs: { rating: ["sourceOdds"] },
setupSections: ["participants", "ratings", "futuresOdds", "bracket"],
bracketAware: true,
},
ncaaw_bracket: {
defaultConfig: { ...BASE_CONFIG, inputPolicy: { ratingMin: 0.70, ratingMax: 0.97, missingRatingStrategy: "worstKnownMinus", fallbackRatingDelta: 0.01 } },
requiredInputs: ["rating"],
optionalInputs: ["sourceOdds", "seed", "region"],
derivableInputs: { rating: ["sourceOdds"] },
setupSections: ["participants", "ratings", "futuresOdds", "bracket"],
bracketAware: true,
},
nba_bracket: {
defaultConfig: { ...BASE_CONFIG, parityFactor: 400, seasonGames: 82 },
requiredInputs: ["sourceElo"],
optionalInputs: ["sourceOdds", "projectedWins"],
derivableInputs: { sourceElo: ["projectedWins", "sourceOdds"] },
setupSections: ["participants", "eloRatings", "regularStandings", "bracket"],
bracketAware: true,
},
nhl_bracket: {
defaultConfig: { ...BASE_CONFIG, parityFactor: 1000, seasonGames: 82, overtimeRate: 0.23 },
requiredInputs: ["sourceElo"],
optionalInputs: ["sourceOdds", "projectedWins"],
derivableInputs: { sourceElo: ["projectedWins", "sourceOdds"] },
setupSections: ["participants", "eloRatings", "regularStandings", "bracket"],
bracketAware: true,
},
nfl_bracket: {
defaultConfig: { ...BASE_CONFIG, parityFactor: 400, seasonGames: 17, homeFieldElo: 48 },
requiredInputs: ["sourceElo"],
optionalInputs: ["sourceOdds", "projectedWins"],
derivableInputs: { sourceElo: ["projectedWins", "sourceOdds"] },
setupSections: ["participants", "eloRatings", "regularStandings"],
},
afl_bracket: {
defaultConfig: { ...BASE_CONFIG, parityFactor: 450, seasonGames: 23 },
requiredInputs: ["sourceElo"],
optionalInputs: ["projectedWins"],
derivableInputs: { sourceElo: ["projectedWins"] },
// The bracket is optional — before one exists the ladder is projected from Elo — but once
// it is drawn the simulator seeds from it and honors completed results.
setupSections: ["participants", "eloRatings", "regularStandings", "bracket"],
bracketAware: true,
},
epl_standings: {
defaultConfig: {
...BASE_CONFIG,
iterations: 10_000,
seasonGames: 38,
parityFactor: 400,
matchParityFactor: 400,
averageOpponentElo: 1500,
baseDrawRate: 0.26,
drawDecay: 0.002,
},
requiredInputs: ["sourceElo"],
optionalInputs: ["sourceOdds", "projectedTablePoints"],
derivableInputs: { sourceElo: ["projectedTablePoints", "sourceOdds"] },
setupSections: ["participants", "eloRatings", "regularStandings"],
},
snooker_bracket: {
defaultConfig: { ...BASE_CONFIG, eloDivisor: 400 },
requiredInputs: ["sourceElo"],
optionalInputs: ["worldRanking", "seed"],
setupSections: ["participants", "eloRatings", "rankings", "bracket"],
bracketAware: true,
},
tennis_qualifying_points: {
defaultConfig: { iterations: 10_000, eloDivisor: 400, fallbackElo: 1500 },
requiredInputs: [],
optionalInputs: ["worldRanking"],
setupSections: ["participants", "surfaceElo", "events"],
},
mlb_bracket: {
defaultConfig: { ...BASE_CONFIG, seasonGames: 162, inputPolicy: { oddsWeight: 0.3 } },
requiredInputs: ["sourceElo"],
optionalInputs: ["sourceOdds", "projectedWins"],
derivableInputs: { sourceElo: ["projectedWins", "sourceOdds"] },
setupSections: ["participants", "eloRatings", "regularStandings"],
},
wnba_bracket: {
defaultConfig: { ...BASE_CONFIG, parityFactor: 400, seasonGames: 44, srsEloScale: 30 },
requiredInputs: ["sourceElo"],
optionalInputs: ["sourceOdds", "projectedWins"],
derivableInputs: { sourceElo: ["projectedWins", "sourceOdds"] },
setupSections: ["participants", "eloRatings", "regularStandings"],
},
world_cup: {
defaultConfig: { ...BASE_CONFIG, iterations: 10_000, inputPolicy: { oddsWeight: 0.3 } },
requiredInputs: ["sourceElo"],
optionalInputs: ["sourceOdds", "worldRanking"],
derivableInputs: { sourceElo: ["sourceOdds"] },
setupSections: ["participants", "eloRatings", "futuresOdds", "events"],
bracketAware: true,
},
darts_bracket: {
defaultConfig: { ...BASE_CONFIG, iterations: 10_000, eloDivisor: 400 },
requiredInputs: ["sourceElo", "worldRanking"],
optionalInputs: ["seed"],
setupSections: ["participants", "eloRatings", "rankings"],
bracketAware: true,
},
cs2_major_qualifying_points: {
defaultConfig: { iterations: 10_000, fieldSize: 32, guaranteedCount: 12 },
requiredInputs: ["sourceElo"],
optionalInputs: ["worldRanking", "metadata"],
setupSections: ["participants", "eloRatings", "rankings", "cs2Setup", "events"],
bracketAware: true,
},
ncaa_football_bracket: {
defaultConfig: { ...BASE_CONFIG, parityFactor: 400, bracketSize: 12, inputPolicy: { oddsWeight: 0.4 } },
requiredInputs: ["sourceElo"],
optionalInputs: ["sourceOdds", "worldRanking"],
derivableInputs: { sourceElo: ["sourceOdds"] },
setupSections: ["participants", "eloRatings", "futuresOdds", "bracket"],
},
llws_bracket: {
defaultConfig: { ...BASE_CONFIG, parityFactor: 550, usTeamCount: 10, internationalTeamCount: 10 },
requiredInputs: ["sourceOdds"],
optionalInputs: ["metadata"],
// The bracket is optional — without one the draw is randomized — but once it
// exists the simulator reads the real draw and honors completed results from it.
setupSections: ["participants", "futuresOdds", "bracket"],
bracketAware: true,
},
college_hockey_bracket: {
// 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"] },
setupSections: ["participants", "eloRatings", "rankings", "futuresOdds", "bracket"],
bracketAware: true,
},
brackt: {
defaultConfig: { iterations: 20_000 },
requiredInputs: [],
optionalInputs: [],
setupSections: ["participants"],
},
nll_bracket: {
defaultConfig: {
...BASE_CONFIG,
seasonGames: 18,
parityFactor: 400,
bracketSize: 8,
playoffTeams: 8,
regularSeasonTeamCount: 14,
homeFieldElo: 0,
regularSeasonMode: "project_remaining_games",
regularSeasonNoise: 0.9,
},
requiredInputs: ["sourceElo"],
optionalInputs: ["projectedWins", "sourceOdds", "seed"],
derivableInputs: { sourceElo: ["projectedWins", "sourceOdds"] },
setupSections: ["participants", "eloRatings", "regularStandings", "bracket"],
bracketAware: true,
},
mls_bracket: {
defaultConfig: {
...BASE_CONFIG,
seasonGames: 34,
parityFactor: 400,
matchParityFactor: 400,
averageOpponentElo: 1500,
baseDrawRate: 0.26,
drawDecay: 0.002,
playoffTeamsPerConference: 9,
},
requiredInputs: ["sourceElo"],
optionalInputs: ["sourceOdds", "projectedTablePoints", "seed", "region"],
derivableInputs: { sourceElo: ["projectedTablePoints", "sourceOdds"] },
setupSections: ["participants", "eloRatings", "regularStandings"],
},
};
export const SIMULATOR_MANIFEST: Record<SimulatorType, SimulatorManifestProfile> =
Object.fromEntries(
SIMULATOR_TYPES.map((simulatorType) => {
const info = getSimulatorInfo(simulatorType);
const profile = PROFILES[simulatorType];
return [
simulatorType,
{
simulatorType,
displayName: info?.name ?? simulatorType,
description: info?.description ?? "",
...profile,
minParticipantInputs: profile.minParticipantInputs ?? 1,
},
];
})
) as Record<SimulatorType, SimulatorManifestProfile>;
export function getManifestSimulatorProfile(
simulatorType: SimulatorType
): SimulatorManifestProfile {
return SIMULATOR_MANIFEST[simulatorType];
}
export function simulatorInputLabel(key: SimulatorInputKey): string {
const labels: Record<SimulatorInputKey, string> = {
sourceOdds: "futures odds",
sourceElo: "Elo rating",
worldRanking: "ranking",
rating: "rating",
projectedWins: "projected wins",
projectedTablePoints: "projected table points",
seed: "seed",
region: "region",
metadata: "metadata",
};
return labels[key];
}