From e3870112cf5b47e4dad526f3050e984cd1b3b8c1 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Fri, 29 May 2026 14:22:34 -0700 Subject: [PATCH] clean solution --- card_game/src/lib.rs | 48 ++++++++++++++++++++++++++++++++++++++-- klondike-cli/src/test.rs | 3 ++- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/card_game/src/lib.rs b/card_game/src/lib.rs index 24a5f85..94e7736 100644 --- a/card_game/src/lib.rs +++ b/card_game/src/lib.rs @@ -324,6 +324,47 @@ impl std::fmt::Display for SolveError { } impl std::error::Error for SolveError {} +/// The solution tends to be very large with long chains of moves that go back to the same state. +/// It is recommended to call .clean_solution() if the solution is actually going to be shown to a user. +pub struct Solution { + solution: Vec>, +} +impl Solution { + pub const fn raw_solution(&self) -> &[StateSnapshot] { + self.solution.as_slice() + } + /// Repeatedly remove the largest range of moves that goes back into the same state. + /// This is a very expensive operation when the solution is very long! + pub fn clean_solution(self) -> Vec> { + let mut history = self.solution; + // history includes cycles + let mut state_index: std::collections::HashMap<_, _> = history + .iter() + .enumerate() + .map(|(i, snapshot)| (snapshot.state().clone(), i)) + .collect(); + + // find the longest range where the start and end are the same state + while let Some(longest_range) = history + .iter() + .enumerate() + .filter_map(|(index, snapshot)| { + let &last_index = state_index.get(snapshot.state())?; + let longness = last_index - index; + (longness != 0).then_some(index..last_index) + }) + .max_by_key(|range| range.len()) + { + history.drain(longest_range); + for (i, snapshot) in history.iter().enumerate() { + state_index.insert(snapshot.state().clone(), i); + } + } + + history + } +} + #[derive(Clone, Debug)] pub enum SessionInstruction { Undo, @@ -454,7 +495,8 @@ where pub fn is_win(&self) -> bool { self.state.is_win() } - pub fn solve(&self) -> Result>>, SolveError> { + /// Attempt to produce a solution. + pub fn solve(&self) -> Result>, SolveError> { let mut state_moves = std::collections::HashMap::new(); let mut state = self.clone(); let mut moves = 0; @@ -489,7 +531,9 @@ where state.undo(); } } - Ok(Some(state.state.history)) + Ok(Some(Solution { + solution: state.state.history, + })) } } impl> Game for SessionState diff --git a/klondike-cli/src/test.rs b/klondike-cli/src/test.rs index 99d96de..28c6471 100644 --- a/klondike-cli/src/test.rs +++ b/klondike-cli/src/test.rs @@ -4,7 +4,8 @@ use klondike::Klondike; fn test_is_winnable() { // is winnable let solution_result = Session::new_default(Klondike::with_seed(124)).solve(); - if let Ok(Some(win_moves)) = solution_result { + if let Ok(Some(solution)) = solution_result { + let win_moves = solution.clean_solution(); // for (i, ins) in win_moves.into_iter().enumerate() { // println!("{i} = {:?}", ins.instruction()); // }