281 lines
10 KiB
TypeScript
281 lines
10 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, inputPolicy: { oddsWeight: 0.3 } },
|
|
requiredInputs: ["sourceElo"],
|
|
optionalInputs: ["sourceOdds"],
|
|
derivableInputs: { sourceElo: ["sourceOdds"] },
|
|
setupSections: ["participants", "futuresOdds", "bracket"],
|
|
},
|
|
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"],
|
|
},
|
|
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"],
|
|
},
|
|
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,
|
|
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"],
|
|
},
|
|
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, 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"],
|
|
},
|
|
darts_bracket: {
|
|
defaultConfig: { ...BASE_CONFIG, iterations: 10_000, 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, inputPolicy: { 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: {
|
|
// 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"],
|
|
},
|
|
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"],
|
|
},
|
|
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];
|
|
}
|