From 169b768c62835e029e08f8970cfa89ae1562d14c Mon Sep 17 00:00:00 2001 From: Geoffrey Allott Date: Sun, 28 May 2023 22:39:10 +0100 Subject: [PATCH] integrate cribbage into back end, add a first simple test --- src/game/cribbage/mod.rs | 103 +++++++++++++++++++++++++++++++++++---- src/game/filter.rs | 1 + src/game/mod.rs | 4 ++ 3 files changed, 99 insertions(+), 9 deletions(-) diff --git a/src/game/cribbage/mod.rs b/src/game/cribbage/mod.rs index 07fcc04..1373ac3 100644 --- a/src/game/cribbage/mod.rs +++ b/src/game/cribbage/mod.rs @@ -54,6 +54,7 @@ pub struct Cribbage { hands: HashMap>, turn_up: Option, box_cards: HashSet, + revealed: HashSet, pegging_cards: Vec<(Username, Card)>, used_pegging_cards: Vec<(Username, Card)>, players_still_in: HashSet, @@ -76,6 +77,7 @@ impl Cribbage { hands: HashMap::new(), turn_up: None, box_cards: HashSet::new(), + revealed: HashSet::new(), pegging_cards: Vec::new(), used_pegging_cards: Vec::new(), players_still_in: HashSet::new(), @@ -128,6 +130,10 @@ impl Cribbage { } } + fn unrevealed_card(&self, username: Username) -> Option { + self.hands.get(&username).and_then(|hand| hand.iter().filter(|card| !self.revealed.contains(*card)).next().copied()) + } + fn four_card_hand(&self, username: Username) -> Option<[Card; 4]> { self.hands.get(&username).and_then(|hand| { let hand: Vec<_> = hand.into_iter().collect(); @@ -140,11 +146,11 @@ impl Cribbage { } fn player_has_won(&self, username: Username) -> bool { - self.points.get(&username).map_or(false, |points| *points >= 121) + self.points.get(&username).map_or(false, |points| *points >= self.settings.target_score) } fn winner(&self) -> Option { - self.points.iter().filter(|&(_, points)| *points >= 121).next().map(|(username, _)| *username) + self.points.iter().filter(|&(_, points)| *points >= self.settings.target_score).next().map(|(username, _)| *username) } } @@ -241,6 +247,7 @@ impl Game for Cribbage { self.hands.clear(); self.receiver = self.seats.player_after(username); self.box_cards.clear(); + self.revealed.clear(); self.pegging_cards.clear(); self.players_still_in.clear(); self.active = None; @@ -293,6 +300,7 @@ impl Game for Cribbage { if self.last_pegging_score().is_some() { self.state = State::ScoringPegging; } + self.active = self.next_player_still_in(); Ok(()) } (State::Pegging, Action::Pass) => { @@ -302,7 +310,6 @@ impl Game for Cribbage { 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.next_player_still_in(); }, active => self.active = active, } @@ -317,13 +324,13 @@ impl Game for Cribbage { } (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(); + if self.next_player_still_in().is_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 = Some(username); + if !self.players_still_in.contains(&username) { self.active = self.next_player_still_in(); } - active => self.active = active, } if self.player_has_won(username) { self.state = State::ScoringPegging; @@ -338,6 +345,10 @@ impl Game for Cribbage { } Ok(()) } + (State::Scoring, Action::RevealCard { card }) => { + self.revealed.insert(card); + Ok(()) + } (State::Scoring, Action::Score { points, .. }) => { *self.points.entry(username).or_default() += points as u32; if self.dealer == Some(username) { @@ -347,6 +358,10 @@ impl Game for Cribbage { } Ok(()) } + (State::ScoringBox, Action::RevealCard { card }) => { + self.revealed.insert(card); + Ok(()) + } (State::ScoringBox, Action::Score { points, .. }) => { *self.points.entry(username).or_default() += points as u32; self.active = None; @@ -430,7 +445,9 @@ impl Game for Cribbage { 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) { + if let Some(card) = self.unrevealed_card(username) { + DealerAction::TakeAction(ValidatedUserAction(UserAction { timestamp, username, action: Action::RevealCard { card } })) + } else 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 { @@ -448,3 +465,71 @@ impl Game for Cribbage { } } } + +#[cfg(test)] +mod tests { + use super::*; + + fn test_game(actions: Vec, settings: CribbageSettings, seed: Seed) { + let mut game = Cribbage::new(0, settings, seed); + for action in actions { + match action.action { + Action::Join { .. } | Action::PutInBox { .. } | Action::PlayCard { .. } => { + let validated = game.validate_action(action.clone()).unwrap(); + assert_eq!(ValidatedUserAction(action), validated); + game.take_action(validated).unwrap(); + } + _ => { + let dealer_action = game.next_dealer_action(action.timestamp); + if let DealerAction::TakeAction(ValidatedUserAction(dealer_action)) = dealer_action { + assert_eq!(action, dealer_action); + game.take_action(ValidatedUserAction(action)).unwrap(); + } else { + panic!("Expected DealerAction::TakeAction{{ {:?} }}, got {:?}", action, dealer_action); + } + } + } + } + } + + #[test] + fn simple_cribbage_game() { + let actions = r#"[ + {"timestamp":1685220615999,"username":"Geoff","action":{"action":"Join","seat":0,"chips":0}}, + {"timestamp":1685220772960,"username":"Aga","action":{"action":"Join","seat":1,"chips":0}}, + {"timestamp":1685308376245,"username":"Geoff","action":{"action":"NextToDeal"}}, + {"timestamp":1685308376245,"username":"Aga","action":{"action":"ReceiveCard","card":{"rank":"Three","suit":"Hearts"}}}, + {"timestamp":1685308376246,"username":"Geoff","action":{"action":"ReceiveCard","card":{"rank":"Three","suit":"Clubs"}}}, + {"timestamp":1685308376246,"username":"Aga","action":{"action":"ReceiveCard","card":{"rank":"King","suit":"Hearts"}}}, + {"timestamp":1685308376246,"username":"Geoff","action":{"action":"ReceiveCard","card":{"rank":"Nine","suit":"Clubs"}}}, + {"timestamp":1685308376246,"username":"Aga","action":{"action":"ReceiveCard","card":{"rank":"Four","suit":"Clubs"}}}, + {"timestamp":1685308376246,"username":"Geoff","action":{"action":"ReceiveCard","card":{"rank":"Two","suit":"Spades"}}}, + {"timestamp":1685308376247,"username":"Aga","action":{"action":"ReceiveCard","card":{"rank":"Jack","suit":"Diamonds"}}}, + {"timestamp":1685308376247,"username":"Geoff","action":{"action":"ReceiveCard","card":{"rank":"King","suit":"Clubs"}}}, + {"timestamp":1685308376247,"username":"Aga","action":{"action":"ReceiveCard","card":{"rank":"King","suit":"Diamonds"}}}, + {"timestamp":1685308376247,"username":"Geoff","action":{"action":"ReceiveCard","card":{"rank":"Seven","suit":"Clubs"}}}, + {"timestamp":1685308376248,"username":"Aga","action":{"action":"ReceiveCard","card":{"rank":"Ten","suit":"Clubs"}}}, + {"timestamp":1685308376248,"username":"Geoff","action":{"action":"ReceiveCard","card":{"rank":"Five","suit":"Clubs"}}}, + {"timestamp":1685308376248,"username":"Geoff","action":{"action":"EndDeal"}}, + {"timestamp":1685308376248,"username":"Geoff","action":{"action":"PutInBox","card":{"rank":"Five","suit":"Clubs"}}}, + {"timestamp":1685308376248,"username":"Geoff","action":{"action":"PutInBox","card":{"rank":"King","suit":"Clubs"}}}, + {"timestamp":1685308376248,"username":"Aga","action":{"action":"PutInBox","card":{"rank":"Ten","suit":"Clubs"}}}, + {"timestamp":1685308376248,"username":"Aga","action":{"action":"PutInBox","card":{"rank":"Jack","suit":"Diamonds"}}}, + {"timestamp":1685308750725,"username":"Geoff","action":{"action":"CommunityCard","card":{"rank":"Six","suit":"Diamonds"}}}, + {"timestamp":1685308376248,"username":"Aga","action":{"action":"PlayCard","card":{"rank":"King","suit":"Diamonds"}}}, + {"timestamp":1685308376248,"username":"Geoff","action":{"action":"PlayCard","card":{"rank":"Two","suit":"Spades"}}}, + {"timestamp":1685308376248,"username":"Aga","action":{"action":"PlayCard","card":{"rank":"Three","suit":"Hearts"}}}, + {"timestamp":1685309372399,"username":"Aga","action":{"action":"Score","points":2,"reason":"Fifteen for two"}} + ]"#; + + let actions = serde_json::from_str(actions).unwrap(); + + let settings = r#"{"format":"Cribbage","title":"Cribbage Testing","max_players":2,"target_score":181,"start_time":null}"#; + let settings = serde_json::from_str(settings).unwrap(); + + let seed = r#"{"rng":"ChaCha20","seed":"3789582e9d1a9229bd22d2a61b156bcf907e4cb44d613f97c08b16ec73ff0d90"}"#; + let seed = serde_json::from_str(seed).unwrap(); + + test_game(actions, settings, seed); + } +} diff --git a/src/game/filter.rs b/src/game/filter.rs index 05a7fe1..4b75f49 100644 --- a/src/game/filter.rs +++ b/src/game/filter.rs @@ -42,6 +42,7 @@ impl Filter for Field { GameSettings::Chatroom(_) => format.eq_ignore_ascii_case("Chatroom"), GameSettings::KnockOutWhist(_) => format.eq_ignore_ascii_case("KnockOutWhist"), GameSettings::TexasHoldEm(_) => format.eq_ignore_ascii_case("TexasHoldEm"), + GameSettings::Cribbage(_) => format.eq_ignore_ascii_case("Cribbage"), }, Field::Title(title) => summary.settings.title().to_lowercase().contains(&title.to_lowercase()), Field::Completed(completed) => summary.status.completed == *completed, diff --git a/src/game/mod.rs b/src/game/mod.rs index 0d962a4..4bb7999 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -13,6 +13,7 @@ use crate::username::Username; use crate::util::timestamp::Timestamp; use self::chatroom::{Chatroom, ChatroomSettings}; +use self::cribbage::{Cribbage, CribbageSettings}; use self::filter::{parse_filter, Filter, ParsedFilter}; use self::poker::{TexasHoldEm, TexasHoldEmSettings}; use self::whist::{KnockOutWhist, KnockOutWhistSettings}; @@ -50,6 +51,7 @@ impl dyn Game { GameSettings::Chatroom(settings) => Box::new(Chatroom::new(id, settings)), GameSettings::KnockOutWhist(settings) => Box::new(KnockOutWhist::new(id, settings, seed)), GameSettings::TexasHoldEm(settings) => Box::new(TexasHoldEm::new(id, settings, seed)), + GameSettings::Cribbage(settings) => Box::new(Cribbage::new(id, settings, seed)), } } } @@ -60,6 +62,7 @@ pub enum GameSettings { Chatroom(ChatroomSettings), KnockOutWhist(KnockOutWhistSettings), TexasHoldEm(TexasHoldEmSettings), + Cribbage(CribbageSettings), } impl GameSettings { @@ -68,6 +71,7 @@ impl GameSettings { GameSettings::Chatroom(settings) => settings.title(), GameSettings::KnockOutWhist(settings) => settings.title(), GameSettings::TexasHoldEm(settings) => settings.title(), + GameSettings::Cribbage(settings) => settings.title(), } } } -- 2.34.1