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
import type { Route } from "./+types/admin.sports-seasons.$id.events.$eventId.bracket" ;
2026-03-21 09:44:05 -07:00
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
import { findSportsSeasonById } from "~/models/sports-season" ;
Canonical tournament layer: schema + backfill (1/2) (#365)
* refactor(schema): rename per-window tables to season_* prefix
Renames participants, participant_expected_values, participant_qualifying_totals,
participant_results, participant_surface_elos to season_* prefixed names.
Renames event_results.participant_id to season_participant_id.
Phase 1a of canonical tournament layer migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor: rename participant.ts model file to season-participant.ts
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(models): update model layer to use renamed schema exports
Updated all model files to use the renamed schema exports from Task 1:
- participants → seasonParticipants
- participantExpectedValues → seasonParticipantExpectedValues
- participantQualifyingTotals → seasonParticipantQualifyingTotals
- participantResults → seasonParticipantResults
- participantSurfaceElos → seasonParticipantSurfaceElos
- eventResults.participantId → eventResults.seasonParticipantId
- db.query relation accessors updated
- Relation field .participant → .seasonParticipant where applicable
- Import paths updated: ./participant → ./season-participant
Files updated (14 model files + 3 test files):
- draft-pick.ts
- draft-utils.ts
- event-result.ts
- group-stage-match.ts
- participant-result.ts
- qualifying-points.ts
- scoring-calculator.ts
- scoring-event.ts
- sports-season.ts
- surface-elo.ts
- team-score-events.ts
- cs2-major-stage.ts
- golf-skills.ts
- participant-expected-value.ts
- __tests__/sports-season.clone.test.ts
- __tests__/auto-pick.test.ts
- __tests__/executeAutoPick.timer.test.ts
Typecheck errors decreased: 779 → 499 (280 fewer)
All model file errors related to renamed schemas resolved.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(routes): update route layer to use renamed schema exports
- Update model import from ~/models/participant to ~/models/season-participant
- Rename schema.participants to schema.seasonParticipants
- Rename schema.participantResults to schema.seasonParticipantResults
- Rename db.query.participants to db.query.seasonParticipants
- Update 9 route files and 1 test file
Affected files:
- admin.sports-seasons.$id.events.$eventId.bracket.server.ts
- admin.sports-seasons.$id.participants.tsx
- api/draft.force-manual-pick.ts
- api/draft.make-pick.ts
- api/draft.replace-pick.ts
- api/seasons.$seasonId.draft.ts
- leagues/$leagueId.draft-board.$seasonId.tsx
- leagues/$leagueId.sports-seasons.$sportsSeasonId.server.ts
- admin/__tests__/sports-seasons-participants.test.ts
Error count reduced from 499 to 453 (46 errors fixed).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(routes): update route files for schema rename
Update route imports from ~/models/participant to ~/models/season-participant
and fix references to .participant/.participantId on event results to use
.seasonParticipant/.seasonParticipantId after schema rename.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(services): update simulators and services for renamed schema
Update all simulators, services, and server files to use renamed schema tables:
- participants → seasonParticipants
- participantExpectedValues → seasonParticipantExpectedValues
- participantResults → seasonParticipantResults
- eventResults.participantId → eventResults.seasonParticipantId
Files updated:
- 20 sport simulators (NBA, NHL, NFL, MLB, etc.)
- probability-updater.ts
- standings-sync/index.ts
- sports-data-sync.server.ts
- server/socket.ts
Typecheck errors reduced from 365 to 0.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* migration: rename per-window tables to season_* prefix
* fix(tests): update mock query keys after participants table rename
Change mock db.query.participants to db.query.seasonParticipants in test
files to match the schema rename from commit 66145a9. This fixes
"Cannot read properties of undefined (reading 'findFirst'/'findMany')"
errors that occurred when production code queries db.query.seasonParticipants
but test mocks only defined the old participants key.
Files updated:
- app/services/simulations/__tests__/world-cup-simulator.test.ts
- app/routes/api/__tests__/draft.force-manual-pick.test.ts
- app/routes/api/__tests__/draft.force-manual-pick.timer-mode.test.ts
- app/routes/api/__tests__/draft.make-pick.timer-mode.test.ts
- server/__tests__/timer-autodraft.test.ts
- app/models/__tests__/team-score-events.test.ts
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(tests): update remaining mock paths and keys after schema rename
* fix(tests): final two mock stragglers after schema rename
- draft-pick.test.ts: assertion on db.query.participantQualifyingTotals
- process-match-result.test.ts: mock key participants → seasonParticipants
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: add post-phase1a baseline capture (temp, for diff verification)
* chore: capture pre-migration baselines
* chore: remove post-phase1a capture helper after verification
* schema: add canonical tournament & participant tables
Adds tournaments, participants (canonical), tournament_results, and
participant_surface_elos (canonical). Adds nullable tournament_id to
scoring_events and nullable participant_id to season_participants.
Phase 1b of canonical tournament layer migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(models): add canonical tournament, participant, result, surface-elo models
Adds CRUD modules for the canonical tables created in commit 775b905.
Each module mirrors existing app/models conventions (database() from
~/database/context, schema from ~/database/schema, mock-based tests).
Key implementation notes:
- participant.ts exports use "Canonical" prefix (CanonicalParticipant,
createCanonicalParticipant, etc.) to avoid collision with existing
season-participant.ts exports
- All four models include comprehensive unit tests following the
audit-log.test.ts pattern
- Tests use mocked db responses (no real database access)
- Upsert functions use onConflictDoUpdate for appropriate unique constraints
Part of Phase 1b of canonical tournament layer migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* migration: create canonical tables, add nullable FKs
* scripts: add extractTournamentIdentity helper for backfill
Pure function that derives canonical (name, year) identity from a
scoring_events row, stripping trailing 4-digit years from the name or
falling back to eventDate. Used by the Phase 2 backfill to group
per-window events into canonical tournaments.
* scripts: add backfill orchestrator for canonical layer
Populates canonical tournaments, participants, tournament_results, and
participant_surface_elos from per-window data for qualifying-points
sports. Skips already-linked rows, is idempotent, and supports dry-run
mode.
Critical invariants enforced by the implementation:
- qualifying_points_awarded is never copied to tournament_results
- season_participant_qualifying_totals is never touched
- conflicting surface-Elo values between windows raise a loud error
(recorded in report.errors) rather than overwriting
* scripts: add backfill CLI with dry-run default
Wires backfill-canonical-layer.ts to a CLI entry point exposed as
`npm run backfill:canonical`. Defaults to --dry-run; requires --apply
to actually write. Supports --sport=<uuid> to limit to a single sport.
Exits 2 if the backfill reports errors (e.g., surface-Elo conflicts).
* fix(backfill-cli): wrap runBackfill in DatabaseContext.run
The orchestrator uses database() from ~/database/context, which requires
AsyncLocalStorage to be populated. Wrap the CLI invocation with
DatabaseContext.run(db, ...) using server/db's cached connection pool.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(backfill-cli): exit 0 on success so pg pool doesn't block
The cached postgres connection pool keeps the Node event loop open after
main() returns. Explicit process.exit(0) on success mirrors the pattern
in scripts/capture-baseline.ts.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Chris Parsons <chrisp@extrahop.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 20:13:18 -07:00
import { findParticipantsBySportsSeasonId } from "~/models/season-participant" ;
2025-11-04 22:09:44 -08:00
import { getScoringEventById , updateScoringEvent } from "~/models/scoring-event" ;
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
import {
findPlayoffMatchesByEventId ,
2025-11-04 22:09:44 -08:00
generateBracketFromTemplate ,
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
setMatchWinner ,
2025-11-04 22:09:44 -08:00
advanceWinnerTemplate ,
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
findPlayoffMatchById ,
2026-02-14 22:30:12 -08:00
assignParticipantsToKnockout ,
2026-04-14 22:16:57 -07:00
doesLoserAdvance ,
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
} from "~/models/playoff-match" ;
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
import {
createGame ,
updateGame ,
deleteGame ,
type PlayoffMatchGameStatus ,
} from "~/models/playoff-match-game" ;
import {
upsertMatchOdds ,
deleteOddsForParticipant ,
} from "~/models/playoff-match-odds" ;
Partial bracket scoring, code review fixes, and double-chance logic (#156)
## Partial bracket scoring
- `processMatchResult`: new exported function that scores a single match
immediately (loser → final placement, winner → provisional floor).
Called from `set-winner` and `set-round-winners` so points are awarded
as soon as a winner is set, before the full round is complete.
- `set-winner`: passes `eventName` to `processMatchResult`.
- `set-round-winners`: batches side effects — calls `recalculateAffectedLeagues`
and `updateProbabilitiesAfterResult` once after the loop instead of per-match
(`skipSideEffects: true` per match).
## Code review fixes
- **ROUND_CONFIG** (S1): extracted a `RoundScoringConfig` lookup table +
`getRoundConfig()` helper, eliminating three parallel `if/else` chains in
`processPlayoffEvent`, `processMatchResult`, and `getGuaranteedMinimumPosition`.
AFL template overrides live in `TEMPLATE_ROUND_CONFIG` with an explanatory
comment about why the `bracketTemplateId` guard is required.
- **skipSideEffects** (C2): new param on `processMatchResult`; bracket server
uses it to batch standings/probability recalc in `set-round-winners`.
- **eventName** (W1): passed through `processMatchResult` → `recalculateAffectedLeagues`.
- **Round validation** (W2): `set-round-winners` now guards `match.round === round`
before processing each assignment.
- **Comments** (C3/S3/W3): added notes on non-scoring loser assumption,
`isScoring ?? true` default, and AFL Semi-Finals template requirement.
## PlayoffBracket eliminated-teams fix + tests
- Fixed `computeEliminatedByRound` to track participant *appearances* (not just
wins), so AFL QF losers who advance to Semi-Finals via double-chance are
correctly excluded from the QF eliminated list.
- Extracted the logic as an exported pure function for testability.
- Added 4 tests: standard elimination, AFL QF loser → SF win, AFL QF loser →
SF loss, and normal advancement not protecting a later loser.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 10:50:30 -07:00
import {
processPlayoffEvent ,
processMatchResult ,
Award progressive guaranteed-minimum QP for qualifying-event brackets
Winning a round in a major bracket (CS2 Champions Stage) now immediately
awards the team the guaranteed-minimum qualifying points for the position
they've locked in — e.g. a QF win guarantees at least a T3 finish (9 QP),
an SF win guarantees 2nd (14 QP), and a Final win earns 1st (20 QP).
This also fixes a bug where CS2 majors (a qualifying_points sport) wrongly
banked actual fantasy points: the generic bracket admin routed results
through processMatchResult/processPlayoffEvent, which write the fantasy
placement table (seasonParticipantResults). For qualifying sports, fantasy
points must come only from finalizeQualifyingPoints across all majors.
- scoring-calculator.ts: add deriveBracketQualifyingStates (pure) and
processQualifyingBracketEvent, deriving each team's (placement, tieCount)
floor from playoffMatches via the existing ROUND_CONFIG and writing QP to
event_results. Export getRoundConfig for testing.
- bracket.server.ts: branch all five write handlers on isQualifyingEvent so
qualifying brackets are scored via the QP path and never write fantasy
placements. reprocess-bracket additionally clears stale
seasonParticipantResults rows, making it the cleanup tool for majors that
already banked points under the old path.
- Add qualifying-bracket-scoring.test.ts covering the floor derivation,
partial/un-set brackets, idempotency, and the worked QP example.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVLqaYGXCeSYYqTHZmbRZi
2026-06-18 20:23:37 +00:00
processQualifyingBracketEvent ,
processQualifyingEvent ,
2026-06-18 23:46:59 +00:00
finalizeQualifyingPoints ,
Partial bracket scoring, code review fixes, and double-chance logic (#156)
## Partial bracket scoring
- `processMatchResult`: new exported function that scores a single match
immediately (loser → final placement, winner → provisional floor).
Called from `set-winner` and `set-round-winners` so points are awarded
as soon as a winner is set, before the full round is complete.
- `set-winner`: passes `eventName` to `processMatchResult`.
- `set-round-winners`: batches side effects — calls `recalculateAffectedLeagues`
and `updateProbabilitiesAfterResult` once after the loop instead of per-match
(`skipSideEffects: true` per match).
## Code review fixes
- **ROUND_CONFIG** (S1): extracted a `RoundScoringConfig` lookup table +
`getRoundConfig()` helper, eliminating three parallel `if/else` chains in
`processPlayoffEvent`, `processMatchResult`, and `getGuaranteedMinimumPosition`.
AFL template overrides live in `TEMPLATE_ROUND_CONFIG` with an explanatory
comment about why the `bracketTemplateId` guard is required.
- **skipSideEffects** (C2): new param on `processMatchResult`; bracket server
uses it to batch standings/probability recalc in `set-round-winners`.
- **eventName** (W1): passed through `processMatchResult` → `recalculateAffectedLeagues`.
- **Round validation** (W2): `set-round-winners` now guards `match.round === round`
before processing each assignment.
- **Comments** (C3/S3/W3): added notes on non-scoring loser assumption,
`isScoring ?? true` default, and AFL Semi-Finals template requirement.
## PlayoffBracket eliminated-teams fix + tests
- Fixed `computeEliminatedByRound` to track participant *appearances* (not just
wins), so AFL QF losers who advance to Semi-Finals via double-chance are
correctly excluded from the QF eliminated list.
- Extracted the logic as an exported pure function for testability.
- Added 4 tests: standard elimination, AFL QF loser → SF win, AFL QF loser →
SF loss, and normal advancement not protecting a later loser.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 10:50:30 -07:00
recalculateAffectedLeagues ,
2026-03-18 22:15:28 -07:00
recalculateStandings ,
2026-06-12 22:35:35 +00:00
autoCompleteRoundIfDone ,
Partial bracket scoring, code review fixes, and double-chance logic (#156)
## Partial bracket scoring
- `processMatchResult`: new exported function that scores a single match
immediately (loser → final placement, winner → provisional floor).
Called from `set-winner` and `set-round-winners` so points are awarded
as soon as a winner is set, before the full round is complete.
- `set-winner`: passes `eventName` to `processMatchResult`.
- `set-round-winners`: batches side effects — calls `recalculateAffectedLeagues`
and `updateProbabilitiesAfterResult` once after the loop instead of per-match
(`skipSideEffects: true` per match).
## Code review fixes
- **ROUND_CONFIG** (S1): extracted a `RoundScoringConfig` lookup table +
`getRoundConfig()` helper, eliminating three parallel `if/else` chains in
`processPlayoffEvent`, `processMatchResult`, and `getGuaranteedMinimumPosition`.
AFL template overrides live in `TEMPLATE_ROUND_CONFIG` with an explanatory
comment about why the `bracketTemplateId` guard is required.
- **skipSideEffects** (C2): new param on `processMatchResult`; bracket server
uses it to batch standings/probability recalc in `set-round-winners`.
- **eventName** (W1): passed through `processMatchResult` → `recalculateAffectedLeagues`.
- **Round validation** (W2): `set-round-winners` now guards `match.round === round`
before processing each assignment.
- **Comments** (C3/S3/W3): added notes on non-scoring loser assumption,
`isScoring ?? true` default, and AFL Semi-Finals template requirement.
## PlayoffBracket eliminated-teams fix + tests
- Fixed `computeEliminatedByRound` to track participant *appearances* (not just
wins), so AFL QF losers who advance to Semi-Finals via double-chance are
correctly excluded from the QF eliminated list.
- Extracted the logic as an exported pure function for testability.
- Added 4 tests: standard elimination, AFL QF loser → SF win, AFL QF loser →
SF loss, and normal advancement not protecting a later loser.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 10:50:30 -07:00
} from "~/models/scoring-calculator" ;
import { updateProbabilitiesAfterResult } from "~/services/probability-updater" ;
2026-03-15 21:52:47 -07:00
import { getBracketTemplate , ALL_16_SEEDS , type BracketRegion } from "~/lib/bracket-templates" ;
2026-06-18 23:46:59 +00:00
import {
setParticipantResult ,
findParticipantResultsBySportsSeasonId ,
deleteParticipantResultsBySportsSeasonId ,
} from "~/models/participant-result" ;
2026-03-18 22:15:28 -07:00
import { findSeasonSportsBySportsSeasonId } from "~/models/season-sport" ;
import { createDailySnapshot } from "~/models/standings" ;
2026-02-14 22:30:12 -08:00
import {
createGroupsForEvent ,
addMembersToGroup ,
findGroupsByEventId ,
toggleMemberEliminated ,
getAdvancingParticipantIds ,
getEliminatedParticipantIds ,
} from "~/models/tournament-group" ;
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
import {
createGroupStageMatches ,
generateRoundRobinPairings ,
updateGroupStageMatchResult ,
updateGroupStageMatchSchedule ,
findMatchesByGroupId ,
} from "~/models/group-stage-match" ;
2026-03-21 13:41:39 -07:00
import { logger } from "~/lib/logger" ;
2025-11-03 13:16:37 -08:00
import { database } from "~/database/context" ;
import * as schema from "~/database/schema" ;
2026-03-21 09:44:05 -07:00
import { eq } from "drizzle-orm" ;
2026-05-04 20:31:44 -07:00
import { maybeResolveCompletedBracktForSportsSeason } from "~/services/brackt.server" ;
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
export async function loader ( { params } : Route . LoaderArgs ) {
const sportsSeason = await findSportsSeasonById ( params . id ) ;
if ( ! sportsSeason ) {
throw new Response ( "Sports season not found" , { status : 404 } ) ;
}
const event = await getScoringEventById ( params . eventId ) ;
if ( ! event ) {
throw new Response ( "Event not found" , { status : 404 } ) ;
}
2026-06-15 22:05:25 +00:00
if ( event . eventType !== "playoff_game" && event . eventType !== "major_tournament" ) {
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
throw new Response ( "This event is not a playoff event" , { status : 400 } ) ;
}
const participants = await findParticipantsBySportsSeasonId ( params . id ) ;
const matches = await findPlayoffMatchesByEventId ( params . eventId ) ;
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
// Fetch tournament groups with their round-robin matches
const tournamentGroupsRaw = await findGroupsByEventId ( params . eventId ) ;
const tournamentGroups = await Promise . all (
tournamentGroupsRaw . map ( async ( group ) = > ( {
. . . group ,
groupMatches : await findMatchesByGroupId ( group . id ) ,
} ) )
) ;
2026-02-14 22:30:12 -08:00
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
// The matches already include participant relations from the model query
return {
sportsSeason : sportsSeason as typeof sportsSeason & {
sport : { id : string ; name : string ; type : string ; slug : string } ;
} ,
event ,
participants ,
matches : matches as Array < typeof matches [ 0 ] & {
participant1 : { id : string ; name : string } | null ;
participant2 : { id : string ; name : string } | null ;
winner : { id : string ; name : string } | null ;
loser : { id : string ; name : string } | null ;
2026-03-21 09:44:05 -07:00
games : Array < { id : string ; gameNumber : number ; scheduledAt : Date | null ; status : string ; winner ? : { name : string } | null } > ;
odds : Array < { id : string ; participantId : string ; moneylineOdds : number ; impliedProbability : string ; oddsSource? : string | null ; participant : { id : string ; name : string } | null } > ;
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
} > ,
2026-02-14 22:30:12 -08:00
tournamentGroups ,
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
} ;
}
2026-06-18 23:46:59 +00:00
/ * *
* Score a qualifying - event bracket ( e . g . CS2 Champions Stage ) : derive each team ' s
* guaranteed - minimum QP from the bracket and refresh affected league standings .
*
* Qualifying brackets award QUALIFYING POINTS , not fantasy points , so this never
* writes seasonParticipantResults and never records team_score_events — match
* results surface via the QP Standings and the Discord standings update . Final
* fantasy placements come from finalizeQualifyingPoints across all majors .
* /
async function scoreQualifyingBracket (
event : { id : string ; sportsSeasonId : string ; name : string | null } ,
db : ReturnType < typeof database > ,
recalcOptions? : Parameters < typeof recalculateAffectedLeagues > [ 2 ]
) : Promise < void > {
await processQualifyingBracketEvent ( event . id , db ) ;
await recalculateAffectedLeagues (
event . sportsSeasonId ,
db ,
recalcOptions ? ? { eventId : event.id , eventName : event.name ? ? undefined }
) ;
}
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
export async function action ( { request , params } : Route . ActionArgs ) {
const formData = await request . formData ( ) ;
const intent = formData . get ( "intent" ) ;
if ( intent === "generate-bracket" ) {
2025-11-04 22:09:44 -08:00
const templateId = formData . get ( "templateId" ) ;
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
2025-11-04 22:09:44 -08:00
if ( typeof templateId !== "string" || ! templateId ) {
return { error : "Template ID is required" } ;
}
const template = getBracketTemplate ( templateId ) ;
if ( ! template ) {
return { error : "Invalid bracket template" } ;
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
}
2026-03-15 21:52:47 -07:00
// Parse per-event region config for NCAA-style brackets
let regionOverride : BracketRegion [ ] | undefined ;
if ( template . regions && template . regions . length > 0 ) {
const regions : BracketRegion [ ] = [ ] ;
for ( let r = 0 ; r < template . regions . length ; r ++ ) {
const name = ( formData . get ( ` regionName ${ r } ` ) as string | null ) ? . trim ( )
|| ` Region ${ r + 1 } ` ;
const seedsRaw = ( formData . get ( ` regionPlayInSeeds ${ r } ` ) as string | null ) || "" ;
const playInSeeds = seedsRaw
. split ( "," )
. map ( ( s ) = > parseInt ( s . trim ( ) , 10 ) )
. filter ( ( n ) = > ! isNaN ( n ) && n >= 1 && n <= 16 ) ;
const playInSet = new Set ( playInSeeds ) ;
regions . push ( {
name ,
directSeeds : ALL_16_SEEDS.filter ( ( s ) = > ! playInSet . has ( s ) ) ,
playIns : playInSeeds.map ( ( s ) = > ( { seedSlot : s , teams : 2 as const } ) ) ,
} ) ;
}
// Validate total = template.totalTeams
const total = regions . reduce (
( sum , r ) = > sum + r . directSeeds . length + r . playIns . length * 2 ,
0
) ;
if ( total !== template . totalTeams ) {
return {
error : ` Region config accounts for ${ total } team slots but template requires ${ template . totalTeams } ` ,
} ;
}
regionOverride = regions ;
}
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
const participantIds : string [ ] = [ ] ;
2025-11-04 22:09:44 -08:00
for ( let i = 0 ; i < template . totalTeams ; i ++ ) {
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
const participantId = formData . get ( ` participant ${ i } ` ) ;
if ( typeof participantId !== "string" || ! participantId ) {
return { error : ` Participant ${ i + 1 } is required ` } ;
}
participantIds . push ( participantId ) ;
}
// Check for duplicates
const uniqueParticipants = new Set ( participantIds ) ;
if ( uniqueParticipants . size !== participantIds . length ) {
return { error : "Each participant can only be selected once" } ;
}
try {
2026-03-15 21:52:47 -07:00
await generateBracketFromTemplate ( params . eventId , templateId , participantIds , regionOverride ) ;
2025-11-04 22:09:44 -08:00
2025-11-21 22:05:50 -08:00
// PHASE 5.3: Mark participants NOT in the bracket as eliminated
const event = await getScoringEventById ( params . eventId ) ;
if ( event ) {
// Get all participants in the sports season
const allParticipants = await findParticipantsBySportsSeasonId ( params . id ) ;
// Create a set of participants in the bracket for fast lookup
const participantsInBracket = new Set ( participantIds ) ;
// Mark participants NOT in the bracket as eliminated
for ( const participant of allParticipants ) {
if ( ! participantsInBracket . has ( participant . id ) ) {
await setParticipantResult (
participant . id ,
event . sportsSeasonId ,
0 // 0 = didn't make playoffs, eliminated
) ;
}
}
2026-03-21 13:41:39 -07:00
logger . log ( ` [BracketGeneration] Marked ${ allParticipants . length - participantIds . length } participants as eliminated ` ) ;
2025-11-21 22:05:50 -08:00
}
2026-03-15 21:52:47 -07:00
// Update the event to store the template ID, scoring start round, and region config
2025-11-04 22:09:44 -08:00
await updateScoringEvent ( params . eventId , {
bracketTemplateId : templateId ,
scoringStartsAtRound : template.scoringStartsAtRound ,
2026-03-15 21:52:47 -07:00
bracketRegionConfig : regionOverride ,
2025-11-04 22:09:44 -08:00
} ) ;
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
return { success : "Bracket generated successfully" } ;
} catch ( error ) {
2026-03-21 13:41:39 -07:00
logger . error ( "Error generating bracket:" , error ) ;
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
return {
error :
error instanceof Error
? error . message
: "Failed to generate bracket" ,
} ;
}
}
if ( intent === "set-winner" ) {
const matchId = formData . get ( "matchId" ) ;
const winnerId = formData . get ( "winnerId" ) ;
if ( typeof matchId !== "string" || ! matchId ) {
return { error : "Match ID is required" } ;
}
if ( typeof winnerId !== "string" || ! winnerId ) {
return { error : "Winner ID is required" } ;
}
try {
const match = await findPlayoffMatchById ( matchId ) ;
if ( ! match ) {
return { error : "Match not found" } ;
}
2025-11-04 22:09:44 -08:00
// Get the event to determine the template
const event = await getScoringEventById ( match . scoringEventId ) ;
if ( ! event ) {
return { error : "Event not found" } ;
}
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
// Determine loser
const loserId =
match . participant1Id === winnerId
? match . participant2Id
: match . participant1Id ;
if ( ! loserId ) {
return { error : "Could not determine loser" } ;
}
// Set the winner
await setMatchWinner ( matchId , winnerId , loserId ) ;
2025-11-04 22:09:44 -08:00
// Try to advance the winner to the next round using template
2026-04-15 11:27:00 -07:00
const setWinnerTemplate = event . bracketTemplateId ? getBracketTemplate ( event . bracketTemplateId ) : null ;
if ( setWinnerTemplate ) {
try {
await advanceWinnerTemplate ( matchId , winnerId , setWinnerTemplate ) ;
} catch ( error ) {
// Only ignore "already filled" errors, re-throw unexpected errors
if (
error instanceof Error &&
error . message . includes ( "already filled" )
) {
logger . warn ( "Match already filled, skipping advancement" ) ;
} else {
throw error ; // Re-throw unexpected errors
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
}
}
}
2026-03-17 12:34:10 -07:00
const db = database ( ) ;
Award progressive guaranteed-minimum QP for qualifying-event brackets
Winning a round in a major bracket (CS2 Champions Stage) now immediately
awards the team the guaranteed-minimum qualifying points for the position
they've locked in — e.g. a QF win guarantees at least a T3 finish (9 QP),
an SF win guarantees 2nd (14 QP), and a Final win earns 1st (20 QP).
This also fixes a bug where CS2 majors (a qualifying_points sport) wrongly
banked actual fantasy points: the generic bracket admin routed results
through processMatchResult/processPlayoffEvent, which write the fantasy
placement table (seasonParticipantResults). For qualifying sports, fantasy
points must come only from finalizeQualifyingPoints across all majors.
- scoring-calculator.ts: add deriveBracketQualifyingStates (pure) and
processQualifyingBracketEvent, deriving each team's (placement, tieCount)
floor from playoffMatches via the existing ROUND_CONFIG and writing QP to
event_results. Export getRoundConfig for testing.
- bracket.server.ts: branch all five write handlers on isQualifyingEvent so
qualifying brackets are scored via the QP path and never write fantasy
placements. reprocess-bracket additionally clears stale
seasonParticipantResults rows, making it the cleanup tool for majors that
already banked points under the old path.
- Add qualifying-bracket-scoring.test.ts covering the floor derivation,
partial/un-set brackets, idempotency, and the worked QP example.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVLqaYGXCeSYYqTHZmbRZi
2026-06-18 20:23:37 +00:00
if ( event . isQualifyingEvent ) {
// Qualifying major (e.g. CS2): bracket results award QUALIFYING POINTS, not
2026-06-18 23:46:59 +00:00
// fantasy points. matchIds scopes the Discord notification to just this match.
await scoreQualifyingBracket ( event , db , {
Award progressive guaranteed-minimum QP for qualifying-event brackets
Winning a round in a major bracket (CS2 Champions Stage) now immediately
awards the team the guaranteed-minimum qualifying points for the position
they've locked in — e.g. a QF win guarantees at least a T3 finish (9 QP),
an SF win guarantees 2nd (14 QP), and a Final win earns 1st (20 QP).
This also fixes a bug where CS2 majors (a qualifying_points sport) wrongly
banked actual fantasy points: the generic bracket admin routed results
through processMatchResult/processPlayoffEvent, which write the fantasy
placement table (seasonParticipantResults). For qualifying sports, fantasy
points must come only from finalizeQualifyingPoints across all majors.
- scoring-calculator.ts: add deriveBracketQualifyingStates (pure) and
processQualifyingBracketEvent, deriving each team's (placement, tieCount)
floor from playoffMatches via the existing ROUND_CONFIG and writing QP to
event_results. Export getRoundConfig for testing.
- bracket.server.ts: branch all five write handlers on isQualifyingEvent so
qualifying brackets are scored via the QP path and never write fantasy
placements. reprocess-bracket additionally clears stale
seasonParticipantResults rows, making it the cleanup tool for majors that
already banked points under the old path.
- Add qualifying-bracket-scoring.test.ts covering the floor derivation,
partial/un-set brackets, idempotency, and the worked QP example.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVLqaYGXCeSYYqTHZmbRZi
2026-06-18 20:23:37 +00:00
eventId : event.id ,
eventName : event.name ? ? undefined ,
2026-06-18 23:46:59 +00:00
matchIds : [ matchId ] ,
Award progressive guaranteed-minimum QP for qualifying-event brackets
Winning a round in a major bracket (CS2 Champions Stage) now immediately
awards the team the guaranteed-minimum qualifying points for the position
they've locked in — e.g. a QF win guarantees at least a T3 finish (9 QP),
an SF win guarantees 2nd (14 QP), and a Final win earns 1st (20 QP).
This also fixes a bug where CS2 majors (a qualifying_points sport) wrongly
banked actual fantasy points: the generic bracket admin routed results
through processMatchResult/processPlayoffEvent, which write the fantasy
placement table (seasonParticipantResults). For qualifying sports, fantasy
points must come only from finalizeQualifyingPoints across all majors.
- scoring-calculator.ts: add deriveBracketQualifyingStates (pure) and
processQualifyingBracketEvent, deriving each team's (placement, tieCount)
floor from playoffMatches via the existing ROUND_CONFIG and writing QP to
event_results. Export getRoundConfig for testing.
- bracket.server.ts: branch all five write handlers on isQualifyingEvent so
qualifying brackets are scored via the QP path and never write fantasy
placements. reprocess-bracket additionally clears stale
seasonParticipantResults rows, making it the cleanup tool for majors that
already banked points under the old path.
- Add qualifying-bracket-scoring.test.ts covering the floor derivation,
partial/un-set brackets, idempotency, and the worked QP example.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVLqaYGXCeSYYqTHZmbRZi
2026-06-18 20:23:37 +00:00
} ) ;
} else {
// Immediately score this match: loser gets their final placement,
// winner gets provisional floor points (isPartialScore=true).
// Prefer the template-defined isScoring over the DB field: the DB column
// defaults to true, so legacy play-in rows may be incorrectly marked as scoring.
const setWinnerRoundIsScoring = setWinnerTemplate ? . rounds . find ( ( r ) = > r . name === match . round ) ? . isScoring ;
await processMatchResult ( {
round : match.round ,
winnerId ,
loserId ,
isScoring : setWinnerRoundIsScoring !== undefined ? setWinnerRoundIsScoring : ( match . isScoring ? ? true ) ,
sportsSeasonId : event.sportsSeasonId ,
bracketTemplateId : event.bracketTemplateId ,
eventId : event.id ,
eventName : event.name ? ? undefined ,
matchId ,
loserAdvances : doesLoserAdvance ( match . round , match . matchNumber , event . bracketTemplateId ? ? "" ) ,
} ) ;
await autoCompleteRoundIfDone ( event . id , match . round , event . sportsSeasonId , db ) ;
}
2026-03-17 12:34:10 -07:00
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
return { success : "Winner set successfully" } ;
} catch ( error ) {
2026-03-21 13:41:39 -07:00
logger . error ( "Error setting winner:" , error ) ;
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
return {
error :
error instanceof Error ? error . message : "Failed to set winner" ,
} ;
}
}
2025-11-04 22:09:44 -08:00
if ( intent === "set-round-winners" ) {
const round = formData . get ( "round" ) ;
if ( typeof round !== "string" || ! round ) {
return { error : "Round is required" } ;
}
try {
// Get all winner assignments from form data
const winnerAssignments : Array < { matchId : string ; winnerId : string } > = [ ] ;
for ( const [ key , value ] of formData . entries ( ) ) {
if ( key . startsWith ( "winner-" ) && typeof value === "string" ) {
const matchId = key . replace ( "winner-" , "" ) ;
winnerAssignments . push ( { matchId , winnerId : value } ) ;
}
}
if ( winnerAssignments . length === 0 ) {
return { error : "No winners selected" } ;
}
// Get the event to determine the template
const event = await getScoringEventById ( params . eventId ) ;
if ( ! event ) {
return { error : "Event not found" } ;
}
const template = event . bracketTemplateId ? getBracketTemplate ( event . bracketTemplateId ) : null ;
2026-04-15 11:27:00 -07:00
// Prefer template-defined isScoring over the DB field (DB defaults to true,
// so legacy play-in rows may be incorrectly marked as scoring).
const roundIsScoring = new Map < string , boolean > (
template ? . rounds . map ( ( r ) = > [ r . name , r . isScoring ] ) ? ? [ ]
) ;
2025-11-04 22:09:44 -08:00
// Process each winner assignment
let successCount = 0 ;
const errors : string [ ] = [ ] ;
2026-03-17 12:34:10 -07:00
const processedMatchIds : string [ ] = [ ] ;
2025-11-04 22:09:44 -08:00
for ( const { matchId , winnerId } of winnerAssignments ) {
try {
const match = await findPlayoffMatchById ( matchId ) ;
if ( ! match ) {
errors . push ( ` Match ${ matchId } not found ` ) ;
continue ;
}
Partial bracket scoring, code review fixes, and double-chance logic (#156)
## Partial bracket scoring
- `processMatchResult`: new exported function that scores a single match
immediately (loser → final placement, winner → provisional floor).
Called from `set-winner` and `set-round-winners` so points are awarded
as soon as a winner is set, before the full round is complete.
- `set-winner`: passes `eventName` to `processMatchResult`.
- `set-round-winners`: batches side effects — calls `recalculateAffectedLeagues`
and `updateProbabilitiesAfterResult` once after the loop instead of per-match
(`skipSideEffects: true` per match).
## Code review fixes
- **ROUND_CONFIG** (S1): extracted a `RoundScoringConfig` lookup table +
`getRoundConfig()` helper, eliminating three parallel `if/else` chains in
`processPlayoffEvent`, `processMatchResult`, and `getGuaranteedMinimumPosition`.
AFL template overrides live in `TEMPLATE_ROUND_CONFIG` with an explanatory
comment about why the `bracketTemplateId` guard is required.
- **skipSideEffects** (C2): new param on `processMatchResult`; bracket server
uses it to batch standings/probability recalc in `set-round-winners`.
- **eventName** (W1): passed through `processMatchResult` → `recalculateAffectedLeagues`.
- **Round validation** (W2): `set-round-winners` now guards `match.round === round`
before processing each assignment.
- **Comments** (C3/S3/W3): added notes on non-scoring loser assumption,
`isScoring ?? true` default, and AFL Semi-Finals template requirement.
## PlayoffBracket eliminated-teams fix + tests
- Fixed `computeEliminatedByRound` to track participant *appearances* (not just
wins), so AFL QF losers who advance to Semi-Finals via double-chance are
correctly excluded from the QF eliminated list.
- Extracted the logic as an exported pure function for testability.
- Added 4 tests: standard elimination, AFL QF loser → SF win, AFL QF loser →
SF loss, and normal advancement not protecting a later loser.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 10:50:30 -07:00
// Guard: ensure the match actually belongs to the submitted round.
if ( match . round !== round ) {
errors . push ( ` Match ${ match . matchNumber } is in round " ${ match . round } ", not " ${ round } " ` ) ;
continue ;
}
2025-11-04 22:09:44 -08:00
// Determine loser
const loserId =
match . participant1Id === winnerId
? match . participant2Id
: match . participant1Id ;
if ( ! loserId ) {
errors . push ( ` Could not determine loser for match ${ match . matchNumber } ` ) ;
continue ;
}
// Set the winner
await setMatchWinner ( matchId , winnerId , loserId ) ;
// Try to advance the winner to the next round using template
if ( template ) {
try {
await advanceWinnerTemplate ( matchId , winnerId , template ) ;
} catch ( error ) {
// Only ignore "already filled" errors
if (
error instanceof Error &&
error . message . includes ( "already filled" )
) {
2026-03-21 13:41:39 -07:00
logger . warn ( "Match already filled, skipping advancement" ) ;
2025-11-04 22:09:44 -08:00
} else {
throw error ;
}
}
}
Partial bracket scoring, code review fixes, and double-chance logic (#156)
## Partial bracket scoring
- `processMatchResult`: new exported function that scores a single match
immediately (loser → final placement, winner → provisional floor).
Called from `set-winner` and `set-round-winners` so points are awarded
as soon as a winner is set, before the full round is complete.
- `set-winner`: passes `eventName` to `processMatchResult`.
- `set-round-winners`: batches side effects — calls `recalculateAffectedLeagues`
and `updateProbabilitiesAfterResult` once after the loop instead of per-match
(`skipSideEffects: true` per match).
## Code review fixes
- **ROUND_CONFIG** (S1): extracted a `RoundScoringConfig` lookup table +
`getRoundConfig()` helper, eliminating three parallel `if/else` chains in
`processPlayoffEvent`, `processMatchResult`, and `getGuaranteedMinimumPosition`.
AFL template overrides live in `TEMPLATE_ROUND_CONFIG` with an explanatory
comment about why the `bracketTemplateId` guard is required.
- **skipSideEffects** (C2): new param on `processMatchResult`; bracket server
uses it to batch standings/probability recalc in `set-round-winners`.
- **eventName** (W1): passed through `processMatchResult` → `recalculateAffectedLeagues`.
- **Round validation** (W2): `set-round-winners` now guards `match.round === round`
before processing each assignment.
- **Comments** (C3/S3/W3): added notes on non-scoring loser assumption,
`isScoring ?? true` default, and AFL Semi-Finals template requirement.
## PlayoffBracket eliminated-teams fix + tests
- Fixed `computeEliminatedByRound` to track participant *appearances* (not just
wins), so AFL QF losers who advance to Semi-Finals via double-chance are
correctly excluded from the QF eliminated list.
- Extracted the logic as an exported pure function for testability.
- Added 4 tests: standard elimination, AFL QF loser → SF win, AFL QF loser →
SF loss, and normal advancement not protecting a later loser.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 10:50:30 -07:00
// Score this match without triggering standings/probability recalc yet —
// we batch those side effects into a single call after the loop.
Award progressive guaranteed-minimum QP for qualifying-event brackets
Winning a round in a major bracket (CS2 Champions Stage) now immediately
awards the team the guaranteed-minimum qualifying points for the position
they've locked in — e.g. a QF win guarantees at least a T3 finish (9 QP),
an SF win guarantees 2nd (14 QP), and a Final win earns 1st (20 QP).
This also fixes a bug where CS2 majors (a qualifying_points sport) wrongly
banked actual fantasy points: the generic bracket admin routed results
through processMatchResult/processPlayoffEvent, which write the fantasy
placement table (seasonParticipantResults). For qualifying sports, fantasy
points must come only from finalizeQualifyingPoints across all majors.
- scoring-calculator.ts: add deriveBracketQualifyingStates (pure) and
processQualifyingBracketEvent, deriving each team's (placement, tieCount)
floor from playoffMatches via the existing ROUND_CONFIG and writing QP to
event_results. Export getRoundConfig for testing.
- bracket.server.ts: branch all five write handlers on isQualifyingEvent so
qualifying brackets are scored via the QP path and never write fantasy
placements. reprocess-bracket additionally clears stale
seasonParticipantResults rows, making it the cleanup tool for majors that
already banked points under the old path.
- Add qualifying-bracket-scoring.test.ts covering the floor derivation,
partial/un-set brackets, idempotency, and the worked QP example.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVLqaYGXCeSYYqTHZmbRZi
2026-06-18 20:23:37 +00:00
// Qualifying majors derive QP from the whole bracket once after the loop
// (processQualifyingBracketEvent), so skip the per-match fantasy scoring here.
if ( ! event . isQualifyingEvent ) {
await processMatchResult ( {
round : match.round ,
winnerId ,
loserId ,
isScoring : roundIsScoring.get ( match . round ) ? ? ( match . isScoring ? ? true ) ,
sportsSeasonId : event.sportsSeasonId ,
bracketTemplateId : event.bracketTemplateId ,
eventId : event.id ,
eventName : event.name ? ? undefined ,
matchId ,
skipSideEffects : true ,
loserAdvances : doesLoserAdvance ( match . round , match . matchNumber , event . bracketTemplateId ? ? "" ) ,
} ) ;
}
Partial bracket scoring, code review fixes, and double-chance logic (#156)
## Partial bracket scoring
- `processMatchResult`: new exported function that scores a single match
immediately (loser → final placement, winner → provisional floor).
Called from `set-winner` and `set-round-winners` so points are awarded
as soon as a winner is set, before the full round is complete.
- `set-winner`: passes `eventName` to `processMatchResult`.
- `set-round-winners`: batches side effects — calls `recalculateAffectedLeagues`
and `updateProbabilitiesAfterResult` once after the loop instead of per-match
(`skipSideEffects: true` per match).
## Code review fixes
- **ROUND_CONFIG** (S1): extracted a `RoundScoringConfig` lookup table +
`getRoundConfig()` helper, eliminating three parallel `if/else` chains in
`processPlayoffEvent`, `processMatchResult`, and `getGuaranteedMinimumPosition`.
AFL template overrides live in `TEMPLATE_ROUND_CONFIG` with an explanatory
comment about why the `bracketTemplateId` guard is required.
- **skipSideEffects** (C2): new param on `processMatchResult`; bracket server
uses it to batch standings/probability recalc in `set-round-winners`.
- **eventName** (W1): passed through `processMatchResult` → `recalculateAffectedLeagues`.
- **Round validation** (W2): `set-round-winners` now guards `match.round === round`
before processing each assignment.
- **Comments** (C3/S3/W3): added notes on non-scoring loser assumption,
`isScoring ?? true` default, and AFL Semi-Finals template requirement.
## PlayoffBracket eliminated-teams fix + tests
- Fixed `computeEliminatedByRound` to track participant *appearances* (not just
wins), so AFL QF losers who advance to Semi-Finals via double-chance are
correctly excluded from the QF eliminated list.
- Extracted the logic as an exported pure function for testability.
- Added 4 tests: standard elimination, AFL QF loser → SF win, AFL QF loser →
SF loss, and normal advancement not protecting a later loser.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 10:50:30 -07:00
2025-11-04 22:09:44 -08:00
successCount ++ ;
2026-03-17 12:34:10 -07:00
processedMatchIds . push ( matchId ) ;
2025-11-04 22:09:44 -08:00
} catch ( error ) {
2026-03-21 13:41:39 -07:00
logger . error ( ` Error setting winner for match ${ matchId } : ` , error ) ;
2025-11-04 22:09:44 -08:00
errors . push (
` Match ${ matchId } : ${ error instanceof Error ? error . message : "Unknown error" } `
) ;
}
}
Partial bracket scoring, code review fixes, and double-chance logic (#156)
## Partial bracket scoring
- `processMatchResult`: new exported function that scores a single match
immediately (loser → final placement, winner → provisional floor).
Called from `set-winner` and `set-round-winners` so points are awarded
as soon as a winner is set, before the full round is complete.
- `set-winner`: passes `eventName` to `processMatchResult`.
- `set-round-winners`: batches side effects — calls `recalculateAffectedLeagues`
and `updateProbabilitiesAfterResult` once after the loop instead of per-match
(`skipSideEffects: true` per match).
## Code review fixes
- **ROUND_CONFIG** (S1): extracted a `RoundScoringConfig` lookup table +
`getRoundConfig()` helper, eliminating three parallel `if/else` chains in
`processPlayoffEvent`, `processMatchResult`, and `getGuaranteedMinimumPosition`.
AFL template overrides live in `TEMPLATE_ROUND_CONFIG` with an explanatory
comment about why the `bracketTemplateId` guard is required.
- **skipSideEffects** (C2): new param on `processMatchResult`; bracket server
uses it to batch standings/probability recalc in `set-round-winners`.
- **eventName** (W1): passed through `processMatchResult` → `recalculateAffectedLeagues`.
- **Round validation** (W2): `set-round-winners` now guards `match.round === round`
before processing each assignment.
- **Comments** (C3/S3/W3): added notes on non-scoring loser assumption,
`isScoring ?? true` default, and AFL Semi-Finals template requirement.
## PlayoffBracket eliminated-teams fix + tests
- Fixed `computeEliminatedByRound` to track participant *appearances* (not just
wins), so AFL QF losers who advance to Semi-Finals via double-chance are
correctly excluded from the QF eliminated list.
- Extracted the logic as an exported pure function for testability.
- Added 4 tests: standard elimination, AFL QF loser → SF win, AFL QF loser →
SF loss, and normal advancement not protecting a later loser.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 10:50:30 -07:00
// Run side effects once for the whole batch (avoids N redundant recalcs).
2026-03-17 12:34:10 -07:00
// Pass matchIds so Discord only shows the matches from this submission, not all
// previously completed matches in the event.
Partial bracket scoring, code review fixes, and double-chance logic (#156)
## Partial bracket scoring
- `processMatchResult`: new exported function that scores a single match
immediately (loser → final placement, winner → provisional floor).
Called from `set-winner` and `set-round-winners` so points are awarded
as soon as a winner is set, before the full round is complete.
- `set-winner`: passes `eventName` to `processMatchResult`.
- `set-round-winners`: batches side effects — calls `recalculateAffectedLeagues`
and `updateProbabilitiesAfterResult` once after the loop instead of per-match
(`skipSideEffects: true` per match).
## Code review fixes
- **ROUND_CONFIG** (S1): extracted a `RoundScoringConfig` lookup table +
`getRoundConfig()` helper, eliminating three parallel `if/else` chains in
`processPlayoffEvent`, `processMatchResult`, and `getGuaranteedMinimumPosition`.
AFL template overrides live in `TEMPLATE_ROUND_CONFIG` with an explanatory
comment about why the `bracketTemplateId` guard is required.
- **skipSideEffects** (C2): new param on `processMatchResult`; bracket server
uses it to batch standings/probability recalc in `set-round-winners`.
- **eventName** (W1): passed through `processMatchResult` → `recalculateAffectedLeagues`.
- **Round validation** (W2): `set-round-winners` now guards `match.round === round`
before processing each assignment.
- **Comments** (C3/S3/W3): added notes on non-scoring loser assumption,
`isScoring ?? true` default, and AFL Semi-Finals template requirement.
## PlayoffBracket eliminated-teams fix + tests
- Fixed `computeEliminatedByRound` to track participant *appearances* (not just
wins), so AFL QF losers who advance to Semi-Finals via double-chance are
correctly excluded from the QF eliminated list.
- Extracted the logic as an exported pure function for testability.
- Added 4 tests: standard elimination, AFL QF loser → SF win, AFL QF loser →
SF loss, and normal advancement not protecting a later loser.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 10:50:30 -07:00
if ( successCount > 0 ) {
const db = database ( ) ;
Award progressive guaranteed-minimum QP for qualifying-event brackets
Winning a round in a major bracket (CS2 Champions Stage) now immediately
awards the team the guaranteed-minimum qualifying points for the position
they've locked in — e.g. a QF win guarantees at least a T3 finish (9 QP),
an SF win guarantees 2nd (14 QP), and a Final win earns 1st (20 QP).
This also fixes a bug where CS2 majors (a qualifying_points sport) wrongly
banked actual fantasy points: the generic bracket admin routed results
through processMatchResult/processPlayoffEvent, which write the fantasy
placement table (seasonParticipantResults). For qualifying sports, fantasy
points must come only from finalizeQualifyingPoints across all majors.
- scoring-calculator.ts: add deriveBracketQualifyingStates (pure) and
processQualifyingBracketEvent, deriving each team's (placement, tieCount)
floor from playoffMatches via the existing ROUND_CONFIG and writing QP to
event_results. Export getRoundConfig for testing.
- bracket.server.ts: branch all five write handlers on isQualifyingEvent so
qualifying brackets are scored via the QP path and never write fantasy
placements. reprocess-bracket additionally clears stale
seasonParticipantResults rows, making it the cleanup tool for majors that
already banked points under the old path.
- Add qualifying-bracket-scoring.test.ts covering the floor derivation,
partial/un-set brackets, idempotency, and the worked QP example.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVLqaYGXCeSYYqTHZmbRZi
2026-06-18 20:23:37 +00:00
// Qualifying majors: derive QP from the full bracket once for the batch.
if ( event . isQualifyingEvent ) {
await processQualifyingBracketEvent ( event . id , db ) ;
}
2026-05-27 04:28:10 +00:00
// Update probabilities first so recalculateAffectedLeagues reads fresh EVs
// when computing projected points.
Partial bracket scoring, code review fixes, and double-chance logic (#156)
## Partial bracket scoring
- `processMatchResult`: new exported function that scores a single match
immediately (loser → final placement, winner → provisional floor).
Called from `set-winner` and `set-round-winners` so points are awarded
as soon as a winner is set, before the full round is complete.
- `set-winner`: passes `eventName` to `processMatchResult`.
- `set-round-winners`: batches side effects — calls `recalculateAffectedLeagues`
and `updateProbabilitiesAfterResult` once after the loop instead of per-match
(`skipSideEffects: true` per match).
## Code review fixes
- **ROUND_CONFIG** (S1): extracted a `RoundScoringConfig` lookup table +
`getRoundConfig()` helper, eliminating three parallel `if/else` chains in
`processPlayoffEvent`, `processMatchResult`, and `getGuaranteedMinimumPosition`.
AFL template overrides live in `TEMPLATE_ROUND_CONFIG` with an explanatory
comment about why the `bracketTemplateId` guard is required.
- **skipSideEffects** (C2): new param on `processMatchResult`; bracket server
uses it to batch standings/probability recalc in `set-round-winners`.
- **eventName** (W1): passed through `processMatchResult` → `recalculateAffectedLeagues`.
- **Round validation** (W2): `set-round-winners` now guards `match.round === round`
before processing each assignment.
- **Comments** (C3/S3/W3): added notes on non-scoring loser assumption,
`isScoring ?? true` default, and AFL Semi-Finals template requirement.
## PlayoffBracket eliminated-teams fix + tests
- Fixed `computeEliminatedByRound` to track participant *appearances* (not just
wins), so AFL QF losers who advance to Semi-Finals via double-chance are
correctly excluded from the QF eliminated list.
- Extracted the logic as an exported pure function for testability.
- Added 4 tests: standard elimination, AFL QF loser → SF win, AFL QF loser →
SF loss, and normal advancement not protecting a later loser.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 10:50:30 -07:00
try {
await updateProbabilitiesAfterResult ( event . sportsSeasonId , true ) ;
} catch ( error ) {
2026-03-21 13:41:39 -07:00
logger . error ( ` Error updating probabilities after batch round winners: ` , error ) ;
Partial bracket scoring, code review fixes, and double-chance logic (#156)
## Partial bracket scoring
- `processMatchResult`: new exported function that scores a single match
immediately (loser → final placement, winner → provisional floor).
Called from `set-winner` and `set-round-winners` so points are awarded
as soon as a winner is set, before the full round is complete.
- `set-winner`: passes `eventName` to `processMatchResult`.
- `set-round-winners`: batches side effects — calls `recalculateAffectedLeagues`
and `updateProbabilitiesAfterResult` once after the loop instead of per-match
(`skipSideEffects: true` per match).
## Code review fixes
- **ROUND_CONFIG** (S1): extracted a `RoundScoringConfig` lookup table +
`getRoundConfig()` helper, eliminating three parallel `if/else` chains in
`processPlayoffEvent`, `processMatchResult`, and `getGuaranteedMinimumPosition`.
AFL template overrides live in `TEMPLATE_ROUND_CONFIG` with an explanatory
comment about why the `bracketTemplateId` guard is required.
- **skipSideEffects** (C2): new param on `processMatchResult`; bracket server
uses it to batch standings/probability recalc in `set-round-winners`.
- **eventName** (W1): passed through `processMatchResult` → `recalculateAffectedLeagues`.
- **Round validation** (W2): `set-round-winners` now guards `match.round === round`
before processing each assignment.
- **Comments** (C3/S3/W3): added notes on non-scoring loser assumption,
`isScoring ?? true` default, and AFL Semi-Finals template requirement.
## PlayoffBracket eliminated-teams fix + tests
- Fixed `computeEliminatedByRound` to track participant *appearances* (not just
wins), so AFL QF losers who advance to Semi-Finals via double-chance are
correctly excluded from the QF eliminated list.
- Extracted the logic as an exported pure function for testability.
- Added 4 tests: standard elimination, AFL QF loser → SF win, AFL QF loser →
SF loss, and normal advancement not protecting a later loser.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 10:50:30 -07:00
}
2026-05-27 04:28:10 +00:00
await recalculateAffectedLeagues ( event . sportsSeasonId , db , { eventId : event.id , eventName : event.name ? ? undefined , matchIds : processedMatchIds } ) ;
Award progressive guaranteed-minimum QP for qualifying-event brackets
Winning a round in a major bracket (CS2 Champions Stage) now immediately
awards the team the guaranteed-minimum qualifying points for the position
they've locked in — e.g. a QF win guarantees at least a T3 finish (9 QP),
an SF win guarantees 2nd (14 QP), and a Final win earns 1st (20 QP).
This also fixes a bug where CS2 majors (a qualifying_points sport) wrongly
banked actual fantasy points: the generic bracket admin routed results
through processMatchResult/processPlayoffEvent, which write the fantasy
placement table (seasonParticipantResults). For qualifying sports, fantasy
points must come only from finalizeQualifyingPoints across all majors.
- scoring-calculator.ts: add deriveBracketQualifyingStates (pure) and
processQualifyingBracketEvent, deriving each team's (placement, tieCount)
floor from playoffMatches via the existing ROUND_CONFIG and writing QP to
event_results. Export getRoundConfig for testing.
- bracket.server.ts: branch all five write handlers on isQualifyingEvent so
qualifying brackets are scored via the QP path and never write fantasy
placements. reprocess-bracket additionally clears stale
seasonParticipantResults rows, making it the cleanup tool for majors that
already banked points under the old path.
- Add qualifying-bracket-scoring.test.ts covering the floor derivation,
partial/un-set brackets, idempotency, and the worked QP example.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVLqaYGXCeSYYqTHZmbRZi
2026-06-18 20:23:37 +00:00
// autoCompleteRoundIfDone runs processPlayoffEvent (fantasy path); skip for
// qualifying majors, which are scored via processQualifyingBracketEvent above.
if ( ! event . isQualifyingEvent ) {
await autoCompleteRoundIfDone ( event . id , round , event . sportsSeasonId , db ) ;
}
Partial bracket scoring, code review fixes, and double-chance logic (#156)
## Partial bracket scoring
- `processMatchResult`: new exported function that scores a single match
immediately (loser → final placement, winner → provisional floor).
Called from `set-winner` and `set-round-winners` so points are awarded
as soon as a winner is set, before the full round is complete.
- `set-winner`: passes `eventName` to `processMatchResult`.
- `set-round-winners`: batches side effects — calls `recalculateAffectedLeagues`
and `updateProbabilitiesAfterResult` once after the loop instead of per-match
(`skipSideEffects: true` per match).
## Code review fixes
- **ROUND_CONFIG** (S1): extracted a `RoundScoringConfig` lookup table +
`getRoundConfig()` helper, eliminating three parallel `if/else` chains in
`processPlayoffEvent`, `processMatchResult`, and `getGuaranteedMinimumPosition`.
AFL template overrides live in `TEMPLATE_ROUND_CONFIG` with an explanatory
comment about why the `bracketTemplateId` guard is required.
- **skipSideEffects** (C2): new param on `processMatchResult`; bracket server
uses it to batch standings/probability recalc in `set-round-winners`.
- **eventName** (W1): passed through `processMatchResult` → `recalculateAffectedLeagues`.
- **Round validation** (W2): `set-round-winners` now guards `match.round === round`
before processing each assignment.
- **Comments** (C3/S3/W3): added notes on non-scoring loser assumption,
`isScoring ?? true` default, and AFL Semi-Finals template requirement.
## PlayoffBracket eliminated-teams fix + tests
- Fixed `computeEliminatedByRound` to track participant *appearances* (not just
wins), so AFL QF losers who advance to Semi-Finals via double-chance are
correctly excluded from the QF eliminated list.
- Extracted the logic as an exported pure function for testability.
- Added 4 tests: standard elimination, AFL QF loser → SF win, AFL QF loser →
SF loss, and normal advancement not protecting a later loser.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 10:50:30 -07:00
}
2025-11-04 22:09:44 -08:00
if ( errors . length > 0 && successCount === 0 ) {
return { error : ` Failed to set winners: ${ errors . join ( ", " ) } ` } ;
}
if ( errors . length > 0 ) {
return {
success : ` Set ${ successCount } winner(s) successfully ` ,
error : ` Some errors occurred: ${ errors . join ( ", " ) } ` ,
} ;
}
return { success : ` Successfully set ${ successCount } winner(s) for ${ round } ` } ;
} catch ( error ) {
2026-03-21 13:41:39 -07:00
logger . error ( "Error setting round winners:" , error ) ;
2025-11-04 22:09:44 -08:00
return {
error :
error instanceof Error ? error . message : "Failed to set winners" ,
} ;
}
}
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
if ( intent === "complete-round" ) {
const round = formData . get ( "round" ) ;
2025-11-04 22:09:44 -08:00
if ( typeof round !== "string" || ! round ) {
return { error : "Round is required" } ;
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
}
try {
// Get the event and update playoffRound to the completed round
const event = await getScoringEventById ( params . eventId ) ;
if ( ! event ) {
return { error : "Event not found" } ;
}
// Verify all matches in this round are complete
const matches = await findPlayoffMatchesByEventId ( params . eventId ) ;
2025-11-04 22:09:44 -08:00
// Validate that the round exists in this event's matches
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
const roundMatches = matches . filter ( ( m ) = > m . round === round ) ;
2025-11-04 22:09:44 -08:00
if ( roundMatches . length === 0 ) {
return { error : ` Round " ${ round } " not found in this bracket ` } ;
}
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
const allComplete = roundMatches . every ( ( m ) = > m . isComplete ) ;
if ( ! allComplete ) {
return { error : ` Not all matches in ${ round } are complete ` } ;
}
Award progressive guaranteed-minimum QP for qualifying-event brackets
Winning a round in a major bracket (CS2 Champions Stage) now immediately
awards the team the guaranteed-minimum qualifying points for the position
they've locked in — e.g. a QF win guarantees at least a T3 finish (9 QP),
an SF win guarantees 2nd (14 QP), and a Final win earns 1st (20 QP).
This also fixes a bug where CS2 majors (a qualifying_points sport) wrongly
banked actual fantasy points: the generic bracket admin routed results
through processMatchResult/processPlayoffEvent, which write the fantasy
placement table (seasonParticipantResults). For qualifying sports, fantasy
points must come only from finalizeQualifyingPoints across all majors.
- scoring-calculator.ts: add deriveBracketQualifyingStates (pure) and
processQualifyingBracketEvent, deriving each team's (placement, tieCount)
floor from playoffMatches via the existing ROUND_CONFIG and writing QP to
event_results. Export getRoundConfig for testing.
- bracket.server.ts: branch all five write handlers on isQualifyingEvent so
qualifying brackets are scored via the QP path and never write fantasy
placements. reprocess-bracket additionally clears stale
seasonParticipantResults rows, making it the cleanup tool for majors that
already banked points under the old path.
- Add qualifying-bracket-scoring.test.ts covering the floor derivation,
partial/un-set brackets, idempotency, and the worked QP example.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVLqaYGXCeSYYqTHZmbRZi
2026-06-18 20:23:37 +00:00
// Qualifying majors (e.g. CS2): QP is derived directly from the bracket via
// processQualifyingBracketEvent — there are no seasonParticipantResults rows to
// validate against, and final fantasy placements come from finalizeQualifyingPoints.
if ( event . isQualifyingEvent ) {
2026-06-18 23:46:59 +00:00
await scoreQualifyingBracket ( event , database ( ) ) ;
Award progressive guaranteed-minimum QP for qualifying-event brackets
Winning a round in a major bracket (CS2 Champions Stage) now immediately
awards the team the guaranteed-minimum qualifying points for the position
they've locked in — e.g. a QF win guarantees at least a T3 finish (9 QP),
an SF win guarantees 2nd (14 QP), and a Final win earns 1st (20 QP).
This also fixes a bug where CS2 majors (a qualifying_points sport) wrongly
banked actual fantasy points: the generic bracket admin routed results
through processMatchResult/processPlayoffEvent, which write the fantasy
placement table (seasonParticipantResults). For qualifying sports, fantasy
points must come only from finalizeQualifyingPoints across all majors.
- scoring-calculator.ts: add deriveBracketQualifyingStates (pure) and
processQualifyingBracketEvent, deriving each team's (placement, tieCount)
floor from playoffMatches via the existing ROUND_CONFIG and writing QP to
event_results. Export getRoundConfig for testing.
- bracket.server.ts: branch all five write handlers on isQualifyingEvent so
qualifying brackets are scored via the QP path and never write fantasy
placements. reprocess-bracket additionally clears stale
seasonParticipantResults rows, making it the cleanup tool for majors that
already banked points under the old path.
- Add qualifying-bracket-scoring.test.ts covering the floor derivation,
partial/un-set brackets, idempotency, and the worked QP example.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVLqaYGXCeSYYqTHZmbRZi
2026-06-18 20:23:37 +00:00
return { success : ` ${ round } completed and qualifying points updated ` } ;
}
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
// Validate round order: ensure previous rounds are complete
const existingResults = await findParticipantResultsBySportsSeasonId (
params . id
) ;
if ( round === "Finals" ) {
// Finals requires Semifinals to be complete (3rd/4th place results exist)
const hasSemifinalResults = existingResults . some (
( r ) = > r . finalPosition === 3 || r . finalPosition === 4
) ;
if ( ! hasSemifinalResults ) {
return {
error :
"Semifinals must be completed before Finals (no 3rd/4th place results found)" ,
} ;
}
} else if ( round === "Semifinals" ) {
// Semifinals requires Quarterfinals to be complete if QF matches exist
const hasQuarterfinals = matches . some ( ( m ) = > m . round === "Quarterfinals" ) ;
if ( hasQuarterfinals ) {
const hasQuarterfinalsResults = existingResults . some (
( r ) = >
r . finalPosition === 5 ||
r . finalPosition === 6 ||
r . finalPosition === 7 ||
r . finalPosition === 8
) ;
if ( ! hasQuarterfinalsResults ) {
return {
error :
"Quarterfinals must be completed before Semifinals (no 5th-8th place results found)" ,
} ;
}
}
}
// Get the sports season to find a fantasy season for scoring rules
const sportsSeason = await findSportsSeasonById ( params . id ) ;
if ( ! sportsSeason ) {
return { error : "Sports season not found" } ;
}
// Process the playoff event to calculate placements
2025-11-03 13:16:37 -08:00
// TODO Phase 2.7: Refactor processPlayoffEvent to use template system
// For now, we still need to set playoffRound for the old scoring logic
// even though we removed it from the create event UI
const db = database ( ) ;
await db
. update ( schema . scoringEvents )
. set ( { playoffRound : round , updatedAt : new Date ( ) } )
. where ( eq ( schema . scoringEvents . id , params . eventId ) ) ;
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
// Get a fantasy season ID for scoring calculation
const seasonSports = await findSeasonSportsBySportsSeasonId ( params . id ) ;
if ( seasonSports . length === 0 ) {
return {
error : "No fantasy seasons found for this sports season" ,
} ;
}
2026-04-05 15:11:43 -07:00
// Process playoff event to update participant results.
// skipRecalculate=true: match winners were already set via set-winner/set-round-winners,
// which triggered recalculateAffectedLeagues and Discord per match. No need to re-fire.
await processPlayoffEvent ( params . eventId , undefined , { skipRecalculate : true } ) ;
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
return {
success : ` ${ round } completed and placements calculated successfully ` ,
} ;
} catch ( error ) {
2026-03-21 13:41:39 -07:00
logger . error ( "Error completing round:" , error ) ;
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
return {
error :
error instanceof Error ? error . message : "Failed to complete round" ,
} ;
}
}
2026-04-15 14:56:33 -07:00
if ( intent === "reprocess-bracket" ) {
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
try {
const event = await getScoringEventById ( params . eventId ) ;
if ( ! event ) return { error : "Event not found" } ;
const matches = await findPlayoffMatchesByEventId ( params . eventId ) ;
const completed = matches . filter ( ( m ) = > m . isComplete && m . winnerId && m . loserId ) ;
Award progressive guaranteed-minimum QP for qualifying-event brackets
Winning a round in a major bracket (CS2 Champions Stage) now immediately
awards the team the guaranteed-minimum qualifying points for the position
they've locked in — e.g. a QF win guarantees at least a T3 finish (9 QP),
an SF win guarantees 2nd (14 QP), and a Final win earns 1st (20 QP).
This also fixes a bug where CS2 majors (a qualifying_points sport) wrongly
banked actual fantasy points: the generic bracket admin routed results
through processMatchResult/processPlayoffEvent, which write the fantasy
placement table (seasonParticipantResults). For qualifying sports, fantasy
points must come only from finalizeQualifyingPoints across all majors.
- scoring-calculator.ts: add deriveBracketQualifyingStates (pure) and
processQualifyingBracketEvent, deriving each team's (placement, tieCount)
floor from playoffMatches via the existing ROUND_CONFIG and writing QP to
event_results. Export getRoundConfig for testing.
- bracket.server.ts: branch all five write handlers on isQualifyingEvent so
qualifying brackets are scored via the QP path and never write fantasy
placements. reprocess-bracket additionally clears stale
seasonParticipantResults rows, making it the cleanup tool for majors that
already banked points under the old path.
- Add qualifying-bracket-scoring.test.ts covering the floor derivation,
partial/un-set brackets, idempotency, and the worked QP example.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVLqaYGXCeSYYqTHZmbRZi
2026-06-18 20:23:37 +00:00
// Qualifying majors: this is also the cleanup tool for majors that wrongly banked
// fantasy points under the old path. Delete the stale seasonParticipantResults rows
// (erasing those points), then rebuild correct QP from the bracket. Qualifying sports
// have no legitimate per-major fantasy placements — those come from
// finalizeQualifyingPoints across all majors.
if ( event . isQualifyingEvent ) {
const db = database ( ) ;
2026-06-18 23:46:59 +00:00
await deleteParticipantResultsBySportsSeasonId ( event . sportsSeasonId , db ) ;
Award progressive guaranteed-minimum QP for qualifying-event brackets
Winning a round in a major bracket (CS2 Champions Stage) now immediately
awards the team the guaranteed-minimum qualifying points for the position
they've locked in — e.g. a QF win guarantees at least a T3 finish (9 QP),
an SF win guarantees 2nd (14 QP), and a Final win earns 1st (20 QP).
This also fixes a bug where CS2 majors (a qualifying_points sport) wrongly
banked actual fantasy points: the generic bracket admin routed results
through processMatchResult/processPlayoffEvent, which write the fantasy
placement table (seasonParticipantResults). For qualifying sports, fantasy
points must come only from finalizeQualifyingPoints across all majors.
- scoring-calculator.ts: add deriveBracketQualifyingStates (pure) and
processQualifyingBracketEvent, deriving each team's (placement, tieCount)
floor from playoffMatches via the existing ROUND_CONFIG and writing QP to
event_results. Export getRoundConfig for testing.
- bracket.server.ts: branch all five write handlers on isQualifyingEvent so
qualifying brackets are scored via the QP path and never write fantasy
placements. reprocess-bracket additionally clears stale
seasonParticipantResults rows, making it the cleanup tool for majors that
already banked points under the old path.
- Add qualifying-bracket-scoring.test.ts covering the floor derivation,
partial/un-set brackets, idempotency, and the worked QP example.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVLqaYGXCeSYYqTHZmbRZi
2026-06-18 20:23:37 +00:00
await processQualifyingBracketEvent ( params . eventId , db ) ;
2026-06-18 23:46:59 +00:00
// If the season's QP was already finalized, the delete above wiped the final
// placements — recompute them from QP totals so standings aren't left blank.
const sportsSeason = await findSportsSeasonById ( params . id ) ;
if ( sportsSeason ? . qualifyingPointsFinalized ) {
await finalizeQualifyingPoints ( event . sportsSeasonId , db ) ; // recalcs leagues itself
} else {
// skipDiscord: reprocess is a data-correction tool, not a result announcement.
await recalculateAffectedLeagues ( event . sportsSeasonId , db , { skipDiscord : true } ) ;
}
Award progressive guaranteed-minimum QP for qualifying-event brackets
Winning a round in a major bracket (CS2 Champions Stage) now immediately
awards the team the guaranteed-minimum qualifying points for the position
they've locked in — e.g. a QF win guarantees at least a T3 finish (9 QP),
an SF win guarantees 2nd (14 QP), and a Final win earns 1st (20 QP).
This also fixes a bug where CS2 majors (a qualifying_points sport) wrongly
banked actual fantasy points: the generic bracket admin routed results
through processMatchResult/processPlayoffEvent, which write the fantasy
placement table (seasonParticipantResults). For qualifying sports, fantasy
points must come only from finalizeQualifyingPoints across all majors.
- scoring-calculator.ts: add deriveBracketQualifyingStates (pure) and
processQualifyingBracketEvent, deriving each team's (placement, tieCount)
floor from playoffMatches via the existing ROUND_CONFIG and writing QP to
event_results. Export getRoundConfig for testing.
- bracket.server.ts: branch all five write handlers on isQualifyingEvent so
qualifying brackets are scored via the QP path and never write fantasy
placements. reprocess-bracket additionally clears stale
seasonParticipantResults rows, making it the cleanup tool for majors that
already banked points under the old path.
- Add qualifying-bracket-scoring.test.ts covering the floor derivation,
partial/un-set brackets, idempotency, and the worked QP example.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVLqaYGXCeSYYqTHZmbRZi
2026-06-18 20:23:37 +00:00
return {
success : ` Reprocessed qualifying bracket: cleared stale fantasy points and recomputed QP ( ${ completed . length } completed match(es)). ` ,
} ;
}
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
if ( completed . length === 0 ) {
return { error : "No completed matches to reprocess" } ;
}
// Delete ALL results for this sports season and rebuild from scratch.
// Only deleting partial rows leaves stale finalized rows that block
// the "never un-finalize" guard in upsertParticipantResult.
const db = database ( ) ;
2026-06-18 23:46:59 +00:00
await deleteParticipantResultsBySportsSeasonId ( event . sportsSeasonId , db ) ;
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
// Replay each completed match in bracket order (earlier rounds first).
const template = event . bracketTemplateId ? getBracketTemplate ( event . bracketTemplateId ) : null ;
const roundOrder = template ? template . rounds . map ( ( r ) = > r . name ) : [ ] ;
2026-04-15 11:27:00 -07:00
// Build a round→isScoring map from the template so we use the template as the
// source of truth rather than the DB column (which defaults to true and may be
// wrong for brackets created before the isScoring column was added).
const templateRoundIsScoring = new Map < string , boolean > (
template ? . rounds . map ( ( r ) = > [ r . name , r . isScoring ] ) ? ? [ ]
) ;
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
const sortedMatches = completed . toSorted ( ( a , b ) = > {
const ai = roundOrder . indexOf ( a . round ) ;
const bi = roundOrder . indexOf ( b . round ) ;
return ( ai === - 1 ? 999 : ai ) - ( bi === - 1 ? 999 : bi ) ;
} ) ;
for ( const match of sortedMatches ) {
New design (#309)
* Redesign home page with new layout and component system
- Two-column layout (My Leagues 2/3, Upcoming Events 1/3) with mobile stack
- LeagueRow: square avatar, gradient draft highlight, rank/points display, progress bar
- MyLeaguesCard, CreateLeagueCard with shared SectionCardHeader
- UpcomingEventsCard: vertical timeline with grouped multi-league events
- Shared gradient system: BracktGradients SVG defs, GradientIcon wrapper, brand.ts constants
- Button default variant updated to green→cyan gradient
- Navbar: plain nav links with gradient hover, support/admin icon buttons
- Accessibility fixes: semantic h2 headings, aria-label on LeagueAvatar and nav elements
- Storybook stories for all new components
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Responsive league row layout and mobile polish
- League rows stack avatar+name on top, stats full-width below on mobile
- Stats spread to right side on sm+ screens with border separator on mobile
- Tighter padding on mobile (px-3/py-3), full padding on sm+
- Card headers and content use px-3 sm:px-6 to reduce mobile gutters
- Two-column home layout deferred to lg breakpoint (tablet gets stacked)
- Active leagues sorted by completion percentage descending
- Default rank 1 / 0 points for active leagues with no scoring events yet
- Fix ordinal bug for 11th/12th/13th; add aria-labels to rank change indicators
- Remove dead StatDivider className prop
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Improve claude file.
* Add StandingsPreview card component with podium row styling
- New StandingsPreview component with gold/silver/bronze row tints for
top 3, team avatar, and LeagueRow-style stat columns (Ranking + Points)
with rank and 7-day point change indicators
- Fix GradientIcon in Storybook by adding BracktGradients decorator to
preview.tsx (renamed from .ts to support JSX)
- Fix degenerate SVG gradient on horizontal strokes by switching
BracktGradients to gradientUnits="userSpaceOnUse" with Lucide-space
coordinates (0→24)
- Revert erroneous fill: url(#gradient) from GradientIcon; stroke-only
fix was sufficient once gradientUnits was corrected
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Update components on league homepage.
* Finish up league page styling.
* Work on standings page.
* Add story for RecentScoresCard
* Update Point Progression Chart.
* Sort point progression legend by ranking and add team links to standings rows
* Fix standings discrepancy on change.
* Create draft cell component.
* Update draft board page
* Draft room improvements.
* Update some draft room styling.
* Fix context menu missing.
* Move tab navigation and autodraft to header row, narrow sidebar
* Virtualize available participants list, memoize draft room props
Adds @tanstack/react-virtual to replace separate mobile/desktop lists
with a single unified virtual scroll loop. Also memoizes miniDraftGrid
and availableParticipantsSectionProps, and switches pick lookup from
Array.find to a Map for O(1) access.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Update draft room UI.
* More draft room fixes.
* Draft room tweaks.
* Fix Rosters page.
* Queue Section fixes.
* Mobile Draft fixes.
* Fix draft board page.
* Create bracket look.
* Bracket work.
* Finish bracket page.
* Homepage initial styling
* homepage copy
* Add privacy policy. Fixes #88.
* how to play copy
* rules copy
* Fix brackets on homepage.
* Add footer to website.
* Glow on dots.
* Landing page copy.
* Fix sidebar.
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 13:14:55 -07:00
if ( ! match . winnerId || ! match . loserId ) continue ;
2026-04-15 11:27:00 -07:00
const isScoring = templateRoundIsScoring . get ( match . round ) ? ? ( match . isScoring ? ? true ) ;
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
await processMatchResult (
{
round : match.round ,
New design (#309)
* Redesign home page with new layout and component system
- Two-column layout (My Leagues 2/3, Upcoming Events 1/3) with mobile stack
- LeagueRow: square avatar, gradient draft highlight, rank/points display, progress bar
- MyLeaguesCard, CreateLeagueCard with shared SectionCardHeader
- UpcomingEventsCard: vertical timeline with grouped multi-league events
- Shared gradient system: BracktGradients SVG defs, GradientIcon wrapper, brand.ts constants
- Button default variant updated to green→cyan gradient
- Navbar: plain nav links with gradient hover, support/admin icon buttons
- Accessibility fixes: semantic h2 headings, aria-label on LeagueAvatar and nav elements
- Storybook stories for all new components
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Responsive league row layout and mobile polish
- League rows stack avatar+name on top, stats full-width below on mobile
- Stats spread to right side on sm+ screens with border separator on mobile
- Tighter padding on mobile (px-3/py-3), full padding on sm+
- Card headers and content use px-3 sm:px-6 to reduce mobile gutters
- Two-column home layout deferred to lg breakpoint (tablet gets stacked)
- Active leagues sorted by completion percentage descending
- Default rank 1 / 0 points for active leagues with no scoring events yet
- Fix ordinal bug for 11th/12th/13th; add aria-labels to rank change indicators
- Remove dead StatDivider className prop
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Improve claude file.
* Add StandingsPreview card component with podium row styling
- New StandingsPreview component with gold/silver/bronze row tints for
top 3, team avatar, and LeagueRow-style stat columns (Ranking + Points)
with rank and 7-day point change indicators
- Fix GradientIcon in Storybook by adding BracktGradients decorator to
preview.tsx (renamed from .ts to support JSX)
- Fix degenerate SVG gradient on horizontal strokes by switching
BracktGradients to gradientUnits="userSpaceOnUse" with Lucide-space
coordinates (0→24)
- Revert erroneous fill: url(#gradient) from GradientIcon; stroke-only
fix was sufficient once gradientUnits was corrected
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Update components on league homepage.
* Finish up league page styling.
* Work on standings page.
* Add story for RecentScoresCard
* Update Point Progression Chart.
* Sort point progression legend by ranking and add team links to standings rows
* Fix standings discrepancy on change.
* Create draft cell component.
* Update draft board page
* Draft room improvements.
* Update some draft room styling.
* Fix context menu missing.
* Move tab navigation and autodraft to header row, narrow sidebar
* Virtualize available participants list, memoize draft room props
Adds @tanstack/react-virtual to replace separate mobile/desktop lists
with a single unified virtual scroll loop. Also memoizes miniDraftGrid
and availableParticipantsSectionProps, and switches pick lookup from
Array.find to a Map for O(1) access.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Update draft room UI.
* More draft room fixes.
* Draft room tweaks.
* Fix Rosters page.
* Queue Section fixes.
* Mobile Draft fixes.
* Fix draft board page.
* Create bracket look.
* Bracket work.
* Finish bracket page.
* Homepage initial styling
* homepage copy
* Add privacy policy. Fixes #88.
* how to play copy
* rules copy
* Fix brackets on homepage.
* Add footer to website.
* Glow on dots.
* Landing page copy.
* Fix sidebar.
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 13:14:55 -07:00
winnerId : match.winnerId ,
loserId : match.loserId ,
2026-04-15 11:27:00 -07:00
isScoring ,
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
sportsSeasonId : event.sportsSeasonId ,
bracketTemplateId : event.bracketTemplateId ,
New design (#309)
* Redesign home page with new layout and component system
- Two-column layout (My Leagues 2/3, Upcoming Events 1/3) with mobile stack
- LeagueRow: square avatar, gradient draft highlight, rank/points display, progress bar
- MyLeaguesCard, CreateLeagueCard with shared SectionCardHeader
- UpcomingEventsCard: vertical timeline with grouped multi-league events
- Shared gradient system: BracktGradients SVG defs, GradientIcon wrapper, brand.ts constants
- Button default variant updated to green→cyan gradient
- Navbar: plain nav links with gradient hover, support/admin icon buttons
- Accessibility fixes: semantic h2 headings, aria-label on LeagueAvatar and nav elements
- Storybook stories for all new components
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Responsive league row layout and mobile polish
- League rows stack avatar+name on top, stats full-width below on mobile
- Stats spread to right side on sm+ screens with border separator on mobile
- Tighter padding on mobile (px-3/py-3), full padding on sm+
- Card headers and content use px-3 sm:px-6 to reduce mobile gutters
- Two-column home layout deferred to lg breakpoint (tablet gets stacked)
- Active leagues sorted by completion percentage descending
- Default rank 1 / 0 points for active leagues with no scoring events yet
- Fix ordinal bug for 11th/12th/13th; add aria-labels to rank change indicators
- Remove dead StatDivider className prop
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Improve claude file.
* Add StandingsPreview card component with podium row styling
- New StandingsPreview component with gold/silver/bronze row tints for
top 3, team avatar, and LeagueRow-style stat columns (Ranking + Points)
with rank and 7-day point change indicators
- Fix GradientIcon in Storybook by adding BracktGradients decorator to
preview.tsx (renamed from .ts to support JSX)
- Fix degenerate SVG gradient on horizontal strokes by switching
BracktGradients to gradientUnits="userSpaceOnUse" with Lucide-space
coordinates (0→24)
- Revert erroneous fill: url(#gradient) from GradientIcon; stroke-only
fix was sufficient once gradientUnits was corrected
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Update components on league homepage.
* Finish up league page styling.
* Work on standings page.
* Add story for RecentScoresCard
* Update Point Progression Chart.
* Sort point progression legend by ranking and add team links to standings rows
* Fix standings discrepancy on change.
* Create draft cell component.
* Update draft board page
* Draft room improvements.
* Update some draft room styling.
* Fix context menu missing.
* Move tab navigation and autodraft to header row, narrow sidebar
* Virtualize available participants list, memoize draft room props
Adds @tanstack/react-virtual to replace separate mobile/desktop lists
with a single unified virtual scroll loop. Also memoizes miniDraftGrid
and availableParticipantsSectionProps, and switches pick lookup from
Array.find to a Map for O(1) access.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Update draft room UI.
* More draft room fixes.
* Draft room tweaks.
* Fix Rosters page.
* Queue Section fixes.
* Mobile Draft fixes.
* Fix draft board page.
* Create bracket look.
* Bracket work.
* Finish bracket page.
* Homepage initial styling
* homepage copy
* Add privacy policy. Fixes #88.
* how to play copy
* rules copy
* Fix brackets on homepage.
* Add footer to website.
* Glow on dots.
* Landing page copy.
* Fix sidebar.
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 13:14:55 -07:00
eventId : event.id ,
eventName : event.name ? ? undefined ,
matchId : match.id ,
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
skipSideEffects : true ,
2026-04-15 09:40:53 -07:00
loserAdvances : doesLoserAdvance ( match . round , match . matchNumber , event . bracketTemplateId ? ? "" ) ,
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
}
) ;
}
2026-04-15 14:56:33 -07:00
// Mark participants NOT in any bracket match as eliminated (finalPosition = 0).
// This covers teams that didn't make the playoffs/play-in tournament.
2025-11-21 22:05:50 -08:00
const allParticipants = await findParticipantsBySportsSeasonId ( params . id ) ;
2026-04-15 14:56:33 -07:00
const bracketParticipantIds = new Set < string > ( ) ;
2025-11-21 22:05:50 -08:00
for ( const match of matches ) {
2026-04-15 14:56:33 -07:00
if ( match . participant1Id ) bracketParticipantIds . add ( match . participant1Id ) ;
if ( match . participant2Id ) bracketParticipantIds . add ( match . participant2Id ) ;
2025-11-21 22:05:50 -08:00
}
let eliminatedCount = 0 ;
for ( const participant of allParticipants ) {
2026-04-15 14:56:33 -07:00
if ( ! bracketParticipantIds . has ( participant . id ) ) {
2025-11-21 22:05:50 -08:00
await setParticipantResult (
participant . id ,
event . sportsSeasonId ,
2026-04-15 14:56:33 -07:00
0
2025-11-21 22:05:50 -08:00
) ;
eliminatedCount ++ ;
}
}
2026-04-15 14:56:33 -07:00
// skipDiscord: reprocess-bracket is a data-correction tool, not a result announcement.
await recalculateAffectedLeagues ( event . sportsSeasonId , undefined , { skipDiscord : true } ) ;
2025-11-21 22:05:50 -08:00
2026-04-15 14:56:33 -07:00
return { success : ` Reprocessed bracket: ${ sortedMatches . length } match(es) replayed, ${ eliminatedCount } non-bracket participant(s) eliminated ` } ;
2025-11-21 22:05:50 -08:00
} catch ( error ) {
2026-04-15 14:56:33 -07:00
logger . error ( "Error reprocessing bracket:" , error ) ;
2025-11-21 22:05:50 -08:00
return {
2026-04-15 14:56:33 -07:00
error : error instanceof Error ? error . message : "Failed to reprocess bracket" ,
2025-11-21 22:05:50 -08:00
} ;
}
}
2025-11-14 20:01:21 -08:00
if ( intent === "finalize-bracket" ) {
try {
// Get the event
const event = await getScoringEventById ( params . eventId ) ;
if ( ! event ) {
return { error : "Event not found" } ;
}
// Get all matches
const matches = await findPlayoffMatchesByEventId ( params . eventId ) ;
if ( matches . length === 0 ) {
return { error : "No bracket exists for this event" } ;
}
2026-04-05 15:11:43 -07:00
// Verify the event has a valid bracket template configured
2025-11-14 20:01:21 -08:00
const template = event . bracketTemplateId ? getBracketTemplate ( event . bracketTemplateId ) : null ;
if ( ! template ) {
return { error : "Bracket template not found" } ;
}
// Verify ALL matches are complete
const incompleteMatches = matches . filter ( ( m ) = > ! m . isComplete ) ;
if ( incompleteMatches . length > 0 ) {
return {
error : ` Cannot finalize: ${ incompleteMatches . length } match(es) still incomplete ` ,
} ;
}
Award progressive guaranteed-minimum QP for qualifying-event brackets
Winning a round in a major bracket (CS2 Champions Stage) now immediately
awards the team the guaranteed-minimum qualifying points for the position
they've locked in — e.g. a QF win guarantees at least a T3 finish (9 QP),
an SF win guarantees 2nd (14 QP), and a Final win earns 1st (20 QP).
This also fixes a bug where CS2 majors (a qualifying_points sport) wrongly
banked actual fantasy points: the generic bracket admin routed results
through processMatchResult/processPlayoffEvent, which write the fantasy
placement table (seasonParticipantResults). For qualifying sports, fantasy
points must come only from finalizeQualifyingPoints across all majors.
- scoring-calculator.ts: add deriveBracketQualifyingStates (pure) and
processQualifyingBracketEvent, deriving each team's (placement, tieCount)
floor from playoffMatches via the existing ROUND_CONFIG and writing QP to
event_results. Export getRoundConfig for testing.
- bracket.server.ts: branch all five write handlers on isQualifyingEvent so
qualifying brackets are scored via the QP path and never write fantasy
placements. reprocess-bracket additionally clears stale
seasonParticipantResults rows, making it the cleanup tool for majors that
already banked points under the old path.
- Add qualifying-bracket-scoring.test.ts covering the floor derivation,
partial/un-set brackets, idempotency, and the worked QP example.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVLqaYGXCeSYYqTHZmbRZi
2026-06-18 20:23:37 +00:00
// Qualifying majors (e.g. CS2): lock in final bracket placements/QP, then run the
// standard qualifying finalizer for THIS event. Do not mark the whole sports season
// completed or recalc fantasy standings — a season spans multiple majors and final
// fantasy placements come from finalizeQualifyingPoints across all of them.
if ( event . isQualifyingEvent ) {
const db = database ( ) ;
2026-06-18 23:46:59 +00:00
// processQualifyingEvent derives the bracket QP (via processQualifyingBracketEvent),
// increments majorsCompleted, and recalcs participant QP totals. Season-wide
// fantasy finalization stays with finalizeQualifyingPoints across all majors.
Award progressive guaranteed-minimum QP for qualifying-event brackets
Winning a round in a major bracket (CS2 Champions Stage) now immediately
awards the team the guaranteed-minimum qualifying points for the position
they've locked in — e.g. a QF win guarantees at least a T3 finish (9 QP),
an SF win guarantees 2nd (14 QP), and a Final win earns 1st (20 QP).
This also fixes a bug where CS2 majors (a qualifying_points sport) wrongly
banked actual fantasy points: the generic bracket admin routed results
through processMatchResult/processPlayoffEvent, which write the fantasy
placement table (seasonParticipantResults). For qualifying sports, fantasy
points must come only from finalizeQualifyingPoints across all majors.
- scoring-calculator.ts: add deriveBracketQualifyingStates (pure) and
processQualifyingBracketEvent, deriving each team's (placement, tieCount)
floor from playoffMatches via the existing ROUND_CONFIG and writing QP to
event_results. Export getRoundConfig for testing.
- bracket.server.ts: branch all five write handlers on isQualifyingEvent so
qualifying brackets are scored via the QP path and never write fantasy
placements. reprocess-bracket additionally clears stale
seasonParticipantResults rows, making it the cleanup tool for majors that
already banked points under the old path.
- Add qualifying-bracket-scoring.test.ts covering the floor derivation,
partial/un-set brackets, idempotency, and the worked QP example.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVLqaYGXCeSYYqTHZmbRZi
2026-06-18 20:23:37 +00:00
await processQualifyingEvent ( params . eventId , db ) ;
await db
. update ( schema . scoringEvents )
. set ( { isComplete : true , completedAt : new Date ( ) , updatedAt : new Date ( ) } )
. where ( eq ( schema . scoringEvents . id , params . eventId ) ) ;
await recalculateAffectedLeagues ( event . sportsSeasonId , db , {
eventId : params.eventId ,
eventName : event.name ? ? undefined ,
} ) ;
return { success : "Major bracket finalized — qualifying points awarded." } ;
}
2025-11-14 20:01:21 -08:00
// Get all participants in this sports season
const allParticipants = await findParticipantsBySportsSeasonId ( params . id ) ;
// Get participants in matches
const participantsInMatches = new Set (
matches . flatMap ( ( m ) = > [ m . participant1Id , m . participant2Id ] . filter ( Boolean ) )
) ;
const db = database ( ) ;
// Assign 0 points to participants not in the bracket (Q20)
for ( const participant of allParticipants ) {
if ( ! participantsInMatches . has ( participant . id ) ) {
await setParticipantResult (
participant . id ,
params . id ,
0 // 0 placement = 0 points
) ;
}
}
// Mark event as complete
await db
. update ( schema . scoringEvents )
. set ( { isComplete : true , completedAt : new Date ( ) , updatedAt : new Date ( ) } )
. where ( eq ( schema . scoringEvents . id , params . eventId ) ) ;
2026-05-04 20:31:44 -07:00
await db
. update ( schema . sportsSeasons )
. set ( { status : "completed" , updatedAt : new Date ( ) } )
. where ( eq ( schema . sportsSeasons . id , params . id ) ) ;
2025-11-14 20:01:21 -08:00
// Recalculate standings for all affected fantasy seasons
const seasonSports = await findSeasonSportsBySportsSeasonId ( params . id ) ;
for ( const seasonSport of seasonSports ) {
await recalculateStandings ( seasonSport . seasonId , db ) ;
2026-03-18 22:15:28 -07:00
await createDailySnapshot ( seasonSport . seasonId , db ) ;
2025-11-14 20:01:21 -08:00
}
2026-05-04 20:31:44 -07:00
await maybeResolveCompletedBracktForSportsSeason ( params . id , db ) ;
2025-11-14 20:01:21 -08:00
return {
success : ` Bracket finalized! All placements calculated and standings updated. ` ,
} ;
} catch ( error ) {
2026-03-21 13:41:39 -07:00
logger . error ( "Error finalizing bracket:" , error ) ;
2025-11-14 20:01:21 -08:00
return {
error :
error instanceof Error ? error . message : "Failed to finalize bracket" ,
} ;
}
}
2026-02-14 22:30:12 -08:00
if ( intent === "generate-groups" ) {
const templateId = formData . get ( "templateId" ) ;
if ( typeof templateId !== "string" || ! templateId ) {
return { error : "Template ID is required" } ;
}
const template = getBracketTemplate ( templateId ) ;
if ( ! template || ! template . groupStage ) {
return { error : "Invalid template or template has no group stage" } ;
}
const { groupStage } = template ;
const totalTeams = groupStage . groupCount * groupStage . teamsPerGroup ;
// Collect participant IDs from form
const participantIds : string [ ] = [ ] ;
for ( let i = 0 ; i < totalTeams ; i ++ ) {
const participantId = formData . get ( ` participant ${ i } ` ) ;
if ( typeof participantId !== "string" || ! participantId ) {
return { error : ` Participant ${ i + 1 } is required ` } ;
}
participantIds . push ( participantId ) ;
}
// Check for duplicates
const uniqueParticipants = new Set ( participantIds ) ;
if ( uniqueParticipants . size !== participantIds . length ) {
return { error : "Each participant can only be selected once" } ;
}
try {
// Update the event with template info
await updateScoringEvent ( params . eventId , {
bracketTemplateId : templateId ,
scoringStartsAtRound : template.scoringStartsAtRound ,
} ) ;
// Create tournament groups
const groups = await createGroupsForEvent ( params . eventId , groupStage . groupLabels ) ;
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
// Distribute participants into groups (in selection order) and create round-robin matches
2026-02-14 22:30:12 -08:00
for ( let groupIndex = 0 ; groupIndex < groups . length ; groupIndex ++ ) {
const startIdx = groupIndex * groupStage . teamsPerGroup ;
const groupParticipantIds = participantIds . slice (
startIdx ,
startIdx + groupStage . teamsPerGroup
) ;
await addMembersToGroup ( groups [ groupIndex ] . id , groupParticipantIds ) ;
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
// Create the 6 round-robin match pairings for this group
if ( groupParticipantIds . length === 4 ) {
const pairings = generateRoundRobinPairings ( groupParticipantIds ) ;
await createGroupStageMatches ( groups [ groupIndex ] . id , pairings ) ;
}
2026-02-14 22:30:12 -08:00
}
// Generate the empty knockout bracket structure
await generateBracketFromTemplate ( params . eventId , templateId ) ;
2026-04-01 00:40:55 -07:00
// Eliminate participants from this sport season who are not in any group
const allParticipants = await findParticipantsBySportsSeasonId ( params . id ) ;
let eliminatedCount = 0 ;
for ( const participant of allParticipants ) {
if ( ! uniqueParticipants . has ( participant . id ) ) {
await setParticipantResult ( participant . id , params . id , 0 ) ;
eliminatedCount ++ ;
}
}
return {
success : ` Groups and knockout bracket structure created successfully ${ eliminatedCount > 0 ? ` ( ${ eliminatedCount } participant(s) not in any group marked as eliminated) ` : "" } ` ,
} ;
2026-02-14 22:30:12 -08:00
} catch ( error ) {
2026-03-21 13:41:39 -07:00
logger . error ( "Error generating groups:" , error ) ;
2026-02-14 22:30:12 -08:00
return {
error : error instanceof Error ? error . message : "Failed to generate groups" ,
} ;
}
}
if ( intent === "toggle-elimination" ) {
const memberId = formData . get ( "memberId" ) ;
if ( typeof memberId !== "string" || ! memberId ) {
return { error : "Member ID is required" } ;
}
try {
const updated = await toggleMemberEliminated ( memberId ) ;
return {
success : updated.eliminated
? "Team marked as eliminated"
: "Team reinstated" ,
} ;
} catch ( error ) {
2026-03-21 13:41:39 -07:00
logger . error ( "Error toggling elimination:" , error ) ;
2026-02-14 22:30:12 -08:00
return {
error : error instanceof Error ? error . message : "Failed to toggle elimination" ,
} ;
}
}
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
if ( intent === "update-group-match" ) {
const matchId = formData . get ( "matchId" ) ;
const p1Score = formData . get ( "participant1Score" ) ;
const p2Score = formData . get ( "participant2Score" ) ;
if ( typeof matchId !== "string" || ! matchId ) return { error : "matchId is required" } ;
if ( typeof p1Score !== "string" || typeof p2Score !== "string" ) {
return { error : "Both scores are required" } ;
}
const s1 = parseInt ( p1Score , 10 ) ;
const s2 = parseInt ( p2Score , 10 ) ;
if ( isNaN ( s1 ) || isNaN ( s2 ) || s1 < 0 || s2 < 0 ) {
return { error : "Scores must be non-negative integers" } ;
}
try {
await updateGroupStageMatchResult ( matchId , s1 , s2 ) ;
return { success : "Match result saved" } ;
} catch ( error ) {
logger . error ( "Error updating group match:" , error ) ;
return { error : error instanceof Error ? error . message : "Failed to update match" } ;
}
}
if ( intent === "update-group-match-schedule" ) {
const matchId = formData . get ( "matchId" ) ;
const scheduledAt = formData . get ( "scheduledAt" ) ;
if ( typeof matchId !== "string" || ! matchId ) return { error : "matchId is required" } ;
try {
const dateValue = typeof scheduledAt === "string" && scheduledAt
? new Date ( scheduledAt )
: null ;
await updateGroupStageMatchSchedule ( matchId , dateValue ) ;
return { success : "Match time saved" } ;
} catch ( error ) {
logger . error ( "Error updating group match schedule:" , error ) ;
return { error : error instanceof Error ? error . message : "Failed to update schedule" } ;
}
}
2026-02-14 22:30:12 -08:00
if ( intent === "populate-knockout" ) {
try {
const event = await getScoringEventById ( params . eventId ) ;
if ( ! event ) {
return { error : "Event not found" } ;
}
// Parse match assignments from form
const assignments : Array < {
matchNumber : number ;
slot : "participant1Id" | "participant2Id" ;
participantId : string ;
} > = [ ] ;
for ( const [ key , value ] of formData . entries ( ) ) {
const matchPattern = /^match-(\d+)-(participant1Id|participant2Id)$/ ;
const match = key . match ( matchPattern ) ;
if ( match && typeof value === "string" && value ) {
assignments . push ( {
matchNumber : parseInt ( match [ 1 ] , 10 ) ,
slot : match [ 2 ] as "participant1Id" | "participant2Id" ,
participantId : value ,
} ) ;
}
}
if ( assignments . length !== 32 ) {
return { error : ` Expected 32 assignments but got ${ assignments . length } ` } ;
}
// Validate all assignments are unique participants
const assignedParticipantIds = new Set ( assignments . map ( ( a ) = > a . participantId ) ) ;
if ( assignedParticipantIds . size !== 32 ) {
return { error : "All 32 knockout slots must have unique participants" } ;
}
// Validate all assigned participants are non-eliminated
const advancingIds = new Set ( await getAdvancingParticipantIds ( params . eventId ) ) ;
for ( const participantId of assignedParticipantIds ) {
if ( ! advancingIds . has ( participantId ) ) {
return { error : "All assigned participants must be non-eliminated group members" } ;
}
}
// Assign participants to knockout bracket
await assignParticipantsToKnockout ( params . eventId , assignments ) ;
// Mark eliminated group participants with finalPosition = 0
const eliminatedIds = await getEliminatedParticipantIds ( params . eventId ) ;
for ( const participantId of eliminatedIds ) {
await setParticipantResult ( participantId , event . sportsSeasonId , 0 ) ;
}
2026-03-21 13:41:39 -07:00
logger . log (
2026-02-14 22:30:12 -08:00
` [PopulateKnockout] Assigned 32 participants to knockout, marked ${ eliminatedIds . length } as eliminated `
) ;
return { success : "Knockout bracket populated successfully" } ;
} catch ( error ) {
2026-03-21 13:41:39 -07:00
logger . error ( "Error populating knockout:" , error ) ;
2026-02-14 22:30:12 -08:00
return {
error : error instanceof Error ? error . message : "Failed to populate knockout" ,
} ;
}
}
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
// ── Game scheduling ──────────────────────────────────────────────────────
if ( intent === "add-game" ) {
const matchId = formData . get ( "matchId" ) ;
const gameNumber = formData . get ( "gameNumber" ) ;
const scheduledAt = formData . get ( "scheduledAt" ) ;
const notes = formData . get ( "notes" ) ;
if ( typeof matchId !== "string" || ! matchId ) return { error : "matchId is required" } ;
if ( typeof gameNumber !== "string" || ! gameNumber ) return { error : "gameNumber is required" } ;
try {
await createGame ( {
playoffMatchId : matchId ,
gameNumber : parseInt ( gameNumber , 10 ) ,
scheduledAt : scheduledAt ? new Date ( scheduledAt as string ) : null ,
notes : notes ? ( notes as string ) : null ,
} ) ;
return { success : "Game added" } ;
} catch ( error ) {
return { error : error instanceof Error ? error . message : "Failed to add game" } ;
}
}
if ( intent === "update-game" ) {
const gameId = formData . get ( "gameId" ) ;
const scheduledAt = formData . get ( "scheduledAt" ) ;
const status = formData . get ( "status" ) ;
const participant1Score = formData . get ( "participant1Score" ) ;
const participant2Score = formData . get ( "participant2Score" ) ;
const winnerId = formData . get ( "winnerId" ) ;
const notes = formData . get ( "notes" ) ;
if ( typeof gameId !== "string" || ! gameId ) return { error : "gameId is required" } ;
const validStatuses : PlayoffMatchGameStatus [ ] = [ "scheduled" , "complete" , "postponed" ] ;
if ( status && ! validStatuses . includes ( status as PlayoffMatchGameStatus ) ) {
return { error : ` Invalid status: must be one of ${ validStatuses . join ( ", " ) } ` } ;
}
try {
const updated = await updateGame ( gameId , {
. . . ( scheduledAt !== null && { scheduledAt : scheduledAt ? new Date ( scheduledAt as string ) : null } ) ,
. . . ( status && { status : status as PlayoffMatchGameStatus } ) ,
. . . ( participant1Score !== null && { participant1Score : ( participant1Score as string ) || null } ) ,
. . . ( participant2Score !== null && { participant2Score : ( participant2Score as string ) || null } ) ,
. . . ( winnerId !== null && { winnerId : ( winnerId as string ) || null } ) ,
. . . ( notes !== null && { notes : ( notes as string ) || null } ) ,
} ) ;
if ( ! updated ) return { error : "Game not found" } ;
return { success : "Game updated" } ;
} catch ( error ) {
return { error : error instanceof Error ? error . message : "Failed to update game" } ;
}
}
if ( intent === "delete-game" ) {
const gameId = formData . get ( "gameId" ) ;
if ( typeof gameId !== "string" || ! gameId ) return { error : "gameId is required" } ;
try {
await deleteGame ( gameId ) ;
return { success : "Game deleted" } ;
} catch ( error ) {
return { error : error instanceof Error ? error . message : "Failed to delete game" } ;
}
}
// ── Odds management ──────────────────────────────────────────────────────
if ( intent === "upsert-odds" ) {
const matchId = formData . get ( "matchId" ) ;
const participantId = formData . get ( "participantId" ) ;
const moneylineOdds = formData . get ( "moneylineOdds" ) ;
const oddsSource = formData . get ( "oddsSource" ) ;
if ( typeof matchId !== "string" || ! matchId ) return { error : "matchId is required" } ;
if ( typeof participantId !== "string" || ! participantId ) return { error : "participantId is required" } ;
if ( typeof moneylineOdds !== "string" || ! moneylineOdds ) return { error : "moneylineOdds is required" } ;
const moneylineInt = parseInt ( moneylineOdds , 10 ) ;
if ( isNaN ( moneylineInt ) ) return { error : "moneylineOdds must be an integer" } ;
try {
await upsertMatchOdds ( matchId , participantId , {
moneylineOdds : moneylineInt ,
oddsSource : oddsSource ? ( oddsSource as string ) : undefined ,
} ) ;
return { success : "Odds updated" } ;
} catch ( error ) {
return { error : error instanceof Error ? error . message : "Failed to update odds" } ;
}
}
if ( intent === "delete-odds" ) {
const matchId = formData . get ( "matchId" ) ;
const participantId = formData . get ( "participantId" ) ;
if ( typeof matchId !== "string" || ! matchId ) return { error : "matchId is required" } ;
if ( typeof participantId !== "string" || ! participantId ) return { error : "participantId is required" } ;
try {
await deleteOddsForParticipant ( matchId , participantId ) ;
return { success : "Odds deleted" } ;
} catch ( error ) {
return { error : error instanceof Error ? error . message : "Failed to delete odds" } ;
}
}
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
return { error : "Invalid action" } ;
}