aboutsummaryrefslogtreecommitdiff
path: root/poker
diff options
context:
space:
mode:
authorPaul Harrison <paul@harrison.sh>2022-11-18 21:00:37 +0000
committerPaul Harrison <paul@harrison.sh>2022-12-15 16:02:14 +0000
commitd924e39608861362e08c5e3706ff46a7a1af919b (patch)
tree93a3db318a4f46fed2a545afea8474f7193d7308 /poker
parent5393e7b79939f7da36d55570af3374cef2db2d51 (diff)
feat: Get hand rank
Method to get rank of a given hand.
Diffstat (limited to 'poker')
-rw-r--r--poker/constants.py25
-rw-r--r--poker/models.py7
-rw-r--r--poker/rank/__init__.py0
-rw-r--r--poker/rank/descriptions.py0
-rw-r--r--poker/rank/hands.py110
5 files changed, 141 insertions, 1 deletions
diff --git a/poker/constants.py b/poker/constants.py
index 04a8b34..e0a7348 100644
--- a/poker/constants.py
+++ b/poker/constants.py
@@ -3,6 +3,21 @@ from enum import IntEnum, auto
from poker.utils.enum import AutoName
+class Rank(IntEnum):
+ """Poker rank enum."""
+
+ ROYAL_FLUSH = 1
+ STRAIGHT_FLUSH = 2
+ FOUR_OF_A_KIND = 3
+ FULL_HOUSE = 4
+ FLUSH = 5
+ STRAIGHT = 6
+ THREE_OF_A_KIND = 7
+ TWO_PAIR = 8
+ PAIR = 9
+ HIGH_CARD = 10
+
+
class Suit(AutoName):
"""Card suit enum."""
@@ -15,7 +30,6 @@ class Suit(AutoName):
class Value(IntEnum):
"""Card value enum."""
- ACE = 1
TWO = 2
THREE = 3
FOUR = 4
@@ -28,3 +42,12 @@ class Value(IntEnum):
JACK = 11
QUEEN = 12
KING = 13
+ ACE = 14
+
+ def __str__(self) -> str:
+ """Return string representation."""
+ if self.value in [1, 11, 12, 13]:
+ out: str = self.name.lower()
+ else:
+ out = str(self.value)
+ return out
diff --git a/poker/models.py b/poker/models.py
index 1d6af05..bbb474c 100644
--- a/poker/models.py
+++ b/poker/models.py
@@ -1,5 +1,7 @@
from __future__ import annotations
+from collections import Counter
+
from pydantic import BaseModel, validator
from poker.constants import Rank, Suit, Value
@@ -43,6 +45,11 @@ class Hand(BaseModel):
raise ValueError("Hand must have five cards.")
return cards
+ @property
+ def value_counts(self) -> list[tuple[Value, int]]:
+ """Return count of each card value in hand."""
+ return Counter([card.value for card in self.cards]).most_common()
+
class RankedHand(Hand):
"""Ranked hand domain model class."""
diff --git a/poker/rank/__init__.py b/poker/rank/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/poker/rank/__init__.py
diff --git a/poker/rank/descriptions.py b/poker/rank/descriptions.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/poker/rank/descriptions.py
diff --git a/poker/rank/hands.py b/poker/rank/hands.py
new file mode 100644
index 0000000..8764ce9
--- /dev/null
+++ b/poker/rank/hands.py
@@ -0,0 +1,110 @@
+from poker.constants import Rank, Value
+from poker.models import Card, Hand
+
+
+def _is_flush(cards: list[Card]) -> bool:
+ """Calculate whether hand is a flush."""
+ return len(set(card.suit for card in cards)) == 1
+
+
+def _is_straight(cards: list[Card]) -> bool:
+ """Calculate whether hand is a straight."""
+ card_values = sorted([card.value for card in cards])
+
+ # If card values are not unique it is not a straight
+ if len(set(card_values)) != len(card_values):
+ return False
+
+ # Check for a ace low straight
+ if card_values == [Value.TWO, Value.THREE, Value.FOUR, Value.FIVE, Value.ACE]:
+ return True
+
+ # If all card differences are 1 it is a straight
+ diffs = [second - first for first, second in zip(card_values[:-1], card_values[1:])]
+ if set(diffs) == {1}:
+ return True
+
+ return False
+
+
+def is_royal_flush(hand: Hand) -> bool:
+ """Calculate whether hand a royal flush."""
+ if not _is_flush(hand.cards):
+ return False
+
+ values = [card.value for card in hand.cards]
+ return all(
+ value in values
+ for value in [Value.ACE, Value.KING, Value.QUEEN, Value.JACK, Value.TEN]
+ )
+
+
+def is_straight_flush(hand: Hand) -> bool:
+ """Calculate whether hand a straight flush."""
+ return _is_straight(hand.cards) and _is_flush(hand.cards)
+
+
+def is_four_of_a_kind(hand: Hand) -> bool:
+ """Calculate whether hand has four of a kind."""
+ return hand.value_counts[0][1] == 4
+
+
+def is_full_house(hand: Hand) -> bool:
+ """Calculate whether hand is a full house."""
+ return hand.value_counts[0][1] == 3 and hand.value_counts[1][1] == 2
+
+
+def is_flush(hand: Hand) -> bool:
+ """Calculate whether hand is a flush."""
+ return _is_flush(hand.cards) and not _is_straight(hand.cards)
+
+
+def is_straight(hand: Hand) -> bool:
+ """Calculate whether hand is a flush."""
+ return _is_straight(hand.cards) and not _is_flush(hand.cards)
+
+
+def is_three_of_a_kind(hand: Hand) -> bool:
+ """Calculate whether hand is a full house."""
+ return (
+ hand.value_counts[0][1] == 3
+ and hand.value_counts[1][1] == 1
+ and hand.value_counts[2][1] == 1
+ )
+
+
+def is_two_pair(hand: Hand) -> bool:
+ """Calculate whether hand is a two pair."""
+ return hand.value_counts[0][1] == 2 and hand.value_counts[1][1] == 2
+
+
+def is_pair(hand: Hand) -> bool:
+ """Calculate whether hand is a pair."""
+ return hand.value_counts[0][1] == 2 and hand.value_counts[1][1] == 1
+
+
+def get_rank(hand: Hand) -> Rank:
+ """Get hand rank.
+
+ TODO: Use a factory pattern to avoid this huge flow control.
+ """
+ if is_royal_flush(hand):
+ return Rank.ROYAL_FLUSH
+ elif is_straight_flush(hand):
+ return Rank.STRAIGHT_FLUSH
+ elif is_four_of_a_kind(hand):
+ return Rank.FOUR_OF_A_KIND
+ elif is_full_house(hand):
+ return Rank.FULL_HOUSE
+ elif is_flush(hand):
+ return Rank.FLUSH
+ elif is_straight(hand):
+ return Rank.STRAIGHT
+ elif is_three_of_a_kind(hand):
+ return Rank.THREE_OF_A_KIND
+ elif is_two_pair(hand):
+ return Rank.TWO_PAIR
+ elif is_pair(hand):
+ return Rank.PAIR
+ else:
+ return Rank.HIGH_CARD