use thiserror::Error; use strum::IntoEnumIterator; use strum_macros::EnumIter; use std::fmt; use rand::seq::SliceRandom; use rand::rng; #[derive(PartialEq, Debug, EnumIter, Copy, Clone)] pub enum Suit { Heart, Diamond, Club, Spade } #[derive(PartialEq, Debug)] pub enum Colour { Black, Red, } impl Suit { pub fn colour(&self) -> Colour { match *self { Self::Heart | Suit::Diamond => Colour::Red, Self::Club | Suit::Spade => Colour::Black, } } } impl fmt::Display for Suit { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Self::Heart => write!(f, "♥"), Self::Diamond => write!(f, "♦"), Self::Club => write!(f, "♣"), Self::Spade => write!(f, "♠"), } } } #[derive(PartialEq, Debug, EnumIter, Copy, Clone)] pub enum Value { Ace, Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten, Jack, Queen, King } impl Value { fn indexed_values(&self) -> u8 { // It might also make sense for Ace to be high... depends on context match self { Self::Ace => 1, Self::Two => 2, Self::Three => 3, Self::Four => 4, Self::Five => 5, Self::Six => 6, Self::Seven => 7, Self::Eight => 8, Self::Nine => 9, Self::Ten => 10, Self::Jack => 11, Self::Queen => 12, Self::King => 13, } } } impl fmt::Display for Value { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Self::Ace => write!(f, "A"), Self::Two => write!(f, "2"), Self::Three => write!(f, "3"), Self::Four => write!(f, "4"), Self::Five => write!(f, "5"), Self::Six => write!(f, "6"), Self::Seven => write!(f, "7"), Self::Eight => write!(f, "8"), Self::Nine => write!(f, "9"), Self::Ten => write!(f, "T"), Self::Jack => write!(f, "J"), Self::Queen => write!(f, "Q"), Self::King => write!(f, "K"), } } } #[derive(PartialEq, Debug, Copy, Clone)] pub struct Card { pub suit: Suit, pub value: Value, pub visible: bool, } impl fmt::Display for Card { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}{}", self.value, self.suit) } } impl Default for Card { fn default() -> Self { Card { suit: Suit::Spade, value: Value::Ace, // If you like to gamble... visible: false, } } } #[derive(Error, Debug, PartialEq)] pub enum StackingError { #[error("Trying to stack the same coloured suit")] SameColour, #[error("{0} is not \"next\" to {1}")] NotAdjacent(String, String), #[error("Foundations need to be the same suit")] WrongSuit, } impl Card { pub fn can_be_placed_on_pile(&self, top: &Card) -> Result<(), StackingError> { // Can't be the same Colour if self.suit.colour() == top.suit.colour() { return Err(StackingError::SameColour); } // Needs to be adjacent if self.value == Value::King || self.value.indexed_values() + 1 != top.value.indexed_values() { return Err(StackingError::NotAdjacent(self.to_string(), top.to_string())); } Ok(()) } pub fn can_be_placed_on_foundation(&self, top: &Option) -> Result<(), StackingError> { match top { None => { if self.value == Value::Ace { return Ok(()); } else { return Err(StackingError::NotAdjacent(self.to_string(), "an empty foundation".to_string())); } }, Some(c) => { if self.suit != c.suit { return Err(StackingError::WrongSuit); } if self.value.indexed_values() + 1 == c.value.indexed_values() { return Ok(()); } else { return Err(StackingError::NotAdjacent(self.to_string(), c.to_string())); } } } } } #[derive(Debug)] pub struct Deck { pub cards: Vec } impl Default for Deck { fn default() -> Self { let mut array = Vec::new(); for suit in Suit::iter() { for value in Value::iter() { array.push( Card { suit, value, ..Default::default() } ); } } Deck { cards: array, } } } pub enum Seed { Unseeded, SeedVal(u64), } impl Deck { pub fn shuffle(&mut self, seed: Seed) { match seed { Seed::SeedVal(_s) => unimplemented!("Not yet"), Seed::Unseeded => { let mut rng = rng(); self.cards.shuffle(&mut rng); } } } } #[derive(Debug)] pub struct CardAndPosition { card: Option, pos: CardPosition, } #[derive(Debug, PartialEq, Copy, Clone)] pub enum CardPosition { TopWaste, // I don't think we'd ever interact with anything other than the top of the Waste Pile(usize, usize), // (PileNumber, Index) Foundation(usize) } #[derive(Debug, Copy, Clone)] pub enum NumPassesThroughDeck { Unlimited, Limited(u64), } pub const NUM_PILES_KLONDIKE: usize = 7; #[derive(Debug, Clone)] pub struct Klondike { pub piles: [Vec; NUM_PILES_KLONDIKE], pub deck: Vec, // this term is a bit overloaded pub waste: Vec, pub foundation: [Vec; 4], // 4 = len of num suits max_num_passes_through_deck: NumPassesThroughDeck, pub current_num_passes_through_deck: u64, pub num_cards_turned: u8, } impl Klondike { pub fn deck_to_waste(self: &mut Self) { for _ in 0..self.num_cards_turned { let card = self.deck.pop(); match card { None => (), Some(c) => self.waste.push(c), } } } // TODO return some sort of error pub fn waste_to_deck(self: &mut Self) { match self.max_num_passes_through_deck { NumPassesThroughDeck::Unlimited => (), NumPassesThroughDeck::Limited(n) => if n >= self.current_num_passes_through_deck { // no! return }, } if self.deck.len() != 0 { // no! } else { self.deck = self.waste.clone(); self.waste = vec![]; self.current_num_passes_through_deck += 1; } } pub fn move_card(self, source_card: &CardAndPosition, dest_card: &CardAndPosition) -> bool { // TODO raise errors properly assert!(source_card.card.is_some()); assert_eq!(source_card.card.unwrap().visible, true); dest_card.pos.is_valid_dest(); // Maybe TODO - check the .cards is the actual card in that position match dest_card.pos { CardPosition::Pile(_, _) => self.move_card_to_pile(&source_card, &dest_card), CardPosition::Foundation(_f) => self.move_card_to_foundation(source_card, dest_card), CardPosition::TopWaste => unreachable!() } } pub fn move_card_to_foundation(mut self, source_card: &CardAndPosition, dest_card: &CardAndPosition) -> bool { // TODO check the cards referenced in source and dest are the ones that are actually there // TODO - ditto this for the "move to pile" function // TODO actually learn Rust properly so I can figure out why I need to clone the whole struct to check a value if source_card.pos != CardPosition::TopWaste && !self.clone().is_card_top_of_pile(&source_card.pos) { // TODO as above - make all these proper errors return false; } if source_card.card.unwrap().can_be_placed_on_foundation(&dest_card.card).is_err() { return false; } if let CardPosition::Foundation(foundation_index) = dest_card.pos { match source_card.pos { CardPosition::TopWaste => { let card = self.waste.pop().unwrap(); self.foundation[foundation_index].push(card); return true; }, CardPosition::Pile(pile_index, _) => { let card = self.piles[pile_index].pop().unwrap(); self.foundation[foundation_index].push(card); return true; }, CardPosition::Foundation(_) => { unreachable!() }, } } unreachable!(); } pub fn move_card_to_pile(mut self, source_card: &CardAndPosition, dest_card: &CardAndPosition) -> bool { if source_card.card.unwrap().can_be_placed_on_pile(&dest_card.card.unwrap()).is_err() { return false; } let (dpile_index, dcard_index) = match dest_card.pos { CardPosition::Pile(p, i) => (p, i), CardPosition::TopWaste | CardPosition::Foundation(_) => return false }; if dcard_index != self.piles[dpile_index].len() - 1 { // Can't move to anything other than top of pile // this should already have been checked in the is_card_top... // maybe I'll just delete this check... return false; } match source_card.pos { CardPosition::TopWaste => { let card = self.waste.pop().unwrap(); self.piles[dpile_index].push(card); }, CardPosition::Pile(spile_index, scard_index) => { let num_cards_to_take = self.piles[spile_index].len() - scard_index; // -1 maybe? let mut cards: Vec = Vec::new(); for _ in 0..num_cards_to_take { cards.push(self.piles[spile_index].pop().unwrap()); } for card in cards { self.piles[dpile_index].push(card); } // TODO Properly learn rust and why I can't use drain & extend methods // https://doc.rust-lang.org/std/vec/struct.Vec.html#method.drain }, CardPosition::Foundation(s_index) => { let card = self.foundation[s_index].pop().unwrap(); self.piles[dpile_index].push(card); }, } true } fn is_card_top_of_pile(self, pos: &CardPosition) -> bool { // TODO consider, at which point the Pos::Pile() ranges etc are correct match pos { CardPosition::Pile(pile_index, card_index) => { match self.piles.get(*pile_index) { Some(pile_index) => { if *card_index == (pile_index.len() - 1) { true } else { false } } None => false } }, CardPosition::TopWaste | CardPosition::Foundation(_) => false } } pub fn lowest_visible_card_in_pile_from_index(self, pile: usize, index: usize) -> usize { if self.piles[pile].len() == 0 { return 0; } for (i, card) in self.piles[pile].iter().skip(index).enumerate() { if card.visible { return i; } } panic!("Pile with only facedown cards - this is wrong") } } impl CardPosition { fn is_valid_dest(&self) { // TODO as with many other places - should probably raise an error instead of panic match self { CardPosition::TopWaste => panic!("You can't move cards to waste"), CardPosition::Pile(_, _) | CardPosition::Foundation(_) => (), } } } impl Default for Klondike { fn default() -> Self { let mut deck = Deck::default(); deck.shuffle(Seed::Unseeded); let mut piles: [Vec; NUM_PILES_KLONDIKE] = [Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new()]; for pile in 0..NUM_PILES_KLONDIKE { for num in 0..pile+1 { let mut c = deck.cards.pop().unwrap(); if num == pile { c.visible = true; } piles[pile].push(c); } } for card in &mut deck.cards { card.visible = true; } Self { piles, deck: deck.cards, waste: Vec::new(), foundation: [Vec::new(), Vec::new(), Vec::new(), Vec::new()], // is this really the best way? max_num_passes_through_deck: NumPassesThroughDeck::Unlimited, current_num_passes_through_deck: 0, num_cards_turned: 3, } } } #[cfg(test)] mod tests { use super::*; #[test] fn basic_card_stacking() { let testing_card = Card { suit: Suit::Heart, value: Value::Five, ..Default::default() }; let bad_same_suit = Card { suit: Suit::Heart, value: Value::Six, ..Default::default() }; assert_eq!(testing_card.can_be_placed_on_pile(&bad_same_suit), Err(StackingError::SameColour)); let bad_same_colour = Card { suit: Suit::Diamond, value: Value::Six, ..Default::default() }; assert_eq!(testing_card.can_be_placed_on_pile(&bad_same_colour), Err(StackingError::SameColour)); let should_stack_card = Card { suit: Suit::Club, value: Value::Six, ..Default::default() }; assert_eq!(testing_card.can_be_placed_on_pile(&should_stack_card), Ok(())); let value_too_high = Card { suit: Suit::Club, value: Value::Seven, ..Default::default() }; let not_adj_error = testing_card.can_be_placed_on_pile(&value_too_high); if let Err(e) = not_adj_error { match e { StackingError::NotAdjacent(_, _) => assert!(true), StackingError::SameColour => assert!(false, "Colour is different - incorrect error"), StackingError::WrongSuit => unreachable!(), } } else { assert!(false, "Cards are not adjacent - should be an error") } } #[test] fn basic_foundation_stacking() { let testing_card = Card { suit: Suit::Spade, value: Value::Ace, ..Default::default() }; assert_eq!(testing_card.can_be_placed_on_foundation(&None), Ok(())); let testing_card = Card { suit: Suit::Spade, value: Value::Two, ..Default::default() }; assert!(testing_card.can_be_placed_on_foundation(&None).is_err()); } #[test] fn moving_to_foundation() { // GOOD: Ace from Pile to Empty Foundation let mut klon = Klondike::default(); let ace = Card { suit: Suit::Spade, value: Value::Ace, .. Default::default() }; klon.piles[0].push(ace.clone()); let source_card = CardAndPosition { card: Some(ace.clone()), pos: CardPosition::Pile(0, 1) }; let dest_card = CardAndPosition { card: None, pos: CardPosition::Foundation(0), }; assert!(klon.move_card_to_foundation(&source_card, &dest_card)); // BAD: Non-Ace from Pile to Empty Foundaction let mut klon = Klondike::default(); let two = Card { suit: Suit::Spade, value: Value::Two, .. Default::default() }; klon.piles[0].push(two.clone()); let source_card = CardAndPosition { card: Some(two.clone()), pos: CardPosition::Pile(0, 1) }; let dest_card = CardAndPosition { card: None, pos: CardPosition::Foundation(0), }; assert!(!klon.move_card_to_foundation(&source_card, &dest_card)); // GOOD: Ace from TopWaste to Empty Foundaction let mut klon = Klondike::default(); let ace = Card { suit: Suit::Spade, value: Value::Ace, .. Default::default() }; klon.waste.push(ace.clone()); let source_card = CardAndPosition { card: Some(ace.clone()), pos: CardPosition::TopWaste, }; let dest_card = CardAndPosition { card: None, pos: CardPosition::Foundation(0), }; assert!(klon.move_card_to_foundation(&source_card, &dest_card)); // BAD: Non-Ace from TopWaste to Empty Foundaction let mut klon = Klondike::default(); let two = Card { suit: Suit::Spade, value: Value::Two, .. Default::default() }; klon.waste.push(two.clone()); let source_card = CardAndPosition { card: Some(two.clone()), pos: CardPosition::TopWaste, }; let dest_card = CardAndPosition { card: None, pos: CardPosition::Foundation(0), }; assert!(!klon.move_card_to_foundation(&source_card, &dest_card)); // GOOD: Two from TopWaste to Foundaction with Ace there let mut klon = Klondike::default(); let ace = Card { suit: Suit::Spade, value: Value::Ace, .. Default::default() }; let two = Card { suit: Suit::Spade, value: Value::Two, .. Default::default() }; klon.waste.push(two.clone()); klon.piles[0].push(ace.clone()); let source_card = CardAndPosition { card: Some(ace.clone()), pos: CardPosition::TopWaste, }; let dest_card = CardAndPosition { card: None, pos: CardPosition::Foundation(0), }; assert!(klon.move_card_to_foundation(&source_card, &dest_card)); // TODO the following cases: // - moving a card from pile to foundation when something is already there // - moving Ace from waste to top of pile // - moving a card frmo waste to foundation when something is already there // => for cases where it'll both work and not work - when cards are / aren't present } #[test] fn move_to_foundation_with_wrong_suit() { let mut klon = Klondike::default(); let ace = Card { suit: Suit::Heart, value: Value::Ace, .. Default::default() }; let two = Card { suit: Suit::Spade, value: Value::Two, .. Default::default() }; klon.waste.push(two.clone()); klon.piles[0].push(ace.clone()); let source_card = CardAndPosition { card: Some(ace.clone()), pos: CardPosition::TopWaste, }; let dest_card = CardAndPosition { card: Some(two.clone()), pos: CardPosition::Foundation(0), }; assert!(!klon.move_card_to_foundation(&source_card, &dest_card)); // TODO this is passing - but I don't think it's passing for the right reason } #[test] fn move_ace_to_foundation_with_card_present() { let mut klon = Klondike::default(); let ace_heart = Card { suit: Suit::Heart, value: Value::Ace, .. Default::default() }; let ace_spade = Card { suit: Suit::Spade, value: Value::Ace, .. Default::default() }; klon.waste.push(ace_heart.clone()); klon.piles[0].push(ace_spade.clone()); let source_card = CardAndPosition { card: Some(ace_heart.clone()), pos: CardPosition::TopWaste, }; let dest_card = CardAndPosition { card: Some(ace_spade.clone()), pos: CardPosition::Foundation(0), }; assert!(!klon.move_card_to_foundation(&source_card, &dest_card)); } #[test] fn get_a_whole_deck() { let d = Deck::default(); assert_eq!(d.cards.len(), 52); // Probably should test whether all cards are in... eh //println!("{:#?}", d); // A "manual" review looks alright } #[test] fn move_pile_card_to_good_pile() { let mut klon = Klondike::default(); let ace = Card { suit: Suit::Heart, value: Value::Ace, .. Default::default() }; let two = Card { suit: Suit::Spade, value: Value::Two, .. Default::default() }; klon.piles[0].push(two.clone()); klon.piles[1].push(ace.clone()); let source_card = CardAndPosition { card: Some(ace.clone()), pos: CardPosition::Pile(1, 2), }; let dest_card = CardAndPosition { card: Some(two.clone()), pos: CardPosition::Pile(0, 1), }; assert!(klon.move_card_to_pile(&source_card, &dest_card)); } #[test] fn move_pile_card_to_bad_pile() { let mut klon = Klondike::default(); let ace = Card { suit: Suit::Heart, value: Value::Ace, .. Default::default() }; let two = Card { suit: Suit::Diamond, value: Value::Two, .. Default::default() }; klon.piles[0].push(two.clone()); klon.piles[1].push(ace.clone()); let source_card = CardAndPosition { card: Some(ace.clone()), pos: CardPosition::Pile(1, 2), }; let dest_card = CardAndPosition { card: Some(two.clone()), pos: CardPosition::Pile(0, 1), }; assert!(!klon.move_card_to_pile(&source_card, &dest_card)); } }