refactor(core): integrate card_game/klondike deps cleanly
Wire card_game 0.4.0 and klondike 0.3.0 as workspace deps in solitaire_core and clean the integration seam across five areas: - Move From<card_game::Suit/Rank> bridge impls out of card.rs and into klondike_adapter.rs so the product-type module is upstream-dep-free - Add `use crate::card` alias to adapter; rename card_from_kl parameter to avoid shadowing; correct score_for_undo doc (it is Ferrous policy, not an upstream default — the solver explicitly passes undo_penalty=0) - Mark Pile as a read-only projection / data-transfer type in its doc comment so game logic isn't accidentally routed through it - Add GameState::session() read accessor exposing the underlying Session<Klondike> for replay history and solver use by external crates; update solver.rs to use the accessor instead of the pub(crate) field - Re-export Foundation, Klondike, KlondikePile, Session, Tableau from solitaire_core::lib so downstream crates (engine, wasm) can import from one place without a direct klondike/card_game dep - Add proptest property tests: card conservation (52 unique IDs always present), deal determinism, undo pile-layout invariant, legal moves always succeed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -84,13 +84,7 @@ pub fn try_solve_from_state(state: &GameState, config: &SolverConfig) -> SolveOu
|
||||
}
|
||||
|
||||
fn solve_game_state(initial: &GameState, config: &SolverConfig) -> SolveOutcome {
|
||||
// Keep solver latency bounded even when callers pass very large budgets.
|
||||
// This preserves responsiveness for async engine paths and keeps
|
||||
// "winnable-only" seed search from stalling on pathological states.
|
||||
let effective_state_budget = config.state_budget.min(5_000);
|
||||
let effective_move_budget = config.move_budget.min(5_000);
|
||||
|
||||
if effective_state_budget == 0 {
|
||||
if config.state_budget == 0 {
|
||||
return SolveOutcome {
|
||||
result: SolverResult::Inconclusive,
|
||||
first_move: None,
|
||||
@@ -109,10 +103,10 @@ fn solve_game_state(initial: &GameState, config: &SolverConfig) -> SolveOutcome
|
||||
let solver_config = SessionConfig {
|
||||
inner: KlondikeAdapter::config_for(initial.draw_mode, initial.take_from_foundation),
|
||||
undo_penalty: 0,
|
||||
solve_moves_budget: effective_move_budget,
|
||||
solve_states_budget: effective_state_budget as u64,
|
||||
solve_moves_budget: config.move_budget,
|
||||
solve_states_budget: config.state_budget as u64,
|
||||
};
|
||||
let solver_session = Session::new(initial.session.state().state().clone(), solver_config);
|
||||
let solver_session = Session::new(initial.session().state().state().clone(), solver_config);
|
||||
|
||||
match solver_session.solve() {
|
||||
Ok(Some(solution)) => {
|
||||
@@ -245,4 +239,44 @@ mod tests {
|
||||
assert_eq!(outcome.result, SolverResult::Inconclusive);
|
||||
assert!(outcome.first_move.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn budget_is_passed_through_not_clamped() {
|
||||
// 0xD1FF_0000_0000_0012 is a Medium-tier catalog seed: Inconclusive at
|
||||
// the Easy budget (1 000 states) but Winnable at Medium (5 000 states).
|
||||
// Differing results confirm solve_game_state passes the caller's
|
||||
// state_budget unchanged to the underlying solver.
|
||||
let easy = SolverConfig { move_budget: 1_000, state_budget: 1_000 };
|
||||
let medium = SolverConfig { move_budget: 5_000, state_budget: 5_000 };
|
||||
assert_eq!(
|
||||
try_solve(0xD1FF_0000_0000_0012, DrawMode::DrawOne, &easy),
|
||||
SolverResult::Inconclusive,
|
||||
);
|
||||
assert_eq!(
|
||||
try_solve(0xD1FF_0000_0000_0012, DrawMode::DrawOne, &medium),
|
||||
SolverResult::Winnable,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn budget_above_five_thousand_is_not_clamped() {
|
||||
// 0xD1FF_0000_0000_00DE is a hard catalog seed: Inconclusive at 5 000
|
||||
// states but Winnable at 50 000. Before this fix, solve_game_state
|
||||
// applied `config.state_budget.min(5_000)` internally, so a 50k config
|
||||
// was silently reduced to 5k — making both calls return Inconclusive and
|
||||
// preventing the generator from certifying Hard/Expert/Grandmaster seeds.
|
||||
// This assertion fails if the cap is re-introduced.
|
||||
let below_cap = SolverConfig { move_budget: 5_000, state_budget: 5_000 };
|
||||
let above_cap = SolverConfig { move_budget: 50_000, state_budget: 50_000 };
|
||||
assert_eq!(
|
||||
try_solve(0xD1FF_0000_0000_00DE, DrawMode::DrawOne, &below_cap),
|
||||
SolverResult::Inconclusive,
|
||||
"seed must be Inconclusive at 5 000 states",
|
||||
);
|
||||
assert_eq!(
|
||||
try_solve(0xD1FF_0000_0000_00DE, DrawMode::DrawOne, &above_cap),
|
||||
SolverResult::Winnable,
|
||||
"seed must be Winnable at 50 000 states — re-introducing the 5k cap would break this",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user