refactor: delete solitaire_data::solver wrapper; solve via card_game directly
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) <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,7 @@
|
|||||||
//! --help Print this message
|
//! --help Print this message
|
||||||
|
|
||||||
use solitaire_core::DrawMode;
|
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
|
// Budget boundaries defining each tier. A seed belongs to the lowest tier
|
||||||
// whose budget proves it Winnable.
|
// whose budget proves it Winnable.
|
||||||
@@ -99,7 +99,7 @@ fn main() {
|
|||||||
if buckets[i].len() >= per_tier {
|
if buckets[i].len() >= per_tier {
|
||||||
continue;
|
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(_)) => {
|
Ok(Some(_)) => {
|
||||||
buckets[i].push(seed);
|
buckets[i].push(seed);
|
||||||
eprintln!(
|
eprintln!(
|
||||||
|
|||||||
@@ -18,7 +18,8 @@
|
|||||||
//! --help Print this message
|
//! --help Print this message
|
||||||
|
|
||||||
use solitaire_core::DrawMode;
|
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() {
|
fn main() {
|
||||||
let mut args = std::env::args().skip(1).peekable();
|
let mut args = std::env::args().skip(1).peekable();
|
||||||
@@ -77,7 +78,7 @@ fn main() {
|
|||||||
while found.len() < count {
|
while found.len() < count {
|
||||||
tried += 1;
|
tried += 1;
|
||||||
if matches!(
|
if matches!(
|
||||||
try_solve(
|
GameState::solve_fresh_deal(
|
||||||
seed,
|
seed,
|
||||||
draw_mode,
|
draw_mode,
|
||||||
DEFAULT_SOLVE_MOVES_BUDGET,
|
DEFAULT_SOLVE_MOVES_BUDGET,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use crate::klondike_adapter::{
|
|||||||
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,
|
||||||
};
|
};
|
||||||
use card_game::{Card, Game as _, Session, SessionConfig};
|
use card_game::{Card, Game as _, Session, SessionConfig, SolveError};
|
||||||
use klondike::{
|
use klondike::{
|
||||||
DrawStockConfig, DstFoundation, DstTableau, Foundation, Klondike, KlondikeConfig,
|
DrawStockConfig, DstFoundation, DstTableau, Foundation, Klondike, KlondikeConfig,
|
||||||
KlondikeInstruction, KlondikePile, KlondikePileStack, SkipCards, Tableau, TableauStack,
|
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.
|
/// still carry those keys load fine — the extra fields are ignored.
|
||||||
pub const GAME_STATE_SCHEMA_VERSION: u32 = 5;
|
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<Option<KlondikeInstruction>, SolveError>;
|
||||||
|
|
||||||
/// Default value for `GameState::schema_version` when deserialising older
|
/// Default value for `GameState::schema_version` when deserialising older
|
||||||
/// save files that pre-date the field.
|
/// save files that pre-date the field.
|
||||||
fn schema_v1() -> u32 {
|
fn schema_v1() -> u32 {
|
||||||
@@ -1090,6 +1106,56 @@ impl GameState {
|
|||||||
pub fn session(&self) -> &Session<Klondike> {
|
pub fn session(&self) -> &Session<Klondike> {
|
||||||
&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)]
|
#[cfg(test)]
|
||||||
@@ -1240,4 +1306,66 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert!(game.move_cards(from, to, 1).is_err());
|
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<KlondikeInstruction>) {
|
||||||
|
(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(_))));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,13 @@ pub mod klondike_adapter;
|
|||||||
// re-exported — they are only used internally (in `klondike_adapter.rs` and
|
// re-exported — they are only used internally (in `klondike_adapter.rs` and
|
||||||
// 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};
|
pub use card_game::{Card, Session, SolveError};
|
||||||
pub use klondike::{Foundation, Klondike, KlondikeInstruction, KlondikePile, Tableau};
|
pub use klondike::{Foundation, Klondike, KlondikeInstruction, KlondikePile, Tableau};
|
||||||
pub use klondike_adapter::DrawMode;
|
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)]
|
#[cfg(test)]
|
||||||
mod proptest_tests;
|
mod proptest_tests;
|
||||||
|
|||||||
@@ -99,12 +99,6 @@ impl SyncProvider for Box<dyn SyncProvider + Send + Sync> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 mod stats;
|
||||||
pub use stats::{StatsExt, StatsSnapshot};
|
pub use stats::{StatsExt, StatsSnapshot};
|
||||||
|
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ pub struct Settings {
|
|||||||
#[serde(default = "default_time_bonus_multiplier")]
|
#[serde(default = "default_time_bonus_multiplier")]
|
||||||
pub time_bonus_multiplier: f32,
|
pub time_bonus_multiplier: f32,
|
||||||
/// When `true`, the engine rejects new-game deals the
|
/// 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
|
/// fresh seeds up to [`SOLVER_DEAL_RETRY_CAP`] attempts before
|
||||||
/// giving up and using the last tried seed. Off by default —
|
/// giving up and using the last tried seed. Off by default —
|
||||||
/// the solver adds a few hundred milliseconds of latency on the
|
/// the solver adds a few hundred milliseconds of latency on the
|
||||||
|
|||||||
@@ -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<Option<KlondikeInstruction>, 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<KlondikeInstruction>) {
|
|
||||||
(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"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -15,9 +15,7 @@ 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::{DrawMode, game_state::{GameMode, GameState}};
|
||||||
use solitaire_data::solver::{
|
use solitaire_core::{DEFAULT_SOLVE_MOVES_BUDGET, DEFAULT_SOLVE_STATES_BUDGET};
|
||||||
DEFAULT_SOLVE_MOVES_BUDGET, DEFAULT_SOLVE_STATES_BUDGET, try_solve,
|
|
||||||
};
|
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
use solitaire_data::latest_replay_path;
|
use solitaire_data::latest_replay_path;
|
||||||
use solitaire_data::{
|
use solitaire_data::{
|
||||||
@@ -318,7 +316,7 @@ fn seed_from_system_time() -> u64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Walks forward from `initial_seed` (incrementing by 1 with wrapping
|
/// 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`]
|
/// the engine accepts as winnable, or until [`SOLVER_DEAL_RETRY_CAP`]
|
||||||
/// attempts have elapsed.
|
/// 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 {
|
pub(crate) fn choose_winnable_seed(initial_seed: u64, draw_mode: DrawMode) -> 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 try_solve(
|
match GameState::solve_fresh_deal(
|
||||||
seed,
|
seed,
|
||||||
draw_mode,
|
draw_mode,
|
||||||
DEFAULT_SOLVE_MOVES_BUDGET,
|
DEFAULT_SOLVE_MOVES_BUDGET,
|
||||||
|
|||||||
@@ -95,8 +95,8 @@ pub struct HintSolverConfig {
|
|||||||
impl Default for HintSolverConfig {
|
impl Default for HintSolverConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
moves_budget: solitaire_data::solver::DEFAULT_SOLVE_MOVES_BUDGET,
|
moves_budget: solitaire_core::DEFAULT_SOLVE_MOVES_BUDGET,
|
||||||
states_budget: solitaire_data::solver::DEFAULT_SOLVE_STATES_BUDGET,
|
states_budget: solitaire_core::DEFAULT_SOLVE_STATES_BUDGET,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ use bevy::prelude::*;
|
|||||||
use bevy::tasks::{AsyncComputeTaskPool, Task, futures_lite::future};
|
use bevy::tasks::{AsyncComputeTaskPool, Task, futures_lite::future};
|
||||||
use solitaire_core::KlondikeInstruction;
|
use solitaire_core::KlondikeInstruction;
|
||||||
use solitaire_core::game_state::GameState;
|
use solitaire_core::game_state::GameState;
|
||||||
use solitaire_data::solver::try_solve_from_state;
|
|
||||||
|
|
||||||
use crate::card_plugin::CardEntity;
|
use crate::card_plugin::CardEntity;
|
||||||
use crate::events::{HintVisualEvent, InfoToastEvent, StateChangedEvent};
|
use crate::events::{HintVisualEvent, InfoToastEvent, StateChangedEvent};
|
||||||
@@ -66,7 +65,7 @@ impl PendingHintTask {
|
|||||||
// Winnable (`Ok(Some)`) carries the first move on a winning path;
|
// Winnable (`Ok(Some)`) carries the first move on a winning path;
|
||||||
// unwinnable (`Ok(None)`) and inconclusive (`Err`) both fall back
|
// unwinnable (`Ok(None)`) and inconclusive (`Err`) both fall back
|
||||||
// to the live-state heuristic so H always produces feedback.
|
// 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(Some(first_move)) => HintTaskOutput::SolverMove(first_move),
|
||||||
Ok(None) | Err(_) => HintTaskOutput::NeedsHeuristic,
|
Ok(None) | Err(_) => HintTaskOutput::NeedsHeuristic,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
//! 3. `handle_text_input` appends decimal digits / handles Backspace while
|
//! 3. `handle_text_input` appends decimal digits / handles Backspace while
|
||||||
//! the modal is open, updating [`SeedInputBuffer`] each frame.
|
//! the modal is open, updating [`SeedInputBuffer`] each frame.
|
||||||
//! 4. `tick_debounce_and_spawn_solver_task` waits for 12 frames (~200 ms at
|
//! 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
|
//! [`AsyncComputeTaskPool`]. Any fresh keypress drops the in-flight task
|
||||||
//! by resetting the resource.
|
//! by resetting the resource.
|
||||||
//! 5. `poll_solver_task` polls the in-flight task each frame and updates the
|
//! 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::prelude::*;
|
||||||
use bevy::tasks::{AsyncComputeTaskPool, Task, futures_lite::future};
|
use bevy::tasks::{AsyncComputeTaskPool, Task, futures_lite::future};
|
||||||
use solitaire_core::DrawMode;
|
use solitaire_core::DrawMode;
|
||||||
use solitaire_data::solver::{
|
use solitaire_core::game_state::GameState;
|
||||||
DEFAULT_SOLVE_MOVES_BUDGET, DEFAULT_SOLVE_STATES_BUDGET, SolveOutcome, try_solve,
|
use solitaire_core::{DEFAULT_SOLVE_MOVES_BUDGET, DEFAULT_SOLVE_STATES_BUDGET, SolveOutcome};
|
||||||
};
|
|
||||||
|
|
||||||
use crate::events::{NewGameRequestEvent, StartPlayBySeedRequestEvent};
|
use crate::events::{NewGameRequestEvent, StartPlayBySeedRequestEvent};
|
||||||
use crate::font_plugin::FontResource;
|
use crate::font_plugin::FontResource;
|
||||||
@@ -343,7 +342,7 @@ fn tick_debounce_and_spawn_solver_task(
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or(DrawMode::DrawOne, |s| s.0.draw_mode);
|
.map_or(DrawMode::DrawOne, |s| s.0.draw_mode);
|
||||||
let task = AsyncComputeTaskPool::get().spawn(async move {
|
let task = AsyncComputeTaskPool::get().spawn(async move {
|
||||||
try_solve(
|
GameState::solve_fresh_deal(
|
||||||
seed,
|
seed,
|
||||||
draw_mode,
|
draw_mode,
|
||||||
DEFAULT_SOLVE_MOVES_BUDGET,
|
DEFAULT_SOLVE_MOVES_BUDGET,
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ enum SettingsButton {
|
|||||||
ToggleTouchInputMode,
|
ToggleTouchInputMode,
|
||||||
/// Toggle the [`Settings::winnable_deals_only`] flag. When on, new
|
/// Toggle the [`Settings::winnable_deals_only`] flag. When on, new
|
||||||
/// random Classic-mode deals are filtered through
|
/// 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.
|
/// winnable (or the retry cap is hit). Off by default.
|
||||||
ToggleWinnableDealsOnly,
|
ToggleWinnableDealsOnly,
|
||||||
/// Toggle the inverse of [`Settings::disable_smart_default_size`].
|
/// Toggle the inverse of [`Settings::disable_smart_default_size`].
|
||||||
|
|||||||
Reference in New Issue
Block a user