pub type Rng = rand::rngs::StdRng; use card_game::{Card, Game, Pile, Rank, Stack}; // test readme #[doc = include_str!("../README.md")] #[cfg(doctest)] struct ReadmeDoctests; #[derive(Clone, Copy, Debug, Default)] pub enum DrawStockConfig { #[default] DrawOne = 1, DrawThree = 3, } #[derive(Clone, Debug, Default)] pub struct KlondikeConfig { pub draw_stock: DrawStockConfig, } #[derive(Clone, Debug, Default)] pub struct KlondikeStats { score: usize, recycle_count: usize, moves: usize, } impl KlondikeStats { pub const fn new() -> Self { KlondikeStats { score: 0, recycle_count: 0, moves: 0, } } pub const fn score(&self) -> usize { self.score } pub const fn recycle_count(&self) -> usize { self.recycle_count } pub const fn moves(&self) -> usize { self.moves } /// A card was moved to a foundation. const fn increment_score_foundation(&mut self) { self.score += 10; } /// A card was moved from stock to tableau. const fn increment_score_tableau(&mut self) { self.score += 5; } const fn increment_recycle_count(&mut self) { self.recycle_count += 1; } const fn increment_moves(&mut self) { self.moves += 1; } } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum Tableau { Tableau1, Tableau2, Tableau3, Tableau4, Tableau5, Tableau6, Tableau7, } impl Tableau { const ITER_BEGIN: Self = Self::Tableau1; const fn next(self) -> Option { use Tableau::*; Some(match self { Tableau1 => Tableau2, Tableau2 => Tableau3, Tableau3 => Tableau4, Tableau4 => Tableau5, Tableau5 => Tableau6, Tableau6 => Tableau7, Tableau7 => return None, }) } } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum Foundation { Foundation1, Foundation2, Foundation3, Foundation4, } impl Foundation { const ITER_BEGIN: Self = Self::Foundation1; const fn next(self) -> Option { use Foundation::*; Some(match self { Foundation1 => Foundation2, Foundation2 => Foundation3, Foundation3 => Foundation4, Foundation4 => return None, }) } } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum KlondikePile { Tableau(Tableau), Stock, Foundation(Foundation), } impl KlondikePile { const ITER_BEGIN: Self = Self::Tableau(Tableau::ITER_BEGIN); const fn next(self) -> Option { Some(match self { Self::Tableau(tableau_stack) => match tableau_stack.next() { Some(tableau_stack) => Self::Tableau(tableau_stack), None => Self::Stock, }, Self::Stock => Self::Foundation(Foundation::ITER_BEGIN), Self::Foundation(foundation) => match foundation.next() { Some(foundation) => Self::Foundation(foundation), None => return None, }, }) } } impl From for KlondikePile { fn from(value: Tableau) -> Self { KlondikePile::Tableau(value) } } impl From for KlondikePile { fn from(value: Foundation) -> Self { KlondikePile::Foundation(value) } } #[repr(u8)] #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum SkipCards { Skip0, Skip1, Skip2, Skip3, Skip4, Skip5, Skip6, Skip7, Skip8, Skip9, Skip10, Skip11, Skip12, } impl SkipCards { const ITER_BEGIN: Self = Self::Skip0; const fn next(self) -> Option { use SkipCards::*; Some(match self { Skip0 => Skip1, Skip1 => Skip2, Skip2 => Skip3, Skip3 => Skip4, Skip4 => Skip5, Skip5 => Skip6, Skip6 => Skip7, Skip7 => Skip8, Skip8 => Skip9, Skip9 => Skip10, Skip10 => Skip11, Skip11 => Skip12, Skip12 => return None, }) } } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct TableauStack { pub tableau: Tableau, pub skip_cards: SkipCards, } impl TableauStack { const ITER_BEGIN: Self = Self { tableau: Tableau::ITER_BEGIN, skip_cards: SkipCards::ITER_BEGIN, }; const fn next(self) -> Option { let TableauStack { tableau, skip_cards, } = self; if let Some(skip_cards) = skip_cards.next() { return Some(Self { tableau, skip_cards, }); } if let Some(tableau) = tableau.next() { let skip_cards = SkipCards::ITER_BEGIN; return Some(Self { tableau, skip_cards, }); } None } } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum KlondikePileStack { Tableau(TableauStack), Stock, Foundation(Foundation), } impl KlondikePileStack { const ITER_BEGIN: Self = Self::Tableau(TableauStack::ITER_BEGIN); const fn next(self) -> Option { Some(match self { Self::Tableau(tableau_stack) => match tableau_stack.next() { Some(tableau_stack) => Self::Tableau(tableau_stack), None => Self::Stock, }, Self::Stock => Self::Foundation(Foundation::ITER_BEGIN), Self::Foundation(foundation) => match foundation.next() { Some(foundation) => Self::Foundation(foundation), None => return None, }, }) } } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct DstFoundation { pub src: KlondikePile, pub foundation: Foundation, } impl DstFoundation { const ITER_BEGIN: Self = Self { src: KlondikePile::ITER_BEGIN, foundation: Foundation::ITER_BEGIN, }; const fn next(self) -> Option { let DstFoundation { src, foundation } = self; if let Some(src) = src.next() { return Some(Self { src, foundation }); } if let Some(foundation) = foundation.next() { let src = KlondikePile::ITER_BEGIN; return Some(Self { src, foundation }); } None } } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct DstTableau { pub src: KlondikePileStack, pub tableau: Tableau, } impl DstTableau { const ITER_BEGIN: Self = Self { src: KlondikePileStack::ITER_BEGIN, tableau: Tableau::ITER_BEGIN, }; const fn next(self) -> Option { let DstTableau { src, tableau } = self; if let Some(src) = src.next() { return Some(Self { src, tableau }); } if let Some(tableau) = tableau.next() { let src = KlondikePileStack::ITER_BEGIN; return Some(Self { src, tableau }); } None } } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum KlondikeInstruction { DstFoundation(DstFoundation), DstTableau(DstTableau), RotateStock, } impl KlondikeInstruction { const ITER_BEGIN: Self = Self::DstFoundation(DstFoundation::ITER_BEGIN); const fn next(self) -> Option { Some(match self { Self::DstFoundation(dst_foundation) => match dst_foundation.next() { Some(dst_foundation) => Self::DstFoundation(dst_foundation), None => Self::DstTableau(DstTableau::ITER_BEGIN), }, Self::DstTableau(tableau) => match tableau.next() { Some(tableau) => Self::DstTableau(tableau), None => Self::RotateStock, }, Self::RotateStock => return None, }) } /// foundation -> foundation is a useless move pub fn is_useless(&self) -> bool { matches!( self, KlondikeInstruction::DstFoundation(DstFoundation { src: KlondikePile::Foundation(_), .. }) ) } } const TABLEAUS: usize = 7; const fn sum(n: usize) -> usize { n * (n + 1) / 2 } const STOCK: usize = 52 - sum(TABLEAUS); #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct KlondikeState { stock: Pile, foundations: [Stack<13>; 4], tableau1: Pile<0, 13>, tableau2: Pile<1, 13>, tableau3: Pile<2, 13>, tableau4: Pile<3, 13>, tableau5: Pile<4, 13>, tableau6: Pile<5, 13>, tableau7: Pile<6, 13>, } impl KlondikeState { pub const fn stock(&self) -> &Pile { &self.stock } pub const fn foundation1(&self) -> &Stack<13> { &self.foundations[Foundation::Foundation1 as usize] } pub const fn foundation2(&self) -> &Stack<13> { &self.foundations[Foundation::Foundation2 as usize] } pub const fn foundation3(&self) -> &Stack<13> { &self.foundations[Foundation::Foundation3 as usize] } pub const fn foundation4(&self) -> &Stack<13> { &self.foundations[Foundation::Foundation4 as usize] } pub const fn tableau1(&self) -> &Pile<0, 13> { &self.tableau1 } pub const fn tableau2(&self) -> &Pile<1, 13> { &self.tableau2 } pub const fn tableau3(&self) -> &Pile<2, 13> { &self.tableau3 } pub const fn tableau4(&self) -> &Pile<3, 13> { &self.tableau4 } pub const fn tableau5(&self) -> &Pile<4, 13> { &self.tableau5 } pub const fn tableau6(&self) -> &Pile<5, 13> { &self.tableau6 } pub const fn tableau7(&self) -> &Pile<6, 13> { &self.tableau7 } pub fn is_tableau_face_down_empty(&self, tableau: Tableau) -> bool { match tableau { Tableau::Tableau1 => self.tableau1.face_down().is_empty(), Tableau::Tableau2 => self.tableau2.face_down().is_empty(), Tableau::Tableau3 => self.tableau3.face_down().is_empty(), Tableau::Tableau4 => self.tableau4.face_down().is_empty(), Tableau::Tableau5 => self.tableau5.face_down().is_empty(), Tableau::Tableau6 => self.tableau6.face_down().is_empty(), Tableau::Tableau7 => self.tableau7.face_down().is_empty(), } } pub fn stack_bottom_card(&self, src: KlondikePileStack) -> Option<&Card> { match src { KlondikePileStack::Tableau(TableauStack { tableau, skip_cards, }) => match tableau { Tableau::Tableau1 => self.tableau1.face_up().get(skip_cards as usize), Tableau::Tableau2 => self.tableau2.face_up().get(skip_cards as usize), Tableau::Tableau3 => self.tableau3.face_up().get(skip_cards as usize), Tableau::Tableau4 => self.tableau4.face_up().get(skip_cards as usize), Tableau::Tableau5 => self.tableau5.face_up().get(skip_cards as usize), Tableau::Tableau6 => self.tableau6.face_up().get(skip_cards as usize), Tableau::Tableau7 => self.tableau7.face_up().get(skip_cards as usize), }, KlondikePileStack::Foundation(foundation) => { self.foundations[foundation as usize].last() } KlondikePileStack::Stock => self.stock.face_up().last(), } } pub fn top_card>(&self, src: S) -> Option<&Card> { match src.into() { KlondikePile::Tableau(tableau) => match tableau { Tableau::Tableau1 => self.tableau1.face_up().last(), Tableau::Tableau2 => self.tableau2.face_up().last(), Tableau::Tableau3 => self.tableau3.face_up().last(), Tableau::Tableau4 => self.tableau4.face_up().last(), Tableau::Tableau5 => self.tableau5.face_up().last(), Tableau::Tableau6 => self.tableau6.face_up().last(), Tableau::Tableau7 => self.tableau7.face_up().last(), }, KlondikePile::Foundation(foundation) => self.foundations[foundation as usize].last(), KlondikePile::Stock => self.stock.face_up().last(), } } fn take_stack(&mut self, src: KlondikePileStack) -> Stack<13> { match src { KlondikePileStack::Tableau(TableauStack { tableau, skip_cards, }) => match tableau { Tableau::Tableau1 => self.tableau1.take_range_flip_up(skip_cards as usize..), Tableau::Tableau2 => self.tableau2.take_range_flip_up(skip_cards as usize..), Tableau::Tableau3 => self.tableau3.take_range_flip_up(skip_cards as usize..), Tableau::Tableau4 => self.tableau4.take_range_flip_up(skip_cards as usize..), Tableau::Tableau5 => self.tableau5.take_range_flip_up(skip_cards as usize..), Tableau::Tableau6 => self.tableau6.take_range_flip_up(skip_cards as usize..), Tableau::Tableau7 => self.tableau7.take_range_flip_up(skip_cards as usize..), }, KlondikePileStack::Foundation(foundation) => { Stack::from_iter(self.foundations[foundation as usize].pop()) } KlondikePileStack::Stock => Stack::from_iter(self.stock.pop()), } } fn take_top_card>(&mut self, src: S) -> Option { match src.into() { KlondikePile::Tableau(tableau) => match tableau { Tableau::Tableau1 => self.tableau1.pop_flip_up(), Tableau::Tableau2 => self.tableau2.pop_flip_up(), Tableau::Tableau3 => self.tableau3.pop_flip_up(), Tableau::Tableau4 => self.tableau4.pop_flip_up(), Tableau::Tableau5 => self.tableau5.pop_flip_up(), Tableau::Tableau6 => self.tableau6.pop_flip_up(), Tableau::Tableau7 => self.tableau7.pop_flip_up(), }, KlondikePile::Foundation(foundation) => self.foundations[foundation as usize].pop(), KlondikePile::Stock => self.stock.pop(), } } fn extend_foundation>( &mut self, foundation: Foundation, cards: I, ) { self.foundations[foundation as usize].extend(cards) } fn extend_tableau>(&mut self, tableau: Tableau, cards: I) { match tableau { Tableau::Tableau1 => self.tableau1.extend(cards), Tableau::Tableau2 => self.tableau2.extend(cards), Tableau::Tableau3 => self.tableau3.extend(cards), Tableau::Tableau4 => self.tableau4.extend(cards), Tableau::Tableau5 => self.tableau5.extend(cards), Tableau::Tableau6 => self.tableau6.extend(cards), Tableau::Tableau7 => self.tableau7.extend(cards), } } pub fn is_instruction_valid(&self, instruction: KlondikeInstruction) -> bool { match instruction { // Stock -> Stock draws a card or resets the stock KlondikeInstruction::RotateStock => { // cannot move stock when stock is empty !self.stock.is_empty() } // moving to foundation has special rules KlondikeInstruction::DstFoundation(dst_foundation) => { // get the top cards if let Some(src_card) = self.top_card(dst_foundation.src) { match self.top_card(dst_foundation.foundation) { // destination card exists Some(dst_card) => { // suit matches? src_card.suit() == dst_card.suit() // value is +1? && dst_card.rank().checked_add(1) == Some(src_card.rank()) } // only ace is allowed to go onto empty foundation None => src_card.rank() == Rank::Ace, } } else { false } } // other = move to tableau KlondikeInstruction::DstTableau(dst_tableau) => { // get the cards if let Some(src_card) = self.stack_bottom_card(dst_tableau.src) { match self.top_card(dst_tableau.tableau) { // destination card exists Some(dst_card) => { // red-ness is opposite? src_card.is_red() != dst_card.is_red() // value is -1? && dst_card.rank().checked_sub(1) == Some(src_card.rank()) } // only king is allowed to go onto empty tableau None => src_card.rank() == Rank::King, } } else { false } } } } } pub struct KlondikeIter { instruction: Option, } impl KlondikeIter { const fn new() -> Self { Self { instruction: Some(KlondikeInstruction::ITER_BEGIN), } } } impl Iterator for KlondikeIter { type Item = KlondikeInstruction; fn next(&mut self) -> Option { let instruction = self.instruction; self.instruction = instruction?.next(); instruction } } #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct Klondike { state: KlondikeState, } impl Klondike { pub fn with_seed(seed: u64) -> Self { use rand::SeedableRng; let mut rng = Rng::seed_from_u64(seed); Self::with_rng(&mut rng) } pub fn with_rng(rng: &mut Rng) -> Self { // shuffle a new deck let mut deck = Stack::full_deck(card_game::Deck::Deck1); use rand::seq::SliceRandom; deck.shuffle(rng); let mut deck = deck.into_iter(); // generate tableaus fn pile(deck: &mut as IntoIterator>::IntoIter) -> Pile { let stack = Stack::from_iter(deck.take(DN)); let mut pile = Pile::new_face_down(stack); pile.push(deck.next().unwrap()); pile } let tableau1 = pile(&mut deck); let tableau2 = pile(&mut deck); let tableau3 = pile(&mut deck); let tableau4 = pile(&mut deck); let tableau5 = pile(&mut deck); let tableau6 = pile(&mut deck); let tableau7 = pile(&mut deck); // stock is remaining cards let stock = Pile::new_face_down(Stack::from_iter(deck)); let state = KlondikeState { stock, foundations: core::array::from_fn(|_| Stack::new()), tableau1, tableau2, tableau3, tableau4, tableau5, tableau6, tableau7, }; Self { state } } pub const fn state(&self) -> &KlondikeState { &self.state } /// Check if the game should be auto-completed pub fn is_win_trivial(&self) -> bool { // all face down cards empty means win self.state.stock.face_down().is_empty() && self.state.tableau1.face_down().is_empty() && self.state.tableau2.face_down().is_empty() && self.state.tableau3.face_down().is_empty() && self.state.tableau4.face_down().is_empty() && self.state.tableau5.face_down().is_empty() && self.state.tableau6.face_down().is_empty() && self.state.tableau7.face_down().is_empty() } fn instruction_priority(&self, instruction: &KlondikeInstruction) -> usize { // 1 Move into foundation // 2 T->T Move to reveal new card (moving a non-king to reveal empty tableau also counts) // 3 Move from stock // 4 Rotate stock // 5 T->T Move not revealing new card // 6 Move from foundation match instruction { KlondikeInstruction::DstFoundation(_) => 1, &KlondikeInstruction::DstTableau(dst_tableau) => match dst_tableau.src { KlondikePileStack::Tableau(TableauStack { tableau, skip_cards: SkipCards::Skip0, }) if !self.state().is_tableau_face_down_empty(tableau) || self .state() .stack_bottom_card(dst_tableau.src) .is_some_and(|card| card.rank() != Rank::King) => { 2 } KlondikePileStack::Stock => 3, KlondikePileStack::Tableau(_) => 5, KlondikePileStack::Foundation(_) => 6, }, KlondikeInstruction::RotateStock => 4, } } /// A single move that usually makes progress towards a winning game pub fn get_auto_move(&self) -> Option { self.possible_instructions() .filter(|ins| !ins.is_useless()) .min_by_key(|ins| self.instruction_priority(ins)) } /// A list of possible moves with useless moves filtered out and sorted by a simple priority function pub fn get_sorted_moves(&self) -> Vec { let mut useful_moves: Vec<_> = self .possible_instructions() .filter(|ins| !ins.is_useless()) .collect(); useful_moves.sort_by_key(|ins| self.instruction_priority(ins)); useful_moves } } 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, _config: &Self::Config, instruction: Self::Instruction) -> bool { self.state.is_instruction_valid(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 { 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_score_foundation(); 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 }) => { if src == KlondikePileStack::Stock { stats.increment_score_tableau(); } let cards = self.state.take_stack(src); self.state.extend_tableau(tableau, cards); } } } fn is_win(&self) -> bool { // all foundations contain all ranks self.state.foundations.iter().all(|foundation| { foundation.len() == Rank::RANKS.len() && foundation .iter() .zip(Rank::RANKS) .all(|(card, rank)| card.rank() == rank) }) } }