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;
#[derive(Copy, Clone, Debug)]
enum KnockOutWhistState {
- NotYetStarted,
+ NotStarted,
Dealing,
ChoosingTrumps,
Playing,
#[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
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
+ }
+ }
}
}