Skip to content

Reseed knockout slots from closed groups and forfeit unscored picks - #6

Merged
borland667 merged 1 commit into
mainfrom
ux/closed-group-bracket-seeding
Jun 15, 2026
Merged

Reseed knockout slots from closed groups and forfeit unscored picks#6
borland667 merged 1 commit into
mainfrom
ux/closed-group-bracket-seeding

Conversation

@borland667

Copy link
Copy Markdown
Owner

Summary

Two coordinated changes that take effect once a group's official group.result is published:

  • Predict wizard bracket display: knockout slots that reference a closed group (1A, 2B, 3A/B/C/D, ...) now resolve to the real finishers instead of the user's predicted placement. The user's stored group prediction is preserved untouched — this is purely a display change so the user picks a winner among the teams that actually advanced.
  • Scoring carve-out for invalidated picks: a knockout match is treated as unscoreable when the user's stored predictedWinner is a team that did not actually play it (e.g. they predicted 1A = MEX, picked MEX to win 1A vs 3B, but the real 1A was Korea so the match was played as KOR vs HAI). The match contributes 0 to earned knockoutScore AND 0 to per-user maximum-possible. The forfeited pointsPerCorrect is summed into a new Score.forfeitedKnockoutPoints column so UIs can compute an effective max as tournament.rules.totalMaximumPoints - forfeitedKnockoutPoints. The user is neither rewarded nor penalised for a pick they could not have made correctly.

Scope is intentionally minimal — the leaderboard UI is not yet showing the per-user effective max, that can be a follow-up.

Files

  • src/pages/Predict.jsx: merge group.result over groupPredictions for slot resolution in RoundStep
  • api/scoring.cjs: scoreKnockoutPrediction returns { score, scoreable, maxPoints }; calculateTotalScore aggregates forfeitedKnockoutPoints
  • api/app.cjs: ship selectedHomeTeamId / selectedAwayTeamId into the scoring context; persist + serialize forfeitedKnockoutPoints on leaderboard responses
  • prisma/schema.prisma + prisma/migrations/20260615000000_score_forfeited_knockout_points/: add Score.forfeitedKnockoutPoints Int @default(0)
  • tests/scoring.test.mjs: cover the new return shape, the skip rule, and the forfeit aggregation
  • docs/ARCHITECTURE.md §10.2 + docs/IMPLEMENTATION_STATUS.md: document the new scoring and bracket-seeding behaviour

Test plan

  • npm run verify — 49/49 tests pass, build is clean
  • In the Predict wizard, seed a closed group whose real 1A differs from the user's pick. Open a Round-of-16 step that references 1A vs 3B/C/D. Confirm the bracket card shows the real 1A team and the dropdown lists the real participants
  • Confirm the user's stored group placement is unchanged after viewing the wizard (no auto-save)
  • Run a results import for a knockout match whose participants don't include the user's stored pick. Confirm Score.knockoutScore is unchanged and Score.forfeitedKnockoutPoints increased by pointsPerCorrect for that round
  • Confirm correctly-predicted users score normally — the skip rule only fires when the picked team did not appear in the match

Made with Cursor

Once a group has an official `group.result`, the Predict wizard's
downstream knockout slots (1A, 2B, 3A/B/C/D, ...) now resolve to the
real finishers instead of the user's predicted placement. The user's
stored group prediction is preserved untouched — this only changes the
displayed teams in the bracket so the user picks a winner among teams
that actually advanced.

The scoring engine also treats a knockout match as unscoreable when the
user's stored `predictedWinner` is a team that did not appear in the
match's recorded participants. Those matches add zero to the user's
earned `knockoutScore` and zero to their per-user maximum-possible
score — the forfeited `pointsPerCorrect` is summed into a new
`Score.forfeitedKnockoutPoints` column so callers can compute an
effective max as `tournament.rules.totalMaximumPoints -
forfeitedKnockoutPoints`. The user is neither rewarded nor penalised
for a pick they could not have made correctly.

- `src/pages/Predict.jsx`: merge `group.result` over `groupPredictions`
  for slot resolution in `RoundStep`
- `api/scoring.cjs`: `scoreKnockoutPrediction` returns
  `{ score, scoreable, maxPoints }`; `calculateTotalScore` aggregates
  `forfeitedKnockoutPoints`
- `api/app.cjs`: ship `selectedHomeTeamId` / `selectedAwayTeamId` to
  the scoring context, persist + serialize `forfeitedKnockoutPoints`
- `prisma/schema.prisma` + migration: add
  `Score.forfeitedKnockoutPoints Int @default(0)`
- `tests/scoring.test.mjs`: cover the new return shape and skip rule
- `docs/ARCHITECTURE.md` + `docs/IMPLEMENTATION_STATUS.md`: document
  the new scoring and bracket-seeding behaviour

Co-authored-by: Cursor <cursoragent@cursor.com>
@borland667
borland667 merged commit 0e2ae64 into main Jun 15, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant