refactor: replace local DrawMode with upstream klondike::DrawStockConfig (#82)

DrawMode was a 1:1 mirror of klondike::DrawStockConfig (DrawOne/DrawThree).
Delete it and use the upstream type everywhere; re-export DrawStockConfig from
solitaire_core. config_for assigns draw_stock directly and draw_mode() returns
session.config().inner.draw_stock.

Serde is unchanged — DrawStockConfig serialises to the same "DrawOne"/"DrawThree"
named variants, so persisted game_state.json / replay JSON stay byte-compatible
(no migration). Field/method/variable names containing draw_mode are unchanged.

35 files, mechanical type swap across all crates. Implemented via a multi-agent
workflow (core → per-crate consumers → verify). cargo test --workspace and
clippy --workspace --all-targets -- -D warnings green.

Closes #82

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
funman300
2026-06-11 16:01:11 -07:00
parent d045781119
commit 5c992cbdca
35 changed files with 257 additions and 274 deletions
@@ -19,7 +19,7 @@
//! --per-tier Seeds to emit per tier (default 40) //! --per-tier Seeds to emit per tier (default 40)
//! --help Print this message //! --help Print this message
use solitaire_core::DrawMode; use solitaire_core::DrawStockConfig;
use solitaire_core::game_state::GameState; use solitaire_core::game_state::GameState;
// Budget boundaries defining each tier. A seed belongs to the lowest tier // Budget boundaries defining each tier. A seed belongs to the lowest tier
@@ -74,7 +74,7 @@ fn main() {
std::process::exit(1); std::process::exit(1);
} }
let draw_mode = DrawMode::DrawOne; let draw_mode = DrawStockConfig::DrawOne;
let num_tiers = BUDGETS.len(); let num_tiers = BUDGETS.len();
let mut buckets: Vec<Vec<u64>> = vec![Vec::with_capacity(per_tier); num_tiers]; let mut buckets: Vec<Vec<u64>> = vec![Vec::with_capacity(per_tier); num_tiers];
let mut tried: u64 = 0; let mut tried: u64 = 0;
+2 -2
View File
@@ -17,7 +17,7 @@
//! --count Number of Winnable seeds to emit (default 75) //! --count Number of Winnable seeds to emit (default 75)
//! --help Print this message //! --help Print this message
use solitaire_core::DrawMode; use solitaire_core::DrawStockConfig;
use solitaire_core::game_state::GameState; use solitaire_core::game_state::GameState;
use solitaire_core::{DEFAULT_SOLVE_MOVES_BUDGET, DEFAULT_SOLVE_STATES_BUDGET}; use solitaire_core::{DEFAULT_SOLVE_MOVES_BUDGET, DEFAULT_SOLVE_STATES_BUDGET};
@@ -68,7 +68,7 @@ fn main() {
std::process::exit(1); std::process::exit(1);
} }
let draw_mode = DrawMode::DrawOne; let draw_mode = DrawStockConfig::DrawOne;
let mut found: Vec<u64> = Vec::with_capacity(count); let mut found: Vec<u64> = Vec::with_capacity(count);
let mut tried: u64 = 0; let mut tried: u64 = 0;
let mut seed = start; let mut seed = start;
+22 -25
View File
@@ -1,6 +1,6 @@
use crate::error::MoveError; use crate::error::MoveError;
use crate::klondike_adapter::{ use crate::klondike_adapter::{
DrawMode, KlondikeAdapter, SavedInstruction, KlondikeAdapter, SavedInstruction,
foundation_from_slot as adapter_foundation_from_slot, foundation_from_slot as adapter_foundation_from_slot,
skip_cards_from_count as adapter_skip_cards_from_count, skip_cards_from_count as adapter_skip_cards_from_count,
tableau_from_index as adapter_tableau_from_index, tableau_from_index as adapter_tableau_from_index,
@@ -101,7 +101,7 @@ pub enum GameMode {
/// `KlondikeInstruction` serde, which produces named enum variants. /// `KlondikeInstruction` serde, which produces named enum variants.
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
struct PersistedGameState { struct PersistedGameState {
pub draw_mode: DrawMode, pub draw_mode: DrawStockConfig,
pub mode: GameMode, pub mode: GameMode,
pub elapsed_seconds: u64, pub elapsed_seconds: u64,
pub seed: u64, pub seed: u64,
@@ -134,7 +134,7 @@ enum AnyInstruction {
/// them. /// them.
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
struct PersistedGameStateIn { struct PersistedGameStateIn {
pub draw_mode: DrawMode, pub draw_mode: DrawStockConfig,
#[serde(default)] #[serde(default)]
pub mode: GameMode, pub mode: GameMode,
pub elapsed_seconds: u64, pub elapsed_seconds: u64,
@@ -306,12 +306,12 @@ impl<'de> Deserialize<'de> for GameState {
impl GameState { impl GameState {
/// Creates a new Classic-mode game dealt from the given seed and draw mode. /// Creates a new Classic-mode game dealt from the given seed and draw mode.
pub fn new(seed: u64, draw_mode: DrawMode) -> Self { pub fn new(seed: u64, draw_mode: DrawStockConfig) -> Self {
Self::new_with_mode(seed, draw_mode, GameMode::Classic) Self::new_with_mode(seed, draw_mode, GameMode::Classic)
} }
/// Creates a new game with an explicit `GameMode`. /// Creates a new game with an explicit `GameMode`.
pub fn new_with_mode(seed: u64, draw_mode: DrawMode, mode: GameMode) -> Self { pub fn new_with_mode(seed: u64, draw_mode: DrawStockConfig, mode: GameMode) -> Self {
Self { Self {
mode, mode,
elapsed_seconds: 0, elapsed_seconds: 0,
@@ -325,11 +325,8 @@ impl GameState {
/// Whether the player draws one or three cards from the stock per turn. /// Whether the player draws one or three cards from the stock per turn.
/// Derived from the underlying session config (set once at deal time). /// Derived from the underlying session config (set once at deal time).
pub fn draw_mode(&self) -> DrawMode { pub fn draw_mode(&self) -> DrawStockConfig {
match self.session.config().inner.draw_stock { self.session.config().inner.draw_stock
DrawStockConfig::DrawOne => DrawMode::DrawOne,
DrawStockConfig::DrawThree => DrawMode::DrawThree,
}
} }
/// Current game score, derived from the upstream session stats. /// Current game score, derived from the upstream session stats.
@@ -405,11 +402,11 @@ impl GameState {
!self.check_win() && self.check_auto_complete() !self.check_win() && self.check_auto_complete()
} }
fn new_session(seed: u64, draw_mode: DrawMode) -> Session<Klondike> { fn new_session(seed: u64, draw_mode: DrawStockConfig) -> Session<Klondike> {
Session::new(Klondike::with_seed(seed), Self::session_config(draw_mode)) Session::new(Klondike::with_seed(seed), Self::session_config(draw_mode))
} }
fn session_config(draw_mode: DrawMode) -> SessionConfig<KlondikeConfig> { fn session_config(draw_mode: DrawStockConfig) -> SessionConfig<KlondikeConfig> {
SessionConfig { SessionConfig {
inner: Self::replay_config(draw_mode), inner: Self::replay_config(draw_mode),
// The 15 WXP undo penalty is now applied by the upstream score // The 15 WXP undo penalty is now applied by the upstream score
@@ -419,7 +416,7 @@ impl GameState {
} }
} }
fn replay_config(draw_mode: DrawMode) -> KlondikeConfig { fn replay_config(draw_mode: DrawStockConfig) -> KlondikeConfig {
// Always allow foundation returns during replay, regardless of the // Always allow foundation returns during replay, regardless of the
// player's current `take_from_foundation` setting. A move recorded // player's current `take_from_foundation` setting. A move recorded
// when the rule was enabled must replay correctly even if the player // when the rule was enabled must replay correctly even if the player
@@ -587,7 +584,7 @@ impl GameState {
/// mode. `draw_mode()` is otherwise fixed at deal time, so tests that need /// mode. `draw_mode()` is otherwise fixed at deal time, so tests that need
/// a specific mode use this instead of mutating a field. /// a specific mode use this instead of mutating a field.
#[cfg(feature = "test-support")] #[cfg(feature = "test-support")]
pub fn set_test_draw_mode(&mut self, draw_mode: DrawMode) { pub fn set_test_draw_mode(&mut self, draw_mode: DrawStockConfig) {
self.session = Self::new_session(self.seed, draw_mode); self.session = Self::new_session(self.seed, draw_mode);
} }
@@ -1148,7 +1145,7 @@ impl GameState {
/// "Winnable deals only" retry loop. /// "Winnable deals only" retry loop.
pub fn solve_fresh_deal( pub fn solve_fresh_deal(
seed: u64, seed: u64,
draw_mode: DrawMode, draw_mode: DrawStockConfig,
moves_budget: u64, moves_budget: u64,
states_budget: u64, states_budget: u64,
) -> SolveOutcome { ) -> SolveOutcome {
@@ -1177,7 +1174,7 @@ mod tests {
const MAX_STEPS: usize = 160; const MAX_STEPS: usize = 160;
for seed in 1..=MAX_SEED { for seed in 1..=MAX_SEED {
let mut game = GameState::new(seed, DrawMode::DrawOne); let mut game = GameState::new(seed, DrawStockConfig::DrawOne);
game.take_from_foundation = true; game.take_from_foundation = true;
for _ in 0..MAX_STEPS { for _ in 0..MAX_STEPS {
@@ -1226,7 +1223,7 @@ mod tests {
/// iteration limit (shouldn't happen in practice). /// iteration limit (shouldn't happen in practice).
fn game_at_first_recycle() -> Option<GameState> { fn game_at_first_recycle() -> Option<GameState> {
for seed in 1..=256_u64 { for seed in 1..=256_u64 {
let mut game = GameState::new(seed, DrawMode::DrawOne); let mut game = GameState::new(seed, DrawStockConfig::DrawOne);
for _ in 0..200 { for _ in 0..200 {
if game.stock_cards().is_empty() && !game.waste_cards().is_empty() { if game.stock_cards().is_empty() && !game.waste_cards().is_empty() {
// This draw will recycle. // This draw will recycle.
@@ -1259,7 +1256,7 @@ mod tests {
fn undo_applies_minus_15_penalty_via_upstream_score() { fn undo_applies_minus_15_penalty_via_upstream_score() {
// A foundation move scores +10 upstream; undoing it nets the move score // A foundation move scores +10 upstream; undoing it nets the move score
// back to 0 and adds the 15 undo penalty, which `score()` floors at 0. // back to 0 and adds the 15 undo penalty, which `score()` floors at 0.
let mut game = GameState::new(1, DrawMode::DrawOne); let mut game = GameState::new(1, DrawStockConfig::DrawOne);
// Find and play any scoring move, then undo it. // Find and play any scoring move, then undo it.
let scoring_move = game let scoring_move = game
.possible_instructions() .possible_instructions()
@@ -1319,13 +1316,13 @@ mod tests {
fn solve_fresh_deal_is_deterministic() { fn solve_fresh_deal_is_deterministic() {
let a = GameState::solve_fresh_deal( let a = GameState::solve_fresh_deal(
7, 7,
DrawMode::DrawOne, DrawStockConfig::DrawOne,
DEFAULT_SOLVE_MOVES_BUDGET, DEFAULT_SOLVE_MOVES_BUDGET,
DEFAULT_SOLVE_STATES_BUDGET, DEFAULT_SOLVE_STATES_BUDGET,
); );
let b = GameState::solve_fresh_deal( let b = GameState::solve_fresh_deal(
7, 7,
DrawMode::DrawOne, DrawStockConfig::DrawOne,
DEFAULT_SOLVE_MOVES_BUDGET, DEFAULT_SOLVE_MOVES_BUDGET,
DEFAULT_SOLVE_STATES_BUDGET, DEFAULT_SOLVE_STATES_BUDGET,
); );
@@ -1335,7 +1332,7 @@ mod tests {
#[test] #[test]
fn winnable_verdict_carries_a_first_move() { fn winnable_verdict_carries_a_first_move() {
// Contract: a first move is present iff the verdict is winnable. // Contract: a first move is present iff the verdict is winnable.
let outcome = GameState::solve_fresh_deal(7, DrawMode::DrawOne, 5_000, 5_000); let outcome = GameState::solve_fresh_deal(7, DrawStockConfig::DrawOne, 5_000, 5_000);
let winnable = matches!(outcome, Ok(Some(_))); let winnable = matches!(outcome, Ok(Some(_)));
let has_move = outcome.ok().flatten().is_some(); let has_move = outcome.ok().flatten().is_some();
assert_eq!(winnable, has_move); assert_eq!(winnable, has_move);
@@ -1343,7 +1340,7 @@ mod tests {
#[test] #[test]
fn solve_first_move_uses_live_game_state() { fn solve_first_move_uses_live_game_state() {
let mut game = GameState::new(42, DrawMode::DrawOne); let mut game = GameState::new(42, DrawStockConfig::DrawOne);
game.draw().expect("draw must succeed"); game.draw().expect("draw must succeed");
let outcome = game.solve_first_move(5_000, 5_000); let outcome = game.solve_first_move(5_000, 5_000);
@@ -1354,7 +1351,7 @@ mod tests {
#[test] #[test]
fn zero_state_budget_is_inconclusive() { fn zero_state_budget_is_inconclusive() {
let outcome = GameState::solve_fresh_deal(7, DrawMode::DrawOne, 5_000, 0); let outcome = GameState::solve_fresh_deal(7, DrawStockConfig::DrawOne, 5_000, 0);
assert!(matches!(outcome, Err(SolveError::StatesBudgetExceeded))); assert!(matches!(outcome, Err(SolveError::StatesBudgetExceeded)));
} }
@@ -1362,9 +1359,9 @@ mod tests {
fn budget_is_passed_through_not_clamped() { fn budget_is_passed_through_not_clamped() {
// This seed is Inconclusive at 1k states but Winnable at 5k — proving the // This seed is Inconclusive at 1k states but Winnable at 5k — proving the
// budget reaches the solver unchanged. // budget reaches the solver unchanged.
let easy = GameState::solve_fresh_deal(0xD1FF_0000_0000_0012, DrawMode::DrawOne, 1_000, 1_000); let easy = GameState::solve_fresh_deal(0xD1FF_0000_0000_0012, DrawStockConfig::DrawOne, 1_000, 1_000);
let medium = let medium =
GameState::solve_fresh_deal(0xD1FF_0000_0000_0012, DrawMode::DrawOne, 5_000, 5_000); GameState::solve_fresh_deal(0xD1FF_0000_0000_0012, DrawStockConfig::DrawOne, 5_000, 5_000);
assert!(easy.is_err()); assert!(easy.is_err());
assert!(matches!(medium, Ok(Some(_)))); assert!(matches!(medium, Ok(Some(_))));
} }
+2 -14
View File
@@ -16,15 +16,6 @@ use klondike::{
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// Whether cards are drawn one at a time or three at a time from the stock.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum DrawMode {
/// Draw one card from stock per turn.
DrawOne,
/// Draw three cards from stock per turn; only the top is playable.
DrawThree,
}
/// Bridges `solitaire_core` game config and scoring to the upstream `klondike` crate. /// Bridges `solitaire_core` game config and scoring to the upstream `klondike` crate.
/// ///
/// This type is intentionally zero-sized: it does not carry mutable runtime /// This type is intentionally zero-sized: it does not carry mutable runtime
@@ -35,12 +26,9 @@ pub struct KlondikeAdapter;
impl KlondikeAdapter { impl KlondikeAdapter {
/// Build a [`KlondikeConfig`] from draw mode and foundation house-rule setting. /// Build a [`KlondikeConfig`] from draw mode and foundation house-rule setting.
pub fn config_for(draw_mode: DrawMode, take_from_foundation: bool) -> KlondikeConfig { pub fn config_for(draw_mode: DrawStockConfig, take_from_foundation: bool) -> KlondikeConfig {
KlondikeConfig { KlondikeConfig {
draw_stock: match draw_mode { draw_stock: draw_mode,
DrawMode::DrawOne => DrawStockConfig::DrawOne,
DrawMode::DrawThree => DrawStockConfig::DrawThree,
},
move_from_foundation: if take_from_foundation { move_from_foundation: if take_from_foundation {
MoveFromFoundationConfig::Allowed MoveFromFoundationConfig::Allowed
} else { } else {
+1 -2
View File
@@ -13,8 +13,7 @@ pub mod klondike_adapter;
// when decoding instructions to piles in `instruction_to_piles`) and do not // when decoding instructions to piles in `instruction_to_piles`) and do not
// appear in any public method signature. // appear in any public method signature.
pub use card_game::{Card, Session, SolveError}; pub use card_game::{Card, Session, SolveError};
pub use klondike::{Foundation, Klondike, KlondikeInstruction, KlondikePile, Tableau}; pub use klondike::{DrawStockConfig, Foundation, Klondike, KlondikeInstruction, KlondikePile, Tableau};
pub use klondike_adapter::DrawMode;
// Solvability check API (delegates to `card_game::Session::solve`); replaces the // Solvability check API (delegates to `card_game::Session::solve`); replaces the
// former `solitaire_data::solver` wrapper module. // former `solitaire_data::solver` wrapper module.
+3 -4
View File
@@ -1,9 +1,8 @@
use card_game::{Card, Game}; use card_game::{Card, Game};
use klondike::{Foundation, KlondikePile, KlondikeInstruction, SkipCards, Tableau}; use klondike::{DrawStockConfig, Foundation, KlondikePile, KlondikeInstruction, SkipCards, Tableau};
use proptest::prelude::*; use proptest::prelude::*;
use crate::game_state::GameState; use crate::game_state::GameState;
use crate::klondike_adapter::DrawMode;
use crate::klondike_adapter::{ use crate::klondike_adapter::{
InvalidSavedInstruction, SavedDstFoundation, SavedDstTableau, SavedFoundation, InvalidSavedInstruction, SavedDstFoundation, SavedDstTableau, SavedFoundation,
SavedInstruction, SavedKlondikePile, SavedKlondikePileStack, SavedSkipCards, SavedTableau, SavedInstruction, SavedKlondikePile, SavedKlondikePileStack, SavedSkipCards, SavedTableau,
@@ -52,8 +51,8 @@ fn all_cards(game: &GameState) -> Vec<Card> {
cards cards
} }
fn draw_mode_strategy() -> impl Strategy<Value = DrawMode> { fn draw_mode_strategy() -> impl Strategy<Value = DrawStockConfig> {
prop_oneof![Just(DrawMode::DrawOne), Just(DrawMode::DrawThree)] prop_oneof![Just(DrawStockConfig::DrawOne), Just(DrawStockConfig::DrawThree)]
} }
/// Apply a sequence of random actions to a game, silently ignoring errors. /// Apply a sequence of random actions to a game, silently ignoring errors.
+5 -5
View File
@@ -26,7 +26,7 @@ use std::path::{Path, PathBuf};
use chrono::NaiveDate; use chrono::NaiveDate;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use solitaire_core::{DrawMode, game_state::GameMode}; use solitaire_core::{DrawStockConfig, game_state::GameMode};
use solitaire_core::klondike_adapter::SavedKlondikePile; use solitaire_core::klondike_adapter::SavedKlondikePile;
const LATEST_REPLAY_FILE_NAME: &str = "latest_replay.json"; const LATEST_REPLAY_FILE_NAME: &str = "latest_replay.json";
@@ -124,7 +124,7 @@ pub struct Replay {
/// `GameState::new_with_mode(seed, draw_mode, mode)`. /// `GameState::new_with_mode(seed, draw_mode, mode)`.
pub seed: u64, pub seed: u64,
/// Draw mode the recorded game was played in. /// Draw mode the recorded game was played in.
pub draw_mode: DrawMode, pub draw_mode: DrawStockConfig,
/// Game mode the recorded game was played in. /// Game mode the recorded game was played in.
pub mode: GameMode, pub mode: GameMode,
/// Total wall-clock seconds the win took. Used for the Stats UI /// Total wall-clock seconds the win took. Used for the Stats UI
@@ -180,7 +180,7 @@ impl Replay {
/// latter directly when the upload task resolves. /// latter directly when the upload task resolves.
pub fn new( pub fn new(
seed: u64, seed: u64,
draw_mode: DrawMode, draw_mode: DrawStockConfig,
mode: GameMode, mode: GameMode,
time_seconds: u64, time_seconds: u64,
final_score: i32, final_score: i32,
@@ -453,7 +453,7 @@ mod tests {
let date = NaiveDate::from_ymd_opt(2026, 5, 2).expect("valid date"); let date = NaiveDate::from_ymd_opt(2026, 5, 2).expect("valid date");
Replay::new( Replay::new(
12345, 12345,
DrawMode::DrawThree, DrawStockConfig::DrawThree,
GameMode::Classic, GameMode::Classic,
134, 134,
5_120, 5_120,
@@ -596,7 +596,7 @@ mod tests {
let date = NaiveDate::from_ymd_opt(2026, 5, 2).expect("valid date"); let date = NaiveDate::from_ymd_opt(2026, 5, 2).expect("valid date");
Replay::new( Replay::new(
id as u64, id as u64,
DrawMode::DrawOne, DrawStockConfig::DrawOne,
GameMode::Classic, GameMode::Classic,
60, 60,
id, id,
+5 -5
View File
@@ -9,7 +9,7 @@ use std::io;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use solitaire_core::{DrawMode, game_state::DifficultyLevel}; use solitaire_core::{DrawStockConfig, game_state::DifficultyLevel};
const SETTINGS_FILE_NAME: &str = "settings.json"; const SETTINGS_FILE_NAME: &str = "settings.json";
@@ -101,7 +101,7 @@ pub struct WindowGeometry {
pub struct Settings { pub struct Settings {
/// Draw mode selected for new games. /// Draw mode selected for new games.
#[serde(default = "default_draw_mode")] #[serde(default = "default_draw_mode")]
pub draw_mode: DrawMode, pub draw_mode: DrawStockConfig,
/// Linear SFX volume in `[0.0, 1.0]`. Applied to kira's SFX channel gain. /// Linear SFX volume in `[0.0, 1.0]`. Applied to kira's SFX channel gain.
#[serde(default = "default_sfx_volume")] #[serde(default = "default_sfx_volume")]
pub sfx_volume: f32, pub sfx_volume: f32,
@@ -288,8 +288,8 @@ pub struct Settings {
pub touch_input_mode: TouchInputMode, pub touch_input_mode: TouchInputMode,
} }
fn default_draw_mode() -> DrawMode { fn default_draw_mode() -> DrawStockConfig {
DrawMode::DrawOne DrawStockConfig::DrawOne
} }
fn default_sfx_volume() -> f32 { fn default_sfx_volume() -> f32 {
@@ -392,7 +392,7 @@ pub const SOLVER_DEAL_RETRY_CAP: u32 = 50;
impl Default for Settings { impl Default for Settings {
fn default() -> Self { fn default() -> Self {
Self { Self {
draw_mode: DrawMode::DrawOne, draw_mode: DrawStockConfig::DrawOne,
sfx_volume: default_sfx_volume(), sfx_volume: default_sfx_volume(),
music_volume: default_music_volume(), music_volume: default_music_volume(),
animation_speed: AnimSpeed::Normal, animation_speed: AnimSpeed::Normal,
+26 -26
View File
@@ -2,10 +2,10 @@
//! //!
//! [`StatsSnapshot`] is defined in `solitaire_sync` and re-exported here. //! [`StatsSnapshot`] is defined in `solitaire_sync` and re-exported here.
//! This module adds the [`StatsExt`] extension trait, which supplies the //! This module adds the [`StatsExt`] extension trait, which supplies the
//! `update_on_win` method that depends on [`DrawMode`] from `solitaire_core`. //! `update_on_win` method that depends on [`DrawStockConfig`] from `solitaire_core`.
use chrono::Utc; use chrono::Utc;
use solitaire_core::{DrawMode, game_state::GameMode}; use solitaire_core::{DrawStockConfig, game_state::GameMode};
pub use solitaire_sync::StatsSnapshot; pub use solitaire_sync::StatsSnapshot;
@@ -18,9 +18,9 @@ pub trait StatsExt {
/// ///
/// Tracks lifetime totals only — per-mode best scores and times are /// Tracks lifetime totals only — per-mode best scores and times are
/// updated separately via [`StatsExt::update_per_mode_bests`] so the /// updated separately via [`StatsExt::update_per_mode_bests`] so the
/// long-standing call sites that only know about [`DrawMode`] keep /// long-standing call sites that only know about [`DrawStockConfig`] keep
/// compiling. /// compiling.
fn update_on_win(&mut self, score: i32, time_seconds: u64, draw_mode: &DrawMode); fn update_on_win(&mut self, score: i32, time_seconds: u64, draw_mode: &DrawStockConfig);
/// Updates the per-mode best score and fastest-win-time fields for the /// Updates the per-mode best score and fastest-win-time fields for the
/// given [`GameMode`]. Call alongside [`StatsExt::update_on_win`] from /// given [`GameMode`]. Call alongside [`StatsExt::update_on_win`] from
@@ -37,7 +37,7 @@ pub trait StatsExt {
} }
impl StatsExt for StatsSnapshot { impl StatsExt for StatsSnapshot {
fn update_on_win(&mut self, score: i32, time_seconds: u64, draw_mode: &DrawMode) { fn update_on_win(&mut self, score: i32, time_seconds: u64, draw_mode: &DrawStockConfig) {
let prev_wins = self.games_won; let prev_wins = self.games_won;
self.games_played += 1; self.games_played += 1;
self.games_won += 1; self.games_won += 1;
@@ -64,8 +64,8 @@ impl StatsExt for StatsSnapshot {
}; };
match draw_mode { match draw_mode {
DrawMode::DrawOne => self.draw_one_wins += 1, DrawStockConfig::DrawOne => self.draw_one_wins += 1,
DrawMode::DrawThree => self.draw_three_wins += 1, DrawStockConfig::DrawThree => self.draw_three_wins += 1,
} }
self.last_modified = Utc::now(); self.last_modified = Utc::now();
@@ -135,7 +135,7 @@ mod tests {
#[test] #[test]
fn first_win_sets_all_fields() { fn first_win_sets_all_fields() {
let mut s = StatsSnapshot::default(); let mut s = StatsSnapshot::default();
s.update_on_win(1500, 120, &DrawMode::DrawOne); s.update_on_win(1500, 120, &DrawStockConfig::DrawOne);
assert_eq!(s.games_played, 1); assert_eq!(s.games_played, 1);
assert_eq!(s.games_won, 1); assert_eq!(s.games_won, 1);
assert_eq!(s.win_streak_current, 1); assert_eq!(s.win_streak_current, 1);
@@ -152,7 +152,7 @@ mod tests {
fn streak_tracks_across_wins() { fn streak_tracks_across_wins() {
let mut s = StatsSnapshot::default(); let mut s = StatsSnapshot::default();
for _ in 0..3 { for _ in 0..3 {
s.update_on_win(100, 60, &DrawMode::DrawOne); s.update_on_win(100, 60, &DrawStockConfig::DrawOne);
} }
assert_eq!(s.win_streak_current, 3); assert_eq!(s.win_streak_current, 3);
assert_eq!(s.win_streak_best, 3); assert_eq!(s.win_streak_best, 3);
@@ -161,8 +161,8 @@ mod tests {
#[test] #[test]
fn record_abandoned_resets_streak_and_increments_played() { fn record_abandoned_resets_streak_and_increments_played() {
let mut s = StatsSnapshot::default(); let mut s = StatsSnapshot::default();
s.update_on_win(100, 60, &DrawMode::DrawOne); s.update_on_win(100, 60, &DrawStockConfig::DrawOne);
s.update_on_win(100, 60, &DrawMode::DrawOne); s.update_on_win(100, 60, &DrawStockConfig::DrawOne);
assert_eq!(s.win_streak_current, 2); assert_eq!(s.win_streak_current, 2);
s.record_abandoned(); s.record_abandoned();
assert_eq!(s.games_played, 3); assert_eq!(s.games_played, 3);
@@ -174,35 +174,35 @@ mod tests {
#[test] #[test]
fn fastest_win_takes_minimum() { fn fastest_win_takes_minimum() {
let mut s = StatsSnapshot::default(); let mut s = StatsSnapshot::default();
s.update_on_win(100, 300, &DrawMode::DrawOne); s.update_on_win(100, 300, &DrawStockConfig::DrawOne);
s.update_on_win(100, 120, &DrawMode::DrawOne); s.update_on_win(100, 120, &DrawStockConfig::DrawOne);
s.update_on_win(100, 500, &DrawMode::DrawOne); s.update_on_win(100, 500, &DrawStockConfig::DrawOne);
assert_eq!(s.fastest_win_seconds, 120); assert_eq!(s.fastest_win_seconds, 120);
} }
#[test] #[test]
fn avg_time_is_correct_rolling_average() { fn avg_time_is_correct_rolling_average() {
let mut s = StatsSnapshot::default(); let mut s = StatsSnapshot::default();
s.update_on_win(100, 100, &DrawMode::DrawOne); s.update_on_win(100, 100, &DrawStockConfig::DrawOne);
s.update_on_win(100, 200, &DrawMode::DrawOne); s.update_on_win(100, 200, &DrawStockConfig::DrawOne);
s.update_on_win(100, 300, &DrawMode::DrawOne); s.update_on_win(100, 300, &DrawStockConfig::DrawOne);
assert_eq!(s.avg_time_seconds, 200); assert_eq!(s.avg_time_seconds, 200);
} }
#[test] #[test]
fn best_score_updates_only_on_higher_score() { fn best_score_updates_only_on_higher_score() {
let mut s = StatsSnapshot::default(); let mut s = StatsSnapshot::default();
s.update_on_win(500, 60, &DrawMode::DrawOne); s.update_on_win(500, 60, &DrawStockConfig::DrawOne);
s.update_on_win(300, 60, &DrawMode::DrawOne); s.update_on_win(300, 60, &DrawStockConfig::DrawOne);
assert_eq!(s.best_single_score, 500); assert_eq!(s.best_single_score, 500);
s.update_on_win(800, 60, &DrawMode::DrawOne); s.update_on_win(800, 60, &DrawStockConfig::DrawOne);
assert_eq!(s.best_single_score, 800); assert_eq!(s.best_single_score, 800);
} }
#[test] #[test]
fn negative_score_treated_as_zero() { fn negative_score_treated_as_zero() {
let mut s = StatsSnapshot::default(); let mut s = StatsSnapshot::default();
s.update_on_win(-50, 60, &DrawMode::DrawOne); s.update_on_win(-50, 60, &DrawStockConfig::DrawOne);
assert_eq!(s.best_single_score, 0); assert_eq!(s.best_single_score, 0);
assert_eq!(s.lifetime_score, 0); assert_eq!(s.lifetime_score, 0);
} }
@@ -210,8 +210,8 @@ mod tests {
#[test] #[test]
fn draw_three_wins_tracked_separately() { fn draw_three_wins_tracked_separately() {
let mut s = StatsSnapshot::default(); let mut s = StatsSnapshot::default();
s.update_on_win(100, 60, &DrawMode::DrawOne); s.update_on_win(100, 60, &DrawStockConfig::DrawOne);
s.update_on_win(100, 60, &DrawMode::DrawThree); s.update_on_win(100, 60, &DrawStockConfig::DrawThree);
assert_eq!(s.draw_one_wins, 1); assert_eq!(s.draw_one_wins, 1);
assert_eq!(s.draw_three_wins, 1); assert_eq!(s.draw_three_wins, 1);
} }
@@ -221,7 +221,7 @@ mod tests {
let mut s = StatsSnapshot::default(); let mut s = StatsSnapshot::default();
// Build a streak of 5. // Build a streak of 5.
for _ in 0..5 { for _ in 0..5 {
s.update_on_win(100, 60, &DrawMode::DrawOne); s.update_on_win(100, 60, &DrawStockConfig::DrawOne);
} }
assert_eq!(s.win_streak_best, 5); assert_eq!(s.win_streak_best, 5);
// Lose (abandon), resetting current. // Lose (abandon), resetting current.
@@ -229,7 +229,7 @@ mod tests {
assert_eq!(s.win_streak_current, 0); assert_eq!(s.win_streak_current, 0);
assert_eq!(s.win_streak_best, 5, "best must survive the loss"); assert_eq!(s.win_streak_best, 5, "best must survive the loss");
// Win once — current becomes 1, best must remain 5. // Win once — current becomes 1, best must remain 5.
s.update_on_win(100, 60, &DrawMode::DrawOne); s.update_on_win(100, 60, &DrawStockConfig::DrawOne);
assert_eq!(s.win_streak_current, 1); assert_eq!(s.win_streak_current, 1);
assert_eq!( assert_eq!(
s.win_streak_best, 5, s.win_streak_best, 5,
@@ -243,7 +243,7 @@ mod tests {
lifetime_score: u64::MAX - 100, lifetime_score: u64::MAX - 100,
..Default::default() ..Default::default()
}; };
s.update_on_win(200, 60, &DrawMode::DrawOne); s.update_on_win(200, 60, &DrawStockConfig::DrawOne);
assert_eq!( assert_eq!(
s.lifetime_score, s.lifetime_score,
u64::MAX, u64::MAX,
+8 -8
View File
@@ -279,7 +279,7 @@ fn cleanup_tmp_files_in(dir: &Path) {
mod tests { mod tests {
use super::*; use super::*;
use crate::stats::{StatsExt, StatsSnapshot}; use crate::stats::{StatsExt, StatsSnapshot};
use solitaire_core::DrawMode; use solitaire_core::DrawStockConfig;
use std::env; use std::env;
fn tmp_path(name: &str) -> PathBuf { fn tmp_path(name: &str) -> PathBuf {
@@ -292,7 +292,7 @@ mod tests {
let _ = fs::remove_file(&path); let _ = fs::remove_file(&path);
let mut stats = StatsSnapshot::default(); let mut stats = StatsSnapshot::default();
stats.update_on_win(1000, 180, &DrawMode::DrawOne); stats.update_on_win(1000, 180, &DrawStockConfig::DrawOne);
save_stats_to(&path, &stats).expect("save"); save_stats_to(&path, &stats).expect("save");
let loaded = load_stats_from(&path); let loaded = load_stats_from(&path);
@@ -381,7 +381,7 @@ mod tests {
let path = gs_path("round_trip"); let path = gs_path("round_trip");
let _ = fs::remove_file(&path); let _ = fs::remove_file(&path);
let gs = GameState::new(12345, DrawMode::DrawOne); let gs = GameState::new(12345, DrawStockConfig::DrawOne);
save_game_state_to(&path, &gs).expect("save"); save_game_state_to(&path, &gs).expect("save");
let loaded = load_game_state_from(&path).expect("load"); let loaded = load_game_state_from(&path).expect("load");
@@ -410,7 +410,7 @@ mod tests {
let path = gs_path("won_skip"); let path = gs_path("won_skip");
let _ = fs::remove_file(&path); let _ = fs::remove_file(&path);
let mut gs = GameState::new(99, DrawMode::DrawOne); let mut gs = GameState::new(99, DrawStockConfig::DrawOne);
gs.set_test_won(true); gs.set_test_won(true);
save_game_state_to(&path, &gs).expect("save should be no-op, not error"); save_game_state_to(&path, &gs).expect("save should be no-op, not error");
assert!( assert!(
@@ -423,7 +423,7 @@ mod tests {
fn delete_game_state_removes_file() { fn delete_game_state_removes_file() {
use solitaire_core::game_state::GameState; use solitaire_core::game_state::GameState;
let path = gs_path("delete"); let path = gs_path("delete");
let gs = GameState::new(1, DrawMode::DrawOne); let gs = GameState::new(1, DrawStockConfig::DrawOne);
save_game_state_to(&path, &gs).expect("save"); save_game_state_to(&path, &gs).expect("save");
assert!(path.exists()); assert!(path.exists());
delete_game_state_at(&path).expect("delete"); delete_game_state_at(&path).expect("delete");
@@ -441,7 +441,7 @@ mod tests {
fn save_game_state_is_atomic() { fn save_game_state_is_atomic() {
use solitaire_core::game_state::GameState; use solitaire_core::game_state::GameState;
let path = gs_path("atomic"); let path = gs_path("atomic");
let gs = GameState::new(55, DrawMode::DrawThree); let gs = GameState::new(55, DrawStockConfig::DrawThree);
save_game_state_to(&path, &gs).expect("save"); save_game_state_to(&path, &gs).expect("save");
let tmp = path.with_extension("json.tmp"); let tmp = path.with_extension("json.tmp");
assert!(!tmp.exists(), ".tmp must be cleaned up after rename"); assert!(!tmp.exists(), ".tmp must be cleaned up after rename");
@@ -512,7 +512,7 @@ mod tests {
let path = gs_path("v4_mid_game"); let path = gs_path("v4_mid_game");
let _ = fs::remove_file(&path); let _ = fs::remove_file(&path);
let mut gs = GameState::new(42, DrawMode::DrawOne); let mut gs = GameState::new(42, DrawStockConfig::DrawOne);
// Draw several times to populate the instruction history with // Draw several times to populate the instruction history with
// RotateStock entries and expose waste cards for further moves. // RotateStock entries and expose waste cards for further moves.
@@ -619,7 +619,7 @@ mod tests {
.expect("schema v3 must be accepted and migrated to v4"); .expect("schema v3 must be accepted and migrated to v4");
// The loaded game should match a fresh game that had one draw applied. // The loaded game should match a fresh game that had one draw applied.
let mut expected = GameState::new(42, DrawMode::DrawOne); let mut expected = GameState::new(42, DrawStockConfig::DrawOne);
expected.draw().expect("draw must succeed on a fresh game"); expected.draw().expect("draw must succeed on a fresh game");
assert_eq!(loaded, expected, "migrated v3 game state must match equivalent v4 state"); assert_eq!(loaded, expected, "migrated v3 game state must match equivalent v4 state");
} }
+5 -5
View File
@@ -4,7 +4,7 @@
//! increments matching counters in `PlayerProgress::weekly_goal_progress`. //! increments matching counters in `PlayerProgress::weekly_goal_progress`.
use chrono::{Datelike, NaiveDate}; use chrono::{Datelike, NaiveDate};
use solitaire_core::DrawMode; use solitaire_core::DrawStockConfig;
/// XP awarded each time a weekly goal is just completed. /// XP awarded each time a weekly goal is just completed.
pub const WEEKLY_GOAL_XP: u64 = 75; pub const WEEKLY_GOAL_XP: u64 = 75;
@@ -36,7 +36,7 @@ pub struct WeeklyGoalDef {
pub struct WeeklyGoalContext { pub struct WeeklyGoalContext {
pub time_seconds: u64, pub time_seconds: u64,
pub used_undo: bool, pub used_undo: bool,
pub draw_mode: DrawMode, pub draw_mode: DrawStockConfig,
} }
impl WeeklyGoalDef { impl WeeklyGoalDef {
@@ -47,7 +47,7 @@ impl WeeklyGoalDef {
WeeklyGoalKind::WinGame => true, WeeklyGoalKind::WinGame => true,
WeeklyGoalKind::WinWithoutUndo => !ctx.used_undo, WeeklyGoalKind::WinWithoutUndo => !ctx.used_undo,
WeeklyGoalKind::WinUnder { seconds } => ctx.time_seconds < seconds, WeeklyGoalKind::WinUnder { seconds } => ctx.time_seconds < seconds,
WeeklyGoalKind::WinDrawThree => ctx.draw_mode == DrawMode::DrawThree, WeeklyGoalKind::WinDrawThree => ctx.draw_mode == DrawStockConfig::DrawThree,
} }
} }
} }
@@ -106,7 +106,7 @@ mod tests {
WeeklyGoalContext { WeeklyGoalContext {
time_seconds: time, time_seconds: time,
used_undo: undo, used_undo: undo,
draw_mode: DrawMode::DrawOne, draw_mode: DrawStockConfig::DrawOne,
} }
} }
@@ -114,7 +114,7 @@ mod tests {
WeeklyGoalContext { WeeklyGoalContext {
time_seconds: time, time_seconds: time,
used_undo: false, used_undo: false,
draw_mode: DrawMode::DrawThree, draw_mode: DrawStockConfig::DrawThree,
} }
} }
+4 -4
View File
@@ -819,7 +819,7 @@ mod tests {
app.world_mut() app.world_mut()
.resource_mut::<GameStateResource>() .resource_mut::<GameStateResource>()
.0 .0
.set_test_draw_mode(solitaire_core::DrawMode::DrawThree); .set_test_draw_mode(solitaire_core::DrawStockConfig::DrawThree);
app.world_mut().write_message(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 500, score: 500,
@@ -868,7 +868,7 @@ mod tests {
app.world_mut() app.world_mut()
.resource_mut::<GameStateResource>() .resource_mut::<GameStateResource>()
.0 .0
.set_test_draw_mode(solitaire_core::DrawMode::DrawThree); .set_test_draw_mode(solitaire_core::DrawStockConfig::DrawThree);
app.world_mut().write_message(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 500, score: 500,
@@ -1393,7 +1393,7 @@ mod tests {
use crate::replay_playback::ReplayPlaybackState; use crate::replay_playback::ReplayPlaybackState;
use chrono::NaiveDate; use chrono::NaiveDate;
use solitaire_core::{DrawMode, game_state::GameMode}; use solitaire_core::{DrawStockConfig, game_state::GameMode};
use solitaire_data::{Replay, ReplayMove}; use solitaire_data::{Replay, ReplayMove};
/// Headless app variant that injects a default `ReplayPlaybackState` /// Headless app variant that injects a default `ReplayPlaybackState`
@@ -1409,7 +1409,7 @@ mod tests {
fn dummy_replay() -> Replay { fn dummy_replay() -> Replay {
Replay::new( Replay::new(
1, 1,
DrawMode::DrawOne, DrawStockConfig::DrawOne,
GameMode::Classic, GameMode::Classic,
10, 10,
100, 100,
+3 -3
View File
@@ -169,7 +169,7 @@ mod tests {
use crate::table_plugin::TablePlugin; use crate::table_plugin::TablePlugin;
use solitaire_core::{Foundation, KlondikePile, Tableau}; use solitaire_core::{Foundation, KlondikePile, Tableau};
use solitaire_core::card::{Deck, Rank, Suit}; use solitaire_core::card::{Deck, Rank, Suit};
use solitaire_core::{DrawMode, game_state::GameState}; use solitaire_core::{DrawStockConfig, game_state::GameState};
fn headless_app() -> App { fn headless_app() -> App {
let mut app = App::new(); let mut app = App::new();
@@ -183,7 +183,7 @@ mod tests {
} }
fn seeded_state_with_auto_move() -> (GameState, (KlondikePile, KlondikePile)) { fn seeded_state_with_auto_move() -> (GameState, (KlondikePile, KlondikePile)) {
let mut g = GameState::new(1, DrawMode::DrawOne); let mut g = GameState::new(1, DrawStockConfig::DrawOne);
g.set_test_stock_cards(Vec::new()); g.set_test_stock_cards(Vec::new());
g.set_test_waste_cards(Vec::new()); g.set_test_waste_cards(Vec::new());
for foundation in [ for foundation in [
@@ -227,7 +227,7 @@ mod tests {
#[test] #[test]
fn detect_activates_when_auto_completable() { fn detect_activates_when_auto_completable() {
let mut app = headless_app(); let mut app = headless_app();
let mut g = GameState::new(42, DrawMode::DrawOne); let mut g = GameState::new(42, DrawStockConfig::DrawOne);
g.set_test_auto_completable(true); g.set_test_auto_completable(true);
app.world_mut().resource_mut::<GameStateResource>().0 = g; app.world_mut().resource_mut::<GameStateResource>().0 = g;
app.world_mut().write_message(StateChangedEvent); app.world_mut().write_message(StateChangedEvent);
+24 -24
View File
@@ -18,7 +18,7 @@ use bevy::sprite::Anchor;
use bevy::window::WindowResized; use bevy::window::WindowResized;
use solitaire_core::{Foundation, KlondikePile, Tableau}; use solitaire_core::{Foundation, KlondikePile, Tableau};
use solitaire_core::card::{Card, Rank, Suit}; use solitaire_core::card::{Card, Rank, Suit};
use solitaire_core::{DrawMode, game_state::GameState}; use solitaire_core::{DrawStockConfig, game_state::GameState};
use crate::animation_plugin::{CARD_ANIM_Z_LIFT, CardAnim, EffectiveSlideDuration}; use crate::animation_plugin::{CARD_ANIM_Z_LIFT, CardAnim, EffectiveSlideDuration};
use crate::card_animation::CardAnimation; use crate::card_animation::CardAnimation;
@@ -789,8 +789,8 @@ fn sync_cards(
// and its rank/suit peek behind the incoming card. // and its rank/suit peek behind the incoming card.
let waste_buffer_id: Option<Card> = { let waste_buffer_id: Option<Card> = {
let visible = match game.draw_mode() { let visible = match game.draw_mode() {
DrawMode::DrawOne => 1_usize, DrawStockConfig::DrawOne => 1_usize,
DrawMode::DrawThree => 3_usize, DrawStockConfig::DrawThree => 3_usize,
}; };
let waste_cards = game.waste_cards(); let waste_cards = game.waste_cards();
(waste_cards.len() > visible) (waste_cards.len() > visible)
@@ -958,8 +958,8 @@ fn card_positions(game: &GameState, layout: &Layout) -> Vec<((Card, bool), Vec2,
// shows up to 3 fanned in X (matching the standard Klondike presentation). // shows up to 3 fanned in X (matching the standard Klondike presentation).
let render_start = if is_waste { let render_start = if is_waste {
let visible = match game.draw_mode() { let visible = match game.draw_mode() {
DrawMode::DrawOne => 1_usize, DrawStockConfig::DrawOne => 1_usize,
DrawMode::DrawThree => 3_usize, DrawStockConfig::DrawThree => 3_usize,
}; };
// Render one extra card so that the card sliding off the waste // Render one extra card so that the card sliding off the waste
// during a draw animation is still present in the world at z=0 // during a draw animation is still present in the world at z=0
@@ -972,7 +972,7 @@ fn card_positions(game: &GameState, layout: &Layout) -> Vec<((Card, bool), Vec2,
let mut y_offset = 0.0_f32; let mut y_offset = 0.0_f32;
let rendered_len = cards[render_start..].len(); let rendered_len = cards[render_start..].len();
for (slot, (card, face_up)) in cards[render_start..].iter().enumerate() { for (slot, (card, face_up)) in cards[render_start..].iter().enumerate() {
let x_offset = if is_waste && matches!(game.draw_mode(), DrawMode::DrawThree) { let x_offset = if is_waste && matches!(game.draw_mode(), DrawStockConfig::DrawThree) {
// When len > visible, slot 0 is a hidden buffer card kept at // When len > visible, slot 0 is a hidden buffer card kept at
// x=0 to prevent a flash during the draw tween. When len ≤ // x=0 to prevent a flash during the draw tween. When len ≤
// visible (small pile), every card is visible and should fan // visible (small pile), every card is visible and should fan
@@ -2566,7 +2566,7 @@ mod tests {
#[test] #[test]
fn card_positions_includes_all_52_cards_at_game_start() { fn card_positions_includes_all_52_cards_at_game_start() {
// At game start waste is empty, so all 52 cards are across stock + tableau. // At game start waste is empty, so all 52 cards are across stock + tableau.
let g = GameState::new(42, solitaire_core::DrawMode::DrawOne); let g = GameState::new(42, solitaire_core::DrawStockConfig::DrawOne);
let layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true); let layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
let positions = card_positions(&g, &layout); let positions = card_positions(&g, &layout);
assert_eq!(positions.len(), 52); assert_eq!(positions.len(), 52);
@@ -2574,8 +2574,8 @@ mod tests {
#[test] #[test]
fn waste_draw_one_only_renders_top_card() { fn waste_draw_one_only_renders_top_card() {
use solitaire_core::DrawMode; use solitaire_core::DrawStockConfig;
let mut g = GameState::new(42, DrawMode::DrawOne); let mut g = GameState::new(42, DrawStockConfig::DrawOne);
// Draw 3 cards so the waste pile has 3 cards. // Draw 3 cards so the waste pile has 3 cards.
for _ in 0..3 { for _ in 0..3 {
let _ = g.draw(); let _ = g.draw();
@@ -2612,8 +2612,8 @@ mod tests {
#[test] #[test]
fn waste_draw_three_renders_up_to_three_fanned_cards() { fn waste_draw_three_renders_up_to_three_fanned_cards() {
use solitaire_core::DrawMode; use solitaire_core::DrawStockConfig;
let mut g = GameState::new(42, DrawMode::DrawThree); let mut g = GameState::new(42, DrawStockConfig::DrawThree);
// 5 draw() calls in Draw-Three mode accumulates multiple waste cards. // 5 draw() calls in Draw-Three mode accumulates multiple waste cards.
for _ in 0..5 { for _ in 0..5 {
let _ = g.draw(); let _ = g.draw();
@@ -2666,8 +2666,8 @@ mod tests {
// Regression: slot.saturating_sub(1) always hid slot-0 even when the // Regression: slot.saturating_sub(1) always hid slot-0 even when the
// pile was too small to have a buffer card, collapsing 2 visible cards // pile was too small to have a buffer card, collapsing 2 visible cards
// onto x=0 instead of fanning them. // onto x=0 instead of fanning them.
use solitaire_core::DrawMode; use solitaire_core::DrawStockConfig;
let mut g = GameState::new(42, DrawMode::DrawThree); let mut g = GameState::new(42, DrawStockConfig::DrawThree);
// Draw exactly once — in Draw-Three mode with a full stock this gives // Draw exactly once — in Draw-Three mode with a full stock this gives
// 3 waste cards (still ≤ visible=3, so no hidden buffer needed). // 3 waste cards (still ≤ visible=3, so no hidden buffer needed).
let _ = g.draw(); let _ = g.draw();
@@ -2709,8 +2709,8 @@ mod tests {
/// top card so that hiding it (`Visibility::Hidden`) leaves no visible gap. /// top card so that hiding it (`Visibility::Hidden`) leaves no visible gap.
#[test] #[test]
fn waste_draw_one_buffer_card_at_same_xy_as_top() { fn waste_draw_one_buffer_card_at_same_xy_as_top() {
use solitaire_core::DrawMode; use solitaire_core::DrawStockConfig;
let mut g = GameState::new(42, DrawMode::DrawOne); let mut g = GameState::new(42, DrawStockConfig::DrawOne);
// Draw 3 times so the waste pile has 3 cards and the buffer exists. // Draw 3 times so the waste pile has 3 cards and the buffer exists.
for _ in 0..3 { for _ in 0..3 {
let _ = g.draw(); let _ = g.draw();
@@ -2740,7 +2740,7 @@ mod tests {
#[test] #[test]
fn card_positions_tableau_cards_are_fanned_downward() { fn card_positions_tableau_cards_are_fanned_downward() {
let g = GameState::new(42, solitaire_core::DrawMode::DrawOne); let g = GameState::new(42, solitaire_core::DrawStockConfig::DrawOne);
let layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true); let layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
let positions = card_positions(&g, &layout); let positions = card_positions(&g, &layout);
@@ -3083,7 +3083,7 @@ mod tests {
#[test] #[test]
fn facedown_cards_use_tighter_fan_than_uniform_faceup_fan() { fn facedown_cards_use_tighter_fan_than_uniform_faceup_fan() {
let g = GameState::new(42, solitaire_core::DrawMode::DrawOne); let g = GameState::new(42, solitaire_core::DrawStockConfig::DrawOne);
let layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true); let layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
let positions = card_positions(&g, &layout); let positions = card_positions(&g, &layout);
@@ -3533,7 +3533,7 @@ mod tests {
#[test] #[test]
fn stock_card_count_helper_reads_zero_for_empty_stock() { fn stock_card_count_helper_reads_zero_for_empty_stock() {
let g = GameState::new(42, solitaire_core::DrawMode::DrawOne); let g = GameState::new(42, solitaire_core::DrawStockConfig::DrawOne);
let mut g_empty_stock = g.clone(); let mut g_empty_stock = g.clone();
g_empty_stock.set_test_stock_cards(Vec::new()); g_empty_stock.set_test_stock_cards(Vec::new());
assert_eq!(stock_card_count(&g_empty_stock), 0); assert_eq!(stock_card_count(&g_empty_stock), 0);
@@ -3805,8 +3805,8 @@ mod tests {
#[test] #[test]
fn waste_pile_cards_have_strictly_increasing_z() { fn waste_pile_cards_have_strictly_increasing_z() {
use solitaire_core::DrawMode; use solitaire_core::DrawStockConfig;
let mut g = GameState::new(42, DrawMode::DrawThree); let mut g = GameState::new(42, DrawStockConfig::DrawThree);
for _ in 0..5 { for _ in 0..5 {
let _ = g.draw(); let _ = g.draw();
} }
@@ -3849,8 +3849,8 @@ mod tests {
/// offsets or flips the fan direction is caught immediately. /// offsets or flips the fan direction is caught immediately.
#[test] #[test]
fn waste_cards_do_not_overlap_stock_column_on_portrait() { fn waste_cards_do_not_overlap_stock_column_on_portrait() {
use solitaire_core::DrawMode; use solitaire_core::DrawStockConfig;
let mut g = GameState::new(42, DrawMode::DrawThree); let mut g = GameState::new(42, DrawStockConfig::DrawThree);
for _ in 0..5 { for _ in 0..5 {
let _ = g.draw(); let _ = g.draw();
} }
@@ -3885,8 +3885,8 @@ mod tests {
#[test] #[test]
fn waste_pile_draw_one_cards_have_distinct_z() { fn waste_pile_draw_one_cards_have_distinct_z() {
use solitaire_core::DrawMode; use solitaire_core::DrawStockConfig;
let mut g = GameState::new(42, DrawMode::DrawOne); let mut g = GameState::new(42, DrawStockConfig::DrawOne);
for _ in 0..3 { for _ in 0..3 {
let _ = g.draw(); let _ = g.draw();
} }
+3 -3
View File
@@ -117,7 +117,7 @@ mod tests {
use crate::game_plugin::GamePlugin; use crate::game_plugin::GamePlugin;
use crate::progress_plugin::ProgressPlugin; use crate::progress_plugin::ProgressPlugin;
use crate::table_plugin::TablePlugin; use crate::table_plugin::TablePlugin;
use solitaire_core::{DrawMode, game_state::GameState}; use solitaire_core::{DrawStockConfig, game_state::GameState};
fn headless_app() -> App { fn headless_app() -> App {
let mut app = App::new(); let mut app = App::new();
@@ -135,7 +135,7 @@ mod tests {
fn challenge_win_advances_index() { fn challenge_win_advances_index() {
let mut app = headless_app(); let mut app = headless_app();
app.world_mut().resource_mut::<GameStateResource>().0 = app.world_mut().resource_mut::<GameStateResource>().0 =
GameState::new_with_mode(1, DrawMode::DrawOne, GameMode::Challenge); GameState::new_with_mode(1, DrawStockConfig::DrawOne, GameMode::Challenge);
app.world_mut().write_message(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 500, score: 500,
@@ -224,7 +224,7 @@ mod tests {
.0 .0
.challenge_index = 2; .challenge_index = 2;
app.world_mut().resource_mut::<GameStateResource>().0 = app.world_mut().resource_mut::<GameStateResource>().0 =
GameState::new_with_mode(1, DrawMode::DrawOne, GameMode::Challenge); GameState::new_with_mode(1, DrawStockConfig::DrawOne, GameMode::Challenge);
app.world_mut().write_message(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 500, score: 500,
+6 -6
View File
@@ -36,7 +36,7 @@ use bevy::prelude::*;
use bevy::window::{CursorIcon, PrimaryWindow, SystemCursorIcon}; use bevy::window::{CursorIcon, PrimaryWindow, SystemCursorIcon};
use solitaire_core::card::Card; use solitaire_core::card::Card;
use solitaire_core::{Foundation, KlondikePile, Tableau}; use solitaire_core::{Foundation, KlondikePile, Tableau};
use solitaire_core::{DrawMode, game_state::GameState}; use solitaire_core::{DrawStockConfig, game_state::GameState};
use crate::card_plugin::RightClickHighlight; use crate::card_plugin::RightClickHighlight;
use crate::layout::{Layout, LayoutResource}; use crate::layout::{Layout, LayoutResource};
@@ -437,7 +437,7 @@ fn tableau_or_stack_pos(
base.x, base.x,
base.y - layout.card_size.y * layout.tableau_fan_frac * (index as f32), base.y - layout.card_size.y * layout.tableau_fan_frac * (index as f32),
) )
} else if matches!(pile, KlondikePile::Stock) && game.draw_mode() == DrawMode::DrawThree { } else if matches!(pile, KlondikePile::Stock) && game.draw_mode() == DrawStockConfig::DrawThree {
let pile_len = game.waste_cards().len(); let pile_len = game.waste_cards().len();
let visible_start = pile_len.saturating_sub(3); let visible_start = pile_len.saturating_sub(3);
let slot = index.saturating_sub(visible_start) as f32; let slot = index.saturating_sub(visible_start) as f32;
@@ -563,9 +563,9 @@ mod tests {
#[test] #[test]
fn cursor_over_draggable_returns_false_for_empty_game() { fn cursor_over_draggable_returns_false_for_empty_game() {
use crate::layout::compute_layout; use crate::layout::compute_layout;
use solitaire_core::{DrawMode, game_state::GameState}; use solitaire_core::{DrawStockConfig, game_state::GameState};
let game = GameState::new(42, DrawMode::DrawOne); let game = GameState::new(42, DrawStockConfig::DrawOne);
let layout = compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true); let layout = compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
// A cursor far off-screen should never hit anything. // A cursor far off-screen should never hit anything.
assert!(!cursor_over_draggable( assert!(!cursor_over_draggable(
@@ -581,7 +581,7 @@ mod tests {
use crate::layout::compute_layout; use crate::layout::compute_layout;
use solitaire_core::card::{Card, Deck, Rank, Suit}; use solitaire_core::card::{Card, Deck, Rank, Suit};
use solitaire_core::{DrawMode, game_state::{GameMode, GameState}}; use solitaire_core::{DrawStockConfig, game_state::{GameMode, GameState}};
/// Builds an `App` with `MinimalPlugins` and the overlay system /// Builds an `App` with `MinimalPlugins` and the overlay system
/// registered, plus the resources the system needs. Callers /// registered, plus the resources the system needs. Callers
@@ -629,7 +629,7 @@ mod tests {
// 5 of Spades (black) onto Tableau(2)'s 6 of Clubs (also black) // 5 of Spades (black) onto Tableau(2)'s 6 of Clubs (also black)
// — same colour family, illegal. Tableau(2) must NOT be // — same colour family, illegal. Tableau(2) must NOT be
// highlighted. // highlighted.
let mut game = GameState::new_with_mode(7, DrawMode::DrawOne, GameMode::Classic); let mut game = GameState::new_with_mode(7, DrawStockConfig::DrawOne, GameMode::Classic);
set_tableau_top( set_tableau_top(
&mut game, &mut game,
2, 2,
@@ -362,7 +362,7 @@ mod tests {
use crate::progress_plugin::ProgressPlugin; use crate::progress_plugin::ProgressPlugin;
use crate::table_plugin::TablePlugin; use crate::table_plugin::TablePlugin;
#[allow(unused_imports)] #[allow(unused_imports)]
use solitaire_core::{DrawMode, game_state::GameState}; use solitaire_core::{DrawStockConfig, game_state::GameState};
fn headless_app() -> App { fn headless_app() -> App {
let mut app = App::new(); let mut app = App::new();
@@ -391,7 +391,7 @@ mod tests {
// Replace the GameState with one whose seed matches the daily seed. // Replace the GameState with one whose seed matches the daily seed.
app.world_mut().resource_mut::<GameStateResource>().0 = app.world_mut().resource_mut::<GameStateResource>().0 =
GameState::new(daily_seed, DrawMode::DrawOne); GameState::new(daily_seed, DrawStockConfig::DrawOne);
app.world_mut().write_message(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 500, score: 500,
@@ -419,7 +419,7 @@ mod tests {
let daily_seed = app.world().resource::<DailyChallengeResource>().seed; let daily_seed = app.world().resource::<DailyChallengeResource>().seed;
// Use a deliberately different seed. // Use a deliberately different seed.
app.world_mut().resource_mut::<GameStateResource>().0 = app.world_mut().resource_mut::<GameStateResource>().0 =
GameState::new(daily_seed.wrapping_add(7777), DrawMode::DrawOne); GameState::new(daily_seed.wrapping_add(7777), DrawStockConfig::DrawOne);
app.world_mut().write_message(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 500, score: 500,
@@ -442,7 +442,7 @@ mod tests {
let mut app = headless_app(); let mut app = headless_app();
let daily_seed = app.world().resource::<DailyChallengeResource>().seed; let daily_seed = app.world().resource::<DailyChallengeResource>().seed;
app.world_mut().resource_mut::<GameStateResource>().0 = app.world_mut().resource_mut::<GameStateResource>().0 =
GameState::new(daily_seed, DrawMode::DrawOne); GameState::new(daily_seed, DrawStockConfig::DrawOne);
app.world_mut().write_message(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 500, score: 500,
+4 -4
View File
@@ -846,13 +846,13 @@ mod tests {
fn shake_anim_skipped_under_reduce_motion() { fn shake_anim_skipped_under_reduce_motion() {
use bevy::ecs::message::Messages; use bevy::ecs::message::Messages;
use solitaire_core::Tableau; use solitaire_core::Tableau;
use solitaire_core::{DrawMode, game_state::GameState}; use solitaire_core::{DrawStockConfig, game_state::GameState};
use solitaire_data::Settings; use solitaire_data::Settings;
let mut app = App::new(); let mut app = App::new();
app.add_plugins(MinimalPlugins) app.add_plugins(MinimalPlugins)
.add_plugins(FeedbackAnimPlugin); .add_plugins(FeedbackAnimPlugin);
app.insert_resource(GameStateResource(GameState::new(1, DrawMode::DrawOne))); app.insert_resource(GameStateResource(GameState::new(1, DrawStockConfig::DrawOne)));
app.insert_resource(SettingsResource(Settings { app.insert_resource(SettingsResource(Settings {
reduce_motion_mode: true, reduce_motion_mode: true,
..Settings::default() ..Settings::default()
@@ -900,13 +900,13 @@ mod tests {
#[test] #[test]
fn foundation_flourish_skipped_under_reduce_motion() { fn foundation_flourish_skipped_under_reduce_motion() {
use bevy::ecs::message::Messages; use bevy::ecs::message::Messages;
use solitaire_core::{DrawMode, game_state::GameState}; use solitaire_core::{DrawStockConfig, game_state::GameState};
use solitaire_data::Settings; use solitaire_data::Settings;
let mut app = App::new(); let mut app = App::new();
app.add_plugins(MinimalPlugins) app.add_plugins(MinimalPlugins)
.add_plugins(FeedbackAnimPlugin); .add_plugins(FeedbackAnimPlugin);
app.insert_resource(GameStateResource(GameState::new(1, DrawMode::DrawOne))); app.insert_resource(GameStateResource(GameState::new(1, DrawStockConfig::DrawOne)));
app.insert_resource(SettingsResource(Settings { app.insert_resource(SettingsResource(Settings {
reduce_motion_mode: true, reduce_motion_mode: true,
..Settings::default() ..Settings::default()
+16 -16
View File
@@ -14,7 +14,7 @@ use bevy::prelude::*;
use bevy::tasks::{AsyncComputeTaskPool, Task, futures_lite::future}; use bevy::tasks::{AsyncComputeTaskPool, Task, futures_lite::future};
use bevy::window::AppLifecycle; use bevy::window::AppLifecycle;
use solitaire_core::KlondikePile; use solitaire_core::KlondikePile;
use solitaire_core::{DrawMode, game_state::{GameMode, GameState}}; use solitaire_core::{DrawStockConfig, game_state::{GameMode, GameState}};
use solitaire_core::{DEFAULT_SOLVE_MOVES_BUDGET, DEFAULT_SOLVE_STATES_BUDGET}; use solitaire_core::{DEFAULT_SOLVE_MOVES_BUDGET, DEFAULT_SOLVE_STATES_BUDGET};
#[allow(deprecated)] #[allow(deprecated)]
use solitaire_data::latest_replay_path; use solitaire_data::latest_replay_path;
@@ -157,12 +157,12 @@ impl Plugin for GamePlugin {
.is_some_and(|g| g.move_count() > 0 && !g.is_won()); .is_some_and(|g| g.move_count() > 0 && !g.is_won());
let (initial_state, pending_restore) = if prompt_worthy { let (initial_state, pending_restore) = if prompt_worthy {
( (
GameState::new(seed_from_system_time(), DrawMode::DrawOne), GameState::new(seed_from_system_time(), DrawStockConfig::DrawOne),
saved, saved,
) )
} else { } else {
( (
saved.unwrap_or_else(|| GameState::new(seed_from_system_time(), DrawMode::DrawOne)), saved.unwrap_or_else(|| GameState::new(seed_from_system_time(), DrawStockConfig::DrawOne)),
None, None,
) )
}; };
@@ -388,7 +388,7 @@ fn poll_pending_new_game_seed(
/// Pure helper extracted for testability — `new_game_with_solver_*` /// Pure helper extracted for testability — `new_game_with_solver_*`
/// engine tests in the same file exercise this path. /// engine tests in the same file exercise this path.
pub(crate) fn choose_winnable_seed(initial_seed: u64, draw_mode: DrawMode) -> u64 { pub(crate) fn choose_winnable_seed(initial_seed: u64, draw_mode: DrawStockConfig) -> u64 {
let mut seed = initial_seed; let mut seed = initial_seed;
for _ in 0..SOLVER_DEAL_RETRY_CAP { for _ in 0..SOLVER_DEAL_RETRY_CAP {
match GameState::solve_fresh_deal( match GameState::solve_fresh_deal(
@@ -830,8 +830,8 @@ fn handle_draw(
Vec::new() Vec::new()
} else { } else {
let draw_count = match game.0.draw_mode() { let draw_count = match game.0.draw_mode() {
DrawMode::DrawOne => 1_usize, DrawStockConfig::DrawOne => 1_usize,
DrawMode::DrawThree => 3_usize, DrawStockConfig::DrawThree => 3_usize,
}; };
let n = stock.len(); let n = stock.len();
let take = n.min(draw_count); let take = n.min(draw_count);
@@ -1324,7 +1324,7 @@ mod tests {
app.insert_resource(PendingRestoredGame(None)); app.insert_resource(PendingRestoredGame(None));
// Override the system-time seed with a known value. // Override the system-time seed with a known value.
app.world_mut().resource_mut::<GameStateResource>().0 = app.world_mut().resource_mut::<GameStateResource>().0 =
GameState::new(seed, DrawMode::DrawOne); GameState::new(seed, DrawStockConfig::DrawOne);
app app
} }
@@ -1540,7 +1540,7 @@ mod tests {
app.insert_resource(GameStatePath(Some(path.clone()))); app.insert_resource(GameStatePath(Some(path.clone())));
// Override the seed so we can verify it was written. // Override the seed so we can verify it was written.
app.world_mut().resource_mut::<GameStateResource>().0 = app.world_mut().resource_mut::<GameStateResource>().0 =
GameState::new(7654, DrawMode::DrawOne); GameState::new(7654, DrawStockConfig::DrawOne);
app.world_mut().write_message(AppExit::Success); app.world_mut().write_message(AppExit::Success);
app.update(); app.update();
@@ -1559,7 +1559,7 @@ mod tests {
let path = tmp_gs_path("new_game_delete"); let path = tmp_gs_path("new_game_delete");
// Pre-create a saved file. // Pre-create a saved file.
save_game_state_to(&path, &GameState::new(1, DrawMode::DrawOne)).unwrap(); save_game_state_to(&path, &GameState::new(1, DrawStockConfig::DrawOne)).unwrap();
assert!(path.exists()); assert!(path.exists());
let mut app = test_app(1); let mut app = test_app(1);
@@ -1693,7 +1693,7 @@ mod tests {
fn has_legal_moves_returns_true_for_fresh_game() { fn has_legal_moves_returns_true_for_fresh_game() {
// A fresh deal always has a non-empty stock (24 cards), so drawing // A fresh deal always has a non-empty stock (24 cards), so drawing
// is always a legal move regardless of the initial face-up tableau cards. // is always a legal move regardless of the initial face-up tableau cards.
let game = GameState::new(42, DrawMode::DrawOne); let game = GameState::new(42, DrawStockConfig::DrawOne);
assert!( assert!(
has_legal_moves(&game), has_legal_moves(&game),
"fresh deal must contain at least one legal move" "fresh deal must contain at least one legal move"
@@ -1707,7 +1707,7 @@ mod tests {
// immediately placed. The game is only stuck when both stock AND waste // immediately placed. The game is only stuck when both stock AND waste
// are exhausted and no visible card can be moved. // are exhausted and no visible card can be moved.
use solitaire_core::card::{Card, Deck, Rank, Suit}; use solitaire_core::card::{Card, Deck, Rank, Suit};
let mut game = GameState::new(1, DrawMode::DrawOne); let mut game = GameState::new(1, DrawStockConfig::DrawOne);
for foundation in [ for foundation in [
Foundation::Foundation1, Foundation::Foundation1,
Foundation::Foundation2, Foundation::Foundation2,
@@ -1743,7 +1743,7 @@ mod tests {
#[test] #[test]
fn has_legal_moves_returns_true_when_ace_can_go_to_foundation() { fn has_legal_moves_returns_true_when_ace_can_go_to_foundation() {
use solitaire_core::card::{Card, Deck, Rank, Suit}; use solitaire_core::card::{Card, Deck, Rank, Suit};
let mut game = GameState::new(1, DrawMode::DrawOne); let mut game = GameState::new(1, DrawStockConfig::DrawOne);
// Empty stock and waste so draw is NOT available. // Empty stock and waste so draw is NOT available.
game.set_test_stock_cards(Vec::new()); game.set_test_stock_cards(Vec::new());
@@ -1787,7 +1787,7 @@ mod tests {
// card of its column the previous code would return false (softlock) // card of its column the previous code would return false (softlock)
// even though the player can still move that run. // even though the player can still move that run.
use solitaire_core::card::{Card, Deck, Rank, Suit}; use solitaire_core::card::{Card, Deck, Rank, Suit};
let mut game = GameState::new(1, DrawMode::DrawOne); let mut game = GameState::new(1, DrawStockConfig::DrawOne);
game.set_test_stock_cards(Vec::new()); game.set_test_stock_cards(Vec::new());
game.set_test_waste_cards(Vec::new()); game.set_test_waste_cards(Vec::new());
@@ -2185,7 +2185,7 @@ mod tests {
assert_eq!(loaded.seed, 7654, "seed must match the live game state"); assert_eq!(loaded.seed, 7654, "seed must match the live game state");
assert_eq!( assert_eq!(
loaded.draw_mode, loaded.draw_mode,
DrawMode::DrawOne, DrawStockConfig::DrawOne,
"draw_mode must be captured" "draw_mode must be captured"
); );
assert_eq!( assert_eq!(
@@ -2326,7 +2326,7 @@ mod tests {
"with solver toggle off, the requested seed must be honoured exactly" "with solver toggle off, the requested seed must be honoured exactly"
); );
// Cross-check: the dealt tableau must match GameState::new(999) byte-for-byte. // Cross-check: the dealt tableau must match GameState::new(999) byte-for-byte.
let expected = GameState::new(999, DrawMode::DrawOne); let expected = GameState::new(999, DrawStockConfig::DrawOne);
for tableau in [ for tableau in [
Tableau::Tableau1, Tableau::Tableau1,
Tableau::Tableau2, Tableau::Tableau2,
@@ -2403,7 +2403,7 @@ mod tests {
// //
// Seed 394 was previously Unwinnable under the old DFS; now it resolves // Seed 394 was previously Unwinnable under the old DFS; now it resolves
// as Inconclusive, so the helper must accept it immediately. // as Inconclusive, so the helper must accept it immediately.
let chosen = choose_winnable_seed(394, DrawMode::DrawOne); let chosen = choose_winnable_seed(394, DrawStockConfig::DrawOne);
assert_eq!( assert_eq!(
chosen, 394, chosen, 394,
"seed 394 resolves as Inconclusive; choose_winnable_seed must accept it as-is" "seed 394 resolves as Inconclusive; choose_winnable_seed must accept it as-is"
+6 -6
View File
@@ -16,7 +16,7 @@
use bevy::input::ButtonInput; use bevy::input::ButtonInput;
use bevy::input::mouse::{MouseScrollUnit, MouseWheel}; use bevy::input::mouse::{MouseScrollUnit, MouseWheel};
use bevy::prelude::*; use bevy::prelude::*;
use solitaire_core::{DrawMode, game_state::DifficultyLevel}; use solitaire_core::{DrawStockConfig, game_state::DifficultyLevel};
use solitaire_data::save_settings_to; use solitaire_data::save_settings_to;
use crate::challenge_plugin::CHALLENGE_UNLOCK_LEVEL; use crate::challenge_plugin::CHALLENGE_UNLOCK_LEVEL;
@@ -432,7 +432,7 @@ fn build_home_context<'a>(
zen_best: stats.map_or(0, |s| s.0.zen_best_score), zen_best: stats.map_or(0, |s| s.0.zen_best_score),
challenge_best: stats.map_or(0, |s| s.0.challenge_best_score), challenge_best: stats.map_or(0, |s| s.0.challenge_best_score),
daily_today, daily_today,
draw_mode: settings.map(|s| s.0.draw_mode).unwrap_or(DrawMode::DrawOne), draw_mode: settings.map(|s| s.0.draw_mode).unwrap_or(DrawStockConfig::DrawOne),
font_res, font_res,
difficulty_expanded, difficulty_expanded,
last_difficulty: settings.and_then(|s| s.0.last_difficulty), last_difficulty: settings.and_then(|s| s.0.last_difficulty),
@@ -620,9 +620,9 @@ fn handle_home_draw_mode_buttons(
return; return;
}; };
let target = if want_one { let target = if want_one {
DrawMode::DrawOne DrawStockConfig::DrawOne
} else { } else {
DrawMode::DrawThree DrawStockConfig::DrawThree
}; };
if settings.0.draw_mode == target { if settings.0.draw_mode == target {
return; // already in this mode — avoid a redundant respawn. return; // already in this mode — avoid a redundant respawn.
@@ -857,7 +857,7 @@ struct HomeContext<'a> {
challenge_best: u32, challenge_best: u32,
daily_streak: u32, daily_streak: u32,
daily_today: Option<DailyToday>, daily_today: Option<DailyToday>,
draw_mode: DrawMode, draw_mode: DrawStockConfig,
font_res: Option<&'a FontResource>, font_res: Option<&'a FontResource>,
/// Whether the difficulty section header is currently expanded. /// Whether the difficulty section header is currently expanded.
difficulty_expanded: bool, difficulty_expanded: bool,
@@ -1038,7 +1038,7 @@ fn spawn_draw_mode_row(parent: &mut ChildSpawnerCommands, ctx: &HomeContext<'_>)
..default() ..default()
}; };
let active_one = matches!(ctx.draw_mode, DrawMode::DrawOne); let active_one = matches!(ctx.draw_mode, DrawStockConfig::DrawOne);
parent parent
.spawn(Node { .spawn(Node {
+12 -12
View File
@@ -10,7 +10,7 @@ use bevy::prelude::*;
use bevy::window::WindowResized; use bevy::window::WindowResized;
use solitaire_core::{Foundation, KlondikePile, Tableau}; use solitaire_core::{Foundation, KlondikePile, Tableau};
use solitaire_core::card::Suit; use solitaire_core::card::Suit;
use solitaire_core::{DrawMode, game_state::GameMode}; use solitaire_core::{DrawStockConfig, game_state::GameMode};
use crate::auto_complete_plugin::AutoCompleteState; use crate::auto_complete_plugin::AutoCompleteState;
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
@@ -2284,8 +2284,8 @@ fn update_hud(
if let Ok(mut t) = mode_q.single_mut() { if let Ok(mut t) = mode_q.single_mut() {
**t = match g.mode { **t = match g.mode {
GameMode::Classic => match g.draw_mode() { GameMode::Classic => match g.draw_mode() {
DrawMode::DrawOne => String::new(), DrawStockConfig::DrawOne => String::new(),
DrawMode::DrawThree => "Draw 3".to_string(), DrawStockConfig::DrawThree => "Draw 3".to_string(),
}, },
GameMode::Zen => "ZEN".to_string(), GameMode::Zen => "ZEN".to_string(),
GameMode::Challenge => "CHALLENGE".to_string(), GameMode::Challenge => "CHALLENGE".to_string(),
@@ -2334,7 +2334,7 @@ fn update_hud(
// --- Draw-cycle indicator (Draw-Three mode only) --- // --- Draw-cycle indicator (Draw-Three mode only) ---
if let Ok(mut t) = draw_cycle_q.single_mut() { if let Ok(mut t) = draw_cycle_q.single_mut() {
**t = if g.is_won() || g.draw_mode() != DrawMode::DrawThree { **t = if g.is_won() || g.draw_mode() != DrawStockConfig::DrawThree {
// Hide when not in Draw-Three or after the game is won. // Hide when not in Draw-Three or after the game is won.
String::new() String::new()
} else { } else {
@@ -2726,7 +2726,7 @@ mod tests {
use crate::game_plugin::GamePlugin; use crate::game_plugin::GamePlugin;
use crate::table_plugin::TablePlugin; use crate::table_plugin::TablePlugin;
use chrono::Local; use chrono::Local;
use solitaire_core::{DrawMode, game_state::GameState}; use solitaire_core::{DrawStockConfig, game_state::GameState};
fn headless_app() -> App { fn headless_app() -> App {
let mut app = App::new(); let mut app = App::new();
@@ -2747,7 +2747,7 @@ mod tests {
fn update_hud_runs_after_game_mutation_without_panic() { fn update_hud_runs_after_game_mutation_without_panic() {
let mut app = headless_app(); let mut app = headless_app();
app.world_mut().resource_mut::<GameStateResource>().0 = app.world_mut().resource_mut::<GameStateResource>().0 =
GameState::new(42, DrawMode::DrawOne); GameState::new(42, DrawStockConfig::DrawOne);
app.update(); app.update();
} }
@@ -2784,7 +2784,7 @@ mod tests {
use solitaire_core::game_state::GameMode; use solitaire_core::game_state::GameMode;
let mut app = headless_app(); let mut app = headless_app();
app.world_mut().resource_mut::<GameStateResource>().0 = app.world_mut().resource_mut::<GameStateResource>().0 =
GameState::new_with_mode(42, DrawMode::DrawThree, GameMode::Classic); GameState::new_with_mode(42, DrawStockConfig::DrawThree, GameMode::Classic);
app.update(); app.update();
assert_eq!(read_hud_text::<HudMode>(&mut app), "Draw 3"); assert_eq!(read_hud_text::<HudMode>(&mut app), "Draw 3");
} }
@@ -2794,7 +2794,7 @@ mod tests {
use solitaire_core::game_state::GameMode; use solitaire_core::game_state::GameMode;
let mut app = headless_app(); let mut app = headless_app();
app.world_mut().resource_mut::<GameStateResource>().0 = app.world_mut().resource_mut::<GameStateResource>().0 =
GameState::new_with_mode(42, DrawMode::DrawOne, GameMode::Zen); GameState::new_with_mode(42, DrawStockConfig::DrawOne, GameMode::Zen);
app.update(); app.update();
// Zen mode spec: "No score display" → text must be empty. // Zen mode spec: "No score display" → text must be empty.
assert_eq!(read_hud_text::<HudScore>(&mut app), ""); assert_eq!(read_hud_text::<HudScore>(&mut app), "");
@@ -3037,7 +3037,7 @@ mod tests {
let mut app = headless_app(); let mut app = headless_app();
// Draw-One, no recycles yet — text must be empty. // Draw-One, no recycles yet — text must be empty.
app.world_mut().resource_mut::<GameStateResource>().0 = app.world_mut().resource_mut::<GameStateResource>().0 =
GameState::new(42, DrawMode::DrawOne); GameState::new(42, DrawStockConfig::DrawOne);
app.update(); app.update();
assert_eq!(read_hud_text::<HudRecycles>(&mut app), ""); assert_eq!(read_hud_text::<HudRecycles>(&mut app), "");
} }
@@ -3047,7 +3047,7 @@ mod tests {
let mut app = headless_app(); let mut app = headless_app();
// Draw-Three, no recycles yet — text must also be empty. // Draw-Three, no recycles yet — text must also be empty.
app.world_mut().resource_mut::<GameStateResource>().0 = app.world_mut().resource_mut::<GameStateResource>().0 =
GameState::new(42, DrawMode::DrawThree); GameState::new(42, DrawStockConfig::DrawThree);
app.update(); app.update();
assert_eq!(read_hud_text::<HudRecycles>(&mut app), ""); assert_eq!(read_hud_text::<HudRecycles>(&mut app), "");
} }
@@ -3055,7 +3055,7 @@ mod tests {
#[test] #[test]
fn recycles_hud_shows_count_draw_three() { fn recycles_hud_shows_count_draw_three() {
let mut app = headless_app(); let mut app = headless_app();
let mut gs = GameState::new(42, DrawMode::DrawThree); let mut gs = GameState::new(42, DrawStockConfig::DrawThree);
gs.force_test_recycles(3); gs.force_test_recycles(3);
app.world_mut().resource_mut::<GameStateResource>().0 = gs; app.world_mut().resource_mut::<GameStateResource>().0 = gs;
app.update(); app.update();
@@ -3066,7 +3066,7 @@ mod tests {
fn recycles_hud_shows_count_draw_one() { fn recycles_hud_shows_count_draw_one() {
let mut app = headless_app(); let mut app = headless_app();
// Draw-One with recycle_count > 0 must now show the counter too. // Draw-One with recycle_count > 0 must now show the counter too.
let mut gs = GameState::new(42, DrawMode::DrawOne); let mut gs = GameState::new(42, DrawStockConfig::DrawOne);
gs.force_test_recycles(2); gs.force_test_recycles(2);
app.world_mut().resource_mut::<GameStateResource>().0 = gs; app.world_mut().resource_mut::<GameStateResource>().0 = gs;
app.update(); app.update();
+21 -21
View File
@@ -54,7 +54,7 @@ use crate::settings_plugin::SettingsResource;
use crate::time_attack_plugin::TimeAttackResource; use crate::time_attack_plugin::TimeAttackResource;
use crate::touch_selection_plugin::TouchSelectionState; use crate::touch_selection_plugin::TouchSelectionState;
use crate::ui_theme::{MOTION_DRAG_REJECT_SECS, STATE_SUCCESS, STATE_WARNING}; use crate::ui_theme::{MOTION_DRAG_REJECT_SECS, STATE_SUCCESS, STATE_WARNING};
use solitaire_core::DrawMode; use solitaire_core::DrawStockConfig;
/// System-set labels used to anchor external systems relative to the touch /// System-set labels used to anchor external systems relative to the touch
/// drag pipeline without duplicating the internal chain ordering. /// drag pipeline without duplicating the internal chain ordering.
@@ -1173,7 +1173,7 @@ fn card_position(
y_offset -= layout.card_size.y * step; y_offset -= layout.card_size.y * step;
} }
Vec2::new(base.x, base.y + y_offset) Vec2::new(base.x, base.y + y_offset)
} else if matches!(pile, KlondikePile::Stock) && game.draw_mode() == DrawMode::DrawThree { } else if matches!(pile, KlondikePile::Stock) && game.draw_mode() == DrawStockConfig::DrawThree {
// In Draw-Three mode the top 3 waste cards are fanned in X to match // In Draw-Three mode the top 3 waste cards are fanned in X to match
// card_plugin::card_positions(). Hit-testing must use the same offsets // card_plugin::card_positions(). Hit-testing must use the same offsets
// so clicking the visually rightmost (top) card actually registers. // so clicking the visually rightmost (top) card actually registers.
@@ -1830,7 +1830,7 @@ mod tests {
use super::*; use super::*;
use crate::layout::compute_layout; use crate::layout::compute_layout;
use solitaire_core::{Foundation, Tableau}; use solitaire_core::{Foundation, Tableau};
use solitaire_core::{DrawMode, game_state::GameState}; use solitaire_core::{DrawStockConfig, game_state::GameState};
fn clear_test_piles(game: &mut GameState) { fn clear_test_piles(game: &mut GameState) {
game.set_test_stock_cards(Vec::new()); game.set_test_stock_cards(Vec::new());
@@ -1898,7 +1898,7 @@ mod tests {
#[test] #[test]
fn find_draggable_picks_top_of_tableau() { fn find_draggable_picks_top_of_tableau() {
let game = GameState::new(42, DrawMode::DrawOne); let game = GameState::new(42, DrawStockConfig::DrawOne);
let layout = compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true); let layout = compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
// In tableau 6, the visually topmost card is the last (face-up) one. // In tableau 6, the visually topmost card is the last (face-up) one.
@@ -1912,7 +1912,7 @@ mod tests {
#[test] #[test]
fn find_draggable_skips_face_down_cards() { fn find_draggable_skips_face_down_cards() {
let game = GameState::new(42, DrawMode::DrawOne); let game = GameState::new(42, DrawStockConfig::DrawOne);
let layout = compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true); let layout = compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
// Tableau 6 has 7 cards: 6 face-down (indices 0..5) + 1 face-up at // Tableau 6 has 7 cards: 6 face-down (indices 0..5) + 1 face-up at
@@ -1934,7 +1934,7 @@ mod tests {
// at 0.12 — so for any column with face-down cards above the // at 0.12 — so for any column with face-down cards above the
// face-up bottom card, clicking the visible card face missed the // face-up bottom card, clicking the visible card face missed the
// hit-test box and only the bottom strip of the card responded. // hit-test box and only the bottom strip of the card responded.
let game = GameState::new(42, DrawMode::DrawOne); let game = GameState::new(42, DrawStockConfig::DrawOne);
let layout = compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true); let layout = compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
// Tableau 6 starts with 6 face-down + 1 face-up. The face-up card // Tableau 6 starts with 6 face-down + 1 face-up. The face-up card
@@ -1952,7 +1952,7 @@ mod tests {
#[test] #[test]
fn find_draggable_returns_run_when_picking_mid_stack() { fn find_draggable_returns_run_when_picking_mid_stack() {
// Manually construct a tableau with three face-up cards all stacked. // Manually construct a tableau with three face-up cards all stacked.
let mut game = GameState::new(1, DrawMode::DrawOne); let mut game = GameState::new(1, DrawStockConfig::DrawOne);
use solitaire_core::card::Deck as D; use solitaire_core::card::Deck as D;
use solitaire_core::card::{Card, Rank, Suit}; use solitaire_core::card::{Card, Rank, Suit};
let king = Card::new(D::Deck1, Suit::Spades, Rank::King); let king = Card::new(D::Deck1, Suit::Spades, Rank::King);
@@ -1979,7 +1979,7 @@ mod tests {
#[test] #[test]
fn find_draggable_skips_non_top_waste_card() { fn find_draggable_skips_non_top_waste_card() {
let mut game = GameState::new(1, DrawMode::DrawOne); let mut game = GameState::new(1, DrawStockConfig::DrawOne);
use solitaire_core::card::Deck as D; use solitaire_core::card::Deck as D;
use solitaire_core::card::{Card, Rank, Suit}; use solitaire_core::card::{Card, Rank, Suit};
let two_spades = Card::new(D::Deck1, Suit::Spades, Rank::Two); let two_spades = Card::new(D::Deck1, Suit::Spades, Rank::Two);
@@ -1998,7 +1998,7 @@ mod tests {
#[test] #[test]
fn find_drop_target_hits_empty_tableau_pile_marker() { fn find_drop_target_hits_empty_tableau_pile_marker() {
let game = GameState::new(42, DrawMode::DrawOne); let game = GameState::new(42, DrawStockConfig::DrawOne);
let layout = compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true); let layout = compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
// Move all cards out of tableau 0 so its marker is the only drop area. // Move all cards out of tableau 0 so its marker is the only drop area.
let mut game = game; let mut game = game;
@@ -2015,7 +2015,7 @@ mod tests {
#[test] #[test]
fn find_drop_target_returns_none_for_origin() { fn find_drop_target_returns_none_for_origin() {
let game = GameState::new(42, DrawMode::DrawOne); let game = GameState::new(42, DrawStockConfig::DrawOne);
let layout = compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true); let layout = compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
let pos = layout.pile_positions[&KlondikePile::Tableau(Tableau::Tableau4)]; let pos = layout.pile_positions[&KlondikePile::Tableau(Tableau::Tableau4)];
let target = find_drop_target( let target = find_drop_target(
@@ -2029,7 +2029,7 @@ mod tests {
#[test] #[test]
fn pile_drop_rect_extends_for_tableau_with_cards() { fn pile_drop_rect_extends_for_tableau_with_cards() {
let game = GameState::new(42, DrawMode::DrawOne); let game = GameState::new(42, DrawStockConfig::DrawOne);
let layout = compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true); let layout = compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
// Tableau 6 has 7 cards. // Tableau 6 has 7 cards.
let (_, size) = pile_drop_rect(&KlondikePile::Tableau(Tableau::Tableau7), &layout, &game); let (_, size) = pile_drop_rect(&KlondikePile::Tableau(Tableau::Tableau7), &layout, &game);
@@ -2046,8 +2046,8 @@ mod tests {
fn find_draggable_draw_three_waste_top_card_hit_at_fanned_position() { fn find_draggable_draw_three_waste_top_card_hit_at_fanned_position() {
use solitaire_core::card::Deck as D; use solitaire_core::card::Deck as D;
use solitaire_core::card::{Card, Rank, Suit}; use solitaire_core::card::{Card, Rank, Suit};
use solitaire_core::{DrawMode, game_state::GameMode}; use solitaire_core::{DrawStockConfig, game_state::GameMode};
let mut game = GameState::new_with_mode(1, DrawMode::DrawThree, GameMode::Classic); let mut game = GameState::new_with_mode(1, DrawStockConfig::DrawThree, GameMode::Classic);
// Three waste cards; top (four_clubs) is rightmost in the fan. // Three waste cards; top (four_clubs) is rightmost in the fan.
let two_spades = Card::new(D::Deck1, Suit::Spades, Rank::Two); let two_spades = Card::new(D::Deck1, Suit::Spades, Rank::Two);
let three_hearts = Card::new(D::Deck1, Suit::Hearts, Rank::Three); let three_hearts = Card::new(D::Deck1, Suit::Hearts, Rank::Three);
@@ -2072,7 +2072,7 @@ mod tests {
#[test] #[test]
fn find_draggable_returns_none_for_click_on_empty_pile() { fn find_draggable_returns_none_for_click_on_empty_pile() {
let mut game = GameState::new(42, DrawMode::DrawOne); let mut game = GameState::new(42, DrawStockConfig::DrawOne);
let layout = compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true); let layout = compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
// Clear tableau 0 so it's an empty slot. // Clear tableau 0 so it's an empty slot.
game.set_test_tableau_cards(Tableau::Tableau1, Vec::new()); game.set_test_tableau_cards(Tableau::Tableau1, Vec::new());
@@ -2086,7 +2086,7 @@ mod tests {
#[test] #[test]
fn pile_drop_rect_is_card_sized_for_non_tableau() { fn pile_drop_rect_is_card_sized_for_non_tableau() {
let game = GameState::new(42, DrawMode::DrawOne); let game = GameState::new(42, DrawStockConfig::DrawOne);
let layout = compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true); let layout = compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
for pile in [ for pile in [
KlondikePile::Stock, KlondikePile::Stock,
@@ -2105,7 +2105,7 @@ mod tests {
fn best_destination_returns_none_when_no_legal_move() { fn best_destination_returns_none_when_no_legal_move() {
use solitaire_core::card::Deck as D; use solitaire_core::card::Deck as D;
use solitaire_core::card::{Card, Rank, Suit}; use solitaire_core::card::{Card, Rank, Suit};
let mut game = GameState::new(1, DrawMode::DrawOne); let mut game = GameState::new(1, DrawStockConfig::DrawOne);
// Clear everything except one card that has nowhere to go. // Clear everything except one card that has nowhere to go.
clear_test_piles(&mut game); clear_test_piles(&mut game);
@@ -2123,7 +2123,7 @@ mod tests {
fn best_tableau_destination_for_stack_skips_source_pile() { fn best_tableau_destination_for_stack_skips_source_pile() {
use solitaire_core::card::Deck as D; use solitaire_core::card::Deck as D;
use solitaire_core::card::{Card, Rank, Suit}; use solitaire_core::card::{Card, Rank, Suit};
let mut game = GameState::new(1, DrawMode::DrawOne); let mut game = GameState::new(1, DrawStockConfig::DrawOne);
clear_test_piles(&mut game); clear_test_piles(&mut game);
@@ -2149,7 +2149,7 @@ mod tests {
fn best_tableau_destination_for_stack_returns_none_when_no_legal_move() { fn best_tableau_destination_for_stack_returns_none_when_no_legal_move() {
use solitaire_core::card::Deck as D; use solitaire_core::card::Deck as D;
use solitaire_core::card::{Card, Rank, Suit}; use solitaire_core::card::{Card, Rank, Suit};
let mut game = GameState::new(1, DrawMode::DrawOne); let mut game = GameState::new(1, DrawStockConfig::DrawOne);
clear_test_piles(&mut game); clear_test_piles(&mut game);
@@ -2178,7 +2178,7 @@ mod tests {
fn find_hint_finds_ace_to_foundation() { fn find_hint_finds_ace_to_foundation() {
use solitaire_core::card::Deck as D; use solitaire_core::card::Deck as D;
use solitaire_core::card::{Card, Rank, Suit}; use solitaire_core::card::{Card, Rank, Suit};
let mut game = GameState::new(1, DrawMode::DrawOne); let mut game = GameState::new(1, DrawStockConfig::DrawOne);
// Place Ace of Clubs on top of tableau 0. // Place Ace of Clubs on top of tableau 0.
clear_test_piles(&mut game); clear_test_piles(&mut game);
@@ -2222,7 +2222,7 @@ mod tests {
fn all_hints_suggests_draw_when_no_moves_and_stock_nonempty() { fn all_hints_suggests_draw_when_no_moves_and_stock_nonempty() {
use solitaire_core::card::Deck as D; use solitaire_core::card::Deck as D;
use solitaire_core::card::{Card, Rank, Suit}; use solitaire_core::card::{Card, Rank, Suit};
let mut game = GameState::new(1, DrawMode::DrawOne); let mut game = GameState::new(1, DrawStockConfig::DrawOne);
// Remove all foundation, tableau, and waste cards so no pile-to-pile // Remove all foundation, tableau, and waste cards so no pile-to-pile
// move exists. Leave one card in the stock. // move exists. Leave one card in the stock.
@@ -2431,7 +2431,7 @@ mod tests {
app.insert_resource(crate::layout::LayoutResource( app.insert_resource(crate::layout::LayoutResource(
crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true), crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true),
)); ));
app.insert_resource(GameStateResource(GameState::new(42, DrawMode::DrawOne))); app.insert_resource(GameStateResource(GameState::new(42, DrawStockConfig::DrawOne)));
app.add_systems(Update, handle_keyboard_hint); app.add_systems(Update, handle_keyboard_hint);
// Simulate the H key being pressed this frame. // Simulate the H key being pressed this frame.
+21 -21
View File
@@ -21,7 +21,7 @@
//! active opens the overlay as normal. //! active opens the overlay as normal.
use bevy::prelude::*; use bevy::prelude::*;
use solitaire_core::DrawMode; use solitaire_core::DrawStockConfig;
use solitaire_data::save_game_state_to; use solitaire_data::save_game_state_to;
use crate::events::{ use crate::events::{
@@ -86,10 +86,10 @@ struct ForfeitConfirmButton;
/// Returns the human-readable label for a draw mode. /// Returns the human-readable label for a draw mode.
/// ///
/// Used on the pause overlay draw-mode toggle button. /// Used on the pause overlay draw-mode toggle button.
pub fn draw_mode_label(mode: DrawMode) -> &'static str { pub fn draw_mode_label(mode: DrawStockConfig) -> &'static str {
match mode { match mode {
DrawMode::DrawOne => "Draw 1", DrawStockConfig::DrawOne => "Draw 1",
DrawMode::DrawThree => "Draw 3", DrawStockConfig::DrawThree => "Draw 3",
} }
} }
@@ -273,9 +273,9 @@ fn handle_pause_draw_buttons(
} }
let Some(mut settings) = settings else { return }; let Some(mut settings) = settings else { return };
let new_mode = if pressed_one { let new_mode = if pressed_one {
DrawMode::DrawOne DrawStockConfig::DrawOne
} else { } else {
DrawMode::DrawThree DrawStockConfig::DrawThree
}; };
if settings.0.draw_mode == new_mode { if settings.0.draw_mode == new_mode {
return; return;
@@ -477,7 +477,7 @@ fn spawn_pause_screen(
commands: &mut Commands, commands: &mut Commands,
level: Option<u32>, level: Option<u32>,
streak: Option<u32>, streak: Option<u32>,
draw_mode: Option<DrawMode>, draw_mode: Option<DrawStockConfig>,
font_res: Option<&FontResource>, font_res: Option<&FontResource>,
) { ) {
spawn_modal(commands, PauseScreen, ui_theme::Z_PAUSE, |card| { spawn_modal(commands, PauseScreen, ui_theme::Z_PAUSE, |card| {
@@ -516,7 +516,7 @@ fn spawn_pause_screen(
/// `Tertiary` (recessed), giving an obvious selection state at a glance. /// `Tertiary` (recessed), giving an obvious selection state at a glance.
fn spawn_draw_mode_row( fn spawn_draw_mode_row(
parent: &mut ChildSpawnerCommands, parent: &mut ChildSpawnerCommands,
mode: DrawMode, mode: DrawStockConfig,
font_res: Option<&FontResource>, font_res: Option<&FontResource>,
) { ) {
let label_font = TextFont { let label_font = TextFont {
@@ -530,8 +530,8 @@ fn spawn_draw_mode_row(
..default() ..default()
}; };
let (one_variant, three_variant) = match mode { let (one_variant, three_variant) = match mode {
DrawMode::DrawOne => (ButtonVariant::Secondary, ButtonVariant::Tertiary), DrawStockConfig::DrawOne => (ButtonVariant::Secondary, ButtonVariant::Tertiary),
DrawMode::DrawThree => (ButtonVariant::Tertiary, ButtonVariant::Secondary), DrawStockConfig::DrawThree => (ButtonVariant::Tertiary, ButtonVariant::Secondary),
}; };
parent parent
.spawn(Node { .spawn(Node {
@@ -800,20 +800,20 @@ mod tests {
#[test] #[test]
fn draw_mode_label_draw_one() { fn draw_mode_label_draw_one() {
assert_eq!(draw_mode_label(DrawMode::DrawOne), "Draw 1"); assert_eq!(draw_mode_label(DrawStockConfig::DrawOne), "Draw 1");
} }
#[test] #[test]
fn draw_mode_label_draw_three() { fn draw_mode_label_draw_three() {
assert_eq!(draw_mode_label(DrawMode::DrawThree), "Draw 3"); assert_eq!(draw_mode_label(DrawStockConfig::DrawThree), "Draw 3");
} }
/// Both variants are covered so the match is exhaustive — this test would /// Both variants are covered so the match is exhaustive — this test would
/// fail to compile if a new DrawMode variant were added without updating /// fail to compile if a new DrawStockConfig variant were added without updating
/// `draw_mode_label`. /// `draw_mode_label`.
#[test] #[test]
fn draw_mode_label_covers_all_variants() { fn draw_mode_label_covers_all_variants() {
for mode in [DrawMode::DrawOne, DrawMode::DrawThree] { for mode in [DrawStockConfig::DrawOne, DrawStockConfig::DrawThree] {
let label = draw_mode_label(mode); let label = draw_mode_label(mode);
assert!( assert!(
!label.is_empty(), !label.is_empty(),
@@ -842,7 +842,7 @@ mod tests {
app.world_mut() app.world_mut()
.resource_mut::<SettingsResource>() .resource_mut::<SettingsResource>()
.0 .0
.draw_mode = DrawMode::DrawOne; .draw_mode = DrawStockConfig::DrawOne;
// Set paused so handle_pause_draw_toggle acts. // Set paused so handle_pause_draw_toggle acts.
app.world_mut().resource_mut::<PausedResource>().0 = true; app.world_mut().resource_mut::<PausedResource>().0 = true;
@@ -856,7 +856,7 @@ mod tests {
let mode = &app.world().resource::<SettingsResource>().0.draw_mode; let mode = &app.world().resource::<SettingsResource>().0.draw_mode;
assert_eq!( assert_eq!(
*mode, *mode,
DrawMode::DrawThree, DrawStockConfig::DrawThree,
"pressing Draw 3 must set mode to DrawThree" "pressing Draw 3 must set mode to DrawThree"
); );
@@ -869,7 +869,7 @@ mod tests {
let mode2 = &app.world().resource::<SettingsResource>().0.draw_mode; let mode2 = &app.world().resource::<SettingsResource>().0.draw_mode;
assert_eq!( assert_eq!(
*mode2, *mode2,
DrawMode::DrawOne, DrawStockConfig::DrawOne,
"pressing Draw 1 must set mode to DrawOne" "pressing Draw 1 must set mode to DrawOne"
); );
@@ -965,11 +965,11 @@ mod tests {
/// Provides a fresh `GameStateResource` (not won) so the modal can /// Provides a fresh `GameStateResource` (not won) so the modal can
/// open. `move_count` doesn't matter — the gate is just `!is_won`. /// open. `move_count` doesn't matter — the gate is just `!is_won`.
fn forfeit_app() -> App { fn forfeit_app() -> App {
use solitaire_core::{DrawMode, game_state::GameState}; use solitaire_core::{DrawStockConfig, game_state::GameState};
let mut app = App::new(); let mut app = App::new();
app.add_plugins(MinimalPlugins).add_plugins(PausePlugin); app.add_plugins(MinimalPlugins).add_plugins(PausePlugin);
app.init_resource::<ButtonInput<KeyCode>>(); app.init_resource::<ButtonInput<KeyCode>>();
app.insert_resource(GameStateResource(GameState::new(1, DrawMode::DrawOne))); app.insert_resource(GameStateResource(GameState::new(1, DrawStockConfig::DrawOne)));
app.update(); app.update();
app app
} }
@@ -1020,11 +1020,11 @@ mod tests {
/// hotkey was received but is currently a no-op. /// hotkey was received but is currently a no-op.
#[test] #[test]
fn forfeit_request_emits_toast_and_skips_modal_when_game_is_won() { fn forfeit_request_emits_toast_and_skips_modal_when_game_is_won() {
use solitaire_core::{DrawMode, game_state::GameState}; use solitaire_core::{DrawStockConfig, game_state::GameState};
let mut app = App::new(); let mut app = App::new();
app.add_plugins(MinimalPlugins).add_plugins(PausePlugin); app.add_plugins(MinimalPlugins).add_plugins(PausePlugin);
app.init_resource::<ButtonInput<KeyCode>>(); app.init_resource::<ButtonInput<KeyCode>>();
let mut game = GameState::new(1, DrawMode::DrawOne); let mut game = GameState::new(1, DrawStockConfig::DrawOne);
game.set_test_won(true); game.set_test_won(true);
app.insert_resource(GameStateResource(game)); app.insert_resource(GameStateResource(game));
app.update(); app.update();
+2 -2
View File
@@ -180,7 +180,7 @@ mod tests {
use crate::input_plugin::HintSolverConfig; use crate::input_plugin::HintSolverConfig;
use solitaire_core::{Foundation, KlondikePile, Tableau}; use solitaire_core::{Foundation, KlondikePile, Tableau};
use solitaire_core::card::{Card, Deck, Rank, Suit}; use solitaire_core::card::{Card, Deck, Rank, Suit};
use solitaire_core::{DrawMode, game_state::GameState}; use solitaire_core::{DrawStockConfig, game_state::GameState};
/// Build a minimal Bevy app exercising only the polling system /// Build a minimal Bevy app exercising only the polling system
/// and the resources/messages it touches. /// and the resources/messages it touches.
@@ -209,7 +209,7 @@ mod tests {
/// foundations hold A..Q for each suit, four Kings sit on /// foundations hold A..Q for each suit, four Kings sit on
/// tableau columns 0..3, stock and waste empty. /// tableau columns 0..3, stock and waste empty.
fn near_finished_state() -> GameState { fn near_finished_state() -> GameState {
let mut game = GameState::new(1, DrawMode::DrawOne); let mut game = GameState::new(1, DrawStockConfig::DrawOne);
game.set_test_stock_cards(Vec::new()); game.set_test_stock_cards(Vec::new());
game.set_test_waste_cards(Vec::new()); game.set_test_waste_cards(Vec::new());
for foundation in [ for foundation in [
+2 -2
View File
@@ -23,7 +23,7 @@
use bevy::input::ButtonInput; use bevy::input::ButtonInput;
use bevy::prelude::*; use bevy::prelude::*;
use bevy::tasks::{AsyncComputeTaskPool, Task, futures_lite::future}; use bevy::tasks::{AsyncComputeTaskPool, Task, futures_lite::future};
use solitaire_core::DrawMode; use solitaire_core::DrawStockConfig;
use solitaire_core::game_state::GameState; use solitaire_core::game_state::GameState;
use solitaire_core::{DEFAULT_SOLVE_MOVES_BUDGET, DEFAULT_SOLVE_STATES_BUDGET, SolveOutcome}; use solitaire_core::{DEFAULT_SOLVE_MOVES_BUDGET, DEFAULT_SOLVE_STATES_BUDGET, SolveOutcome};
@@ -340,7 +340,7 @@ fn tick_debounce_and_spawn_solver_task(
let draw_mode = settings let draw_mode = settings
.as_ref() .as_ref()
.map_or(DrawMode::DrawOne, |s| s.0.draw_mode); .map_or(DrawStockConfig::DrawOne, |s| s.0.draw_mode);
let task = AsyncComputeTaskPool::get().spawn(async move { let task = AsyncComputeTaskPool::get().spawn(async move {
GameState::solve_fresh_deal( GameState::solve_fresh_deal(
seed, seed,
+3 -3
View File
@@ -797,7 +797,7 @@ mod tests {
use crate::layout::compute_layout; use crate::layout::compute_layout;
use bevy::ecs::message::Messages; use bevy::ecs::message::Messages;
use solitaire_core::card::{Card as CoreCard, Deck, Rank, Suit}; use solitaire_core::card::{Card as CoreCard, Deck, Rank, Suit};
use solitaire_core::{DrawMode, game_state::GameState}; use solitaire_core::{DrawStockConfig, game_state::GameState};
/// Build a minimal Bevy app wired with `RadialMenuPlugin` and the /// Build a minimal Bevy app wired with `RadialMenuPlugin` and the
/// resources / messages it depends on. No window, no camera — the /// resources / messages it depends on. No window, no camera — the
@@ -820,7 +820,7 @@ mod tests {
/// destination — Foundation(0) — under the standard rules /// destination — Foundation(0) — under the standard rules
/// (`can_place_on_foundation` accepts the Ace on an empty foundation). /// (`can_place_on_foundation` accepts the Ace on an empty foundation).
fn ace_only_state() -> GameState { fn ace_only_state() -> GameState {
let mut g = GameState::new(0, DrawMode::DrawOne); let mut g = GameState::new(0, DrawStockConfig::DrawOne);
// Wipe everything. // Wipe everything.
g.set_test_stock_cards(Vec::new()); g.set_test_stock_cards(Vec::new());
g.set_test_waste_cards(Vec::new()); g.set_test_waste_cards(Vec::new());
@@ -854,7 +854,7 @@ mod tests {
/// Place a face-down King on Tableau(0). `find_top_face_up_card_at` /// Place a face-down King on Tableau(0). `find_top_face_up_card_at`
/// must skip it. /// must skip it.
fn face_down_only_state() -> GameState { fn face_down_only_state() -> GameState {
let mut g = GameState::new(0, DrawMode::DrawOne); let mut g = GameState::new(0, DrawStockConfig::DrawOne);
g.set_test_stock_cards(Vec::new()); g.set_test_stock_cards(Vec::new());
g.set_test_waste_cards(Vec::new()); g.set_test_waste_cards(Vec::new());
for foundation in [ for foundation in [
+4 -4
View File
@@ -2,7 +2,7 @@ use super::*;
use chrono::NaiveDate; use chrono::NaiveDate;
use solitaire_core::{Foundation, KlondikePile, Tableau}; use solitaire_core::{Foundation, KlondikePile, Tableau};
use solitaire_core::card::{Rank, Suit}; use solitaire_core::card::{Rank, Suit};
use solitaire_core::{DrawMode, game_state::GameMode}; use solitaire_core::{DrawStockConfig, game_state::GameMode};
use solitaire_core::klondike_adapter::{SavedKlondikePile, SavedTableau}; use solitaire_core::klondike_adapter::{SavedKlondikePile, SavedTableau};
use solitaire_data::{Replay, ReplayMove}; use solitaire_data::{Replay, ReplayMove};
@@ -13,7 +13,7 @@ use solitaire_data::{Replay, ReplayMove};
fn synthetic_replay(move_count: usize) -> Replay { fn synthetic_replay(move_count: usize) -> Replay {
Replay::new( Replay::new(
42, 42,
DrawMode::DrawOne, DrawStockConfig::DrawOne,
GameMode::Classic, GameMode::Classic,
120, 120,
1_000, 1_000,
@@ -2314,7 +2314,7 @@ fn format_suit_glyph_all_suits() {
fn format_foundations_row_empty_board() { fn format_foundations_row_empty_board() {
let game = solitaire_core::game_state::GameState::new_with_mode( let game = solitaire_core::game_state::GameState::new_with_mode(
42, 42,
solitaire_core::DrawMode::DrawOne, solitaire_core::DrawStockConfig::DrawOne,
solitaire_core::game_state::GameMode::Classic, solitaire_core::game_state::GameMode::Classic,
); );
assert_eq!(format_foundations_row(&game), "F: -- -- -- --"); assert_eq!(format_foundations_row(&game), "F: -- -- -- --");
@@ -2326,7 +2326,7 @@ fn format_foundations_row_empty_board() {
fn format_stock_waste_row_initial_state() { fn format_stock_waste_row_initial_state() {
let game = solitaire_core::game_state::GameState::new_with_mode( let game = solitaire_core::game_state::GameState::new_with_mode(
42, 42,
solitaire_core::DrawMode::DrawOne, solitaire_core::DrawStockConfig::DrawOne,
solitaire_core::game_state::GameMode::Classic, solitaire_core::game_state::GameMode::Classic,
); );
let text = format_stock_waste_row(&game); let text = format_stock_waste_row(&game);
+4 -4
View File
@@ -556,7 +556,7 @@ mod tests {
use bevy::time::TimeUpdateStrategy; use bevy::time::TimeUpdateStrategy;
use chrono::NaiveDate; use chrono::NaiveDate;
use solitaire_core::{KlondikePile, Tableau}; use solitaire_core::{KlondikePile, Tableau};
use solitaire_core::{DrawMode, game_state::GameMode}; use solitaire_core::{DrawStockConfig, game_state::GameMode};
use solitaire_core::klondike_adapter::{SavedKlondikePile, SavedTableau}; use solitaire_core::klondike_adapter::{SavedKlondikePile, SavedTableau};
use std::time::Duration; use std::time::Duration;
@@ -598,7 +598,7 @@ mod tests {
fn sample_replay_three_moves() -> Replay { fn sample_replay_three_moves() -> Replay {
Replay::new( Replay::new(
12345, 12345,
DrawMode::DrawOne, DrawStockConfig::DrawOne,
GameMode::Classic, GameMode::Classic,
60, 60,
500, 500,
@@ -771,7 +771,7 @@ mod tests {
let mut app = headless_app(); let mut app = headless_app();
let one_move = Replay::new( let one_move = Replay::new(
42, 42,
DrawMode::DrawOne, DrawStockConfig::DrawOne,
GameMode::Classic, GameMode::Classic,
10, 10,
100, 100,
@@ -880,7 +880,7 @@ mod tests {
fn ten_draws_replay() -> Replay { fn ten_draws_replay() -> Replay {
Replay::new( Replay::new(
7, 7,
DrawMode::DrawOne, DrawStockConfig::DrawOne,
GameMode::Classic, GameMode::Classic,
10, 10,
100, 100,
+2 -2
View File
@@ -935,7 +935,7 @@ mod tests {
use bevy::ecs::message::Messages; use bevy::ecs::message::Messages;
use solitaire_core::card::{Card, Deck, Rank, Suit}; use solitaire_core::card::{Card, Deck, Rank, Suit};
use solitaire_core::{DrawMode, game_state::GameState}; use solitaire_core::{DrawStockConfig, game_state::GameState};
/// Build a minimal app with `SelectionPlugin` only — no GamePlugin, no /// Build a minimal app with `SelectionPlugin` only — no GamePlugin, no
/// AssetServer. The `MoveRequestEvent` / `StateChangedEvent` / /// AssetServer. The `MoveRequestEvent` / `StateChangedEvent` /
@@ -968,7 +968,7 @@ mod tests {
/// Ace first). It cannot go to an empty tableau (only Kings). /// Ace first). It cannot go to an empty tableau (only Kings).
/// Empty tableaus T3..T6 only accept Kings, so they are filtered out. /// Empty tableaus T3..T6 only accept Kings, so they are filtered out.
fn deterministic_state() -> GameState { fn deterministic_state() -> GameState {
let mut g = GameState::new(0, DrawMode::DrawOne); let mut g = GameState::new(0, DrawStockConfig::DrawOne);
// Clear stock, waste, all tableaus. // Clear stock, waste, all tableaus.
g.set_test_stock_cards(Vec::new()); g.set_test_stock_cards(Vec::new());
g.set_test_waste_cards(Vec::new()); g.set_test_waste_cards(Vec::new());
+6 -6
View File
@@ -15,7 +15,7 @@ use bevy::input::mouse::{MouseScrollUnit, MouseWheel};
use bevy::prelude::*; use bevy::prelude::*;
use bevy::ui::{ComputedNode, UiGlobalTransform}; use bevy::ui::{ComputedNode, UiGlobalTransform};
use bevy::window::{WindowMoved, WindowResized}; use bevy::window::{WindowMoved, WindowResized};
use solitaire_core::DrawMode; use solitaire_core::DrawStockConfig;
use solitaire_data::{ use solitaire_data::{
AnimSpeed, REPLAY_MOVE_INTERVAL_STEP_SECS, Settings, TIME_BONUS_MULTIPLIER_STEP, AnimSpeed, REPLAY_MOVE_INTERVAL_STEP_SECS, Settings, TIME_BONUS_MULTIPLIER_STEP,
TOOLTIP_DELAY_STEP_SECS, WindowGeometry, load_settings_from, save_settings_to, settings::Theme, TOOLTIP_DELAY_STEP_SECS, WindowGeometry, load_settings_from, save_settings_to, settings::Theme,
@@ -1086,8 +1086,8 @@ fn handle_settings_buttons(
} }
SettingsButton::ToggleDrawMode => { SettingsButton::ToggleDrawMode => {
settings.0.draw_mode = match settings.0.draw_mode { settings.0.draw_mode = match settings.0.draw_mode {
DrawMode::DrawOne => DrawMode::DrawThree, DrawStockConfig::DrawOne => DrawStockConfig::DrawThree,
DrawMode::DrawThree => DrawMode::DrawOne, DrawStockConfig::DrawThree => DrawStockConfig::DrawOne,
}; };
persist(&path, &settings.0); persist(&path, &settings.0);
changed.write(SettingsChangedEvent(settings.0.clone())); changed.write(SettingsChangedEvent(settings.0.clone()));
@@ -1310,10 +1310,10 @@ fn handle_sync_buttons(
} }
} }
fn draw_mode_label(mode: &DrawMode) -> String { fn draw_mode_label(mode: &DrawStockConfig) -> String {
match mode { match mode {
DrawMode::DrawOne => "Draw 1".into(), DrawStockConfig::DrawOne => "Draw 1".into(),
DrawMode::DrawThree => "Draw 3".into(), DrawStockConfig::DrawThree => "Draw 3".into(),
} }
} }
+2 -2
View File
@@ -1327,7 +1327,7 @@ mod tests {
app.world_mut() app.world_mut()
.resource_mut::<crate::resources::GameStateResource>() .resource_mut::<crate::resources::GameStateResource>()
.0 .0
.set_test_draw_mode(solitaire_core::DrawMode::DrawThree); .set_test_draw_mode(solitaire_core::DrawStockConfig::DrawThree);
app.world_mut().write_message(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 500, score: 500,
@@ -1952,7 +1952,7 @@ mod tests {
let date = chrono::NaiveDate::from_ymd_opt(2026, 5, 8).expect("valid date"); let date = chrono::NaiveDate::from_ymd_opt(2026, 5, 8).expect("valid date");
let mut r = solitaire_data::Replay::new( let mut r = solitaire_data::Replay::new(
1, 1,
solitaire_core::DrawMode::DrawOne, solitaire_core::DrawStockConfig::DrawOne,
solitaire_core::game_state::GameMode::Classic, solitaire_core::game_state::GameMode::Classic,
time_seconds, time_seconds,
0, 0,
+2 -2
View File
@@ -604,7 +604,7 @@ mod tests {
/// would silently drop the link. /// would silently drop the link.
#[test] #[test]
fn upload_result_writes_share_url_into_replay_and_persists() { fn upload_result_writes_share_url_into_replay_and_persists() {
use solitaire_core::{DrawMode, game_state::GameMode}; use solitaire_core::{DrawStockConfig, game_state::GameMode};
use solitaire_data::{ use solitaire_data::{
Replay, ReplayHistory, load_replay_history_from, save_replay_history_to, Replay, ReplayHistory, load_replay_history_from, save_replay_history_to,
}; };
@@ -617,7 +617,7 @@ mod tests {
// share_url — the upload-poll path must populate it. // share_url — the upload-poll path must populate it.
let initial = Replay::new( let initial = Replay::new(
42, 42,
DrawMode::DrawOne, DrawStockConfig::DrawOne,
GameMode::Classic, GameMode::Classic,
60, 60,
500, 500,
+3 -3
View File
@@ -299,7 +299,7 @@ mod tests {
use crate::game_plugin::GamePlugin; use crate::game_plugin::GamePlugin;
use crate::progress_plugin::ProgressPlugin; use crate::progress_plugin::ProgressPlugin;
use crate::table_plugin::TablePlugin; use crate::table_plugin::TablePlugin;
use solitaire_core::{DrawMode, game_state::GameState}; use solitaire_core::{DrawStockConfig, game_state::GameState};
fn headless_app() -> App { fn headless_app() -> App {
let mut app = App::new(); let mut app = App::new();
@@ -430,7 +430,7 @@ mod tests {
}; };
// The current game must be in TimeAttack mode for auto-deal to fire. // The current game must be in TimeAttack mode for auto-deal to fire.
app.world_mut().resource_mut::<GameStateResource>().0 = app.world_mut().resource_mut::<GameStateResource>().0 =
GameState::new_with_mode(7, DrawMode::DrawOne, GameMode::TimeAttack); GameState::new_with_mode(7, DrawStockConfig::DrawOne, GameMode::TimeAttack);
app.world_mut().write_message(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 500, score: 500,
@@ -454,7 +454,7 @@ mod tests {
let mut app = headless_app(); let mut app = headless_app();
// Default session is inactive. Game is TimeAttack mode — still no count. // Default session is inactive. Game is TimeAttack mode — still no count.
app.world_mut().resource_mut::<GameStateResource>().0 = app.world_mut().resource_mut::<GameStateResource>().0 =
GameState::new_with_mode(7, DrawMode::DrawOne, GameMode::TimeAttack); GameState::new_with_mode(7, DrawStockConfig::DrawOne, GameMode::TimeAttack);
app.world_mut().write_message(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 500, score: 500,
+5 -5
View File
@@ -1205,7 +1205,7 @@ mod tests {
.insert_resource(StatsResource(StatsSnapshot::default())) .insert_resource(StatsResource(StatsSnapshot::default()))
.insert_resource(GameStateResource(GameState::new( .insert_resource(GameStateResource(GameState::new(
0, 0,
solitaire_core::DrawMode::DrawOne, solitaire_core::DrawStockConfig::DrawOne,
))) )))
.insert_resource(ProgressResource(PlayerProgress::default())); .insert_resource(ProgressResource(PlayerProgress::default()));
app.update(); app.update();
@@ -1534,9 +1534,9 @@ mod tests {
.challenge_index = 4; .challenge_index = 4;
// Switch game mode to Challenge. // Switch game mode to Challenge.
{ {
use solitaire_core::DrawMode; use solitaire_core::DrawStockConfig;
app.world_mut().resource_mut::<GameStateResource>().0 = app.world_mut().resource_mut::<GameStateResource>().0 =
GameState::new_with_mode(1, DrawMode::DrawOne, GameMode::Challenge); GameState::new_with_mode(1, DrawStockConfig::DrawOne, GameMode::Challenge);
} }
app.world_mut().write_message(GameWonEvent { app.world_mut().write_message(GameWonEvent {
@@ -1580,13 +1580,13 @@ mod tests {
/// mode-multiplier rows. /// mode-multiplier rows.
#[test] #[test]
fn cache_win_data_captures_undo_count_and_mode() { fn cache_win_data_captures_undo_count_and_mode() {
use solitaire_core::DrawMode; use solitaire_core::DrawStockConfig;
let mut app = make_app(); let mut app = make_app();
// Set up a Zen-mode game with 2 undos used. // Set up a Zen-mode game with 2 undos used.
{ {
let mut game = app.world_mut().resource_mut::<GameStateResource>(); let mut game = app.world_mut().resource_mut::<GameStateResource>();
game.0 = GameState::new_with_mode(7, DrawMode::DrawOne, GameMode::Zen); game.0 = GameState::new_with_mode(7, DrawStockConfig::DrawOne, GameMode::Zen);
game.0.force_test_undos(2); game.0.force_test_undos(2);
} }
+17 -17
View File
@@ -23,7 +23,7 @@ use solitaire_core::{Foundation, KlondikePile, Tableau};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use solitaire_core::card::Suit; use solitaire_core::card::Suit;
use solitaire_core::error::MoveError; use solitaire_core::error::MoveError;
use solitaire_core::{DrawMode, game_state::{GameMode, GameState}}; use solitaire_core::{DrawStockConfig, game_state::{GameMode, GameState}};
use solitaire_core::klondike_adapter::{ use solitaire_core::klondike_adapter::{
SavedInstruction, SavedKlondikePile, SavedKlondikePileStack, tableau_from_index, SavedInstruction, SavedKlondikePile, SavedKlondikePileStack, tableau_from_index,
}; };
@@ -48,7 +48,7 @@ pub struct Replay {
#[serde(default)] #[serde(default)]
pub schema_version: u32, pub schema_version: u32,
pub seed: u64, pub seed: u64,
pub draw_mode: DrawMode, pub draw_mode: DrawStockConfig,
pub mode: GameMode, pub mode: GameMode,
pub time_seconds: u64, pub time_seconds: u64,
pub final_score: i32, pub final_score: i32,
@@ -332,7 +332,7 @@ pub struct DebugInvariantReport {
#[derive(Debug, Clone, Serialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct DebugSnapshot { pub struct DebugSnapshot {
pub seed: u64, pub seed: u64,
pub draw_mode: DrawMode, pub draw_mode: DrawStockConfig,
pub mode: GameMode, pub mode: GameMode,
pub state: GameSnapshot, pub state: GameSnapshot,
pub legal_moves: Vec<DebugMove>, pub legal_moves: Vec<DebugMove>,
@@ -726,9 +726,9 @@ impl SolitaireGame {
#[cfg(feature = "console_error_panic_hook")] #[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once(); console_error_panic_hook::set_once();
let dm = if draw_three { let dm = if draw_three {
DrawMode::DrawThree DrawStockConfig::DrawThree
} else { } else {
DrawMode::DrawOne DrawStockConfig::DrawOne
}; };
SolitaireGame { SolitaireGame {
game: GameState::new_with_mode(seed as u64, dm, GameMode::Classic), game: GameState::new_with_mode(seed as u64, dm, GameMode::Classic),
@@ -952,7 +952,7 @@ mod tests {
key key
} }
fn run_autonomous(seed: u64, draw_mode: DrawMode, max_steps: usize) -> DebugSnapshot { fn run_autonomous(seed: u64, draw_mode: DrawStockConfig, max_steps: usize) -> DebugSnapshot {
let mut game = SolitaireGame { let mut game = SolitaireGame {
game: GameState::new_with_mode(seed, draw_mode, GameMode::Classic), game: GameState::new_with_mode(seed, draw_mode, GameMode::Classic),
}; };
@@ -983,7 +983,7 @@ mod tests {
#[test] #[test]
fn debug_snapshot_exposes_replayable_seed_and_history() { fn debug_snapshot_exposes_replayable_seed_and_history() {
let seed = 42_u64; let seed = 42_u64;
let final_snapshot = run_autonomous(seed, DrawMode::DrawOne, 1500); let final_snapshot = run_autonomous(seed, DrawStockConfig::DrawOne, 1500);
assert_eq!(final_snapshot.seed, seed); assert_eq!(final_snapshot.seed, seed);
assert!( assert!(
!final_snapshot.state_json.is_empty(), !final_snapshot.state_json.is_empty(),
@@ -1000,7 +1000,7 @@ mod tests {
#[test] #[test]
fn replay_moves_export_is_json_compatible_and_replayable() { fn replay_moves_export_is_json_compatible_and_replayable() {
let seed = 7_u64; let seed = 7_u64;
let draw_mode = DrawMode::DrawThree; let draw_mode = DrawStockConfig::DrawThree;
let mut game = SolitaireGame { let mut game = SolitaireGame {
game: GameState::new_with_mode(seed, draw_mode, GameMode::Classic), game: GameState::new_with_mode(seed, draw_mode, GameMode::Classic),
}; };
@@ -1098,9 +1098,9 @@ mod tests {
fn debug_api_autonomous_seed_batch_smoke() { fn debug_api_autonomous_seed_batch_smoke() {
for seed in 0_u64..128_u64 { for seed in 0_u64..128_u64 {
let draw_mode = if seed % 2 == 0 { let draw_mode = if seed % 2 == 0 {
DrawMode::DrawOne DrawStockConfig::DrawOne
} else { } else {
DrawMode::DrawThree DrawStockConfig::DrawThree
}; };
let snapshot = run_autonomous(seed, draw_mode, 2000); let snapshot = run_autonomous(seed, draw_mode, 2000);
assert_invariants(&snapshot, seed); assert_invariants(&snapshot, seed);
@@ -1112,9 +1112,9 @@ mod tests {
fn debug_api_autonomous_thousands_seed_soak() { fn debug_api_autonomous_thousands_seed_soak() {
for seed in 10_000_u64..12_000_u64 { for seed in 10_000_u64..12_000_u64 {
let draw_mode = if seed % 2 == 0 { let draw_mode = if seed % 2 == 0 {
DrawMode::DrawOne DrawStockConfig::DrawOne
} else { } else {
DrawMode::DrawThree DrawStockConfig::DrawThree
}; };
let snapshot = run_autonomous(seed, draw_mode, 3000); let snapshot = run_autonomous(seed, draw_mode, 3000);
assert_invariants(&snapshot, seed); assert_invariants(&snapshot, seed);
@@ -1125,7 +1125,7 @@ mod tests {
fn serialize_from_saved_round_trip() { fn serialize_from_saved_round_trip() {
let seed = 55_u64; let seed = 55_u64;
let mut game = SolitaireGame { let mut game = SolitaireGame {
game: GameState::new_with_mode(seed, DrawMode::DrawOne, GameMode::Classic), game: GameState::new_with_mode(seed, DrawStockConfig::DrawOne, GameMode::Classic),
}; };
// Advance a few moves so there is non-trivial state to round-trip. // Advance a few moves so there is non-trivial state to round-trip.
for _ in 0..20 { for _ in 0..20 {
@@ -1160,7 +1160,7 @@ mod tests {
fn undo_reverts_to_prior_state() { fn undo_reverts_to_prior_state() {
let seed = 99_u64; let seed = 99_u64;
let mut game = SolitaireGame { let mut game = SolitaireGame {
game: GameState::new_with_mode(seed, DrawMode::DrawOne, GameMode::Classic), game: GameState::new_with_mode(seed, DrawStockConfig::DrawOne, GameMode::Classic),
}; };
let before_key = board_key(&game.debug_snapshot_native().state); let before_key = board_key(&game.debug_snapshot_native().state);
@@ -1198,7 +1198,7 @@ mod tests {
fn draw_one_advances_waste_by_one() { fn draw_one_advances_waste_by_one() {
let seed = 1_u64; let seed = 1_u64;
let mut game = SolitaireGame { let mut game = SolitaireGame {
game: GameState::new_with_mode(seed, DrawMode::DrawOne, GameMode::Classic), game: GameState::new_with_mode(seed, DrawStockConfig::DrawOne, GameMode::Classic),
}; };
let stock_before = game.game.stock_cards().len(); let stock_before = game.game.stock_cards().len();
@@ -1224,7 +1224,7 @@ mod tests {
fn draw_three_advances_waste_by_three() { fn draw_three_advances_waste_by_three() {
let seed = 1_u64; let seed = 1_u64;
let mut game = SolitaireGame { let mut game = SolitaireGame {
game: GameState::new_with_mode(seed, DrawMode::DrawThree, GameMode::Classic), game: GameState::new_with_mode(seed, DrawStockConfig::DrawThree, GameMode::Classic),
}; };
let stock_before = game.game.stock_cards().len(); let stock_before = game.game.stock_cards().len();
@@ -1254,7 +1254,7 @@ mod tests {
fn debug_apply_move_json_stock_click_advances_waste() { fn debug_apply_move_json_stock_click_advances_waste() {
let seed = 3_u64; let seed = 3_u64;
let mut game = SolitaireGame { let mut game = SolitaireGame {
game: GameState::new_with_mode(seed, DrawMode::DrawOne, GameMode::Classic), game: GameState::new_with_mode(seed, DrawStockConfig::DrawOne, GameMode::Classic),
}; };
let waste_before = game.game.waste_cards().len(); let waste_before = game.game.waste_cards().len();