X-Git-Url: http://git.scottworley.com/pluta-lesnura/blobdiff_plain/10e7da7b429085b86b7d157514e4eedb9f426bd8..58616aebd3c387d97b4cc8c319deb8ceb5af745b:/src/lib.rs?ds=sidebyside diff --git a/src/lib.rs b/src/lib.rs index c3736e9..59a47a9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,22 +1,46 @@ +use rand::seq::SliceRandom; use rand::Rng; -pub const NUM_RANKS: u8 = 13; -pub const NUM_SUITS: u8 = 4; -pub const NUM_JOKERS: u8 = 2; -pub const NUM_CARDS: u8 = NUM_RANKS * NUM_SUITS + NUM_JOKERS; +pub const NUM_RANKS: usize = 13; +pub const NUM_SUITS: usize = 4; +pub const NUM_JOKERS: usize = 2; +pub const NUM_CARDS: usize = NUM_RANKS * NUM_SUITS + NUM_JOKERS; -#[derive(Clone, Copy, Eq, PartialEq)] +pub const STARTING_CARDS: u8 = 3; +pub const STARTING_MAD_SCIENCE_TOKENS: i8 = 15; +pub const STARTING_PROGRESS: i8 = -10; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct Rank(u8); +impl Rank { + #[must_use] + pub fn value(&self) -> u8 { + self.0 + 1 + } + #[must_use] + pub fn is_face(&self) -> bool { + self.value() > 10 + } + #[must_use] + pub fn random() -> Self { + Self( + rand::thread_rng() + .gen_range(0..NUM_RANKS) + .try_into() + .expect("Too many ranks?"), + ) + } +} #[derive(Clone, Copy, Eq, PartialEq)] pub struct Suit(u8); -#[derive(Clone, Copy, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct Card(u8); impl Card { #[must_use] pub fn is_joker(&self) -> bool { - self.0 >= NUM_RANKS * NUM_SUITS + usize::from(self.0) >= NUM_RANKS * NUM_SUITS } #[must_use] pub fn rank(&self) -> Option { @@ -36,15 +60,31 @@ pub enum WithOrWithoutJokers { #[must_use] pub fn deck(j: WithOrWithoutJokers) -> Vec { - let limit = match j { + let limit = u8::try_from(match j { WithOrWithoutJokers::WithJokers => NUM_CARDS, WithOrWithoutJokers::WithoutJokers => NUM_SUITS * NUM_RANKS, - }; + }) + .expect("Too many cards?"); (0..limit).map(Card).collect() } -#[derive(Clone, Copy)] +fn shuffle(cards: &mut Vec) { + cards.shuffle(&mut rand::thread_rng()); +} +#[must_use] +fn shuffled(mut cards: Vec) -> Vec { + shuffle(&mut cards); + cards +} + +#[derive(Clone, Copy, Debug)] pub struct PathLength(Rank); +impl PathLength { + #[must_use] + pub fn random() -> Self { + Self(Rank::random()) + } +} #[derive(Clone, Copy, Default)] pub struct PathLengthInfo(u16); @@ -57,7 +97,7 @@ impl PathLengthInfo { self.0 |= 1 << i.0; } pub fn reveal_random(&mut self, true_length: PathLength) -> Option { - let showing = u8::try_from(self.0.count_ones()).expect("There aren't that many bits"); + let showing = usize::try_from(self.0.count_ones()).expect("There aren't that many bits"); let not_showing = NUM_RANKS - showing; if not_showing <= 1 { return None; @@ -65,7 +105,7 @@ impl PathLengthInfo { let mut show = rand::thread_rng().gen_range(0..not_showing - 1); for i in 0..NUM_RANKS { - let r = Rank(i); + let r = Rank(u8::try_from(i).expect("Too many cards?")); if !self.is_showing(r) && r != true_length.0 { if show == 0 { self.reveal(r); @@ -78,6 +118,319 @@ impl PathLengthInfo { } } +#[derive(Default)] +pub struct Discard { + cards: Vec, +} +impl Discard { + pub fn discard(&mut self, card: Card) { + self.cards.push(card); + } + #[must_use] + pub fn top(&self) -> Option<&Card> { + self.cards.last() + } + fn len(&self) -> usize { + self.cards.len() + } +} + +pub struct Library { + cards: Vec, +} +impl Library { + #[must_use] + pub fn new(cards: Vec) -> Self { + Self { cards } + } + pub fn draw(&mut self, discard: &mut Discard) -> Option { + if self.cards.is_empty() { + if let Some(top_discard) = discard.cards.pop() { + std::mem::swap(&mut self.cards, &mut discard.cards); + discard.discard(top_discard); + shuffle(&mut self.cards); + } + } + self.cards.pop() + } + fn len(&self) -> usize { + self.cards.len() + } +} + +#[derive(Debug, Default)] +pub struct Hand { + cards: Vec, +} +impl Hand { + fn add(&mut self, card: Card) { + self.cards.push(card); + } + fn remove(&mut self, card: Card) -> Result<(), &'static str> { + let i = self + .cards + .iter() + .position(|&e| e == card) + .ok_or("That card is not in your hand")?; + self.cards.swap_remove(i); + Ok(()) + } + fn len(&self) -> usize { + self.cards.len() + } + #[cfg(test)] + fn random(&self) -> Option<&Card> { + self.cards.choose(&mut rand::thread_rng()) + } +} + +#[derive(Copy, Clone)] +pub struct PlayerIndex(usize); +impl PlayerIndex { + fn next(self, num_players: usize) -> Self { + Self((self.0 + 1) % num_players) + } +} + +#[derive(Copy, Clone, Debug)] +pub enum Play { + Play(Card), + Draw, +} + +#[derive(Eq, PartialEq)] +pub enum Phase { + Play, + Momentum, +} + +pub enum GameOutcome { + Loss, + Win, +} + +pub enum PlayOutcome { + Continue, + End(GameOutcome), +} + +pub struct Game { + mad_science_tokens: i8, + progress: [i8; NUM_SUITS], + path_lengths: [PathLength; NUM_SUITS], + path_length_info: [PathLengthInfo; NUM_SUITS], + library: Library, + discard: Discard, + hands: Vec, + turn: PlayerIndex, + phase: Phase, +} +impl Game { + pub fn add_player(&mut self) { + self.hands.push(Hand::default()); + for _ in 0..STARTING_CARDS { + self.draw_for_player(PlayerIndex(self.hands.len() - 1)); + } + } + /// # Errors + /// + /// Will return `Err` on invalid plays, like trying to draw during Play phase, + /// or trying to play a card that's not in your hand. + pub fn play(&mut self, play: Play) -> Result { + match play { + Play::Play(card) => self.play_card(card), + Play::Draw => self.draw_for_momentum(), + } + } + + #[must_use] + pub fn current_player_hand(&self) -> &Hand { + &self.hands[self.turn.0] + } + fn player_hand_mut(&mut self, pi: PlayerIndex) -> &mut Hand { + &mut self.hands[pi.0] + } + fn current_player_hand_mut(&mut self) -> &mut Hand { + self.player_hand_mut(self.turn) + } + + fn play_card(&mut self, card: Card) -> Result { + let momentum = self.apply_card(card)?; + if self.phase == Phase::Play && momentum { + self.phase = Phase::Momentum; + Ok(PlayOutcome::Continue) + } else { + Ok(self.end_of_turn()) + } + } + fn draw_for_momentum(&mut self) -> Result { + if self.phase != Phase::Momentum { + return Err("You don't have momentum"); + } + self.draw_for_player(self.turn); + Ok(self.end_of_turn()) + } + + fn draw_for_player(&mut self, pi: PlayerIndex) { + loop { + if let Some(card) = self.library.draw(&mut self.discard) { + if card.is_joker() { + self.remove_mad_science_token(); + self.discard.discard(card); + } else { + self.player_hand_mut(pi).add(card); + break; + } + } else { + println!("Library ran out of cards"); + } + } + } + fn remove_mad_science_token(&mut self) { + loop { + self.mad_science_tokens -= 1; + if self.mad_science_tokens != 0 { + break; + } + } + } + fn make_progress(&mut self, card: Card) { + let rank = card.rank().expect("Can't play jokers").0; + if rank < 6 { + let roll = rand::thread_rng().gen_range(1..=6); + if roll > rank { + self.remove_mad_science_token(); + } + } + self.progress[usize::from(card.suit().expect("Can't play jokers").0)] += 1; + } + fn forecast(&mut self, card: Card) { + let suit = usize::from(card.suit().expect("Can't play jokers").0); + self.path_length_info[suit].reveal_random(self.path_lengths[suit]); + } + // Returns whether or not this play grants momentum + fn apply_card(&mut self, card: Card) -> Result { + self.current_player_hand_mut().remove(card)?; + if card.rank().expect("Can't play jokers").is_face() { + self.forecast(card); + } else { + self.make_progress(card); + } + let suits_match = self + .discard + .top() + .map_or(false, |dis| dis.suit() == card.suit()); + self.discard.discard(card); + Ok(suits_match) + } + fn valid(&self) -> bool { + 108 == (self.library.len() + + self.discard.len() + + self.hands.iter().map(Hand::len).sum::()) + } + fn roll_mad_science(&mut self) -> PlayOutcome { + let mut tokens = std::iter::from_fn(|| Some(rand::thread_rng().gen_bool(0.5))) + .take(usize::try_from(self.mad_science_tokens.abs()).expect("wat?")); + let keep_going = if self.mad_science_tokens > 0 { + tokens.any(|t| !t) + } else { + tokens.all(|t| !t) + }; + if keep_going { + PlayOutcome::Continue + } else { + PlayOutcome::End(self.final_score()) + } + } + fn final_score(&self) -> GameOutcome { + if self + .progress + .iter() + .zip(self.path_lengths.iter()) + .any(|(&prog, len)| prog >= len.0 .0.try_into().expect("wat?")) + { + GameOutcome::Win + } else { + GameOutcome::Loss + } + } + fn end_of_turn(&mut self) -> PlayOutcome { + assert!(self.valid()); + self.phase = Phase::Play; + self.turn = self.turn.next(self.hands.len()); + if self.turn.0 == 0 { + if let PlayOutcome::End(game_outcome) = self.roll_mad_science() { + return PlayOutcome::End(game_outcome); + } + } + self.draw_for_player(self.turn); + assert!(self.valid()); + PlayOutcome::Continue + } +} +impl Default for Game { + fn default() -> Self { + Self { + mad_science_tokens: STARTING_MAD_SCIENCE_TOKENS, + progress: [STARTING_PROGRESS; NUM_SUITS], + path_lengths: std::iter::from_fn(|| Some(PathLength::random())) + .take(NUM_SUITS) + .collect::>() + .try_into() + .expect("wat?"), + path_length_info: [PathLengthInfo::default(); NUM_SUITS], + library: Library::new(shuffled( + [ + deck(WithOrWithoutJokers::WithJokers), + deck(WithOrWithoutJokers::WithJokers), + ] + .concat(), + )), + discard: Discard::default(), + hands: vec![], + turn: PlayerIndex(0), + phase: Phase::Play, + } + } +} + +pub struct Player(Box Play>); + +#[cfg(test)] +fn random_player(game: &Game) -> Play { + match game.phase { + Phase::Play => Play::Play( + *game + .current_player_hand() + .random() + .expect("I always have a card to play because I just drew one"), + ), + Phase::Momentum => { + if rand::thread_rng().gen_bool(0.5) { + Play::Draw + } else { + match game.current_player_hand().random() { + Some(card) => Play::Play(*card), + None => Play::Draw, + } + } + } + } +} + +/// # Errors +/// +/// Will return `Err` on invalid plays, like trying to draw during Play phase, +/// or trying to play a card that's not in your hand. +pub fn play(mut game: Game, mut players: Vec) -> Result { + game.draw_for_player(game.turn); + loop { + if let PlayOutcome::End(game_outcome) = game.play(players[game.turn.0].0(&game))? { + return Ok(game_outcome); + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -103,7 +456,49 @@ mod tests { #[test] fn test_deck() { use WithOrWithoutJokers::*; - let _d = deck(WithoutJokers); + let d = deck(WithoutJokers); + let rank_sum: u32 = d + .iter() + .map(Card::rank) + .flatten() + .map(|r| u32::from(r.value())) + .sum(); + assert_eq!(rank_sum, 364); let _dj = deck(WithJokers); } + + #[test] + fn test_library() { + let mut lib = Library::new(vec![Card(7)]); + let mut dis = Discard::default(); + dis.discard(Card(8)); + dis.discard(Card(9)); + assert_eq!(lib.draw(&mut dis), Some(Card(7))); + assert_eq!(lib.draw(&mut dis), Some(Card(8))); + assert_eq!(lib.draw(&mut dis), None); + } + + #[test] + fn test_hand() { + let mut h = Hand::default(); + assert!(h.remove(Card(4)).is_err()); + h.add(Card(4)); + assert!(h.remove(Card(3)).is_err()); + assert!(h.remove(Card(4)).is_ok()); + assert!(h.remove(Card(4)).is_err()); + } + + #[test] + fn test_game() { + for num_players in 1..10 { + let players: Vec<_> = std::iter::from_fn(|| Some(Player(Box::new(random_player)))) + .take(num_players) + .collect(); + let mut game = Game::default(); + for _ in 0..num_players { + game.add_player(); + } + assert!(play(game, players).is_ok()); + } + } }