Commit graph

31 commits

Author SHA1 Message Date
Chris Parsons
a6bb1330e6
Fix darts simulator: bracket bug, ELO calibration, fallback Elo (#284)
* fix: lower darts ELO_DIVISOR to 400 and fallback Elo to 1400

ELO_DIVISOR 500 → 400: elite darts is more skill-dominated than the
previous setting implied. A 400-point gap now gives ~78% per-set win
probability (vs ~73% before).

fallbackElo 1600 → 1400: unseeded PDC World Championship entrants
(regional qualifiers, Q-School players) are materially weaker than
seeded tour professionals. 1400 better reflects that gap.

Combined effect: a dominant player (e.g. 2000 Elo vs 1400-1600 field)
now produces EV ≈ 65 instead of ≈ 30, which better matches expectations
for a top-ranked player in a 128-player bracket.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: lower darts ELO_DIVISOR to 200 for realistic EV distribution

With the actual PDC field (top players clustered 1800–1970 Elo, Littler at
~2080), ELO_DIVISOR=400 made the gap between world #1 and the top 8 too
soft — EV for Littler came out ~37 instead of the expected 65–70.

ELO_DIVISOR=200 calibrates correctly for this field: a 100-pt gap now
gives ~62% per-set win probability (vs ~56% at 400), sharply rewarding
the best players while still allowing upsets. Littler at 2084 produces
EV ≈ 65–70 against the real PDC tour roster.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: seed darts bracket by Elo instead of world ranking

World ranking (PDC Order of Merit) is prize-money-based and can diverge
from current skill — e.g. Kevin Doets (1818 Elo) was ranked 35th while
Joe Cullen (1667 Elo) held the 32nd seed. Seeding the weaker player and
leaving the stronger one unseeded contradicts the Elo-based match model.

Seeding by Elo descending ensures the 32 strongest players (by the same
metric used to compute match probabilities) get protected bracket positions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* revert: restore world-ranking-based seeding for darts bracket

Seeding should follow the PDC Order of Merit (world ranking), not Elo.
Reverts the previous Elo-seeding commit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: interleave seeded and unseeded R1 pairs in darts bracket

Previously, all 32 seeded-vs-unseeded matches were added to r1Pairs
first (indices 0–31) and all 32 unseeded-vs-unseeded matches last
(indices 32–63). Since R2 pairs adjacent R1 winners, this created two
completely separate sub-brackets that only converged at the Final:
  - Seeded sub-bracket: top players eliminating each other early
  - Unseeded sub-bracket: high-Elo unseeded players (e.g. Doets 1818,
    Zonneveld 1806) steamrolling weak opponents and making the Final
    ~20% of the time

Fix: interleave each seeded match with its adjacent unseeded-vs-unseeded
match. Seeded pair i is at r1Pairs[2i], unseeded pair i is at r1Pairs[2i+1].
From R2 onwards, winners from seeded and unseeded regions now merge
correctly as in a real single-elimination bracket.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 00:47:47 -04:00
Chris Parsons
9e5d1cda5f
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
Chris Parsons
0da80bd2c0
Add LLWS bracket Monte Carlo simulator (#280)
* Add LLWS bracket Monte Carlo simulator

Simulates the 20-team Little League World Series: pool play round-robin
(5 teams/pool, top 2 advance) → 4-team double-elimination per side
(US and International) → consolation game → World Series final.

Uses championship futures odds as the sole win-probability signal.
Pool assignments are auto-detected from externalId: bare "US"/"Intl"
randomizes pools each simulation (pre-draw mode); "US:A"/"Intl:B" etc.
uses fixed assignments (post-draw mode). Sides can differ.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix oxlint violations in LLWS simulator

- Replace non-null assertions with null-safe access (! → ?? / optional chaining)
- Replace .sort() with .toSorted() per linting rules
- Promote bump() to simulate() scope and pass it into simulateSideBracket,
  removing the need to pass counts into that function

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 16:52:10 -04:00
Chris Parsons
4d5fea05ab
Add NCAA Football CFP simulator (12-team bracket) (#278)
Fixes #124

* Add NCAA Football CFP simulator (12-team bracket)

Implements a Monte Carlo simulator for the College Football Playoff using
the 2024-present 12-team format. Elo/FPI ratings are entered manually via
the existing admin Elo Ratings page; championship futures odds can
optionally be blended in (60% Elo / 40% odds).

- Add CFP_12 bracket template (First Round not scoring, QFs onward score)
- Add generateCFP12Bracket() with correct seeding: 5v12, 6v11, 7v10, 8v9
  in First Round; seeds 1–4 receive QF byes
- Add NCAAFootballSimulator: 50k Monte Carlo sims, seeds teams by blended
  Elo+odds strength, tracks champion/finalist/SF/QF placement tiers
- Register ncaa_football_bracket simulator type in registry and schema enum
- Add migration 0071: ALTER TYPE simulator_type ADD VALUE 'ncaa_football_bracket'
- Add tests: 30 tests covering bracket template structure and simulator
  probability distributions, seeding, edge cases, futures blending

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix lint errors in NCAA Football CFP simulator

Replace non-null assertions with optional chaining, change let to const,
use toSorted() instead of sort(), and add a bump() helper to avoid
repeated map lookups with non-null assertions in simulateBracket.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix TS2345 in NCAA football simulator test

Add ?? 0 fallback so optional-chained probFirst is number, not number | undefined.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 09:48:32 -04:00
Chris Parsons
ad49d6246d
Fix CS Major simulator bugs and accuracy issues (#276)
* Fix CS Major simulator bugs and accuracy issues (fixes #275)

- Fix crash when pool teams are missing Elo ratings by including all
  participants with FALLBACK_ELO, ensuring Champions Stage always gets
  exactly 8 teams
- Add AdvancedTeam type tracking losses-at-advancement; seed Champions
  Stage by stage performance (fewer losses = higher seed) instead of
  world rank alone
- Promote decisive Stage 2 matches (either team at ≥2W or ≥2L) to Bo3,
  matching the real Challengers Stage format
- Tie-split QP for QF losers (slots 5–8) and SF losers (slots 3–4)
  instead of assigning arbitrary individual placements
- Change stage-complete threshold from === 8 to >= 8 for robustness
- Validate even team count in simulateSwiss to prevent silent infinite loops
- Short-circuit Monte Carlo loop when all events are complete, returning
  deterministic 0/1 probabilities
- Parallelize stage results DB fetches with Promise.all
- Export calcStage3ExitQP and simulateOneMajor; add test coverage for
  both, plus new simulateSwiss and simulateChampionsStage edge cases

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix oxlint violations in CS Major simulator

- Replace non-null assertion (!) with null-safe guard in simulateOneMajor
- Replace non-null assertions in test expectations with nullish coalescing
- Move makeStage3QPConfig out of describe block (no captured variables)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 17:28:24 -04:00
Chris Parsons
102cb781ad
Add NFL season + playoffs Monte Carlo simulator (#272)
Implements a new `nfl_bracket` simulator that projects the NFL regular
season and full playoff bracket using nfelo Elo ratings.

- Simulates remaining regular season games (17 total) per team using
  Elo win probability vs an average opponent, then seeds both conferences
  (division winners = seeds 1–4, wildcards = 5–7) per simulation
- Simulates Wild Card, Divisional, Conference Championship, and Super
  Bowl rounds with correct NFL bracket structure (seed 1 bye)
- Applies +48 Elo home-field advantage (~57% win rate) for all rounds
  except the neutral-site Super Bowl
- Pre-season mode (no standings in DB): simulates all 17 games from
  scratch; falls back to futures odds → Elo conversion if no sourceElo
- Validates all 8 NFL divisions have Elo-rated teams before simulating,
  with a clear error listing missing divisions
- Registers as `nfl_bracket` simulator type in the registry and schema
- Adds migration to extend the `simulator_type` enum
- Also updates drizzle.config.ts to prefer DIRECT_DATABASE_URL when set

Fixes #129

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 09:39:34 -04:00
Chris Parsons
a71e256bfd
Add CS2 Major Qualifying Points simulator and stage management (#260)
* Add CS2 Major qualifying points simulator

Implements a full CS2 Major tournament simulator with:
- 3-stage Swiss format (Opening Bo1, Elimination Bo1/Bo3, Decider all Bo3)
  + Champions Stage 8-team single-elimination (QF Bo3, SF Bo3, GF Bo5)
- Monte Carlo simulation (10,000 iterations) accumulating QP across 2 majors/season
- Sampled 24-team field per iteration: top 12 guaranteed, remaining weighted by 1/rank
- Stage 3 exits (placements 9-16) sub-ranked by W-L record (2-3 > 1-3 > 0-3)
- Stage assignments stored per-event so actual field composition drives simulation
- Admin CS Elo form for entering team Elo + HLTV world rankings
- Admin CS2 stage setup page for assigning teams to stages and tracking advancement
- Database migration: cs2_major_qualifying_points enum value + cs2_major_stage_results table
- 24 unit tests covering all exported pure functions

https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR

* Consolidate Elo + ranking input into generic elo-ratings page

The darts-elo and cs-elo pages were unreachable from the admin nav,
which always links to the generic elo-ratings page. Extended elo-ratings
to conditionally show world ranking fields for simulator types that need
it (darts_bracket, cs2_major_qualifying_points), then deleted the
redundant sport-specific pages.

https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR

* Consolidate server postgres connections into one shared pool

Four separate postgres() clients were open simultaneously (app, timer,
snapshots, socket), each defaulting to 10 connections, exhausting the
database's max_connections limit. Replaced with a single shared lazy-
initialized client in server/db.ts using a Proxy to defer the
DATABASE_URL check until first use (preserving test compatibility).

Also bumps the CS2 Champions Stage stochastic test from 200 → 1000
iterations to eliminate flakiness.

https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR

* Fix and() bug and add Swiss loop safety guard

- cs2-major-stage.ts: markCs2StageEliminations and setCs2FinalPlacements
  were using JS && instead of Drizzle and(), causing WHERE to filter only
  by participantId (not scoringEventId), which would update rows across
  all events instead of just the target event
- cs-major-simulator.ts: add break guard in simulateSwiss while loop to
  prevent infinite loop if pairGroups returns no pairs

https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR

* Fix all remaining code review issues

- cs2-major-stage.ts: use schema column reference for stageEliminated
  in markCs2StageEliminations instead of raw SQL string
- cs-major-simulator.ts: simulateOneMajor now locks in known stage
  results when a stage is complete (8 recorded eliminations), only
  simulating the remaining stages during live events
- admin event page: add CS2 Stage Setup button for cs2_major_qualifying_points
  simulator types; expose simulatorType in server loader type cast
- cs2-setup.tsx: replace document.getElementById DOM manipulation with
  React state (eliminatedChecked map) for checkbox show/hide logic;
  remove unused stageMap and unassignedParticipants variables

https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR

* Fix oxlint errors: non-null assertions, sort→toSorted, unused vars

- cs-major-simulator.ts: replace 5 non-null assertions (!) with safe
  optional chaining / if-guards; replace 6 .sort() with .toSorted()
- cs2-major-stage.ts: remove unused `inArray` import
- cs2-setup.tsx: remove unused `assignedIds` variable

https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR

* Fix flaky Champions Stage stochastic test

The makeTeams(8) helper creates only a 70-pt Elo spread (1800→1730).
With the Champions Stage bracket math this gives team-0 a ~19.6% win
rate — right at the 0.2 threshold, causing the test to fail ~63% of
the time in CI despite 1000 iterations.

Use 100-pt steps (1800→1100) instead, giving team-0 a ~40% win rate
and raising the assertion threshold to 0.25 for a clear safety margin.

https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-05 16:40:05 -04:00
Chris Parsons
005a5a82a7 Fix noisy and broken tests. 2026-03-31 15:53:43 -07:00
Chris Parsons
8c3663e01b
Add PDC World Darts Championship simulator with Elo-based bracket (#248)
Fixes #123

* Add PDC World Darts Championship simulator (128-player bracket)

- New DartsSimulator: 128-player single-elimination, 7 rounds with
  PDC-accurate best-of-sets formats (bo3/bo5/bo5/bo7/bo7/bo11/bo13).
  ELO_DIVISOR=500 via set-level Bernoulli model. Two simulation paths:
  Path A (bracket drawn) simulates from actual DB matches; Path B
  (pre-bracket) seeds top 32 by world ranking in fixed positions and
  randomly draws the remaining 96 per simulation run (50,000 iterations).
- darts_bracket added to simulatorTypeEnum and simulator registry.
- world_ranking nullable integer column added to participant_expected_values
  (migration 0067); batchSaveSourceElos now accepts and persists it.
- Admin route /admin/sports-seasons/:id/darts-elo: bulk import with format
  "Player Name, 2099, 1" (name, Elo, optional world ranking), fuzzy name
  matching, auto-runs simulation and updates EV/snapshots on save.
- DARTS_128 bracket template added (scoring starts at Quarterfinals).
- 30 unit tests: math helpers, bracket seeding structure, Path A/B integration.

https://claude.ai/code/session_01WZ6FjMmC2eBeZ1564M9Ruz

* Review fixes: pre-compute hot-loop invariants, import normalizeName

- Pre-compute seededSlots and getSeededMatchOrder(32) before the 50k
  simulation loop — was being recomputed every iteration
- Pad unseeded pool to 96 once before the loop instead of inside it
- Inline bracket-building in the hot loop using pre-computed seededSlots;
  removes the per-iteration call to buildR1Bracket/getSeededMatchOrder
- Remove dead SEEDED_R1_PAIRS constant (was never referenced)
- Import normalizeName from ~/lib/fuzzy-match instead of redefining it locally

https://claude.ai/code/session_01WZ6FjMmC2eBeZ1564M9Ruz

* Fix lint failures: unused vars, eqeqeq, toSorted

- Remove unused seededSet/unseededSet variables in test
- Replace != null with !== null && !== undefined (eqeqeq rule)
- Replace .sort() with .toSorted() in simulator and route (unicorn/no-array-sort)

https://claude.ai/code/session_01WZ6FjMmC2eBeZ1564M9Ruz

* Fix TS2552: restore seededSet declaration removed during lint fix

https://claude.ai/code/session_01WZ6FjMmC2eBeZ1564M9Ruz

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-31 15:37:28 -07:00
Chris Parsons
338979e0a8
Add FIFA World Cup 2026 support with group stage display and Monte Carlo simulator (#242)
* Add FIFA World Cup 2026 support with group stage display and Monte Carlo simulator, fixes #127

- New `groupStageMatches` table for recording group play results (W/D/L, scores, matchday, schedule)
- `computeGroupStandings()` model function: pts → GD → GF → name tiebreaker ordering
- `GroupStageStandings` component showing all 12 groups with standings table and manager column
- Admin bracket UI: group match score entry, per-group standings, "Recalculate Floors" action
- `WorldCupSimulator`: 50k Monte Carlo covering group stage + best-8 3rd-place + knockout + 3rd place game
  - Fuzzy name matching for national team Elo lookup (exact → substring → word-overlap), warns on miss
  - Partial group completion: completed matches replayed with real scores, remaining matches simulated
  - Elo priority: admin-entered sourceElo > futures odds converted to Elo > hardcoded national team ratings
- `fifa_48` bracket template: added Third Place Game round with `loserFeedsInto` on Semifinals
- Scoring rules: distinct 3rd/4th place for `fifa_48` (not averaged), QF losers share 5th–8th equally
- Floor scoring: SF participants guaranteed 4th (provisional), finalized after 3rd place game
- `recalculate-floors` admin action deletes and replays all results from scratch (fixes stale guard bug)
- Unique index on `(tournamentGroupId, participant1Id, participant2Id)` to prevent duplicate pairings
- Batch `findMatchesByGroupIds()` replacing N sequential queries in the sport season loader
- League home mini-standings now shows `actualPoints` (includes floor) instead of `totalPoints` only
- Elo ratings admin page supports World Cup (same bulk-import flow as snooker)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix TypeScript errors: update GroupStandingData type to use findMatchesByGroupIds

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Increase Node heap to 4GB for unit tests in CI to prevent OOM

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix OOM in CI: make WorldCupSimulator simulation count configurable for tests

Tests now pass numSimulations=500 instead of the production default of 50,000.
Six simulator tests × 50k iterations each was exhausting the 4GB heap on GitHub
Actions runners. Also reduce simGroupMatch stat tests from 50k to 5k iterations.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 10:27:47 -07:00
Chris Parsons
bde1e6e5f0
Add WNBA playoff simulator with SRS-based Elo ratings, fixes #125 (#231)
* Add WNBA playoff simulator with SRS-based Elo ratings, fixes #125

- New WNBASimulator: Monte Carlo (50k sims) projecting remaining regular
  season games → seeding → R1 Bo3 / Semis Bo5 / Finals Bo7 bracket
- Hybrid Elo sourcing: pre-season uses futures odds (ICM); once avg
  gamesPlayed ≥ 5, switches automatically to SRS-derived Elo
  (elo = 1500 + srs × 20)
- New WnbaStandingsAdapter: fetches ESPN standings + teams endpoints in
  parallel; includes zero-records for 2026 expansion teams (Portland
  Fire, Toronto Tempo) not yet in standings
- Added srs column to regular_season_standings (migration 0063);
  stored as net rating proxy (avgPointsFor − avgPointsAgainst)
- Added wnba_bracket to simulatorTypeEnum

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix missing afterEach import in wnba standings test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 00:34:51 -07:00
Chris Parsons
2b48ef285d
Rework MLB simulator: RDif-based matchup probs, 2026 data, fixes #227 (#228)
Fixes #227

- Replace hardcoded Elo ratings with 2026 FanGraphs Depth Charts projected
  run differential (RDif) for all 30 teams
- Replace Elo formula with Bill James log5 using RDif → win rate conversion
- Restore p_div/p_wc from FanGraphs playoff odds (divTitle/wcTitle) for
  seeding draws — previously these were incorrectly derived from raw win
  rates, which gave the Dodgers only ~24% division win probability instead
  of the correct ~90%
- Add RDIF_DIVISOR constant to compress team strengths toward .500 for
  playoff parity; set to 8000 (Dodgers +137 → ~51.7% per-game win rate)
- Fix minimum team check: < 5 → < 6 (need 3 div winners + 3 wild cards)
- Fix Colorado Rockies p_div 0.000 → 0.001 for consistency
- Fix stale comments throughout; update tests

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 19:05:12 -07:00
Chris Parsons
2e2d8db777
Add MLB playoff simulator with AL/NL division standings, fixes #121 (#225)
* Add MLB playoff simulator with AL/NL division standings, fixes #121

- New `mlb-simulator.ts`: 50k Monte Carlo sim drawing division winners
  (weighted by p_div) and 3 WC teams per league, then simulating
  WC best-of-3 → DS best-of-5 → LCS best-of-7 → World Series.
  Elo parity factor 350; blends Vegas odds 30/70 when sourceOdds available.
- New `standings-sync/mlb.ts`: adapter for free statsapi.mlb.com API,
  mapping AL/NL conferences, division ranks, streaks, home/away/L10 splits.
- New `mlb-divisions` display mode in RegularSeasonStandings: shows 1 division
  winner per division section + Wild Card section with 3-spot playoff line,
  headings read "AL/NL League" instead of "Conference".
- Refactored buildNhlSections/buildMlbSections into shared buildDivisionSections
  (divisionSpots + wcSpots params); conferenceLabel now flows through group
  objects rather than being derived from displayMode in the render.
- DB migration 0062 adds `mlb_bracket` to simulator_type enum.
- 37 new tests across simulator, adapter, and component.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix flaky tennis simulator test: increase trials and lower threshold

500 trials with a ~3–5% expected win rate had enough variance to
occasionally land below the 0.03 threshold (~2% failure rate).
Bumping to 2000 trials reduces the std dev by 2x; lowering the
threshold to 0.02 keeps the assertion meaningful (still well above
random 0.78%) while eliminating the flakiness.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 08:47:02 -07:00
Chris Parsons
e62e9554c9
Add golf qualifying points simulator (Plackett-Luce Monte Carlo) (#223)
* Add golf QP simulator with Plackett-Luce model, fixes #120

- New `participant_golf_skills` table (migration 0061) for SG: Total and
  per-major American odds per player/season
- New `app/models/golf-skills.ts` with getGolfSkillsMap, getGolfSkillsForSeason,
  batchUpsertGolfSkills
- Full `GolfSimulator` implementation replacing the TODO stub: Plackett-Luce
  ranking model (PL_BETA=1.5, FIELD_SIZE=156), 10k Monte Carlo iterations,
  awards QP by finishing position, ranks by total QP across all 4 majors
- New admin route `sports-seasons/:id/golf-skills` with bulk CSV import,
  fuzzy name matching, per-player SG + per-major odds inputs; saves skills
  and auto-runs simulation on submit
- Simulator dropdown on sport admin sorted alphabetically; renamed to
  "Golf Qualifying Points Monte Carlo"
- Golf Skills button shown on sports season admin when simulator type is
  golf_qualifying_points
- Extract normalizeName/diceCoefficient to shared `app/lib/fuzzy-match.ts`,
  removing duplication from surface-elo and golf-skills routes
- Parallelize 4 DB queries in GolfSimulator.simulate() with Promise.all
- O(1) field array removal via swap-to-end + pop (was O(N) splice)
- Fix source tag: performance_model (not elo_simulation) for SG-based model
- 23 unit tests covering americanToImplied, getMajorOddsKey, resolveSkill,
  simulateMajor, and Monte Carlo calibration properties

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix oxlint errors: no-non-null-assertion and eqeqeq

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 21:46:02 -07:00
Chris Parsons
d7ba20d9ea
Add tennis Grand Slam simulator with surface Elo ratings, fixes #116 (#216)
Implements a Monte Carlo simulator for men's/women's tennis seasons scored
on the qualifying_points pattern. Simulates all 4 Grand Slam majors
(Australian Open, French Open, Wimbledon, US Open) using surface-specific
Elo ratings and ATP/WTA world rankings for seeding.

New table: participant_surface_elos — one row per (participant, season)
storing worldRanking, eloHard, eloClay, eloGrass.

Key design decisions:
- Seeding uses ATP/WTA world ranking (not Elo), matching real draw procedure
- Top 32 seeded with standard slot placement (1→0, 2→64, 3-4→quarters, etc.)
- QP per round with tie-splitting pre-applied: W=20, F=14, SF=9, QF=4, R16=1.5
- Completed majors read actual qualifyingPointsAwarded from eventResults
- 10,000 Monte Carlo simulations; column sums naturally 1.0 (no normalization)

Admin UI at /admin/sports-seasons/:id/surface-elo:
- 5-column grid (Player | Rank | Hard | Clay | Grass)
- Bulk import: "Name, ranking, hardElo, clayElo, grassElo" one per line
- Fuzzy name matching (bigram Dice coefficient) with "Did you mean?" suggestions
- Inline participant creation for unmatched names via useFetcher
- Saves Elos and auto-runs simulation on submit

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 23:59:35 -07:00
Chris Parsons
1bdd5c3372 Add snooker bracket simulator and Elo ratings admin UI, fixes #119
- SnookerSimulator: Monte Carlo simulation of the 32-player World
  Championship bracket using per-frame Bernoulli win probabilities
  derived from Elo ratings (ELO_DIVISOR=700). Two paths: bracket-
  populated (respects completed matches) and pre-bracket (simulates
  qualifying, then seeds full draw).
- Admin route /sports-seasons/:id/elo-ratings: bulk-import and per-
  player Elo entry with fuzzy name matching; saves ratings and auto-
  runs the simulation in one action.
- schema: add snooker_bracket simulator type, source_elo column on
  participant_expected_values, unique index on (participant_id,
  sports_season_id).
- Fix batchSaveSourceElos to use INSERT ... ON CONFLICT DO UPDATE
  instead of N+1 SELECT+UPDATE loop.
- Fix existingElos returned as plain Record (Map doesn't survive JSON
  serialization through useLoaderData).
- Fix "How It Works" formula to show correct divisor (700, not 400).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 08:24:28 -07:00
Chris Parsons
c1be92b2af
Add AFL season + finals simulator (closes #126) (#204)
Monte Carlo simulation of the 2026 AFL season and 10-team finals series.
Elo ratings are backsolved from Squiggle's projected season win totals using
the inverse formula: elo = 1500 - 450×log₁₀((1−wins/23)/(wins/23)).

- New `afl_bracket` simulator type (schema + migration)
- `afl-simulator.ts`: projects remaining regular season via Elo, seeds the
  AFL_10 finals bracket (Wildcard → QF/EF → SF → PF → Grand Final), and
  correctly tracks P5/P6 and P7/P8 as separate scoring tiers
- `afl.ts` standings sync adapter pulling from Squiggle API (no auth required)
- Finals line on standings display set to 10 for AFL seasons
- Substring name matching with longest-key-wins to prevent "Port Adelaide"
  colliding with "Adelaide" when matching "Port Adelaide Power"
- 27 unit tests covering bracket logic, column-sum guarantees, and name matching

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 01:57:39 -07:00
Chris Parsons
46a2e9c199
Tune NHL simulator: Vegas futures blending + parity factor calibration (closes #168) (#202)
- Add 70/30 Elo + Vegas futures blending for per-game win probability,
  mirroring the UCL simulator pattern; falls back to Elo-only when no
  sourceOdds are stored in participantExpectedValues
- Adjust PARITY_FACTOR to better match Vegas championship implied
  probabilities (COL was ~12% vs ~20% market expectation)
- Add 15 unit tests covering name normalization, team data lookup,
  parity-factor math, seeding probability sanity, and eliminated-team checks

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 00:14:18 -07:00
Chris Parsons
7093b22726
Simulate NBA remaining regular season games from standings (#201)
Replace static Basketball-Reference seed probability distributions
(p_1..p_10) with dynamic simulation of each team's remaining games
based on current standings from the DB.

- Load regularSeasonStandings in parallel with participants query
- Compute remainingGames = 82 - gamesPlayed per team
- Pre-compute per-game win probability (Elo vs average opponent 1500)
  on TeamEntry at construction time — not inside the hot loop
- Sort conference standings by projected wins to assign seeds, replacing
  the drawSeed() weighted-probability approach
- Conference resolved from standings table, falls back to TEAMS_DATA
- Strip all p_1..p_10 seed probability data from TEAMS_DATA
- Move simulateProjectedWins to module scope (no closure captures)
- Remove const N alias; use NUM_SIMULATIONS directly

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 23:37:20 -07:00
Chris Parsons
618bc57ec1
Replace console.* with structured logger, fix no-inferrable-types (closes #98) (#199)
- Add app/lib/logger.ts: dev passes through to console; prod routes errors
  to Sentry.captureException and warnings to Sentry.captureMessage, with
  extra context preserved. Uses captureMessage (not captureException) for
  string-only args to avoid fabricated stack traces.
- Add server/logger.ts: dev passes through; prod silences log/info but
  keeps warn/error on stderr (Sentry not initialized in that process).
- Replace all console.* calls across 44 app files and 4 server files.
- Upgrade no-console from warn → error in oxlint; exempt logger files and
  scripts/** via overrides.
- Add typescript/no-inferrable-types rule; fix violations in services and
  simulators. Exempt test files (intentional string widening for switch/if
  tests would break under literal type inference).

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 13:41:39 -07:00
Chris Parsons
4bffa40606
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
Chris Parsons
e2b178221a
Add oxlint linting setup with zero errors (#194)
* Add oxlint and fix all lint errors

- Install oxlint, add .oxlintrc.json with rules for TypeScript/React
- Add npm run lint / lint:fix scripts
- Add Claude PostToolUse hook to run oxlint on every edited file
- Fix 101 errors: unused vars/imports, eqeqeq, prefer-const, no-new-array
- Fix no-array-index-key (use stable keys or suppress positional cases)
- Fix exhaustive-deps missing dependency in useEffect
- Promote exhaustive-deps and no-array-index-key to errors
- Fix Map.get() !== null bug in $leagueId.server.ts (should be !== undefined)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix no-explicit-any warnings and upgrade tsconfig to ES2023

- Replace all `any` types with proper types or `unknown` across ~20 files
- Add typed socket payload interfaces in draft route and useDraftSocket
- Use any[] with eslint-disable for socket.io callbacks (legitimate escape hatch)
- Bump all tsconfigs from ES2022 → ES2023 to support toSorted/toReversed
- Fix cascading type errors uncovered by removing any: Map.get narrowing,
  participant relation types, ChartDataPoint, Partial<NewSeason> indexing
- Add ParticipantResultWithParticipant type to participant-result model
- Fix test fixtures to match updated interfaces (DraftCell, ParticipantResult)
- Fix duplicate getQPStandings import in sportsSeasonId.server.ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Promote no-explicit-any to error

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 09:44:05 -07:00
Chris Parsons
bcca8b76fa
Add regular season standings for NBA/NHL (fixes #89) (#192)
Adds live standings sync and display for bracket-based sports (NBA/NHL),
so league members can see W/L tables and which teams their opponents drafted
during the regular season — not just after the playoff bracket is set.

- New `regular_season_standings` table with upsert-on-conflict sync
- Standings sync service with NHL (api-web.nhle.com) and NBA (ESPN) adapters,
  externalId write-back for future syncs, and unmatched-team resolution UI
- `RegularSeasonStandings` component: flat (NBA) + division/wild-card (NHL) modes,
  playoff line, TeamOwnerBadge, projected Brackt points (EV), mobile horizontal scroll
- Admin "Sync Standings" card + "Resolve Unmatched" UI on sports season page
- Admin manual standings edit hatch at /admin/sports-seasons/:id/regular-standings
- Show standings above bracket until matches exist; below once bracket is set
- `normalize-team-name` utility extracted to shared lib

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 00:12:01 -07:00
Chris Parsons
143698cb96
Fix NCAAM/NCAAW simulators: FF completion handling and validation hardening (#169)
Fixes the "slot is already filled" simulation error when First Four games
have already been played and their winners advanced to the Round of 64.

Changes:
- Fix FF→R64 validation to allow a filled R64 slot when the corresponding
  First Four match is already complete (the original bug)
- Fix isComplete guard for E8/FF/Championship: derive loser from stored
  participant IDs when loserId is null rather than re-simulating a real game
- Add round count validation for all rounds (R32 16, S16 8, E8 4, FF 2, Champ 1)
  to catch malformed brackets before the hot sim loop instead of producing
  silent garbage output
- Add participant1Id null check in the First Four path (mirrors the no-FF path)
- Add console.warn when participant IDs are missing from DB (fall-back to
  average strength instead of crashing)
- Import NCAA_68.regions from bracket-templates.ts instead of duplicating the
  hardcoded fallback in each simulator
- Fix duplicate step-9 comment number in ncaam-simulator.ts

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 01:37:55 -07:00
Chris Parsons
6f5ea35952
Add NHL playoff simulator and fix simulator dropdown (#166)
Closes #115

- Add `NHLSimulator` using NHL's divisional bracket format (top 3 per
  division + 2 wildcards per conference, best-of-7 series, parity
  factor 800). Elo ratings and seeding probabilities sourced from
  elo.harvitronix.com/nhl/2025-2026 as of March 18, 2026.
- Add `nhl_bracket` to `simulatorTypeEnum` with migration.
- Register `NHLSimulator` in the simulator registry.
- Fix simulator type dropdown in admin to auto-generate from the
  registry instead of a hardcoded list, so new simulators appear
  automatically without a separate UI change.

Code review fixes applied to the simulator:
- Narrow `weightedPick` key type to the five seeding probability fields
  (was `keyof NhlTeamData`, allowing `"elo"` which would silently
  corrupt results)
- Remove uniform-random fallback in `weightedPick` — zero-weight draws
  now return `undefined` and are skipped, preventing eliminated teams
  from becoming wildcards
- Exclude unrecognized participants entirely (with a console warning)
  instead of silently routing them into the Eastern wildcard pool
- Replace the confusing for-of loop over both conferences (with unused
  `champ` variable and `!`-suppressed unitialized vars) with two
  explicit sequential East/West blocks
- Track `effectiveN` for skipped degenerate draws so probability
  denominators stay accurate; throw if all iterations are skipped

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 01:23:53 -07:00
Chris Parsons
cbaab920f9
Add NBA playoff bracket Monte Carlo simulator (#154)
Adds a new `nba_bracket` simulator that projects both regular season
seedings and the full NBA playoff bracket, unlike other simulators that
operate on a pre-set bracket stored in the DB.

Algorithm:
- Per iteration: each team draws a conference seed (1–10) weighted by
  BBRef seed probabilities, then seeds 7–10 play the Play-In tournament
  (single elimination), then the resulting 8-team bracket is simulated
  as best-of-7 series using Elo playoff ratings
- Elo source: playoff rating (last 110 games, no regression to mean,
  postseason games weighted 3×) — appropriate for playoff matchup quality
- Seed probs source: Basketball-Reference (March 16, 2026), scaled from
  conditional to unconditional by multiplying by each team's Playoffs%

Placement tiers map to SimulationProbabilities:
  probFirst/Second → NBA champion / Finals loser
  probThird/Fourth → Conference Finals losers (2 per sim)
  probFifth–Eighth → Conference Semis losers (4 per sim)

Files:
- app/services/simulations/nba-simulator.ts (new)
- drizzle/0046_add_nba_bracket_simulator_type.sql (new)
- database/schema.ts: adds `nba_bracket` to simulatorTypeEnum
- app/services/simulations/registry.ts: registers NBASimulator

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 21:43:39 -07:00
Chris Parsons
84787c9a4b
Consolidate F1/IndyCar simulators into shared AutoRacingSimulator class (#151)
Replaces the duplicated F1Simulator (which was copy-pasted for IndyCar) with a
single AutoRacingSimulator that accepts a race points table at construction time.
Registry entries for f1_standings and indycar_standings remain separate, each
wired to the correct scoring system (F1: top-10, IndyCar: top-26 positions).

Also imports SIMULATOR_TYPES from registry into the admin route to eliminate the
duplicate hardcoded array, and updates stale comments mentioning F1/IndyCar.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 12:22:08 -07:00
Chris Parsons
714e6e5908 Fix UConn in NCAAW sim 2026-03-15 23:43:46 -07:00
Chris Parsons
47b8935be6
Add NCAAM and NCAAW bracket Monte Carlo simulators (#150)
- NCAAM: KenPom AEM logistic formula (1/(1+exp(-diff/7.5))), data through 2025-26 March 15
- NCAAW: Barttorvik Barthag Log5 formula (A*(1-B)/(A*(1-B)+B*(1-A))), same bracket structure
- Both simulators: 50,000-iteration Monte Carlo, First Four simulation, honors completed matches
- Track E8+ placements only: champion, finalist, FF losers (3rd/4th), E8 losers (5th–8th)
- Add bracket configuration validation: null R64 slots must exactly match First Four mapping
- Fix DEFAULT_SCORING_RULES to 100/70/45/45/20/20/20/20 (3rd/4th=45, 5th–8th=20)
- Align scoring constants across simulate route, expected-values display, and server action
- Zero out EVs for non-bracket participants on every simulation run (prevents EV inflation)
- Add EV total invariant warning (expected ~340) on expected-values admin page
- 98 unit tests across NCAAM, NCAAW, and UCL simulators — all passing

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 23:31:47 -07:00
Chris Parsons
70c22d03fc
feat: add UCL bracket Monte Carlo simulator (#131)
Implements a 16-team UEFA Champions League knockout bracket simulator
using blended Elo + futures odds probabilities (70/30 split). Tracks
integer placement counts per tier to avoid fractional accumulation
drift, ensuring total EV sums to exactly 340 by construction.

- New UCLSimulator class reading live playoffMatches from DB
- Respects completed match results (eliminated teams get exact EV)
- New ucl_bracket simulator_type enum value + migration
- Registered in simulator registry; added to admin sport selector
- 18 unit tests covering math, bracket path, and integration scenarios

Fixes #113

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 23:04:51 -07:00
Chris Parsons
c6ba59b0e6
User/chris/ev f1 framework (#93)
* feat: EV simulation framework with F1 Monte Carlo simulator

- Add EV snapshot tables (participant_ev_snapshots, team_ev_snapshots) and simulation_status column on sports seasons
- Add ev-snapshot model with upsert and history query functions
- Add simulator framework: types, bracket/F1/golf simulators, registry
- F1 simulator: vig-removed ICM weighted draw (pre-season) + race-by-race Monte Carlo from current standings (in-season); per-position column normalization to prevent floating-point EV drift
- Add admin simulate route and Run Simulation button on sports season page
- Rework futures-odds admin page to save odds then run simulation in one action
- Remove recalculate-probabilities route (superseded by simulate route)
- Remove EV trend chart panel and associated DB queries

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: map simulators to sports via simulatorType field

Adds a `simulator_type` enum column to the `sports` table so each sport
can be assigned a specific simulation algorithm rather than deriving it
from the sports season's scoring pattern.

- Add `simulatorTypeEnum` (f1_standings, indycar_standings,
  golf_qualifying_points, playoff_bracket) + `simulatorType` nullable
  column on `sports` table; migration 0037
- Rewrite simulator registry to key off `SimulatorType` instead of
  `ScoringPattern`; indycar_standings shares F1Simulator for now
- `findSportsSeasonById` now returns `SportsSeasonWithSport` so callers
  have typed access to `sport.simulatorType`
- Simulate and futures-odds actions read `sport.simulatorType`; guard
  fires before setting `simulationStatus: running`
- Admin sport edit page gains a Simulator Type dropdown

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 15:34:31 -07:00