From ea9dd848fd32bf0207a81f2b3f6ebb2af46f8dc2 Mon Sep 17 00:00:00 2001 From: funman300 Date: Tue, 19 May 2026 13:40:32 -0700 Subject: [PATCH] fix(multi): resolve 14 bugs from second comprehensive review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Core (solitaire_core): - fix(scoring): apply -15 penalty for Foundation→Tableau moves when take_from_foundation is enabled; update test - fix(solver): is_won() validates full Ace→King suit sequence, not just card count — prevents hint system from emitting invalid paths Engine — animation / layout: - fix(animation): guard CardAnim advance against duration=0 to prevent NaN-poisoned Transform (analogous to CardAnimation's instant-snap path) - fix(card_plugin): align TABLEAU_FAN_FRAC (0.25→0.18) and TABLEAU_FACEDOWN_FAN_FRAC (0.20→0.14) with layout.rs so the initial layout and first dynamic update produce identical fan spacing - fix(layout): update tableau_fan_frac doc comment from 0.25→0.18 Engine — ECS / modal guards: - fix(auto_complete): drive_auto_complete now checks PausedResource so cooldown does not tick while paused (prevents instant-move on unpause) - fix(play_by_seed): handle_open_dialog checks global ModalScrim guard to prevent stacking over an existing modal - fix(win_summary): spawn_win_summary_after_delay checks global ModalScrim guard; collect_session_achievements uses .next() not .last() to avoid draining the new_games stream Engine — message registration: - fix(leaderboard): register InfoToastEvent in LeaderboardPlugin::build so opt-in/opt-out toasts work under MinimalPlugins - fix(replay_playback): register StateChangedEvent in ReplayPlaybackPlugin::build to prevent panic when used standalone Security: - fix(sync_setup): zero password SyncFieldBuffer immediately after spawning auth task — credential must not linger in ECS components Server: - fix(auth): replace MIME contains-chain with exact match for avatar upload; removes illusory starts_with guard and dead ALLOWED_IMAGE_TYPES Co-Authored-By: Claude Sonnet 4.6 --- solitaire_core/src/scoring.rs | 17 +++++++------- solitaire_core/src/solver.rs | 11 +++++++-- solitaire_engine/src/animation_plugin.rs | 5 ++++ solitaire_engine/src/auto_complete_plugin.rs | 5 ++++ solitaire_engine/src/card_plugin.rs | 8 ++++--- solitaire_engine/src/layout.rs | 2 +- solitaire_engine/src/leaderboard_plugin.rs | 1 + solitaire_engine/src/play_by_seed_plugin.rs | 5 ++-- solitaire_engine/src/replay_playback.rs | 1 + solitaire_engine/src/sync_setup_plugin.rs | 10 +++++++- solitaire_engine/src/win_summary_plugin.rs | 7 +++--- solitaire_server/src/auth.rs | 24 ++++++-------------- 12 files changed, 59 insertions(+), 37 deletions(-) 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