initial attempt at ghost dealers/blinds
authorGeoffrey Allott <geoffrey@allott.email>
Fri, 5 Mar 2021 17:33:18 +0000 (17:33 +0000)
committerGeoffrey Allott <geoffrey@allott.email>
Fri, 5 Mar 2021 17:33:18 +0000 (17:33 +0000)
src/game/poker/holdem.rs

index 9814972b60690df593d561ed379ad6b13184dc62..104c8b77e6946130ecabfc8e74d11ba09349b9c0 100644 (file)
@@ -57,10 +57,11 @@ pub struct TexasHoldEm {
     active: Option<Username>,
     bets: HashMap<Username, u64>,
     committed: HashMap<Username, u64>,
-    players: HashSet<Username>,
+    in_hand: HashSet<Username>,
     pot: u64,
     small_blind: u64,
     big_blind: u64,
+    ghosts: HashMap<Username, u8>,
 }
 
 impl TexasHoldEm {
@@ -84,10 +85,11 @@ impl TexasHoldEm {
             active: None,
             bets: HashMap::new(),
             committed: HashMap::new(),
-            players: HashSet::new(),
+            in_hand: HashSet::new(),
             pot: 0,
             small_blind,
             big_blind,
+            ghosts: HashMap::new(),
         }
     }
 
@@ -123,20 +125,20 @@ impl TexasHoldEm {
     }
 
     fn is_able_to_bet(&self, username: Username) -> bool {
-        self.players.contains(&username) && !matches!(self.stacks.get(&username), Some(&0) | None)
+        self.in_hand.contains(&username) && !matches!(self.stacks.get(&username), Some(&0) | None)
     }
 
     fn players_able_to_bet(&self) -> usize {
-        self.players.iter().filter(|&&username| self.is_able_to_bet(username)).count()
+        self.in_hand.iter().filter(|&&username| self.is_able_to_bet(username)).count()
     }
 
     fn all_bets_are_in(&self) -> bool {
-        self.players.iter().filter(|&&username| self.is_able_to_bet(username))
+        self.in_hand.iter().filter(|&&username| self.is_able_to_bet(username))
             .all(|username| self.bets.contains_key(username))
     }
 
     fn player_is_all_in(&self, username: Username) -> bool {
-        self.players.contains(&username) && matches!(self.stacks.get(&username), Some(&0) | None)
+        self.in_hand.contains(&username) && matches!(self.stacks.get(&username), Some(&0) | None)
     }
 
     fn bets_of_players_who_are_not_all_in(&self) -> impl Iterator<Item=u64> + '_ {
@@ -173,6 +175,19 @@ impl TexasHoldEm {
         self.all_bets_are_in() && self.all_bets_are_equal()
             || self.players_able_to_bet() <= 1 && self.bets.len() == 0
     }
+
+    fn remove_ghosts(&mut self) -> Result<(), ActionError> {
+        for (&ghost, &turns) in &self.ghosts {
+            if turns == 0 {
+                self.seats.remove_player(ghost)?;
+            }
+        }
+        self.ghosts.retain(|_, &mut turns| turns > 0);
+        for (_, turns) in &mut self.ghosts {
+            *turns -= 1;
+        }
+        Ok(())
+    }
 }
 
 impl Game for TexasHoldEm {
@@ -220,7 +235,7 @@ impl Game for TexasHoldEm {
             (State::Dealing, _) | (State::DealingFlop, _) | (State::DealingTurn, _) | (State::DealingRiver, _) => {
                 Err(ActionError::Dealing)
             }
-            (_, Action::Fold) | (_, Action::Bet{..}) if !self.players.contains(&username) => Err(ActionError::NotInHand),
+            (_, Action::Fold) | (_, Action::Bet{..}) if !self.in_hand.contains(&username) => Err(ActionError::NotInHand),
             (_, Action::Fold) | (_, Action::Bet{..}) if self.active != Some(username) => Err(ActionError::OutOfTurn),
             (_, Action::Fold) if self.chips_to_call(username) == 0 => Err(ActionError::CannotFold),
             (_, Action::Fold) => Ok(ValidatedUserAction(UserAction{timestamp, username, action: Action::Fold})),
@@ -258,18 +273,37 @@ impl Game for TexasHoldEm {
                 self.stacks.insert(username, chips);
                 self.seats.add_player(seat, username)
             }
-            (State::NotStarted, Action::Leave) | (_, Action::KnockedOut) => {
+            (State::NotStarted, Action::Leave) => {
                 self.stacks.remove(&username);
                 self.seats.remove_player(username)
             }
+            (_, Action::KnockedOut) => {
+                self.stacks.remove(&username);
+                if self.stacks.len() <= 1 {
+                    return self.seats.remove_player(username);
+                }
+                let small_blind = self.dealer.and_then(|dealer| self.seats.player_after(dealer));
+                let big_blind = small_blind.and_then(|dealer| self.seats.player_after(dealer));
+                if Some(username) == self.dealer {
+                    self.ghosts.insert(username, 0);
+                } else if Some(username) == small_blind {
+                    self.ghosts.insert(username, 1);
+                } else if Some(username) == big_blind {
+                    self.ghosts.insert(username, 2);
+                } else {
+                    return self.seats.remove_player(username);
+                }
+                Ok(())
+            }
             (_, Action::NextToDeal) => {
+                self.remove_ghosts()?;
                 self.dealer = Some(username);
                 self.deck = FIFTY_TWO_CARD_DECK.iter().cloned().collect();
                 self.hands.clear();
                 self.community.clear();
                 self.receiver = self.seats.player_after(username);
                 self.active = None;
-                self.players = self.seats.player_set();
+                self.in_hand = self.seats.player_set();
                 self.bets.clear();
                 self.committed.clear();
                 if self.pot != 0 {
@@ -305,7 +339,7 @@ impl Game for TexasHoldEm {
                 *self.bets.entry(username).or_default() += chips;
                 *self.committed.entry(username).or_default() += chips;
                 *self.stacks.entry(username).or_default() -= chips;
-                self.active = self.seats.player_after_where(username, |username| self.players.contains(&username));
+                self.active = self.seats.player_after_where(username, |username| self.in_hand.contains(&username));
                 self.state = State::PreFlopBetting;
                 Ok(())
             }
@@ -335,7 +369,7 @@ impl Game for TexasHoldEm {
             (_, Action::Fold) | (_, Action::TimeoutFold) => {
                 self.pot += *self.bets.entry(username).or_default();
                 self.bets.remove(&username);
-                self.players.remove(&username);
+                self.in_hand.remove(&username);
                 self.hands.remove(&username);
                 if self.players_able_to_bet() == 1 {
                     self.pot += self.bets.values().sum::<u64>();
@@ -394,7 +428,7 @@ impl Game for TexasHoldEm {
                 self.pot -= chips;
                 *self.stacks.entry(username).or_default() += chips;
                 self.hands.remove(&username);
-                self.players.remove(&username);
+                self.in_hand.remove(&username);
                 Ok(())
             }
             (_, Action::WinGame) => {
@@ -482,9 +516,9 @@ impl Game for TexasHoldEm {
                 }
             }
             State::PreFlopBetting | State::PostFlopBetting | State::TurnBetting | State::RiverBetting => {
-                if self.players.len() <= 1 {
+                if self.in_hand.len() <= 1 {
                     if self.pot > 0 {
-                        if let Some(&username) = self.players.iter().next() {
+                        if let Some(&username) = self.in_hand.iter().next() {
                             DealerAction::TakeAction(
                                 ValidatedUserAction(UserAction {
                                     timestamp,
@@ -901,4 +935,153 @@ mod tests {
 
         test_game(actions, settings, seed);
     }
+
+    #[test]
+    fn four_player_game() {
+        let actions = r#"[
+            {"timestamp":1614885864159,"username":"geoff","action":{"action":"Join","seat":0,"chips":1000}},
+            {"timestamp":1614885868318,"username":"kat","action":{"action":"Join","seat":1,"chips":1000}},
+            {"timestamp":1614885911250,"username":"pete","action":{"action":"Join","seat":2,"chips":1000}},
+            {"timestamp":1614885937481,"username":"mum","action":{"action":"Join","seat":3,"chips":1000}},
+            {"timestamp":1614885937489,"username":"kat","action":{"action":"NextToDeal"}},
+            {"timestamp":1614885937495,"username":"pete","action":{"action":"ReceiveCard","card":{"rank":"Nine","suit":"Spades"}}},
+            {"timestamp":1614885937505,"username":"mum","action":{"action":"ReceiveCard","card":{"rank":"Four","suit":"Hearts"}}},
+            {"timestamp":1614885937522,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Ace","suit":"Diamonds"}}},
+            {"timestamp":1614885937538,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Ten","suit":"Diamonds"}}},
+            {"timestamp":1614885937550,"username":"pete","action":{"action":"ReceiveCard","card":{"rank":"Five","suit":"Clubs"}}},
+            {"timestamp":1614885937560,"username":"mum","action":{"action":"ReceiveCard","card":{"rank":"Eight","suit":"Clubs"}}},
+            {"timestamp":1614885937568,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Four","suit":"Spades"}}},
+            {"timestamp":1614885937576,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Five","suit":"Spades"}}},
+            {"timestamp":1614885937588,"username":"kat","action":{"action":"EndDeal"}},
+            {"timestamp":1614885937596,"username":"pete","action":{"action":"PostBlind","chips":25}},
+            {"timestamp":1614885937603,"username":"mum","action":{"action":"PostBlind","chips":50}},
+            {"timestamp":1614885957974,"username":"geoff","action":{"action":"Bet","chips":50}},
+            {"timestamp":1614885960853,"username":"kat","action":{"action":"Bet","chips":50}},
+            {"timestamp":1614885970334,"username":"pete","action":{"action":"Bet","chips":25}},
+            {"timestamp":1614885977189,"username":"mum","action":{"action":"Bet","chips":0}},
+            {"timestamp":1614885977198,"username":"kat","action":{"action":"CommunityCard","card":{"rank":"Jack","suit":"Clubs"}}},
+            {"timestamp":1614885977206,"username":"kat","action":{"action":"CommunityCard","card":{"rank":"Six","suit":"Clubs"}}},
+            {"timestamp":1614885977213,"username":"kat","action":{"action":"CommunityCard","card":{"rank":"Seven","suit":"Hearts"}}},
+            {"timestamp":1614885997797,"username":"pete","action":{"action":"Bet","chips":0}},
+            {"timestamp":1614886001294,"username":"mum","action":{"action":"Bet","chips":0}},
+            {"timestamp":1614886004614,"username":"geoff","action":{"action":"Bet","chips":0}},
+            {"timestamp":1614886006860,"username":"kat","action":{"action":"Bet","chips":0}},
+            {"timestamp":1614886006867,"username":"kat","action":{"action":"CommunityCard","card":{"rank":"Four","suit":"Diamonds"}}},
+            {"timestamp":1614886011036,"username":"pete","action":{"action":"Bet","chips":0}},
+            {"timestamp":1614886013068,"username":"mum","action":{"action":"Bet","chips":50}},
+            {"timestamp":1614886016952,"username":"geoff","action":{"action":"Bet","chips":50}},
+            {"timestamp":1614886020246,"username":"kat","action":{"action":"Fold"}},
+            {"timestamp":1614886025672,"username":"pete","action":{"action":"Bet","chips":50}},
+            {"timestamp":1614886025683,"username":"kat","action":{"action":"CommunityCard","card":{"rank":"Two","suit":"Clubs"}}},
+            {"timestamp":1614886030407,"username":"pete","action":{"action":"Bet","chips":0}},
+            {"timestamp":1614886040056,"username":"mum","action":{"action":"Bet","chips":0}},
+            {"timestamp":1614886048942,"username":"geoff","action":{"action":"Bet","chips":0}},
+            {"timestamp":1614886048953,"username":"geoff","action":{"action":"WinHand","chips":350,"hand":"Pair of 4s, AJ7 Kickers"}},
+            {"timestamp":1614886048962,"username":"pete","action":{"action":"NextToDeal"}},
+            {"timestamp":1614886048970,"username":"mum","action":{"action":"ReceiveCard","card":{"rank":"Six","suit":"Clubs"}}},
+            {"timestamp":1614886048996,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"King","suit":"Clubs"}}},
+            {"timestamp":1614886049014,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Ten","suit":"Clubs"}}},
+            {"timestamp":1614886049024,"username":"pete","action":{"action":"ReceiveCard","card":{"rank":"Ten","suit":"Hearts"}}},
+            {"timestamp":1614886049036,"username":"mum","action":{"action":"ReceiveCard","card":{"rank":"King","suit":"Diamonds"}}},
+            {"timestamp":1614886049050,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Eight","suit":"Diamonds"}}},
+            {"timestamp":1614886049060,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"King","suit":"Hearts"}}},
+            {"timestamp":1614886049067,"username":"pete","action":{"action":"ReceiveCard","card":{"rank":"Jack","suit":"Hearts"}}},
+            {"timestamp":1614886049076,"username":"pete","action":{"action":"EndDeal"}},
+            {"timestamp":1614886049087,"username":"mum","action":{"action":"PostBlind","chips":25}},
+            {"timestamp":1614886049094,"username":"geoff","action":{"action":"PostBlind","chips":50}},
+            {"timestamp":1614886057865,"username":"kat","action":{"action":"Bet","chips":150}},
+            {"timestamp":1614886061940,"username":"pete","action":{"action":"Bet","chips":150}},
+            {"timestamp":1614886065218,"username":"mum","action":{"action":"Bet","chips":125}},
+            {"timestamp":1614886067287,"username":"geoff","action":{"action":"Bet","chips":100}},
+            {"timestamp":1614886067293,"username":"pete","action":{"action":"CommunityCard","card":{"rank":"Three","suit":"Hearts"}}},
+            {"timestamp":1614886067298,"username":"pete","action":{"action":"CommunityCard","card":{"rank":"Seven","suit":"Diamonds"}}},
+            {"timestamp":1614886067305,"username":"pete","action":{"action":"CommunityCard","card":{"rank":"Ace","suit":"Clubs"}}},
+            {"timestamp":1614886072977,"username":"mum","action":{"action":"Bet","chips":0}},
+            {"timestamp":1614886075292,"username":"geoff","action":{"action":"Bet","chips":0}},
+            {"timestamp":1614886086731,"username":"kat","action":{"action":"Bet","chips":0}},
+            {"timestamp":1614886090495,"username":"pete","action":{"action":"Bet","chips":200}},
+            {"timestamp":1614886092107,"username":"mum","action":{"action":"Fold"}},
+            {"timestamp":1614886093007,"username":"geoff","action":{"action":"Fold"}},
+            {"timestamp":1614886093946,"username":"kat","action":{"action":"Fold"}},
+            {"timestamp":1614886093956,"username":"pete","action":{"action":"WinHand","chips":800,"hand":null}},
+            {"timestamp":1614886093963,"username":"mum","action":{"action":"NextToDeal"}},
+            {"timestamp":1614886093970,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"King","suit":"Hearts"}}},
+            {"timestamp":1614886093980,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Queen","suit":"Hearts"}}},
+            {"timestamp":1614886093984,"username":"pete","action":{"action":"ReceiveCard","card":{"rank":"Five","suit":"Spades"}}},
+            {"timestamp":1614886093995,"username":"mum","action":{"action":"ReceiveCard","card":{"rank":"Two","suit":"Spades"}}},
+            {"timestamp":1614886094007,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Three","suit":"Spades"}}},
+            {"timestamp":1614886094017,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Queen","suit":"Spades"}}},
+            {"timestamp":1614886094027,"username":"pete","action":{"action":"ReceiveCard","card":{"rank":"Six","suit":"Clubs"}}},
+            {"timestamp":1614886094037,"username":"mum","action":{"action":"ReceiveCard","card":{"rank":"Three","suit":"Diamonds"}}},
+            {"timestamp":1614886094048,"username":"mum","action":{"action":"EndDeal"}},
+            {"timestamp":1614886094065,"username":"geoff","action":{"action":"PostBlind","chips":25}},
+            {"timestamp":1614886094075,"username":"kat","action":{"action":"PostBlind","chips":50}},
+            {"timestamp":1614886098297,"username":"pete","action":{"action":"Fold"}},
+            {"timestamp":1614886101531,"username":"mum","action":{"action":"Fold"}},
+            {"timestamp":1614886105343,"username":"geoff","action":{"action":"Fold"}},
+            {"timestamp":1614886105350,"username":"kat","action":{"action":"WinHand","chips":75,"hand":null}},
+            {"timestamp":1614886105357,"username":"geoff","action":{"action":"NextToDeal"}},
+            {"timestamp":1614886105364,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Ace","suit":"Diamonds"}}},
+            {"timestamp":1614886105375,"username":"pete","action":{"action":"ReceiveCard","card":{"rank":"Ten","suit":"Clubs"}}},
+            {"timestamp":1614886105392,"username":"mum","action":{"action":"ReceiveCard","card":{"rank":"Four","suit":"Clubs"}}},
+            {"timestamp":1614886105407,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Two","suit":"Diamonds"}}},
+            {"timestamp":1614886105418,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Eight","suit":"Spades"}}},
+            {"timestamp":1614886105435,"username":"pete","action":{"action":"ReceiveCard","card":{"rank":"Four","suit":"Spades"}}},
+            {"timestamp":1614886105445,"username":"mum","action":{"action":"ReceiveCard","card":{"rank":"Two","suit":"Hearts"}}},
+            {"timestamp":1614886105455,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"King","suit":"Spades"}}},
+            {"timestamp":1614886105462,"username":"geoff","action":{"action":"EndDeal"}},
+            {"timestamp":1614886105470,"username":"kat","action":{"action":"PostBlind","chips":25}},
+            {"timestamp":1614886105481,"username":"pete","action":{"action":"PostBlind","chips":50}},
+            {"timestamp":1614886108692,"username":"mum","action":{"action":"Fold"}},
+            {"timestamp":1614886110046,"username":"geoff","action":{"action":"Fold"}},
+            {"timestamp":1614886115869,"username":"kat","action":{"action":"Bet","chips":75}},
+            {"timestamp":1614886118390,"username":"pete","action":{"action":"Bet","chips":50}},
+            {"timestamp":1614886118398,"username":"geoff","action":{"action":"CommunityCard","card":{"rank":"King","suit":"Clubs"}}},
+            {"timestamp":1614886118405,"username":"geoff","action":{"action":"CommunityCard","card":{"rank":"Seven","suit":"Hearts"}}},
+            {"timestamp":1614886118413,"username":"geoff","action":{"action":"CommunityCard","card":{"rank":"Ace","suit":"Hearts"}}},
+            {"timestamp":1614886124923,"username":"kat","action":{"action":"Bet","chips":250}},
+            {"timestamp":1614886126145,"username":"pete","action":{"action":"Fold"}},
+            {"timestamp":1614886126155,"username":"kat","action":{"action":"WinHand","chips":450,"hand":null}},
+            {"timestamp":1614886126162,"username":"kat","action":{"action":"NextToDeal"}},
+            {"timestamp":1614886126168,"username":"pete","action":{"action":"ReceiveCard","card":{"rank":"Jack","suit":"Clubs"}}},
+            {"timestamp":1614886126176,"username":"mum","action":{"action":"ReceiveCard","card":{"rank":"Eight","suit":"Diamonds"}}},
+            {"timestamp":1614886126186,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Queen","suit":"Diamonds"}}},
+            {"timestamp":1614886126191,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Nine","suit":"Diamonds"}}},
+            {"timestamp":1614886126199,"username":"pete","action":{"action":"ReceiveCard","card":{"rank":"Four","suit":"Hearts"}}},
+            {"timestamp":1614886126216,"username":"mum","action":{"action":"ReceiveCard","card":{"rank":"Jack","suit":"Diamonds"}}},
+            {"timestamp":1614886126226,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Five","suit":"Diamonds"}}},
+            {"timestamp":1614886126237,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Ace","suit":"Diamonds"}}},
+            {"timestamp":1614886126247,"username":"kat","action":{"action":"EndDeal"}},
+            {"timestamp":1614886126263,"username":"pete","action":{"action":"PostBlind","chips":25}},
+            {"timestamp":1614886126274,"username":"mum","action":{"action":"PostBlind","chips":50}},
+            {"timestamp":1614886136276,"username":"geoff","action":{"action":"Bet","chips":50}},
+            {"timestamp":1614886139281,"username":"kat","action":{"action":"Bet","chips":200}},
+            {"timestamp":1614886140745,"username":"pete","action":{"action":"Fold"}},
+            {"timestamp":1614886143894,"username":"mum","action":{"action":"Bet","chips":150}},
+            {"timestamp":1614886146039,"username":"geoff","action":{"action":"Bet","chips":150}},
+            {"timestamp":1614886146049,"username":"kat","action":{"action":"CommunityCard","card":{"rank":"Queen","suit":"Hearts"}}},
+            {"timestamp":1614886146054,"username":"kat","action":{"action":"CommunityCard","card":{"rank":"Five","suit":"Spades"}}},
+            {"timestamp":1614886146064,"username":"kat","action":{"action":"CommunityCard","card":{"rank":"Nine","suit":"Clubs"}}},
+            {"timestamp":1614886153343,"username":"mum","action":{"action":"Bet","chips":0}},
+            {"timestamp":1614886155753,"username":"geoff","action":{"action":"Bet","chips":0}},
+            {"timestamp":1614886159375,"username":"kat","action":{"action":"Bet","chips":300}},
+            {"timestamp":1614886164423,"username":"mum","action":{"action":"Fold"}},
+            {"timestamp":1614886169591,"username":"geoff","action":{"action":"Bet","chips":875}},
+            {"timestamp":1614886196830,"username":"kat","action":{"action":"Bet","chips":425}},
+            {"timestamp":1614886196838,"username":"kat","action":{"action":"CommunityCard","card":{"rank":"Eight","suit":"Hearts"}}},
+            {"timestamp":1614886196846,"username":"kat","action":{"action":"CommunityCard","card":{"rank":"Queen","suit":"Clubs"}}},
+            {"timestamp":1614886196854,"username":"geoff","action":{"action":"WinHand","chips":2225,"hand":"Full House, Qs full of 5s"}},
+            {"timestamp":1614886196859,"username":"kat","action":{"action":"KnockedOut"}},
+            {"timestamp":1614886196863,"username":"pete","action":{"action":"NextToDeal"}}
+        ]"#;
+        let actions = serde_json::from_str(actions).unwrap();
+
+        let settings = r#"{"format":"TexasHoldEm","title":"4-Player TexasHoldEm Test","max_players":4,"small_blind":25,"starting_stack":1000,"action_timeout":null}"#;
+        let settings = serde_json::from_str(settings).unwrap();
+
+        let seed = r#"{"rng":"ChaCha20","seed":"48e2f45eb4a1ac6bc4ab4f2368ba2d9b0d7c1f132d7fc7f51036e92112dae136"}"#;
+        let seed = serde_json::from_str(seed).unwrap();
+
+        test_game(actions, settings, seed);
+    }
 }