Trik: zmetanie po kazdom stichu, ovalne meno, dohrata hra mizne z lobby
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 <noreply@anthropic.com>
This commit is contained in:
+13
-1
@@ -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) --------------------------------------------------
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
// Only the active player is highlighted (gold ring + glow) — colors come
|
||||
// from the velvet-table palette tokens (tailwind.config.js), not literals.
|
||||
className={`flex flex-col items-center justify-center rounded-[50%] ${
|
||||
className={`flex flex-col items-center justify-center rounded-full ${
|
||||
active ? 'bg-circle-active border-2 border-gold' : 'bg-circle border-[1.5px] border-gold/20'
|
||||
}`}
|
||||
style={{
|
||||
width: size,
|
||||
width: 'fit-content',
|
||||
minWidth: size,
|
||||
maxWidth,
|
||||
height,
|
||||
paddingLeft: hPad,
|
||||
paddingRight: hPad,
|
||||
boxShadow: '0 2px 10px rgba(0,0,0,.45)',
|
||||
animation: active ? 'ar 2.2s ease-in-out infinite' : undefined,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={`uppercase leading-tight text-center ${active ? 'text-gold' : 'text-green-circle'}`}
|
||||
className={`uppercase leading-tight text-center overflow-hidden text-ellipsis whitespace-nowrap max-w-full ${active ? 'text-gold' : 'text-green-circle'}`}
|
||||
style={{
|
||||
fontFamily: '"DM Sans",sans-serif',
|
||||
fontSize: nameFont,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useGameStore } from '../store/gameStore';
|
||||
import { emit } from '../lib/socket';
|
||||
@@ -46,6 +46,17 @@ export default function GameTable() {
|
||||
? `${previousStash.first_player}:${JSON.stringify(previousStash.cards)}`
|
||||
: null;
|
||||
|
||||
// On first load of an already-running game (reconnect / restore-on-startup) a
|
||||
// completed trick is already present; adopt it as "already swept" so we don't
|
||||
// replay a stale sweep over the live board. A freshly started game has no
|
||||
// completed trick at this point, so its very first trick still animates.
|
||||
const booted = useRef(false);
|
||||
useEffect(() => {
|
||||
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 ? (
|
||||
<div style={collectAnim ? { animation: `${collectAnim} ${COLLECT_MS}ms ease-in both` } : undefined}>
|
||||
<Trick stash={displayedStash} players={players} myOrder={myOrder} />
|
||||
</div>
|
||||
@@ -222,7 +243,7 @@ export default function GameTable() {
|
||||
);
|
||||
|
||||
const handArea = (
|
||||
<Hand hand={hand} myTurn={myTurnToPlay} isPlayPhase={isPlayPhase} playableKeys={playableKeys} desktop={desktop} />
|
||||
<Hand hand={hand} myTurn={canPlayNow} isPlayPhase={isPlayPhase} playableKeys={playableKeys} desktop={desktop} />
|
||||
);
|
||||
|
||||
// ── DESKTOP LAYOUT ───────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user