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, }; // Source-resolution priority that makes freshly entered futures odds win over a // stored Elo/rating (and projections). Used as the default for simulators where // futures odds are a deliberate, primary input (those exposing a Futures Odds // setup section). Admins can still override this per season in the Input Policy. // Kept as a local literal (not imported from input-policy) to avoid a circular // module dependency that would be evaluated before input-policy's consts exist. // Mirrors FUTURES_FIRST_SOURCE_PRIORITY in input-policy.ts. const FUTURES_FIRST_PRIORITY = ["sourceOdds", "sourceElo", "projectedWins", "projectedTablePoints"] as const; 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, 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, fallbackRatingDelta: 5, sourceEloPriority: FUTURES_FIRST_PRIORITY } }, 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, sourceEloPriority: FUTURES_FIRST_PRIORITY } }, 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, 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, iterations: 10_000, eloWeight: 0.7, oddsWeight: 0.3, inputPolicy: { sourceEloPriority: FUTURES_FIRST_PRIORITY, 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, eloWeight: 0.6, oddsWeight: 0.4, inputPolicy: { sourceEloPriority: FUTURES_FIRST_PRIORITY, 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, inputPolicy: { sourceEloPriority: FUTURES_FIRST_PRIORITY } }, 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]; }