diff --git a/card_game/src/lib.rs b/card_game/src/lib.rs index 603eed2..df874bd 100644 --- a/card_game/src/lib.rs +++ b/card_game/src/lib.rs @@ -2,10 +2,17 @@ use core::ops::RangeBounds; // TODO: pub struct ValidInstruction(I); pub trait Game { + type Stats; + type Config; type Instruction; fn possible_instructions(&self) -> impl Iterator + use; - fn is_instruction_valid(&self, instruction: Self::Instruction) -> bool; - fn process_instruction(&mut self, instruction: Self::Instruction); + fn is_instruction_valid(&self, config: &Self::Config, instruction: Self::Instruction) -> bool; + fn process_instruction( + &mut self, + stats: &mut Self::Stats, + config: &Self::Config, + instruction: Self::Instruction, + ); fn is_win(&self) -> bool; } @@ -225,28 +232,72 @@ impl Pile { } } -#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[derive(Clone, Debug)] +pub enum SessionInstruction { + Undo, + InnerInstruction(I), +} + +#[derive(Clone, Debug)] +pub struct SessionStats { + inner_stats: S, + undos: usize, +} +impl SessionStats { + const fn new(inner_stats: S) -> Self { + SessionStats { + inner_stats, + undos: 0, + } + } + const fn increment_undos(&mut self) { + self.undos += 1; + } +} + pub struct Session { + stats: SessionStats, + config: G::Config, + state: SessionState, +} +#[derive(Clone, Eq, Hash, PartialEq)] +pub struct SessionState { seed: G, state: G, history: Vec, } -impl Session +impl Session where + G: Clone + Eq + core::hash::Hash, + G::Stats: Clone, G::Instruction: Clone + Eq + core::hash::Hash, { - pub fn new(state: G) -> Self { + pub fn new(state: G, stats: G::Stats, config: G::Config) -> Self { Self { - seed: state.clone(), - state, - history: Vec::new(), + stats: SessionStats { + inner_stats: stats, + undos: 0, + }, + config, + state: SessionState { + seed: state.clone(), + state, + history: Vec::new(), + }, } } + pub fn new_default(state: G) -> Self + where + G::Stats: Default, + G::Config: Default, + { + Self::new(state, Default::default(), Default::default()) + } pub fn state(&self) -> &G { - &self.state + &self.state.state } pub fn history(&self) -> &[G::Instruction] { - &self.history + &self.state.history } pub fn is_winnable(&self) -> Option> { let mut observed = std::collections::HashSet::new(); @@ -255,14 +306,15 @@ where possible_instructions_iter: P, instruction: I, } - let mut state = self.state.clone(); + let mut dummy_stats = self.stats.inner_stats.clone(); + let mut state = self.state.state.clone(); let mut it = state.possible_instructions(); let mut path = Vec::new(); 'outer: while !state.is_win() { observed.insert(state.clone()); for instruction in &mut it { let mut next_state = state.clone(); - next_state.process_instruction(instruction.clone()); + next_state.process_instruction(&mut dummy_stats, &self.config, instruction.clone()); if !observed.contains(&next_state) { let possible_instructions_iter = core::mem::replace(&mut it, next_state.possible_instructions()); @@ -283,30 +335,54 @@ where } Some(path.into_iter().map(|state| state.instruction).collect()) } - pub fn undo(&mut self) { - // replay the entire history of the game except one move - self.history.pop(); - let mut state = self.seed.clone(); - for instruction in self.history() { - state.process_instruction(instruction.clone()); - } - self.state = state; - } } -impl Game for Session +impl Game for SessionState where + G: Clone, G::Instruction: Clone, + G::Stats: Default, { - type Instruction = G::Instruction; + type Stats = SessionStats; + type Config = G::Config; + type Instruction = SessionInstruction; fn possible_instructions(&self) -> impl Iterator + use { - self.state.possible_instructions() + self.state + .possible_instructions() + .map(SessionInstruction::InnerInstruction) } - fn is_instruction_valid(&self, instruction: Self::Instruction) -> bool { - self.state.is_instruction_valid(instruction) + fn is_instruction_valid(&self, config: &Self::Config, instruction: Self::Instruction) -> bool { + match instruction { + SessionInstruction::Undo => !self.history.is_empty(), + SessionInstruction::InnerInstruction(instruction) => { + self.state.is_instruction_valid(config, instruction) + } + } } - fn process_instruction(&mut self, instruction: Self::Instruction) { - self.history.push(instruction.clone()); - self.state.process_instruction(instruction); + fn process_instruction( + &mut self, + stats: &mut Self::Stats, + config: &Self::Config, + instruction: Self::Instruction, + ) { + 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, instruction.clone()); + } + self.state = state; + stats.inner_stats = inner_stats; + stats.increment_undos(); + } + SessionInstruction::InnerInstruction(instruction) => { + self.history.push(instruction.clone()); + self.state + .process_instruction(&mut stats.inner_stats, config, instruction); + } + } } fn is_win(&self) -> bool { self.state.is_win() diff --git a/klondike-cli/src/main.rs b/klondike-cli/src/main.rs index 85da347..4c399f0 100644 --- a/klondike-cli/src/main.rs +++ b/klondike-cli/src/main.rs @@ -241,7 +241,7 @@ fn get_good_move(state: &Klondike) -> Option { } fn main() -> Result<(), std::io::Error> { - let mut session = Session::new(Klondike::new_random_default()); + let mut session = Session::new(Klondike::new_random()); let mut input = String::new(); loop { // display game @@ -257,7 +257,7 @@ fn main() -> Result<(), std::io::Error> { // run game match instruction { - SessionInstruction::New => session = Session::new(Klondike::new_random_default()), + SessionInstruction::New => session = Session::new(Klondike::new_random()), SessionInstruction::Undo => session.undo(), SessionInstruction::Exit => break Ok(()), SessionInstruction::Hint => { diff --git a/klondike/src/lib.rs b/klondike/src/lib.rs index 8211356..210bc95 100644 --- a/klondike/src/lib.rs +++ b/klondike/src/lib.rs @@ -10,11 +10,47 @@ mod test; #[cfg(doctest)] struct ReadmeDoctests; -#[derive(Clone, Debug, Eq, Hash, PartialEq)] -pub struct KlondikeConfig {} -impl Default for KlondikeConfig { - fn default() -> Self { - KlondikeConfig {} +#[derive(Clone, Copy, Debug, Default)] +enum DrawStockConfig { + #[default] + DrawOne = 1, + DrawThree = 3, +} + +#[derive(Clone, Debug, Default)] +pub struct KlondikeConfig { + draw_stock: DrawStockConfig, +} +impl KlondikeConfig { + pub const fn draw_one_stock() -> Self { + KlondikeConfig { + draw_stock: DrawStockConfig::DrawOne, + } + } + pub const fn draw_three_stock() -> Self { + KlondikeConfig { + draw_stock: DrawStockConfig::DrawThree, + } + } +} + +#[derive(Clone, Debug, Default)] +pub struct KlondikeStats { + recycle_count: usize, + moves: usize, +} +impl KlondikeStats { + pub const fn new() -> Self { + KlondikeStats { + recycle_count: 0, + moves: 0, + } + } + const fn increment_recycle_count(&mut self) { + self.recycle_count += 1; + } + const fn increment_moves(&mut self) { + self.moves += 1; } } @@ -486,14 +522,13 @@ impl Iterator for KlondikeIter { #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct Klondike { - config: KlondikeConfig, state: KlondikeState, } impl Klondike { - pub fn new_random_default() -> Self { - Self::new(Rng::default(), KlondikeConfig::default()) + pub fn new_random() -> Self { + Self::new(Rng::default()) } - pub fn new(mut seed: Rng, config: KlondikeConfig) -> Self { + pub fn new(mut seed: Rng) -> Self { // shuffle a new deck let mut deck = Stack::full_deck(0); use rand::seq::SliceRandom; @@ -529,7 +564,7 @@ impl Klondike { tableau6, tableau7, }; - Self { config, state } + Self { state } } pub const fn state(&self) -> &KlondikeState { &self.state @@ -537,31 +572,43 @@ impl Klondike { } impl Game for Klondike { + type Stats = KlondikeStats; + type Config = KlondikeConfig; type Instruction = KlondikeInstruction; fn possible_instructions(&self) -> impl Iterator + use<> { let state = self.state.clone(); KlondikeIter::new().filter(move |&instruction| state.is_instruction_valid(instruction)) } - fn is_instruction_valid(&self, instruction: Self::Instruction) -> bool { + fn is_instruction_valid(&self, _config: &Self::Config, instruction: Self::Instruction) -> bool { self.state.is_instruction_valid(instruction) } - fn process_instruction(&mut self, instruction: Self::Instruction) { + fn process_instruction( + &mut self, + stats: &mut Self::Stats, + config: &Self::Config, + instruction: Self::Instruction, + ) { match instruction { // Reset the stock if it's empty KlondikeInstruction::RotateStock => { if self.state.stock.face_down().is_empty() { self.state.stock.flip_it_and_reverse_it(); + stats.increment_recycle_count(); } else { - self.state.stock.flip_up(); + for _ in 0..config.draw_stock as usize { + self.state.stock.flip_up(); + } } } // Move a card from anywhere to a foundation KlondikeInstruction::DstFoundation(DstFoundation { src, foundation }) => { + stats.increment_moves(); let card = self.state.take_top_card(src); self.state.extend_foundation(foundation, card); } // Move a stack of cards from anywhere to a tableau KlondikeInstruction::DstTableau(DstTableau { src, tableau }) => { + stats.increment_moves(); let cards = self.state.take_stack(src); self.state.extend_tableau(tableau, cards); } diff --git a/klondike/src/test.rs b/klondike/src/test.rs index be6b7bf..6ba4d92 100644 --- a/klondike/src/test.rs +++ b/klondike/src/test.rs @@ -3,14 +3,14 @@ use card_game::{Game, Session}; #[test] fn test_is_winnable() { // is winnable - let is_winnable = Session::new(Klondike::new_random_default()).is_winnable(); + let is_winnable = Session::new_default(Klondike::new_random()).is_winnable(); println!("is_winnable = {is_winnable:?}"); } #[test] fn test_klondike() { // create game session - let game = Klondike::new_random_default(); - let mut session = Session::new(game); + let game = Klondike::new_random(); + let mut session = Session::new_default(game); // is winnable let is_winnable = session.is_winnable();