initial implementation of texas hold 'em
authorGeoffrey Allott <geoffrey@allott.email>
Sat, 27 Feb 2021 22:08:51 +0000 (22:08 +0000)
committerGeoffrey Allott <geoffrey@allott.email>
Sat, 27 Feb 2021 22:08:51 +0000 (22:08 +0000)
src/game/action.rs
src/game/mod.rs
src/game/poker.rs [deleted file]
src/game/poker/classify.rs [new file with mode: 0644]
src/game/poker/holdem.rs [new file with mode: 0644]
src/game/poker/mod.rs [new file with mode: 0644]
src/game/whist.rs

index ed5af55a86e02d11499f7213ac969336819ed083..a5d66ae50a0199b2b287739801501a0a1a84a1ba 100644 (file)
@@ -54,7 +54,7 @@ impl Action {
     }
 }
 
-#[derive(Debug, Clone, Serialize)]
+#[derive(Debug, Copy, Clone, Serialize)]
 pub enum ActionError {
     NotAuthorised,
     AlreadyJoined,
@@ -66,6 +66,13 @@ pub enum ActionError {
     MustChooseTrumps,
     CardNotPlayable,
     CardNotAvailable,
+    StartingStackTooSmall,
+    StartingStackTooLarge,
+    CannotFold,
+    NotEnoughChips,
+    BetSizeTooSmall,
+    BetSizeTooLarge,
+    NotInHand,
     InvalidActionForGameType,
     GameHasEnded,
 }
@@ -83,6 +90,13 @@ impl Display for ActionError {
             ActionError::MustChooseTrumps => f.write_str("Trumps must be chosen first"),
             ActionError::CardNotPlayable => f.write_str("Card is not legally playable"),
             ActionError::CardNotAvailable => f.write_str("Card does not exist in player's hand"),
+            ActionError::StartingStackTooSmall => f.write_str("Starting stack provided is too small"),
+            ActionError::StartingStackTooLarge => f.write_str("Starting stack provided is too large"),
+            ActionError::CannotFold => f.write_str("Cannot fold when able to check"),
+            ActionError::NotEnoughChips => f.write_str("Not enough chips to make bet"),
+            ActionError::BetSizeTooSmall => f.write_str("Bet size is too small"),
+            ActionError::BetSizeTooLarge => f.write_str("Bet size is too large"),
+            ActionError::NotInHand => f.write_str("Not in hand"),
             ActionError::InvalidActionForGameType => f.write_str("Invalid action for game type"),
             ActionError::GameHasEnded => f.write_str("Game has ended"),
         }
index b0fd05f890da46a299220859d5bf9ae925d7c2e7..c8e01f73c91f75eb956ca0c5a06e89b30afb7b2a 100644 (file)
@@ -1,5 +1,6 @@
 mod action;
 mod chatroom;
+mod poker;
 mod whist;
 
 use std::collections::HashSet;
diff --git a/src/game/poker.rs b/src/game/poker.rs
deleted file mode 100644 (file)
index 52a90cf..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-pub enum TexasHoldEmState {
-    NotStarted,
-    Dealing,
-    PostingSmallBlind,
-    PostingBigBlind,
-    PreFlopBetting,
-    DealingFlop,
-    PostFlopBetting,
-    DealingTurn,
-    TurnBetting,
-    DealingRiver,
-    RiverBetting,
-    Completed,
-}
-
-pub struct TexasHoldEmSettings {
-    title: String,
-    max_players: u32,
-    blinds: u64,
-    starting_stack: u64,
-}
-
-pub struct TexasHoldEm {
-    id: i64,
-    settings: TexasHoldEmSettings,
-    actions_len: usize,
-    state: TexasHoldEmState,
-    seats: Seats,
-    stacks: HashMap<Username, u64>,
-    hands: HashMap<Username, HashSet<Card>>,
-    deck: HashSet<Card>,
-    dealer: Username,
-    action: Username,
-    bets: HashMap<Username, u64>,
-    players: HashSet<Username>,
-    pot: u64,
-    community: HashSet<Card>,
-}
diff --git a/src/game/poker/classify.rs b/src/game/poker/classify.rs
new file mode 100644 (file)
index 0000000..cad9402
--- /dev/null
@@ -0,0 +1,759 @@
+use std::cmp::Ordering;
+use std::fmt::{self, Display, Formatter};
+
+use crate::card::*;
+use crate::card::Rank::*;
+use crate::card::Suit::*;
+
+use self::Hand::*;
+
+#[derive(Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Debug)]
+pub enum Hand {
+    HighCard{kickers: (Rank, Rank, Rank, Rank, Rank)},
+    Pair{pair: Rank, kickers: (Rank, Rank, Rank)},
+    TwoPair{pairs: (Rank, Rank), kicker: Rank},
+    ThreeOfAKind{trips: Rank, kickers: (Rank, Rank)},
+    Straight{high_card: Rank},
+    Flush{flush: (Rank, Rank, Rank, Rank, Rank)},
+    FullHouse{trips: Rank, pair: Rank},
+    FourOfAKind{quads: Rank, kicker: Rank},
+    StraightFlush{high_card: Rank},
+    RoyalFlush,
+}
+
+impl Display for Hand {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        match *self {
+            HighCard{kickers: (kicker1, kicker2, kicker3, kicker4, kicker5)} => write!(f, "High Card {}, {}{}{}{} Kickers", kicker1, kicker2, kicker3, kicker4, kicker5),
+            Pair{pair, kickers: (kicker1, kicker2, kicker3)} => write!(f, "Pair of {}s, {}{}{} Kickers", pair, kicker1, kicker2, kicker3),
+            TwoPair{pairs: (pair1, pair2), kicker} => write!(f, "Two Pair, {}s & {}s, {} Kicker", pair1, pair2, kicker),
+            ThreeOfAKind{trips, kickers: (kicker1, kicker2)} => write!(f, "Three of a Kind, {}s, {}{} Kickers", trips, kicker1, kicker2),
+            Straight{high_card} => write!(f, "Straight, {} High", high_card),
+            Flush{flush: (rank1, rank2, rank3, rank4, rank5)} => write!(f, "Flush, {}{}{}{}{}", rank1, rank2, rank3, rank4, rank5),
+            FullHouse{trips, pair} => write!(f, "Full House, {}s full of {}s", trips, pair),
+            FourOfAKind{quads, kicker} => write!(f, "Four of a Kind, {}s, {} Kicker", quads, kicker),
+            StraightFlush{high_card} => write!(f, "Straight Flush, {} High", high_card),
+            RoyalFlush => write!(f, "Royal Flush"),
+        }
+    }
+}
+
+pub fn rank_7_card_hand(mut cards: [Card; 7]) -> Hand {
+    cards.sort_by(|a, b| b.rank.cmp(&a.rank));
+    let ranks = [cards[0].rank, cards[1].rank, cards[2].rank, cards[3].rank, cards[4].rank, cards[5].rank, cards[6].rank];
+    cards.sort_by(|a, b| b.suit.cmp(&a.suit));
+    let suits = [cards[0].suit, cards[1].suit, cards[2].suit, cards[3].suit, cards[4].suit, cards[5].suit, cards[6].suit];
+
+    match suits {
+        [Spades, Spades, Spades, Spades, Spades, Spades, Spades] |
+        [Hearts, Hearts, Hearts, Hearts, Hearts, Hearts, Hearts] |
+        [Diamonds, Diamonds, Diamonds, Diamonds, Diamonds, Diamonds, Diamonds] |
+        [Clubs, Clubs, Clubs, Clubs, Clubs, Clubs, Clubs] =>
+            match ranks {
+                [Ace, King, Queen, Jack, Ten, _, _] => RoyalFlush,
+                [King, Queen, Jack, Ten, Nine, _, _] => StraightFlush{high_card: King},
+                [Queen, Jack, Ten, Nine, Eight, _, _] => StraightFlush{high_card: Queen},
+                [_, Queen, Jack, Ten, Nine, Eight, _] => StraightFlush{high_card: Queen},
+                [Jack, Ten, Nine, Eight, Seven, _, _] => StraightFlush{high_card: Jack},
+                [_, Jack, Ten, Nine, Eight, Seven, _] => StraightFlush{high_card: Jack},
+                [_, _, Jack, Ten, Nine, Eight, Seven] => StraightFlush{high_card: Jack},
+                [Ten, Nine, Eight, Seven, Six, _, _] => StraightFlush{high_card: Ten},
+                [_, Ten, Nine, Eight, Seven, Six, _] => StraightFlush{high_card: Ten},
+                [_, _, Ten, Nine, Eight, Seven, Six] => StraightFlush{high_card: Ten},
+                [Nine, Eight, Seven, Six, Five, _, _] => StraightFlush{high_card: Nine},
+                [_, Nine, Eight, Seven, Six, Five, _] => StraightFlush{high_card: Nine},
+                [_, _, Nine, Eight, Seven, Six, Five] => StraightFlush{high_card: Nine},
+                [Eight, Seven, Six, Five, Four, _, _] => StraightFlush{high_card: Eight},
+                [_, Eight, Seven, Six, Five, Four, _] => StraightFlush{high_card: Eight},
+                [_, _, Eight, Seven, Six, Five, Four] => StraightFlush{high_card: Eight},
+                [Seven, Six, Five, Four, Three, _, _] => StraightFlush{high_card: Seven},
+                [_, Seven, Six, Five, Four, Three, _] => StraightFlush{high_card: Seven},
+                [_, _, Seven, Six, Five, Four, Three] => StraightFlush{high_card: Seven},
+                [Six, Five, Four, Three, Two, _, _] => StraightFlush{high_card: Six},
+                [_, Six, Five, Four, Three, Two, _] => StraightFlush{high_card: Six},
+                [_, _, Six, Five, Four, Three, Two] => StraightFlush{high_card: Six},
+                [Ace, _, _, Five, Four, Three, Two] => StraightFlush{high_card: Five},
+                [rank1, rank2, rank3, rank4, rank5, _, _] => Flush{flush: (rank1, rank2, rank3, rank4, rank5)},
+            }
+        [Spades, Spades, Spades, Spades, Spades, Spades, _] |
+        [Hearts, Hearts, Hearts, Hearts, Hearts, Hearts, _] |
+        [Diamonds, Diamonds, Diamonds, Diamonds, Diamonds, Diamonds, _] =>
+            match [cards[0].rank, cards[1].rank, cards[2].rank, cards[3].rank, cards[4].rank, cards[5].rank] {
+                [Ace, King, Queen, Jack, Ten, _] => RoyalFlush,
+                [King, Queen, Jack, Ten, Nine, _] => StraightFlush{high_card: King},
+                [Queen, Jack, Ten, Nine, Eight, _] => StraightFlush{high_card: Queen},
+                [_, Queen, Jack, Ten, Nine, Eight] => StraightFlush{high_card: Queen},
+                [Jack, Ten, Nine, Eight, Seven, _] => StraightFlush{high_card: Jack},
+                [_, Jack, Ten, Nine, Eight, Seven] => StraightFlush{high_card: Jack},
+                [Ten, Nine, Eight, Seven, Six, _] => StraightFlush{high_card: Ten},
+                [_, Ten, Nine, Eight, Seven, Six] => StraightFlush{high_card: Ten},
+                [Nine, Eight, Seven, Six, Five, _] => StraightFlush{high_card: Nine},
+                [_, Nine, Eight, Seven, Six, Five] => StraightFlush{high_card: Nine},
+                [Eight, Seven, Six, Five, Four, _] => StraightFlush{high_card: Eight},
+                [_, Eight, Seven, Six, Five, Four] => StraightFlush{high_card: Eight},
+                [Seven, Six, Five, Four, Three, _] => StraightFlush{high_card: Seven},
+                [_, Seven, Six, Five, Four, Three] => StraightFlush{high_card: Seven},
+                [Six, Five, Four, Three, Two, _] => StraightFlush{high_card: Six},
+                [_, Six, Five, Four, Three, Two] => StraightFlush{high_card: Six},
+                [Ace, _, Five, Four, Three, Two] => StraightFlush{high_card: Five},
+                [rank1, rank2, rank3, rank4, rank5, _] => Flush{flush: (rank1, rank2, rank3, rank4, rank5)},
+            }
+        [_, Hearts, Hearts, Hearts, Hearts, Hearts, Hearts] |
+        [_, Diamonds, Diamonds, Diamonds, Diamonds, Diamonds, Diamonds] |
+        [_, Clubs, Clubs, Clubs, Clubs, Clubs, Clubs] =>
+            match [cards[1].rank, cards[2].rank, cards[3].rank, cards[4].rank, cards[5].rank, cards[6].rank] {
+                [Ace, King, Queen, Jack, Ten, _] => RoyalFlush,
+                [King, Queen, Jack, Ten, Nine, _] => StraightFlush{high_card: King},
+                [Queen, Jack, Ten, Nine, Eight, _] => StraightFlush{high_card: Queen},
+                [_, Queen, Jack, Ten, Nine, Eight] => StraightFlush{high_card: Queen},
+                [Jack, Ten, Nine, Eight, Seven, _] => StraightFlush{high_card: Jack},
+                [_, Jack, Ten, Nine, Eight, Seven] => StraightFlush{high_card: Jack},
+                [Ten, Nine, Eight, Seven, Six, _] => StraightFlush{high_card: Ten},
+                [_, Ten, Nine, Eight, Seven, Six] => StraightFlush{high_card: Ten},
+                [Nine, Eight, Seven, Six, Five, _] => StraightFlush{high_card: Nine},
+                [_, Nine, Eight, Seven, Six, Five] => StraightFlush{high_card: Nine},
+                [Eight, Seven, Six, Five, Four, _] => StraightFlush{high_card: Eight},
+                [_, Eight, Seven, Six, Five, Four] => StraightFlush{high_card: Eight},
+                [Seven, Six, Five, Four, Three, _] => StraightFlush{high_card: Seven},
+                [_, Seven, Six, Five, Four, Three] => StraightFlush{high_card: Seven},
+                [Six, Five, Four, Three, Two, _] => StraightFlush{high_card: Six},
+                [_, Six, Five, Four, Three, Two] => StraightFlush{high_card: Six},
+                [Ace, _, Five, Four, Three, Two] => StraightFlush{high_card: Five},
+                [rank1, rank2, rank3, rank4, rank5, _] => Flush{flush: (rank1, rank2, rank3, rank4, rank5)},
+            }
+        [Spades, Spades, Spades, Spades, Spades, _, _] |
+        [Hearts, Hearts, Hearts, Hearts, Hearts, _, _] |
+        [Diamonds, Diamonds, Diamonds, Diamonds, Diamonds, _, _] =>
+            match [cards[0].rank, cards[1].rank, cards[2].rank, cards[3].rank, cards[4].rank] {
+                [Ace, King, Queen, Jack, Ten] => RoyalFlush,
+                [King, Queen, Jack, Ten, Nine] => StraightFlush{high_card: King},
+                [Queen, Jack, Ten, Nine, Eight] => StraightFlush{high_card: Queen},
+                [Jack, Ten, Nine, Eight, Seven] => StraightFlush{high_card: Jack},
+                [Ten, Nine, Eight, Seven, Six] => StraightFlush{high_card: Ten},
+                [Nine, Eight, Seven, Six, Five] => StraightFlush{high_card: Nine},
+                [Eight, Seven, Six, Five, Four] => StraightFlush{high_card: Eight},
+                [Seven, Six, Five, Four, Three] => StraightFlush{high_card: Seven},
+                [Six, Five, Four, Three, Two] => StraightFlush{high_card: Six},
+                [Ace, Five, Four, Three, Two] => StraightFlush{high_card: Five},
+                [rank1, rank2, rank3, rank4, rank5] => Flush{flush: (rank1, rank2, rank3, rank4, rank5)},
+            }
+        [_, Hearts, Hearts, Hearts, Hearts, Hearts, _] |
+        [_, Diamonds, Diamonds, Diamonds, Diamonds, Diamonds, _] =>
+            match [cards[1].rank, cards[2].rank, cards[3].rank, cards[4].rank, cards[5].rank] {
+                [Ace, King, Queen, Jack, Ten] => RoyalFlush,
+                [King, Queen, Jack, Ten, Nine] => StraightFlush{high_card: King},
+                [Queen, Jack, Ten, Nine, Eight] => StraightFlush{high_card: Queen},
+                [Jack, Ten, Nine, Eight, Seven] => StraightFlush{high_card: Jack},
+                [Ten, Nine, Eight, Seven, Six] => StraightFlush{high_card: Ten},
+                [Nine, Eight, Seven, Six, Five] => StraightFlush{high_card: Nine},
+                [Eight, Seven, Six, Five, Four] => StraightFlush{high_card: Eight},
+                [Seven, Six, Five, Four, Three] => StraightFlush{high_card: Seven},
+                [Six, Five, Four, Three, Two] => StraightFlush{high_card: Six},
+                [Ace, Five, Four, Three, Two] => StraightFlush{high_card: Five},
+                [rank1, rank2, rank3, rank4, rank5] => Flush{flush: (rank1, rank2, rank3, rank4, rank5)},
+            }
+        [_, _, Hearts, Hearts, Hearts, Hearts, Hearts] |
+        [_, _, Diamonds, Diamonds, Diamonds, Diamonds, Diamonds] |
+        [_, _, Clubs, Clubs, Clubs, Clubs, Clubs] =>
+            match [cards[2].rank, cards[3].rank, cards[4].rank, cards[5].rank, cards[6].rank] {
+                [Ace, King, Queen, Jack, Ten] => RoyalFlush,
+                [King, Queen, Jack, Ten, Nine] => StraightFlush{high_card: King},
+                [Queen, Jack, Ten, Nine, Eight] => StraightFlush{high_card: Queen},
+                [Jack, Ten, Nine, Eight, Seven] => StraightFlush{high_card: Jack},
+                [Ten, Nine, Eight, Seven, Six] => StraightFlush{high_card: Ten},
+                [Nine, Eight, Seven, Six, Five] => StraightFlush{high_card: Nine},
+                [Eight, Seven, Six, Five, Four] => StraightFlush{high_card: Eight},
+                [Seven, Six, Five, Four, Three] => StraightFlush{high_card: Seven},
+                [Six, Five, Four, Three, Two] => StraightFlush{high_card: Six},
+                [Ace, Five, Four, Three, Two] => StraightFlush{high_card: Five},
+                [rank1, rank2, rank3, rank4, rank5] => Flush{flush: (rank1, rank2, rank3, rank4, rank5)},
+            }
+        [_, _, _, _, _, _, _] =>
+            match ranks {
+                [quad1, quad2, quad3, quad4, kicker, _, _] if quad1 == quad2 && quad2 == quad3 && quad3 == quad4 => FourOfAKind{quads: quad1, kicker: kicker},
+                [kicker, quad1, quad2, quad3, quad4, _, _] if quad1 == quad2 && quad2 == quad3 && quad3 == quad4 => FourOfAKind{quads: quad1, kicker: kicker},
+                [kicker, _, quad1, quad2, quad3, quad4, _] if quad1 == quad2 && quad2 == quad3 && quad3 == quad4 => FourOfAKind{quads: quad1, kicker: kicker},
+                [kicker, _, _, quad1, quad2, quad3, quad4] if quad1 == quad2 && quad2 == quad3 && quad3 == quad4 => FourOfAKind{quads: quad1, kicker: kicker},
+
+                [trip1, trip2, trip3, pair1, pair2, _, _] if trip1 == trip2 && trip2 == trip3 && pair1 == pair2 => FullHouse{trips: trip1, pair: pair1},
+                [trip1, trip2, trip3, _, pair1, pair2, _] if trip1 == trip2 && trip2 == trip3 && pair1 == pair2 => FullHouse{trips: trip1, pair: pair1},
+                [trip1, trip2, trip3, _, _, pair1, pair2] if trip1 == trip2 && trip2 == trip3 && pair1 == pair2 => FullHouse{trips: trip1, pair: pair1},
+                [_, trip1, trip2, trip3, pair1, pair2, _] if trip1 == trip2 && trip2 == trip3 && pair1 == pair2 => FullHouse{trips: trip1, pair: pair1},
+                [_, trip1, trip2, trip3, _, pair1, pair2] if trip1 == trip2 && trip2 == trip3 && pair1 == pair2 => FullHouse{trips: trip1, pair: pair1},
+                [pair1, pair2, trip1, trip2, trip3, _, _] if trip1 == trip2 && trip2 == trip3 && pair1 == pair2 => FullHouse{trips: trip1, pair: pair1},
+                [pair1, pair2, _, trip1, trip2, trip3, _] if trip1 == trip2 && trip2 == trip3 && pair1 == pair2 => FullHouse{trips: trip1, pair: pair1},
+                [pair1, pair2, _, _, trip1, trip2, trip3] if trip1 == trip2 && trip2 == trip3 && pair1 == pair2 => FullHouse{trips: trip1, pair: pair1},
+                [_, _, trip1, trip2, trip3, pair1, pair2] if trip1 == trip2 && trip2 == trip3 && pair1 == pair2 => FullHouse{trips: trip1, pair: pair1},
+                [_, pair1, pair2, trip1, trip2, trip3, _] if trip1 == trip2 && trip2 == trip3 && pair1 == pair2 => FullHouse{trips: trip1, pair: pair1},
+                [_, pair1, pair2, _, trip1, trip2, trip3] if trip1 == trip2 && trip2 == trip3 && pair1 == pair2 => FullHouse{trips: trip1, pair: pair1},
+                [_, _, pair1, pair2, trip1, trip2, trip3] if trip1 == trip2 && trip2 == trip3 && pair1 == pair2 => FullHouse{trips: trip1, pair: pair1},
+
+                [Ace, King, Queen, Jack, Ten, _, _] => Straight{high_card: Ace},
+                [Ace, King, Queen, Jack, _, Ten, _] => Straight{high_card: Ace},
+                [Ace, King, Queen, Jack, _, _, Ten] => Straight{high_card: Ace},
+                [Ace, King, Queen, _, Jack, Ten, _] => Straight{high_card: Ace},
+                [Ace, King, Queen, _, Jack, _, Ten] => Straight{high_card: Ace},
+                [Ace, King, Queen, _, _, Jack, Ten] => Straight{high_card: Ace},
+                [Ace, King, _, Queen, Jack, Ten, _] => Straight{high_card: Ace},
+                [Ace, King, _, Queen, Jack, _, Ten] => Straight{high_card: Ace},
+                [Ace, King, _, Queen, _, Jack, Ten] => Straight{high_card: Ace},
+                [Ace, King, _, _, Queen, Jack, Ten] => Straight{high_card: Ace},
+                [Ace, _, King, Queen, Jack, Ten, _] => Straight{high_card: Ace},
+                [Ace, _, King, Queen, Jack, _, Ten] => Straight{high_card: Ace},
+                [Ace, _, King, Queen, _, Jack, Ten] => Straight{high_card: Ace},
+                [Ace, _, King, _, Queen, Jack, Ten] => Straight{high_card: Ace},
+                [Ace, _, _, King, Queen, Jack, Ten] => Straight{high_card: Ace},
+                [_, Ace, King, Queen, Jack, Ten, _] => Straight{high_card: Ace},
+                [_, Ace, King, Queen, Jack, _, Ten] => Straight{high_card: Ace},
+                [_, Ace, King, Queen, _, Jack, Ten] => Straight{high_card: Ace},
+                [_, Ace, King, _, Queen, Jack, Ten] => Straight{high_card: Ace},
+                [_, Ace, _, King, Queen, Jack, Ten] => Straight{high_card: Ace},
+                [_, _, Ace, King, Queen, Jack, Ten] => Straight{high_card: Ace},
+
+                [King, Queen, Jack, Ten, Nine, _, _] => Straight{high_card: King},
+                [King, Queen, Jack, Ten, _, Nine, _] => Straight{high_card: King},
+                [King, Queen, Jack, Ten, _, _, Nine] => Straight{high_card: King},
+                [King, Queen, Jack, _, Ten, Nine, _] => Straight{high_card: King},
+                [King, Queen, Jack, _, Ten, _, Nine] => Straight{high_card: King},
+                [King, Queen, Jack, _, _, Ten, Nine] => Straight{high_card: King},
+                [King, Queen, _, Jack, Ten, Nine, _] => Straight{high_card: King},
+                [King, Queen, _, Jack, Ten, _, Nine] => Straight{high_card: King},
+                [King, Queen, _, Jack, _, Ten, Nine] => Straight{high_card: King},
+                [King, Queen, _, _, Jack, Ten, Nine] => Straight{high_card: King},
+                [King, _, Queen, Jack, Ten, Nine, _] => Straight{high_card: King},
+                [King, _, Queen, Jack, Ten, _, Nine] => Straight{high_card: King},
+                [King, _, Queen, Jack, _, Ten, Nine] => Straight{high_card: King},
+                [King, _, Queen, _, Jack, Ten, Nine] => Straight{high_card: King},
+                [King, _, _, Queen, Jack, Ten, Nine] => Straight{high_card: King},
+                [_, King, Queen, Jack, Ten, Nine, _] => Straight{high_card: King},
+                [_, King, Queen, Jack, Ten, _, Nine] => Straight{high_card: King},
+                [_, King, Queen, Jack, _, Ten, Nine] => Straight{high_card: King},
+                [_, King, Queen, _, Jack, Ten, Nine] => Straight{high_card: King},
+                [_, King, _, Queen, Jack, Ten, Nine] => Straight{high_card: King},
+                [_, _, King, Queen, Jack, Ten, Nine] => Straight{high_card: King},
+
+                [Queen, Jack, Ten, Nine, Eight, _, _] => Straight{high_card: Queen},
+                [Queen, Jack, Ten, Nine, _, Eight, _] => Straight{high_card: Queen},
+                [Queen, Jack, Ten, Nine, _, _, Eight] => Straight{high_card: Queen},
+                [Queen, Jack, Ten, _, Nine, Eight, _] => Straight{high_card: Queen},
+                [Queen, Jack, Ten, _, Nine, _, Eight] => Straight{high_card: Queen},
+                [Queen, Jack, Ten, _, _, Nine, Eight] => Straight{high_card: Queen},
+                [Queen, Jack, _, Ten, Nine, Eight, _] => Straight{high_card: Queen},
+                [Queen, Jack, _, Ten, Nine, _, Eight] => Straight{high_card: Queen},
+                [Queen, Jack, _, Ten, _, Nine, Eight] => Straight{high_card: Queen},
+                [Queen, Jack, _, _, Ten, Nine, Eight] => Straight{high_card: Queen},
+                [Queen, _, Jack, Ten, Nine, Eight, _] => Straight{high_card: Queen},
+                [Queen, _, Jack, Ten, Nine, _, Eight] => Straight{high_card: Queen},
+                [Queen, _, Jack, Ten, _, Nine, Eight] => Straight{high_card: Queen},
+                [Queen, _, Jack, _, Ten, Nine, Eight] => Straight{high_card: Queen},
+                [Queen, _, _, Jack, Ten, Nine, Eight] => Straight{high_card: Queen},
+                [_, Queen, Jack, Ten, Nine, Eight, _] => Straight{high_card: Queen},
+                [_, Queen, Jack, Ten, Nine, _, Eight] => Straight{high_card: Queen},
+                [_, Queen, Jack, Ten, _, Nine, Eight] => Straight{high_card: Queen},
+                [_, Queen, Jack, _, Ten, Nine, Eight] => Straight{high_card: Queen},
+                [_, Queen, _, Jack, Ten, Nine, Eight] => Straight{high_card: Queen},
+                [_, _, Queen, Jack, Ten, Nine, Eight] => Straight{high_card: Queen},
+
+                [Jack, Ten, Nine, Eight, Seven, _, _] => Straight{high_card: Jack},
+                [Jack, Ten, Nine, Eight, _, Seven, _] => Straight{high_card: Jack},
+                [Jack, Ten, Nine, Eight, _, _, Seven] => Straight{high_card: Jack},
+                [Jack, Ten, Nine, _, Eight, Seven, _] => Straight{high_card: Jack},
+                [Jack, Ten, Nine, _, Eight, _, Seven] => Straight{high_card: Jack},
+                [Jack, Ten, Nine, _, _, Eight, Seven] => Straight{high_card: Jack},
+                [Jack, Ten, _, Nine, Eight, Seven, _] => Straight{high_card: Jack},
+                [Jack, Ten, _, Nine, Eight, _, Seven] => Straight{high_card: Jack},
+                [Jack, Ten, _, Nine, _, Eight, Seven] => Straight{high_card: Jack},
+                [Jack, Ten, _, _, Nine, Eight, Seven] => Straight{high_card: Jack},
+                [Jack, _, Ten, Nine, Eight, Seven, _] => Straight{high_card: Jack},
+                [Jack, _, Ten, Nine, Eight, _, Seven] => Straight{high_card: Jack},
+                [Jack, _, Ten, Nine, _, Eight, Seven] => Straight{high_card: Jack},
+                [Jack, _, Ten, _, Nine, Eight, Seven] => Straight{high_card: Jack},
+                [Jack, _, _, Ten, Nine, Eight, Seven] => Straight{high_card: Jack},
+                [_, Jack, Ten, Nine, Eight, Seven, _] => Straight{high_card: Jack},
+                [_, Jack, Ten, Nine, Eight, _, Seven] => Straight{high_card: Jack},
+                [_, Jack, Ten, Nine, _, Eight, Seven] => Straight{high_card: Jack},
+                [_, Jack, Ten, _, Nine, Eight, Seven] => Straight{high_card: Jack},
+                [_, Jack, _, Ten, Nine, Eight, Seven] => Straight{high_card: Jack},
+                [_, _, Jack, Ten, Nine, Eight, Seven] => Straight{high_card: Jack},
+
+                [Ten, Nine, Eight, Seven, Six, _, _] => Straight{high_card: Ten},
+                [Ten, Nine, Eight, Seven, _, Six, _] => Straight{high_card: Ten},
+                [Ten, Nine, Eight, Seven, _, _, Six] => Straight{high_card: Ten},
+                [Ten, Nine, Eight, _, Seven, Six, _] => Straight{high_card: Ten},
+                [Ten, Nine, Eight, _, Seven, _, Six] => Straight{high_card: Ten},
+                [Ten, Nine, Eight, _, _, Seven, Six] => Straight{high_card: Ten},
+                [Ten, Nine, _, Eight, Seven, Six, _] => Straight{high_card: Ten},
+                [Ten, Nine, _, Eight, Seven, _, Six] => Straight{high_card: Ten},
+                [Ten, Nine, _, Eight, _, Seven, Six] => Straight{high_card: Ten},
+                [Ten, Nine, _, _, Eight, Seven, Six] => Straight{high_card: Ten},
+                [Ten, _, Nine, Eight, Seven, Six, _] => Straight{high_card: Ten},
+                [Ten, _, Nine, Eight, Seven, _, Six] => Straight{high_card: Ten},
+                [Ten, _, Nine, Eight, _, Seven, Six] => Straight{high_card: Ten},
+                [Ten, _, Nine, _, Eight, Seven, Six] => Straight{high_card: Ten},
+                [Ten, _, _, Nine, Eight, Seven, Six] => Straight{high_card: Ten},
+                [_, Ten, Nine, Eight, Seven, Six, _] => Straight{high_card: Ten},
+                [_, Ten, Nine, Eight, Seven, _, Six] => Straight{high_card: Ten},
+                [_, Ten, Nine, Eight, _, Seven, Six] => Straight{high_card: Ten},
+                [_, Ten, Nine, _, Eight, Seven, Six] => Straight{high_card: Ten},
+                [_, Ten, _, Nine, Eight, Seven, Six] => Straight{high_card: Ten},
+                [_, _, Ten, Nine, Eight, Seven, Six] => Straight{high_card: Ten},
+
+                [Nine, Eight, Seven, Six, Five, _, _] => Straight{high_card: Nine},
+                [Nine, Eight, Seven, Six, _, Five, _] => Straight{high_card: Nine},
+                [Nine, Eight, Seven, Six, _, _, Five] => Straight{high_card: Nine},
+                [Nine, Eight, Seven, _, Six, Five, _] => Straight{high_card: Nine},
+                [Nine, Eight, Seven, _, Six, _, Five] => Straight{high_card: Nine},
+                [Nine, Eight, Seven, _, _, Six, Five] => Straight{high_card: Nine},
+                [Nine, Eight, _, Seven, Six, Five, _] => Straight{high_card: Nine},
+                [Nine, Eight, _, Seven, Six, _, Five] => Straight{high_card: Nine},
+                [Nine, Eight, _, Seven, _, Six, Five] => Straight{high_card: Nine},
+                [Nine, Eight, _, _, Seven, Six, Five] => Straight{high_card: Nine},
+                [Nine, _, Eight, Seven, Six, Five, _] => Straight{high_card: Nine},
+                [Nine, _, Eight, Seven, Six, _, Five] => Straight{high_card: Nine},
+                [Nine, _, Eight, Seven, _, Six, Five] => Straight{high_card: Nine},
+                [Nine, _, Eight, _, Seven, Six, Five] => Straight{high_card: Nine},
+                [Nine, _, _, Eight, Seven, Six, Five] => Straight{high_card: Nine},
+                [_, Nine, Eight, Seven, Six, Five, _] => Straight{high_card: Nine},
+                [_, Nine, Eight, Seven, Six, _, Five] => Straight{high_card: Nine},
+                [_, Nine, Eight, Seven, _, Six, Five] => Straight{high_card: Nine},
+                [_, Nine, Eight, _, Seven, Six, Five] => Straight{high_card: Nine},
+                [_, Nine, _, Eight, Seven, Six, Five] => Straight{high_card: Nine},
+                [_, _, Nine, Eight, Seven, Six, Five] => Straight{high_card: Nine},
+
+                [Eight, Seven, Six, Five, Four, _, _] => Straight{high_card: Eight},
+                [Eight, Seven, Six, Five, _, Four, _] => Straight{high_card: Eight},
+                [Eight, Seven, Six, Five, _, _, Four] => Straight{high_card: Eight},
+                [Eight, Seven, Six, _, Five, Four, _] => Straight{high_card: Eight},
+                [Eight, Seven, Six, _, Five, _, Four] => Straight{high_card: Eight},
+                [Eight, Seven, Six, _, _, Five, Four] => Straight{high_card: Eight},
+                [Eight, Seven, _, Six, Five, Four, _] => Straight{high_card: Eight},
+                [Eight, Seven, _, Six, Five, _, Four] => Straight{high_card: Eight},
+                [Eight, Seven, _, Six, _, Five, Four] => Straight{high_card: Eight},
+                [Eight, Seven, _, _, Six, Five, Four] => Straight{high_card: Eight},
+                [Eight, _, Seven, Six, Five, Four, _] => Straight{high_card: Eight},
+                [Eight, _, Seven, Six, Five, _, Four] => Straight{high_card: Eight},
+                [Eight, _, Seven, Six, _, Five, Four] => Straight{high_card: Eight},
+                [Eight, _, Seven, _, Six, Five, Four] => Straight{high_card: Eight},
+                [Eight, _, _, Seven, Six, Five, Four] => Straight{high_card: Eight},
+                [_, Eight, Seven, Six, Five, Four, _] => Straight{high_card: Eight},
+                [_, Eight, Seven, Six, Five, _, Four] => Straight{high_card: Eight},
+                [_, Eight, Seven, Six, _, Five, Four] => Straight{high_card: Eight},
+                [_, Eight, Seven, _, Six, Five, Four] => Straight{high_card: Eight},
+                [_, Eight, _, Seven, Six, Five, Four] => Straight{high_card: Eight},
+                [_, _, Eight, Seven, Six, Five, Four] => Straight{high_card: Eight},
+
+                [Seven, Six, Five, Four, Three, _, _] => Straight{high_card: Seven},
+                [Seven, Six, Five, Four, _, Three, _] => Straight{high_card: Seven},
+                [Seven, Six, Five, Four, _, _, Three] => Straight{high_card: Seven},
+                [Seven, Six, Five, _, Four, Three, _] => Straight{high_card: Seven},
+                [Seven, Six, Five, _, Four, _, Three] => Straight{high_card: Seven},
+                [Seven, Six, Five, _, _, Four, Three] => Straight{high_card: Seven},
+                [Seven, Six, _, Five, Four, Three, _] => Straight{high_card: Seven},
+                [Seven, Six, _, Five, Four, _, Three] => Straight{high_card: Seven},
+                [Seven, Six, _, Five, _, Four, Three] => Straight{high_card: Seven},
+                [Seven, Six, _, _, Five, Four, Three] => Straight{high_card: Seven},
+                [Seven, _, Six, Five, Four, Three, _] => Straight{high_card: Seven},
+                [Seven, _, Six, Five, Four, _, Three] => Straight{high_card: Seven},
+                [Seven, _, Six, Five, _, Four, Three] => Straight{high_card: Seven},
+                [Seven, _, Six, _, Five, Four, Three] => Straight{high_card: Seven},
+                [Seven, _, _, Six, Five, Four, Three] => Straight{high_card: Seven},
+                [_, Seven, Six, Five, Four, Three, _] => Straight{high_card: Seven},
+                [_, Seven, Six, Five, Four, _, Three] => Straight{high_card: Seven},
+                [_, Seven, Six, Five, _, Four, Three] => Straight{high_card: Seven},
+                [_, Seven, Six, _, Five, Four, Three] => Straight{high_card: Seven},
+                [_, Seven, _, Six, Five, Four, Three] => Straight{high_card: Seven},
+                [_, _, Seven, Six, Five, Four, Three] => Straight{high_card: Seven},
+
+                [Six, Five, Four, Three, Two, _, _] => Straight{high_card: Six},
+                [Six, Five, Four, Three, _, Two, _] => Straight{high_card: Six},
+                [Six, Five, Four, Three, _, _, Two] => Straight{high_card: Six},
+                [Six, Five, Four, _, Three, Two, _] => Straight{high_card: Six},
+                [Six, Five, Four, _, Three, _, Two] => Straight{high_card: Six},
+                [Six, Five, Four, _, _, Three, Two] => Straight{high_card: Six},
+                [Six, Five, _, Four, Three, Two, _] => Straight{high_card: Six},
+                [Six, Five, _, Four, Three, _, Two] => Straight{high_card: Six},
+                [Six, Five, _, Four, _, Three, Two] => Straight{high_card: Six},
+                [Six, Five, _, _, Four, Three, Two] => Straight{high_card: Six},
+                [Six, _, Five, Four, Three, Two, _] => Straight{high_card: Six},
+                [Six, _, Five, Four, Three, _, Two] => Straight{high_card: Six},
+                [Six, _, Five, Four, _, Three, Two] => Straight{high_card: Six},
+                [Six, _, Five, _, Four, Three, Two] => Straight{high_card: Six},
+                [Six, _, _, Five, Four, Three, Two] => Straight{high_card: Six},
+                [_, Six, Five, Four, Three, Two, _] => Straight{high_card: Six},
+                [_, Six, Five, Four, Three, _, Two] => Straight{high_card: Six},
+                [_, Six, Five, Four, _, Three, Two] => Straight{high_card: Six},
+                [_, Six, Five, _, Four, Three, Two] => Straight{high_card: Six},
+                [_, Six, _, Five, Four, Three, Two] => Straight{high_card: Six},
+                [_, _, Six, Five, Four, Three, Two] => Straight{high_card: Six},
+
+                [Ace, Five, Four, Three, Two, _, _] => Straight{high_card: Five},
+                [Ace, Five, Four, Three, _, Two, _] => Straight{high_card: Five},
+                [Ace, Five, Four, Three, _, _, Two] => Straight{high_card: Five},
+                [Ace, Five, Four, _, Three, Two, _] => Straight{high_card: Five},
+                [Ace, Five, Four, _, Three, _, Two] => Straight{high_card: Five},
+                [Ace, Five, Four, _, _, Three, Two] => Straight{high_card: Five},
+                [Ace, Five, _, Four, Three, Two, _] => Straight{high_card: Five},
+                [Ace, Five, _, Four, Three, _, Two] => Straight{high_card: Five},
+                [Ace, Five, _, Four, _, Three, Two] => Straight{high_card: Five},
+                [Ace, Five, _, _, Four, Three, Two] => Straight{high_card: Five},
+                [Ace, _, Five, Four, Three, Two, _] => Straight{high_card: Five},
+                [Ace, _, Five, Four, Three, _, Two] => Straight{high_card: Five},
+                [Ace, _, Five, Four, _, Three, Two] => Straight{high_card: Five},
+                [Ace, _, Five, _, Four, Three, Two] => Straight{high_card: Five},
+                [Ace, _, _, Five, Four, Three, Two] => Straight{high_card: Five},
+                [_, Ace, Five, Four, Three, Two, _] => Straight{high_card: Five},
+                [_, Ace, Five, Four, Three, _, Two] => Straight{high_card: Five},
+                [_, Ace, Five, Four, _, Three, Two] => Straight{high_card: Five},
+                [_, Ace, Five, _, Four, Three, Two] => Straight{high_card: Five},
+                [_, Ace, _, Five, Four, Three, Two] => Straight{high_card: Five},
+                [_, _, Ace, Five, Four, Three, Two] => Straight{high_card: Five},
+
+                [trip1, trip2, trip3, kicker1, kicker2, _, _] if trip1 == trip2 && trip2 == trip3 => ThreeOfAKind{trips: trip1, kickers: (kicker1, kicker2)},
+                [kicker1, trip1, trip2, trip3, kicker2, _, _] if trip1 == trip2 && trip2 == trip3 => ThreeOfAKind{trips: trip1, kickers: (kicker1, kicker2)},
+                [kicker1, kicker2, trip1, trip2, trip3, _, _] if trip1 == trip2 && trip2 == trip3 => ThreeOfAKind{trips: trip1, kickers: (kicker1, kicker2)},
+                [kicker1, kicker2, _, trip1, trip2, trip3, _] if trip1 == trip2 && trip2 == trip3 => ThreeOfAKind{trips: trip1, kickers: (kicker1, kicker2)},
+                [kicker1, kicker2, _, _, trip1, trip2, trip3] if trip1 == trip2 && trip2 == trip3 => ThreeOfAKind{trips: trip1, kickers: (kicker1, kicker2)},
+
+                [pair1, pair2, pair3, pair4, kicker, _, _] if pair1 == pair2 && pair3 == pair4 => TwoPair{pairs: (pair1, pair3), kicker: kicker},
+                [pair1, pair2, kicker, pair3, pair4, _, _] if pair1 == pair2 && pair3 == pair4 => TwoPair{pairs: (pair1, pair3), kicker: kicker},
+                [pair1, pair2, kicker, _, pair3, pair4, _] if pair1 == pair2 && pair3 == pair4 => TwoPair{pairs: (pair1, pair3), kicker: kicker},
+                [pair1, pair2, kicker, _, _, pair3, pair4] if pair1 == pair2 && pair3 == pair4 => TwoPair{pairs: (pair1, pair3), kicker: kicker},
+                [kicker, pair1, pair2, pair3, pair4, _, _] if pair1 == pair2 && pair3 == pair4 => TwoPair{pairs: (pair1, pair3), kicker: kicker},
+                [kicker, pair1, pair2, _, pair3, pair4, _] if pair1 == pair2 && pair3 == pair4 => TwoPair{pairs: (pair1, pair3), kicker: kicker},
+                [kicker, pair1, pair2, _, _, pair3, pair4] if pair1 == pair2 && pair3 == pair4 => TwoPair{pairs: (pair1, pair3), kicker: kicker},
+                [kicker, _, pair1, pair2, pair3, pair4, _] if pair1 == pair2 && pair3 == pair4 => TwoPair{pairs: (pair1, pair3), kicker: kicker},
+                [kicker, _, pair1, pair2, _, pair3, pair4] if pair1 == pair2 && pair3 == pair4 => TwoPair{pairs: (pair1, pair3), kicker: kicker},
+                [kicker, _, _, pair1, pair2, pair3, pair4] if pair1 == pair2 && pair3 == pair4 => TwoPair{pairs: (pair1, pair3), kicker: kicker},
+
+                [pair1, pair2, kicker1, kicker2, kicker3, _, _] if pair1 == pair2 => Pair{pair: pair1, kickers: (kicker1, kicker2, kicker3)},
+                [kicker1, pair1, pair2, kicker2, kicker3, _, _] if pair1 == pair2 => Pair{pair: pair1, kickers: (kicker1, kicker2, kicker3)},
+                [kicker1, kicker2, pair1, pair2, kicker3, _, _] if pair1 == pair2 => Pair{pair: pair1, kickers: (kicker1, kicker2, kicker3)},
+                [kicker1, kicker2, kicker3, pair1, pair2, _, _] if pair1 == pair2 => Pair{pair: pair1, kickers: (kicker1, kicker2, kicker3)},
+                [kicker1, kicker2, kicker3, _, pair1, pair2, _] if pair1 == pair2 => Pair{pair: pair1, kickers: (kicker1, kicker2, kicker3)},
+                [kicker1, kicker2, kicker3, _, _, pair1, pair2] if pair1 == pair2 => Pair{pair: pair1, kickers: (kicker1, kicker2, kicker3)},
+
+                [rank1, rank2, rank3, rank4, rank5, _, _] => HighCard{kickers: (rank1, rank2, rank3, rank4, rank5)},
+            }
+    }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub struct Odds {
+    player1_wins: usize,
+    player2_wins: usize,
+    draws: usize,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub enum Player {
+    Player1,
+    Player2,
+    Draw,
+}
+
+impl Display for Player {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        f.write_str(match *self {
+            Player::Player1 => "Player 1",
+            Player::Player2 => "Player 2",
+            Player::Draw => "Draw",
+        })
+    }
+}
+
+impl Odds {
+    pub fn player1_probability(&self) -> f64 {
+        self.player1_wins as f64 / (self.player1_wins + self.player2_wins + self.draws) as f64
+    }
+
+    pub fn player2_probability(&self) -> f64 {
+        self.player2_wins as f64 / (self.player1_wins + self.player2_wins + self.draws) as f64
+    }
+
+    pub fn draw_probability(&self) -> f64 {
+        self.draws as f64 / (self.player1_wins + self.player2_wins + self.draws) as f64
+    }
+
+    pub fn favourite(&self) -> Player {
+        match self.player1_wins.cmp(&self.player2_wins) {
+            Ordering::Greater => Player::Player1,
+            Ordering::Less => Player::Player2,
+            Ordering::Equal => Player::Draw,
+        }
+    }
+}
+
+impl Display for Odds {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        write!(f, "Player 1: {:.02}%, Player 2: {:.02}%, Draw: {:.02}%",
+            self.player1_probability() * 100.0,
+            self.player2_probability() * 100.0,
+            self.draw_probability() * 100.0)
+    }
+}
+
+pub fn heads_up_odds(player1: [Card; 2], player2: [Card; 2]) -> Odds {
+    let mut odds = Odds {
+        player1_wins: 0,
+        player2_wins: 0,
+        draws: 0,
+    };
+    let cards: Vec<Card> = FIFTY_TWO_CARD_DECK.iter()
+        .filter(|&&card| card != player1[0])
+        .filter(|&&card| card != player1[1])
+        .filter(|&&card| card != player2[0])
+        .filter(|&&card| card != player2[1])
+        .cloned().collect();
+    for i in 0..48 {
+        for j in i+1..48 {
+            for k in j+1..48 {
+                for l in k+1..48 {
+                    for m in l+1..48 {
+                        let player1_rank = rank_7_card_hand([player1[0], player1[1], cards[i], cards[j], cards[k], cards[l], cards[m]]);
+                        let player2_rank = rank_7_card_hand([player2[0], player2[1], cards[i], cards[j], cards[k], cards[l], cards[m]]);
+                        match player1_rank.cmp(&player2_rank) {
+                            Ordering::Greater => odds.player1_wins += 1,
+                            Ordering::Less => odds.player2_wins += 1,
+                            Ordering::Equal => odds.draws += 1,
+                        }
+                    }
+                }
+            }
+        }
+    }
+    odds
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    use rand::Rng;
+    use rand::seq::SliceRandom;
+
+    pub fn random_4_card_draw<R: Rng>(rng: &mut R) -> [Card; 4] {
+        let mut cards = FIFTY_TWO_CARD_DECK.choose_multiple(rng, 4);
+        [
+            *cards.next().unwrap(),
+            *cards.next().unwrap(),
+            *cards.next().unwrap(),
+            *cards.next().unwrap(),
+        ]
+    }
+
+    pub fn random_7_card_draw<R: Rng>(rng: &mut R) -> [Card; 7] {
+        let mut cards = FIFTY_TWO_CARD_DECK.choose_multiple(rng, 7);
+        [
+            *cards.next().unwrap(),
+            *cards.next().unwrap(),
+            *cards.next().unwrap(),
+            *cards.next().unwrap(),
+            *cards.next().unwrap(),
+            *cards.next().unwrap(),
+            *cards.next().unwrap(),
+        ]
+    }
+
+    fn rank_7_card_hand_naive(cards: [Card; 7]) -> Hand {
+        let rankings = [
+            rank_5_card_hand([cards[0], cards[1], cards[2], cards[3], cards[4]]),
+            rank_5_card_hand([cards[0], cards[1], cards[2], cards[3], cards[5]]),
+            rank_5_card_hand([cards[0], cards[1], cards[2], cards[3], cards[6]]),
+            rank_5_card_hand([cards[0], cards[1], cards[2], cards[4], cards[5]]),
+            rank_5_card_hand([cards[0], cards[1], cards[2], cards[4], cards[6]]),
+            rank_5_card_hand([cards[0], cards[1], cards[2], cards[5], cards[6]]),
+            rank_5_card_hand([cards[0], cards[1], cards[3], cards[4], cards[5]]),
+            rank_5_card_hand([cards[0], cards[1], cards[3], cards[4], cards[6]]),
+            rank_5_card_hand([cards[0], cards[1], cards[3], cards[5], cards[6]]),
+            rank_5_card_hand([cards[0], cards[1], cards[4], cards[5], cards[6]]),
+            rank_5_card_hand([cards[0], cards[2], cards[3], cards[4], cards[5]]),
+            rank_5_card_hand([cards[0], cards[2], cards[3], cards[4], cards[6]]),
+            rank_5_card_hand([cards[0], cards[2], cards[3], cards[5], cards[6]]),
+            rank_5_card_hand([cards[0], cards[2], cards[4], cards[5], cards[6]]),
+            rank_5_card_hand([cards[0], cards[3], cards[4], cards[5], cards[6]]),
+            rank_5_card_hand([cards[1], cards[2], cards[3], cards[4], cards[5]]),
+            rank_5_card_hand([cards[1], cards[2], cards[3], cards[4], cards[6]]),
+            rank_5_card_hand([cards[1], cards[2], cards[3], cards[5], cards[6]]),
+            rank_5_card_hand([cards[1], cards[2], cards[4], cards[5], cards[6]]),
+            rank_5_card_hand([cards[1], cards[3], cards[4], cards[5], cards[6]]),
+            rank_5_card_hand([cards[2], cards[3], cards[4], cards[5], cards[6]]),
+        ];
+        *rankings.iter().max().unwrap()
+    }
+
+    fn rank_5_card_hand(mut cards: [Card; 5]) -> Hand {
+        cards.sort_by(|a, b| b.rank.cmp(&a.rank));
+        let ranks = [cards[0].rank, cards[1].rank, cards[2].rank, cards[3].rank, cards[4].rank];
+        let suits = [cards[0].suit, cards[1].suit, cards[2].suit, cards[3].suit, cards[4].suit];
+
+        match suits {
+            [Spades, Spades, Spades, Spades, Spades] |
+            [Hearts, Hearts, Hearts, Hearts, Hearts] |
+            [Diamonds, Diamonds, Diamonds, Diamonds, Diamonds] |
+            [Clubs, Clubs, Clubs, Clubs, Clubs] =>
+                match ranks {
+                    [Ace, King, Queen, Jack, Ten] => RoyalFlush,
+                    [King, Queen, Jack, Ten, Nine] => StraightFlush{high_card: King},
+                    [Queen, Jack, Ten, Nine, Eight] => StraightFlush{high_card: Queen},
+                    [Jack, Ten, Nine, Eight, Seven] => StraightFlush{high_card: Jack},
+                    [Ten, Nine, Eight, Seven, Six] => StraightFlush{high_card: Ten},
+                    [Nine, Eight, Seven, Six, Five] => StraightFlush{high_card: Nine},
+                    [Eight, Seven, Six, Five, Four] => StraightFlush{high_card: Eight},
+                    [Seven, Six, Five, Four, Three] => StraightFlush{high_card: Seven},
+                    [Six, Five, Four, Three, Two] => StraightFlush{high_card: Six},
+                    [Ace, Five, Four, Three, Two] => StraightFlush{high_card: Five},
+                    [rank1, rank2, rank3, rank4, rank5] => Flush{flush: (rank1, rank2, rank3, rank4, rank5)},
+                }
+            [_, _, _, _, _] =>
+                match ranks {
+                    [quad1, quad2, quad3, quad4, kicker] if quad1 == quad2 && quad2 == quad3 && quad3 == quad4 => FourOfAKind{quads: quad1, kicker: kicker},
+                    [kicker, quad1, quad2, quad3, quad4] if quad1 == quad2 && quad2 == quad3 && quad3 == quad4 => FourOfAKind{quads: quad1, kicker: kicker},
+                    [trip1, trip2, trip3, pair1, pair2] if trip1 == trip2 && trip2 == trip3 && pair1 == pair2 => FullHouse{trips: trip1, pair: pair1},
+                    [pair1, pair2, trip1, trip2, trip3] if trip1 == trip2 && trip2 == trip3 && pair1 == pair2 => FullHouse{trips: trip1, pair: pair1},
+                    [Ace, King, Queen, Jack, Ten] => Straight{high_card: Ace},
+                    [King, Queen, Jack, Ten, Nine] => Straight{high_card: King},
+                    [Queen, Jack, Ten, Nine, Eight] => Straight{high_card: Queen},
+                    [Jack, Ten, Nine, Eight, Seven] => Straight{high_card: Jack},
+                    [Ten, Nine, Eight, Seven, Six] => Straight{high_card: Ten},
+                    [Nine, Eight, Seven, Six, Five] => Straight{high_card: Nine},
+                    [Eight, Seven, Six, Five, Four] => Straight{high_card: Eight},
+                    [Seven, Six, Five, Four, Three] => Straight{high_card: Seven},
+                    [Six, Five, Four, Three, Two] => Straight{high_card: Six},
+                    [Ace, Five, Four, Three, Two] => Straight{high_card: Five},
+                    [trip1, trip2, trip3, kicker1, kicker2] if trip1 == trip2 && trip2 == trip3 => ThreeOfAKind{trips: trip1, kickers: (kicker1, kicker2)},
+                    [kicker1, trip1, trip2, trip3, kicker2] if trip1 == trip2 && trip2 == trip3 => ThreeOfAKind{trips: trip1, kickers: (kicker1, kicker2)},
+                    [kicker1, kicker2, trip1, trip2, trip3] if trip1 == trip2 && trip2 == trip3 => ThreeOfAKind{trips: trip1, kickers: (kicker1, kicker2)},
+                    [pair1, pair2, pair3, pair4, kicker] if pair1 == pair2 && pair3 == pair4 => TwoPair{pairs: (pair1, pair3), kicker: kicker},
+                    [pair1, pair2, kicker, pair3, pair4] if pair1 == pair2 && pair3 == pair4 => TwoPair{pairs: (pair1, pair3), kicker: kicker},
+                    [kicker, pair1, pair2, pair3, pair4] if pair1 == pair2 && pair3 == pair4 => TwoPair{pairs: (pair1, pair3), kicker: kicker},
+                    [pair1, pair2, kicker1, kicker2, kicker3] if pair1 == pair2 => Pair{pair: pair1, kickers: (kicker1, kicker2, kicker3)},
+                    [kicker1, pair1, pair2, kicker2, kicker3] if pair1 == pair2 => Pair{pair: pair1, kickers: (kicker1, kicker2, kicker3)},
+                    [kicker1, kicker2, pair1, pair2, kicker3] if pair1 == pair2 => Pair{pair: pair1, kickers: (kicker1, kicker2, kicker3)},
+                    [kicker1, kicker2, kicker3, pair1, pair2] if pair1 == pair2 => Pair{pair: pair1, kickers: (kicker1, kicker2, kicker3)},
+                    [rank1, rank2, rank3, rank4, rank5] => HighCard{kickers: (rank1, rank2, rank3, rank4, rank5)},
+                }
+        }
+    }
+
+    pub fn heads_up_odds_naive(player1: [Card; 2], player2: [Card; 2]) -> Odds {
+        let mut odds = Odds {
+            player1_wins: 0,
+            player2_wins: 0,
+            draws: 0,
+        };
+        let cards: Vec<Card> = FIFTY_TWO_CARD_DECK.iter()
+            .filter(|&&card| card != player1[0])
+            .filter(|&&card| card != player1[1])
+            .filter(|&&card| card != player2[0])
+            .filter(|&&card| card != player2[1])
+            .cloned().collect();
+        for i in 0..48 {
+            for j in i+1..48 {
+                for k in j+1..48 {
+                    for l in k+1..48 {
+                        for m in l+1..48 {
+                            let player1_rank = rank_7_card_hand([player1[0], player1[1], cards[i], cards[j], cards[k], cards[l], cards[m]]);
+                            let player2_rank = rank_7_card_hand([player2[0], player2[1], cards[i], cards[j], cards[k], cards[l], cards[m]]);
+                            match player1_rank.cmp(&player2_rank) {
+                                Ordering::Greater => odds.player1_wins += 1,
+                                Ordering::Less => odds.player2_wins += 1,
+                                Ordering::Equal => odds.draws += 1,
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        odds
+    }
+
+    #[test]
+    fn rank_royal_flush() {
+        assert_eq!(rank_5_card_hand([ACE_OF_SPADES, KING_OF_SPADES, QUEEN_OF_SPADES, JACK_OF_SPADES, TEN_OF_SPADES]), RoyalFlush);
+        assert_eq!(rank_5_card_hand([ACE_OF_CLUBS, KING_OF_CLUBS, QUEEN_OF_CLUBS, JACK_OF_CLUBS, TEN_OF_CLUBS]), RoyalFlush);
+    }
+
+    #[test]
+    fn rank_straight_flush() {
+        assert_eq!(rank_5_card_hand([KING_OF_SPADES, QUEEN_OF_SPADES, JACK_OF_SPADES, TEN_OF_SPADES, NINE_OF_SPADES]), StraightFlush{high_card: King});
+        assert_eq!(rank_5_card_hand([FIVE_OF_CLUBS, FOUR_OF_CLUBS, THREE_OF_CLUBS, TWO_OF_CLUBS, ACE_OF_CLUBS]), StraightFlush{high_card: Five});
+    }
+
+    #[test]
+    fn rank_four_of_a_kind() {
+        assert_eq!(rank_5_card_hand([THREE_OF_CLUBS, THREE_OF_DIAMONDS, THREE_OF_HEARTS, THREE_OF_SPADES, ACE_OF_CLUBS]), FourOfAKind{quads: Three, kicker: Ace});
+        assert_eq!(rank_5_card_hand([THREE_OF_CLUBS, TWO_OF_HEARTS, THREE_OF_HEARTS, THREE_OF_SPADES, THREE_OF_DIAMONDS]), FourOfAKind{quads: Three, kicker: Two});
+    }
+
+    #[test]
+    fn rank_full_house() {
+        assert_eq!(rank_5_card_hand([KING_OF_HEARTS, THREE_OF_DIAMONDS, THREE_OF_HEARTS, KING_OF_DIAMONDS, KING_OF_SPADES]), FullHouse{trips: King, pair: Three});
+        assert_eq!(rank_5_card_hand([THREE_OF_SPADES, THREE_OF_DIAMONDS, THREE_OF_HEARTS, KING_OF_DIAMONDS, KING_OF_SPADES]), FullHouse{trips: Three, pair: King});
+    }
+
+    #[test]
+    fn rank_flush() {
+        assert_eq!(rank_5_card_hand([THREE_OF_SPADES, SEVEN_OF_SPADES, ACE_OF_SPADES, FOUR_OF_SPADES, QUEEN_OF_SPADES]), Flush{flush: (Ace, Queen, Seven, Four, Three)});
+        assert_eq!(rank_5_card_hand([TWO_OF_HEARTS, EIGHT_OF_HEARTS, FIVE_OF_HEARTS, SEVEN_OF_HEARTS, NINE_OF_HEARTS]), Flush{flush: (Nine, Eight, Seven, Five, Two)});
+    }
+
+    #[test]
+    fn rank_straight() {
+        assert_eq!(rank_5_card_hand([EIGHT_OF_HEARTS, QUEEN_OF_SPADES, JACK_OF_SPADES, TEN_OF_SPADES, NINE_OF_SPADES]), Straight{high_card: Queen});
+        assert_eq!(rank_5_card_hand([FIVE_OF_CLUBS, FOUR_OF_SPADES, THREE_OF_CLUBS, TWO_OF_HEARTS, ACE_OF_CLUBS]), Straight{high_card: Five});
+    }
+
+    #[test]
+    fn rank_three_of_a_kind() {
+        assert_eq!(rank_5_card_hand([THREE_OF_CLUBS, THREE_OF_DIAMONDS, SEVEN_OF_CLUBS, THREE_OF_SPADES, ACE_OF_CLUBS]), ThreeOfAKind{trips: Three, kickers: (Ace, Seven)});
+        assert_eq!(rank_5_card_hand([THREE_OF_CLUBS, TWO_OF_HEARTS, THREE_OF_HEARTS, SIX_OF_CLUBS, THREE_OF_DIAMONDS]), ThreeOfAKind{trips: Three, kickers: (Six, Two)});
+    }
+
+    #[test]
+    fn rank_two_pair() {
+        assert_eq!(rank_5_card_hand([THREE_OF_CLUBS, SEVEN_OF_HEARTS, SEVEN_OF_CLUBS, THREE_OF_SPADES, ACE_OF_CLUBS]), TwoPair{pairs: (Seven, Three), kicker: Ace});
+        assert_eq!(rank_5_card_hand([THREE_OF_CLUBS, TWO_OF_HEARTS, SIX_OF_HEARTS, SIX_OF_CLUBS, THREE_OF_DIAMONDS]), TwoPair{pairs: (Six, Three), kicker: Two});
+    }
+
+    #[test]
+    fn rank_pair() {
+        assert_eq!(rank_5_card_hand([THREE_OF_CLUBS, SEVEN_OF_HEARTS, NINE_OF_HEARTS, THREE_OF_SPADES, ACE_OF_CLUBS]), Pair{pair: Three, kickers: (Ace, Nine, Seven)});
+        assert_eq!(rank_5_card_hand([THREE_OF_CLUBS, TWO_OF_HEARTS, SEVEN_OF_HEARTS, SIX_OF_CLUBS, THREE_OF_DIAMONDS]), Pair{pair: Three, kickers: (Seven, Six, Two)});
+    }
+
+    #[test]
+    fn rank_high_card() {
+        assert_eq!(rank_5_card_hand([ACE_OF_SPADES, TWO_OF_HEARTS, SEVEN_OF_HEARTS, SIX_OF_CLUBS, THREE_OF_DIAMONDS]), HighCard{kickers: (Ace, Seven, Six, Three, Two)});
+        assert_eq!(rank_5_card_hand([EIGHT_OF_SPADES, SIX_OF_DIAMONDS, SEVEN_OF_HEARTS, KING_OF_HEARTS, FOUR_OF_CLUBS]), HighCard{kickers: (King, Eight, Seven, Six, Four)});
+    }
+
+    #[test]
+    fn rank_7_card_pair() {
+        assert_eq!(rank_7_card_hand([NINE_OF_CLUBS, TWO_OF_SPADES, SIX_OF_CLUBS, ACE_OF_DIAMONDS, FOUR_OF_HEARTS, THREE_OF_HEARTS, FOUR_OF_CLUBS]), Pair{pair: Four, kickers: (Ace, Nine, Six)});
+    }
+
+    #[test]
+    fn rank_7_card_two_pair() {
+        assert_eq!(rank_7_card_hand([SIX_OF_HEARTS, JACK_OF_SPADES, TWO_OF_DIAMONDS, TEN_OF_CLUBS, TWO_OF_CLUBS, SIX_OF_DIAMONDS, FOUR_OF_SPADES]), TwoPair{pairs: (Six, Two), kicker: Jack});
+    }
+
+    #[test]
+    fn rank_7_card_hands_against_naive() {
+        let mut rng = rand::thread_rng();
+        for _ in 0..100000 {
+            let cards = random_7_card_draw(&mut rng);
+            let hand = rank_7_card_hand_naive(cards);
+            assert_eq!(hand, rank_7_card_hand(cards), "cards were: {} {} {} {} {} {} {}", cards[0], cards[1], cards[2], cards[3], cards[4], cards[5], cards[6]);
+        }
+    }
+
+    #[test]
+    fn heads_up_odds_against_naive() {
+        let mut rng = rand::thread_rng();
+        let cards = random_4_card_draw(&mut rng);
+        let player1 = [cards[0], cards[1]];
+        let player2 = [cards[2], cards[3]];
+        let odds = heads_up_odds_naive(player1, player2);
+        assert_eq!(odds, heads_up_odds(player1, player2), "cards were: {}{} vs {}{}", cards[0], cards[1], cards[2], cards[3]);
+    }
+}
diff --git a/src/game/poker/holdem.rs b/src/game/poker/holdem.rs
new file mode 100644 (file)
index 0000000..c6b1da7
--- /dev/null
@@ -0,0 +1,336 @@
+use std::collections::{HashMap, HashSet};
+
+use itertools::Itertools;
+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;
+
+use super::super::{Action, ActionError, Game, UserAction, ValidatedUserAction};
+
+#[derive(Copy, Clone, Debug)]
+enum State {
+    NotStarted,
+    Dealing,
+    PostingSmallBlind,
+    PostingBigBlind,
+    PreFlopBetting,
+    DealingFlop,
+    PostFlopBetting,
+    DealingTurn,
+    TurnBetting,
+    DealingRiver,
+    RiverBetting,
+    Showdown,
+    Completed,
+}
+
+#[derive(Clone, Debug)]
+pub struct TexasHoldEmSettings {
+    title: String,
+    max_players: u32,
+    small_blind: u64,
+    starting_stack: u64,
+}
+
+#[derive(Clone, Debug)]
+pub struct TexasHoldEm {
+    id: i64,
+    settings: TexasHoldEmSettings,
+    actions_len: usize,
+    state: State,
+    seats: Seats,
+    stacks: HashMap<Username, u64>,
+    deck: HashSet<Card>,
+    hands: HashMap<Username, HashSet<Card>>,
+    community: HashSet<Card>,
+    dealer: Option<Username>,
+    receiver: Option<Username>,
+    active: Option<Username>,
+    bets: HashMap<Username, u64>,
+    players: HashSet<Username>,
+    pot: u64,
+    small_blind: u64,
+    big_blind: u64,
+}
+
+impl TexasHoldEm {
+    pub fn new(id: i64, settings: TexasHoldEmSettings) -> Self {
+        let small_blind = settings.small_blind;
+        let big_blind = small_blind * 2;
+        Self {
+            id,
+            settings,
+            actions_len: 0,
+            state: State::NotStarted,
+            seats: Seats::new(),
+            stacks: HashMap::new(),
+            deck: FIFTY_TWO_CARD_DECK.iter().cloned().collect(),
+            hands: HashMap::new(),
+            community: HashSet::new(),
+            dealer: None,
+            receiver: None,
+            active: None,
+            bets: HashMap::new(),
+            players: HashSet::new(),
+            pot: 0,
+            small_blind,
+            big_blind,
+        }
+    }
+
+    fn chips_to_call(&self, username: Username) -> u64 {
+        todo!()
+    }
+
+    fn stack(&self, username: Username) -> u64 {
+        match self.stacks.get(&username) {
+            Some(&stack) => stack,
+            None => 0,
+        }
+    }
+}
+
+impl Game for TexasHoldEm {
+    fn id(&self) -> i64 {
+        self.id
+    }
+
+    fn players(&self) -> HashSet<Username> {
+        self.seats.player_set()
+    }
+
+    fn actions_len(&self) -> usize {
+        self.actions_len
+    }
+
+    fn validate_action(&self, UserAction{username, action}: UserAction) -> Result<ValidatedUserAction, ActionError> {
+        match (self.state, action) {
+            (_, Action::PlayCard{..}) | (_, Action::ChooseTrumps{..}) => {
+                Err(ActionError::InvalidActionForGameType)
+            }
+            (State::NotStarted, Action::Join{seat, chips}) => {
+                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 if chips < self.settings.starting_stack {
+                    Err(ActionError::StartingStackTooSmall)
+                } else if chips > self.settings.starting_stack {
+                    Err(ActionError::StartingStackTooLarge)
+                } else {
+                    Ok(ValidatedUserAction(UserAction{username, action: Action::Join{seat, chips}}))
+                }
+            }
+            (State::Completed, Action::Join{..}) => Err(ActionError::GameHasEnded),
+            (_, Action::Join{..}) => Err(ActionError::GameHasStarted),
+            (_, _) if !self.seats.contains_player(username) => Err(ActionError::NotAuthorised),
+            (State::NotStarted, Action::Leave) => {
+                Ok(ValidatedUserAction(UserAction{username, action: Action::Leave}))
+            }
+            (_, _) if !self.seats.contains_player(username) => Err(ActionError::NotAuthorised),
+            (State::NotStarted, Action::Leave) => {
+                Ok(ValidatedUserAction(UserAction{username, action: Action::Leave}))
+            }
+            (State::Completed, _) => Err(ActionError::GameHasEnded),
+            (_, Action::Leave) => Err(ActionError::GameHasStarted),
+            (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.active != Some(username) => Err(ActionError::OutOfTurn),
+            (_, Action::Fold) if self.chips_to_call(username) == 0 => Err(ActionError::CannotFold),
+            (_, Action::Fold) => Ok(ValidatedUserAction(UserAction{username, action: Action::Fold})),
+            (_, Action::Bet{chips}) if chips > self.stack(username) => Err(ActionError::NotEnoughChips),
+            (_, Action::Bet{chips}) if chips < self.chips_to_call(username) => Err(ActionError::BetSizeTooSmall),
+            (_, Action::Bet{chips}) if chips > self.chips_to_call(username) && chips < 2 * self.chips_to_call(username) => Err(ActionError::BetSizeTooSmall),
+            (_, Action::Bet{chips}) => Ok(ValidatedUserAction(UserAction{username, action: Action::Bet{chips}})),
+            (_, _) => Err(ActionError::InvalidActionForGameType),
+        }
+    }
+
+    fn take_action(&mut self, ValidatedUserAction(UserAction{username, action}): ValidatedUserAction) -> Result<(), ActionError> {
+        self.actions_len += 1;
+        match (self.state, action) {
+            (_, Action::PlayCard{..}) | (_, Action::ChooseTrumps{..}) => {
+                Err(ActionError::InvalidActionForGameType)
+            }
+            (State::NotStarted, Action::Join{seat, chips}) => {
+                self.stacks.insert(username, chips);
+                self.seats.add_player(seat, username)
+            }
+            (State::NotStarted, Action::Leave) => {
+                self.stacks.remove(&username);
+                self.seats.remove_player(username)
+            }
+            (_, Action::NextToDeal) => {
+                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.bets.clear();
+                if self.pot != 0 {
+                    error!("Logic error: pot was {} upon dealing: {:#?}", self.pot, self);
+                    self.pot = 0;
+                }
+                Ok(())
+            }
+            (State::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() == 2) {
+                    self.receiver = None;
+                } else {
+                    self.receiver = self.receiver.and_then(|player| self.seats.player_after(player));
+                }
+                Ok(())
+            }
+            (State::Dealing, Action::EndDeal) => {
+                self.state = State::PostingSmallBlind;
+                self.receiver = None;
+                Ok(())
+            }
+            (State::PostingSmallBlind, Action::Bet{chips}) => {
+                *self.bets.entry(username).or_default() += chips;
+                *self.stacks.entry(username).or_default() -= chips;
+                self.state = State::PostingBigBlind;
+                Ok(())
+            }
+            (State::PostingBigBlind, Action::Bet{chips}) => {
+                *self.bets.entry(username).or_default() += chips;
+                *self.stacks.entry(username).or_default() -= chips;
+                self.active = self.seats.player_after_in(username, &self.players);
+                self.state = State::PreFlopBetting;
+                Ok(())
+            }
+            (_, Action::Bet{chips}) => {
+                if chips == 0 && self.bets.len() == self.players.len() && self.bets.values().all_equal() {
+                    self.active = None;
+                    self.pot += self.bets.values().sum::<u64>();
+                    self.bets.clear();
+                    self.state = match self.state {
+                        State::PreFlopBetting => State::DealingFlop,
+                        State::PostFlopBetting => State::DealingTurn,
+                        State::TurnBetting => State::DealingRiver,
+                        State::RiverBetting => State::Showdown,
+                        state => {
+                            error!("In unexpected state while bet of {} received: {:?}: {:?}", chips, state, self);
+                            state
+                        }
+                    };
+                } else {
+                    *self.bets.entry(username).or_default() += chips;
+                    *self.stacks.entry(username).or_default() -= chips;
+                    self.active = self.seats.player_after_in(username, &self.players);
+                }
+                Ok(())
+            }
+            (_, Action::Fold) => {
+                self.pot += *self.bets.entry(username).or_default();
+                self.bets.remove(&username);
+                self.players.remove(&username);
+                self.active = self.seats.player_after_in(username, &self.players);
+                Ok(())
+            }
+            (State::DealingFlop, Action::CommunityCard{card}) => {
+                self.community.insert(card);
+                if self.community.len() == 3 {
+                    self.active = self.seats.player_after_in(username, &self.players);
+                    self.state = State::PostFlopBetting;
+                }
+                Ok(())
+            }
+            (State::DealingTurn, Action::CommunityCard{card}) => {
+                self.community.insert(card);
+                self.active = self.seats.player_after_in(username, &self.players);
+                self.state = State::TurnBetting;
+                Ok(())
+            }
+            (State::DealingRiver, Action::CommunityCard{card}) => {
+                self.active = self.seats.player_after_in(username, &self.players);
+                self.state = State::RiverBetting;
+                Ok(())
+            }
+            (_, Action::WinHand{chips}) if chips <= self.pot => {
+                self.pot -= chips;
+                *self.stacks.entry(username).or_default() += chips;
+                Ok(())
+            }
+            (_, Action::WinGame) => {
+                self.state = State::Completed;
+                Ok(())
+            }
+            (State::Completed, _) => Err(ActionError::GameHasEnded),
+            (_, _) => Err(ActionError::InvalidActionForGameType),
+        }
+    }
+
+    fn next_dealer_action(&self) -> Option<ValidatedUserAction> {
+        let mut rng = thread_rng();
+        match self.state {
+            State::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
+            }
+            State::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 {
+                    None
+                }
+            }
+            State::PostingSmallBlind => {
+                self.dealer.and_then(|dealer| self.seats.player_after(dealer))
+                    .map(|username| {
+                        let chips = self.stack(username).min(self.small_blind);
+                        ValidatedUserAction(UserAction{username, action: Action::Bet{chips}})
+                    })
+            }
+            State::PostingBigBlind => {
+                self.dealer.and_then(|dealer| self.seats.player_after(dealer))
+                    .and_then(|small_blind| self.seats.player_after(small_blind))
+                    .map(|username| {
+                        let chips = self.stack(username).min(self.small_blind);
+                        ValidatedUserAction(UserAction{username, action: Action::Bet{chips}})
+                    })
+            }
+            State::PreFlopBetting | State::PostFlopBetting | State::TurnBetting | State::RiverBetting => {
+                if self.players.len() == 1 {
+                    if self.pot > 0 {
+                        self.players.iter().next()
+                            .map(|&username| ValidatedUserAction(UserAction{username, action: Action::WinHand{chips: self.pot}}))
+                    } else if self.seats.players_len() == 1 {
+                        self.players.iter().next()
+                            .map(|&username| ValidatedUserAction(UserAction{username, action: Action::WinGame}))
+                    } else {
+                        self.stacks.iter().find(|&(_, &stack)| stack == 0)
+                            .map(|(&username, _)| ValidatedUserAction(UserAction{username, action: Action::KnockedOut}))
+                    }
+                } else {
+                    None
+                }
+            }
+            State::DealingFlop | State::DealingTurn | State::DealingRiver => {
+                self.dealer.and_then(|username|
+                    self.deck.iter().choose(&mut rng).map(|&card|
+                        ValidatedUserAction(UserAction{username, action: Action::CommunityCard{card}})))
+            }
+            State::Showdown => {
+                todo!()
+            }
+            State::Completed => None,
+        }
+    }
+}
diff --git a/src/game/poker/mod.rs b/src/game/poker/mod.rs
new file mode 100644 (file)
index 0000000..49d96a6
--- /dev/null
@@ -0,0 +1,4 @@
+mod classify;
+mod holdem;
+
+pub use self::holdem::{TexasHoldEm, TexasHoldEmSettings};
index 411fee797f4d0ac54c34891caca904c3bb033243..9d0017371d869fb24f4d4810e9c63b280359f8aa 100644 (file)
@@ -10,7 +10,7 @@ use crate::username::Username;
 use super::{Action, ActionError, Game, UserAction, ValidatedUserAction};
 
 #[derive(Copy, Clone, Debug)]
-enum KnockOutWhistState {
+enum State {
     NotStarted,
     Dealing,
     ChoosingTrumps,
@@ -31,7 +31,7 @@ pub struct KnockOutWhist {
     id: i64,
     settings: KnockOutWhistSettings,
     actions_len: usize,
-    state: KnockOutWhistState,
+    state: State,
     seats: Seats,
     dealer: Option<Username>,
     receiver: Option<Username>,
@@ -54,7 +54,7 @@ impl KnockOutWhist {
             id,
             settings,
             actions_len: 0,
-            state: KnockOutWhistState::NotStarted,
+            state: State::NotStarted,
             seats: Seats::new(),
             dealer: None,
             receiver: None,
@@ -118,7 +118,7 @@ impl Game for KnockOutWhist {
             (_, Action::AddOn{..}) | (_, Action::RevealCard{..}) | (_, Action::Fold) | (_, Action::Bet{..}) => {
                 Err(ActionError::InvalidActionForGameType)
             }
-            (KnockOutWhistState::NotStarted, Action::Join{seat, ..}) => {
+            (State::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 {
@@ -129,23 +129,24 @@ impl Game for KnockOutWhist {
                     Ok(ValidatedUserAction(UserAction{username, action: Action::Join{seat, chips: 0}}))
                 }
             }
-            (KnockOutWhistState::Completed, Action::Join{..}) => Err(ActionError::GameHasEnded),
+            (State::Completed, Action::Join{..}) => Err(ActionError::GameHasEnded),
             (_, Action::Join{..}) => Err(ActionError::GameHasStarted),
             (_, _) if !self.seats.contains_player(username) => Err(ActionError::NotAuthorised),
-            (KnockOutWhistState::NotStarted, Action::Leave) => {
+            (State::NotStarted, Action::Leave) => {
                 Ok(ValidatedUserAction(UserAction{username, action: Action::Leave}))
             }
+            (State::Completed, Action::Leave) => Err(ActionError::GameHasEnded),
             (_, Action::Leave) => Err(ActionError::GameHasStarted),
-            (KnockOutWhistState::Dealing, _) => Err(ActionError::Dealing),
-            (KnockOutWhistState::ChoosingTrumps, Action::ChooseTrumps{suit}) => {
+            (State::Dealing, _) => Err(ActionError::Dealing),
+            (State::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::MustChooseTrumps),
-            (KnockOutWhistState::Playing, Action::PlayCard{card}) => {
+            (State::ChoosingTrumps, _) => Err(ActionError::MustChooseTrumps),
+            (State::Playing, Action::PlayCard{card}) => {
                 if Some(username) != self.active {
                     Err(ActionError::OutOfTurn)
                 } else if !self.hand_contains_card(username, card) {
@@ -156,9 +157,9 @@ impl Game for KnockOutWhist {
                     Ok(ValidatedUserAction(UserAction{username, action: Action::PlayCard{card}}))
                 }
             }
-            (KnockOutWhistState::CutForCall, _) => Err(ActionError::Dealing),
-            (KnockOutWhistState::RoundCompleted, _) => Err(ActionError::Dealing),
-            (KnockOutWhistState::Completed, _) => Err(ActionError::GameHasEnded),
+            (State::CutForCall, _) => Err(ActionError::Dealing),
+            (State::RoundCompleted, _) => Err(ActionError::Dealing),
+            (State::Completed, _) => Err(ActionError::GameHasEnded),
             (_, _) => Err(ActionError::InvalidActionForGameType),
         }
     }
@@ -169,10 +170,10 @@ impl Game for KnockOutWhist {
             (_, Action::AddOn{..}) | (_, Action::Fold) | (_, Action::Bet{..}) => {
                 Err(ActionError::InvalidActionForGameType)
             }
-            (KnockOutWhistState::NotStarted, Action::Join{seat, ..}) => {
+            (State::NotStarted, Action::Join{seat, ..}) => {
                 self.seats.add_player(seat, username)
             }
-            (KnockOutWhistState::NotStarted, Action::Leave) => {
+            (State::NotStarted, Action::Leave) => {
                 self.seats.remove_player(username)
             }
             (_, Action::NextToDeal) => {
@@ -184,10 +185,10 @@ impl Game for KnockOutWhist {
                 self.trumps = None;
                 self.tricks_won.clear();
                 self.winners.clear();
-                self.state = KnockOutWhistState::Dealing;
+                self.state = State::Dealing;
                 Ok(())
             }
-            (KnockOutWhistState::Dealing, Action::ReceiveCard{card: Some(card)}) => {
+            (State::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) {
@@ -197,30 +198,30 @@ impl Game for KnockOutWhist {
                 }
                 Ok(())
             }
-            (KnockOutWhistState::Dealing, Action::CommunityCard{card}) => {
+            (State::Dealing, Action::CommunityCard{card}) => {
                 self.trump_card = Some(card);
                 self.trumps = Some(card.suit);
                 Ok(())
             }
-            (KnockOutWhistState::Dealing, Action::EndDeal) => {
+            (State::Dealing, Action::EndDeal) => {
                 match self.trump_card {
                     Some(_) => {
-                        self.state = KnockOutWhistState::Playing;
+                        self.state = State::Playing;
                         self.receiver = None;
                         self.active = self.dealer.and_then(|dealer| self.seats.player_after(dealer));
                     }
-                    None => self.state = KnockOutWhistState::ChoosingTrumps,
+                    None => self.state = State::ChoosingTrumps,
                 }
                 Ok(())
             }
-            (KnockOutWhistState::ChoosingTrumps, Action::ChooseTrumps{suit}) => {
+            (State::ChoosingTrumps, Action::ChooseTrumps{suit}) => {
                 self.trumps = Some(suit);
                 self.call = None;
-                self.state = KnockOutWhistState::Playing;
+                self.state = State::Playing;
                 self.active = self.dealer.and_then(|dealer| self.seats.player_after(dealer));
                 Ok(())
             }
-            (KnockOutWhistState::Playing, Action::PlayCard{card}) => {
+            (State::Playing, Action::PlayCard{card}) => {
                 if let Some(hand) = self.hands.get_mut(&username) {
                     hand.remove(&card);
                 }
@@ -235,7 +236,7 @@ impl Game for KnockOutWhist {
                 }
                 Ok(())
             }
-            (KnockOutWhistState::Playing, Action::WinTrick) => {
+            (State::Playing, Action::WinTrick) => {
                 *self.tricks_won.entry(username).or_default() += 1;
                 self.led = None;
                 self.trick.clear();
@@ -250,7 +251,7 @@ impl Game for KnockOutWhist {
                             self.deck = FIFTY_TWO_CARD_DECK.iter().cloned().collect();
                             self.hands.clear();
                             self.trump_card = None;
-                            self.state = KnockOutWhistState::CutForCall;
+                            self.state = State::CutForCall;
                         }
                     }
                     self.active = None;
@@ -259,16 +260,16 @@ impl Game for KnockOutWhist {
                 }
                 Ok(())
             }
-            (KnockOutWhistState::Playing, Action::KnockedOut) => {
+            (State::Playing, Action::KnockedOut) => {
                 self.seats.remove_player(username)
             }
-            (KnockOutWhistState::Playing, Action::WinGame) => {
+            (State::Playing, Action::WinGame) => {
                 self.winners.clear();
                 self.winners.insert(username);
-                self.state = KnockOutWhistState::Completed;
+                self.state = State::Completed;
                 Ok(())
             }
-            (KnockOutWhistState::CutForCall, Action::RevealCard{card}) => {
+            (State::CutForCall, Action::RevealCard{card}) => {
                 self.deck.remove(&card);
                 self.hands.entry(username).or_default().insert(card);
                 if self.hands.values().map(HashSet::len).sum::<usize>() == self.winners.len() {
@@ -291,12 +292,12 @@ impl Game for KnockOutWhist {
                 }
                 Ok(())
             }
-            (KnockOutWhistState::CutForCall, Action::WinCall) | (KnockOutWhistState::Playing, Action::WinCall) => {
+            (State::CutForCall, Action::WinCall) | (State::Playing, Action::WinCall) => {
                 self.call = Some(username);
-                self.state = KnockOutWhistState::RoundCompleted;
+                self.state = State::RoundCompleted;
                 Ok(())
             }
-            (KnockOutWhistState::Completed, _) => Err(ActionError::GameHasEnded),
+            (State::Completed, _) => Err(ActionError::GameHasEnded),
             (_, _) => Err(ActionError::InvalidActionForGameType),
         }
     }
@@ -304,7 +305,7 @@ impl Game for KnockOutWhist {
     fn next_dealer_action(&self) -> Option<ValidatedUserAction> {
         let mut rng = thread_rng();
         match self.state {
-            KnockOutWhistState::NotStarted => {
+            State::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}));
@@ -312,7 +313,7 @@ impl Game for KnockOutWhist {
                 }
                 None
             }
-            KnockOutWhistState::Dealing => {
+            State::Dealing => {
                 if let Some(username) = self.receiver {
                     let card = self.deck.iter().choose(&mut rng).cloned();
                     Some(ValidatedUserAction(UserAction{username, action: Action::ReceiveCard{card}}))
@@ -331,10 +332,10 @@ impl Game for KnockOutWhist {
                     None
                 }
             }
-            KnockOutWhistState::ChoosingTrumps => {
+            State::ChoosingTrumps => {
                 None
             }
-            KnockOutWhistState::Playing => {
+            State::Playing => {
                 if !self.winners.is_empty() {
                     for username in self.seats.player_set() {
                         if matches!(self.tricks_won.get(&username), Some(0) | None) {
@@ -356,7 +357,7 @@ impl Game for KnockOutWhist {
                     None
                 }
             }
-            KnockOutWhistState::CutForCall => {
+            State::CutForCall => {
                 if let Some(username) = self.receiver {
                     if let Some(card) = self.deck.iter().choose(&mut rng).cloned() {
                         Some(ValidatedUserAction(UserAction{username, action: Action::RevealCard{card}}))
@@ -369,14 +370,14 @@ impl Game for KnockOutWhist {
                     None
                 }
             }
-            KnockOutWhistState::RoundCompleted => {
+            State::RoundCompleted => {
                 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 => {
+            State::Completed => {
                 None
             }
         }