Commit graph

18 commits

Author SHA1 Message Date
Chris Parsons
81d063faa1 Auto-populate & auto-score tennis Grand Slam brackets from Wikipedia
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 3m13s
🚀 Deploy / ʦ🔍 Typecheck & Lint (pull_request) Successful in 1m23s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
Add a per-event "Sync Draw" that pulls a tennis major's full 128-player
draw from its Wikipedia article, auto-creates/links participants (and
propagates them to every linked sibling season), builds the bracket, and
runs the qualifying-points scorer. Re-running advances the bracket and
scoring as matches complete.

Core
- match-sync: WikipediaTennisAdapter + wikitext bracket parser, DrawSync
  DTOs, syncTennisDraw orchestrator, pure draw->rows mapping
- playoff-match: populateBracketFromDraw (idempotent upsert on externalMatchId)
- scoring_events.externalSourceKey column (Wikipedia article; migration 0123)
- admin bracket "Sync Draw" card (accepts URL or title) + cron pass

Scoring fix
- deriveBracketQualifyingStates only floors players who have reached the
  scoring stage; for deep brackets (tennis_128) early-round losers earn 0 QP
  instead of a phantom 9th-place floor. CS2/simple_8 behavior preserved
  (gated on rounds[0].isScoring). Re-sync reconciles stale QP rows in a
  transaction.

Matching & parsing
- accent-folding in normalizeTeamName; strip Wikipedia "(tennis)"
  disambiguators; treat Bye/TBD/Qualifier as TBD; extract wikilink before
  template-stripping so {{nowrap}}-wrapped players parse

Admin UX
- dry-run preview (matched / will-create / possible duplicates / unfilled
  slots) with inline "rename existing" / "create as new" resolution via
  fetcher (no full reload)

Tests: parser vs real 2025 Wimbledon fixture, draw mapping, tennis_128
scoring, accent/disambiguator/nowrap parsing, URL parsing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 21:48:00 -07:00
Chris Parsons
0eb486f8d3
Fix bracket scoring logic to prefer template over DB defaults (#298)
* Emit standings-updated socket event after recalculate-floors so league pages refresh automatically

When the admin runs "Recalculate Floors" on the bracket page, the league
sports-season homepage was showing stale elimination data because there was
no mechanism to notify it of the change.

Fix: after recalculate-floors updates participant_results, emit a
standings-updated socket event to all fantasy-season draft rooms linked to
the sports season. The sports-season page now joins its draft room and
revalidates its loader whenever it receives that event for the matching
sports season.

https://claude.ai/code/session_01D3NbnVQdaH82Fk7Jm8y3sB

* Fix recalculate-floors incorrectly eliminating play-in losers who still advance

The playoff_matches.isScoring column defaults to true in the database. Brackets
created before this column was added (or before the migration set correct values)
have isScoring=true on play-in rounds that should be false. When recalculate-floors
replayed those matches, it took the "scoring round" path; since "Play-In Round 1"
isn't in ROUND_CONFIG, config===null, and the loser was assigned finalPosition=0
regardless of loserAdvances — permanently eliminating teams like the Suns who had
a second play-in game remaining.

Fix: build a round→isScoring lookup from the bracket template before replaying
matches and use it as the source of truth, falling back to the DB field only when
the template doesn't define the round. This ensures non-scoring play-in rounds
are always processed with isScoring=false so the loserAdvances guard fires
correctly.

Also revert unrelated socket changes from the previous (wrong) commit.

https://claude.ai/code/session_01D3NbnVQdaH82Fk7Jm8y3sB

* Fix isScoring fallback and loserAdvances gaps across all match-processing paths

Three issues found in code review around the recalculate-floors fix:

1. set-winner and set-round-winners (bracket.server.ts) used `match.isScoring ?? true`
   just like recalculate-floors did. Both now derive isScoring from the bracket
   template as the source of truth, falling back to the DB field only when the round
   isn't defined in the template.

2. processPlayoffEvent (scoring-calculator.ts) used `matches[0]?.isScoring ?? true`
   with the same DB-default problem. Now uses BRACKET_TEMPLATES[bracketTemplateId]
   to look up the round's isScoring before falling back to the DB field.

3. doesLoserAdvance (playoff-match.ts) was missing the AFL afl_10 Qualifying Finals
   case: both losers advance to Semi-Finals. Without this, AFL QF losers would
   incorrectly receive finalPosition=0 when processed through the non-scoring path.

https://claude.ai/code/session_01D3NbnVQdaH82Fk7Jm8y3sB

* Fix no-non-null-assertion lint errors in isScoring Map lookups

Replace Map.has(key) ? Map.get(key)! : fallback pattern with
Map.get(key) ?? fallback to satisfy oxlint no-non-null-assertion rule.

https://claude.ai/code/session_01D3NbnVQdaH82Fk7Jm8y3sB

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-15 11:27:00 -07:00
Chris Parsons
d6a92c44ff
fix: NBA play-in 7v8 loser incorrectly marked as eliminated (#295)
The loser of NBA Play-In Round 1 matches 1 and 3 (East/West 7v8 games)
advances to Play-In Round 2 rather than being eliminated, but was being
assigned finalPosition=0 and appearing in Discord as knocked out.

- Add `loserAdvances` param to `processMatchResult` to skip recording an
  elimination result when the loser continues to another match
- Extract `doesLoserAdvance()` to `playoff-match.ts` as the single source
  of truth for which play-in matches have advancing losers (replaces
  duplicated hardcoded checks at both call sites in bracket.server.ts)
- Extract `isLoserNotifiable()` as a pure exported helper so Discord
  notification eligibility is testable; losers only appear when their
  team's score changed OR they have a finalized (non-partial) result,
  correctly suppressing advancing losers while still surfacing 0-pt
  eliminations that produce no score delta
- Add 14 new unit tests covering `doesLoserAdvance`, `isLoserNotifiable`,
  and the advancing-loser/eliminated-loser `processMatchResult` branches

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 22:16:57 -07:00
Chris Parsons
66b0235678
fix: NBA play-in bracket advancement bugs (#292)
* fix: NBA play-in advancement writes E7/W7 to wrong participant slot

advanceNBAPlayInWinner was writing the PIR1 7v8 winner to participant1Id
of First Round M3/M7, but those slots already hold E2/W2 (seeded at
generation time). The "already filled" guard fired, silently swallowing
both the winner advancement and the loser's placement into Play-In
Round 2 — so the 7/8 loser never appeared in the second play-in game.

Fix: write the 7v8 winner to participant2Id (the correct null slot) and
update the guard, doc comment, and load-bearing comment accordingly.
Also swap the p2Override arguments in the simulator and test fixture to
match the corrected slot layout (FR M3: E2=p1, E7=p2).

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

* fix: hasGroupStage check for templates without groupStage property

undefined !== null is true, so templates that omit groupStage (like
nba_20) were incorrectly treated as group-stage templates, triggering
the generate-groups action and the "Invalid template or template has
no group stage" error.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 22:25:32 -07:00
Chris Parsons
040c97137b
feat: NBA playoff bracket template and bracket-aware simulator (#291)
Adds the NBA_20 bracket template (20-team structure with play-in
tournament) and rewrites the NBA simulator to be bracket-aware like UCL.

- Add NBA_20 bracket template: PIR1 (4) → PIR2 (2) → First Round (8)
  → Conference Semifinals → Conference Finals → NBA Finals
- Add participantLabels (East/West 1–10) for admin bracket UI slot labels
- Add generateNBA20Bracket and advanceNBAPlayInWinner in playoff-match.ts
  with custom play-in advancement logic (PIR1 winners go to two different
  destinations)
- Rewrite NBASimulator with two auto-detected modes:
  - Bracket-aware (preferred): reads actual bracket state, simulates only
    remaining rounds forward using 50k Monte Carlo iterations
  - Season-projection (fallback): existing logic for pre-playoff use
- Guard resolveGame/resolveSeries against null participants (throws with
  match ID and slot info instead of silently using empty string)
- Document load-bearing First Round match ordering in generateNBA20Bracket
- Add 25 unit tests covering Elo math, team data, template structure,
  and all simulator integration paths

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 21:51:53 -07: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
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
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
0277c4d25e
Add per-event configurable region config for NCAA-style brackets (#149) (#149)
Replaces the hardcoded 2026 Men's region structure with a per-event
configurable system so the same ncaa_68 template works for Women's
March Madness (different region names and play-in placements each year).

- Add `bracket_region_config jsonb` column to `scoring_events`
- Export `ALL_16_SEEDS` and `BracketRegion`/`BracketPlayIn` interfaces
  from bracket-templates.ts so both server and client share the type
- Admin bracket entry form now shows a "Configure Regions" section:
  editable region names + add/remove play-in seed selectors (ShadCN
  Select) per region; slot map updates live as config changes
- Server action parses region config from form, validates total = 68,
  stores it on the event, and passes it to bracket generation
- `advanceFirstFourWinner` reads stored `bracketRegionConfig` from the
  event row first, falls back to template.regions for existing events
- Fix opponentSeed formula (was `seedNum <= 8 ? 17-n : n`, always `17-n`)
- Remove duplicate ALL_SEEDS inline definitions (now a shared constant)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 21:52:47 -07:00
Chris Parsons
2f14565d43
Add playoff match game scheduling and odds management (#135)
* Add playoff match games and odds storage

Introduces two new tables for bracket matchup detail storage:

- `playoff_match_games`: tracks individual game schedules within a
  series matchup (game number, scheduledAt, status, per-game scores,
  winner). Supports scheduled/complete/postponed status enum.
- `playoff_match_odds`: stores moneyline odds per participant per
  matchup (single upsert record, no isLatest complexity).

Includes:
- Drizzle schema + relations with CASCADE deletes from playoff_matches
- Migration 0040_fat_puma.sql
- playoff-match-game.ts model with pure helpers: computeSeriesScore,
  isSeriesComplete, getSeriesLeader — plus full CRUD
- playoff-match-odds.ts model with pure helpers: americanToImpliedProbability,
  impliedProbabilityToAmerican, normalizeOdds — plus upsert/read/delete
- findPlayoffMatchesByEventId and findPlayoffMatchById updated to
  include games and odds in their query results
- Bracket server route: add-game, update-game, delete-game,
  upsert-odds, delete-odds actions
- Bracket admin UI: expandable per-match panel for game schedule
  management and moneyline odds entry
- 41 new unit tests (18 game + 23 odds), all 810 tests passing

https://claude.ai/code/session_01Twt3D1bsEK3eXUhMaz6ee7

* Code review fixes: type safety, abstraction, and React correctness

- Derive PlayoffMatchGameStatus from schema enum instead of hardcoding
  the string union, eliminating the duplicate source of truth
- updateGame now returns PlayoffMatchGame | undefined to reflect reality
  when no row matches the ID
- Remove TOCTOU check-then-act in update-game action: call updateGame
  directly and check the return value instead of a pre-flight findGameById
- Add status enum validation before the cast in update-game action
- Move impliedProbability computation inside upsertMatchOdds so callers
  only provide moneylineOdds; the model owns the derivation
- Remove unnecessary dynamic import of americanToImpliedProbability in
  the upsert-odds action (was already imported from the same module)
- Fix React list reconciliation bug: replace bare <> fragment with
  <Fragment key={match.id}> so React can correctly track rows

https://claude.ai/code/session_01Twt3D1bsEK3eXUhMaz6ee7

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-11 14:17:43 -07:00
Chris Parsons
7970cb6a9c feat: add FIFA World Cup 48-team bracket template with group stage and projected scoring
- Add FIFA_48 bracket template with 12 groups of 4, knockout stage of 32
- Add tournament groups schema, model, and GroupStageDisplay component
- Add projected/expected value scoring to standings and team breakdowns
- Add docker-compose for local PostgreSQL development
- Move DRAFT_ORDER_IMPLEMENTATION.md to plans directory

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 22:30:12 -08:00
Chris Parsons
4d5b4efc23 feat: Implement AFL Finals bracket template and scoring logic with double-chance system 2025-11-12 23:21:22 -08:00
Chris Parsons
5d0e6b999c feat: Implement NFL 14 bracket template with bye week handling and comprehensive unit tests 2025-11-08 21:56:57 -08:00
Chris Parsons
bbacb00d6c feat: Implement NCAA 68 tournament structure with First Four play-in games and Round of 64 seeding logic 2025-11-08 21:18:09 -08:00
Chris Parsons
dfe4b7b643 Add bracket seeding tests and implement tournament-style seeding for 16 and 32-team brackets
- Introduced `generateStandardSeeding()` function to create proper tournament matchups (e.g., 1v16, 8v9).
- Added comprehensive tests for 4, 8, 16, and 32-team brackets to ensure correct seeding and balance.
- Updated testing instructions to reflect new features and improvements in bracket generation.
- Fixed issues with sequential seeding and ensured all matchups sum to n+1 for balance.
- Implemented batch winner selection and duplicate participant prevention for improved user experience.
- Enhanced fantasy points display on event pages for better visibility of participant results.
2025-11-04 22:09:44 -08:00
Chris Parsons
da1a17f2d3 Refactor code structure for improved readability and maintainability 2025-11-03 13:16:37 -08:00
Chris Parsons
a3b8fa6309 feat: Implement bracket expansion plan to support various tournament structures
- Added a comprehensive plan for bracket expansion, including support for 4, 8, 16, 32, and 68 team formats.
- Introduced a template-based bracket system with predefined templates for NCAA March Madness, NFL Playoffs, NBA Playoffs, and simple brackets.
- Updated UI flow for bracket creation, allowing admins to select templates and assign participants flexibly.
- Enhanced database schema to accommodate new scoring rules and bracket templates.
- Proposed updates to scoring logic to handle non-scoring rounds and participant placements correctly.
- Documented implementation phases for gradual rollout of new features.
- Addressed critical bugs in the playoff event processing and scoring logic, ensuring proper advancement and scoring rules across multiple leagues.
2025-11-03 09:36:16 -08:00