Reseed knockout slots from closed groups and forfeit unscored picks - #6
Merged
Conversation
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>
This was referenced Jun 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two coordinated changes that take effect once a group's official
group.resultis published: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.predictedWinneris a team that did not actually play it (e.g. they predicted1A = MEX, picked MEX to win1A vs 3B, but the real1Awas Korea so the match was played asKOR vs HAI). The match contributes 0 to earnedknockoutScoreAND 0 to per-user maximum-possible. The forfeitedpointsPerCorrectis summed into a newScore.forfeitedKnockoutPointscolumn so UIs can compute an effective max astournament.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: mergegroup.resultovergroupPredictionsfor slot resolution inRoundStepapi/scoring.cjs:scoreKnockoutPredictionreturns{ score, scoreable, maxPoints };calculateTotalScoreaggregatesforfeitedKnockoutPointsapi/app.cjs: shipselectedHomeTeamId/selectedAwayTeamIdinto the scoring context; persist + serializeforfeitedKnockoutPointson leaderboard responsesprisma/schema.prisma+prisma/migrations/20260615000000_score_forfeited_knockout_points/: addScore.forfeitedKnockoutPoints Int @default(0)tests/scoring.test.mjs: cover the new return shape, the skip rule, and the forfeit aggregationdocs/ARCHITECTURE.md§10.2 +docs/IMPLEMENTATION_STATUS.md: document the new scoring and bracket-seeding behaviourTest plan
npm run verify— 49/49 tests pass, build is clean1Adiffers from the user's pick. Open a Round-of-16 step that references1A vs 3B/C/D. Confirm the bracket card shows the real1Ateam and the dropdown lists the real participantsScore.knockoutScoreis unchanged andScore.forfeitedKnockoutPointsincreased bypointsPerCorrectfor that roundMade with Cursor