brackt/app/services/simulations/nhl-simulator.ts

543 lines
25 KiB
TypeScript
Raw Normal View History

/**
* NHL Playoff Simulator
*
* Monte Carlo simulation of the NHL playoffs including seeding projection
* for the current season (2025-26).
*
* Algorithm:
* 1. Load all participants for the sports season from DB
Fix NHL simulator seeding bugs and code quality issues (#281) * Fix NHL simulator dynamic seeding and code quality issues - Replace hardcoded seeding probabilities with standings-based Monte Carlo projection: simulates remaining regular season games per team using Elo win probability vs. a league-average opponent, so mathematically eliminated teams naturally fall out of playoff contention - Fix double-simulation bug in wildcard pool: all 16 conference teams are now projected exactly once via sortProjected([...divA, ...divB].map(projectTeam)), then filtered — wildcard pool no longer re-runs simulateProjectedPoints with fresh randomness independent of the division seeding - Move ProjectedTeam interface, projectTeam(), and sortProjected() to module scope (defined once, not recreated on every buildConferenceBracket call) - Replace conference-level participant count check (east < 8 || west < 8) with per-division checks (each division < 3) for a more actionable error message - Add logger.warn when standings.length === 0 so admins know the sim is running without synced data - Remove unused easternTeams/westernTeams variables - Simplify test: replace Object.fromEntries pattern with a plain for..of loop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix oxlint errors in NHL simulator and tests - Replace non-null assertions (data!) with optional chaining (data?.x ?? "") - Move makeEntry helper to module scope to avoid recreating on every call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:33:33 -04:00
* 2. Load current regular season standings (wins, otLosses, gamesPlayed, conference, division)
* 3. Match participant names to hardcoded team data (Elo ratings + conference/division fallback)
* 4. For each simulation:
* a. For each team, simulate remaining regular season games (82 - gamesPlayed):
* - Win (2 pts) with eloWinProb vs. a league-average opponent (Elo 1500)
* - OT/SO loss (1 pt) at NHL_OT_RATE × (1 winProb)
* - Regulation loss (0 pts) otherwise
* projectedPoints = currentPoints + simulated extra points
* b. Per division: rank by projected points (random tiebreaker) top 3 are division seeds
* c. Per conference: top 2 non-division-seed teams by projected points WC1 and WC2
* (WC1 has more points than WC2)
* d. Build the 8-team bracket per conference:
* - Division winner with more points = 1st seed, faces WC2
* - Other division winner = 2nd seed, faces WC1
* - Each division's 2nd and 3rd seeds face each other
* e. Simulate 4 rounds of best-of-7 series:
* Round 1 (Wild Card): m0=1stSeed/WC2, m1=2ndSeed/WC1,
* m2=topDiv2nd/3rd, m3=otherDiv2nd/3rd
* Round 2 (Div Finals): m0w vs m2w, m1w vs m3w
* Round 3 (Conf Finals): two division-final winners per conference
* Stanley Cup Final: East champ vs West champ
Fix NHL simulator seeding bugs and code quality issues (#281) * Fix NHL simulator dynamic seeding and code quality issues - Replace hardcoded seeding probabilities with standings-based Monte Carlo projection: simulates remaining regular season games per team using Elo win probability vs. a league-average opponent, so mathematically eliminated teams naturally fall out of playoff contention - Fix double-simulation bug in wildcard pool: all 16 conference teams are now projected exactly once via sortProjected([...divA, ...divB].map(projectTeam)), then filtered — wildcard pool no longer re-runs simulateProjectedPoints with fresh randomness independent of the division seeding - Move ProjectedTeam interface, projectTeam(), and sortProjected() to module scope (defined once, not recreated on every buildConferenceBracket call) - Replace conference-level participant count check (east < 8 || west < 8) with per-division checks (each division < 3) for a more actionable error message - Add logger.warn when standings.length === 0 so admins know the sim is running without synced data - Remove unused easternTeams/westernTeams variables - Simplify test: replace Object.fromEntries pattern with a plain for..of loop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix oxlint errors in NHL simulator and tests - Replace non-null assertions (data!) with optional chaining (data?.x ?? "") - Move makeEntry helper to module scope to avoid recreating on every call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:33:33 -04:00
* 5. Track placement counts per scoring tier
* 6. Convert counts to probability distributions
*
Fix NHL simulator seeding bugs and code quality issues (#281) * Fix NHL simulator dynamic seeding and code quality issues - Replace hardcoded seeding probabilities with standings-based Monte Carlo projection: simulates remaining regular season games per team using Elo win probability vs. a league-average opponent, so mathematically eliminated teams naturally fall out of playoff contention - Fix double-simulation bug in wildcard pool: all 16 conference teams are now projected exactly once via sortProjected([...divA, ...divB].map(projectTeam)), then filtered — wildcard pool no longer re-runs simulateProjectedPoints with fresh randomness independent of the division seeding - Move ProjectedTeam interface, projectTeam(), and sortProjected() to module scope (defined once, not recreated on every buildConferenceBracket call) - Replace conference-level participant count check (east < 8 || west < 8) with per-division checks (each division < 3) for a more actionable error message - Add logger.warn when standings.length === 0 so admins know the sim is running without synced data - Remove unused easternTeams/westernTeams variables - Simplify test: replace Object.fromEntries pattern with a plain for..of loop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix oxlint errors in NHL simulator and tests - Replace non-null assertions (data!) with optional chaining (data?.x ?? "") - Move makeEntry helper to module scope to avoid recreating on every call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:33:33 -04:00
* Win probability (Elo, PARITY_FACTOR = 1000):
* P(A beats B) = 1 / (1 + 10^((eloB - eloA) / 1000))
* NHL uses a higher parity factor than the standard 400 to reflect the high
* variance of hockey.
*
* Futures blending:
* If sourceOdds are stored in participantExpectedValues for this season,
* the per-game win probability is blended:
* P(game) = ELO_WEIGHT * eloProb + ODDS_WEIGHT * oddsProb
* where oddsProb = normalizedOdds(A) / (normalizedOdds(A) + normalizedOdds(B)).
* Normalized odds are vig-removed futures win probabilities.
* ELO_WEIGHT = 0.7, ODDS_WEIGHT = 0.3 (same calibration as UCL simulator).
* Falls back to Elo-only when no odds are stored.
*
* Placement tiers SimulationProbabilities mapping:
* probFirst = Stanley Cup champion (1 per sim)
* probSecond = Stanley Cup Final loser (1 per sim)
* probThird / probFourth = Conference Final losers (2 per sim East + West)
* probFifthprobEighth = Second Round losers (4 per sim)
* First Round losers all 0 (score 0 points)
* Missed playoffs all 0
*
Fix NHL simulator seeding bugs and code quality issues (#281) * Fix NHL simulator dynamic seeding and code quality issues - Replace hardcoded seeding probabilities with standings-based Monte Carlo projection: simulates remaining regular season games per team using Elo win probability vs. a league-average opponent, so mathematically eliminated teams naturally fall out of playoff contention - Fix double-simulation bug in wildcard pool: all 16 conference teams are now projected exactly once via sortProjected([...divA, ...divB].map(projectTeam)), then filtered — wildcard pool no longer re-runs simulateProjectedPoints with fresh randomness independent of the division seeding - Move ProjectedTeam interface, projectTeam(), and sortProjected() to module scope (defined once, not recreated on every buildConferenceBracket call) - Replace conference-level participant count check (east < 8 || west < 8) with per-division checks (each division < 3) for a more actionable error message - Add logger.warn when standings.length === 0 so admins know the sim is running without synced data - Remove unused easternTeams/westernTeams variables - Simplify test: replace Object.fromEntries pattern with a plain for..of loop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix oxlint errors in NHL simulator and tests - Replace non-null assertions (data!) with optional chaining (data?.x ?? "") - Move makeEntry helper to module scope to avoid recreating on every call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:33:33 -04:00
* Elo ratings are hardcoded (2025-26 season data).
* Source: https://elo.harvitronix.com/nhl/2025-2026
* Conference and division are read from the standings table (synced from the
* NHL API); TEAMS_DATA values serve as fallbacks if standings are missing.
* Update Elo values at the start of each season.
*/
import { database } from "~/database/context";
import { eq } from "drizzle-orm";
import * as schema from "~/database/schema";
import type { Simulator, SimulationResult } from "./types";
import { logger } from "~/lib/logger";
import {
convertAmericanOddsToProbability,
normalizeProbabilities,
} from "~/services/probability-engine";
Fix NHL simulator seeding bugs and code quality issues (#281) * Fix NHL simulator dynamic seeding and code quality issues - Replace hardcoded seeding probabilities with standings-based Monte Carlo projection: simulates remaining regular season games per team using Elo win probability vs. a league-average opponent, so mathematically eliminated teams naturally fall out of playoff contention - Fix double-simulation bug in wildcard pool: all 16 conference teams are now projected exactly once via sortProjected([...divA, ...divB].map(projectTeam)), then filtered — wildcard pool no longer re-runs simulateProjectedPoints with fresh randomness independent of the division seeding - Move ProjectedTeam interface, projectTeam(), and sortProjected() to module scope (defined once, not recreated on every buildConferenceBracket call) - Replace conference-level participant count check (east < 8 || west < 8) with per-division checks (each division < 3) for a more actionable error message - Add logger.warn when standings.length === 0 so admins know the sim is running without synced data - Remove unused easternTeams/westernTeams variables - Simplify test: replace Object.fromEntries pattern with a plain for..of loop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix oxlint errors in NHL simulator and tests - Replace non-null assertions (data!) with optional chaining (data?.x ?? "") - Move makeEntry helper to module scope to avoid recreating on every call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:33:33 -04:00
import { getRegularSeasonStandings } from "~/models/regular-season-standings";
// ─── Simulation parameters ────────────────────────────────────────────────────
const NUM_SIMULATIONS = 50_000;
Fix NHL simulator seeding bugs and code quality issues (#281) * Fix NHL simulator dynamic seeding and code quality issues - Replace hardcoded seeding probabilities with standings-based Monte Carlo projection: simulates remaining regular season games per team using Elo win probability vs. a league-average opponent, so mathematically eliminated teams naturally fall out of playoff contention - Fix double-simulation bug in wildcard pool: all 16 conference teams are now projected exactly once via sortProjected([...divA, ...divB].map(projectTeam)), then filtered — wildcard pool no longer re-runs simulateProjectedPoints with fresh randomness independent of the division seeding - Move ProjectedTeam interface, projectTeam(), and sortProjected() to module scope (defined once, not recreated on every buildConferenceBracket call) - Replace conference-level participant count check (east < 8 || west < 8) with per-division checks (each division < 3) for a more actionable error message - Add logger.warn when standings.length === 0 so admins know the sim is running without synced data - Remove unused easternTeams/westernTeams variables - Simplify test: replace Object.fromEntries pattern with a plain for..of loop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix oxlint errors in NHL simulator and tests - Replace non-null assertions (data!) with optional chaining (data?.x ?? "") - Move makeEntry helper to module scope to avoid recreating on every call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:33:33 -04:00
/** NHL regular season games per team. */
const NHL_REGULAR_SEASON_GAMES = 82;
/**
* Fraction of NHL games that go to overtime / shootout.
* The losing team earns 1 point (instead of 0) in these games.
* Historical average: ~23% of games.
*/
const NHL_OT_RATE = 0.23;
/**
* Elo parity factor. NHL uses 1000 (higher than the standard 400) to reflect
* the elevated game-to-game variance in hockey.
*/
const PARITY_FACTOR = 1000;
/**
* Blend weights for Elo vs. Vegas futures odds when sourceOdds are available.
* Same calibration as the UCL simulator (0.7 / 0.3).
*/
const ELO_WEIGHT = 0.7;
const ODDS_WEIGHT = 1 - ELO_WEIGHT;
Fix NHL simulator seeding bugs and code quality issues (#281) * Fix NHL simulator dynamic seeding and code quality issues - Replace hardcoded seeding probabilities with standings-based Monte Carlo projection: simulates remaining regular season games per team using Elo win probability vs. a league-average opponent, so mathematically eliminated teams naturally fall out of playoff contention - Fix double-simulation bug in wildcard pool: all 16 conference teams are now projected exactly once via sortProjected([...divA, ...divB].map(projectTeam)), then filtered — wildcard pool no longer re-runs simulateProjectedPoints with fresh randomness independent of the division seeding - Move ProjectedTeam interface, projectTeam(), and sortProjected() to module scope (defined once, not recreated on every buildConferenceBracket call) - Replace conference-level participant count check (east < 8 || west < 8) with per-division checks (each division < 3) for a more actionable error message - Add logger.warn when standings.length === 0 so admins know the sim is running without synced data - Remove unused easternTeams/westernTeams variables - Simplify test: replace Object.fromEntries pattern with a plain for..of loop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix oxlint errors in NHL simulator and tests - Replace non-null assertions (data!) with optional chaining (data?.x ?? "") - Move makeEntry helper to module scope to avoid recreating on every call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:33:33 -04:00
// ─── Team data (2025-26 season) ───────────────────────────────────────────────
//
Fix NHL simulator seeding bugs and code quality issues (#281) * Fix NHL simulator dynamic seeding and code quality issues - Replace hardcoded seeding probabilities with standings-based Monte Carlo projection: simulates remaining regular season games per team using Elo win probability vs. a league-average opponent, so mathematically eliminated teams naturally fall out of playoff contention - Fix double-simulation bug in wildcard pool: all 16 conference teams are now projected exactly once via sortProjected([...divA, ...divB].map(projectTeam)), then filtered — wildcard pool no longer re-runs simulateProjectedPoints with fresh randomness independent of the division seeding - Move ProjectedTeam interface, projectTeam(), and sortProjected() to module scope (defined once, not recreated on every buildConferenceBracket call) - Replace conference-level participant count check (east < 8 || west < 8) with per-division checks (each division < 3) for a more actionable error message - Add logger.warn when standings.length === 0 so admins know the sim is running without synced data - Remove unused easternTeams/westernTeams variables - Simplify test: replace Object.fromEntries pattern with a plain for..of loop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix oxlint errors in NHL simulator and tests - Replace non-null assertions (data!) with optional chaining (data?.x ?? "") - Move makeEntry helper to module scope to avoid recreating on every call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:33:33 -04:00
// elo: Elo rating from elo.harvitronix.com/nhl/2025-2026.
// Used for both regular-season game simulation (vs. avg opponent Elo 1500)
// and for all playoff matchup win probabilities.
//
Fix NHL simulator seeding bugs and code quality issues (#281) * Fix NHL simulator dynamic seeding and code quality issues - Replace hardcoded seeding probabilities with standings-based Monte Carlo projection: simulates remaining regular season games per team using Elo win probability vs. a league-average opponent, so mathematically eliminated teams naturally fall out of playoff contention - Fix double-simulation bug in wildcard pool: all 16 conference teams are now projected exactly once via sortProjected([...divA, ...divB].map(projectTeam)), then filtered — wildcard pool no longer re-runs simulateProjectedPoints with fresh randomness independent of the division seeding - Move ProjectedTeam interface, projectTeam(), and sortProjected() to module scope (defined once, not recreated on every buildConferenceBracket call) - Replace conference-level participant count check (east < 8 || west < 8) with per-division checks (each division < 3) for a more actionable error message - Add logger.warn when standings.length === 0 so admins know the sim is running without synced data - Remove unused easternTeams/westernTeams variables - Simplify test: replace Object.fromEntries pattern with a plain for..of loop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix oxlint errors in NHL simulator and tests - Replace non-null assertions (data!) with optional chaining (data?.x ?? "") - Move makeEntry helper to module scope to avoid recreating on every call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:33:33 -04:00
// conference / division: Used as fallbacks when the standings table has no
// conference or division data for a participant. In normal operation these
// are read from regularSeasonStandings (synced from the NHL API).
//
// Divisions:
// Eastern — Atlantic: BOS, BUF, DET, FLA, MTL, OTT, TBL, TOR
// Eastern — Metropolitan: CAR, CBJ, NJD, NYI, NYR, PHI, PIT, WSH
// Western — Central: CHI, COL, DAL, MIN, NSH, STL, UTA, WPG
// Western — Pacific: ANA, CGY, EDM, LAK, SJS, SEA, VAN, VGK
interface NhlTeamData {
conference: "Eastern" | "Western";
division: "Atlantic" | "Metropolitan" | "Central" | "Pacific";
elo: number;
}
const TEAMS_DATA: Record<string, NhlTeamData> = {
// ── Eastern Conference — Atlantic ──────────────────────────────────────────
Fix NHL simulator seeding bugs and code quality issues (#281) * Fix NHL simulator dynamic seeding and code quality issues - Replace hardcoded seeding probabilities with standings-based Monte Carlo projection: simulates remaining regular season games per team using Elo win probability vs. a league-average opponent, so mathematically eliminated teams naturally fall out of playoff contention - Fix double-simulation bug in wildcard pool: all 16 conference teams are now projected exactly once via sortProjected([...divA, ...divB].map(projectTeam)), then filtered — wildcard pool no longer re-runs simulateProjectedPoints with fresh randomness independent of the division seeding - Move ProjectedTeam interface, projectTeam(), and sortProjected() to module scope (defined once, not recreated on every buildConferenceBracket call) - Replace conference-level participant count check (east < 8 || west < 8) with per-division checks (each division < 3) for a more actionable error message - Add logger.warn when standings.length === 0 so admins know the sim is running without synced data - Remove unused easternTeams/westernTeams variables - Simplify test: replace Object.fromEntries pattern with a plain for..of loop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix oxlint errors in NHL simulator and tests - Replace non-null assertions (data!) with optional chaining (data?.x ?? "") - Move makeEntry helper to module scope to avoid recreating on every call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:33:33 -04:00
"Tampa Bay Lightning": { conference: "Eastern", division: "Atlantic", elo: 1587 },
"Buffalo Sabres": { conference: "Eastern", division: "Atlantic", elo: 1572 },
"Montreal Canadiens": { conference: "Eastern", division: "Atlantic", elo: 1527 },
"Ottawa Senators": { conference: "Eastern", division: "Atlantic", elo: 1542 },
"Detroit Red Wings": { conference: "Eastern", division: "Atlantic", elo: 1506 },
"Boston Bruins": { conference: "Eastern", division: "Atlantic", elo: 1501 },
"Florida Panthers": { conference: "Eastern", division: "Atlantic", elo: 1505 },
"Toronto Maple Leafs": { conference: "Eastern", division: "Atlantic", elo: 1479 },
// ── Eastern Conference — Metropolitan ─────────────────────────────────────
Fix NHL simulator seeding bugs and code quality issues (#281) * Fix NHL simulator dynamic seeding and code quality issues - Replace hardcoded seeding probabilities with standings-based Monte Carlo projection: simulates remaining regular season games per team using Elo win probability vs. a league-average opponent, so mathematically eliminated teams naturally fall out of playoff contention - Fix double-simulation bug in wildcard pool: all 16 conference teams are now projected exactly once via sortProjected([...divA, ...divB].map(projectTeam)), then filtered — wildcard pool no longer re-runs simulateProjectedPoints with fresh randomness independent of the division seeding - Move ProjectedTeam interface, projectTeam(), and sortProjected() to module scope (defined once, not recreated on every buildConferenceBracket call) - Replace conference-level participant count check (east < 8 || west < 8) with per-division checks (each division < 3) for a more actionable error message - Add logger.warn when standings.length === 0 so admins know the sim is running without synced data - Remove unused easternTeams/westernTeams variables - Simplify test: replace Object.fromEntries pattern with a plain for..of loop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix oxlint errors in NHL simulator and tests - Replace non-null assertions (data!) with optional chaining (data?.x ?? "") - Move makeEntry helper to module scope to avoid recreating on every call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:33:33 -04:00
"Carolina Hurricanes": { conference: "Eastern", division: "Metropolitan", elo: 1574 },
"Columbus Blue Jackets":{ conference: "Eastern", division: "Metropolitan", elo: 1530 },
"Pittsburgh Penguins": { conference: "Eastern", division: "Metropolitan", elo: 1518 },
"New York Islanders": { conference: "Eastern", division: "Metropolitan", elo: 1513 },
"Philadelphia Flyers": { conference: "Eastern", division: "Metropolitan", elo: 1473 },
"Washington Capitals": { conference: "Eastern", division: "Metropolitan", elo: 1506 },
"New Jersey Devils": { conference: "Eastern", division: "Metropolitan", elo: 1488 },
"New York Rangers": { conference: "Eastern", division: "Metropolitan", elo: 1480 },
// ── Western Conference — Central ───────────────────────────────────────────
Fix NHL simulator seeding bugs and code quality issues (#281) * Fix NHL simulator dynamic seeding and code quality issues - Replace hardcoded seeding probabilities with standings-based Monte Carlo projection: simulates remaining regular season games per team using Elo win probability vs. a league-average opponent, so mathematically eliminated teams naturally fall out of playoff contention - Fix double-simulation bug in wildcard pool: all 16 conference teams are now projected exactly once via sortProjected([...divA, ...divB].map(projectTeam)), then filtered — wildcard pool no longer re-runs simulateProjectedPoints with fresh randomness independent of the division seeding - Move ProjectedTeam interface, projectTeam(), and sortProjected() to module scope (defined once, not recreated on every buildConferenceBracket call) - Replace conference-level participant count check (east < 8 || west < 8) with per-division checks (each division < 3) for a more actionable error message - Add logger.warn when standings.length === 0 so admins know the sim is running without synced data - Remove unused easternTeams/westernTeams variables - Simplify test: replace Object.fromEntries pattern with a plain for..of loop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix oxlint errors in NHL simulator and tests - Replace non-null assertions (data!) with optional chaining (data?.x ?? "") - Move makeEntry helper to module scope to avoid recreating on every call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:33:33 -04:00
"Colorado Avalanche": { conference: "Western", division: "Central", elo: 1594 },
"Dallas Stars": { conference: "Western", division: "Central", elo: 1581 },
"Minnesota Wild": { conference: "Western", division: "Central", elo: 1548 },
"Utah Mammoth": { conference: "Western", division: "Central", elo: 1529 },
"Nashville Predators": { conference: "Western", division: "Central", elo: 1471 },
"Winnipeg Jets": { conference: "Western", division: "Central", elo: 1483 },
"St. Louis Blues": { conference: "Western", division: "Central", elo: 1473 },
"Chicago Blackhawks": { conference: "Western", division: "Central", elo: 1411 },
// ── Western Conference — Pacific ───────────────────────────────────────────
Fix NHL simulator seeding bugs and code quality issues (#281) * Fix NHL simulator dynamic seeding and code quality issues - Replace hardcoded seeding probabilities with standings-based Monte Carlo projection: simulates remaining regular season games per team using Elo win probability vs. a league-average opponent, so mathematically eliminated teams naturally fall out of playoff contention - Fix double-simulation bug in wildcard pool: all 16 conference teams are now projected exactly once via sortProjected([...divA, ...divB].map(projectTeam)), then filtered — wildcard pool no longer re-runs simulateProjectedPoints with fresh randomness independent of the division seeding - Move ProjectedTeam interface, projectTeam(), and sortProjected() to module scope (defined once, not recreated on every buildConferenceBracket call) - Replace conference-level participant count check (east < 8 || west < 8) with per-division checks (each division < 3) for a more actionable error message - Add logger.warn when standings.length === 0 so admins know the sim is running without synced data - Remove unused easternTeams/westernTeams variables - Simplify test: replace Object.fromEntries pattern with a plain for..of loop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix oxlint errors in NHL simulator and tests - Replace non-null assertions (data!) with optional chaining (data?.x ?? "") - Move makeEntry helper to module scope to avoid recreating on every call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:33:33 -04:00
"Anaheim Ducks": { conference: "Western", division: "Pacific", elo: 1490 },
"Edmonton Oilers": { conference: "Western", division: "Pacific", elo: 1531 },
"Vegas Golden Knights": { conference: "Western", division: "Pacific", elo: 1519 },
"Los Angeles Kings": { conference: "Western", division: "Pacific", elo: 1482 },
"San Jose Sharks": { conference: "Western", division: "Pacific", elo: 1455 },
"Seattle Kraken": { conference: "Western", division: "Pacific", elo: 1469 },
"Calgary Flames": { conference: "Western", division: "Pacific", elo: 1436 },
"Vancouver Canucks": { conference: "Western", division: "Pacific", elo: 1403 },
};
// ─── Public helpers (exported for unit testing) ───────────────────────────────
/** Normalize a team name for lookup (lowercase, trimmed, collapsed whitespace). */
export function normalizeTeamName(name: string): string {
return name.toLowerCase().trim().replace(/\s+/g, " ");
}
/** Look up team data by participant name (case-insensitive). */
export function getTeamData(name: string): NhlTeamData | undefined {
const normalized = normalizeTeamName(name);
for (const [teamName, data] of Object.entries(TEAMS_DATA)) {
if (normalizeTeamName(teamName) === normalized) return data;
}
return undefined;
}
/**
* Elo win probability for team A over team B.
* P(A) = 1 / (1 + 10^((eloB - eloA) / PARITY_FACTOR))
* Exported for unit testing.
*/
export function eloWinProbability(eloA: number, eloB: number): number {
return 1 / (1 + Math.pow(10, (eloB - eloA) / PARITY_FACTOR));
}
// ─── Internal types ───────────────────────────────────────────────────────────
interface TeamEntry {
id: string;
name: string;
data: NhlTeamData | undefined;
Fix NHL simulator seeding bugs and code quality issues (#281) * Fix NHL simulator dynamic seeding and code quality issues - Replace hardcoded seeding probabilities with standings-based Monte Carlo projection: simulates remaining regular season games per team using Elo win probability vs. a league-average opponent, so mathematically eliminated teams naturally fall out of playoff contention - Fix double-simulation bug in wildcard pool: all 16 conference teams are now projected exactly once via sortProjected([...divA, ...divB].map(projectTeam)), then filtered — wildcard pool no longer re-runs simulateProjectedPoints with fresh randomness independent of the division seeding - Move ProjectedTeam interface, projectTeam(), and sortProjected() to module scope (defined once, not recreated on every buildConferenceBracket call) - Replace conference-level participant count check (east < 8 || west < 8) with per-division checks (each division < 3) for a more actionable error message - Add logger.warn when standings.length === 0 so admins know the sim is running without synced data - Remove unused easternTeams/westernTeams variables - Simplify test: replace Object.fromEntries pattern with a plain for..of loop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix oxlint errors in NHL simulator and tests - Replace non-null assertions (data!) with optional chaining (data?.x ?? "") - Move makeEntry helper to module scope to avoid recreating on every call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:33:33 -04:00
conference: "Eastern" | "Western";
division: string;
/** Current regular season points: wins × 2 + OT losses × 1. */
currentPoints: number;
/** Remaining regular season games = 82 gamesPlayed. */
remainingGames: number;
/** Win probability vs. a league-average opponent (Elo 1500). Pre-computed. */
winProb: number;
}
Fix oxlint warnings: no-shadow, consistent-function-scoping, no-non-null-assertion, and others (#196) * Fix no-shadow and consistent-function-scoping lint violations Resolves all 11 no-shadow and 16 consistent-function-scoping oxlint warnings and promotes both rules to errors in .oxlintrc.json. no-shadow: renamed Drizzle callback params (sports→s, matches→m, seasons→s) to avoid shadowing outer imports; removed shadowed destructures (eq, inArray) from where callbacks; renamed inner template→bracketTemplate, prev→currentTimers, season→ss, name→teamName (with name: teamName fix to preserve semantics). consistent-function-scoping: moved formatDate, getRankBadge, getMovementIndicator, getPositionBadge, getStatusBadge, toDateStr, elo (×2), weightedPick, sortByMatchNumber (×2) to module scope; moved formatTime (×2), isValidLeagueName, getDraftTimes, makeSeasonQueues to file scope in test files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix no-non-null-assertion lint violations and promote to error Eliminates all 208 no-non-null-assertion warnings across 38 files. Promotes typescript/no-non-null-assertion from warn to error in .oxlintrc.json. Fix patterns applied: - Map.get(key)! after .has() check → extract with get() + null guard - Map.get(key)! on pre-populated count maps → ?? 0 default - .set(id, map.get(id)! + 1) increment → ?? 0 before adding - participant1Id!/participant2Id! on DB matches → ?? "" fallback - array.find()! in tests → guard + throw or expect().toBeDefined() - bracketTemplateCache.get(id)! → null guard extract - Various nullable field accesses → optional chain or ?? default Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix prefer-add-event-listener, no-unassigned-import, require-module-specifiers Resolves all 9 remaining non-console lint warnings and promotes all three rules to errors in .oxlintrc.json. - prefer-add-event-listener: converted onchange/onclick/onload assignments to addEventListener in useDraftNotifications.ts and admin.data-sync.tsx; stored changeHandler ref for proper cleanup with removeEventListener - no-unassigned-import: configured rule with allow list for legitimate side-effect imports (*.css, @testing-library/jest-dom, @testing-library/cypress/add-commands) - require-module-specifiers: removed redundant `export {}` from cypress/support/e2e.ts (file already has an import) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix TypeScript errors from no-non-null-assertion fixes Two fixes introduced by the non-null assertion cleanup produced type errors: - scoring-event.ts: `?? ""` was wrong type for a participant object map; restructured to explicit null guards so TypeScript can narrow correctly - standings-sync/index.ts: `?? null` after name-match lookup lost the truthy guarantee, causing TS18047 on the write-back block; added `participant &&` guard before accessing its properties Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add npm run typecheck as Stop hook in Claude settings Runs a full project typecheck at the end of each Claude turn so type errors surface as feedback before the next message. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 10:59:51 -07:00
/** Get Elo for a team entry. Fallback 1400 for unknown teams. */
function elo(entry: TeamEntry): number {
return entry.data?.elo ?? 1400;
}
/**
Fix NHL simulator seeding bugs and code quality issues (#281) * Fix NHL simulator dynamic seeding and code quality issues - Replace hardcoded seeding probabilities with standings-based Monte Carlo projection: simulates remaining regular season games per team using Elo win probability vs. a league-average opponent, so mathematically eliminated teams naturally fall out of playoff contention - Fix double-simulation bug in wildcard pool: all 16 conference teams are now projected exactly once via sortProjected([...divA, ...divB].map(projectTeam)), then filtered — wildcard pool no longer re-runs simulateProjectedPoints with fresh randomness independent of the division seeding - Move ProjectedTeam interface, projectTeam(), and sortProjected() to module scope (defined once, not recreated on every buildConferenceBracket call) - Replace conference-level participant count check (east < 8 || west < 8) with per-division checks (each division < 3) for a more actionable error message - Add logger.warn when standings.length === 0 so admins know the sim is running without synced data - Remove unused easternTeams/westernTeams variables - Simplify test: replace Object.fromEntries pattern with a plain for..of loop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix oxlint errors in NHL simulator and tests - Replace non-null assertions (data!) with optional chaining (data?.x ?? "") - Move makeEntry helper to module scope to avoid recreating on every call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:33:33 -04:00
* Simulate remaining regular season games for one team.
* Each game:
* - Win (2 pts) with probability winProb
* - OT/SO loss (1 pt) with probability (1 winProb) × NHL_OT_RATE
* - Regulation loss (0 pts) otherwise
* Returns projected total points for the season.
Fix oxlint warnings: no-shadow, consistent-function-scoping, no-non-null-assertion, and others (#196) * Fix no-shadow and consistent-function-scoping lint violations Resolves all 11 no-shadow and 16 consistent-function-scoping oxlint warnings and promotes both rules to errors in .oxlintrc.json. no-shadow: renamed Drizzle callback params (sports→s, matches→m, seasons→s) to avoid shadowing outer imports; removed shadowed destructures (eq, inArray) from where callbacks; renamed inner template→bracketTemplate, prev→currentTimers, season→ss, name→teamName (with name: teamName fix to preserve semantics). consistent-function-scoping: moved formatDate, getRankBadge, getMovementIndicator, getPositionBadge, getStatusBadge, toDateStr, elo (×2), weightedPick, sortByMatchNumber (×2) to module scope; moved formatTime (×2), isValidLeagueName, getDraftTimes, makeSeasonQueues to file scope in test files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix no-non-null-assertion lint violations and promote to error Eliminates all 208 no-non-null-assertion warnings across 38 files. Promotes typescript/no-non-null-assertion from warn to error in .oxlintrc.json. Fix patterns applied: - Map.get(key)! after .has() check → extract with get() + null guard - Map.get(key)! on pre-populated count maps → ?? 0 default - .set(id, map.get(id)! + 1) increment → ?? 0 before adding - participant1Id!/participant2Id! on DB matches → ?? "" fallback - array.find()! in tests → guard + throw or expect().toBeDefined() - bracketTemplateCache.get(id)! → null guard extract - Various nullable field accesses → optional chain or ?? default Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix prefer-add-event-listener, no-unassigned-import, require-module-specifiers Resolves all 9 remaining non-console lint warnings and promotes all three rules to errors in .oxlintrc.json. - prefer-add-event-listener: converted onchange/onclick/onload assignments to addEventListener in useDraftNotifications.ts and admin.data-sync.tsx; stored changeHandler ref for proper cleanup with removeEventListener - no-unassigned-import: configured rule with allow list for legitimate side-effect imports (*.css, @testing-library/jest-dom, @testing-library/cypress/add-commands) - require-module-specifiers: removed redundant `export {}` from cypress/support/e2e.ts (file already has an import) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix TypeScript errors from no-non-null-assertion fixes Two fixes introduced by the non-null assertion cleanup produced type errors: - scoring-event.ts: `?? ""` was wrong type for a participant object map; restructured to explicit null guards so TypeScript can narrow correctly - standings-sync/index.ts: `?? null` after name-match lookup lost the truthy guarantee, causing TS18047 on the write-back block; added `participant &&` guard before accessing its properties Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add npm run typecheck as Stop hook in Claude settings Runs a full project typecheck at the end of each Claude turn so type errors surface as feedback before the next message. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 10:59:51 -07:00
*/
Fix NHL simulator seeding bugs and code quality issues (#281) * Fix NHL simulator dynamic seeding and code quality issues - Replace hardcoded seeding probabilities with standings-based Monte Carlo projection: simulates remaining regular season games per team using Elo win probability vs. a league-average opponent, so mathematically eliminated teams naturally fall out of playoff contention - Fix double-simulation bug in wildcard pool: all 16 conference teams are now projected exactly once via sortProjected([...divA, ...divB].map(projectTeam)), then filtered — wildcard pool no longer re-runs simulateProjectedPoints with fresh randomness independent of the division seeding - Move ProjectedTeam interface, projectTeam(), and sortProjected() to module scope (defined once, not recreated on every buildConferenceBracket call) - Replace conference-level participant count check (east < 8 || west < 8) with per-division checks (each division < 3) for a more actionable error message - Add logger.warn when standings.length === 0 so admins know the sim is running without synced data - Remove unused easternTeams/westernTeams variables - Simplify test: replace Object.fromEntries pattern with a plain for..of loop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix oxlint errors in NHL simulator and tests - Replace non-null assertions (data!) with optional chaining (data?.x ?? "") - Move makeEntry helper to module scope to avoid recreating on every call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:33:33 -04:00
export function simulateProjectedPoints(entry: TeamEntry): number {
let extra = 0;
const { winProb, remainingGames } = entry;
const otlProb = (1 - winProb) * NHL_OT_RATE;
for (let g = 0; g < remainingGames; g++) {
const r = Math.random();
if (r < winProb) {
extra += 2;
} else if (r < winProb + otlProb) {
extra += 1;
}
Fix oxlint warnings: no-shadow, consistent-function-scoping, no-non-null-assertion, and others (#196) * Fix no-shadow and consistent-function-scoping lint violations Resolves all 11 no-shadow and 16 consistent-function-scoping oxlint warnings and promotes both rules to errors in .oxlintrc.json. no-shadow: renamed Drizzle callback params (sports→s, matches→m, seasons→s) to avoid shadowing outer imports; removed shadowed destructures (eq, inArray) from where callbacks; renamed inner template→bracketTemplate, prev→currentTimers, season→ss, name→teamName (with name: teamName fix to preserve semantics). consistent-function-scoping: moved formatDate, getRankBadge, getMovementIndicator, getPositionBadge, getStatusBadge, toDateStr, elo (×2), weightedPick, sortByMatchNumber (×2) to module scope; moved formatTime (×2), isValidLeagueName, getDraftTimes, makeSeasonQueues to file scope in test files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix no-non-null-assertion lint violations and promote to error Eliminates all 208 no-non-null-assertion warnings across 38 files. Promotes typescript/no-non-null-assertion from warn to error in .oxlintrc.json. Fix patterns applied: - Map.get(key)! after .has() check → extract with get() + null guard - Map.get(key)! on pre-populated count maps → ?? 0 default - .set(id, map.get(id)! + 1) increment → ?? 0 before adding - participant1Id!/participant2Id! on DB matches → ?? "" fallback - array.find()! in tests → guard + throw or expect().toBeDefined() - bracketTemplateCache.get(id)! → null guard extract - Various nullable field accesses → optional chain or ?? default Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix prefer-add-event-listener, no-unassigned-import, require-module-specifiers Resolves all 9 remaining non-console lint warnings and promotes all three rules to errors in .oxlintrc.json. - prefer-add-event-listener: converted onchange/onclick/onload assignments to addEventListener in useDraftNotifications.ts and admin.data-sync.tsx; stored changeHandler ref for proper cleanup with removeEventListener - no-unassigned-import: configured rule with allow list for legitimate side-effect imports (*.css, @testing-library/jest-dom, @testing-library/cypress/add-commands) - require-module-specifiers: removed redundant `export {}` from cypress/support/e2e.ts (file already has an import) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix TypeScript errors from no-non-null-assertion fixes Two fixes introduced by the non-null assertion cleanup produced type errors: - scoring-event.ts: `?? ""` was wrong type for a participant object map; restructured to explicit null guards so TypeScript can narrow correctly - standings-sync/index.ts: `?? null` after name-match lookup lost the truthy guarantee, causing TS18047 on the write-back block; added `participant &&` guard before accessing its properties Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add npm run typecheck as Stop hook in Claude settings Runs a full project typecheck at the end of each Claude turn so type errors surface as feedback before the next message. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 10:59:51 -07:00
}
Fix NHL simulator seeding bugs and code quality issues (#281) * Fix NHL simulator dynamic seeding and code quality issues - Replace hardcoded seeding probabilities with standings-based Monte Carlo projection: simulates remaining regular season games per team using Elo win probability vs. a league-average opponent, so mathematically eliminated teams naturally fall out of playoff contention - Fix double-simulation bug in wildcard pool: all 16 conference teams are now projected exactly once via sortProjected([...divA, ...divB].map(projectTeam)), then filtered — wildcard pool no longer re-runs simulateProjectedPoints with fresh randomness independent of the division seeding - Move ProjectedTeam interface, projectTeam(), and sortProjected() to module scope (defined once, not recreated on every buildConferenceBracket call) - Replace conference-level participant count check (east < 8 || west < 8) with per-division checks (each division < 3) for a more actionable error message - Add logger.warn when standings.length === 0 so admins know the sim is running without synced data - Remove unused easternTeams/westernTeams variables - Simplify test: replace Object.fromEntries pattern with a plain for..of loop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix oxlint errors in NHL simulator and tests - Replace non-null assertions (data!) with optional chaining (data?.x ?? "") - Move makeEntry helper to module scope to avoid recreating on every call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:33:33 -04:00
return entry.currentPoints + extra;
}
// ─── Projected-points helpers (module-level for hot-loop efficiency) ──────────
interface ProjectedTeam {
team: TeamEntry;
pts: number;
tb: number;
}
/** Project one team's end-of-season point total with a random tiebreaker. */
function projectTeam(t: TeamEntry): ProjectedTeam {
return { team: t, pts: simulateProjectedPoints(t), tb: Math.random() };
}
/** Sort projected teams descending by pts, then by random tiebreaker. Mutates arr. */
function sortProjected(arr: ProjectedTeam[]): ProjectedTeam[] {
return arr.toSorted((a, b) => b.pts - a.pts || b.tb - a.tb);
Fix oxlint warnings: no-shadow, consistent-function-scoping, no-non-null-assertion, and others (#196) * Fix no-shadow and consistent-function-scoping lint violations Resolves all 11 no-shadow and 16 consistent-function-scoping oxlint warnings and promotes both rules to errors in .oxlintrc.json. no-shadow: renamed Drizzle callback params (sports→s, matches→m, seasons→s) to avoid shadowing outer imports; removed shadowed destructures (eq, inArray) from where callbacks; renamed inner template→bracketTemplate, prev→currentTimers, season→ss, name→teamName (with name: teamName fix to preserve semantics). consistent-function-scoping: moved formatDate, getRankBadge, getMovementIndicator, getPositionBadge, getStatusBadge, toDateStr, elo (×2), weightedPick, sortByMatchNumber (×2) to module scope; moved formatTime (×2), isValidLeagueName, getDraftTimes, makeSeasonQueues to file scope in test files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix no-non-null-assertion lint violations and promote to error Eliminates all 208 no-non-null-assertion warnings across 38 files. Promotes typescript/no-non-null-assertion from warn to error in .oxlintrc.json. Fix patterns applied: - Map.get(key)! after .has() check → extract with get() + null guard - Map.get(key)! on pre-populated count maps → ?? 0 default - .set(id, map.get(id)! + 1) increment → ?? 0 before adding - participant1Id!/participant2Id! on DB matches → ?? "" fallback - array.find()! in tests → guard + throw or expect().toBeDefined() - bracketTemplateCache.get(id)! → null guard extract - Various nullable field accesses → optional chain or ?? default Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix prefer-add-event-listener, no-unassigned-import, require-module-specifiers Resolves all 9 remaining non-console lint warnings and promotes all three rules to errors in .oxlintrc.json. - prefer-add-event-listener: converted onchange/onclick/onload assignments to addEventListener in useDraftNotifications.ts and admin.data-sync.tsx; stored changeHandler ref for proper cleanup with removeEventListener - no-unassigned-import: configured rule with allow list for legitimate side-effect imports (*.css, @testing-library/jest-dom, @testing-library/cypress/add-commands) - require-module-specifiers: removed redundant `export {}` from cypress/support/e2e.ts (file already has an import) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix TypeScript errors from no-non-null-assertion fixes Two fixes introduced by the non-null assertion cleanup produced type errors: - scoring-event.ts: `?? ""` was wrong type for a participant object map; restructured to explicit null guards so TypeScript can narrow correctly - standings-sync/index.ts: `?? null` after name-match lookup lost the truthy guarantee, causing TS18047 on the write-back block; added `participant &&` guard before accessing its properties Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add npm run typecheck as Stop hook in Claude settings Runs a full project typecheck at the end of each Claude turn so type errors surface as feedback before the next message. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 10:59:51 -07:00
}
// ─── Simulator ────────────────────────────────────────────────────────────────
export class NHLSimulator implements Simulator {
async simulate(sportsSeasonId: string): Promise<SimulationResult[]> {
const db = database();
Fix NHL simulator seeding bugs and code quality issues (#281) * Fix NHL simulator dynamic seeding and code quality issues - Replace hardcoded seeding probabilities with standings-based Monte Carlo projection: simulates remaining regular season games per team using Elo win probability vs. a league-average opponent, so mathematically eliminated teams naturally fall out of playoff contention - Fix double-simulation bug in wildcard pool: all 16 conference teams are now projected exactly once via sortProjected([...divA, ...divB].map(projectTeam)), then filtered — wildcard pool no longer re-runs simulateProjectedPoints with fresh randomness independent of the division seeding - Move ProjectedTeam interface, projectTeam(), and sortProjected() to module scope (defined once, not recreated on every buildConferenceBracket call) - Replace conference-level participant count check (east < 8 || west < 8) with per-division checks (each division < 3) for a more actionable error message - Add logger.warn when standings.length === 0 so admins know the sim is running without synced data - Remove unused easternTeams/westernTeams variables - Simplify test: replace Object.fromEntries pattern with a plain for..of loop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix oxlint errors in NHL simulator and tests - Replace non-null assertions (data!) with optional chaining (data?.x ?? "") - Move makeEntry helper to module scope to avoid recreating on every call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:33:33 -04:00
// 1. Load participants and standings in parallel.
const [participantRows, standings] = await Promise.all([
db
.select({ id: schema.participants.id, name: schema.participants.name })
.from(schema.participants)
.where(eq(schema.participants.sportsSeasonId, sportsSeasonId)),
getRegularSeasonStandings(sportsSeasonId),
]);
if (participantRows.length === 0) {
throw new Error(
`No participants found for sports season ${sportsSeasonId}. ` +
`Add NHL teams as participants before running simulation.`
);
}
const participantIds = participantRows.map((r) => r.id);
Fix NHL simulator seeding bugs and code quality issues (#281) * Fix NHL simulator dynamic seeding and code quality issues - Replace hardcoded seeding probabilities with standings-based Monte Carlo projection: simulates remaining regular season games per team using Elo win probability vs. a league-average opponent, so mathematically eliminated teams naturally fall out of playoff contention - Fix double-simulation bug in wildcard pool: all 16 conference teams are now projected exactly once via sortProjected([...divA, ...divB].map(projectTeam)), then filtered — wildcard pool no longer re-runs simulateProjectedPoints with fresh randomness independent of the division seeding - Move ProjectedTeam interface, projectTeam(), and sortProjected() to module scope (defined once, not recreated on every buildConferenceBracket call) - Replace conference-level participant count check (east < 8 || west < 8) with per-division checks (each division < 3) for a more actionable error message - Add logger.warn when standings.length === 0 so admins know the sim is running without synced data - Remove unused easternTeams/westernTeams variables - Simplify test: replace Object.fromEntries pattern with a plain for..of loop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix oxlint errors in NHL simulator and tests - Replace non-null assertions (data!) with optional chaining (data?.x ?? "") - Move makeEntry helper to module scope to avoid recreating on every call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:33:33 -04:00
// 2. Build standings lookup and construct team entries.
const standingsMap = new Map(standings.map((s) => [s.participantId, s]));
if (standings.length === 0) {
logger.warn(
`[NHLSimulator] No standings data found for sports season ${sportsSeasonId}. ` +
`Simulation will use 0 current points and 82 remaining games for all teams. ` +
`Sync standings before running for accurate results.`
);
}
const teams: TeamEntry[] = participantRows.map((r) => {
const standing = standingsMap.get(r.id);
const data = getTeamData(r.name);
// Conference and division: prefer standings table, fall back to TEAMS_DATA.
const rawConf = standing?.conference;
const conference: "Eastern" | "Western" =
rawConf === "Eastern" || rawConf === "Western"
? rawConf
: (data?.conference ?? "Eastern");
const division: string = standing?.division ?? data?.division ?? "";
const gamesPlayed = standing?.gamesPlayed ?? 0;
const wins = standing?.wins ?? 0;
const otLosses = standing?.otLosses ?? 0;
const currentPoints = wins * 2 + otLosses;
const remainingGames = Math.max(0, NHL_REGULAR_SEASON_GAMES - gamesPlayed);
return {
id: r.id,
name: r.name,
data,
conference,
division,
currentPoints,
remainingGames,
winProb: eloWinProbability(data?.elo ?? 1400, 1500),
};
});
// Warn about participants that don't match any hardcoded team.
const unrecognized = teams.filter((t) => !t.data);
if (unrecognized.length > 0) {
logger.warn(
Fix NHL simulator seeding bugs and code quality issues (#281) * Fix NHL simulator dynamic seeding and code quality issues - Replace hardcoded seeding probabilities with standings-based Monte Carlo projection: simulates remaining regular season games per team using Elo win probability vs. a league-average opponent, so mathematically eliminated teams naturally fall out of playoff contention - Fix double-simulation bug in wildcard pool: all 16 conference teams are now projected exactly once via sortProjected([...divA, ...divB].map(projectTeam)), then filtered — wildcard pool no longer re-runs simulateProjectedPoints with fresh randomness independent of the division seeding - Move ProjectedTeam interface, projectTeam(), and sortProjected() to module scope (defined once, not recreated on every buildConferenceBracket call) - Replace conference-level participant count check (east < 8 || west < 8) with per-division checks (each division < 3) for a more actionable error message - Add logger.warn when standings.length === 0 so admins know the sim is running without synced data - Remove unused easternTeams/westernTeams variables - Simplify test: replace Object.fromEntries pattern with a plain for..of loop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix oxlint errors in NHL simulator and tests - Replace non-null assertions (data!) with optional chaining (data?.x ?? "") - Move makeEntry helper to module scope to avoid recreating on every call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:33:33 -04:00
`[NHLSimulator] ${unrecognized.length} participant(s) not found in TEAMS_DATA and will use fallback Elo 1400: ` +
unrecognized.map((t) => t.name).join(", ")
);
}
// Separate recognized teams by conference + division.
Fix NHL simulator seeding bugs and code quality issues (#281) * Fix NHL simulator dynamic seeding and code quality issues - Replace hardcoded seeding probabilities with standings-based Monte Carlo projection: simulates remaining regular season games per team using Elo win probability vs. a league-average opponent, so mathematically eliminated teams naturally fall out of playoff contention - Fix double-simulation bug in wildcard pool: all 16 conference teams are now projected exactly once via sortProjected([...divA, ...divB].map(projectTeam)), then filtered — wildcard pool no longer re-runs simulateProjectedPoints with fresh randomness independent of the division seeding - Move ProjectedTeam interface, projectTeam(), and sortProjected() to module scope (defined once, not recreated on every buildConferenceBracket call) - Replace conference-level participant count check (east < 8 || west < 8) with per-division checks (each division < 3) for a more actionable error message - Add logger.warn when standings.length === 0 so admins know the sim is running without synced data - Remove unused easternTeams/westernTeams variables - Simplify test: replace Object.fromEntries pattern with a plain for..of loop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix oxlint errors in NHL simulator and tests - Replace non-null assertions (data!) with optional chaining (data?.x ?? "") - Move makeEntry helper to module scope to avoid recreating on every call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:33:33 -04:00
const easternAtlantic = teams.filter((t) => t.conference === "Eastern" && t.division === "Atlantic");
const easternMetro = teams.filter((t) => t.conference === "Eastern" && t.division === "Metropolitan");
const westernCentral = teams.filter((t) => t.conference === "Western" && t.division === "Central");
const westernPacific = teams.filter((t) => t.conference === "Western" && t.division === "Pacific");
if (
easternAtlantic.length < 3 ||
easternMetro.length < 3 ||
westernCentral.length < 3 ||
westernPacific.length < 3
) {
throw new Error(
Fix NHL simulator seeding bugs and code quality issues (#281) * Fix NHL simulator dynamic seeding and code quality issues - Replace hardcoded seeding probabilities with standings-based Monte Carlo projection: simulates remaining regular season games per team using Elo win probability vs. a league-average opponent, so mathematically eliminated teams naturally fall out of playoff contention - Fix double-simulation bug in wildcard pool: all 16 conference teams are now projected exactly once via sortProjected([...divA, ...divB].map(projectTeam)), then filtered — wildcard pool no longer re-runs simulateProjectedPoints with fresh randomness independent of the division seeding - Move ProjectedTeam interface, projectTeam(), and sortProjected() to module scope (defined once, not recreated on every buildConferenceBracket call) - Replace conference-level participant count check (east < 8 || west < 8) with per-division checks (each division < 3) for a more actionable error message - Add logger.warn when standings.length === 0 so admins know the sim is running without synced data - Remove unused easternTeams/westernTeams variables - Simplify test: replace Object.fromEntries pattern with a plain for..of loop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix oxlint errors in NHL simulator and tests - Replace non-null assertions (data!) with optional chaining (data?.x ?? "") - Move makeEntry helper to module scope to avoid recreating on every call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:33:33 -04:00
`Each division needs at least 3 participants ` +
`(got Eastern/Atlantic: ${easternAtlantic.length}, Eastern/Metro: ${easternMetro.length}, ` +
`Western/Central: ${westernCentral.length}, Western/Pacific: ${westernPacific.length}). ` +
`Add all 32 NHL teams before running simulation.`
);
}
// ─── Futures odds blending ─────────────────────────────────────────────────
const evRows = await db
.select({
participantId: schema.participantExpectedValues.participantId,
sourceOdds: schema.participantExpectedValues.sourceOdds,
})
.from(schema.participantExpectedValues)
.where(eq(schema.participantExpectedValues.sportsSeasonId, sportsSeasonId));
const participantIdSet = new Set(participantIds);
const oddsRows = evRows.filter(
(r) => r.sourceOdds !== null && participantIdSet.has(r.participantId)
);
const hasOdds = oddsRows.length > 0;
const normalizedOddsMap = new Map<string, number>();
if (hasOdds) {
const rawProbs = oddsRows.map((r) =>
convertAmericanOddsToProbability(r.sourceOdds ?? 0)
);
const normalized = normalizeProbabilities(rawProbs);
oddsRows.forEach(({ participantId }, i) => {
normalizedOddsMap.set(participantId, normalized[i]);
});
}
// ─── Helpers (defined once, outside the hot loop) ─────────────────────────
/**
* Blended per-game win probability for team A over team B.
* When odds are available: 70% Elo + 30% vig-removed futures head-to-head.
*/
const gameWinProb = (a: TeamEntry, b: TeamEntry): number => {
const eloProb = eloWinProbability(elo(a), elo(b));
if (!hasOdds) return eloProb;
const o1 = normalizedOddsMap.get(a.id) ?? 0;
const o2 = normalizedOddsMap.get(b.id) ?? 0;
const oddsProb = o1 + o2 > 0 ? o1 / (o1 + o2) : 0.5;
return ELO_WEIGHT * eloProb + ODDS_WEIGHT * oddsProb;
};
/** Simulate a best-of-7 series. Returns winner and loser. */
const simSeries = (a: TeamEntry, b: TeamEntry): { winner: TeamEntry; loser: TeamEntry } => {
const winProb = gameWinProb(a, b);
let winsA = 0;
let winsB = 0;
while (winsA < 4 && winsB < 4) {
if (Math.random() < winProb) winsA++; else winsB++;
}
return winsA === 4 ? { winner: a, loser: b } : { winner: b, loser: a };
};
/**
* Build the 8-team playoff bracket for one conference.
*
Fix NHL simulator seeding bugs and code quality issues (#281) * Fix NHL simulator dynamic seeding and code quality issues - Replace hardcoded seeding probabilities with standings-based Monte Carlo projection: simulates remaining regular season games per team using Elo win probability vs. a league-average opponent, so mathematically eliminated teams naturally fall out of playoff contention - Fix double-simulation bug in wildcard pool: all 16 conference teams are now projected exactly once via sortProjected([...divA, ...divB].map(projectTeam)), then filtered — wildcard pool no longer re-runs simulateProjectedPoints with fresh randomness independent of the division seeding - Move ProjectedTeam interface, projectTeam(), and sortProjected() to module scope (defined once, not recreated on every buildConferenceBracket call) - Replace conference-level participant count check (east < 8 || west < 8) with per-division checks (each division < 3) for a more actionable error message - Add logger.warn when standings.length === 0 so admins know the sim is running without synced data - Remove unused easternTeams/westernTeams variables - Simplify test: replace Object.fromEntries pattern with a plain for..of loop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix oxlint errors in NHL simulator and tests - Replace non-null assertions (data!) with optional chaining (data?.x ?? "") - Move makeEntry helper to module scope to avoid recreating on every call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:33:33 -04:00
* For each sim iteration:
* 1. Simulate projected points for all teams in both divisions.
* 2. Top 3 per division (by projected pts + random tiebreaker) division seeds.
* 3. Top 2 remaining conference teams WC1 (more pts) and WC2 (fewer pts).
* 4. Division winner with more pts = 1st seed (faces WC2);
* other division winner = 2nd seed (faces WC1).
*
Fix NHL simulator seeding bugs and code quality issues (#281) * Fix NHL simulator dynamic seeding and code quality issues - Replace hardcoded seeding probabilities with standings-based Monte Carlo projection: simulates remaining regular season games per team using Elo win probability vs. a league-average opponent, so mathematically eliminated teams naturally fall out of playoff contention - Fix double-simulation bug in wildcard pool: all 16 conference teams are now projected exactly once via sortProjected([...divA, ...divB].map(projectTeam)), then filtered — wildcard pool no longer re-runs simulateProjectedPoints with fresh randomness independent of the division seeding - Move ProjectedTeam interface, projectTeam(), and sortProjected() to module scope (defined once, not recreated on every buildConferenceBracket call) - Replace conference-level participant count check (east < 8 || west < 8) with per-division checks (each division < 3) for a more actionable error message - Add logger.warn when standings.length === 0 so admins know the sim is running without synced data - Remove unused easternTeams/westernTeams variables - Simplify test: replace Object.fromEntries pattern with a plain for..of loop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix oxlint errors in NHL simulator and tests - Replace non-null assertions (data!) with optional chaining (data?.x ?? "") - Move makeEntry helper to module scope to avoid recreating on every call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:33:33 -04:00
* Returns [m0, m1, m2, m3] where each element is [higher-seed, lower-seed],
* or undefined if the conference doesn't have enough teams for a valid bracket.
*/
const buildConferenceBracket = (
divATeams: TeamEntry[],
Fix NHL simulator seeding bugs and code quality issues (#281) * Fix NHL simulator dynamic seeding and code quality issues - Replace hardcoded seeding probabilities with standings-based Monte Carlo projection: simulates remaining regular season games per team using Elo win probability vs. a league-average opponent, so mathematically eliminated teams naturally fall out of playoff contention - Fix double-simulation bug in wildcard pool: all 16 conference teams are now projected exactly once via sortProjected([...divA, ...divB].map(projectTeam)), then filtered — wildcard pool no longer re-runs simulateProjectedPoints with fresh randomness independent of the division seeding - Move ProjectedTeam interface, projectTeam(), and sortProjected() to module scope (defined once, not recreated on every buildConferenceBracket call) - Replace conference-level participant count check (east < 8 || west < 8) with per-division checks (each division < 3) for a more actionable error message - Add logger.warn when standings.length === 0 so admins know the sim is running without synced data - Remove unused easternTeams/westernTeams variables - Simplify test: replace Object.fromEntries pattern with a plain for..of loop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix oxlint errors in NHL simulator and tests - Replace non-null assertions (data!) with optional chaining (data?.x ?? "") - Move makeEntry helper to module scope to avoid recreating on every call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:33:33 -04:00
divBTeams: TeamEntry[]
): [[TeamEntry, TeamEntry], [TeamEntry, TeamEntry], [TeamEntry, TeamEntry], [TeamEntry, TeamEntry]] | undefined => {
Fix NHL simulator seeding bugs and code quality issues (#281) * Fix NHL simulator dynamic seeding and code quality issues - Replace hardcoded seeding probabilities with standings-based Monte Carlo projection: simulates remaining regular season games per team using Elo win probability vs. a league-average opponent, so mathematically eliminated teams naturally fall out of playoff contention - Fix double-simulation bug in wildcard pool: all 16 conference teams are now projected exactly once via sortProjected([...divA, ...divB].map(projectTeam)), then filtered — wildcard pool no longer re-runs simulateProjectedPoints with fresh randomness independent of the division seeding - Move ProjectedTeam interface, projectTeam(), and sortProjected() to module scope (defined once, not recreated on every buildConferenceBracket call) - Replace conference-level participant count check (east < 8 || west < 8) with per-division checks (each division < 3) for a more actionable error message - Add logger.warn when standings.length === 0 so admins know the sim is running without synced data - Remove unused easternTeams/westernTeams variables - Simplify test: replace Object.fromEntries pattern with a plain for..of loop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix oxlint errors in NHL simulator and tests - Replace non-null assertions (data!) with optional chaining (data?.x ?? "") - Move makeEntry helper to module scope to avoid recreating on every call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:33:33 -04:00
if (divATeams.length < 3 || divBTeams.length < 3) return undefined;
Fix NHL simulator seeding bugs and code quality issues (#281) * Fix NHL simulator dynamic seeding and code quality issues - Replace hardcoded seeding probabilities with standings-based Monte Carlo projection: simulates remaining regular season games per team using Elo win probability vs. a league-average opponent, so mathematically eliminated teams naturally fall out of playoff contention - Fix double-simulation bug in wildcard pool: all 16 conference teams are now projected exactly once via sortProjected([...divA, ...divB].map(projectTeam)), then filtered — wildcard pool no longer re-runs simulateProjectedPoints with fresh randomness independent of the division seeding - Move ProjectedTeam interface, projectTeam(), and sortProjected() to module scope (defined once, not recreated on every buildConferenceBracket call) - Replace conference-level participant count check (east < 8 || west < 8) with per-division checks (each division < 3) for a more actionable error message - Add logger.warn when standings.length === 0 so admins know the sim is running without synced data - Remove unused easternTeams/westernTeams variables - Simplify test: replace Object.fromEntries pattern with a plain for..of loop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix oxlint errors in NHL simulator and tests - Replace non-null assertions (data!) with optional chaining (data?.x ?? "") - Move makeEntry helper to module scope to avoid recreating on every call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:33:33 -04:00
// Project all conference teams exactly once (single simulateProjectedPoints call per team).
const divASet = new Set(divATeams.map((t) => t.id));
const allProjected = sortProjected([...divATeams, ...divBTeams].map(projectTeam));
const divAProjected = allProjected.filter((p) => divASet.has(p.team.id));
const divBProjected = allProjected.filter((p) => !divASet.has(p.team.id));
Fix NHL simulator seeding bugs and code quality issues (#281) * Fix NHL simulator dynamic seeding and code quality issues - Replace hardcoded seeding probabilities with standings-based Monte Carlo projection: simulates remaining regular season games per team using Elo win probability vs. a league-average opponent, so mathematically eliminated teams naturally fall out of playoff contention - Fix double-simulation bug in wildcard pool: all 16 conference teams are now projected exactly once via sortProjected([...divA, ...divB].map(projectTeam)), then filtered — wildcard pool no longer re-runs simulateProjectedPoints with fresh randomness independent of the division seeding - Move ProjectedTeam interface, projectTeam(), and sortProjected() to module scope (defined once, not recreated on every buildConferenceBracket call) - Replace conference-level participant count check (east < 8 || west < 8) with per-division checks (each division < 3) for a more actionable error message - Add logger.warn when standings.length === 0 so admins know the sim is running without synced data - Remove unused easternTeams/westernTeams variables - Simplify test: replace Object.fromEntries pattern with a plain for..of loop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix oxlint errors in NHL simulator and tests - Replace non-null assertions (data!) with optional chaining (data?.x ?? "") - Move makeEntry helper to module scope to avoid recreating on every call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:33:33 -04:00
const divASeeds = divAProjected.slice(0, 3);
const divBSeeds = divBProjected.slice(0, 3);
Fix NHL simulator seeding bugs and code quality issues (#281) * Fix NHL simulator dynamic seeding and code quality issues - Replace hardcoded seeding probabilities with standings-based Monte Carlo projection: simulates remaining regular season games per team using Elo win probability vs. a league-average opponent, so mathematically eliminated teams naturally fall out of playoff contention - Fix double-simulation bug in wildcard pool: all 16 conference teams are now projected exactly once via sortProjected([...divA, ...divB].map(projectTeam)), then filtered — wildcard pool no longer re-runs simulateProjectedPoints with fresh randomness independent of the division seeding - Move ProjectedTeam interface, projectTeam(), and sortProjected() to module scope (defined once, not recreated on every buildConferenceBracket call) - Replace conference-level participant count check (east < 8 || west < 8) with per-division checks (each division < 3) for a more actionable error message - Add logger.warn when standings.length === 0 so admins know the sim is running without synced data - Remove unused easternTeams/westernTeams variables - Simplify test: replace Object.fromEntries pattern with a plain for..of loop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix oxlint errors in NHL simulator and tests - Replace non-null assertions (data!) with optional chaining (data?.x ?? "") - Move makeEntry helper to module scope to avoid recreating on every call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:33:33 -04:00
// Wildcard pool: non-division-seed teams, already sorted by projected pts.
const divisionSeedIds = new Set([...divASeeds, ...divBSeeds].map((p) => p.team.id));
const wcPool = allProjected.filter((p) => !divisionSeedIds.has(p.team.id));
Fix NHL simulator seeding bugs and code quality issues (#281) * Fix NHL simulator dynamic seeding and code quality issues - Replace hardcoded seeding probabilities with standings-based Monte Carlo projection: simulates remaining regular season games per team using Elo win probability vs. a league-average opponent, so mathematically eliminated teams naturally fall out of playoff contention - Fix double-simulation bug in wildcard pool: all 16 conference teams are now projected exactly once via sortProjected([...divA, ...divB].map(projectTeam)), then filtered — wildcard pool no longer re-runs simulateProjectedPoints with fresh randomness independent of the division seeding - Move ProjectedTeam interface, projectTeam(), and sortProjected() to module scope (defined once, not recreated on every buildConferenceBracket call) - Replace conference-level participant count check (east < 8 || west < 8) with per-division checks (each division < 3) for a more actionable error message - Add logger.warn when standings.length === 0 so admins know the sim is running without synced data - Remove unused easternTeams/westernTeams variables - Simplify test: replace Object.fromEntries pattern with a plain for..of loop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix oxlint errors in NHL simulator and tests - Replace non-null assertions (data!) with optional chaining (data?.x ?? "") - Move makeEntry helper to module scope to avoid recreating on every call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:33:33 -04:00
if (wcPool.length < 2) return undefined;
const [wc1, wc2] = wcPool; // wc1 has more pts → stronger wildcard
// Division winner with more projected points is the top seed (plays WC2).
const [topDiv, otherDiv] =
Fix NHL simulator seeding bugs and code quality issues (#281) * Fix NHL simulator dynamic seeding and code quality issues - Replace hardcoded seeding probabilities with standings-based Monte Carlo projection: simulates remaining regular season games per team using Elo win probability vs. a league-average opponent, so mathematically eliminated teams naturally fall out of playoff contention - Fix double-simulation bug in wildcard pool: all 16 conference teams are now projected exactly once via sortProjected([...divA, ...divB].map(projectTeam)), then filtered — wildcard pool no longer re-runs simulateProjectedPoints with fresh randomness independent of the division seeding - Move ProjectedTeam interface, projectTeam(), and sortProjected() to module scope (defined once, not recreated on every buildConferenceBracket call) - Replace conference-level participant count check (east < 8 || west < 8) with per-division checks (each division < 3) for a more actionable error message - Add logger.warn when standings.length === 0 so admins know the sim is running without synced data - Remove unused easternTeams/westernTeams variables - Simplify test: replace Object.fromEntries pattern with a plain for..of loop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix oxlint errors in NHL simulator and tests - Replace non-null assertions (data!) with optional chaining (data?.x ?? "") - Move makeEntry helper to module scope to avoid recreating on every call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:33:33 -04:00
divASeeds[0].pts >= divBSeeds[0].pts
? [divASeeds, divBSeeds]
: [divBSeeds, divASeeds];
return [
Fix NHL simulator seeding bugs and code quality issues (#281) * Fix NHL simulator dynamic seeding and code quality issues - Replace hardcoded seeding probabilities with standings-based Monte Carlo projection: simulates remaining regular season games per team using Elo win probability vs. a league-average opponent, so mathematically eliminated teams naturally fall out of playoff contention - Fix double-simulation bug in wildcard pool: all 16 conference teams are now projected exactly once via sortProjected([...divA, ...divB].map(projectTeam)), then filtered — wildcard pool no longer re-runs simulateProjectedPoints with fresh randomness independent of the division seeding - Move ProjectedTeam interface, projectTeam(), and sortProjected() to module scope (defined once, not recreated on every buildConferenceBracket call) - Replace conference-level participant count check (east < 8 || west < 8) with per-division checks (each division < 3) for a more actionable error message - Add logger.warn when standings.length === 0 so admins know the sim is running without synced data - Remove unused easternTeams/westernTeams variables - Simplify test: replace Object.fromEntries pattern with a plain for..of loop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix oxlint errors in NHL simulator and tests - Replace non-null assertions (data!) with optional chaining (data?.x ?? "") - Move makeEntry helper to module scope to avoid recreating on every call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:33:33 -04:00
[topDiv[0].team, wc2.team], // m0: 1st seed vs WC2
[otherDiv[0].team, wc1.team], // m1: 2nd seed vs WC1
[topDiv[1].team, topDiv[2].team], // m2: top div 2nd vs 3rd
[otherDiv[1].team, otherDiv[2].team], // m3: other div 2nd vs 3rd
];
};
// ─── Placement count maps ──────────────────────────────────────────────────
const championCounts = new Map<string, number>(participantIds.map((id) => [id, 0]));
const finalistCounts = new Map<string, number>(participantIds.map((id) => [id, 0]));
const confFinalLoserCounts = new Map<string, number>(participantIds.map((id) => [id, 0]));
const r2LoserCounts = new Map<string, number>(participantIds.map((id) => [id, 0]));
// ─── Monte Carlo simulation loop ───────────────────────────────────────────
let effectiveN = 0;
for (let s = 0; s < NUM_SIMULATIONS; s++) {
Fix NHL simulator seeding bugs and code quality issues (#281) * Fix NHL simulator dynamic seeding and code quality issues - Replace hardcoded seeding probabilities with standings-based Monte Carlo projection: simulates remaining regular season games per team using Elo win probability vs. a league-average opponent, so mathematically eliminated teams naturally fall out of playoff contention - Fix double-simulation bug in wildcard pool: all 16 conference teams are now projected exactly once via sortProjected([...divA, ...divB].map(projectTeam)), then filtered — wildcard pool no longer re-runs simulateProjectedPoints with fresh randomness independent of the division seeding - Move ProjectedTeam interface, projectTeam(), and sortProjected() to module scope (defined once, not recreated on every buildConferenceBracket call) - Replace conference-level participant count check (east < 8 || west < 8) with per-division checks (each division < 3) for a more actionable error message - Add logger.warn when standings.length === 0 so admins know the sim is running without synced data - Remove unused easternTeams/westernTeams variables - Simplify test: replace Object.fromEntries pattern with a plain for..of loop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix oxlint errors in NHL simulator and tests - Replace non-null assertions (data!) with optional chaining (data?.x ?? "") - Move makeEntry helper to module scope to avoid recreating on every call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:33:33 -04:00
const eastBracket = buildConferenceBracket(easternAtlantic, easternMetro);
const westBracket = buildConferenceBracket(westernCentral, westernPacific);
if (!eastBracket || !westBracket) continue;
effectiveN++;
// ── Eastern Conference ────────────────────────────────────────────────────
const eastR1Winners = eastBracket.map(([a, b]) => simSeries(a, b).winner);
const eastR2m1 = simSeries(eastR1Winners[0], eastR1Winners[2]);
const eastR2m2 = simSeries(eastR1Winners[1], eastR1Winners[3]);
const eastCF = simSeries(eastR2m1.winner, eastR2m2.winner);
const eastChamp = eastCF.winner;
Fix oxlint warnings: no-shadow, consistent-function-scoping, no-non-null-assertion, and others (#196) * Fix no-shadow and consistent-function-scoping lint violations Resolves all 11 no-shadow and 16 consistent-function-scoping oxlint warnings and promotes both rules to errors in .oxlintrc.json. no-shadow: renamed Drizzle callback params (sports→s, matches→m, seasons→s) to avoid shadowing outer imports; removed shadowed destructures (eq, inArray) from where callbacks; renamed inner template→bracketTemplate, prev→currentTimers, season→ss, name→teamName (with name: teamName fix to preserve semantics). consistent-function-scoping: moved formatDate, getRankBadge, getMovementIndicator, getPositionBadge, getStatusBadge, toDateStr, elo (×2), weightedPick, sortByMatchNumber (×2) to module scope; moved formatTime (×2), isValidLeagueName, getDraftTimes, makeSeasonQueues to file scope in test files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix no-non-null-assertion lint violations and promote to error Eliminates all 208 no-non-null-assertion warnings across 38 files. Promotes typescript/no-non-null-assertion from warn to error in .oxlintrc.json. Fix patterns applied: - Map.get(key)! after .has() check → extract with get() + null guard - Map.get(key)! on pre-populated count maps → ?? 0 default - .set(id, map.get(id)! + 1) increment → ?? 0 before adding - participant1Id!/participant2Id! on DB matches → ?? "" fallback - array.find()! in tests → guard + throw or expect().toBeDefined() - bracketTemplateCache.get(id)! → null guard extract - Various nullable field accesses → optional chain or ?? default Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix prefer-add-event-listener, no-unassigned-import, require-module-specifiers Resolves all 9 remaining non-console lint warnings and promotes all three rules to errors in .oxlintrc.json. - prefer-add-event-listener: converted onchange/onclick/onload assignments to addEventListener in useDraftNotifications.ts and admin.data-sync.tsx; stored changeHandler ref for proper cleanup with removeEventListener - no-unassigned-import: configured rule with allow list for legitimate side-effect imports (*.css, @testing-library/jest-dom, @testing-library/cypress/add-commands) - require-module-specifiers: removed redundant `export {}` from cypress/support/e2e.ts (file already has an import) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix TypeScript errors from no-non-null-assertion fixes Two fixes introduced by the non-null assertion cleanup produced type errors: - scoring-event.ts: `?? ""` was wrong type for a participant object map; restructured to explicit null guards so TypeScript can narrow correctly - standings-sync/index.ts: `?? null` after name-match lookup lost the truthy guarantee, causing TS18047 on the write-back block; added `participant &&` guard before accessing its properties Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add npm run typecheck as Stop hook in Claude settings Runs a full project typecheck at the end of each Claude turn so type errors surface as feedback before the next message. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 10:59:51 -07:00
confFinalLoserCounts.set(eastCF.loser.id, (confFinalLoserCounts.get(eastCF.loser.id) ?? 0) + 1);
// ── Western Conference ────────────────────────────────────────────────────
const westR1Winners = westBracket.map(([a, b]) => simSeries(a, b).winner);
const westR2m1 = simSeries(westR1Winners[0], westR1Winners[2]);
const westR2m2 = simSeries(westR1Winners[1], westR1Winners[3]);
const westCF = simSeries(westR2m1.winner, westR2m2.winner);
const westChamp = westCF.winner;
Fix oxlint warnings: no-shadow, consistent-function-scoping, no-non-null-assertion, and others (#196) * Fix no-shadow and consistent-function-scoping lint violations Resolves all 11 no-shadow and 16 consistent-function-scoping oxlint warnings and promotes both rules to errors in .oxlintrc.json. no-shadow: renamed Drizzle callback params (sports→s, matches→m, seasons→s) to avoid shadowing outer imports; removed shadowed destructures (eq, inArray) from where callbacks; renamed inner template→bracketTemplate, prev→currentTimers, season→ss, name→teamName (with name: teamName fix to preserve semantics). consistent-function-scoping: moved formatDate, getRankBadge, getMovementIndicator, getPositionBadge, getStatusBadge, toDateStr, elo (×2), weightedPick, sortByMatchNumber (×2) to module scope; moved formatTime (×2), isValidLeagueName, getDraftTimes, makeSeasonQueues to file scope in test files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix no-non-null-assertion lint violations and promote to error Eliminates all 208 no-non-null-assertion warnings across 38 files. Promotes typescript/no-non-null-assertion from warn to error in .oxlintrc.json. Fix patterns applied: - Map.get(key)! after .has() check → extract with get() + null guard - Map.get(key)! on pre-populated count maps → ?? 0 default - .set(id, map.get(id)! + 1) increment → ?? 0 before adding - participant1Id!/participant2Id! on DB matches → ?? "" fallback - array.find()! in tests → guard + throw or expect().toBeDefined() - bracketTemplateCache.get(id)! → null guard extract - Various nullable field accesses → optional chain or ?? default Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix prefer-add-event-listener, no-unassigned-import, require-module-specifiers Resolves all 9 remaining non-console lint warnings and promotes all three rules to errors in .oxlintrc.json. - prefer-add-event-listener: converted onchange/onclick/onload assignments to addEventListener in useDraftNotifications.ts and admin.data-sync.tsx; stored changeHandler ref for proper cleanup with removeEventListener - no-unassigned-import: configured rule with allow list for legitimate side-effect imports (*.css, @testing-library/jest-dom, @testing-library/cypress/add-commands) - require-module-specifiers: removed redundant `export {}` from cypress/support/e2e.ts (file already has an import) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix TypeScript errors from no-non-null-assertion fixes Two fixes introduced by the non-null assertion cleanup produced type errors: - scoring-event.ts: `?? ""` was wrong type for a participant object map; restructured to explicit null guards so TypeScript can narrow correctly - standings-sync/index.ts: `?? null` after name-match lookup lost the truthy guarantee, causing TS18047 on the write-back block; added `participant &&` guard before accessing its properties Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add npm run typecheck as Stop hook in Claude settings Runs a full project typecheck at the end of each Claude turn so type errors surface as feedback before the next message. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 10:59:51 -07:00
confFinalLoserCounts.set(westCF.loser.id, (confFinalLoserCounts.get(westCF.loser.id) ?? 0) + 1);
// ── Stanley Cup Final ─────────────────────────────────────────────────────
const final = simSeries(eastChamp, westChamp);
Fix oxlint warnings: no-shadow, consistent-function-scoping, no-non-null-assertion, and others (#196) * Fix no-shadow and consistent-function-scoping lint violations Resolves all 11 no-shadow and 16 consistent-function-scoping oxlint warnings and promotes both rules to errors in .oxlintrc.json. no-shadow: renamed Drizzle callback params (sports→s, matches→m, seasons→s) to avoid shadowing outer imports; removed shadowed destructures (eq, inArray) from where callbacks; renamed inner template→bracketTemplate, prev→currentTimers, season→ss, name→teamName (with name: teamName fix to preserve semantics). consistent-function-scoping: moved formatDate, getRankBadge, getMovementIndicator, getPositionBadge, getStatusBadge, toDateStr, elo (×2), weightedPick, sortByMatchNumber (×2) to module scope; moved formatTime (×2), isValidLeagueName, getDraftTimes, makeSeasonQueues to file scope in test files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix no-non-null-assertion lint violations and promote to error Eliminates all 208 no-non-null-assertion warnings across 38 files. Promotes typescript/no-non-null-assertion from warn to error in .oxlintrc.json. Fix patterns applied: - Map.get(key)! after .has() check → extract with get() + null guard - Map.get(key)! on pre-populated count maps → ?? 0 default - .set(id, map.get(id)! + 1) increment → ?? 0 before adding - participant1Id!/participant2Id! on DB matches → ?? "" fallback - array.find()! in tests → guard + throw or expect().toBeDefined() - bracketTemplateCache.get(id)! → null guard extract - Various nullable field accesses → optional chain or ?? default Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix prefer-add-event-listener, no-unassigned-import, require-module-specifiers Resolves all 9 remaining non-console lint warnings and promotes all three rules to errors in .oxlintrc.json. - prefer-add-event-listener: converted onchange/onclick/onload assignments to addEventListener in useDraftNotifications.ts and admin.data-sync.tsx; stored changeHandler ref for proper cleanup with removeEventListener - no-unassigned-import: configured rule with allow list for legitimate side-effect imports (*.css, @testing-library/jest-dom, @testing-library/cypress/add-commands) - require-module-specifiers: removed redundant `export {}` from cypress/support/e2e.ts (file already has an import) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix TypeScript errors from no-non-null-assertion fixes Two fixes introduced by the non-null assertion cleanup produced type errors: - scoring-event.ts: `?? ""` was wrong type for a participant object map; restructured to explicit null guards so TypeScript can narrow correctly - standings-sync/index.ts: `?? null` after name-match lookup lost the truthy guarantee, causing TS18047 on the write-back block; added `participant &&` guard before accessing its properties Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add npm run typecheck as Stop hook in Claude settings Runs a full project typecheck at the end of each Claude turn so type errors surface as feedback before the next message. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 10:59:51 -07:00
championCounts.set(final.winner.id, (championCounts.get(final.winner.id) ?? 0) + 1);
finalistCounts.set(final.loser.id, (finalistCounts.get(final.loser.id) ?? 0) + 1);
for (const loser of [eastR2m1.loser, eastR2m2.loser, westR2m1.loser, westR2m2.loser]) {
Fix oxlint warnings: no-shadow, consistent-function-scoping, no-non-null-assertion, and others (#196) * Fix no-shadow and consistent-function-scoping lint violations Resolves all 11 no-shadow and 16 consistent-function-scoping oxlint warnings and promotes both rules to errors in .oxlintrc.json. no-shadow: renamed Drizzle callback params (sports→s, matches→m, seasons→s) to avoid shadowing outer imports; removed shadowed destructures (eq, inArray) from where callbacks; renamed inner template→bracketTemplate, prev→currentTimers, season→ss, name→teamName (with name: teamName fix to preserve semantics). consistent-function-scoping: moved formatDate, getRankBadge, getMovementIndicator, getPositionBadge, getStatusBadge, toDateStr, elo (×2), weightedPick, sortByMatchNumber (×2) to module scope; moved formatTime (×2), isValidLeagueName, getDraftTimes, makeSeasonQueues to file scope in test files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix no-non-null-assertion lint violations and promote to error Eliminates all 208 no-non-null-assertion warnings across 38 files. Promotes typescript/no-non-null-assertion from warn to error in .oxlintrc.json. Fix patterns applied: - Map.get(key)! after .has() check → extract with get() + null guard - Map.get(key)! on pre-populated count maps → ?? 0 default - .set(id, map.get(id)! + 1) increment → ?? 0 before adding - participant1Id!/participant2Id! on DB matches → ?? "" fallback - array.find()! in tests → guard + throw or expect().toBeDefined() - bracketTemplateCache.get(id)! → null guard extract - Various nullable field accesses → optional chain or ?? default Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix prefer-add-event-listener, no-unassigned-import, require-module-specifiers Resolves all 9 remaining non-console lint warnings and promotes all three rules to errors in .oxlintrc.json. - prefer-add-event-listener: converted onchange/onclick/onload assignments to addEventListener in useDraftNotifications.ts and admin.data-sync.tsx; stored changeHandler ref for proper cleanup with removeEventListener - no-unassigned-import: configured rule with allow list for legitimate side-effect imports (*.css, @testing-library/jest-dom, @testing-library/cypress/add-commands) - require-module-specifiers: removed redundant `export {}` from cypress/support/e2e.ts (file already has an import) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix TypeScript errors from no-non-null-assertion fixes Two fixes introduced by the non-null assertion cleanup produced type errors: - scoring-event.ts: `?? ""` was wrong type for a participant object map; restructured to explicit null guards so TypeScript can narrow correctly - standings-sync/index.ts: `?? null` after name-match lookup lost the truthy guarantee, causing TS18047 on the write-back block; added `participant &&` guard before accessing its properties Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add npm run typecheck as Stop hook in Claude settings Runs a full project typecheck at the end of each Claude turn so type errors surface as feedback before the next message. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 10:59:51 -07:00
r2LoserCounts.set(loser.id, (r2LoserCounts.get(loser.id) ?? 0) + 1);
}
}
if (effectiveN === 0) {
throw new Error(
"All simulations produced degenerate brackets. " +
Fix NHL simulator seeding bugs and code quality issues (#281) * Fix NHL simulator dynamic seeding and code quality issues - Replace hardcoded seeding probabilities with standings-based Monte Carlo projection: simulates remaining regular season games per team using Elo win probability vs. a league-average opponent, so mathematically eliminated teams naturally fall out of playoff contention - Fix double-simulation bug in wildcard pool: all 16 conference teams are now projected exactly once via sortProjected([...divA, ...divB].map(projectTeam)), then filtered — wildcard pool no longer re-runs simulateProjectedPoints with fresh randomness independent of the division seeding - Move ProjectedTeam interface, projectTeam(), and sortProjected() to module scope (defined once, not recreated on every buildConferenceBracket call) - Replace conference-level participant count check (east < 8 || west < 8) with per-division checks (each division < 3) for a more actionable error message - Add logger.warn when standings.length === 0 so admins know the sim is running without synced data - Remove unused easternTeams/westernTeams variables - Simplify test: replace Object.fromEntries pattern with a plain for..of loop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix oxlint errors in NHL simulator and tests - Replace non-null assertions (data!) with optional chaining (data?.x ?? "") - Move makeEntry helper to module scope to avoid recreating on every call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:33:33 -04:00
"Check that each division has at least 3 participants with standings data."
);
}
// ─── Convert counts to probability distributions ───────────────────────────
Fix NHL simulator seeding bugs and code quality issues (#281) * Fix NHL simulator dynamic seeding and code quality issues - Replace hardcoded seeding probabilities with standings-based Monte Carlo projection: simulates remaining regular season games per team using Elo win probability vs. a league-average opponent, so mathematically eliminated teams naturally fall out of playoff contention - Fix double-simulation bug in wildcard pool: all 16 conference teams are now projected exactly once via sortProjected([...divA, ...divB].map(projectTeam)), then filtered — wildcard pool no longer re-runs simulateProjectedPoints with fresh randomness independent of the division seeding - Move ProjectedTeam interface, projectTeam(), and sortProjected() to module scope (defined once, not recreated on every buildConferenceBracket call) - Replace conference-level participant count check (east < 8 || west < 8) with per-division checks (each division < 3) for a more actionable error message - Add logger.warn when standings.length === 0 so admins know the sim is running without synced data - Remove unused easternTeams/westernTeams variables - Simplify test: replace Object.fromEntries pattern with a plain for..of loop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix oxlint errors in NHL simulator and tests - Replace non-null assertions (data!) with optional chaining (data?.x ?? "") - Move makeEntry helper to module scope to avoid recreating on every call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:33:33 -04:00
const N = effectiveN;
const results: SimulationResult[] = participantIds.map((participantId) => {
Fix oxlint warnings: no-shadow, consistent-function-scoping, no-non-null-assertion, and others (#196) * Fix no-shadow and consistent-function-scoping lint violations Resolves all 11 no-shadow and 16 consistent-function-scoping oxlint warnings and promotes both rules to errors in .oxlintrc.json. no-shadow: renamed Drizzle callback params (sports→s, matches→m, seasons→s) to avoid shadowing outer imports; removed shadowed destructures (eq, inArray) from where callbacks; renamed inner template→bracketTemplate, prev→currentTimers, season→ss, name→teamName (with name: teamName fix to preserve semantics). consistent-function-scoping: moved formatDate, getRankBadge, getMovementIndicator, getPositionBadge, getStatusBadge, toDateStr, elo (×2), weightedPick, sortByMatchNumber (×2) to module scope; moved formatTime (×2), isValidLeagueName, getDraftTimes, makeSeasonQueues to file scope in test files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix no-non-null-assertion lint violations and promote to error Eliminates all 208 no-non-null-assertion warnings across 38 files. Promotes typescript/no-non-null-assertion from warn to error in .oxlintrc.json. Fix patterns applied: - Map.get(key)! after .has() check → extract with get() + null guard - Map.get(key)! on pre-populated count maps → ?? 0 default - .set(id, map.get(id)! + 1) increment → ?? 0 before adding - participant1Id!/participant2Id! on DB matches → ?? "" fallback - array.find()! in tests → guard + throw or expect().toBeDefined() - bracketTemplateCache.get(id)! → null guard extract - Various nullable field accesses → optional chain or ?? default Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix prefer-add-event-listener, no-unassigned-import, require-module-specifiers Resolves all 9 remaining non-console lint warnings and promotes all three rules to errors in .oxlintrc.json. - prefer-add-event-listener: converted onchange/onclick/onload assignments to addEventListener in useDraftNotifications.ts and admin.data-sync.tsx; stored changeHandler ref for proper cleanup with removeEventListener - no-unassigned-import: configured rule with allow list for legitimate side-effect imports (*.css, @testing-library/jest-dom, @testing-library/cypress/add-commands) - require-module-specifiers: removed redundant `export {}` from cypress/support/e2e.ts (file already has an import) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix TypeScript errors from no-non-null-assertion fixes Two fixes introduced by the non-null assertion cleanup produced type errors: - scoring-event.ts: `?? ""` was wrong type for a participant object map; restructured to explicit null guards so TypeScript can narrow correctly - standings-sync/index.ts: `?? null` after name-match lookup lost the truthy guarantee, causing TS18047 on the write-back block; added `participant &&` guard before accessing its properties Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add npm run typecheck as Stop hook in Claude settings Runs a full project typecheck at the end of each Claude turn so type errors surface as feedback before the next message. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 10:59:51 -07:00
const c = championCounts.get(participantId) ?? 0;
const f = finalistCounts.get(participantId) ?? 0;
const cf = confFinalLoserCounts.get(participantId) ?? 0;
const r2 = r2LoserCounts.get(participantId) ?? 0;
return {
participantId,
probabilities: {
probFirst: c / N,
probSecond: f / N,
probThird: cf / (2 * N),
probFourth: cf / (2 * N),
probFifth: r2 / (4 * N),
probSixth: r2 / (4 * N),
probSeventh: r2 / (4 * N),
probEighth: r2 / (4 * N),
},
source: "nhl_bracket_monte_carlo",
};
});
// ─── Per-position normalization ────────────────────────────────────────────
Fix NHL simulator seeding bugs and code quality issues (#281) * Fix NHL simulator dynamic seeding and code quality issues - Replace hardcoded seeding probabilities with standings-based Monte Carlo projection: simulates remaining regular season games per team using Elo win probability vs. a league-average opponent, so mathematically eliminated teams naturally fall out of playoff contention - Fix double-simulation bug in wildcard pool: all 16 conference teams are now projected exactly once via sortProjected([...divA, ...divB].map(projectTeam)), then filtered — wildcard pool no longer re-runs simulateProjectedPoints with fresh randomness independent of the division seeding - Move ProjectedTeam interface, projectTeam(), and sortProjected() to module scope (defined once, not recreated on every buildConferenceBracket call) - Replace conference-level participant count check (east < 8 || west < 8) with per-division checks (each division < 3) for a more actionable error message - Add logger.warn when standings.length === 0 so admins know the sim is running without synced data - Remove unused easternTeams/westernTeams variables - Simplify test: replace Object.fromEntries pattern with a plain for..of loop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix oxlint errors in NHL simulator and tests - Replace non-null assertions (data!) with optional chaining (data?.x ?? "") - Move makeEntry helper to module scope to avoid recreating on every call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:33:33 -04:00
const positionKeys: Array<keyof (typeof results)[0]["probabilities"]> = [
"probFirst", "probSecond", "probThird", "probFourth",
"probFifth", "probSixth", "probSeventh", "probEighth",
];
for (const key of positionKeys) {
const colSum = results.reduce((s, r) => s + r.probabilities[key], 0);
const residual = 1.0 - colSum;
if (residual !== 0) {
const maxResult = results.reduce((best, r) =>
r.probabilities[key] > best.probabilities[key] ? r : best
);
maxResult.probabilities[key] += residual;
}
}
return results;
}
}