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; requiredInputs: SimulatorInputKey[]; optionalInputs: SimulatorInputKey[]; derivableInputs?: Partial>; setupSections: SimulatorSetupSection[]; minParticipantInputs?: number; } const BASE_CONFIG = { iterations: 50_000, }; const PROFILES: Record> = { 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 = 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; export function getManifestSimulatorProfile( simulatorType: SimulatorType ): SimulatorManifestProfile { return SIMULATOR_MANIFEST[simulatorType]; } export function simulatorInputLabel(key: SimulatorInputKey): string { const labels: Record = { 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]; }