Skip to content

Commit

Permalink
adding hands, poker ranks, basic hand comparison, and writing more tests
Browse files Browse the repository at this point in the history
  • Loading branch information
jpetrucciani committed Sep 2, 2019
1 parent 8262e6c commit fd3ce29
Show file tree
Hide file tree
Showing 8 changed files with 318 additions and 11 deletions.
27 changes: 25 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ gamble
Features
--------

- die, dice
- cards, decks
- die, dice, d-notation
- cards, decks, hands
- poker ranks, hand comparison

Usage
-----
Expand Down Expand Up @@ -104,3 +105,25 @@ Cards
# the unicode cards icons are implemented as well!
card.unicode
>>> "🂡"
# draw a poker hand, default size 5
hand = deck.draw_hand(). # you can pass size=(int) to draw a different size hand
>>> <Hand[5](straight flush) [A♠, 2♠, 3♠, 4♠, 5♠]>
hand.rank
>>> Rank(name='straight flush', value=8)
# arbitrary hand, from text notation
new_hand = gamble.Hand.get("2c,3c,4c,Kc,Kh")
>>> <Hand[5](pair) [2♣, 3♣, 4♣, K♣, K♥]>
new_hand.rank
>>> Rank(name='pair', value=1)
hand > new_hand
>>> True
Todo
----
- hand equals/ge/le method
- hand ranking when hands are very similar
24 changes: 24 additions & 0 deletions gamble/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""
custom exceptions and error handling
"""


class GambleException(Exception): # pragma: no cover
"""base gamble exception class"""

def __init__(self, *args, **kwargs): # type: ignore
"""exception constructor"""
self.__dict__.update(kwargs)
extra = ""
if args:
extra = '\n| extra info: "{extra}"'.format(extra=args[0])
print(
"[{exception}]: {doc}{extra}".format(
exception=self.__class__.__name__, doc=self.__doc__, extra=extra
)
)
Exception.__init__(self, *args)


class InvalidCard(GambleException):
"""the given string is not a valid card"""
2 changes: 1 addition & 1 deletion gamble/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
models submodule for gamble
"""
from gamble.models.dice import Die, Dice # noqa
from gamble.models.cards import Card, Deck, EuchreDeck # noqa
from gamble.models.cards import Card, Deck, EuchreDeck, Hand # noqa
194 changes: 188 additions & 6 deletions gamble/models/cards.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
standard deck of cards submodule
"""
import random
from collections import Counter
from types import SimpleNamespace
from typing import Any, List, Union
from typing import Any, Dict, List, Union
from gamble.errors import InvalidCard


class Suit(SimpleNamespace):
Expand All @@ -14,6 +16,10 @@ class Value(SimpleNamespace):
"""value namespace class"""


class Rank(SimpleNamespace):
"""hand ranks namespace class"""


class Card:
"""playing card model"""

Expand All @@ -23,10 +29,18 @@ class Card:
class Suits:
"""card suit enum"""

SPADES = Suit(name="spades", char="♠", value=0, color=0, unicode=127136)
CLUBS = Suit(name="clubs", char="♣", value=1, color=0, unicode=127184)
DIAMONDS = Suit(name="diamonds", char="♦", value=2, color=1, unicode=127168)
HEARTS = Suit(name="hearts", char="♥", value=3, color=1, unicode=127152)
SPADES = Suit(
name="spades", char="S", symbol="♠", value=0, color=0, unicode=127136
)
CLUBS = Suit(
name="clubs", char="C", symbol="♣", value=1, color=0, unicode=127184
)
DIAMONDS = Suit(
name="diamonds", char="D", symbol="♦", value=2, color=1, unicode=127168
)
HEARTS = Suit(
name="hearts", char="H", symbol="♥", value=3, color=1, unicode=127152
)

@classmethod
def all(cls) -> List[Suit]:
Expand All @@ -40,6 +54,11 @@ def all(cls) -> List[Suit]:
key=lambda x: x.value,
)

@classmethod
def dict(cls) -> Dict[str, Suit]:
"""dict of char -> Suit"""
return {x.char: x for x in cls.all()}

class Values:
"""card value enum"""

Expand Down Expand Up @@ -69,11 +88,30 @@ def all(cls) -> List[Value]:
key=lambda x: x.value,
)

@classmethod
def dict(cls) -> Dict[str, Value]:
"""dict of char -> Value"""
return {x.char: x for x in cls.all()}

def __init__(self, value: Value = Values.ACE, suit: Suit = Suits.SPADES) -> None:
"""card constructor"""
self.value = value
self.suit = suit

@classmethod
def get(cls, text: str) -> "Card":
"""get a card by text representation"""
if not len(text) == 2:
raise InvalidCard("Too many characters for a card!")
vals = cls.Values.dict()
suits = cls.Suits.dict()
value_char, suit_char = list(text.upper())
if value_char not in vals:
raise InvalidCard("Invalid value for card!")
if suit_char not in suits:
raise InvalidCard("Invalid suit for card!")
return cls(value=vals[value_char], suit=suits[suit_char])

@property
def color(self) -> int:
"""returns the color of the card"""
Expand Down Expand Up @@ -103,7 +141,7 @@ def is_red(self) -> bool:

def __str__(self) -> str:
"""string representation of this card"""
return "{}{}".format(self.value.char, self.suit.char)
return "{}{}".format(self.value.char, self.suit.symbol)

def __repr__(self) -> str:
"""representation of this card"""
Expand All @@ -113,13 +151,152 @@ def __lt__(self, other: "Card") -> bool:
"""less than dunder method"""
return self.value.value < other.value.value

def __gt__(self, other: "Card") -> bool:
"""greater than dunder method"""
return self.value.value > other.value.value

def __le__(self, other: "Card") -> bool:
"""less than or equal to dunder method"""
return self < other or self == other

def __ge__(self, other: "Card") -> bool:
"""greater than or equal to dunder method"""
return self > other or self == other

def __eq__(self, other: object) -> bool:
"""equal to dunder method"""
if not isinstance(other, Card):
return False
return self.suit == other.suit and self.value == other.value


class Hand:
"""playing card hand model"""

class Ranks:
"""hand ranks for poker"""

STRAIGHT_FLUSH = Rank(value=8, name="straight flush")
FOUR_OF_A_KIND = Rank(value=7, name="four of a kind")
FULL_HOUSE = Rank(value=6, name="full house")
FLUSH = Rank(value=5, name="flush")
STRAIGHT = Rank(value=4, name="straight")
THREE_OF_A_KIND = Rank(value=3, name="three of a kind")
TWO_PAIR = Rank(value=2, name="two pair")
PAIR = Rank(value=1, name="pair")
HIGH_CARD = Rank(value=0, name="high card")

def __init__(self, cards: List[Card]) -> None:
"""hand constructor"""
self._cards = cards
self.cards = sorted(self._cards)
self.size = len(self.cards)
self.value_counts = Counter([x.value.value for x in self.cards])
self.suit_counts = Counter([x.suit.value for x in self.cards])

def __lt__(self, other: "Hand") -> bool:
"""less than dunder method"""
return self.rank.value < other.rank.value

def __gt__(self, other: "Hand") -> bool:
"""greater than dunder method"""
return self.rank.value > other.rank.value

def __len__(self) -> int:
"""dunder len method"""
return len(self.cards)

def __str__(self) -> str:
"""string representation of the hand"""
return "[{}]".format(", ".join([str(x) for x in self.cards]))

def __repr__(self) -> str:
"""repr of the hand"""
return "<Hand[{}]({}) {}>".format(self.size, self.rank.name, str(self))

@classmethod
def get(cls, text: str) -> "Hand":
"""get a hand by text representations"""
card_strings = text.replace(" ", "").upper().split(",")
cards = [Card.get(x) for x in card_strings]
return cls(cards=cards)

@property
def rank(self) -> Rank:
"""get the rank of this hand"""
if self.is_straight_flush:
return Hand.Ranks.STRAIGHT_FLUSH
if self.is_four_of_a_kind:
return Hand.Ranks.FOUR_OF_A_KIND
if self.is_full_house:
return Hand.Ranks.FULL_HOUSE
if self.is_flush:
return Hand.Ranks.FLUSH
if self.is_straight:
return Hand.Ranks.STRAIGHT
if self.is_three_of_a_kind:
return Hand.Ranks.THREE_OF_A_KIND
if self.is_two_pair:
return Hand.Ranks.TWO_PAIR
if self.is_one_pair:
return Hand.Ranks.PAIR
return Hand.Ranks.HIGH_CARD

@property
def _vals(self) -> List[int]:
"""values helper to make the following checks less verbose"""
return sorted(list(self.value_counts.values()), reverse=True)

@property
def is_straight_flush(self) -> bool:
"""check if the hand is a straight flush"""
return self.is_flush and self.is_straight

@property
def is_four_of_a_kind(self) -> bool:
"""check if the hand is four of a kind"""
return self._vals[0] == 4

@property
def is_full_house(self) -> bool:
"""check if the hand is a full house"""
return self._vals[0:2] == [3, 2]

@property
def is_flush(self) -> bool:
"""check if the hand is a flush"""
return len(set([x.suit.value for x in self.cards])) == 1

@property
def is_straight(self) -> bool:
"""check if the hand is a straight"""

def check(value_set: set) -> bool:
"""check if the given set is a straight"""
value_range = max(value_set) - min(value_set)
return (value_range == self.size - 1) and (len(value_set) == self.size)

values = [x.value.value for x in self.cards]
low_ace = set(values)
high_ace = set(x if x != 1 else 14 for x in values)
return check(low_ace) or check(high_ace)

@property
def is_three_of_a_kind(self) -> bool:
"""check if the hand is three of a kind"""
return self._vals[0] == 3

@property
def is_two_pair(self) -> bool:
"""check if the hand contains two pair"""
return self._vals[0:2] == [2, 2]

@property
def is_one_pair(self) -> bool:
"""check if the hand contains one pair"""
return self._vals[0] == 2


class Deck:
"""playing card deck model"""

Expand Down Expand Up @@ -179,6 +356,11 @@ def draw(self, times: int = 1) -> Union[Card, List[Card]]:
cards.append(self.cards.pop())
return cards

def draw_hand(self, size: int = 5) -> Hand:
"""draw a hand from this deck"""
cards = self.draw(times=size)
return Hand(cards=cards if isinstance(cards, list) else [cards])

def shuffle(self, times: int = 1) -> None:
"""shuffle the deck"""
for _ in range(times):
Expand Down
12 changes: 12 additions & 0 deletions gamble/models/dice.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ def __lt__(self, other: "Die") -> bool:
"""less than dunder method"""
return self.net_sides < other.net_sides

def __gt__(self, other: "Die") -> bool:
"""greater than dunder method"""
return self.net_sides > other.net_sides

def __le__(self, other: "Die") -> bool:
"""less than or equal to dunder method"""
return self < other or self == other

def __ge__(self, other: "Die") -> bool:
"""greater than or equal to dunder method"""
return self > other or self == other

@property
def net_sides(self) -> int:
"""the raw max sides * multiplier"""
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@


__library__ = "gamble"
__version__ = "0.0.5"
__version__ = "0.0.6"

with open("README.rst") as readme:
LONG_DESCRIPTION = readme.read()
Expand Down
Loading

0 comments on commit fd3ce29

Please sign in to comment.