Add client-side card reveal before swap and YOUR TURN badge update
Reveal face-down cards briefly (1s) before swap completes, using client-side state diffing instead of a separate server message. Local player reveals use existing card data; opponent reveals use server-sent card_revealed as a fallback. Defers incoming game_state updates during the reveal window to prevent overwrites. Also update YOUR TURN badge to cyan with suit symbols. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
215849703c
commit
7f0f580631
@ -130,6 +130,9 @@ class GameScreen(Screen):
|
|||||||
self._term_width: int = 80
|
self._term_width: int = 80
|
||||||
self._swap_flash: dict[str, int] = {} # player_id -> position of last swap
|
self._swap_flash: dict[str, int] = {} # player_id -> position of last swap
|
||||||
self._discard_flash: bool = False # discard pile just changed
|
self._discard_flash: bool = False # discard pile just changed
|
||||||
|
self._pending_reveal: dict | None = None # server-sent reveal for opponents
|
||||||
|
self._reveal_active: bool = False # reveal animation in progress
|
||||||
|
self._deferred_state: GameState | None = None # queued state during reveal
|
||||||
self._term_height: int = 24
|
self._term_height: int = 24
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
@ -170,10 +173,64 @@ class GameScreen(Screen):
|
|||||||
state_data = data.get("game_state", data)
|
state_data = data.get("game_state", data)
|
||||||
old_state = self._state
|
old_state = self._state
|
||||||
new_state = GameState.from_dict(state_data)
|
new_state = GameState.from_dict(state_data)
|
||||||
self._detect_swaps(old_state, new_state)
|
reveal = self._detect_swaps(old_state, new_state)
|
||||||
self._state = new_state
|
if reveal:
|
||||||
|
# Briefly show the old face-down card before applying new state
|
||||||
|
self._show_reveal_then_update(reveal, new_state)
|
||||||
|
elif self._reveal_active:
|
||||||
|
# A reveal is showing — queue this state for after it finishes
|
||||||
|
self._deferred_state = new_state
|
||||||
|
else:
|
||||||
|
self._state = new_state
|
||||||
|
self._full_refresh()
|
||||||
|
|
||||||
|
def _show_reveal_then_update(
|
||||||
|
self,
|
||||||
|
reveal: dict,
|
||||||
|
new_state: GameState,
|
||||||
|
) -> None:
|
||||||
|
"""Show the old card face-up for 1s, then apply the new state."""
|
||||||
|
from tui_client.models import CardData
|
||||||
|
|
||||||
|
player_id = reveal["player_id"]
|
||||||
|
position = reveal["position"]
|
||||||
|
old_card_data = reveal["card"]
|
||||||
|
|
||||||
|
# Modify current state to show old card face-up
|
||||||
|
for p in self._state.players:
|
||||||
|
if p.id == player_id and position < len(p.cards):
|
||||||
|
p.cards[position] = CardData(
|
||||||
|
suit=old_card_data.get("suit"),
|
||||||
|
rank=old_card_data.get("rank"),
|
||||||
|
face_up=True,
|
||||||
|
deck_id=old_card_data.get("deck_id"),
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
|
self._reveal_active = True
|
||||||
|
self._deferred_state = new_state
|
||||||
self._full_refresh()
|
self._full_refresh()
|
||||||
|
|
||||||
|
# After 1 second, apply the real new state
|
||||||
|
def apply_new():
|
||||||
|
self._reveal_active = False
|
||||||
|
state = self._deferred_state
|
||||||
|
self._deferred_state = None
|
||||||
|
if state:
|
||||||
|
self._state = state
|
||||||
|
self._full_refresh()
|
||||||
|
|
||||||
|
self.set_timer(1.0, apply_new)
|
||||||
|
|
||||||
|
def _handle_card_revealed(self, data: dict) -> None:
|
||||||
|
"""Server sent old card data for an opponent's face-down swap."""
|
||||||
|
# Store the reveal data so next game_state can use it
|
||||||
|
self._pending_reveal = {
|
||||||
|
"player_id": data.get("player_id"),
|
||||||
|
"position": data.get("position", 0),
|
||||||
|
"card": data.get("card", {}),
|
||||||
|
}
|
||||||
|
|
||||||
def _handle_your_turn(self, data: dict) -> None:
|
def _handle_your_turn(self, data: dict) -> None:
|
||||||
self._awaiting_flip = False
|
self._awaiting_flip = False
|
||||||
self._refresh_action_bar()
|
self._refresh_action_bar()
|
||||||
@ -458,10 +515,17 @@ class GameScreen(Screen):
|
|||||||
# Swap/discard detection
|
# Swap/discard detection
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def _detect_swaps(self, old: GameState, new: GameState) -> None:
|
def _detect_swaps(self, old: GameState, new: GameState) -> dict | None:
|
||||||
"""Compare old and new state to find which card positions changed."""
|
"""Compare old and new state to find which card positions changed.
|
||||||
|
|
||||||
|
Returns reveal info dict if a face-down card was swapped, else None.
|
||||||
|
"""
|
||||||
|
reveal = None
|
||||||
if not old or not new or not old.players or not new.players:
|
if not old or not new or not old.players or not new.players:
|
||||||
return
|
return None
|
||||||
|
|
||||||
|
# Only reveal during active play, not initial flip or round end
|
||||||
|
reveal_eligible = old.phase in ("playing", "final_turn")
|
||||||
|
|
||||||
old_map = {p.id: p for p in old.players}
|
old_map = {p.id: p for p in old.players}
|
||||||
for np in new.players:
|
for np in new.players:
|
||||||
@ -469,12 +533,30 @@ class GameScreen(Screen):
|
|||||||
if not op:
|
if not op:
|
||||||
continue
|
continue
|
||||||
for i, (oc, nc) in enumerate(zip(op.cards, np.cards)):
|
for i, (oc, nc) in enumerate(zip(op.cards, np.cards)):
|
||||||
# Card changed: was face-down and now face-up with different rank,
|
# Card changed: rank/suit differ and new card is face-up
|
||||||
# or rank/suit changed
|
if (oc.rank != nc.rank or oc.suit != nc.suit) and nc.face_up:
|
||||||
if oc.rank != nc.rank or oc.suit != nc.suit:
|
self._swap_flash[np.id] = i
|
||||||
if nc.face_up:
|
|
||||||
self._swap_flash[np.id] = i
|
# Was old card face-down? If we have its data, reveal it
|
||||||
break
|
if reveal_eligible and not oc.face_up and oc.rank and oc.suit:
|
||||||
|
# Local player — we know face-down card values
|
||||||
|
reveal = {
|
||||||
|
"player_id": np.id,
|
||||||
|
"position": i,
|
||||||
|
"card": {"rank": oc.rank, "suit": oc.suit, "deck_id": oc.deck_id},
|
||||||
|
}
|
||||||
|
elif reveal_eligible and not oc.face_up and self._pending_reveal:
|
||||||
|
# Opponent — use server-sent reveal data
|
||||||
|
pr = self._pending_reveal
|
||||||
|
if pr.get("player_id") == np.id and pr.get("position") == i:
|
||||||
|
reveal = {
|
||||||
|
"player_id": np.id,
|
||||||
|
"position": i,
|
||||||
|
"card": pr["card"],
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
self._pending_reveal = None
|
||||||
|
|
||||||
# Detect discard change (new discard top differs from old)
|
# Detect discard change (new discard top differs from old)
|
||||||
if old.discard_top and new.discard_top:
|
if old.discard_top and new.discard_top:
|
||||||
@ -488,6 +570,8 @@ class GameScreen(Screen):
|
|||||||
if self._swap_flash or self._discard_flash:
|
if self._swap_flash or self._discard_flash:
|
||||||
self.set_timer(1.0, self._clear_flash)
|
self.set_timer(1.0, self._clear_flash)
|
||||||
|
|
||||||
|
return reveal
|
||||||
|
|
||||||
def _clear_flash(self) -> None:
|
def _clear_flash(self) -> None:
|
||||||
"""Clear swap/discard flash highlights and re-render."""
|
"""Clear swap/discard flash highlights and re-render."""
|
||||||
self._swap_flash.clear()
|
self._swap_flash.clear()
|
||||||
|
|||||||
@ -50,7 +50,15 @@ class StatusBarWidget(Static):
|
|||||||
# Turn info (skip during initial flip - it's misleading)
|
# Turn info (skip during initial flip - it's misleading)
|
||||||
if state.current_player_id and state.players and state.phase != "initial_flip":
|
if state.current_player_id and state.players and state.phase != "initial_flip":
|
||||||
if state.current_player_id == self._player_id:
|
if state.current_player_id == self._player_id:
|
||||||
parts.append("[bold #ffffff on #2e7d32] YOUR TURN [/bold #ffffff on #2e7d32]")
|
parts.append(
|
||||||
|
"[on #00bcd4]"
|
||||||
|
" [bold #000000]♣[/bold #000000]"
|
||||||
|
"[bold #cc0000]♦[/bold #cc0000]"
|
||||||
|
" [bold #000000]YOUR TURN![/bold #000000] "
|
||||||
|
"[bold #000000]♠[/bold #000000]"
|
||||||
|
"[bold #cc0000]♥[/bold #cc0000]"
|
||||||
|
" [/on #00bcd4]"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
for p in state.players:
|
for p in state.players:
|
||||||
if p.id == state.current_player_id:
|
if p.id == state.current_player_id:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user