Fix responsive game table layout - #1
Draft
antash-mishra wants to merge 47 commits into
Draft
Conversation
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>
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>
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.
What changed
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 passedcd app && npm run build