integrate cribbage into back end, add a first simple test
authorGeoffrey Allott <geoffrey@allott.email>
Sun, 28 May 2023 21:39:10 +0000 (22:39 +0100)
committerGeoffrey Allott <geoffrey@allott.email>
Sun, 28 May 2023 21:39:10 +0000 (22:39 +0100)
src/game/cribbage/mod.rs
src/game/filter.rs
src/game/mod.rs

index 07fcc0480ca72da753cb6dbfa4a83d41f2e9d33b..1373ac3e2120b35a8ee07f3ba97732014ad2c5ee 100644 (file)
@@ -54,6 +54,7 @@ pub struct Cribbage {
     hands: HashMap<Username, HashSet<Card>>,
     turn_up: Option<Card>,
     box_cards: HashSet<Card>,
+    revealed: HashSet<Card>,
     pegging_cards: Vec<(Username, Card)>,
     used_pegging_cards: Vec<(Username, Card)>,
     players_still_in: HashSet<Username>,
@@ -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<Card> {
+        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<Username> {
-        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<UserAction>, 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);
+    }
+}
index 05a7fe182e99bb0cbc22a7859e301a217198af63..4b75f4937f676c5033c5318b520b280c795553cc 100644 (file)
@@ -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,
index 0d962a4124f632d9f6c355a2967aaf4a34b177bb..4bb79997655f12325de42abe194b5b479909832d 100644 (file)
@@ -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(),
         }
     }
 }