brackt/app/services/simulations/manifest.ts
Chris Parsons 5b261bf258
Add NLL box lacrosse simulator implementation plan and preseason-draftability guidance (#417)
* docs: plan NLL preseason simulator

* Add NLL Season + Playoffs Monte Carlo simulator

14-team regular season (18 games) projects top-8 via Elo + decaying
projectedWins prior (adjusts for games already played). Playoff bracket:
QF single-game (1v8, 2v7, 3v6, 4v5), SF and Finals best-of-3. Three
runtime modes: bracket-aware → known-seed → regular-season projection.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 14:22:34 -07:00

250 lines
9.1 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;
}
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, eloWeight: 0.7, oddsWeight: 0.3 },
requiredInputs: ["sourceOdds"],
optionalInputs: ["sourceElo"],
setupSections: ["participants", "futuresOdds", "bracket"],
},
ncaam_bracket: {
defaultConfig: { ...BASE_CONFIG, ratingScaleFactor: 7.5, inputPolicy: { ratingMin: -10, ratingMax: 35 } },
requiredInputs: ["rating"],
optionalInputs: ["sourceOdds", "sourceElo", "seed", "region"],
derivableInputs: { rating: ["sourceOdds"] },
setupSections: ["participants", "ratings", "futuresOdds", "bracket"],
},
ncaaw_bracket: {
defaultConfig: { ...BASE_CONFIG, ratingType: "barthag", inputPolicy: { ratingMin: 0.15, ratingMax: 0.99 } },
requiredInputs: ["rating"],
optionalInputs: ["sourceOdds", "seed", "region"],
derivableInputs: { rating: ["sourceOdds"] },
setupSections: ["participants", "ratings", "futuresOdds", "bracket"],
},
nba_bracket: {
defaultConfig: { ...BASE_CONFIG, parityFactor: 400, seasonGames: 82 },
requiredInputs: ["sourceElo"],
optionalInputs: ["sourceOdds", "projectedWins"],
derivableInputs: { sourceElo: ["projectedWins", "sourceOdds"] },
setupSections: ["participants", "eloRatings", "regularStandings", "bracket"],
},
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"],
},
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"] },
setupSections: ["participants", "eloRatings", "regularStandings"],
},
epl_standings: {
defaultConfig: { ...BASE_CONFIG, seasonGames: 38, baseDrawRate: 0.26, drawDecay: 0.75 },
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"],
},
tennis_qualifying_points: {
defaultConfig: { iterations: 10_000, eloDivisor: 400, fallbackElo: 1500 },
requiredInputs: [],
optionalInputs: ["worldRanking"],
setupSections: ["participants", "surfaceElo", "events"],
},
mlb_bracket: {
defaultConfig: { ...BASE_CONFIG, rDiffWeight: 0.7, oddsWeight: 0.3, seasonGames: 162 },
requiredInputs: ["sourceElo"],
optionalInputs: ["sourceOdds", "projectedWins"],
derivableInputs: { sourceElo: ["projectedWins", "sourceOdds"] },
setupSections: ["participants", "eloRatings", "regularStandings"],
},
wnba_bracket: {
defaultConfig: { ...BASE_CONFIG, seasonGames: 44, srsEloScale: 30 },
requiredInputs: ["sourceElo"],
optionalInputs: ["sourceOdds", "projectedWins"],
derivableInputs: { sourceElo: ["projectedWins", "sourceOdds"] },
setupSections: ["participants", "eloRatings", "regularStandings"],
},
world_cup: {
defaultConfig: { ...BASE_CONFIG, eloWeight: 0.7, oddsWeight: 0.3 },
requiredInputs: ["sourceElo"],
optionalInputs: ["sourceOdds", "worldRanking"],
derivableInputs: { sourceElo: ["sourceOdds"] },
setupSections: ["participants", "eloRatings", "futuresOdds", "events"],
},
darts_bracket: {
defaultConfig: { ...BASE_CONFIG, eloDivisor: 400 },
requiredInputs: ["sourceElo", "worldRanking"],
optionalInputs: ["seed"],
setupSections: ["participants", "eloRatings", "rankings"],
},
cs2_major_qualifying_points: {
defaultConfig: { iterations: 10_000, fieldSize: 32, guaranteedCount: 12 },
requiredInputs: ["sourceElo"],
optionalInputs: ["worldRanking", "metadata"],
setupSections: ["participants", "eloRatings", "rankings", "cs2Setup", "events"],
},
ncaa_football_bracket: {
defaultConfig: { ...BASE_CONFIG, bracketSize: 12, eloWeight: 0.6, oddsWeight: 0.4 },
requiredInputs: ["sourceElo"],
optionalInputs: ["sourceOdds", "worldRanking"],
derivableInputs: { sourceElo: ["sourceOdds"] },
setupSections: ["participants", "eloRatings", "futuresOdds", "bracket"],
},
llws_bracket: {
defaultConfig: { ...BASE_CONFIG, usTeamCount: 10, internationalTeamCount: 10, poolSize: 5 },
requiredInputs: ["sourceOdds"],
optionalInputs: ["metadata"],
setupSections: ["participants", "futuresOdds"],
},
college_hockey_bracket: {
defaultConfig: { ...BASE_CONFIG, bracketSize: 16, parityFactor: 850 },
requiredInputs: ["sourceElo"],
optionalInputs: ["sourceOdds", "worldRanking"],
derivableInputs: { sourceElo: ["sourceOdds"] },
setupSections: ["participants", "eloRatings", "rankings", "futuresOdds", "bracket"],
},
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"],
},
};
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];
}