diff --git a/card_game/src/lib.rs b/card_game/src/lib.rs index 603eed2..5981f2f 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,102 @@ 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, + } + } + pub const fn stats(&self) -> &S { + &self.inner_stats + } + const fn increment_undos(&mut self) { + self.undos += 1; + } + pub const fn undos(&self) -> usize { + self.undos + } +} + 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 -where - G::Instruction: Clone + Eq + core::hash::Hash, -{ - pub fn new(state: G) -> Self { +impl SessionState { + fn new(state: G) -> Self { Self { seed: state.clone(), state, history: Vec::new(), } } - pub fn state(&self) -> &G { - &self.state +} +impl Session +where + G: Clone + Eq + core::hash::Hash, + G::Stats: Clone + Default, + G::Instruction: Clone + Eq + core::hash::Hash, +{ + pub fn new(state: G, stats: G::Stats, config: G::Config) -> Self { + Self { + stats: SessionStats::new(stats), + config, + state: SessionState::new(state), + } + } + pub fn new_default(state: G) -> Self + where + G::Config: Default, + { + Self::new(state, Default::default(), Default::default()) + } + pub const fn stats(&self) -> &SessionStats { + &self.stats + } + pub const fn state(&self) -> &G { + &self.state.state + } + pub const fn config(&self) -> &G::Config { + &self.config } pub fn history(&self) -> &[G::Instruction] { - &self.history + &self.state.history + } + pub fn undo(&mut self) { + self.state + .process_instruction(&mut self.stats, &self.config, SessionInstruction::Undo) + } + pub fn possible_instructions(&self) -> impl Iterator + use { + self.state.state.possible_instructions() + } + pub fn process_instruction(&mut self, instruction: G::Instruction) { + self.state.process_instruction( + &mut self.stats, + &self.config, + SessionInstruction::InnerInstruction(instruction), + ) + } + pub fn is_win(&self) -> bool { + self.state.is_win() } pub fn is_winnable(&self) -> Option> { let mut observed = std::collections::HashSet::new(); @@ -255,14 +336,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 +365,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::Stats: Default, G::Instruction: Clone, { - 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..8b31111 100644 --- a/klondike-cli/src/main.rs +++ b/klondike-cli/src/main.rs @@ -1,7 +1,7 @@ -use card_game::{Card, CardValue, Game, Pile, Session, Suit}; +use card_game::{Card, CardValue, Game, Pile, Session, SessionStats, Suit}; use klondike::{ - DstFoundation, DstTableau, Foundation, Klondike, KlondikeInstruction, KlondikePile, - KlondikePileStack, SkipCards, Tableau, TableauStack, + DstFoundation, DstTableau, Foundation, Klondike, KlondikeConfig, KlondikeInstruction, + KlondikePile, KlondikePileStack, KlondikeStats, SkipCards, Tableau, TableauStack, }; use std::fmt::Display; @@ -83,6 +83,18 @@ impl Display for Displayed<&Klondike> { } } +impl Display for Displayed<&SessionStats> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "recycles: {} moves: {} undos: {}", + self.0.stats().recycle_count(), + self.0.stats().moves(), + self.0.undos() + ) + } +} + #[derive(Debug)] struct Invalid; struct Parsed(T); @@ -144,6 +156,7 @@ impl core::str::FromStr for SessionInstruction { } fn find_valid_instruction( + config: &KlondikeConfig, state: &Klondike, naive_instruction: NaiveInstruction, ) -> Option { @@ -173,7 +186,7 @@ fn find_valid_instruction( }); let instruction = KlondikeInstruction::DstTableau(DstTableau { tableau, src }); - if state.is_instruction_valid(instruction) { + if state.is_instruction_valid(config, instruction) { return Some(instruction); } } @@ -191,7 +204,7 @@ fn find_valid_instruction( _ => return None, }; state - .is_instruction_valid(instruction) + .is_instruction_valid(config, instruction) .then_some(instruction) } @@ -241,9 +254,11 @@ 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_default(Klondike::new_random()); let mut input = String::new(); loop { + // display stats + println!("{}", Displayed(session.stats())); // display game println!("{}", Displayed(session.state())); @@ -257,7 +272,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_default(Klondike::new_random()), SessionInstruction::Undo => session.undo(), SessionInstruction::Exit => break Ok(()), SessionInstruction::Hint => { @@ -277,7 +292,7 @@ fn main() -> Result<(), std::io::Error> { } SessionInstruction::Klondike(naive_instruction) => { if let Some(instruction) = - find_valid_instruction(session.state(), naive_instruction) + find_valid_instruction(session.config(), session.state(), naive_instruction) { session.process_instruction(instruction); } else { diff --git a/klondike/src/lib.rs b/klondike/src/lib.rs index 8211356..b3e7d0a 100644 --- a/klondike/src/lib.rs +++ b/klondike/src/lib.rs @@ -10,11 +10,53 @@ 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, + } + } + pub const fn recycle_count(&self) -> usize { + self.recycle_count + } + pub const fn moves(&self) -> usize { + self.moves + } + const fn increment_recycle_count(&mut self) { + self.recycle_count += 1; + } + const fn increment_moves(&mut self) { + self.moves += 1; } } @@ -486,14 +528,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 +570,7 @@ impl Klondike { tableau6, tableau7, }; - Self { config, state } + Self { state } } pub const fn state(&self) -> &KlondikeState { &self.state @@ -537,22 +578,33 @@ 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, + ) { + stats.increment_moves(); 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 diff --git a/klondike/src/test.rs b/klondike/src/test.rs index be6b7bf..1eec73b 100644 --- a/klondike/src/test.rs +++ b/klondike/src/test.rs @@ -1,16 +1,16 @@ use crate::Klondike; -use card_game::{Game, Session}; +use card_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();