From c3fa8b026f79ad90ba2e5eff347f497f14bea2cc Mon Sep 17 00:00:00 2001 From: Geoffrey Allott Date: Fri, 5 Mar 2021 17:33:18 +0000 Subject: [PATCH] initial attempt at ghost dealers/blinds --- src/game/poker/holdem.rs | 211 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 197 insertions(+), 14 deletions(-) diff --git a/src/game/poker/holdem.rs b/src/game/poker/holdem.rs index 9814972..104c8b7 100644 --- a/src/game/poker/holdem.rs +++ b/src/game/poker/holdem.rs @@ -57,10 +57,11 @@ pub struct TexasHoldEm { active: Option, bets: HashMap, committed: HashMap, - players: HashSet, + in_hand: HashSet, pot: u64, small_blind: u64, big_blind: u64, + ghosts: HashMap, } 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 + '_ { @@ -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::(); @@ -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); + } } -- 2.34.1