diff --git a/solitaire_core/src/scoring.rs b/solitaire_core/src/scoring.rs index c604fb8..ee340ee 100644 --- a/solitaire_core/src/scoring.rs +++ b/solitaire_core/src/scoring.rs @@ -9,9 +9,11 @@ use crate::pile::PileType; pub fn score_move(from: &PileType, to: &PileType) -> i32 { match to { PileType::Foundation(_) => 10, - PileType::Tableau(_) => { - if matches!(from, PileType::Waste) { 5 } else { 0 } - } + PileType::Tableau(_) => match from { + PileType::Waste => 5, + PileType::Foundation(_) => -15, + _ => 0, + }, _ => 0, } } @@ -71,13 +73,12 @@ mod tests { } #[test] - fn non_waste_to_tableau_scores_zero() { - // Foundation → Tableau is impossible in practice but must score 0. - assert_eq!(score_move(&PileType::Foundation(0), &PileType::Tableau(0)), 0); - // Tableau → Tableau (restack) scores 0. - assert_eq!(score_move(&PileType::Tableau(1), &PileType::Tableau(2)), 0); + fn foundation_to_tableau_penalises_fifteen() { + // Moving a card back off a foundation (take_from_foundation rule) costs -15. + assert_eq!(score_move(&PileType::Foundation(0), &PileType::Tableau(0)), -15); } + #[test] fn move_to_stock_or_waste_scores_zero() { // These destinations are illegal moves in practice, but the function diff --git a/solitaire_core/src/solver.rs b/solitaire_core/src/solver.rs index 6ad3256..f55576c 100644 --- a/solitaire_core/src/solver.rs +++ b/solitaire_core/src/solver.rs @@ -298,9 +298,16 @@ impl SolverState { } } - /// True when every foundation slot has 13 cards. + /// True when every foundation slot holds a complete Ace-through-King sequence. fn is_won(&self) -> bool { - self.foundation.iter().all(|f| f.len() == 13) + self.foundation.iter().all(|pile| { + pile.len() == 13 + && pile[0].rank == crate::card::Rank::Ace + && pile.windows(2).all(|w| { + w[0].suit == w[1].suit + && w[1].rank.value() == w[0].rank.value() + 1 + }) + }) } /// Returns the foundation slot that already claims `suit`, or the diff --git a/solitaire_engine/src/animation_plugin.rs b/solitaire_engine/src/animation_plugin.rs index 6863cd6..1a80455 100644 --- a/solitaire_engine/src/animation_plugin.rs +++ b/solitaire_engine/src/animation_plugin.rs @@ -258,6 +258,11 @@ fn advance_card_anims( anim.delay = (anim.delay - dt).max(0.0); continue; } + if anim.duration <= 0.0 { + transform.translation = anim.target; + commands.entity(entity).remove::(); + continue; + } anim.elapsed += dt; let t = (anim.elapsed / anim.duration).min(1.0); // Curved interpolation using `MotionCurve::SmoothSnap` (cubic ease-out diff --git a/solitaire_engine/src/auto_complete_plugin.rs b/solitaire_engine/src/auto_complete_plugin.rs index b3ab749..63f864b 100644 --- a/solitaire_engine/src/auto_complete_plugin.rs +++ b/solitaire_engine/src/auto_complete_plugin.rs @@ -13,6 +13,7 @@ use bevy::prelude::*; use crate::audio_plugin::{AudioState, SoundLibrary}; use crate::events::{MoveRequestEvent, StateChangedEvent}; use crate::game_plugin::GameMutation; +use crate::pause_plugin::PausedResource; use crate::resources::GameStateResource; /// Volume amplitude used for the auto-complete activation chime. @@ -111,11 +112,15 @@ fn drive_auto_complete( mut state: ResMut, game: Res, time: Res