fix(engine): double-click moves exactly the clicked run (#158)
Test / test (pull_request) Successful in 36m48s
Test / test (pull_request) Successful in 36m48s
Double-click/tap auto-move previously tried the run's top card alone before the full run headed by the clicked card, so a top card with a foundation move hijacked the intended whole-stack move. Both handlers now share auto_move_for_run: a lone top card goes foundation-first, a multi-card run moves whole to a tableau or not at all. The double-click key is now the clicked card, so two clicks on different cards of the same stack no longer register as a double-click. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -429,6 +429,118 @@ fn best_tableau_destination_for_stack_returns_none_when_no_legal_move() {
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// auto_move_for_run pure-function tests (issue #158)
|
||||
// -----------------------------------------------------------------------
|
||||
//
|
||||
// These need real positions — `can_move_cards` validates against the live
|
||||
// session, not the `set_test_*` overlays. Seeds 51 and 145 both deal an
|
||||
// Ace and its Two on tableau tops plus an opposite-color Three elsewhere,
|
||||
// letting two moves build a face-up [Three, Two] run whose top card is
|
||||
// foundation-eligible (the bait the pre-#158 code would take).
|
||||
|
||||
/// Deal `seed`, send the Ace on tableau 1 to its foundation, then stack the
|
||||
/// matching Two from `two_from` onto the opposite-color Three on `run_on`.
|
||||
/// Returns the game with a 2-card face-up run on `run_on`.
|
||||
fn deal_run_with_foundation_bait(seed: u64, two_from: Tableau, run_on: Tableau) -> GameState {
|
||||
let mut game = GameState::new(seed, DrawStockConfig::DrawOne);
|
||||
let (ace, _) = game
|
||||
.pile(KlondikePile::Tableau(Tableau::Tableau1))
|
||||
.last()
|
||||
.cloned()
|
||||
.expect("seed deals a card on tableau 1");
|
||||
let foundation = best_destination(&ace, &game).expect("ace has a foundation home");
|
||||
game.move_cards(KlondikePile::Tableau(Tableau::Tableau1), foundation, 1)
|
||||
.expect("ace moves to foundation");
|
||||
game.move_cards(
|
||||
KlondikePile::Tableau(two_from),
|
||||
KlondikePile::Tableau(run_on),
|
||||
1,
|
||||
)
|
||||
.expect("two stacks onto three");
|
||||
game
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_move_for_run_moves_exact_clicked_run_not_top_card() {
|
||||
let game = deal_run_with_foundation_bait(51, Tableau::Tableau6, Tableau::Tableau5);
|
||||
let run_pile = KlondikePile::Tableau(Tableau::Tableau5);
|
||||
let cards = game.pile(run_pile);
|
||||
let (top, _) = cards.last().cloned().expect("run pile has cards");
|
||||
let (clicked, _) = cards[cards.len() - 2].clone();
|
||||
|
||||
// The bait: the lone top card has a foundation move available.
|
||||
assert!(
|
||||
matches!(
|
||||
best_destination(&top, &game),
|
||||
Some(KlondikePile::Foundation(_))
|
||||
),
|
||||
"precondition: run top card must be foundation-eligible"
|
||||
);
|
||||
|
||||
// Clicking the run base must move exactly the 2-card run to a tableau.
|
||||
match auto_move_for_run(&clicked, &run_pile, &game, 2) {
|
||||
Some((KlondikePile::Tableau(dest), 2)) => assert_ne!(dest, Tableau::Tableau5),
|
||||
other => panic!("expected a whole-run tableau move, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_move_for_run_rejects_when_clicked_run_cannot_move() {
|
||||
let game = deal_run_with_foundation_bait(145, Tableau::Tableau4, Tableau::Tableau7);
|
||||
let run_pile = KlondikePile::Tableau(Tableau::Tableau7);
|
||||
let cards = game.pile(run_pile);
|
||||
let (top, _) = cards.last().cloned().expect("run pile has cards");
|
||||
let (clicked, _) = cards[cards.len() - 2].clone();
|
||||
|
||||
assert!(
|
||||
matches!(
|
||||
best_destination(&top, &game),
|
||||
Some(KlondikePile::Foundation(_))
|
||||
),
|
||||
"precondition: run top card must be foundation-eligible"
|
||||
);
|
||||
|
||||
// The 2-card run has no legal home — the top card's foundation move
|
||||
// must NOT be taken as a substitute.
|
||||
assert_eq!(
|
||||
auto_move_for_run(&clicked, &run_pile, &game, 2),
|
||||
None,
|
||||
"an immovable clicked run must not fall back to a top-card move"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_move_for_run_single_card_prefers_foundation() {
|
||||
// Seed 51 after the Ace reaches the foundation: the Two on tableau 6
|
||||
// is a lone face-up card that could go to the foundation OR onto the
|
||||
// Three on tableau 5. Foundation must win.
|
||||
let mut game = GameState::new(51, DrawStockConfig::DrawOne);
|
||||
let (ace, _) = game
|
||||
.pile(KlondikePile::Tableau(Tableau::Tableau1))
|
||||
.last()
|
||||
.cloned()
|
||||
.expect("seed 51 deals a card on tableau 1");
|
||||
let foundation = best_destination(&ace, &game).expect("ace has a foundation home");
|
||||
game.move_cards(KlondikePile::Tableau(Tableau::Tableau1), foundation, 1)
|
||||
.expect("ace moves to foundation");
|
||||
|
||||
let two_pile = KlondikePile::Tableau(Tableau::Tableau6);
|
||||
let (two, _) = game.pile(two_pile).last().cloned().expect("two on top");
|
||||
assert!(
|
||||
game.can_move_cards(&two_pile, &KlondikePile::Tableau(Tableau::Tableau5), 1),
|
||||
"precondition: a tableau destination also exists"
|
||||
);
|
||||
|
||||
assert!(
|
||||
matches!(
|
||||
auto_move_for_run(&two, &two_pile, &game, 1),
|
||||
Some((KlondikePile::Foundation(_), 1))
|
||||
),
|
||||
"a lone top card still prefers the foundation"
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Task #28 — find_hint pure-function tests
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user