From d7ffb16df5167fbe93d0d3f690f09b759e4a464c Mon Sep 17 00:00:00 2001 From: funman300 Date: Wed, 6 May 2026 19:54:28 -0700 Subject: [PATCH] fix(engine): single-card double-click with no destination now plays the reject animation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit handle_double_click had a coverage gap. The flow was: - Priority 1: try moving the single top card to its best destination (foundation, then tableau). - Priority 2: if Priority 1 failed AND the player clicked the base of a multi-card stack, try moving the whole stack. `MoveRejectedEvent` was only fired inside the Priority 2 else-branch — so a double-click on a single card with no legal destination fell through both priorities silently: no card_invalid.wav, no shake animation on the source pile, the player got zero feedback that the click was acknowledged. The fix collapses both priorities' failure paths into one unconditional `MoveRejectedEvent` write at the end of the double-click branch. Single-card miss now plays the same feedback as multi-card-stack miss. The early `return` on each successful move keeps the rejection branch from firing on the success path. Pre-fix, a player double-clicking the 7♠ buried under a 6♥ on column 5 (no foundation slot for 7s; no tableau column accepting black 7) saw nothing happen. Post-fix, the source pile shakes and the invalid-move sound plays, exactly like a drag-and-drop rejection. Workspace: 1170 passing tests / 0 failing. cargo clippy --workspace --all-targets -- -D warnings clean. Co-Authored-By: Claude Opus 4.7 --- solitaire_engine/src/input_plugin.rs | 47 +++++++++++++++------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/solitaire_engine/src/input_plugin.rs b/solitaire_engine/src/input_plugin.rs index 07669fe..e394cf0 100644 --- a/solitaire_engine/src/input_plugin.rs +++ b/solitaire_engine/src/input_plugin.rs @@ -1310,32 +1310,37 @@ fn handle_double_click( // Priority 2: if the player clicked the base of a multi-card face-up // stack (card_ids.len() > 1), try moving the whole stack to another // tableau column. - if card_ids.len() > 1 { - let Some(bottom_card) = game.0.piles.get(&pile) - .and_then(|p| p.cards.get(stack_index)) else { return }; - if let Some((dest, count)) = best_tableau_destination_for_stack( + if card_ids.len() > 1 + && let Some(bottom_card) = game.0.piles.get(&pile) + .and_then(|p| p.cards.get(stack_index)) + && let Some((dest, count)) = best_tableau_destination_for_stack( bottom_card, &pile, &game.0, card_ids.len(), - ) { - moves.write(MoveRequestEvent { - from: pile, - to: dest, - count, - }); - } else { - // No legal destination for the stack — play the invalid-move - // sound and shake the source pile cards as feedback. - // `MoveRejectedEvent` with `from == to` routes the shake to - // the source pile (which `start_shake_anim` reads from `ev.to`). - rejected.write(MoveRejectedEvent { - from: pile.clone(), - to: pile, - count: card_ids.len(), - }); - } + ) + { + moves.write(MoveRequestEvent { + from: pile, + to: dest, + count, + }); + return; } + + // Both priorities failed — play the invalid-move sound and shake + // the source pile as feedback. `MoveRejectedEvent` with + // `from == to` routes the shake to the source pile (which + // `start_shake_anim` reads from `ev.to`). Pre-fix, this branch + // only fired for multi-card stacks, so a double-click on a + // single card with no legal destination did nothing — no + // sound, no shake. Now both single-card and stack misses get + // the same feedback. + rejected.write(MoveRejectedEvent { + from: pile.clone(), + to: pile, + count: card_ids.len(), + }); } else { // Single click — record the time. last_click.insert(top_card_id, now);