From e841a7ab4fd0f6b6a19fe4ae3bf36a4c48a90148 Mon Sep 17 00:00:00 2001 From: funman300 Date: Thu, 11 Jun 2026 15:04:47 -0700 Subject: [PATCH] refactor: delete solitaire_data::solver wrapper; solve via card_game directly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the standalone solver wrapper module. Its thin shaping — build a solve-budgeted Session, run card_game::Session::solve(), extract the first useful move — moves onto the domain type in solitaire_core as GameState::solve_first_move() / GameState::solve_fresh_deal(), with the budget consts and the SolveOutcome alias re-exported from solitaire_core. Solving is deterministic, IO-free game logic, so core (which already owns GameState and exposes session().solve()) is its correct home; solitaire_data is the persistence/sync layer and never should have owned it. Consumers now call the core API directly: - engine: pending_hint (solve_first_move), game_plugin + play_by_seed_plugin (solve_fresh_deal), input_plugin (budget consts) - assetgen: gen_seeds + gen_difficulty_seeds (solve_fresh_deal) The solver tests move to solitaire_core. cargo test --workspace and clippy --workspace --all-targets -- -D warnings both green. Resolves the "delete the solver" directive — card_game provides the solver. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/bin/gen_difficulty_seeds.rs | 4 +- solitaire_assetgen/src/bin/gen_seeds.rs | 5 +- solitaire_core/src/game_state.rs | 130 +++++++++++++++- solitaire_core/src/lib.rs | 6 +- solitaire_data/src/lib.rs | 6 - solitaire_data/src/settings.rs | 2 +- solitaire_data/src/solver.rs | 140 ------------------ solitaire_engine/src/game_plugin.rs | 8 +- solitaire_engine/src/input_plugin.rs | 4 +- solitaire_engine/src/pending_hint.rs | 3 +- solitaire_engine/src/play_by_seed_plugin.rs | 9 +- solitaire_engine/src/settings_plugin.rs | 2 +- 12 files changed, 151 insertions(+), 168 deletions(-) delete mode 100644 solitaire_data/src/solver.rs diff --git a/solitaire_assetgen/src/bin/gen_difficulty_seeds.rs b/solitaire_assetgen/src/bin/gen_difficulty_seeds.rs index dbd07c5..aa59e91 100644 --- a/solitaire_assetgen/src/bin/gen_difficulty_seeds.rs +++ b/solitaire_assetgen/src/bin/gen_difficulty_seeds.rs @@ -20,7 +20,7 @@ //! --help Print this message use solitaire_core::DrawMode; -use solitaire_data::solver::try_solve; +use solitaire_core::game_state::GameState; // Budget boundaries defining each tier. A seed belongs to the lowest tier // whose budget proves it Winnable. @@ -99,7 +99,7 @@ fn main() { if buckets[i].len() >= per_tier { continue; } - match try_solve(seed, draw_mode, move_budget, state_budget) { + match GameState::solve_fresh_deal(seed, draw_mode, move_budget, state_budget) { Ok(Some(_)) => { buckets[i].push(seed); eprintln!( diff --git a/solitaire_assetgen/src/bin/gen_seeds.rs b/solitaire_assetgen/src/bin/gen_seeds.rs index 8708f57..12b4871 100644 --- a/solitaire_assetgen/src/bin/gen_seeds.rs +++ b/solitaire_assetgen/src/bin/gen_seeds.rs @@ -18,7 +18,8 @@ //! --help Print this message use solitaire_core::DrawMode; -use solitaire_data::solver::{DEFAULT_SOLVE_MOVES_BUDGET, DEFAULT_SOLVE_STATES_BUDGET, try_solve}; +use solitaire_core::game_state::GameState; +use solitaire_core::{DEFAULT_SOLVE_MOVES_BUDGET, DEFAULT_SOLVE_STATES_BUDGET}; fn main() { let mut args = std::env::args().skip(1).peekable(); @@ -77,7 +78,7 @@ fn main() { while found.len() < count { tried += 1; if matches!( - try_solve( + GameState::solve_fresh_deal( seed, draw_mode, DEFAULT_SOLVE_MOVES_BUDGET, diff --git a/solitaire_core/src/game_state.rs b/solitaire_core/src/game_state.rs index e56e60c..39c99c6 100644 --- a/solitaire_core/src/game_state.rs +++ b/solitaire_core/src/game_state.rs @@ -5,7 +5,7 @@ use crate::klondike_adapter::{ skip_cards_from_count as adapter_skip_cards_from_count, tableau_from_index as adapter_tableau_from_index, }; -use card_game::{Card, Game as _, Session, SessionConfig}; +use card_game::{Card, Game as _, Session, SessionConfig, SolveError}; use klondike::{ DrawStockConfig, DstFoundation, DstTableau, Foundation, Klondike, KlondikeConfig, KlondikeInstruction, KlondikePile, KlondikePileStack, SkipCards, Tableau, TableauStack, @@ -30,6 +30,22 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; /// still carry those keys load fine — the extra fields are ignored. pub const GAME_STATE_SCHEMA_VERSION: u32 = 5; +/// Default move budget for a solvability check. Matches the winnable-deal retry +/// loop in the engine. +pub const DEFAULT_SOLVE_MOVES_BUDGET: u64 = 100_000; +/// Default unique-state budget for a solvability check. +pub const DEFAULT_SOLVE_STATES_BUDGET: u64 = 200_000; + +/// Outcome of a solvability check ([`GameState::solve_first_move`]): +/// +/// * `Ok(Some(instruction))` — winnable; `instruction` is the first useful move +/// on a winning path (used by the hint system). +/// * `Ok(None)` — provably unwinnable (search exhausted with no solution, or the +/// game is already won so no next move exists). +/// * `Err(SolveError)` — inconclusive; the move/state budget was exceeded before +/// a verdict was reached. +pub type SolveOutcome = Result, SolveError>; + /// Default value for `GameState::schema_version` when deserialising older /// save files that pre-date the field. fn schema_v1() -> u32 { @@ -1090,6 +1106,56 @@ impl GameState { pub fn session(&self) -> &Session { &self.session } + + /// Solvability of the current position: the first useful move on a winning + /// path, `Ok(None)` if unwinnable (or already won), or `Err` if the solver + /// hit its budget before reaching a verdict. See [`SolveOutcome`]. + /// + /// Delegates the search to upstream [`card_game::Session::solve`] on a + /// solve-budgeted copy of the current board, then extracts the first + /// non-useless instruction from the returned solution. Backs the hint system + /// and the Play-by-seed verdict badge. + pub fn solve_first_move(&self, moves_budget: u64, states_budget: u64) -> SolveOutcome { + // An already-won game has no "next move"; report it as unwinnable so the + // winnable contract (`Some(_)` ⇒ a real move exists) holds. + if self.is_won() { + return Ok(None); + } + + let config = SessionConfig { + inner: KlondikeAdapter::config_for(self.draw_mode(), self.take_from_foundation), + undo_penalty: 0, + solve_moves_budget: moves_budget, + solve_states_budget: states_budget, + }; + let session = Session::new(self.session.state().state().clone(), config); + + session.solve().map(|solution| { + solution.and_then(|solution| { + solution + .raw_solution() + .iter() + .map(|snapshot| *snapshot.instruction()) + .find(|instruction| !instruction.is_useless()) + }) + }) + } + + /// Solvability of a fresh Classic-mode deal from `seed` + `draw_mode`. + /// + /// Fresh-deal solving models standard Klondike rules, so the non-standard + /// take-from-foundation house rule stays disabled. Backs the + /// "Winnable deals only" retry loop. + pub fn solve_fresh_deal( + seed: u64, + draw_mode: DrawMode, + moves_budget: u64, + states_budget: u64, + ) -> SolveOutcome { + let mut game = Self::new(seed, draw_mode); + game.take_from_foundation = false; + game.solve_first_move(moves_budget, states_budget) + } } #[cfg(test)] @@ -1240,4 +1306,66 @@ mod tests { ); assert!(game.move_cards(from, to, 1).is_err()); } + + // ── Solvability check (solve_first_move / solve_fresh_deal) ────────────── + + /// `SolveError` has no `PartialEq`, so compare the winnable verdict and the + /// extracted first move (both `Eq`) rather than the whole `Result`. + fn verdict_key(outcome: &SolveOutcome) -> (bool, Option) { + (outcome.is_err(), outcome.clone().ok().flatten()) + } + + #[test] + fn solve_fresh_deal_is_deterministic() { + let a = GameState::solve_fresh_deal( + 7, + DrawMode::DrawOne, + DEFAULT_SOLVE_MOVES_BUDGET, + DEFAULT_SOLVE_STATES_BUDGET, + ); + let b = GameState::solve_fresh_deal( + 7, + DrawMode::DrawOne, + DEFAULT_SOLVE_MOVES_BUDGET, + DEFAULT_SOLVE_STATES_BUDGET, + ); + assert_eq!(verdict_key(&a), verdict_key(&b)); + } + + #[test] + fn winnable_verdict_carries_a_first_move() { + // 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 winnable = matches!(outcome, Ok(Some(_))); + let has_move = outcome.ok().flatten().is_some(); + assert_eq!(winnable, has_move); + } + + #[test] + fn solve_first_move_uses_live_game_state() { + let mut game = GameState::new(42, DrawMode::DrawOne); + game.draw().expect("draw must succeed"); + + let outcome = game.solve_first_move(5_000, 5_000); + let winnable = matches!(outcome, Ok(Some(_))); + let has_move = outcome.ok().flatten().is_some(); + assert_eq!(winnable, has_move); + } + + #[test] + fn zero_state_budget_is_inconclusive() { + let outcome = GameState::solve_fresh_deal(7, DrawMode::DrawOne, 5_000, 0); + assert!(matches!(outcome, Err(SolveError::StatesBudgetExceeded))); + } + + #[test] + fn budget_is_passed_through_not_clamped() { + // This seed is Inconclusive at 1k states but Winnable at 5k — proving the + // budget reaches the solver unchanged. + let easy = GameState::solve_fresh_deal(0xD1FF_0000_0000_0012, DrawMode::DrawOne, 1_000, 1_000); + let medium = + GameState::solve_fresh_deal(0xD1FF_0000_0000_0012, DrawMode::DrawOne, 5_000, 5_000); + assert!(easy.is_err()); + assert!(matches!(medium, Ok(Some(_)))); + } } diff --git a/solitaire_core/src/lib.rs b/solitaire_core/src/lib.rs index 136e0d6..acefd8f 100644 --- a/solitaire_core/src/lib.rs +++ b/solitaire_core/src/lib.rs @@ -12,9 +12,13 @@ pub mod klondike_adapter; // re-exported — they are only used internally (in `klondike_adapter.rs` and // when decoding instructions to piles in `instruction_to_piles`) and do not // appear in any public method signature. -pub use card_game::{Card, Session}; +pub use card_game::{Card, Session, SolveError}; pub use klondike::{Foundation, Klondike, KlondikeInstruction, KlondikePile, Tableau}; pub use klondike_adapter::DrawMode; +// Solvability check API (delegates to `card_game::Session::solve`); replaces the +// former `solitaire_data::solver` wrapper module. +pub use game_state::{DEFAULT_SOLVE_MOVES_BUDGET, DEFAULT_SOLVE_STATES_BUDGET, SolveOutcome}; + #[cfg(test)] mod proptest_tests; diff --git a/solitaire_data/src/lib.rs b/solitaire_data/src/lib.rs index d453ae1..8b52881 100644 --- a/solitaire_data/src/lib.rs +++ b/solitaire_data/src/lib.rs @@ -99,12 +99,6 @@ impl SyncProvider for Box { } } -pub mod solver; -pub use solver::{ - DEFAULT_SOLVE_MOVES_BUDGET, DEFAULT_SOLVE_STATES_BUDGET, SolveOutcome, try_solve, - try_solve_from_state, -}; - pub mod stats; pub use stats::{StatsExt, StatsSnapshot}; diff --git a/solitaire_data/src/settings.rs b/solitaire_data/src/settings.rs index 461ca5f..8923b06 100644 --- a/solitaire_data/src/settings.rs +++ b/solitaire_data/src/settings.rs @@ -200,7 +200,7 @@ pub struct Settings { #[serde(default = "default_time_bonus_multiplier")] pub time_bonus_multiplier: f32, /// When `true`, the engine rejects new-game deals the - /// [`solitaire_data::solver`] cannot prove winnable, retrying + /// the solver cannot prove winnable, retrying /// fresh seeds up to [`SOLVER_DEAL_RETRY_CAP`] attempts before /// giving up and using the last tried seed. Off by default — /// the solver adds a few hundred milliseconds of latency on the diff --git a/solitaire_data/src/solver.rs b/solitaire_data/src/solver.rs deleted file mode 100644 index abfd721..0000000 --- a/solitaire_data/src/solver.rs +++ /dev/null @@ -1,140 +0,0 @@ -//! Klondike solvability check using upstream `card_game::Session::solve()`. -//! -//! Backs the **Settings → Gameplay → "Winnable deals only"** toggle, the -//! Play-by-seed verdict badge, and the hint system (which wants the first -//! move on a winning path). All search is delegated to `card_game`; this -//! module only adapts the inputs (a seed or a live [`GameState`]) and extracts -//! the first move from the returned solution. - -use card_game::{Session, SessionConfig, SolveError}; -use klondike::KlondikeInstruction; -use solitaire_core::DrawMode; -use solitaire_core::game_state::GameState; -use solitaire_core::klondike_adapter::KlondikeAdapter; - -/// Default move budget for a solve. Matches the winnable-deal retry loop. -pub const DEFAULT_SOLVE_MOVES_BUDGET: u64 = 100_000; -/// Default unique-state budget for a solve. -pub const DEFAULT_SOLVE_STATES_BUDGET: u64 = 200_000; - -/// Outcome of a solvability check: -/// -/// * `Ok(Some(instruction))` — winnable; `instruction` is the first move on a -/// winning path (used by the hint system). -/// * `Ok(None)` — provably unwinnable (search exhausted with no solution, or -/// the game is already won so no next move exists). -/// * `Err(SolveError)` — inconclusive; the move/state budget was exceeded -/// before a verdict was reached. -pub type SolveOutcome = Result, SolveError>; - -/// Solves a fresh Classic-mode game dealt from `seed` + `draw_mode`. -/// -/// Fresh-deal solving models standard Klondike rules, so the non-standard -/// take-from-foundation house rule stays disabled here. -pub fn try_solve( - seed: u64, - draw_mode: DrawMode, - moves_budget: u64, - states_budget: u64, -) -> SolveOutcome { - let mut game = GameState::new(seed, draw_mode); - game.take_from_foundation = false; - try_solve_from_state(&game, moves_budget, states_budget) -} - -/// Solves from an existing in-progress [`GameState`], returning the first move -/// on a winning path when one exists. -pub fn try_solve_from_state( - state: &GameState, - moves_budget: u64, - states_budget: u64, -) -> SolveOutcome { - // An already-won game has no "next move"; report it as unwinnable so the - // winnable contract (`Some(_)` ⇒ a real move exists) holds. - if state.is_won() { - return Ok(None); - } - - let config = SessionConfig { - inner: KlondikeAdapter::config_for(state.draw_mode(), state.take_from_foundation), - undo_penalty: 0, - solve_moves_budget: moves_budget, - solve_states_budget: states_budget, - }; - let session = Session::new(state.session().state().state().clone(), config); - - session.solve().map(|solution| { - solution.and_then(|solution| { - solution - .raw_solution() - .iter() - .map(|snapshot| *snapshot.instruction()) - .find(|instruction| !instruction.is_useless()) - }) - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - /// `SolveError` has no `PartialEq`, so compare the winnable verdict and the - /// extracted first move (both `Eq`) rather than the whole `Result`. - fn verdict_key(outcome: &SolveOutcome) -> (bool, Option) { - (outcome.is_err(), outcome.clone().ok().flatten()) - } - - #[test] - fn try_solve_is_deterministic() { - let a = try_solve(7, DrawMode::DrawOne, DEFAULT_SOLVE_MOVES_BUDGET, DEFAULT_SOLVE_STATES_BUDGET); - let b = try_solve(7, DrawMode::DrawOne, DEFAULT_SOLVE_MOVES_BUDGET, DEFAULT_SOLVE_STATES_BUDGET); - assert_eq!(verdict_key(&a), verdict_key(&b)); - } - - #[test] - fn winnable_verdict_carries_a_first_move() { - // Contract: a first move is present iff the verdict is winnable. - let outcome = try_solve(7, DrawMode::DrawOne, 5_000, 5_000); - let winnable = matches!(outcome, Ok(Some(_))); - let has_move = outcome.ok().flatten().is_some(); - assert_eq!(winnable, has_move); - } - - #[test] - fn try_solve_from_state_uses_live_game_state() { - let mut game = GameState::new(42, DrawMode::DrawOne); - game.draw().expect("draw must succeed"); - - let outcome = try_solve_from_state(&game, 5_000, 5_000); - let winnable = matches!(outcome, Ok(Some(_))); - let has_move = outcome.ok().flatten().is_some(); - assert_eq!(winnable, has_move); - } - - #[test] - fn zero_state_budget_is_inconclusive() { - let outcome = try_solve(7, DrawMode::DrawOne, 5_000, 0); - assert!(matches!(outcome, Err(SolveError::StatesBudgetExceeded))); - } - - #[test] - fn budget_is_passed_through_not_clamped() { - // This seed is Inconclusive at 1k states but Winnable at 5k — proving - // the budget reaches the solver unchanged. - let easy = try_solve(0xD1FF_0000_0000_0012, DrawMode::DrawOne, 1_000, 1_000); - let medium = try_solve(0xD1FF_0000_0000_0012, DrawMode::DrawOne, 5_000, 5_000); - assert!(easy.is_err()); - assert!(matches!(medium, Ok(Some(_)))); - } - - #[test] - fn budget_above_five_thousand_is_not_clamped() { - let below_cap = try_solve(0xD1FF_0000_0000_00DE, DrawMode::DrawOne, 5_000, 5_000); - let above_cap = try_solve(0xD1FF_0000_0000_00DE, DrawMode::DrawOne, 50_000, 50_000); - assert!(below_cap.is_err(), "seed must be Inconclusive at 5 000 states"); - assert!( - matches!(above_cap, Ok(Some(_))), - "seed must be Winnable at 50 000 states — re-introducing the 5k cap would break this" - ); - } -} diff --git a/solitaire_engine/src/game_plugin.rs b/solitaire_engine/src/game_plugin.rs index b262d53..620128e 100644 --- a/solitaire_engine/src/game_plugin.rs +++ b/solitaire_engine/src/game_plugin.rs @@ -15,9 +15,7 @@ use bevy::tasks::{AsyncComputeTaskPool, Task, futures_lite::future}; use bevy::window::AppLifecycle; use solitaire_core::KlondikePile; use solitaire_core::{DrawMode, game_state::{GameMode, GameState}}; -use solitaire_data::solver::{ - DEFAULT_SOLVE_MOVES_BUDGET, DEFAULT_SOLVE_STATES_BUDGET, try_solve, -}; +use solitaire_core::{DEFAULT_SOLVE_MOVES_BUDGET, DEFAULT_SOLVE_STATES_BUDGET}; #[allow(deprecated)] use solitaire_data::latest_replay_path; use solitaire_data::{ @@ -318,7 +316,7 @@ fn seed_from_system_time() -> u64 { } /// Walks forward from `initial_seed` (incrementing by 1 with wrapping -/// arithmetic) until the [`solitaire_data::solver`] returns a verdict +/// arithmetic) until the [`GameState::solve_fresh_deal`] returns a verdict /// the engine accepts as winnable, or until [`SOLVER_DEAL_RETRY_CAP`] /// attempts have elapsed. /// @@ -393,7 +391,7 @@ fn poll_pending_new_game_seed( pub(crate) fn choose_winnable_seed(initial_seed: u64, draw_mode: DrawMode) -> u64 { let mut seed = initial_seed; for _ in 0..SOLVER_DEAL_RETRY_CAP { - match try_solve( + match GameState::solve_fresh_deal( seed, draw_mode, DEFAULT_SOLVE_MOVES_BUDGET, diff --git a/solitaire_engine/src/input_plugin.rs b/solitaire_engine/src/input_plugin.rs index e3be4ac..1e876f9 100644 --- a/solitaire_engine/src/input_plugin.rs +++ b/solitaire_engine/src/input_plugin.rs @@ -95,8 +95,8 @@ pub struct HintSolverConfig { impl Default for HintSolverConfig { fn default() -> Self { Self { - moves_budget: solitaire_data::solver::DEFAULT_SOLVE_MOVES_BUDGET, - states_budget: solitaire_data::solver::DEFAULT_SOLVE_STATES_BUDGET, + moves_budget: solitaire_core::DEFAULT_SOLVE_MOVES_BUDGET, + states_budget: solitaire_core::DEFAULT_SOLVE_STATES_BUDGET, } } } diff --git a/solitaire_engine/src/pending_hint.rs b/solitaire_engine/src/pending_hint.rs index 63e5b90..1e9fac0 100644 --- a/solitaire_engine/src/pending_hint.rs +++ b/solitaire_engine/src/pending_hint.rs @@ -26,7 +26,6 @@ use bevy::prelude::*; use bevy::tasks::{AsyncComputeTaskPool, Task, futures_lite::future}; use solitaire_core::KlondikeInstruction; use solitaire_core::game_state::GameState; -use solitaire_data::solver::try_solve_from_state; use crate::card_plugin::CardEntity; use crate::events::{HintVisualEvent, InfoToastEvent, StateChangedEvent}; @@ -66,7 +65,7 @@ impl PendingHintTask { // Winnable (`Ok(Some)`) carries the first move on a winning path; // unwinnable (`Ok(None)`) and inconclusive (`Err`) both fall back // to the live-state heuristic so H always produces feedback. - match try_solve_from_state(&state, moves_budget, states_budget) { + match state.solve_first_move(moves_budget, states_budget) { Ok(Some(first_move)) => HintTaskOutput::SolverMove(first_move), Ok(None) | Err(_) => HintTaskOutput::NeedsHeuristic, } diff --git a/solitaire_engine/src/play_by_seed_plugin.rs b/solitaire_engine/src/play_by_seed_plugin.rs index 04cd7ca..35720a1 100644 --- a/solitaire_engine/src/play_by_seed_plugin.rs +++ b/solitaire_engine/src/play_by_seed_plugin.rs @@ -11,7 +11,7 @@ //! 3. `handle_text_input` appends decimal digits / handles Backspace while //! the modal is open, updating [`SeedInputBuffer`] each frame. //! 4. `tick_debounce_and_spawn_solver_task` waits for 12 frames (~200 ms at -//! 60 Hz) of no input before spawning a [`try_solve`] task on +//! 60 Hz) of no input before spawning a [`GameState::solve_fresh_deal`] task on //! [`AsyncComputeTaskPool`]. Any fresh keypress drops the in-flight task //! by resetting the resource. //! 5. `poll_solver_task` polls the in-flight task each frame and updates the @@ -24,9 +24,8 @@ use bevy::input::ButtonInput; use bevy::prelude::*; use bevy::tasks::{AsyncComputeTaskPool, Task, futures_lite::future}; use solitaire_core::DrawMode; -use solitaire_data::solver::{ - DEFAULT_SOLVE_MOVES_BUDGET, DEFAULT_SOLVE_STATES_BUDGET, SolveOutcome, try_solve, -}; +use solitaire_core::game_state::GameState; +use solitaire_core::{DEFAULT_SOLVE_MOVES_BUDGET, DEFAULT_SOLVE_STATES_BUDGET, SolveOutcome}; use crate::events::{NewGameRequestEvent, StartPlayBySeedRequestEvent}; use crate::font_plugin::FontResource; @@ -343,7 +342,7 @@ fn tick_debounce_and_spawn_solver_task( .as_ref() .map_or(DrawMode::DrawOne, |s| s.0.draw_mode); let task = AsyncComputeTaskPool::get().spawn(async move { - try_solve( + GameState::solve_fresh_deal( seed, draw_mode, DEFAULT_SOLVE_MOVES_BUDGET, diff --git a/solitaire_engine/src/settings_plugin.rs b/solitaire_engine/src/settings_plugin.rs index ff5a0b9..32dbe19 100644 --- a/solitaire_engine/src/settings_plugin.rs +++ b/solitaire_engine/src/settings_plugin.rs @@ -241,7 +241,7 @@ enum SettingsButton { ToggleTouchInputMode, /// Toggle the [`Settings::winnable_deals_only`] flag. When on, new /// random Classic-mode deals are filtered through - /// [`solitaire_data::solver::try_solve`] until one is provably + /// [`solitaire_core::game_state::GameState::solve_fresh_deal`] until one is provably /// winnable (or the retry cap is hit). Off by default. ToggleWinnableDealsOnly, /// Toggle the inverse of [`Settings::disable_smart_default_size`].