"""
deck of cards submodule
"""
import random
from collections import Counter
from dataclasses import dataclass
from typing import Any
from gamble.errors import InvalidCard
[docs]
@dataclass
class Suit:
"""
suit
Args:
name: the name of this suit
char: the ascii character representation for this suit
symbol: the unicode symbol char for this suit
value: the value of the suit
color: the color of the suit
unicode: the unicode id for this suit as an int
"""
name: str
char: str
symbol: str
value: int
color: int
unicode: int
[docs]
@dataclass
class Value:
"""
value
Args:
char: the ascii character representation for this value
name: the name of the card
value: the value of the card as an int
"""
char: str
name: str
value: int
[docs]
@dataclass
class Rank:
"""
hand ranks
Args:
value: the integer value of the rank
name: the name of the rank
"""
value: int
name: str
[docs]
class Card:
"""
playing card model
Args:
value: the value of the card to create
suit: the suit of the card to create
"""
BLACK = 0
RED = 1
[docs]
class Suits:
"""
card suit enum
"""
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)
[docs]
@classmethod
def all(cls) -> list[Suit]:
"""
get all suits in this enum
Returns:
a list of the suit objects
"""
return sorted(
[
cls.__dict__[x]
for x in dir(cls)
if not x.startswith("_") and isinstance(cls.__dict__[x], Suit)
],
key=lambda x: x.value,
)
[docs]
@classmethod
def dict(cls) -> dict[str, Suit]:
"""
dict of char -> Suit
Returns:
a dictionary of all the card values
"""
return {x.char: x for x in cls.all()}
[docs]
class Values:
"""
card value enum
"""
ACE = Value(char="A", name="ace", value=1)
TWO = Value(char="2", name="two", value=2)
THREE = Value(char="3", name="three", value=3)
FOUR = Value(char="4", name="four", value=4)
FIVE = Value(char="5", name="five", value=5)
SIX = Value(char="6", name="six", value=6)
SEVEN = Value(char="7", name="seven", value=7)
EIGHT = Value(char="8", name="eight", value=8)
NINE = Value(char="9", name="nine", value=9)
TEN = Value(char="T", name="ten", value=10)
JACK = Value(char="J", name="jack", value=11)
QUEEN = Value(char="Q", name="queen", value=12)
KING = Value(char="K", name="king", value=13)
[docs]
@classmethod
def all(cls) -> list[Value]:
"""
get all suits
Returns:
a list of all the values in this enum
"""
return sorted(
[
cls.__dict__[x]
for x in dir(cls)
if not x.startswith("_") and isinstance(cls.__dict__[x], Value)
],
key=lambda x: x.value,
)
[docs]
@classmethod
def dict(cls) -> dict[str, Value]:
"""
dict of char -> Value
Returns:
a dictionary of card characters to their Value representations
"""
return {x.char: x for x in cls.all()}
def __init__(self, value: Value = Values.ACE, suit: Suit = Suits.SPADES) -> None:
self.value = value
self.suit = suit
[docs]
@classmethod
def get(cls, text: str) -> "Card":
"""
get a card by text representation
Args:
text: a string representation of a card value and suit
Returns:
the created card, if the string was valid
"""
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
Returns:
the color enum int for this card
"""
return self.suit.color
@property
def full_name(self) -> str:
"""
returns the full name for this card
Returns:
the full name of this card
"""
return f"{self.value.name} of {self.suit.name}"
@property
def is_black(self) -> bool:
"""
is_black property
Returns:
if this card is in a black suit
"""
return self.color == Card.BLACK
@property
def is_red(self) -> bool:
"""
is_red property
Returns:
if this card is in a red suit
"""
return self.color == Card.RED
@property
def unicode(self) -> str:
"""
get the fun little unicode card for this card
Returns:
the nice looking unicode char for the suit of this card
"""
# we need to skip the 'knight' card if we're a queen or king
hack = int(self.value.value >= 12)
return chr(self.suit.unicode + self.value.value + hack)
[docs]
def __str__(self) -> str:
"""
string representation of this card
Returns:
a string of this card
"""
return f"{self.value.char}{self.suit.symbol}"
[docs]
def __repr__(self) -> str:
"""
representation of this card
Returns:
a repr of this card
"""
return f"<Card:{self}>"
[docs]
def __lt__(self, other: "Card") -> bool:
"""
less than dunder method
Args:
other: another card to compare against
Returns:
true if this card is less than other
"""
return self.value.value < other.value.value
[docs]
def __gt__(self, other: "Card") -> bool:
"""
greater than dunder method
Args:
other: another card to compare against
Returns:
true if this card is greater than other
"""
return self.value.value > other.value.value
[docs]
def __le__(self, other: "Card") -> bool:
"""
less than or equal to dunder method
Args:
other: another card to compare against
Returns:
true if less than or equal to other
"""
return self < other or self == other
[docs]
def __ge__(self, other: "Card") -> bool:
"""
greater than or equal to dunder method
Args:
other: another card to compare against
Returns:
true if this card is greater than or equal to other
"""
return self > other or self == other
[docs]
def __eq__(self, other: object) -> bool:
"""
equal to dunder method
Args:
other: another card to compare against
Returns:
true if this card is the same as other
"""
if not isinstance(other, Card):
return False
return self.suit == other.suit and self.value == other.value
[docs]
class Hand:
"""
playing card hand model
Args:
cards: a list of card objects for this hand
"""
[docs]
class Ranks:
"""
hand ranks for poker
"""
ROYAL_FLUSH = Rank(value=9, name="royal flush")
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:
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])
[docs]
def __lt__(self, other: "Hand") -> bool:
"""
less than dunder method
Args:
other: another hand to compare against
Returns:
true if this hand is less than the other
"""
return self.rank.value < other.rank.value
[docs]
def __gt__(self, other: "Hand") -> bool:
"""
greater than dunder method
Args:
other: another hand to compare against
Returns:
true if this hand is greater than the other
"""
return self.rank.value > other.rank.value
[docs]
def __len__(self) -> int:
"""
dunder len method
Returns:
the number of cards in this hand
"""
return len(self.cards)
[docs]
def __str__(self) -> str:
"""
string representation of the hand
Returns:
this hand as a string
"""
return f"[{', '.join([str(x) for x in self.cards])}]"
[docs]
def __repr__(self) -> str:
"""
repr of the hand
Returns:
this hand as repr
"""
return f"<Hand[{self.size}]({self.rank.name}) {self}>"
[docs]
@classmethod
def get(cls, text: str) -> "Hand":
"""
get a hand by text representations
Args:
text: a text representation of a hand
Returns:
a hand, if the string was valid
"""
card_strings = text.replace(" ", "").upper().split(",")
cards = [Card.get(x) for x in card_strings]
return cls(cards=cards)
@property
def rank(self) -> Rank: # noqa: PLR0911
"""
get the rank of this hand
Returns:
a rank object representing the rank of this hand
"""
if self.is_royal_flush:
return Hand.Ranks.ROYAL_FLUSH
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
Returns:
a sorted list of all cards in this hand
"""
return sorted(self.value_counts.values(), reverse=True)
@property
def is_royal_flush(self) -> bool:
"""
check if the hand is a royal flush
Returns:
true if royal flush
"""
return (
self.is_flush
and self.is_straight
and self.cards[0].value == Card.Values.ACE
and self.cards[-1].value == Card.Values.KING
)
@property
def is_straight_flush(self) -> bool:
"""
check if the hand is a straight flush
Returns:
true if straight flush
"""
return self.is_flush and self.is_straight and not self.is_royal_flush
@property
def is_four_of_a_kind(self) -> bool:
"""
check if the hand is four of a kind
Returns:
true if four of a kind
"""
return self._vals[0] == 4
@property
def is_full_house(self) -> bool:
"""
check if the hand is a full house
Returns:
true if full house
"""
return self._vals[0:2] == [3, 2]
@property
def is_flush(self) -> bool:
"""
check if the hand is a flush
Returns:
true if flush
"""
return len({x.suit.value for x in self.cards}) == 1
@property
def is_straight(self) -> bool:
"""
check if the hand is a straight
Returns:
true if straight
"""
def check(value_set: set) -> bool:
"""
check if the given set is a straight
Args:
value_set: the set to check for a straight
Returns:
true if this 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 = {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
Returns:
true if is three of a kind
"""
return self._vals[0] == 3
@property
def is_two_pair(self) -> bool:
"""
check if the hand contains two pair
Returns:
true if is two pair
"""
return self._vals[0:2] == [2, 2]
@property
def is_one_pair(self) -> bool:
"""
check if the hand contains one pair
Returns:
true if is one pair
"""
return self._vals[0] == 2
[docs]
class Deck:
"""
playing card deck model
Args:
cards: a list of cards for this deck
shuffle: if we should start with the deck shuffled
"""
def __init__(
self, cards: list[Card] | None = None, shuffle: bool = True, default_draw_count: int = 1
) -> None:
if cards:
self.cards = cards
else:
# lets start with a default deck of 52
self.cards = []
self.default_deck(self.cards)
self.cards.reverse()
self.shuffles = 0
self.draws = 0
self.default_draw_count = default_draw_count
if shuffle:
self.shuffle()
[docs]
def __contains__(self, item: object) -> bool:
"""
dunder contains method
Args:
item: the item to check for in this deck
Returns:
true if this deck contains the given object
"""
if not isinstance(item, Card):
return False
return item in self.cards
[docs]
def __str__(self) -> str:
"""
string representation of a deck
Returns:
a string representation of this deck
"""
return f"<Deck[{self.cards_left}]>"
[docs]
def __repr__(self) -> str:
"""
term representation of a deck
Returns:
a repr representation of this deck
"""
return str(self)
[docs]
def __getitem__(self, index: int) -> Card:
"""
get the card at the given index in the deck
Returns:
the card at the given index
"""
return self.cards[index]
[docs]
def clear(self) -> None:
"""
clear the deck of all cards
"""
self.cards[:] = []
[docs]
def default_deck(self, cards: list[Card]) -> None:
"""
load the standard 52 cards into the given set of cards
"""
for suit in Card.Suits.all():
for value in Card.Values.all():
cards.append(Card(value=value, suit=suit))
@property
def top(self) -> Card:
"""
the top card of the deck
Returns:
a card off the top of the deck
"""
return self.cards[-1]
@property
def bottom(self) -> Card:
"""
the bottom card of the deck
Returns:
a card off the bottom of the deck
"""
return self.cards[0]
@property
def cards_left(self) -> int:
"""
number of cards left in the deck
Returns:
the number of cards left
"""
return len(self.cards)
[docs]
def draw(self, times: int = -1) -> Card | list[Card]:
"""
draws the given number of cards from the deck
Args:
times: the number of times to draw
Returns:
a card or list of cards drawn
"""
if times == -1:
times = self.default_draw_count
if times == 1:
self.draws += 1
return self.cards.pop()
cards = []
for _ in range(times):
self.draws += 1
cards.append(self.cards.pop())
return cards
[docs]
def draw_hand(self, size: int = 5) -> Hand:
"""
draw a hand from this deck
Args:
size: the size of hand to draw
Returns:
a hand object of size size
"""
cards = self.draw(times=size)
return Hand(cards=cards if isinstance(cards, list) else [cards])
[docs]
def shuffle(self, times: int = 1) -> None:
"""
shuffle the deck
Args:
times: the number of times to shuffle the deck
"""
for _ in range(times):
self.shuffles += 1
random.shuffle(self.cards)
[docs]
class EuchreDeck(Deck):
"""
deck specifically for euchre
"""
def __init__(self, **_: Any) -> None:
cards: list[Card] = []
# euchre uses 9, 10, J, Q, K, A of all suits
values = [x for x in Card.Values.all() if x.value >= 9 or x.value == 1]
for suit in Card.Suits.all():
for value in values:
cards.append(Card(value=value, suit=suit))
cards.reverse()
super().__init__(cards=cards)
[docs]
class MultiDeck(Deck):
"""
deck consisting of multiple standard decks
Args:
num_decks: the number of standard decks to combine into one deck
"""
def __init__(self, num_decks: int = 2) -> None:
cards: list[Card] = []
for _ in range(num_decks):
self.default_deck(cards)
super().__init__(cards=cards)
[docs]
class BlackJackDeck(MultiDeck):
"""
a standard blackjack shoe
Args:
num_decks: the number of standard decks to combine into this blackjack shoe
"""
def __init__(self, num_decks: int = 8) -> None:
super().__init__(num_decks=num_decks)