From 15135c404eceb317fbab594b1a5d27aa3031d805 Mon Sep 17 00:00:00 2001 From: "Aaron D. Lee" Date: Tue, 27 Jan 2026 19:02:25 -0500 Subject: [PATCH] Add "Put Back" button to cancel accidental discard draws When you accidentally click the discard pile, you can now put the card back instead of being forced to swap. The "Put Back" button appears only when you've drawn from the discard pile. Co-Authored-By: Claude Opus 4.5 --- client/app.js | 14 ++++++++++++++ client/index.html | 1 + server/game.py | 39 +++++++++++++++++++++++++++++++++++++++ server/main.py | 8 ++++++++ 4 files changed, 62 insertions(+) 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