Commit graph

2 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
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