From ba58747513b36b80e858871e7d89076fac43a259 Mon Sep 17 00:00:00 2001 From: Geoffrey Allott Date: Sun, 8 Aug 2021 17:28:37 +0100 Subject: [PATCH] more logic for cribbage --- src/game/action.rs | 5 + src/game/cribbage/mod.rs | 305 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 310 insertions(+) diff --git a/src/game/action.rs b/src/game/action.rs index 9e17c35..2ce6015 100644 --- a/src/game/action.rs +++ b/src/game/action.rs @@ -46,6 +46,8 @@ pub enum Action { RevealCard { card: Card }, CutCard { card: Card }, PlayCard { card: Card }, + PutInBox { card: Option }, + Pass, ChooseTrumps { suit: Suit }, Fold, TimeoutFold, @@ -55,6 +57,7 @@ pub enum Action { WinTrick, WinCall, WinHand { chips: u64, hand: Option }, + Score { points: u64, reason: String }, WinGame, Message { message: String }, Leave, @@ -85,6 +88,7 @@ pub enum ActionError { StartingStackTooSmall, StartingStackTooLarge, CannotFold, + CannotPass, NotEnoughChips, BetSizeTooSmall, BetSizeTooLarge, @@ -110,6 +114,7 @@ impl Display for ActionError { ActionError::StartingStackTooSmall => f.write_str("Starting stack provided is too small"), ActionError::StartingStackTooLarge => f.write_str("Starting stack provided is too large"), ActionError::CannotFold => f.write_str("Cannot fold when able to check"), + ActionError::CannotPass => f.write_str("Cannot pass when a card is playable"), ActionError::NotEnoughChips => f.write_str("Not enough chips to make bet"), ActionError::BetSizeTooSmall => f.write_str("Bet size is too small"), ActionError::BetSizeTooLarge => f.write_str("Bet size is too large"), diff --git a/src/game/cribbage/mod.rs b/src/game/cribbage/mod.rs index 0d47f79..279640e 100644 --- a/src/game/cribbage/mod.rs +++ b/src/game/cribbage/mod.rs @@ -1 +1,306 @@ mod score; + +/* + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +enum State { + NotStarted, + Dealing, + Choosing, + TurnUp, + Pegging, + Scoring, + Completed, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CribbageSettings { + title: String, + max_players: u32, + target_score: u32, + #[serde(default)] + start_time: StartCondition, +} + +impl CribbageSettings { + pub fn title(&self) -> &str { + &self.title + } +} + +#[derive(Clone, Debug)] +pub struct Cribbage { + id: i64, + settings: CribbageSettings, + rng: WaveRng, + actions_len: usize, + state: State, + seats: Seats, + dealer: Option, + receiver: Option, + deck: HashSet, + hands: HashMap>, + turn_up: Option, + box_cards: HashSet, + pegging_cards: Vec<(Username, Card)>, + used_pegging_cards: Vec<(Username, Card)>, + players_still_in: HashSet, + active: Option, + points: HashMap, +} + +impl Cribbage { + pub fn new(id: i64, settings: CribbageSettings, seed: Seed) -> Self { + Self { + id, + settings, + rng: seed.into_rng(), + actions_len: 0, + state: State::NotStarted, + seats: Seats::new(), + dealer: None, + receiver: None, + deck: FIFTY_TWO_CARD_DECK.iter().cloned().collect(), + hands: HashMap::new(), + turn_up: None, + box_cards: HashSet::new(), + pegging_cards: Vec::new(), + used_pegging_cards: Vec::new(), + players_still_in: HashSet::new(), + active: None, + points: HashMap::new(), + } + } +} + +impl Game for Cribbage { + fn id(&self) -> i64 { + self.id + } + + fn players(&self) -> HashSet { + self.seats.player_set() + } + + fn actions_len(&self) -> usize { + self.actions_len + } + + fn validate_action(&self, UserAction { timestamp, username, action }: UserAction) -> Result { + match (self.state, action) { + (_, Action::AddOn { .. }) | (_, Action::RevealCard { .. }) | (_, Action::Fold) | (_, Action::TimeoutFold) | (_, Action::Bet { .. }) => { + Err(ActionError::InvalidActionForGameType) + } + (_, Action::Message { message }) => Ok(ValidatedUserAction(UserAction { timestamp, username, action: Action::Message { message } })), + (State::NotStarted, Action::Join { seat, .. }) => { + if self.seats.contains_player(username) { + Err(ActionError::AlreadyJoined) + } else if self.seats.players_len() > self.settings.max_players as usize { + Err(ActionError::NoSeatAvailable) + } else if !self.seats.seat_is_available(seat) { + Err(ActionError::SeatNotAvailable) + } else { + Ok(ValidatedUserAction(UserAction { timestamp, username, action: Action::Join { seat, chips: 0 } })) + } + } + (State::Completed, Action::Join { .. }) => Err(ActionError::GameHasEnded), + (_, Action::Join { .. }) => Err(ActionError::GameHasStarted), + (_, _) if !self.seats.contains_player(username) => Err(ActionError::NotAuthorised), + (State::NotStarted, Action::Leave) => Ok(ValidatedUserAction(UserAction { timestamp, username, action: Action::Leave })), + (State::Completed, Action::Leave) => Err(ActionError::GameHasEnded), + (_, Action::Leave) => Err(ActionError::GameHasStarted), + (State::Dealing, _) => Err(ActionError::Dealing), + (State::Choosing, Action::PutInBox { card: Some(card) }) => { + if self.hand_contains_card(username, card) { + if self.hand_size(username) > 4 { + Ok(ValidatedUserAction(UserAction { timestamp, username, action: Action::PutInBox { card: Some(card) } })) + } else { + Err(ActionError::CardNotPlayable) + } + } else { + Err(ActionError::CardNotAvailable) + } + } + (State::Pegging, Action::PlayCard { card }) => { + if Some(username) != self.active { + Err(ActionError::OutOfTurn) + } else if !self.hand_contains_card(username, card) { + Err(ActionError::CardNotAvailable) + } else if self.current_total() + value(card.rank) > 31 { + Err(ActionError::CardNotPlayable) + } else { + Ok(ValidatedUserAction(UserAction { timestamp, username, action: Action::PlayCard { card } })) + } + } + (State::Pegging, Action::Pass) => { + if Some(username) != self.active { + Err(ActionError::OutOfTurn) + } else if self.hand_has_playable_card(username) { + Err(ActionError::CannotPass) + } else { + Ok(ValidatedUserAction(UserAction { timestamp, username, action: Action::Pass })) + } + } + (State::TurnUp, _) => Err(ActionError::Dealing), + (State::Scoring, _) => Err(ActionError::Dealing), + (State::Completed, _) => Err(ActionError::GameHasEnded), + (_, _) => Err(ActionError::InvalidActionForGameType), + } + } + + fn take_action(&mut self, ValidatedUserAction(UserAction { username, action, .. }): ValidatedUserAction) -> Result<(), ActionError> { + self.actions_len += 1; + if matches!(action, Action::Message { .. }) { + return Ok(()); + } + self.rng.advance(); + match (self.state, action) { + (_, Action::AddOn { .. }) | (_, Action::Fold) | (_, Action::TimeoutFold) | (_, Action::Bet { .. }) => Err(ActionError::InvalidActionForGameType), + (State::NotStarted, Action::Join { seat, .. }) => self.seats.add_player(seat, username), + (State::NotStarted, Action::Leave) => self.seats.remove_player(username), + (_, Action::NextToDeal) => { + self.dealer = Some(username); + self.deck = FIFTY_TWO_CARD_DECK.iter().cloned().collect(); + self.turn_up = None; + self.hands.clear(); + self.receiver = self.seats.player_after(username); + self.box_cards.clear(); + self.pegging_cards.clear(); + self.players_still_in.clear(); + self.active = None; + self.state = State::Dealing; + Ok(()) + } + (State::Dealing, Action::ReceiveCard { card: Some(card) }) => { + self.deck.remove(&card); + self.hands.entry(username).or_default().insert(card); + if self.all_hands_dealt() { + self.receiver = None; + } else { + self.receiver = self.receiver.and_then(|player| self.seats.player_after(player)); + } + Ok(()) + } + (State::Dealing, Action::EndDeal) => { + self.state = State::Choosing; + Ok(()) + } + (State::Choosing, Action::PutInBox { card: Some(Card) }) => { + if let Some(hand) = self.hands.get_mut(&username) { + hand.remove(&card); + } + self.box_cards.insert(card); + if self.box_cards.len() == 4 { + self.state = State::TurnUp; + } + Ok(()) + } + (State::TurnUp, Action::CommunityCard { card }) => { + self.turn_up = Some(card); + self.state = State::Pegging; + self.active = self.dealer.and_then(|dealer| self.seats.player_after(dealer)); + self.players_still_in = self.players(); + Ok(()) + } + (State::Pegging, Action::PlayCard { card }) => { + if let Some(hand) = self.hands.get_mut(&username) { + hand.remove(&card); + if hand.is_empty() { + self.players_still_in.remove(&username); + } + } + self.pegging_cards.push((username, card)); + 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))) { + 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))); + } + Some(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.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))) { + 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))); + } + Some(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.state = State::Scoring; + } + } + (State::Scoring, Action::Score { points, .. }) => { + *self.points.entry(username).or_default() += points; + } + (State::Completed, _) => Err(ActionError::GameHasEnded), + (_, _) => Err(ActionError::InvalidActionForGameType), + } + } + + fn next_dealer_action(&self, timestamp: Timestamp) -> DealerAction { + let mut rng = self.rng.clone(); + match self.state { + State::NotStarted => match self.settings.start_time { + StartCondition::WhenFull => { + if self.seats.players_len() == self.settings.max_players as usize { + if let Some(username) = rng.choose_from(self.seats.player_set()) { + return DealerAction::TakeAction(ValidatedUserAction(UserAction { timestamp, username, action: Action::NextToDeal })); + } + } + DealerAction::WaitForPlayer + } + StartCondition::AtTime(start_time) => { + if timestamp >= start_time { + if self.seats.players_len() < 2 { + return DealerAction::WaitForPlayer; + } + if let Some(username) = rng.choose_from(self.seats.player_set()) { + return DealerAction::TakeAction(ValidatedUserAction(UserAction { timestamp, username, action: Action::NextToDeal })); + } + } + DealerAction::WaitUntil(start_time) + } + }, + State::Dealing => { + if let Some(username) = self.receiver { + let card = rng.choose_from(&self.deck).cloned(); + DealerAction::TakeAction(ValidatedUserAction(UserAction { timestamp, username, action: Action::ReceiveCard { card } })) + } else if let Some(username) = self.dealer { + if self.seats.players_len() == 3 && self.box_cards.is_empty() { + if let Some(&card) = rng.choose_from(&self.deck) { + DealerAction::TakeAction(ValidatedUserAction(UserAction { timestamp, username, action: Action::PutInBox { card: Some(card) } })) + } else { + error!("Expected to deal a card but none were left in deck"); + DealerAction::Leave + } + } else { + DealerAction::TakeAction(ValidatedUserAction(UserAction { timestamp, username, action: Action::EndDeal })) + } + } else { + error!("Expected to deal a card but there was no dealer"); + DealerAction::Leave + } + } + } + } +} + +*/ -- 2.34.1