* Add CS2 Major qualifying points simulator Implements a full CS2 Major tournament simulator with: - 3-stage Swiss format (Opening Bo1, Elimination Bo1/Bo3, Decider all Bo3) + Champions Stage 8-team single-elimination (QF Bo3, SF Bo3, GF Bo5) - Monte Carlo simulation (10,000 iterations) accumulating QP across 2 majors/season - Sampled 24-team field per iteration: top 12 guaranteed, remaining weighted by 1/rank - Stage 3 exits (placements 9-16) sub-ranked by W-L record (2-3 > 1-3 > 0-3) - Stage assignments stored per-event so actual field composition drives simulation - Admin CS Elo form for entering team Elo + HLTV world rankings - Admin CS2 stage setup page for assigning teams to stages and tracking advancement - Database migration: cs2_major_qualifying_points enum value + cs2_major_stage_results table - 24 unit tests covering all exported pure functions https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR * Consolidate Elo + ranking input into generic elo-ratings page The darts-elo and cs-elo pages were unreachable from the admin nav, which always links to the generic elo-ratings page. Extended elo-ratings to conditionally show world ranking fields for simulator types that need it (darts_bracket, cs2_major_qualifying_points), then deleted the redundant sport-specific pages. https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR * Consolidate server postgres connections into one shared pool Four separate postgres() clients were open simultaneously (app, timer, snapshots, socket), each defaulting to 10 connections, exhausting the database's max_connections limit. Replaced with a single shared lazy- initialized client in server/db.ts using a Proxy to defer the DATABASE_URL check until first use (preserving test compatibility). Also bumps the CS2 Champions Stage stochastic test from 200 → 1000 iterations to eliminate flakiness. https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR * Fix and() bug and add Swiss loop safety guard - cs2-major-stage.ts: markCs2StageEliminations and setCs2FinalPlacements were using JS && instead of Drizzle and(), causing WHERE to filter only by participantId (not scoringEventId), which would update rows across all events instead of just the target event - cs-major-simulator.ts: add break guard in simulateSwiss while loop to prevent infinite loop if pairGroups returns no pairs https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR * Fix all remaining code review issues - cs2-major-stage.ts: use schema column reference for stageEliminated in markCs2StageEliminations instead of raw SQL string - cs-major-simulator.ts: simulateOneMajor now locks in known stage results when a stage is complete (8 recorded eliminations), only simulating the remaining stages during live events - admin event page: add CS2 Stage Setup button for cs2_major_qualifying_points simulator types; expose simulatorType in server loader type cast - cs2-setup.tsx: replace document.getElementById DOM manipulation with React state (eliminatedChecked map) for checkbox show/hide logic; remove unused stageMap and unassignedParticipants variables https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR * Fix oxlint errors: non-null assertions, sort→toSorted, unused vars - cs-major-simulator.ts: replace 5 non-null assertions (!) with safe optional chaining / if-guards; replace 6 .sort() with .toSorted() - cs2-major-stage.ts: remove unused `inArray` import - cs2-setup.tsx: remove unused `assignedIds` variable https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR * Fix flaky Champions Stage stochastic test The makeTeams(8) helper creates only a 70-pt Elo spread (1800→1730). With the Champions Stage bracket math this gives team-0 a ~19.6% win rate — right at the 0.2 threshold, causing the test to fail ~63% of the time in CI despite 1000 iterations. Use 100-pt steps (1800→1100) instead, giving team-0 a ~40% win rate and raising the assertion threshold to 0.25 for a clear safety margin. https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR --------- Co-authored-by: Claude <noreply@anthropic.com>
153 lines
7.6 KiB
TypeScript
153 lines
7.6 KiB
TypeScript
/**
|
||
* Simulator Registry
|
||
*
|
||
* Maps a sport's simulatorType to the appropriate Simulator implementation.
|
||
* Set simulatorType on the sport record in the admin UI.
|
||
* Add new simulators here as new sports are supported.
|
||
*/
|
||
|
||
import type { Simulator } from "./types";
|
||
import { BracketSimulator } from "./bracket-simulator";
|
||
import { AutoRacingSimulator } from "./auto-racing-simulator";
|
||
import { GolfSimulator } from "./golf-simulator";
|
||
import { UCLSimulator } from "./ucl-simulator";
|
||
import { NCAAMSimulator } from "./ncaam-simulator";
|
||
import { NCAAWSimulator } from "./ncaaw-simulator";
|
||
import { NBASimulator } from "./nba-simulator";
|
||
import { NHLSimulator } from "./nhl-simulator";
|
||
import { AFLSimulator } from "./afl-simulator";
|
||
import { SnookerSimulator } from "./snooker-simulator";
|
||
import { TennisSimulator } from "./tennis-simulator";
|
||
import { DartsSimulator } from "./darts-simulator";
|
||
import { CSMajorSimulator } from "./cs-major-simulator";
|
||
import { MLBSimulator } from "./mlb-simulator";
|
||
import { WNBASimulator } from "./wnba-simulator";
|
||
import { WorldCupSimulator } from "./world-cup-simulator";
|
||
|
||
export const SIMULATOR_TYPES = [
|
||
"f1_standings",
|
||
"indycar_standings",
|
||
"golf_qualifying_points",
|
||
"playoff_bracket",
|
||
"ucl_bracket",
|
||
"ncaam_bracket",
|
||
"ncaaw_bracket",
|
||
"nba_bracket",
|
||
"nhl_bracket",
|
||
"afl_bracket",
|
||
"snooker_bracket",
|
||
"tennis_qualifying_points",
|
||
"mlb_bracket",
|
||
"wnba_bracket",
|
||
"world_cup",
|
||
"darts_bracket",
|
||
"cs2_major_qualifying_points",
|
||
] as const;
|
||
|
||
export type SimulatorType = typeof SIMULATOR_TYPES[number];
|
||
|
||
// ─── Race points tables ───────────────────────────────────────────────────────
|
||
|
||
/** F1 points: positions 1–10. */
|
||
const F1_RACE_POINTS: Record<number, number> = {
|
||
1: 25, 2: 18, 3: 15, 4: 12, 5: 10, 6: 8, 7: 6, 8: 4, 9: 2, 10: 1,
|
||
};
|
||
|
||
/** IndyCar standard race points: positions 1–26. */
|
||
const INDYCAR_RACE_POINTS: Record<number, number> = {
|
||
1: 50, 2: 40, 3: 35, 4: 32, 5: 30, 6: 28, 7: 26, 8: 24, 9: 22, 10: 20,
|
||
11: 19, 12: 18, 13: 17, 14: 16, 15: 15, 16: 14, 17: 13, 18: 12, 19: 11, 20: 10,
|
||
21: 9, 22: 8, 23: 7, 24: 6, 25: 5, 26: 5,
|
||
};
|
||
|
||
export interface SimulatorInfo {
|
||
name: string;
|
||
description: string;
|
||
}
|
||
|
||
const REGISTRY: Record<SimulatorType, { info: SimulatorInfo; create: () => Simulator }> = {
|
||
f1_standings: {
|
||
info: { name: "F1 Standings Model", description: "Simulates remaining races using current F1 standings and futures odds" },
|
||
create: () => new AutoRacingSimulator(F1_RACE_POINTS, "f1_standings_model"),
|
||
},
|
||
indycar_standings: {
|
||
info: { name: "IndyCar Standings Model", description: "Simulates remaining races using current IndyCar standings and futures odds" },
|
||
create: () => new AutoRacingSimulator(INDYCAR_RACE_POINTS, "indycar_standings_model"),
|
||
},
|
||
golf_qualifying_points: {
|
||
info: { name: "Golf Qualifying Points Monte Carlo", description: "Simulates remaining majors using a Plackett-Luce model with SG: Total ratings. Awards QP by finishing position; ranks players by total QP across all 4 majors." },
|
||
create: () => new GolfSimulator(),
|
||
},
|
||
playoff_bracket: {
|
||
info: { name: "Bracket Monte Carlo", description: "Simulates playoff bracket outcomes using Elo ratings" },
|
||
create: () => new BracketSimulator(),
|
||
},
|
||
ucl_bracket: {
|
||
info: { name: "UCL Bracket Monte Carlo", description: "Simulates the UEFA Champions League 16-team knockout bracket using blended Elo + futures odds" },
|
||
create: () => new UCLSimulator(),
|
||
},
|
||
ncaam_bracket: {
|
||
info: { name: "NCAAM Bracket Monte Carlo", description: "Simulates the NCAA Men's Basketball Tournament 64-team bracket using KenPom net ratings" },
|
||
create: () => new NCAAMSimulator(),
|
||
},
|
||
ncaaw_bracket: {
|
||
info: { name: "NCAAW Bracket Monte Carlo", description: "Simulates the NCAA Women's Basketball Tournament 64-team bracket using Barttorvik Barthag ratings" },
|
||
create: () => new NCAAWSimulator(),
|
||
},
|
||
nba_bracket: {
|
||
info: { name: "NBA Playoff Monte Carlo", description: "Simulates NBA playoff seedings (via seed probabilities) and full bracket (best-of-7 series) using Elo ratings" },
|
||
create: () => new NBASimulator(),
|
||
},
|
||
nhl_bracket: {
|
||
info: { name: "NHL Playoff Monte Carlo", description: "Simulates NHL playoff seedings (divisional format, via seed probabilities) and full bracket (best-of-7 series) using Elo ratings" },
|
||
create: () => new NHLSimulator(),
|
||
},
|
||
afl_bracket: {
|
||
info: { name: "AFL Season + Finals Monte Carlo", description: "Projects AFL regular season standings via Elo, then simulates the 10-team finals series (Wildcard → QF/EF → SF → PF → Grand Final). Reads current standings from DB; falls back to full-season projection pre-season." },
|
||
create: () => new AFLSimulator(),
|
||
},
|
||
snooker_bracket: {
|
||
info: { name: "Snooker World Championship Monte Carlo", description: "Simulates the 32-player World Championship bracket using frame-by-frame Bernoulli win probability and direct Elo ratings. Pre-bracket path simulates qualifying (ranks 17-48) and randomly draws qualifiers vs top 16 seeds." },
|
||
create: () => new SnookerSimulator(),
|
||
},
|
||
tennis_qualifying_points: {
|
||
info: { name: "Tennis Grand Slam Monte Carlo", description: "Simulates all 4 Grand Slam majors (128-player seeded bracket) using surface-specific Elo ratings. Accumulates qualifying points per round (tie-split applied) and ranks players by total QP across the season." },
|
||
create: () => new TennisSimulator(),
|
||
},
|
||
mlb_bracket: {
|
||
info: { name: "MLB Playoff Monte Carlo", description: "Simulates MLB division races + full playoff bracket (WC best-of-3, DS best-of-5, LCS/WS best-of-7) using Elo ratings calibrated from FanGraphs projected wins" },
|
||
create: () => new MLBSimulator(),
|
||
},
|
||
wnba_bracket: {
|
||
info: { name: "WNBA Playoff Monte Carlo", description: "Projects WNBA regular season seedings and simulates full playoff bracket (R1 best-of-3, Semis best-of-5, Finals best-of-7) using SRS-derived Elo ratings. SRS values sourced from basketball-reference.com." },
|
||
create: () => new WNBASimulator(),
|
||
},
|
||
world_cup: {
|
||
info: { name: "FIFA World Cup 2026 Monte Carlo", description: "Simulates the full 48-team World Cup: round-robin group stage (12 groups), best-8 third-place selection, R32→R16→QF→SF→3rd place game→Final. Uses blended Elo + futures odds." },
|
||
create: () => new WorldCupSimulator(),
|
||
},
|
||
darts_bracket: {
|
||
info: { name: "PDC World Darts Championship Monte Carlo", description: "Simulates the 128-player World Darts Championship bracket using set-level Bernoulli win probability and Elo ratings. Top 32 seeds placed in fixed positions; remaining 96 players randomly drawn per simulation." },
|
||
create: () => new DartsSimulator(),
|
||
},
|
||
cs2_major_qualifying_points: {
|
||
info: { name: "CS2 Major Qualifying Points Monte Carlo", description: "Simulates 2 CS2 Majors per season (3 Swiss stages: Opening Bo1, Elimination Bo1, Decider Bo3, then Champions Stage single-elimination). Awards QP by final placement; ranks teams by total QP across both majors. Stage 3 exits (placements 9–16) are sub-ranked by W-L record." },
|
||
create: () => new CSMajorSimulator(),
|
||
},
|
||
};
|
||
|
||
export function getSimulator(simulatorType: SimulatorType): Simulator {
|
||
const entry = REGISTRY[simulatorType];
|
||
if (!entry) {
|
||
throw new Error(
|
||
`No simulator registered for type: "${simulatorType}". ` +
|
||
`Add a simulator to app/services/simulations/registry.ts.`
|
||
);
|
||
}
|
||
return entry.create();
|
||
}
|
||
|
||
export function getSimulatorInfo(simulatorType: SimulatorType): SimulatorInfo | null {
|
||
return REGISTRY[simulatorType]?.info ?? null;
|
||
}
|
||
|