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 <noreply@anthropic.com>
This commit is contained in:
Aaron D. Lee 2026-01-27 19:02:25 -05:00
parent 0c8d2b4a9c
commit 15135c404e
4 changed files with 62 additions and 0 deletions

View File

@ -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)

View File

@ -281,6 +281,7 @@
<span id="discard-content"></span>
</div>
<button id="discard-btn" class="btn btn-small hidden">Discard</button>
<button id="cancel-draw-btn" class="btn btn-small btn-secondary hidden">Put Back</button>
<button id="skip-flip-btn" class="btn btn-small btn-secondary hidden">Skip Flip</button>
<button id="knock-early-btn" class="btn btn-small btn-danger hidden">Knock!</button>
</div>

View File

@ -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.

View File

@ -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