add dealer actions to knock-out whist
authorGeoffrey Allott <geoffrey@allott.email>
Thu, 25 Feb 2021 18:58:58 +0000 (18:58 +0000)
committerGeoffrey Allott <geoffrey@allott.email>
Thu, 25 Feb 2021 18:58:58 +0000 (18:58 +0000)
src/dealer.rs
src/game/action.rs
src/game/whist.rs
src/seats.rs

index 058330e19d18449b57385679c0adf1570952b867..8331d57dfd52a02695f4cbfd635a7dc0b1d8cf6d 100644 (file)
@@ -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)
index 3d67b958b747dfb7f38094c2e108b1624ec8981c..4c8b3472a731c20b5762796001abc2096863c32f 100644 (file)
@@ -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"),
         }
     }
 }
index c04ec530ac5c5647c303924afda2c41ad8a2aafa..3ee5cfe4e1c41ad93eb5799948263082ee013dc6 100644 (file)
@@ -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<Username>,
+    receiver: Option<Username>,
+    call: Option<Username>,
     deck: HashSet<Card>,
     hands: HashMap<Username, HashSet<Card>>,
+    trick: HashMap<Username, Card>,
+    active: Option<Username>,
     tricks_won: HashMap<Username, u32>,
     winners: HashSet<Username>,
     trump_card: Option<Card>,
+    trumps: Option<Suit>,
+    led: Option<Suit>,
     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<Username> {
+        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<Item=(Username, Card)> + '_ {
+        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<ValidatedUserAction, ActionError> {
-        todo!()
+    fn validate_action(&self, UserAction{username, action}: UserAction) -> Result<ValidatedUserAction, ActionError> {
+        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::<u32>() == 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::<usize>() == 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<ValidatedUserAction> {
-        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
+            }
+        }
     }
 }
index e654cf91612faf065defcf603e001e8729aeb040..420744fa69718ebc1895c30930b1be5a331f9e4c 100644 (file)
@@ -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)
         }
     }