Skip to content

feat(playoffs): season playoff brackets - #50

Merged
ershisan99 merged 3 commits into
Balatro-Multiplayer:mainfrom
ChronoFinale:feat/playoffs-bracket
Jul 7, 2026
Merged

feat(playoffs): season playoff brackets#50
ershisan99 merged 3 commits into
Balatro-Multiplayer:mainfrom
ChronoFinale:feat/playoffs-bracket

Conversation

@ChronoFinale

@ChronoFinale ChronoFinale commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

feat(playoffs): season playoff brackets

What

Season playoff brackets on the site, per Dunk's request to make playoffs more visible.

Public /playoffs tab (desktop nav + mobile menu)

  • Rendered like the league's playoff sheet: two mirrored wings converging on a center column with the Champion box, Grand Finals, and Third Place Match — red match frames, black player boxes, blue score pills, Balatro m6x11 pixel font, connector elbows, gold winner names, crowned champion.
  • Fully responsive, never scrolls horizontally: scales to fit down to 72%, then reflows into stacked round-by-round sections at full text size.
  • Defaults to the active season's bracket; older seasons stay browsable via picker pills and /playoffs/{id}. Seed names linked to a player are profile links.

Admin /admin/brackets (new brackets.manage permission)

  • Create = pick season (one bracket per season, enforced) + player count + third-place toggle. Series formats per bracket: Bo3/Bo5/… with a separate grand-finals format.
  • The bracket itself is the editor: add players to a pool by searching the ranked leaderboard (rank + MMR, Discord id auto-linked) or typing any name; drag chips onto first-round spots (tap-tap works too), drag between spots to swap; type scores directly on the cards (auto-save after typing stops). Winners gold/advance instantly, losers grey.

How

  • Only a roster and per-match scores are stored (brackets, bracket_seeds, bracket_results; single migration 0015). Pairings, advancement, semifinal-losers-to-third-place, and the champion are all derived in a pure bun-tested module (src/lib/bracket.ts) — the displayed bracket can never disagree with entered results, and correcting an old score reflows later rounds.
  • Series semantics: a winner needs the majority of the best-of (a 1-0 in a Bo3 advances nobody), impossible scores like 2-2 in a Bo3 are rejected with a clear error, and rearranging placements clears exactly the stored results the change invalidates (changed matches + their downstream chain) so old scores can never decide new pairings.
  • Brackets are owned by seasons (season_id NOT NULL + unique); display names derive from the season registry.
  • Public procedures only expose published brackets.

Granting access

brackets.manage must be granted per user in /admin/permissions ("Manage brackets" under the Playoffs group) — permissions come from each user's stored permissions array, so nobody has bracket access until granted, existing admins included. Dunk logs in once via Discord to appear in the manager. The single permission unlocks only the bracket admin.

Follow-up (not in this PR)

Player-profile trophies ("Season X Playoff Champion" badge derived from published bracket results). The groundwork ships here — seeds carry Discord ids — and a working implementation is parked on feat/profile-trophies-wip.

Drive-by

  • Added toMatchObject to the bun:test type shim (the bracket engine tests use it; typecheck fails without it).

Validation

bun test (32), bun run check, bun run typecheck, bun run build all green. Exercised end-to-end on local dev: created brackets via the admin UI, placed players, entered results, verified propagation/champion, published, checked public pages in both themes and at narrow widths.

@ChronoFinale
ChronoFinale force-pushed the feat/playoffs-bracket branch from 73364c8 to e7e08e8 Compare July 7, 2026 00:01
@ChronoFinale

Copy link
Copy Markdown
Contributor Author

Examples

Unfinished

Screenshot 2026-07-06 at 2 06 14 PM

Finished

shotplayoffss4champion

Responsive on Mobile

Screenshot 2026-07-06 at 7 01 19 PM

@ChronoFinale
ChronoFinale force-pushed the feat/playoffs-bracket branch 3 times, most recently from baf7bf6 to c24e1ca Compare July 7, 2026 00:40
One single-elimination bracket per season (4-32 players). Only the
roster and per-match scores are stored (migration 0015); pairings,
advancement, third place, and the champion are derived in a pure,
bun-tested module, so displayed state always matches entered results.
Best-of-N series semantics (finals can differ): partial scores advance
nobody and impossible scores are rejected. Re-seeding clears exactly
the results it invalidates. tRPC router behind a new brackets.manage
permission (granted per-user in /admin/permissions), with leaderboard
player search and published-only public reads.
/playoffs renders the bracket like the league's playoff sheet: mirrored
wings converging on a center champion/finals/third-place column, styled
with the Balatro pixel font on a dark board. One shared BracketSheet
component owns the geometry (the admin board reuses it via a
renderMatch prop). Responsive without horizontal scroll: scales to fit,
then stacks rounds vertically below a readability floor. Pages are
cached and revalidated on demand by admin edits. Defaults to the active
season's bracket; older seasons via picker pills and /playoffs/[id].
/admin/brackets (brackets.manage): create is just season + size +
format; the bracket itself is the editor. Search the ranked leaderboard
or type a name to add players to a pool, drag (or tap) them onto
first-round spots, drag between spots to swap. Scores are typed on the
match cards, auto-save after typing stops, validate the series format
inline, and advance the winner immediately. Roster saves are serialized
against races and re-sync from server truth, so a failed save reverts
visibly.
@ChronoFinale
ChronoFinale force-pushed the feat/playoffs-bracket branch from c24e1ca to 41d9b18 Compare July 7, 2026 00:47
@ChronoFinale
ChronoFinale marked this pull request as ready for review July 7, 2026 00:51
@ershisan99

Copy link
Copy Markdown
Collaborator

I'm gonna merge this and if something breaks we'll fix ig

@ershisan99
ershisan99 merged commit 362a311 into Balatro-Multiplayer:main Jul 7, 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.

2 participants