Skip to content

feat(banpick): server-driven deck+stake draft UI + pool-fetch seam - #16

Open
ChronoFinale wants to merge 1 commit into
Balatro-Multiplayer:mainfrom
ChronoFinale:feat/banpick-server-draft
Open

feat(banpick): server-driven deck+stake draft UI + pool-fetch seam#16
ChronoFinale wants to merge 1 commit into
Balatro-Multiplayer:mainfrom
ChronoFinale:feat/banpick-server-draft

Conversation

@ChronoFinale

@ChronoFinale ChronoFinale commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Server-driven ban-pick draft UI + pool-fetch seam

The deck+stake ban-pick draft engine and the client seam that fetches a server-generated pool. Pairs with the server-side draft PR (BalatroMultiplayerAPI-Server); the PvP/Speedrun consumer wiring lands separately.

Engine (api/ban_pick.lua)

  • Tuple identity is deck+stake — banning Red@White no longer bans Red@Gold
  • Random is a blind commit (you don't see the roll before confirming)
  • Two-column tile hover — deck info alongside stake detail
  • Cocktail composition badge — shows the merged decks for a composite tile
  • Counter + buttons stay visible off-turn (greyed), so the draft never looks frozen
  • Per-draft stale-broadcast protection with an atomic random commit + guest UI sync
  • Draft tiles drag-locked; popups clamp to the screen on both axes
  • Composite-agnostic — a pool item carries its own decks/name; the engine has zero cocktail-specific knowledge

Seam (api/matchmaking/draft.lua, networking/api_client/draft.lua)

  • issue_draft_pool fetches the match's server pool; callback(nil) when there's none, leaving fallback-vs-abort to the consumer
  • Server-authoritative — no max_stake param, no client-side event posting

Testing

  • luajit -bl clean on all touched files
  • Visual scenarios in dev/shots.lua + dev/test_banpick_*.lua (run by the DevTools shot harness)

Draft — opening for eyes on the engine + seam shape.


Screenshots — draft UI

Rendered by the DevTools shot harness (BMP_SHOT_SUITE=1) from the dev/shots.lua scenarios in this PR; each caption is that scenario's expect: contract. Images are hosted on the fork branch pr16-shots — they are not part of the code diff.

Your turn — ban a deck


DECK BAN overlay over the menu: "Your turn: ban a deck" (green), 9 deck tiles, Selected: 0/3, greyed Confirm Ban, blue Random.

Cocktail badge — hover


Badge reads "Casjb Cocktail: Green Deck + Black Deck + Orange Deck"; its hover shows the three decks side by side with full effects. The engine renders the composite verbatim — the consumer owns the wording.

Pick phase


Final PICK step between the last two: seven tiles debuffed, two live; green "pick your deck"; the chosen survivor raised with a green Selected tag; green Confirm Pick.

More scenarios (7) — selection, blind Random, off-turn, banned tiles, tuple/cocktail hovers, Speedrun variant

2 of 3 selected


Two tiles raised with red Selected tags; counter 2/3; Confirm stays greyed until exactly 3.

Random armed (blind commit)


No tiles raised; counter ?/3; Random turns red "Cancel Random"; Confirm goes green "Confirm Random" — nothing is revealed until Confirm, so there is nothing to peek at or reroll-fish.

Opponent's turn


Status not green; counter and both buttons visible but greyed; layout otherwise identical.

Banned tiles


Banned tiles are debuffed (darkened X overlay) and non-interactive.

Tuple hover — stake column


Hover popup: deck name + effects on the left, stake column on the right (stake name in its colour, description, "Also applied" list). Clamped fully on screen.

Cocktail tile hover — compact


The cocktail tile's own hover is compact: title, "rotating 3-deck mix", three deck names only (no effect boxes) plus the stake column — same footprint as a normal deck hover.

Same engine, different consumer — Speedrun


The generic schedule driving Speedrun's multi-pick draft variant — same MPAPI.BanPick engine, different consumer config.


Note on the screenshots

These are captured with the full stack running locally. The engine in this PR is consumer-agnostic — it renders whatever pool and composite items a consumer hands it — so the specific content shown (the deck+stake tuples, the 1 / 3 / 3 / pick schedule, the "Casjb Cocktail" wording and composition) comes from code outside this PR that is not all upstreamed yet:

  • The PvP consumer wiring (the gamemode schedule, deck/cocktail decoration) lives in the PvP mod and is not yet in a PR.
  • The server-generated pool + weekly cocktail live in the server — see below.

The framework code here is self-contained and testable on its own (dev/test_banpick_*.lua); the shots just show it driven by the real consumer + server.

Related — server side

The pools this UI fetches, and the weekly cocktail shown in shots 07–08, are produced server-side in feat(draft): server-generated draft pools with per-queue policy — BalatroMultiplayerAPI-Server#42. This client PR is the rendering + fetch_draft_pool half of that feature.

The weekly cocktail is just data, rotated at runtime via a small admin endpoint (no redeploy):

# Read the current cocktail
curl https://<server>/admin/weekly-cocktail \
  -H "x-admin-secret: $ADMIN_SECRET"
# 200
# { "name": "Casjb", "decks": ["b_green", "b_black", "b_mp_orange"] }

# Rotate it (takes effect at the next match start)
curl -X PUT https://<server>/admin/weekly-cocktail \
  -H "x-admin-secret: $ADMIN_SECRET" \
  -H "content-type: application/json" \
  -d '{ "name": "Casjb", "decks": ["b_green", "b_black", "b_mp_orange"] }'
# 200  — normalized value echoed back
# { "name": "Casjb", "decks": ["b_green", "b_black", "b_mp_orange"] }

Validation is server-side and strict (1–3 distinct cocktail-eligible decks — every deck except the Cocktail deck itself — and a name ≤ 40 chars):

curl -X PUT https://<server>/admin/weekly-cocktail \
  -H "x-admin-secret: $ADMIN_SECRET" -H "content-type: application/json" \
  -d '{ "name": "Nope", "decks": ["b_mp_cocktail"] }'
# 400
# { "error": "deck 'b_mp_cocktail' is not cocktail-eligible" }

The name carries no "Cocktail" suffix — the client appends its own localized wording (so "Casjb" renders as "Casjb Cocktail"), which is why the badge text in the shots is the consumer's, not the server's.

@ChronoFinale
ChronoFinale force-pushed the feat/banpick-server-draft branch 5 times, most recently from a949529 to 2d1bafc Compare July 23, 2026 16:21
The deck+stake ban-pick draft engine plus the client seam that fetches a server-generated pool (pairs with the server-side draft-pool PR).

Engine (api/ban_pick.lua): tuple identity is deck+stake (Red@White != Red@Gold); Random is a blind commit; two-column tile hover (deck + stake detail); cocktail composition badge; counter/buttons stay visible off-turn, greyed; per-draft stale-broadcast protection with atomic random commit + guest UI sync; draft tiles drag-locked; popups clamp to the screen on both axes. Composite-agnostic -- a pool item carries its own decks/name, no cocktail knowledge in the engine.

Seam (api/matchmaking/draft.lua, networking/api_client/draft.lua): issue_draft_pool fetches the match's server pool, callback(nil) when none so the consumer chooses fallback vs abort. Server-authoritative -- no max_stake param, no client-side event posting.

Visual scenarios in dev/shots.lua for the DevTools shot harness.
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