Skip to content

Fix responsive game table layout - #1

Draft
antash-mishra wants to merge 47 commits into
masterfrom
codex/bot-personas
Draft

Fix responsive game table layout#1
antash-mishra wants to merge 47 commits into
masterfrom
codex/bot-personas

Conversation

@antash-mishra

Copy link
Copy Markdown
Collaborator

What changed

  • Reworked opponent seats into lightweight avatar/name markers instead of stacked cards.
  • Added one responsive screen-space layout shared by DOM seats, the Three.js hand, and played cards.
  • Kept the First/Final Four Cards announcement compact while leaving every player visible.
  • Added viewport geometry and full mobile gameplay regressions.

Why

Opponent cards, seat markers, and the local hand were independently positioned, causing overlaps and misalignment across desktop, mobile, and short landscape screens. The announcement state also explicitly hid player avatars.

Impact

The table now stays aligned and readable across supported viewport classes, with player identities and played cards occupying stable lanes.

Validation

  • cd app && npm test -- --run — 97 tests passed
  • cd app && npm run build
  • Responsive geometry suite — 9 viewport classes passed
  • Full 390×844 headed touch game passed
  • Racket unit and integration suites passed during implementation

shakdwipeea and others added 30 commits June 10, 2026 02:46
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- spawn the client-lambda bot AI thread for users added via
  add-bot-to-room, with unique bot names (bot-1/2/3) so channel
  messages route to the right player
- fix start-game double-counting the host (members already
  includes the host), which broke dealing with 5 players
- broadcast (turn idx), (bid-placed idx bid) and (played idx card)
  to all players so clients can render the whole table
- fix arity crashes in the invalid-bid and invalid-card recovery
  paths (re-ask instead of crashing the game thread)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Network layer:
- src/net/sexpr.ts: s-expression codec for the Racket protocol
  (symbols, strings, dotted pairs, prefab structs, hashes)
- src/net/protocol.ts: typed ServerEvent/ClientCommand codecs and
  card mapping mirroring game.rkt deck generation
- src/net/SocketService.ts: Effect-based WebSocket client exposing
  decoded events as a Queue

Game session:
- GameSession quick-play: connect, create room, seat 3 bots, start
  game, translate server events to reducer actions with pacing
- extended GameModel: phases, bidding, trump, pending requests,
  per-team points/tricks; trick winner derived from the next-leader
  broadcast (final trick from points-won totals)

UI:
- start screen, bid panel, trump picker, expose-trump button,
  hand-result overlay, two-team HUD, trick-winner status + sweep
- click-to-play and forgiving drag drop zone; turn-locked input
  with follow-suit hints (server stays authoritative)
- hand fan and trick cross face the camera with adaptive spacing
- cancel in-flight card tweens and recompute hover slots so deals
  and re-arranges can't strand cards in stale positions
- paint the missing J-of-spades art (sprite sheet has none) and fix
  the shifted spades row mapping
- debug bridge (window.__game) for automated UI tests; lil-gui
  panel now behind ?debug, mock game behind ?mock

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drives the real UI (bid/trump buttons, mouse drag and click-to-play)
against the live server, screenshots each phase, regression-tests
hover-during-deal, and fails on console errors or an unfinished game.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- join-room broadcasts room-members (same shape as add-bot) and acks the joiner
- leave-room / kick-from-room messages; host leaving closes the table
- room-closed / removed-from-room notifications; bots released on kick/close/game end
- disconnect cleanup instead of busy-looping on EOF; broadcasts skip dead members
- running games actually tracked; tables delisted once play starts
- dispatch error guard, ping/pong heartbeat, tagged active-rooms reply
- headless main (no stdin -> serve forever); start-service takes a port;
  bots follow --port via GAME-PORT

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- start screen with name entry, create table, room browser with live join
- lobby: seat list, add bot, kick (host), leave/close, start gated on 4 players
- player names float by seat markers in-game (partner tagged); announcements
  and results use real names
- how-to-play overlay reachable from title screen and mid-game
- heartbeat ping, VITE_SERVER_URL/?port/?server endpoint resolution
- Playwright suites: multiplayer, lobby controls, multi-tables; HEADED=1
  opens visible browsers on all scripts

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Live: https://twenty-eight-game.netlify.app + wss://twenty-eight-game.fly.dev/test

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- portrait viewports widen the camera FOV and pull it back so the fan and
  trick stay in frame; hand fan compresses on narrow screens
- phone media queries: stacked HUD (score/status/trump rows), full-width
  panels, smaller type; seat name labels tighten on small screens
- test-mobile.mjs: 390x844 touch emulation — panels/hand must fit the
  viewport, full game played with real taps; documented in CLAUDE.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The sprite sheet has two 9 columns; the second (col 7) overwrote the first
and is corrupted in the hearts row (corner indices read 8) and digit-less in
the other suits. Slice the first 9 column instead and ink the missing corner
9s onto diamonds/clubs/spades, matching the painted J-of-spades approach.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…oring

game.rkt rewrite with a rackunit test submodule (26 tests):
- fresh-deck shuffles per game; the deck was shuffled once at module load,
  so every game on a server run dealt identical hands
- distribute-cards #:shuffle? dealt from the shuffled deck but dropped the
  remainder from the unshuffled one (7/16 cards duplicated)
- trick winner: highest card of the suit led by real rank order
  (J>9>A>10>K>Q>8>7, the numeric rank now encodes strength); exposed trumps
  win; trump-suit cards played before the exposure don't count. Previously
  the highest point card "won" and the trick went to an arbitrary seat
- auction: ends on three consecutive passes (was: any single pass), winner
  is the actual high bidder (was: next seat in rotation), raises strictly
  higher, opener must bid >=16, 28 ends it
- follow-suit enforced: trumping never excuses breaking suit; after calling
  for the exposure you must trump if able; exposing requires being void
- score-game implemented (team vs bid); redeal predicate fixed; module-level
  test code that ran on every require removed

server.rkt: deal from fresh-deck per game; validate bids/plays before
broadcasting; async player channels with tag-filtered receive (stray
messages no longer block threads forever); a mid-game disconnect now aborts
the game — table freed, bots released, humans get game-aborted.

frontend: rank numbers updated in lockstep; isLegalPlay mirrors the fixed
rules incl. must-trump-after-expose; game-aborted tears down to the start
screen; bid panel offers the server's minimum and hides pass for the
opener; TrickEnded clears only the completed trick. New GameModel.test.ts
(39 vitest tests total).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sessions driven through the window.__game debug bridge (Playwright suites,
smoke tests) bypass the start-button click handlers, so the start screen
stayed stacked above the table for the whole game. Entering a lobby now
hides it from updateLobby, which reacts to the model instead of the click.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A SoundService built on the Web Audio API — no sample files. Short breaths
of band-passed noise for the card family (deal riffle, play tap, trick
sweep) and quiet sine chimes for attention (your turn, bid ticks, trump
reveal, win/lose arpeggios), all tuned to stay inside the zen aesthetic.

Sounds react to dispatched actions in GameState.renderAction, mirroring how
the 3D/DOM rendering works. A "sound on/off" toggle sits bottom-left
(mirroring "how to play") and persists in localStorage. The AudioContext
unlocks on the first click or keypress per browser autoplay policy, and
every call no-ops silently when audio is unavailable.

__game.sounds() exposes per-sound attempt counters for the Playwright
suites; verified live: a full game fires deal 2, bidTick 10, yourTurn 10,
cardPlay 32, trickSweep 8 and one result sound, with zero console errors,
and the toggle persists across reloads.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
start-bidding gains #:first-player and play-game #:first-leader so the
dealer can rotate between hands (defaults 0 keep every existing caller
and test unchanged). New pure helpers for match play: hand-game-points
(+1/-2 under 20, +2/-4 from 20 up — defenders never score), match-winner
('evens/'odds/#f), and +match-target+ (6).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
start-game now plays a whole match: hand N's opener and first leader sit
at seat (N-1) mod 4, classic game points accumulate (hand-result
broadcast after every hand), and the first team to the target — default
6, MATCH-TARGET env overrides it for tests — ends the match with a
match-over broadcast. Between hands the game thread blocks until the
host sends (next-hand <email>); the gate polls every player's socket so
any disconnect (mid-hand or between hands) still aborts via the existing
game-aborted flow. The host channel is drained *before* hand-result goes
out — a genuine next-hand can only follow that broadcast, so draining at
the gate itself could swallow a prompt click. Per-hand player copies
start with empty hands, fixing the hand append-stacking. Bots and the
*running-games* entry are now released at match end, not hand end.

Bots bid the minimum while it is at most 20 and pass beyond that;
previously they chased every auction to 27 and were always set, so a
bot team's score could never rise and a match could never end.

tests.rkt drives full matches over real websockets with four scripted
clients: game-point arithmetic and dealer rotation observed on the wire,
a guest disconnect at the gate aborting the match, and a MATCH-TARGET=1
match ending with consistent match-over totals.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The event loop no longer ends at points-won: a hand now finishes into
the new hand-finished phase, hand-result carries the server's verdict
plus the running game points (translated to us/them by seat parity), and
the result panel shows the host a "Deal the next hand" button while
everyone else waits. The next deal resets all per-hand model and session
state — including the locally-played dedupe set, since card ids recur
across hands — while seats, name labels, and the new Match HUD segment
persist. match-over ends the loop, names the winner, and offers Play
again; "finished" now means the match, not a hand. Aborts that land
while the between-hands panel is up also hide it. Help text documents
the +1/−2, +2/−4 scoring and the +6 target.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
test-match.mjs plays a two-hand match with host + guest pages and two
bots: between-hands panel roles (deal button vs waiting note), mirrored
match scores and HUD, an inert guest nextHand(), the host's real click
dealing a reset hand 2 with seats and match score intact, exactly one
team's score moving per hand, and a host-page close aborting the guest
back to a clean start screen. The single-hand scripts (play-game,
play-multiplayer, test-mobile) now stop at the hand-finished gate, since
"finished" means the whole match. CLAUDE.md documents the match loop,
the scoring helpers, MATCH-TARGET, and the new script.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Target 1 was probabilistic: under +1/−2 game points a team of random
players is a negative-drift walk that may never climb to +1, so roughly
one run in six played 25 hands and gave up. Target 0 ends after exactly
one hand either way — made lifts the bidders to the target, set leaves
the defenders already on it — and pins down which team must win.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
web-server shuts down a per-connection custodian when a websocket dies.
The match thread is spawned while dispatching the host's start-game and
every bot's socket+thread while dispatching the host's add-bot-to-room,
so a host disconnect hard-killed the match thread and all bots before
the abort path could run: no game-aborted for anyone, guests stranded
on the between-hands panel forever, gate silently gone. (Latent before
match play too — a mid-hand host disconnect killed the game without
notice — but the host-gated deal made it unmissable.) Game threads and
bots now start under detached custodians; disconnects are detected by
the gate/receive-datum polls and the abort path notifies the survivors.

The new integration scenario closes the HOST at the gate — under the
connection custodian it strands the guests and times out — alongside
the existing guest-at-gate abort test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…imeouts

The reducer nulls activePlayer when a hand finishes, but nothing told
the play area: with no turn broadcast after the last trick, the final
player's enso marker kept pulsing behind the between-hands panel and the
match-over screen. HandFinished rendering now clears it.

The e2e scripts called page.waitForFunction(fn, { timeout }) — Playwright
binds that second positional to the page function's arg, so every custom
timeout silently fell back to the 30s default. All call sites now pass
(fn, undefined, { timeout }); play-multiplayer and test-multi-tables had
the same pre-existing shape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
receive-datum now takes the full player list: a socket that dies while
someone else is thinking aborts the match within a second instead of
lying dormant until the dead seat's own turn. This is the server half of
leave-a-match — a leaving client just closes its socket — and equally
shortens the zombie window for real disconnects. New integration
scenario: the opening bidder goes AFK (muted scripted client) so the
game thread is parked on their channel, a different seat closes, and
the abort must still reach everyone promptly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nfirm

A live table now always offers a way out: a "leave match" text button
joins the sound toggle in the bottom-left corner whenever a hand is in
progress or the between-hands panel is up. The first click arms it
("abandon the match?"), a second within four seconds confirms; leaving
closes the socket — the server aborts the match for the other seats
within a second — and resets the page in place to a working start
screen, so the leaver can host or join a fresh table without a reload.
Dead sessions (failed connect, server gone) now also drop the session
object on the idle transition, fixing the dead start-screen buttons
after a disconnect. test-match.mjs now ends with the guest leaving via
the button: armed-state, prompt host abort, and a fresh table on the
leaver's same page.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Backend job: raco test game.rkt + the websocket integration suite.
Frontend job: vitest + the production build. e2e job (gated on both):
boots the real servers and runs all six Playwright suites headless,
uploading screenshots and server logs on failure. The browser comes from
/usr/bin/chromium — symlinked to the runner's Chrome, or Playwright's
chromium as a fallback — matching the path the scripts probe first.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…build platform entries

The lock predated esbuild 0.28's new platform packages (openharmony et
al.), so a clean `npm ci` — CI's install path — refused it while lazy
local installs kept working. Regenerated from scratch; `npm ci` now
validates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A user struct is now one shared mutable object per identity: connect-user
with a known email rebinds the seat's socket in place, hands are tracked
live on the struct, and handle-disconnect holds the seat of anyone in a
running match instead of forgetting them. Every wait's liveness check
gives a dead human socket RECONNECT-GRACE seconds (default 45) to come
back — announcing player-disconnected/player-reconnected to the table —
and only aborts when the grace runs out; bots get none, and an explicit
(leave-game <email>) aborts at once. The game thread maintains a live
match-state shadow (auction, trump, trick, points, what it awaits) so a
dispatch-thread (rejoin <email>) can answer with a complete per-seat
game-snapshot — the rejoiner's hand, trick on the table, scores, stage,
and their own pending request; trump stays hidden from non-bidders until
the exposure. Deals are best-effort sends now: a player inside their
grace window catches up from the snapshot.

Integration: drop at a parked auction → player-disconnected; same-email
reconnect → user-reconnected + snapshot (seat, stage, awaiting, restored
hand); the hand then plays out for everyone including the rejoiner; a
follow-up leave-game aborts immediately despite a 30s grace. The suite
runs under RECONNECT-GRACE=2 so the old disconnect-abort scenarios stay
fast.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Identity now persists in localStorage (player id + display name + the
active match's email). A page load that finds an interrupted match
reconnects under the seat's exact email, sends (rejoin <email>), and
restores everything from the game-snapshot: the reducer swaps in a fully
rebuilt model (hand, trick on the table, seat names, scores, match HUD,
auction state) and the renderer reconstructs the scene; the pending
request and between-hands result are re-dispatched through the normal
event path so the right panel re-arms itself. Other seats' drops show
as status notices ("X lost connection… / X is back"). Leaving now sends
(leave-game) first so the table aborts at once instead of waiting out
the leaver's grace window; ended matches answer no-running-game and the
page falls back to a fresh start screen with the name prefilled.

e2e: test-reconnect.mjs reloads mid-hand, verifies the restored table
(same cards, tricks, labels, host role), plays the hand out, and checks
that leaving clears the stored match. Wired into CI and the docs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
npm 10 (node 22) reconstructs vitest's nested vite/esbuild tree
differently from the npm 11 that wrote the lock and rejects it; the
same lock validates cleanly under node 24.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bots now evaluate before acting, through pure unit-tested helpers.
Bidding: appetite ≈ 14 + P/2 for P card points in the first four cards
(the expected team total given an even split of the unseen deck) plus a
long-suit bonus, hard-capped at 21 — double-stakes gambles are never
chased, and the opener still always bids. Trump: the longest suit,
points breaking ties. Play: win with the cheapest sufficient card, feed
a winning partner the cheapest follower, dump low value while saving
tens, ruff tricks worth points with the cheapest winning trump (never
the partner's trick), call the exposure when void on a worthwhile trick
or when the bidder sits on trumps, honour the must-trump obligation, and
lead boss cards tracked against every card seen this hand.

New integration scenario: a host plus three real bots play a target-2
match to completion — impossible with the old always-chase bots, whose
teams were set every hand. Making it pass surfaced two genuine lobby
races (concurrent add-bot dispatch threads): the bot-name counter now
increments by compare-and-set, and room joins serialize their
read-modify-write so simultaneous seats are not lost.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The target-2 bot-match scenario was a random walk: with one dummy seat
passing every bid and playing first-valid, reaching +2 sometimes took
over 30 hands and the test flaked. The scripted client can now think
with the real bot heuristics (#:smart? reuses choose-bid/choose-trump/
choose-play, exported from client.rkt), and the scenario asserts the
property that actually matters: a full table of bot brains makes a bid
within fifteen hands. Stable across repeated runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Antash and others added 17 commits June 11, 2026 20:33
Legality you can see: while the server waits on your play, every card
that cannot legally be played fades into the felt (the client's
isLegalPlay already knew; now the felt does too), restored the moment
the request resolves. The debug bridge exposes cardStates() and the
mobile/desktop suites assert dimming mirrors legality exactly.

Invite links: every lobby offers "copy invite link" — a URL carrying
?join=<table> (dev ?port/?server ride along) that lands a friend on a
start screen led by one button: Join "<table>". No more browsing for
the right room name.

Table emotes: a fixed six-emoji vocabulary (anything else the server
drops, with a per-sender 1.5s cooldown) broadcast as (emote-played seat
id) and floated as bubbles at the sender's seat marker, with a small
blip. The strip lives next to the in-match corner actions.

Phones: touch plays are now two-step — the first tap raises the card,
a second commits it, and only legal cards arm — ending fat-finger
misplays; mouse clicks/drags are unchanged. Portrait renders cards 18%
larger (hand and trick), the bid/trump/expose controls grow to thumb
size, the HUD and corner actions respect notch safe areas, and the app
installs to the home screen (manifest + icons + standalone display).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tables now have exactly four chairs server-side: add-user-to-game-room
enforces capacity inside the join lock (returns #f), so a stale invite
link bounces with (error room-full) instead of seating an invisible
fifth member who wedges the lobby; a full table's add-bot releases the
bot it just created. A failed entry (vanished or full table) hands back
a working start screen instead of stranding the user on a bare felt,
the invite's Join button is one-shot so it never re-offers a dead
table, and ?join survives the auto-rejoin path (the fallback start
screen still leads with the invite). The emote and leave-game failure
replies are now bare Ignored symbols — they used to reuse the rejoin
flow's (no-running-game) and could tear down a live session that
emoted during a match abort. Emotes received before a rejoin snapshot
restores the seat are dropped (seat translation was defaulting to 0),
copy-invite falls back to a prompt when the clipboard API is missing
entirely (http on a LAN), an orientation change disarms the raised
touch card (a flat-but-armed card committed on one tap) and rescales
cards already on the table, and a failed mouse click disarms too.

Integration: a fifth join bounces off a full table; e2e: a stale
invite recovers to the start screen with the join button gone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ton redesign

Abandonment is no longer an instant abort. When a seat is lost for good
(an explicit leave, or a reconnect grace running out) and other humans
remain, the table is told who abandoned (seat-abandoned) and a panel
asks the survivors: replace the seat with a bot, or end the match.
Replacing converts the seat's shared user struct in place — new bot
identity and socket, same hand and channel — re-deals it its cards
privately, re-asks any pending request, and broadcasts seat-replaced so
every client updates its seat names; the match simply plays on, with
host duties (the between-hands deal) passing to the next human if the
host's seat was the one botted. The last human leaving closes the match
outright. Bots honour a must-play-trump re-ask, covering a takeover
right after the human called the exposure.

The "open tables" list is now live: every room change (create, join,
leave, kick, bot seated, game started, disconnect cleanup) pushes
active-rooms to all connected players who are not seated anywhere, and
a dead session clears its stale list instead of freezing it.

The emote trigger moved out of the corner-text row to a round,
always-visible button at the right edge beside the hand — where a thumb
rests — opening a vertical emoji column above it.

Integration: the full abandonment arc (leave noticed while another seat
is being waited on, bot replacement, the hand completing with the bot
bidding for real, a second abandonment answered with close-game), the
old disconnect scenarios answering the new gate, and live room pushes
to a watching connection. e2e: test-match now runs guest-leave → host
replaces with a bot → hand 3 vs three bots → last-human close.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
storage.rkt: one SQLite file (Racket db lib) behind one async-channel
worker — the single-machine, single-writer architecture's natural
database. Three tables: live_matches holds a resume row per running
match, rewritten before every hand and at each hand's end, so a crash
or deploy resumes at the top of the interrupted hand (never replaying a
result the table already saw); matches is the history; players feeds
the leaderboard, keyed on the email's stable id suffix so renames keep
a player's record, with bots never written. Every call no-ops cleanly
when storage is off, so the game never depends on the disk.

On boot the server restores each resume row as a running match: human
seats come back as ghosts — registered, inside their reconnect grace,
claimable by the auto-rejoining browser that owns the email — and bot
seats get fresh bots; an unclaimed match aborts itself when the grace
runs out. The client closes the loop: a socket that dies while a match
is stored retries its rejoin every few seconds until the snapshot
restores the table or the server answers no-running-game. Verified end
to end by test-restart.mjs (local-only e2e): kill the dev server mid
hand 2, restart it, and the page finds its way back into the same match
with the score carried, then plays the re-dealt hand out.

The start screen gains a standings panel — (get-leaderboard) returns
the completed-match table, fetched over a throwaway session when none
exists. Integration covers the resume row tracking the gate, an
abort wiping it, a synthetic-row restore rejoined mid-auction with
carried scores and a fresh bot in the bot seat, and the leaderboard
listing all four humans after a completed match.

Fly: a 1GB volume mounts at /data with GAME-DB=/data/game.db; the image
adds libsqlite3. CI runs the storage suite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The dockerignore whitelist silently dropped storage.rkt from the build
context (the remote build failed on COPY), and Racket's block-buffered
stdout made fly logs useless during the restart investigation — the
whole second life of the process sat in an unflushed buffer. Logs now
flush per line. Verified live: a fly machine restart mid-hand resumes
the match — the page auto-rejoins with the score carried and plays the
re-dealt hand out on production.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AGENTS.md was a three-day-old parallel snapshot that already disagreed
with reality (no storage.rkt, no matches, no reconnection, two of the
eight browser suites); two repo guides can only drift, so it is now a
symlink and CLAUDE.md is the single source of truth for every tool.

CLAUDE.md fixes: four backend files, not three; test-match's leave path
describes the abandon panel and bot replacement instead of the long-gone
abort-everyone behavior; lobby-controls and play-multiplayer mention
what they actually cover now (standings, invite links, emotes); the
debug bridge list gains resolveAbandon and standings; server env vars
(GAME-DB, MATCH-TARGET, RECONNECT-GRACE, GAME-PORT) are gathered in one
place; the storage suite joins the unit-test layer and test-restart the
script list. The Concurrency Model section no longer describes the
original blocks-on-channel-get design — it documents the 1s sync/timeout
polls with the liveness/grace/abandonment checks, the one-shared-mutable
user-struct rule, and, finally written down where agents will read it,
the custodian rule: long-lived threads spawned from dispatch die with
the websocket unless parameterized onto the detached custodians.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A first-to-6 match is a real evening of cards, so the host now picks
the table type when creating it: Classic (first to 6, still the
default) or Quick (first to 2 — a ten-minute match). The target is
per-room: (make-room host name [target]) carries it (older clients
omit it and get classic; the server clamps junk to 6), room-members
and active-rooms broadcast it so every lobby and the table browser say
what a table plays to, run-match and the match-state thread it through
hand-result/match-over/rejoin snapshots, and it rides the resume row
so a restored match keeps its type. MATCH-TARGET stays as the test
override. Integration covers the whole path on the wire — lobby
broadcast, browser list, hand-result, resume row, and a restored
synthetic row keeping target 5 — and the lobby-controls e2e creates a
quick table through the real picker.

Mobile overlaps: the HUD strip now fits a 360px screen on one line
(13px, tighter dividers, nowrap); a long status (disconnect notices)
wraps inside a 58vw left column instead of running under the
right-aligned trump indicator; the abandon panel sits clear of the
between-hands result panel; the leaderboard and help panels cap their
height and scroll; and short phones scroll the start column instead
of clipping it (safe-centered flex).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The table-type picker no longer sits on the start screen. Clicking
"Create a table" opens a centered question panel (Classic — first to 6,
Quick — first to 2) with a back link; choosing creates the table.
E2e scripts updated to answer the question; lobby-controls also checks
the panel opens and can be dismissed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- bid/trump panels top-anchor on phones: they sat exactly on the hand
  fan, hiding the four dealt cards while choosing trump
- lobby title drops below the corner Close/Leave button on phones
  instead of running under it; lobby panel near-opaque so the 3D seat
  markers stop bleeding through
- match over now shows a trophy/leaf icon, final score, target, hands
  played, and per-hand swing chips (tracked in the reducer, snapshot
  replay safe); Play again becomes Back to home
- every button press ticks (pointerdown), and the match end plays a
  proper fanfare instead of the per-hand result sound

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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