From 03f5e704988ca87d96aed10252eddf39c2650bc8 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Sun, 17 May 2026 09:20:14 -0700 Subject: [PATCH 1/6] wip stats --- card_game/src/lib.rs | 134 ++++++++++++++++++++++++++++++--------- klondike-cli/src/main.rs | 4 +- klondike/src/lib.rs | 73 +++++++++++++++++---- klondike/src/test.rs | 6 +- 4 files changed, 170 insertions(+), 47 deletions(-) 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(); -- 2.47.3 From 2f2910dce2d45df8b016598d11fd766fb56a55ba Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Sun, 17 May 2026 09:29:28 -0700 Subject: [PATCH 2/6] fini --- card_game/src/lib.rs | 23 ++++++++++++++++++++--- klondike-cli/src/main.rs | 15 ++++++++------- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/card_game/src/lib.rs b/card_game/src/lib.rs index df874bd..a894400 100644 --- a/card_game/src/lib.rs +++ b/card_game/src/lib.rs @@ -269,7 +269,7 @@ pub struct SessionState { impl Session where G: Clone + Eq + core::hash::Hash, - G::Stats: Clone, + G::Stats: Clone + Default, G::Instruction: Clone + Eq + core::hash::Hash, { pub fn new(state: G, stats: G::Stats, config: G::Config) -> Self { @@ -288,17 +288,34 @@ where } 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 { + 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.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 + .state + .process_instruction(&mut self.stats.inner_stats, &self.config, instruction) + } + pub fn is_win(&self) -> bool { + self.state.is_win() + } pub fn is_winnable(&self) -> Option> { let mut observed = std::collections::HashSet::new(); struct StateMachine { diff --git a/klondike-cli/src/main.rs b/klondike-cli/src/main.rs index 4c399f0..2b6d0af 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 klondike::{ - DstFoundation, DstTableau, Foundation, Klondike, KlondikeInstruction, KlondikePile, - KlondikePileStack, SkipCards, Tableau, TableauStack, + DstFoundation, DstTableau, Foundation, Klondike, KlondikeConfig, KlondikeInstruction, + KlondikePile, KlondikePileStack, SkipCards, Tableau, TableauStack, }; use std::fmt::Display; @@ -144,6 +144,7 @@ impl core::str::FromStr for SessionInstruction { } fn find_valid_instruction( + config: &KlondikeConfig, state: &Klondike, naive_instruction: NaiveInstruction, ) -> Option { @@ -173,7 +174,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 +192,7 @@ fn find_valid_instruction( _ => return None, }; state - .is_instruction_valid(instruction) + .is_instruction_valid(config, instruction) .then_some(instruction) } @@ -241,7 +242,7 @@ fn get_good_move(state: &Klondike) -> Option { } fn main() -> Result<(), std::io::Error> { - let mut session = Session::new(Klondike::new_random()); + let mut session = Session::new_default(Klondike::new_random()); let mut input = String::new(); loop { // display game @@ -257,7 +258,7 @@ fn main() -> Result<(), std::io::Error> { // run game match instruction { - SessionInstruction::New => session = Session::new(Klondike::new_random()), + SessionInstruction::New => session = Session::new_default(Klondike::new_random()), SessionInstruction::Undo => session.undo(), SessionInstruction::Exit => break Ok(()), SessionInstruction::Hint => { @@ -277,7 +278,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 { -- 2.47.3 From 97cc81987f2591284ddfcf3591352cf6898087b6 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Sun, 17 May 2026 09:37:52 -0700 Subject: [PATCH 3/6] display stats --- card_game/src/lib.rs | 29 ++++++++++++++++++++--------- klondike-cli/src/main.rs | 18 ++++++++++++++++-- klondike/src/lib.rs | 6 ++++++ 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/card_game/src/lib.rs b/card_game/src/lib.rs index a894400..a089397 100644 --- a/card_game/src/lib.rs +++ b/card_game/src/lib.rs @@ -250,9 +250,15 @@ impl SessionStats { 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 { @@ -266,6 +272,15 @@ pub struct SessionState { state: G, history: Vec, } +impl SessionState { + fn new(state: G) -> Self { + Self { + seed: state.clone(), + state, + history: Vec::new(), + } + } +} impl Session where G: Clone + Eq + core::hash::Hash, @@ -274,16 +289,9 @@ where { pub fn new(state: G, stats: G::Stats, config: G::Config) -> Self { Self { - stats: SessionStats { - inner_stats: stats, - undos: 0, - }, + stats: SessionStats::new(stats), config, - state: SessionState { - seed: state.clone(), - state, - history: Vec::new(), - }, + state: SessionState::new(state), } } pub fn new_default(state: G) -> Self @@ -292,6 +300,9 @@ where { Self::new(state, Default::default(), Default::default()) } + pub const fn stats(&self) -> &SessionStats { + &self.stats + } pub const fn state(&self) -> &G { &self.state.state } diff --git a/klondike-cli/src/main.rs b/klondike-cli/src/main.rs index 2b6d0af..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, KlondikeConfig, KlondikeInstruction, - KlondikePile, KlondikePileStack, SkipCards, Tableau, TableauStack, + 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); @@ -245,6 +257,8 @@ fn main() -> Result<(), std::io::Error> { 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())); diff --git a/klondike/src/lib.rs b/klondike/src/lib.rs index 210bc95..c5176a9 100644 --- a/klondike/src/lib.rs +++ b/klondike/src/lib.rs @@ -46,6 +46,12 @@ impl KlondikeStats { 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; } -- 2.47.3 From b40fc4bf56e7a1a8a7da3a9e4abb8cccd066bde4 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Sun, 17 May 2026 09:41:55 -0700 Subject: [PATCH 4/6] fix undo --- card_game/src/lib.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/card_game/src/lib.rs b/card_game/src/lib.rs index a089397..5981f2f 100644 --- a/card_game/src/lib.rs +++ b/card_game/src/lib.rs @@ -320,9 +320,11 @@ where self.state.state.possible_instructions() } pub fn process_instruction(&mut self, instruction: G::Instruction) { - self.state - .state - .process_instruction(&mut self.stats.inner_stats, &self.config, instruction) + self.state.process_instruction( + &mut self.stats, + &self.config, + SessionInstruction::InnerInstruction(instruction), + ) } pub fn is_win(&self) -> bool { self.state.is_win() @@ -367,8 +369,8 @@ where impl Game for SessionState where G: Clone, - G::Instruction: Clone, G::Stats: Default, + G::Instruction: Clone, { type Stats = SessionStats; type Config = G::Config; -- 2.47.3 From 74343d0c7a191a63076128c494869da0954ac152 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Sun, 17 May 2026 09:42:47 -0700 Subject: [PATCH 5/6] fix move count --- klondike/src/lib.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/klondike/src/lib.rs b/klondike/src/lib.rs index c5176a9..b3e7d0a 100644 --- a/klondike/src/lib.rs +++ b/klondike/src/lib.rs @@ -594,6 +594,7 @@ impl Game for Klondike { config: &Self::Config, instruction: Self::Instruction, ) { + stats.increment_moves(); match instruction { // Reset the stock if it's empty KlondikeInstruction::RotateStock => { @@ -608,13 +609,11 @@ impl Game for Klondike { } // 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); } -- 2.47.3 From 048c69f9e8b942ec00487fdb05215e5954e06e12 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Sun, 17 May 2026 09:44:36 -0700 Subject: [PATCH 6/6] fix lint --- klondike/src/test.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klondike/src/test.rs b/klondike/src/test.rs index 6ba4d92..1eec73b 100644 --- a/klondike/src/test.rs +++ b/klondike/src/test.rs @@ -1,5 +1,5 @@ use crate::Klondike; -use card_game::{Game, Session}; +use card_game::Session; #[test] fn test_is_winnable() { // is winnable -- 2.47.3