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 {
active: None,
bets: HashMap::new(),
committed: HashMap::new(),
- players: HashSet::new(),
+ in_hand: HashSet::new(),
pot: 0,
small_blind,
big_blind,
+ ghosts: HashMap::new(),
}
}
}
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> + '_ {
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 {
(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})),
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 {
*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(())
}
(_, 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>();
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) => {
}
}
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,
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);
+ }
}