fix(engine,wasm,web): detect no-legal-moves correctly and surface banner
Build and Deploy / build-and-push (push) Successful in 4m24s
Build and Deploy / build-and-push (push) Successful in 4m24s
Engine: replace broken has_legal_moves loop (which checked buried mid-column cards without sequence validation) with a delegation to possible_instructions(), mirroring the hint system's logic exactly. WASM: add has_moves: bool to GameSnapshot, computed in snap() using the same stock/waste/possible_instructions check so the web client gets the flag in every state update at no extra round-trip cost. Web: show a non-blocking no-moves banner (slide-up toast) with Undo and New Game actions when has_moves is false and the game is not won. Banner hides automatically once a move restores legal play (e.g. after undo). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1045,9 +1045,7 @@ pub fn record_replay_on_win(
|
||||
/// previous heuristic incorrectly did (Quat hit this with 4 cards
|
||||
/// remaining and the game just sat there).
|
||||
pub fn has_legal_moves(game: &GameState) -> bool {
|
||||
use solitaire_core::card::Card;
|
||||
use solitaire_core::pile::PileType;
|
||||
use solitaire_core::rules::{can_place_on_foundation, can_place_on_tableau};
|
||||
|
||||
// Drawing from a non-empty stock, and recycling a non-empty waste back to
|
||||
// stock, are always legal moves in standard Klondike (unlimited recycles).
|
||||
@@ -1058,40 +1056,14 @@ pub fn has_legal_moves(game: &GameState) -> bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Stock and waste exhausted — check whether any visible card can be placed.
|
||||
let mut sources: Vec<Card> = Vec::new();
|
||||
// Top waste card (waste is empty here, but included for completeness).
|
||||
if let Some(p) = game.piles.get(&PileType::Waste)
|
||||
&& let Some(top) = p.cards.last()
|
||||
{
|
||||
sources.push(top.clone());
|
||||
}
|
||||
// Any face-up card in a tableau column can be the base of a movable run.
|
||||
for i in 0..7_usize {
|
||||
if let Some(t) = game.piles.get(&PileType::Tableau(i)) {
|
||||
for card in t.cards.iter().filter(|c| c.face_up) {
|
||||
sources.push(card.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for card in &sources {
|
||||
for slot in 0..4_u8 {
|
||||
if let Some(dest) = game.piles.get(&PileType::Foundation(slot))
|
||||
&& can_place_on_foundation(card, dest)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for i in 0..7_usize {
|
||||
if let Some(dest) = game.piles.get(&PileType::Tableau(i))
|
||||
&& can_place_on_tableau(card, dest)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
// Stock and waste both exhausted — delegate to the authoritative move
|
||||
// enumeration in core, which validates tableau sequence structure and
|
||||
// foundation placement correctly. The previous hand-rolled loop only
|
||||
// checked can_place_on_tableau(card, dest) for individual face-up cards
|
||||
// without verifying that the cards above them form a valid alternating run,
|
||||
// causing false positives when a useful-looking card was buried under an
|
||||
// invalid sequence.
|
||||
!game.possible_instructions().is_empty()
|
||||
}
|
||||
|
||||
/// After each `StateChangedEvent`, check if the game has no legal moves.
|
||||
|
||||
Reference in New Issue
Block a user