From 17395898ed0c7a5b9ccf28a490205d8bb12c9f2f Mon Sep 17 00:00:00 2001 From: Geoffrey Allott Date: Sun, 28 May 2023 21:35:01 +0100 Subject: [PATCH] finish initial implementation of cribbage --- src/game/cribbage/mod.rs | 178 +++++++++++++++++++++++++++++++++---- src/game/cribbage/score.rs | 89 ++++++++++++------- 2 files changed, 216 insertions(+), 51 deletions(-) diff --git a/src/game/cribbage/mod.rs b/src/game/cribbage/mod.rs index 279640e..07fcc04 100644 --- a/src/game/cribbage/mod.rs +++ b/src/game/cribbage/mod.rs @@ -1,6 +1,16 @@ +use std::collections::{HashMap, HashSet}; + +use crate::card::{Card, FIFTY_TWO_CARD_DECK}; +use crate::rng::{Seed, WaveRng}; +use crate::seats::Seats; +use crate::username::Username; +use crate::util::timestamp::Timestamp; + +use super::{Action, ActionError, DealerAction, Game, StartCondition, UserAction, ValidatedUserAction}; + mod score; -/* +use self::score::{PeggingScore, score_4_card_cribbage_hand, score_pegging, value}; #[derive(Copy, Clone, Debug, PartialEq, Eq)] enum State { @@ -9,7 +19,9 @@ enum State { Choosing, TurnUp, Pegging, + ScoringPegging, Scoring, + ScoringBox, Completed, } @@ -71,6 +83,69 @@ impl Cribbage { points: HashMap::new(), } } + + fn hand_contains_card(&self, username: Username, card: Card) -> bool { + self.hands.get(&username).map_or(false, |hand| hand.contains(&card)) + } + + fn hand_size(&self, username: Username) -> usize { + self.hands.get(&username).map_or(0, HashSet::len) + } + + fn all_hands_are_empty(&self) -> bool { + self.hands.values().all(HashSet::is_empty) + } + + fn all_hands_dealt(&self) -> bool { + self.hands.values().map(HashSet::len).all(|len| len == 6) + } + + fn pegging_total(&self) -> u32 { + self.pegging_cards.iter().map(|(_, card)| value(card.rank)).sum() + } + + fn hand_has_playable_pegging_card(&self, username: Username) -> bool { + let total = self.pegging_total(); + self.hands.get(&username).map_or(false, |hand| hand.iter().any(|card| total + value(card.rank) <= 31)) + } + + fn next_player_still_in(&self) -> Option { + self.active.and_then(|player| self.seats.player_after_where(player, |player| self.players_still_in.contains(&player))) + } + + fn last_pegging_score(&self) -> Option<(Username, PeggingScore)> { + match self.pegging_cards.last() { + None => self.used_pegging_cards.last().map(|(username, _)| (*username, PeggingScore::one_for_a_go())), + Some((username, _)) => { + let cards: Vec<_> = self.pegging_cards.iter().map(|(_, card)| *card).collect(); + let score = score_pegging(&cards, false); + if score.points() > 0 { + Some((*username, score)) + } else { + None + } + } + } + } + + fn four_card_hand(&self, username: Username) -> Option<[Card; 4]> { + self.hands.get(&username).and_then(|hand| { + let hand: Vec<_> = hand.into_iter().collect(); + if let [a, b, c, d] = hand[..] { + Some([*a, *b, *c, *d]) + } else { + None + } + }) + } + + fn player_has_won(&self, username: Username) -> bool { + self.points.get(&username).map_or(false, |points| *points >= 121) + } + + fn winner(&self) -> Option { + self.points.iter().filter(|&(_, points)| *points >= 121).next().map(|(username, _)| *username) + } } impl Game for Cribbage { @@ -126,7 +201,7 @@ impl Game for Cribbage { Err(ActionError::OutOfTurn) } else if !self.hand_contains_card(username, card) { Err(ActionError::CardNotAvailable) - } else if self.current_total() + value(card.rank) > 31 { + } else if self.pegging_total() + value(card.rank) > 31 { Err(ActionError::CardNotPlayable) } else { Ok(ValidatedUserAction(UserAction { timestamp, username, action: Action::PlayCard { card } })) @@ -135,7 +210,7 @@ impl Game for Cribbage { (State::Pegging, Action::Pass) => { if Some(username) != self.active { Err(ActionError::OutOfTurn) - } else if self.hand_has_playable_card(username) { + } else if self.hand_has_playable_pegging_card(username) { Err(ActionError::CannotPass) } else { Ok(ValidatedUserAction(UserAction { timestamp, username, action: Action::Pass })) @@ -143,6 +218,7 @@ impl Game for Cribbage { } (State::TurnUp, _) => Err(ActionError::Dealing), (State::Scoring, _) => Err(ActionError::Dealing), + (State::ScoringBox, _) => Err(ActionError::Dealing), (State::Completed, _) => Err(ActionError::GameHasEnded), (_, _) => Err(ActionError::InvalidActionForGameType), } @@ -171,6 +247,10 @@ impl Game for Cribbage { self.state = State::Dealing; Ok(()) } + (_, Action::WinGame) => { + self.state = State::Completed; + Ok(()) + } (State::Dealing, Action::ReceiveCard { card: Some(card) }) => { self.deck.remove(&card); self.hands.entry(username).or_default().insert(card); @@ -185,7 +265,7 @@ impl Game for Cribbage { self.state = State::Choosing; Ok(()) } - (State::Choosing, Action::PutInBox { card: Some(Card) }) => { + (State::Choosing, Action::PutInBox { card: Some(card) }) => { if let Some(hand) = self.hands.get_mut(&username) { hand.remove(&card); } @@ -210,45 +290,67 @@ impl Game for Cribbage { } } self.pegging_cards.push((username, card)); + if self.last_pegging_score().is_some() { + self.state = State::ScoringPegging; + } Ok(()) } (State::Pegging, Action::Pass) => { self.players_still_in.remove(&username); - match self.active = self.active.and_then(|player| self.seats.player_after_where(player, |player| self.players_still_in.contains(&player))) { + match self.next_player_still_in() { None => { + self.state = State::ScoringPegging; self.used_pegging_cards.extend(self.pegging_cards.drain(..)); self.players_still_in = self.hands.iter().filter(|(_, cards)| !cards.is_empty()).map(|(&username, _)| username).collect(); - self.active = self.active.and_then(|player| self.seats.player_after_where(player, |player| self.players_still_in.contains(&player))); - } - Some(active) => self.active = active, + self.active = self.next_player_still_in(); + }, + active => self.active = active, } if self.all_hands_are_empty() { for (username, card) in self.used_pegging_cards.drain(..) { self.hands.entry(username).or_default().insert(card); } + self.active = self.dealer.and_then(|dealer| self.seats.player_after(dealer)); self.state = State::Scoring; } Ok(()) } - (State::Pegging, Action::Score { points, .. }) => { - *self.points.entry(username).or_default() += points; - match self.active.and_then(|player| self.seats.player_after_where(player, |player| self.players_still_in.contains(&player))) { + (State::ScoringPegging, Action::Score { points, .. }) => { + *self.points.entry(username).or_default() += points as u32; + match self.next_player_still_in() { None => { self.used_pegging_cards.extend(self.pegging_cards.drain(..)); self.players_still_in = self.hands.iter().filter(|(_, cards)| !cards.is_empty()).map(|(&username, _)| username).collect(); - self.active = self.active.and_then(|player| self.seats.player_after_where(player, |player| self.players_still_in.contains(&player))); + self.active = self.next_player_still_in(); } - Some(active) => self.active = active, + active => self.active = active, } - if self.all_hands_are_empty() { + if self.player_has_won(username) { + self.state = State::ScoringPegging; + } else if self.all_hands_are_empty() { for (username, card) in self.used_pegging_cards.drain(..) { self.hands.entry(username).or_default().insert(card); } + self.active = self.dealer.and_then(|dealer| self.seats.player_after(dealer)); self.state = State::Scoring; + } else { + self.state = State::Pegging; } + Ok(()) } (State::Scoring, Action::Score { points, .. }) => { - *self.points.entry(username).or_default() += points; + *self.points.entry(username).or_default() += points as u32; + if self.dealer == Some(username) { + self.state = State::ScoringBox; + } else { + self.active = self.seats.player_after(username); + } + Ok(()) + } + (State::ScoringBox, Action::Score { points, .. }) => { + *self.points.entry(username).or_default() += points as u32; + self.active = None; + Ok(()) } (State::Completed, _) => Err(ActionError::GameHasEnded), (_, _) => Err(ActionError::InvalidActionForGameType), @@ -299,8 +401,50 @@ impl Game for Cribbage { DealerAction::Leave } } + State::Choosing => DealerAction::WaitForPlayer, + State::TurnUp => { + if let Some(username) = self.dealer { + if let Some(&card) = rng.choose_from(&self.deck) { + DealerAction::TakeAction(ValidatedUserAction(UserAction { timestamp, username, action: Action::CommunityCard { card } })) + } else { + error!("Expected to deal a card but none were left in deck"); + DealerAction::Leave + } + } else { + error!("Expected to deal a card but there was no dealer"); + DealerAction::Leave + } + } + State::Pegging => DealerAction::WaitForPlayer, + State::ScoringPegging => { + if let Some(username) = self.winner() { + DealerAction::TakeAction(ValidatedUserAction(UserAction { timestamp, username, action: Action::WinGame })) + } else if let Some((username, score)) = self.last_pegging_score() { + DealerAction::TakeAction(ValidatedUserAction(UserAction { timestamp, username, action: Action::Score { points: score.points() as u64, reason: format!("{}", score) } })) + } else { + error!("Expected a pegging score"); + DealerAction::Leave + } + } + State::Scoring | State::ScoringBox => { + if let Some(username) = self.winner() { + DealerAction::TakeAction(ValidatedUserAction(UserAction { timestamp, username, action: Action::WinGame })) + } else if let Some(username) = self.active { + if let (Some(hand), Some(turn_up)) = (self.four_card_hand(username), self.turn_up) { + let score = score_4_card_cribbage_hand(hand, turn_up, self.state == State::ScoringBox); + DealerAction::TakeAction(ValidatedUserAction(UserAction { timestamp, username, action: Action::Score { points: score.points() as u64, reason: format!("{}", score) } })) + } else { + error!("Found no 4-card hand for scoring user"); + DealerAction::Leave + } + } else if let Some(username) = self.dealer.and_then(|dealer| self.seats.player_after(dealer)) { + DealerAction::TakeAction(ValidatedUserAction(UserAction { timestamp, username, action: Action::NextToDeal })) + } else { + error!("Could not find next dealer"); + DealerAction::Leave + } + } + State::Completed => DealerAction::Leave, } } } - -*/ diff --git a/src/game/cribbage/score.rs b/src/game/cribbage/score.rs index f2f2154..5ab2a6c 100644 --- a/src/game/cribbage/score.rs +++ b/src/game/cribbage/score.rs @@ -108,48 +108,69 @@ pub struct PeggingScore { thirty_one: u8, pair: u8, run: u8, + go: bool, } impl PeggingScore { + pub fn one_for_a_go() -> Self { + PeggingScore { fifteen: 0, thirty_one: 0, pair: 0, run: 0, go: true } + } + pub fn points(&self) -> u8 { - self.fifteen + self.thirty_one + self.pair + self.run + self.fifteen + self.thirty_one + self.pair + self.run + self.go as u8 } } impl Display for PeggingScore { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - PeggingScore { fifteen: 0, thirty_one: 0, pair: 0, run: 0 } => Ok(()), - PeggingScore { fifteen: 2, thirty_one: 0, pair: 0, run: 0 } => f.write_str("Fifteen for two"), - PeggingScore { fifteen: 2, thirty_one: 0, pair: 2, run: 0 } => f.write_str("Fifteen two and two is four"), - PeggingScore { fifteen: 2, thirty_one: 0, pair: 0, run: 3 } => f.write_str("Fifteen two and three is five"), - PeggingScore { fifteen: 2, thirty_one: 0, pair: 0, run: 4 } => f.write_str("Fifteen two and four is six"), - PeggingScore { fifteen: 2, thirty_one: 0, pair: 0, run: 5 } => f.write_str("Fifteen two and five is seven"), - PeggingScore { fifteen: 2, thirty_one: 0, pair: 6, run: 0 } => f.write_str("Fifteen two and six is eight"), - PeggingScore { fifteen: 2, thirty_one: 0, pair: 12, run: 0 } => f.write_str("Fifteen two and twelve is fourteen"), - PeggingScore { fifteen: 0, thirty_one: 2, pair: 0, run: 0 } => f.write_str("Thirty-one for two"), - PeggingScore { fifteen: 0, thirty_one: 2, pair: 2, run: 0 } => f.write_str("Thirty-one for two and two is four"), - PeggingScore { fifteen: 0, thirty_one: 2, pair: 0, run: 3 } => f.write_str("Thirty-one for two and three is five"), - PeggingScore { fifteen: 0, thirty_one: 2, pair: 0, run: 4 } => f.write_str("Thirty-one for two and four is six"), - PeggingScore { fifteen: 0, thirty_one: 2, pair: 0, run: 5 } => f.write_str("Thirty-one for two and five is seven"), - PeggingScore { fifteen: 0, thirty_one: 2, pair: 0, run: 6 } => f.write_str("Thirty-one for two and six is eight"), - PeggingScore { fifteen: 0, thirty_one: 2, pair: 6, run: 0 } => f.write_str("Thirty-one for two and six is eight"), - PeggingScore { fifteen: 0, thirty_one: 2, pair: 0, run: 7 } => f.write_str("Thirty-one for two and seven is nine"), - PeggingScore { fifteen: 0, thirty_one: 2, pair: 12, run: 0 } => f.write_str("Thirty-one for two and twelve is fourteen"), - PeggingScore { fifteen: 0, thirty_one: 0, pair: 2, run: 0 } => f.write_str("Two"), - PeggingScore { fifteen: 0, thirty_one: 0, pair: 0, run: 3 } => f.write_str("Three"), - PeggingScore { fifteen: 0, thirty_one: 0, pair: 0, run: 4 } => f.write_str("Four"), - PeggingScore { fifteen: 0, thirty_one: 0, pair: 0, run: 5 } => f.write_str("Five"), - PeggingScore { fifteen: 0, thirty_one: 0, pair: 0, run: 6 } => f.write_str("Six"), - PeggingScore { fifteen: 0, thirty_one: 0, pair: 6, run: 0 } => f.write_str("Six"), - PeggingScore { fifteen: 0, thirty_one: 0, pair: 0, run: 7 } => f.write_str("Seven"), - PeggingScore { fifteen: 0, thirty_one: 0, pair: 12, run: 0 } => f.write_str("Twelve"), + PeggingScore { fifteen: 0, thirty_one: 0, pair: 0, run: 0, go: false } => Ok(()), + PeggingScore { fifteen: 0, thirty_one: 0, pair: 0, run: 0, go: true } => f.write_str("One for a go"), + PeggingScore { fifteen: 2, thirty_one: 0, pair: 0, run: 0, go: false } => f.write_str("Fifteen for two"), + PeggingScore { fifteen: 2, thirty_one: 0, pair: 0, run: 0, go: true } => f.write_str("Fifteen for two and a go is three"), + PeggingScore { fifteen: 2, thirty_one: 0, pair: 2, run: 0, go: false } => f.write_str("Fifteen two and two is four"), + PeggingScore { fifteen: 2, thirty_one: 0, pair: 2, run: 0, go: true } => f.write_str("Fifteen two and two and a go is five"), + PeggingScore { fifteen: 2, thirty_one: 0, pair: 0, run: 3, go: false } => f.write_str("Fifteen two and three is five"), + PeggingScore { fifteen: 2, thirty_one: 0, pair: 0, run: 3, go: true } => f.write_str("Fifteen two and three and a go is six"), + PeggingScore { fifteen: 2, thirty_one: 0, pair: 0, run: 4, go: false } => f.write_str("Fifteen two and four is six"), + PeggingScore { fifteen: 2, thirty_one: 0, pair: 0, run: 4, go: true } => f.write_str("Fifteen two and four and a go is seven"), + PeggingScore { fifteen: 2, thirty_one: 0, pair: 0, run: 5, go: false } => f.write_str("Fifteen two and five is seven"), + PeggingScore { fifteen: 2, thirty_one: 0, pair: 0, run: 5, go: true } => f.write_str("Fifteen two and five and a go is eight"), + PeggingScore { fifteen: 2, thirty_one: 0, pair: 6, run: 0, go: false } => f.write_str("Fifteen two and six is eight"), + PeggingScore { fifteen: 2, thirty_one: 0, pair: 6, run: 0, go: true } => f.write_str("Fifteen two and six and a go is nine"), + PeggingScore { fifteen: 2, thirty_one: 0, pair: 12, run: 0, go: false } => f.write_str("Fifteen two and twelve is fourteen"), + PeggingScore { fifteen: 2, thirty_one: 0, pair: 12, run: 0, go: true } => f.write_str("Fifteen two and twelve and a go is fifteen"), + PeggingScore { fifteen: 0, thirty_one: 2, pair: 0, run: 0, go: false } => f.write_str("Thirty-one for two"), + PeggingScore { fifteen: 0, thirty_one: 2, pair: 2, run: 0, go: false } => f.write_str("Thirty-one for two and two is four"), + PeggingScore { fifteen: 0, thirty_one: 2, pair: 0, run: 3, go: false } => f.write_str("Thirty-one for two and three is five"), + PeggingScore { fifteen: 0, thirty_one: 2, pair: 0, run: 4, go: false } => f.write_str("Thirty-one for two and four is six"), + PeggingScore { fifteen: 0, thirty_one: 2, pair: 0, run: 5, go: false } => f.write_str("Thirty-one for two and five is seven"), + PeggingScore { fifteen: 0, thirty_one: 2, pair: 0, run: 6, go: false } => f.write_str("Thirty-one for two and six is eight"), + PeggingScore { fifteen: 0, thirty_one: 2, pair: 6, run: 0, go: false } => f.write_str("Thirty-one for two and six is eight"), + PeggingScore { fifteen: 0, thirty_one: 2, pair: 0, run: 7, go: false } => f.write_str("Thirty-one for two and seven is nine"), + PeggingScore { fifteen: 0, thirty_one: 2, pair: 12, run: 0, go: false } => f.write_str("Thirty-one for two and twelve is fourteen"), + PeggingScore { fifteen: 0, thirty_one: 0, pair: 2, run: 0, go: false } => f.write_str("Two"), + PeggingScore { fifteen: 0, thirty_one: 0, pair: 2, run: 0, go: true } => f.write_str("Two and a go is three"), + PeggingScore { fifteen: 0, thirty_one: 0, pair: 0, run: 3, go: false } => f.write_str("Three"), + PeggingScore { fifteen: 0, thirty_one: 0, pair: 0, run: 3, go: true } => f.write_str("Three and a go is four"), + PeggingScore { fifteen: 0, thirty_one: 0, pair: 0, run: 4, go: false } => f.write_str("Four"), + PeggingScore { fifteen: 0, thirty_one: 0, pair: 0, run: 4, go: true } => f.write_str("Four and a go is five"), + PeggingScore { fifteen: 0, thirty_one: 0, pair: 0, run: 5, go: false } => f.write_str("Five"), + PeggingScore { fifteen: 0, thirty_one: 0, pair: 0, run: 5, go: true } => f.write_str("Five and a go is six"), + PeggingScore { fifteen: 0, thirty_one: 0, pair: 0, run: 6, go: false } => f.write_str("Six"), + PeggingScore { fifteen: 0, thirty_one: 0, pair: 0, run: 6, go: true } => f.write_str("Six and a go is seven"), + PeggingScore { fifteen: 0, thirty_one: 0, pair: 6, run: 0, go: false } => f.write_str("Six"), + PeggingScore { fifteen: 0, thirty_one: 0, pair: 6, run: 0, go: true } => f.write_str("Six and a go is seven"), + PeggingScore { fifteen: 0, thirty_one: 0, pair: 0, run: 7, go: false } => f.write_str("Seven"), + PeggingScore { fifteen: 0, thirty_one: 0, pair: 0, run: 7, go: true } => f.write_str("Seven and a go is eight"), + PeggingScore { fifteen: 0, thirty_one: 0, pair: 12, run: 0, go: false } => f.write_str("Twelve"), + PeggingScore { fifteen: 0, thirty_one: 0, pair: 12, run: 0, go: true } => f.write_str("Twelve and a go is thirteen"), _ => write!(f, "[ERROR] {:?}", self), } } } -fn value(rank: Rank) -> u32 { +pub fn value(rank: Rank) -> u32 { match rank { Ace => 1, Two => 2, @@ -167,7 +188,7 @@ fn value(rank: Rank) -> u32 { } } -fn sum_value(cards: &[Card]) -> u32 { +pub fn sum_value(cards: &[Card]) -> u32 { cards.iter().map(|card| card.rank).map(value).sum() } @@ -175,8 +196,8 @@ fn is_run(cards: &[Card]) -> bool { cards.iter().map(|card| card.rank.ace_low_rank()).sorted().tuple_windows().all(|(rank1, rank2)| rank2 == rank1 + 1) } -pub fn score_pegging(cards: &[Card]) -> PeggingScore { - let mut score = PeggingScore { fifteen: 0, thirty_one: 0, pair: 0, run: 0 }; +pub fn score_pegging(cards: &[Card], go: bool) -> PeggingScore { + let mut score = PeggingScore { fifteen: 0, thirty_one: 0, pair: 0, run: 0, go }; if sum_value(cards) == 15 { score.fifteen += 2; @@ -343,19 +364,19 @@ mod test { #[test] fn pegging_scores() { - let score = score_pegging(&[ACE_OF_SPADES, TWO_OF_SPADES, THREE_OF_SPADES, FOUR_OF_SPADES, FIVE_OF_SPADES]); + let score = score_pegging(&[ACE_OF_SPADES, TWO_OF_SPADES, THREE_OF_SPADES, FOUR_OF_SPADES, FIVE_OF_SPADES], false); assert_eq!(7, score.points()); assert_eq!("Fifteen two and five is seven", format!("{}", score)); - let score = score_pegging(&[TEN_OF_HEARTS, FIVE_OF_CLUBS]); + let score = score_pegging(&[TEN_OF_HEARTS, FIVE_OF_CLUBS], false); assert_eq!(2, score.points()); assert_eq!("Fifteen for two", format!("{}", score)); - let score = score_pegging(&[TEN_OF_HEARTS, FIVE_OF_CLUBS, TEN_OF_CLUBS]); + let score = score_pegging(&[TEN_OF_HEARTS, FIVE_OF_CLUBS, TEN_OF_CLUBS], false); assert_eq!(0, score.points()); assert_eq!("", format!("{}", score)); - let score = score_pegging(&[TEN_OF_HEARTS, FIVE_OF_CLUBS, TEN_OF_CLUBS, SIX_OF_DIAMONDS]); + let score = score_pegging(&[TEN_OF_HEARTS, FIVE_OF_CLUBS, TEN_OF_CLUBS, SIX_OF_DIAMONDS], false); assert_eq!(2, score.points()); assert_eq!("Thirty-one for two", format!("{}", score)); } -- 2.34.1