from __future__ import annotations
import math
from typing import Union, Iterable, List
[docs]
class Card(int):
"""
Usage :code:`Card("Kd")`
We represent :class:`~texasholdem.card.card.Card` objects as native Python 32-bit
integers. Most of the bits are used, and have a specific meaning. See below:
.. table:: Card
:align: center
:widths: auto
======== ======== ======== ========
xxxbbbbb bbbbbbbb cdhsrrrr xxpppppp
======== ======== ======== ========
- p = prime number of rank (in binary) (deuce=2, trey=3, four=5, ..., ace=41)
- r = rank of card (in binary) (deuce=0, trey=1, four=2, five=3, ..., ace=12)
- cdhs = suit of card (bit turned on based on suit of card)
- b = bit turned on depending on rank of card (deuce=1st bit, trey=2nd bit, ...)
- x = unused
**Example**
.. table::
:align: center
:widths: auto
================ ======== ======== ======== ========
Card xxxAKQJT 98765432 CDHSrrrr xxPPPPPP
================ ======== ======== ======== ========
King of Diamonds 00001000 00000000 01001011 00100101
Five of Spades 00000000 00001000 00010011 00000111
Jack of Clubs 00000010 00000000 10001001 00011101
================ ======== ======== ======== ========
This representation allows for minimal memory overhead along with fast applications
necessary for poker:
- Make a unique prime product for each hand (by multiplying the prime bits)
- Detect flushes (bitwise && for the suits)
- Detect straights (shift and bitwise &&)
Args:
arg (str | int): A string of the form "{rank}{suit}" e.g. "Kd" or "As" or a properly-formed
Card-int as described above.
"""
# the basics
STR_RANKS = "23456789TJQKA"
INT_RANKS = tuple(range(13))
PRIMES = (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41)
# conversion from string => int
CHAR_RANK_TO_INT_RANK = dict(zip(list(STR_RANKS), INT_RANKS))
CHAR_SUIT_TO_INT_SUIT = {
"s": 1, # spades
"h": 2, # hearts
"d": 4, # diamonds
"c": 8, # clubs
}
INT_SUIT_TO_CHAR_SUIT = "xshxdxxxc"
# for pretty printing
PRETTY_SUITS = {
1: "\u2660", # spades
2: "\u2665", # hearts
4: "\u2666", # diamonds
8: "\u2663", # clubs
}
def __new__(cls, arg: Union[str, int]) -> Card:
if isinstance(arg, str):
return Card.from_string(arg)
return Card.from_int(arg)
[docs]
@classmethod
def from_string(cls, string: str) -> Card:
"""
Converts Card string to binary integer representation of card, inspired by:
http://www.suffecool.net/poker/evaluator.html
Example:
"Kd" --> int(0b 00001000 00000000 01001011 00100101) = 134236965
Args:
string (str): A string representing a card.
Returns:
Card: The 32-bit int representing the card as described above
"""
rank_char = string[0]
suit_char = string[1]
rank_int = Card.CHAR_RANK_TO_INT_RANK[rank_char]
suit_int = Card.CHAR_SUIT_TO_INT_SUIT[suit_char]
rank_prime = Card.PRIMES[rank_int]
bitrank = 1 << rank_int << 16
suit = suit_int << 12
rank = rank_int << 8
card_int = bitrank | suit | rank | rank_prime
return Card.from_int(card_int)
[docs]
@classmethod
def from_int(cls, card_int: int) -> Card:
"""
Converts an already well-formed card integer as described above
Example:
134236965 --> Card("Kd")
Args:
card_int (int): An int representing a card.
Returns:
Card: The 32-bit int representing the card as described above
"""
return super(Card, cls).__new__(cls, card_int)
def __str__(self) -> str:
"""
Translates card into a readable string.
Example:
134236965 --> "Kd"
Returns:
str: The human-readable string representing this card.
"""
return Card.STR_RANKS[self.rank] + Card.INT_SUIT_TO_CHAR_SUIT[self.suit]
def __repr__(self) -> str:
return f'Card("{str(self)}")'
@property
def rank(self) -> int:
"""
The rank of the card as an int.
Example:
134236965 ("Kd") --> 11
268440327 ("As") --> 12
Returns:
int: Number between 0-12, representing the rank of the card.
"""
return (self >> 8) & 0xF
@property
def suit(self) -> int:
"""
The suit int of the card using the following table:
.. table::
:align: center
:widths: auto
======== ======
Suit Number
======== ======
Spades 1
Hearts 2
Diamonds 4
Clubs 8
======== ======
Example:
134236965 ("Kd") --> 2
Returns:
int: 1,2,4, or 8, representing the suit of the card from the above table.
"""
return (self >> 12) & 0xF
@property
def bitrank(self) -> int:
"""
The bitrank of the card. This returns 2^k where k is the
rank of the card.
Example:
134236965 ("Kd") --> 2^11
Returns:
int: 2^k where k is the rank of the card.
"""
return (self >> 16) & 0x1FFF
@property
def prime(self) -> int:
"""
The prime associated with the card. This returns the kth prime
starting at 2 where k is the rank of the card.
Example:
134236965 ("Kd") --> 37
"""
return self & 0x3F
@property
def pretty_string(self) -> str:
"""
A human-readable pretty string with ascii suits.
"""
return f"[ {Card.STR_RANKS[self.rank]} {Card.PRETTY_SUITS[self.suit]} ]"
@property
def binary_string(self) -> str:
"""
For debugging purposes. Displays the binary number as a
human readable string in groups of four digits.
"""
bstr = bin(self)[2:][::-1] # chop off the 0b and THEN reverse string
output = list("".join(["0000" + "\t"] * 7) + "0000")
for i, b_char in enumerate(bstr, 0):
output[i + int(i / 4)] = b_char
# output the string to console
output.reverse()
return "".join(output)
[docs]
def card_strings_to_int(card_strs: Iterable[str]) -> List[Card]:
"""
Args:
card_strs (Iterable[str]): An iterable of card strings.
Returns:
List[Card]: The cards in the corresponding int format.
"""
bhand = []
for card_str in card_strs:
bhand.append(Card(card_str))
return bhand
[docs]
def prime_product_from_hand(cards: Iterable[Card]) -> int:
"""
Args:
cards (Iterable[Card]): An Iterable of cards
Returns:
int: The product of all primes in the hand, corresponding to the rank of the
card (See :meth:`Card.prime`)
"""
return math.prod(card.prime for card in cards)
[docs]
def prime_product_from_rankbits(rankbits: int) -> int:
"""
Returns the prime product using the bitrank (b)
bits of the hand. Each 1 in the sequence is converted
to the correct prime and multiplied in.
Primarily used for evaulating flushes and straights,
two occasions where we know the ranks are *ALL* different.
Assumes that the input is in form (set bits):
.. table::
:align: center
:widths: auto
======== ========
xxxbbbbb bbbbbbbb
======== ========
Args:
rankbits (int): a single 32-bit (only 13-bits set) integer representing
the ranks of 5 *different* ranked card (5 of 13 bits are set)
Returns:
int: The product of all primes in the hand, corresponding
to the rank of the card.
"""
product = 1
for i in Card.INT_RANKS:
# if the ith bit is set
if rankbits & (1 << i):
product *= Card.PRIMES[i]
return product
[docs]
def card_list_to_pretty_str(cards: List[Card]) -> str:
"""
Prints the given card in a human-readable pretty string with
ascii suit.
Args:
cards (List[Card]): A list of card ints in the proper form.
Returns:
string: A human-readable pretty string with ascii suits.
"""
return " ".join(card.pretty_string for card in cards)