ea9dd848fd
Build and Deploy / build-and-push (push) Successful in 4m2s
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 <noreply@anthropic.com>
97 lines
2.7 KiB
Rust
97 lines
2.7 KiB
Rust
use crate::pile::PileType;
|
|
|
|
/// Score delta for moving cards from `from` to `to`.
|
|
///
|
|
/// Windows XP Standard scoring:
|
|
/// - +10 for any card reaching a foundation pile
|
|
/// - +5 for a waste → tableau move
|
|
/// - 0 for all other moves
|
|
pub fn score_move(from: &PileType, to: &PileType) -> i32 {
|
|
match to {
|
|
PileType::Foundation(_) => 10,
|
|
PileType::Tableau(_) => match from {
|
|
PileType::Waste => 5,
|
|
PileType::Foundation(_) => -15,
|
|
_ => 0,
|
|
},
|
|
_ => 0,
|
|
}
|
|
}
|
|
|
|
/// Score penalty applied when the player uses undo: -15.
|
|
pub fn score_undo() -> i32 {
|
|
-15
|
|
}
|
|
|
|
/// Time bonus added to the score on a win: `700_000 / elapsed_seconds`.
|
|
/// Returns 0 when `elapsed_seconds` is 0 to avoid division by zero.
|
|
pub fn compute_time_bonus(elapsed_seconds: u64) -> i32 {
|
|
if elapsed_seconds == 0 {
|
|
return 0;
|
|
}
|
|
(700_000u64 / elapsed_seconds).min(i32::MAX as u64) as i32
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn move_to_foundation_scores_ten() {
|
|
assert_eq!(score_move(&PileType::Waste, &PileType::Foundation(2)), 10);
|
|
assert_eq!(score_move(&PileType::Tableau(0), &PileType::Foundation(0)), 10);
|
|
}
|
|
|
|
#[test]
|
|
fn waste_to_tableau_scores_five() {
|
|
assert_eq!(score_move(&PileType::Waste, &PileType::Tableau(3)), 5);
|
|
}
|
|
|
|
#[test]
|
|
fn tableau_to_tableau_scores_zero() {
|
|
assert_eq!(score_move(&PileType::Tableau(0), &PileType::Tableau(1)), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn undo_penalty_is_negative_fifteen() {
|
|
assert_eq!(score_undo(), -15);
|
|
}
|
|
|
|
#[test]
|
|
fn time_bonus_at_100_seconds() {
|
|
assert_eq!(compute_time_bonus(100), 7000);
|
|
}
|
|
|
|
#[test]
|
|
fn time_bonus_at_zero_is_zero() {
|
|
assert_eq!(compute_time_bonus(0), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn time_bonus_at_one_second() {
|
|
assert_eq!(compute_time_bonus(1), 700_000);
|
|
}
|
|
|
|
#[test]
|
|
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
|
|
// must not panic and should return 0.
|
|
assert_eq!(score_move(&PileType::Waste, &PileType::Stock), 0);
|
|
assert_eq!(score_move(&PileType::Waste, &PileType::Waste), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn time_bonus_is_capped_at_i32_max_for_huge_values() {
|
|
// Very short elapsed time would overflow without the .min() guard.
|
|
let bonus = compute_time_bonus(1);
|
|
assert!(bonus >= 0, "time bonus must be non-negative after u64→i32 cast");
|
|
}
|
|
}
|