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