From f17f85ebd914eb7befdceaa221d3316af91c9651 Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 7 Jul 2026 18:49:40 +0200 Subject: [PATCH] Trik: zmetanie po kazdom stichu, ovalne meno, dohrata hra mizne z lobby MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zmetacia animacia doteraz dobehla len na konci kola/serie (dlha pauza) -- mid-round ju hned prerusila dalsia zahrata karta, lebo `finishing` zavisela na activeCards === 0. Teraz drzi kopku v strede az do konca animacie a zablokuje hranie karty, kym sa nezmetie; boot-guard zabrani falosnej animacii pri reconnecte na uz rozohratu hru. PlayerCircle: ovalne oramovanie mena teraz rastie s dlzkou mena (fit-content + strop + ellipsis) namiesto pevnej sirky. api: hra dohrata do konca (4 serie) sa hned vytrati z lobby zoznamu, namiesto toho aby visela navzdy ako "Začatá"/"Pokračovať" aj ked uz ma v DB ended_at. Co-Authored-By: Claude Opus 4.8 --- api/__init__.py | 14 +++++++- frontend/src/components/PlayerCircle.tsx | 17 +++++++--- frontend/src/pages/GameTable.tsx | 43 ++++++++++++++++++------ 3 files changed, 58 insertions(+), 16 deletions(-) diff --git a/api/__init__.py b/api/__init__.py index 2f95570..9ba14f1 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -214,7 +214,13 @@ class CardStatusEncoder(JSONEncoder): def public_games() -> list: - """Public lobby view — no sids, no reconnect tokens.""" + """Public lobby view — no sids, no reconnect tokens. + + A game that finished naturally (all 4 series played out) stays in the + `games` dict for reconnect purposes (e.g. a reload while still on the + GameOver screen), but it has nothing left to offer the lobby — drop it + here rather than have it linger forever as a "started"/resumable entry. + """ return [ { "gid": g.gid, @@ -231,6 +237,7 @@ def public_games() -> list: ], } for g in games.values() + if g.bridzik_core is None or not g.bridzik_core.is_completed() ] @@ -704,6 +711,11 @@ async def play_card(sid, card_key): await send_game_status(game.gid) for player in game.players: await send_player_cards(game.gid, player.order, player.sid) + # A naturally-finished game (all 4 series played out) has nothing left to + # offer the lobby -- refresh the list so it drops out immediately instead + # of lingering as "started"/resumable until someone happens to leave it. + if core.is_completed(): + await broadcast_lobby() # --- history (read-only) -------------------------------------------------- diff --git a/frontend/src/components/PlayerCircle.tsx b/frontend/src/components/PlayerCircle.tsx index 0924ee0..da82c35 100644 --- a/frontend/src/components/PlayerCircle.tsx +++ b/frontend/src/components/PlayerCircle.tsx @@ -12,25 +12,34 @@ interface Props { export default function PlayerCircle({ name, won, guess, active, size = 52 }: Props) { const nameFont = Math.max(9, Math.round(size * 0.17)); const valueFont = Math.round(size * 0.32); - // Oval: width = size, height a touch shorter so it reads as an ellipse. + // Oval: height a touch shorter than size so it reads as an ellipse. Width + // starts at `size` (a circle for short names) but grows with the name via + // fit-content + padding, up to a cap beyond which the name is ellipsised + // rather than wrapping (wrapping would break the fixed height/oval shape). const height = Math.round(size * 0.78); + const hPad = Math.round(size * 0.16); + const maxWidth = Math.round(size * 2); return (
{ + if (booted.current || !gameStatus) return; + booted.current = true; + if (previousStashKey) setDismissedKey(previousStashKey); + }, [gameStatus, previousStashKey]); + useEffect(() => { if (!previousStashKey) return; setCollecting(false); @@ -83,13 +94,15 @@ export default function GameTable() { const myTurnToPlay = isPlayPhase && active_player === myOrder; const activeCards = active_stash ? Object.keys(active_stash.cards).length : 0; - // Once a player leads, that live trick always wins the centre. Otherwise, if a - // just-completed trick hasn't been swept away yet, keep it face-up. Reading - // `previousStash` synchronously (rather than a state set in an effect) means - // the pile never blinks to empty on the frame the 4th card lands — the last - // card simply joins the three already there, then the whole pile is collected. - const finishing = - previousStashKey !== null && previousStashKey !== dismissedKey && activeCards === 0; + // A just-completed trick that hasn't been swept away yet always wins the centre + // — even once the winner has already led the next trick. The engine reveals that + // lead card (and, at a round boundary, the next bidding phase) the instant the + // 4th card lands, so without holding the pile here the sweep would be cut off + // after every trick. Reading `previousStash` synchronously (rather than a state + // set in an effect) also means the pile never blinks to empty on the frame the + // 4th card lands — the last card simply joins the three already there, then the + // whole pile is collected before the next trick takes over. + const finishing = previousStashKey !== null && previousStashKey !== dismissedKey; const displayedStash: StashData | null = finishing ? previousStash : activeCards > 0 && active_stash @@ -102,7 +115,11 @@ export default function GameTable() { ? COLLECT_BY_OFFSET[(stashWinner(previousStash) - myOrder + 4) % 4] : null; - const playableKeys = myTurnToPlay && active_stash + // Block play until the previous trick's sweep animation has finished — otherwise + // the winner could lead the next card while the pile is still visibly clearing. + const canPlayNow = myTurnToPlay && !finishing; + + const playableKeys = canPlayNow && active_stash ? computePlayable(hand, active_stash.cards[String(active_stash.first_player)]?.color ?? null) : undefined; @@ -185,7 +202,11 @@ export default function GameTable() { ); // Center of the oval: trick during play, guess controls during bidding. - const ovalContent = isPlayPhase ? ( + // `finishing` also keeps the trick on screen while the round's *last* stash is + // swept away: the engine advances to the next round's bidding the instant the + // 4th card lands, so `isPlayPhase` flips to false immediately — without this, + // that final trick would vanish straight into the guess controls with no sweep. + const ovalContent = isPlayPhase || finishing ? (
@@ -222,7 +243,7 @@ export default function GameTable() { ); const handArea = ( - + ); // ── DESKTOP LAYOUT ───────────────────────────────────────────────