From: Geoffrey Allott Date: Thu, 25 Feb 2021 18:58:58 +0000 (+0000) Subject: add dealer actions to knock-out whist X-Git-Url: https://git.pointlesshacks.com/?a=commitdiff_plain;h=dbf63761a00f4c85dad680b3478ad928211d3f58;p=pokerwave.git add dealer actions to knock-out whist --- diff --git a/src/dealer.rs b/src/dealer.rs index 058330e..8331d57 100644 --- a/src/dealer.rs +++ b/src/dealer.rs @@ -25,13 +25,7 @@ impl Dealer { interests.insert(ClientInterest::Game{id}); server.register_interests(interests).await; let summary = server.game_summary(id).await?; - let actions = server.game_state(id, 0).await?; let mut game = Game::new(summary); - for action in actions { - if let Err(err) = game.take_action(action) { - error!("Action from database failed to apply: {}", err); - } - } let mut dealer = Dealer{server, dealer: DealerState{game}}; dealer.retrieve_updates().await?; Ok(dealer) diff --git a/src/game/action.rs b/src/game/action.rs index 3d67b95..4c8b347 100644 --- a/src/game/action.rs +++ b/src/game/action.rs @@ -63,6 +63,7 @@ pub enum ActionError { OutOfTurn, CardNotPlayable, InvalidActionForGameType, + GameHasEnded, } impl Display for ActionError { @@ -76,6 +77,7 @@ impl Display for ActionError { ActionError::OutOfTurn => f.write_str("OutOfTurn"), ActionError::CardNotPlayable => f.write_str("CardNotPlayable"), ActionError::InvalidActionForGameType => f.write_str("InvalidActionForGameType"), + ActionError::GameHasEnded => f.write_str("GameHasEnded"), } } } diff --git a/src/game/whist.rs b/src/game/whist.rs index c04ec53..3ee5cfe 100644 --- a/src/game/whist.rs +++ b/src/game/whist.rs @@ -1,6 +1,9 @@ use std::collections::{HashMap, HashSet}; -use crate::card::Card; +use rand::seq::IteratorRandom; +use rand::thread_rng; + +use crate::card::{Card, Suit, FIFTY_TWO_CARD_DECK}; use crate::seats::Seats; use crate::username::Username; @@ -8,7 +11,7 @@ use super::{Action, ActionError, Game, UserAction, ValidatedUserAction}; #[derive(Copy, Clone, Debug)] enum KnockOutWhistState { - NotYetStarted, + NotStarted, Dealing, ChoosingTrumps, Playing, @@ -19,25 +22,82 @@ enum KnockOutWhistState { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct KnockOutWhistSettings { title: String, + max_players: u32, } #[derive(Clone, Debug)] pub struct KnockOutWhist { id: u32, - actions_len: usize, settings: KnockOutWhistSettings, + actions_len: usize, state: KnockOutWhistState, seats: Seats, - dealer: Username, - call: Username, + dealer: Option, + receiver: Option, + call: Option, deck: HashSet, hands: HashMap>, + trick: HashMap, + active: Option, tricks_won: HashMap, winners: HashSet, trump_card: Option, + trumps: Option, + led: Option, cards_to_deal: u32, } +impl KnockOutWhist { + pub fn new(id: u32, settings: KnockOutWhistSettings) -> Self { + Self { + id, + settings, + actions_len: 0, + state: KnockOutWhistState::NotStarted, + seats: Seats::new(), + dealer: None, + receiver: None, + call: None, + deck: FIFTY_TWO_CARD_DECK.iter().cloned().collect(), + hands: HashMap::new(), + trick: HashMap::new(), + active: None, + tricks_won: HashMap::new(), + winners: HashSet::new(), + trump_card: None, + trumps: None, + led: None, + cards_to_deal: 7, + } + } + + fn hand_contains_card(&self, username: Username, card: Card) -> bool { + self.hands.get(&username) + .map_or(false, |hand| hand.contains(&card)) + } + + fn hand_contains_suit(&self, username: Username, suit: Suit) -> bool { + self.hands.get(&username) + .map_or(false, |hand| hand.iter().any(|card| card.suit == suit)) + } + + fn trick_winner(&self) -> Option { + let highest_trump = self.trick.iter() + .filter(|(_, card)| Some(card.suit) == self.trumps) + .max_by_key(|(_, card)| card.rank); + let highest_led = self.trick.iter() + .filter(|(_, card)| Some(card.suit) == self.led) + .max_by_key(|(_, card)| card.rank); + highest_trump.or(highest_led).map(|(username, _)| *username) + } + + fn cards_by_player(&self) -> impl Iterator + '_ { + self.hands.iter() + .map(|(username, hand)| hand.iter().map(move |card| (*username, *card))) + .flatten() + } +} + impl Game for KnockOutWhist { fn id(&self) -> u32 { self.id @@ -51,15 +111,266 @@ impl Game for KnockOutWhist { self.actions_len } - fn validate_action(&self, action: UserAction) -> Result { - todo!() + fn validate_action(&self, UserAction{username, action}: UserAction) -> Result { + match (self.state, action) { + (_, Action::AddOn{..}) | (_, Action::RevealCard{..}) | (_, Action::Fold) | (_, Action::Bet{..}) => { + Err(ActionError::InvalidActionForGameType) + } + (KnockOutWhistState::NotStarted, Action::Join{seat, ..}) => { + if self.seats.contains_player(username) { + Err(ActionError::AlreadyJoined) + } else if self.seats.players_len() > self.settings.max_players as usize { + Err(ActionError::NoSeatAvailable) + } else if !self.seats.seat_is_available(seat) { + Err(ActionError::SeatNotAvailable) + } else { + Ok(ValidatedUserAction(UserAction{username, action: Action::Join{seat, chips: 0}})) + } + } + (_, _) if !self.seats.contains_player(username) => Err(ActionError::NotAuthorised), + (KnockOutWhistState::NotStarted, Action::Leave) => { + Ok(ValidatedUserAction(UserAction{username, action: Action::Leave})) + } + (_, Action::Leave) => Err(ActionError::GameHasStarted), + (KnockOutWhistState::Dealing, _) => Err(ActionError::OutOfTurn), + (KnockOutWhistState::ChoosingTrumps, Action::ChooseTrumps{suit}) => { + if Some(username) == self.call { + Ok(ValidatedUserAction(UserAction{username, action: Action::ChooseTrumps{suit}})) + } else { + Err(ActionError::OutOfTurn) + } + } + (KnockOutWhistState::ChoosingTrumps, _) => Err(ActionError::OutOfTurn), + (KnockOutWhistState::Playing, Action::PlayCard{card}) => { + if Some(username) != self.active { + Err(ActionError::OutOfTurn) + } else if !self.hand_contains_card(username, card) { + Err(ActionError::CardNotPlayable) + } else if self.led.is_some() && Some(card.suit) != self.led && self.hand_contains_suit(username, card.suit) { + Err(ActionError::CardNotPlayable) + } else { + Ok(ValidatedUserAction(UserAction{username, action: Action::PlayCard{card}})) + } + } + (KnockOutWhistState::CutForCall, _) => Err(ActionError::OutOfTurn), + (KnockOutWhistState::Completed, _) => Err(ActionError::GameHasEnded), + (_, _) => Err(ActionError::InvalidActionForGameType), + } } - fn take_action(&mut self, action: ValidatedUserAction) -> Result<(), ActionError> { - todo!() + fn take_action(&mut self, ValidatedUserAction(UserAction{username, action}): ValidatedUserAction) -> Result<(), ActionError> { + self.actions_len += 1; + match (self.state, action) { + (_, Action::AddOn{..}) | (_, Action::RevealCard{..}) | (_, Action::Fold) | (_, Action::Bet{..}) => { + Err(ActionError::InvalidActionForGameType) + } + (KnockOutWhistState::NotStarted, Action::Join{seat, ..}) => { + self.seats.add_player(seat, username) + } + (KnockOutWhistState::NotStarted, Action::Leave) => { + self.seats.remove_player(username) + } + (KnockOutWhistState::NotStarted, Action::NextToDeal) => { + self.dealer = Some(username); + self.deck = FIFTY_TWO_CARD_DECK.iter().cloned().collect(); + self.hands.clear(); + self.receiver = self.seats.player_after(username); + self.trump_card = None; + self.trumps = None; + self.tricks_won.clear(); + self.winners.clear(); + self.state = KnockOutWhistState::Dealing; + Ok(()) + } + (KnockOutWhistState::Dealing, Action::ReceiveCard{card: Some(card)}) => { + self.deck.remove(&card); + self.hands.entry(username).or_default().insert(card); + if self.hands.values().all(|hand| hand.len() == self.cards_to_deal as usize) { + self.receiver = None; + } else { + self.receiver = self.receiver.and_then(|player| self.seats.player_after(player)); + } + Ok(()) + } + (KnockOutWhistState::Dealing, Action::CommunityCard{card}) => { + self.trump_card = Some(card); + self.trumps = Some(card.suit); + self.state = KnockOutWhistState::Playing; + Ok(()) + } + (KnockOutWhistState::Dealing, Action::EndDeal) => { + match self.trump_card { + Some(_) => { + self.state = KnockOutWhistState::Playing; + self.receiver = None; + self.active = self.dealer.and_then(|dealer| self.seats.player_after(dealer)); + } + None => self.state = KnockOutWhistState::ChoosingTrumps, + } + Ok(()) + } + (KnockOutWhistState::ChoosingTrumps, Action::ChooseTrumps{suit}) => { + self.trumps = Some(suit); + self.call = None; + self.state = KnockOutWhistState::Playing; + self.active = self.dealer.and_then(|dealer| self.seats.player_after(dealer)); + Ok(()) + } + (KnockOutWhistState::Playing, Action::PlayCard{card}) => { + if let Some(hand) = self.hands.get_mut(&username) { + hand.remove(&card); + } + if self.trick.is_empty() { + self.led = Some(card.suit); + } + self.trick.insert(username, card); + if self.trick.len() == self.seats.players_len() { + self.active = None; + } else { + self.active = self.active.and_then(|player| self.seats.player_after(player)); + } + Ok(()) + } + (KnockOutWhistState::Playing, Action::WinTrick) => { + *self.tricks_won.entry(username).or_default() += 1; + self.led = None; + self.trumps = None; + self.trick.clear(); + if self.tricks_won.values().sum::() == self.cards_to_deal { + if let Some(&most_tricks_won) = self.tricks_won.values().max() { + self.winners = self.tricks_won.iter().filter(|&(_, &tricks)| tricks == most_tricks_won).map(|(&username, _)| username).collect(); + self.cards_to_deal -= 1; + if self.winners.len() == 1 { + self.call = self.winners.drain().next(); + } else { + self.receiver = self.dealer; + while let Some(receiver) = self.receiver { + if !self.winners.contains(&receiver) { + self.receiver = self.seats.player_after(receiver); + } else { + break; + } + } + self.deck = FIFTY_TWO_CARD_DECK.iter().cloned().collect(); + self.hands.clear(); + self.trump_card = None; + self.state = KnockOutWhistState::CutForCall; + } + } + self.active = None; + } else { + self.active = Some(username); + } + Ok(()) + } + (KnockOutWhistState::Playing, Action::KnockedOut) => { + self.seats.remove_player(username) + } + (KnockOutWhistState::Playing, Action::WinGame) => { + self.winners.clear(); + self.winners.insert(username); + self.state = KnockOutWhistState::Completed; + Ok(()) + } + (KnockOutWhistState::CutForCall, Action::RevealCard{card}) => { + self.deck.remove(&card); + if let Some(hand) = self.hands.get_mut(&username) { + hand.insert(card); + } + if self.hands.values().map(HashSet::len).sum::() == self.winners.len() { + if let Some((username, max)) = self.cards_by_player().max_by_key(|(_, card)| card.rank) { + if self.cards_by_player().filter(|(_, card)| card.rank == max.rank).count() > 1 { + self.winners = self.cards_by_player().filter(|(_, card)| card.rank == max.rank).map(|(username, _)| username).collect(); + self.hands.clear(); + self.receiver = self.dealer; + } else { + self.call = Some(username); + self.receiver = None; + } + } + } + while let Some(receiver) = self.receiver { + if !self.winners.contains(&receiver) { + self.receiver = self.seats.player_after(receiver); + } else { + break; + } + } + Ok(()) + } + (KnockOutWhistState::Completed, _) => Err(ActionError::GameHasEnded), + (_, _) => Err(ActionError::InvalidActionForGameType), + } } fn next_dealer_action(&self) -> Option { - todo!() + let mut rng = thread_rng(); + match self.state { + KnockOutWhistState::NotStarted => { + if self.seats.players_len() == self.settings.max_players as usize { // TODO + if let Some(username) = self.seats.player_set().into_iter().choose(&mut rng) { + return Some(ValidatedUserAction(UserAction{username, action: Action::NextToDeal})); + } + } + None + } + KnockOutWhistState::Dealing => { + if let Some(username) = self.receiver { + let card = self.deck.iter().choose(&mut rng).cloned(); + Some(ValidatedUserAction(UserAction{username, action: Action::ReceiveCard{card}})) + } else if let Some(username) = self.dealer { + match self.call { + None => { + if let Some(&card) = self.deck.iter().choose(&mut rng) { + Some(ValidatedUserAction(UserAction{username, action: Action::CommunityCard{card}})) + } else { + None + } + } + Some(_) => Some(ValidatedUserAction(UserAction{username, action: Action::EndDeal})), + } + } else { + None + } + } + KnockOutWhistState::ChoosingTrumps => { + None + } + KnockOutWhistState::Playing => { + if !self.winners.is_empty() { + for username in self.seats.player_set() { + if matches!(self.tricks_won.get(&username), Some(0) | None) { + return Some(ValidatedUserAction(UserAction{username, action: Action::KnockedOut})); + } + } + if self.seats.players_len() == 1 { + if let Some(&username) = self.winners.iter().next() { + return Some(ValidatedUserAction(UserAction{username, action: Action::WinGame})); + } + } + if let Some(username) = self.dealer.and_then(|dealer| self.seats.player_after(dealer)) { + return Some(ValidatedUserAction(UserAction{username, action: Action::NextToDeal})); + } + None + } else if let Some(username) = self.trick_winner() { + Some(ValidatedUserAction(UserAction{username, action: Action::WinTrick})) + } else { + None + } + } + KnockOutWhistState::CutForCall => { + if let Some(username) = self.receiver { + let card = self.deck.iter().choose(&mut rng).cloned(); + Some(ValidatedUserAction(UserAction{username, action: Action::ReceiveCard{card}})) + } else if let Some(username) = self.dealer.and_then(|dealer| self.seats.player_after(dealer)) { + Some(ValidatedUserAction(UserAction{username, action: Action::NextToDeal})) + } else { + None + } + } + KnockOutWhistState::Completed => { + None + } + } } } diff --git a/src/seats.rs b/src/seats.rs index e654cf9..420744f 100644 --- a/src/seats.rs +++ b/src/seats.rs @@ -1,5 +1,6 @@ use std::collections::{BTreeMap, HashSet}; +use crate::game::ActionError; use crate::username::Username; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -14,13 +15,21 @@ impl Seats { } } - pub fn add_player(&mut self, seat: u32, username: Username) { - self.players.insert(seat, username); + pub fn add_player(&mut self, seat: u32, username: Username) -> Result<(), ActionError> { + match self.players.insert(seat, username) { + Some(_) => Err(ActionError::SeatNotAvailable), + None => Ok(()) + } } - pub fn remove_player(&mut self, username: Username) { + pub fn remove_player(&mut self, username: Username) -> Result<(), ActionError> { if let Some(seat) = self.players.iter().find(|(_, &player)| player == username).map(|(&seat, _)| seat) { - self.players.remove(&seat); + match self.players.remove(&seat) { + Some(_) => Ok(()), + None => Err(ActionError::NotAuthorised), + } + } else { + Err(ActionError::NotAuthorised) } }