diff --git a/client/app.js b/client/app.js index ada4f85..c430e88 100644 --- a/client/app.js +++ b/client/app.js @@ -206,6 +206,7 @@ class GolfGame { this.discard = document.getElementById('discard'); this.discardContent = document.getElementById('discard-content'); this.discardBtn = document.getElementById('discard-btn'); + this.cancelDrawBtn = document.getElementById('cancel-draw-btn'); this.skipFlipBtn = document.getElementById('skip-flip-btn'); this.knockEarlyBtn = document.getElementById('knock-early-btn'); this.playerCards = document.getElementById('player-cards'); @@ -232,6 +233,7 @@ class GolfGame { this.deck.addEventListener('click', () => { this.drawFromDeck(); }); this.discard.addEventListener('click', () => { this.drawFromDiscard(); }); this.discardBtn.addEventListener('click', () => { this.playSound('card'); this.discardDrawn(); }); + this.cancelDrawBtn.addEventListener('click', () => { this.playSound('click'); this.cancelDraw(); }); this.skipFlipBtn.addEventListener('click', () => { this.playSound('click'); this.skipFlip(); }); this.knockEarlyBtn.addEventListener('click', () => { this.playSound('success'); this.knockEarly(); }); this.nextRoundBtn.addEventListener('click', () => { this.playSound('click'); this.nextRound(); }); @@ -768,6 +770,14 @@ class GolfGame { this.hideToast(); } + cancelDraw() { + if (!this.drawnCard) return; + this.send({ type: 'cancel_draw' }); + this.drawnCard = null; + this.hideDrawnCard(); + this.hideToast(); + } + swapCard(position) { if (!this.drawnCard) return; this.send({ type: 'swap', position }); @@ -1626,6 +1636,7 @@ class GolfGame { // Restore discard pile to show actual top card (handled by renderGame) this.discard.classList.remove('holding'); this.discardBtn.classList.add('hidden'); + this.cancelDrawBtn.classList.add('hidden'); } isRedSuit(suit) { @@ -1851,9 +1862,12 @@ class GolfGame { if (this.drawnCard && !this.gameState.can_discard) { this.discardBtn.disabled = true; this.discardBtn.classList.add('disabled'); + // Show cancel button when drawn from discard (can put it back) + this.cancelDrawBtn.classList.remove('hidden'); } else { this.discardBtn.disabled = false; this.discardBtn.classList.remove('disabled'); + this.cancelDrawBtn.classList.add('hidden'); } // Show/hide skip flip button (only when flip is optional in endgame mode) diff --git a/client/index.html b/client/index.html index 4a79d91..8716e6d 100644 --- a/client/index.html +++ b/client/index.html @@ -281,6 +281,7 @@ + diff --git a/server/game.py b/server/game.py index 0b86cb6..5ce1f1a 100644 --- a/server/game.py +++ b/server/game.py @@ -1051,6 +1051,45 @@ class Game: return False return True + def cancel_discard_draw(self, player_id: str) -> bool: + """ + Cancel a draw from the discard pile, putting the card back. + + Only allowed when the card was drawn from the discard pile. + This is a convenience feature to undo accidental clicks. + + Args: + player_id: ID of the player canceling. + + Returns: + True if cancel was successful, False otherwise. + """ + player = self.current_player() + if not player or player.id != player_id: + return False + + if self.drawn_card is None: + return False + + if not self.drawn_from_discard: + return False # Can only cancel discard draws + + # Put the card back on the discard pile + cancelled_card = self.drawn_card + cancelled_card.face_up = True + self.discard_pile.append(cancelled_card) + self.drawn_card = None + self.drawn_from_discard = False + + # Emit cancel event + self._emit( + "draw_cancelled", + player_id=player_id, + card={"rank": cancelled_card.rank.value, "suit": cancelled_card.suit.value}, + ) + + return True + def discard_drawn(self, player_id: str) -> bool: """ Discard the drawn card without swapping. diff --git a/server/main.py b/server/main.py index bbe9820..b34388a 100644 --- a/server/main.py +++ b/server/main.py @@ -748,6 +748,14 @@ async def websocket_endpoint(websocket: WebSocket): # Turn ended, check for CPU await check_and_run_cpu_turn(current_room) + elif msg_type == "cancel_draw": + if not current_room: + continue + + async with current_room.game_lock: + if current_room.game.cancel_discard_draw(player_id): + await broadcast_game_state(current_room) + elif msg_type == "flip_card": if not current_room: continue