diff --git a/card_game/src/lib.rs b/card_game/src/lib.rs index 666cf12..172e987 100644 --- a/card_game/src/lib.rs +++ b/card_game/src/lib.rs @@ -6,11 +6,11 @@ struct ReadmeDoctests; use core::ops::RangeBounds; // TODO: pub struct ValidInstruction(I); -pub trait Game { - type Score; - type Stats; - type Config; - type Instruction; +pub trait Game: Clone { + type Score: Clone + core::fmt::Debug; + type Stats: Clone + core::fmt::Debug; + type Config: Clone + core::fmt::Debug; + type Instruction: Clone + core::fmt::Debug; fn score(&self, stats: &Self::Stats, config: &Self::Config) -> Self::Score; fn possible_instructions( &self, @@ -353,21 +353,33 @@ impl Default for SessionConfig { } } +#[derive(Clone, Debug)] pub struct Session { stats: SessionStats, config: SessionConfig, state: SessionState, } -#[derive(Clone, Eq, Hash, PartialEq)] -pub struct SessionState { - seed: G, +#[derive(Clone, Debug)] +pub struct StateSnapshot { state: G, - history: Vec, + instruction: G::Instruction, +} +impl StateSnapshot { + pub const fn state(&self) -> &G { + &self.state + } + pub const fn instruction(&self) -> &G::Instruction { + &self.instruction + } +} +#[derive(Clone, Debug)] +pub struct SessionState { + state: G, + history: Vec>, } impl SessionState { fn new(state: G) -> Self { Self { - seed: state.clone(), state, history: Vec::new(), } @@ -380,9 +392,9 @@ impl SessionState { } impl> Session where - G: Clone + Eq + core::hash::Hash, - G::Stats: Clone + Default, - G::Instruction: Clone + Eq + core::hash::Hash, + G: Eq + core::hash::Hash, + G::Stats: Default, + G::Instruction: Eq + core::hash::Hash, { pub fn new(state: G, config: SessionConfig) -> Self { Self { @@ -406,7 +418,7 @@ where pub const fn config(&self) -> &SessionConfig { &self.config } - pub fn history(&self) -> &[G::Instruction] { + pub fn history(&self) -> &[StateSnapshot] { &self.state.history } pub fn undo(&mut self) { @@ -426,12 +438,39 @@ where pub fn is_win(&self) -> bool { self.state.is_win() } + pub fn is_winnable(&self) -> Option>> { + let mut state_moves = std::collections::HashMap::new(); + let mut state = self.clone(); + while !state.is_win() { + // Continue existing iterator if it exists + let it = state_moves + .entry(state.state().state().clone()) + .or_insert_with(|| { + state + .state() + .state() + .possible_instructions(&self.config().inner) + }); + + // Run one possible move + if let Some(instruction) = it.next() { + state.process_instruction(instruction); + continue; + } + + // No more moves. If we can't undo we're done + if state.history().is_empty() { + return None; + } else { + state.undo(); + } + } + Some(state.state.history) + } } impl> Game for SessionState where - G: Clone, G::Stats: Default, - G::Instruction: Clone, { type Score = i32; type Stats = SessionStats; @@ -464,19 +503,16 @@ where ) { match instruction { SessionInstruction::Undo => { - // replay the entire history of the game except one move - self.history.pop(); - let mut inner_stats = G::Stats::default(); - let mut state = self.seed.clone(); - for instruction in &self.history { - state.process_instruction(&mut inner_stats, &config.inner, instruction.clone()); + if let Some(snapshot) = self.history.pop() { + self.state = snapshot.state; + stats.increment_undos(); } - self.state = state; - stats.inner = inner_stats; - stats.increment_undos(); } SessionInstruction::InnerInstruction(instruction) => { - self.history.push(instruction.clone()); + self.history.push(StateSnapshot { + state: self.state.clone(), + instruction: instruction.clone(), + }); self.state .process_instruction(&mut stats.inner, &config.inner, instruction); } diff --git a/klondike-cli/src/main.rs b/klondike-cli/src/main.rs index 06e6fad..1362bd1 100644 --- a/klondike-cli/src/main.rs +++ b/klondike-cli/src/main.rs @@ -4,8 +4,8 @@ use klondike::{ KlondikePile, KlondikePileStack, SkipCards, Tableau, TableauStack, }; -// #[cfg(test)] -// mod test; +#[cfg(test)] +mod test; use std::fmt::Display; struct Displayed(T); diff --git a/klondike-cli/src/test.rs b/klondike-cli/src/test.rs index c403bff..9e0229b 100644 --- a/klondike-cli/src/test.rs +++ b/klondike-cli/src/test.rs @@ -1,33 +1,15 @@ -use klondike::Klondike; use card_game::Session; +use klondike::Klondike; #[test] fn test_is_winnable() { // is winnable - let is_winnable = Session::new_default(Klondike::with_seed(123)).is_winnable(); - println!("is_winnable = {is_winnable:?}"); -} -#[test] -fn test_klondike() { - // create game session - let game = Klondike::with_seed(123); - let mut session = Session::new_default(game); - - // is winnable - let is_winnable = session.is_winnable(); - println!("is_winnable = {is_winnable:?}"); - - // play game - while let Some(instruction) = session.possible_instructions().next() { - session.process_instruction(instruction); + let is_winnable = Session::new_default(Klondike::with_seed(124)).is_winnable(); + if let Some(win_moves) = is_winnable { + // for (i, ins) in win_moves.into_iter().enumerate() { + // println!("{i} = {:?}", ins.instruction()); + // } + println!("Game is winnable with {} moves", win_moves.len()); + } else { + println!("Game is not winnable"); } - - // did win - let is_win = session.is_win(); - - // print session history - for (i, instruction) in session.history().iter().enumerate() { - println!("move {i} = {instruction:?}"); - } - - println!("is_win = {is_win}"); } diff --git a/klondike/src/lib.rs b/klondike/src/lib.rs index cd67dee..47df3c0 100644 --- a/klondike/src/lib.rs +++ b/klondike/src/lib.rs @@ -601,6 +601,10 @@ impl Iterator for KlondikeIter { instruction } } +#[test] +fn test_klondike_iter() { + assert_eq!(KlondikeIter::new().count(), 721); +} #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct Klondike {