Package bluff

Bluff is a pythonic poker framework.

Expand source code
""" Bluff is a pythonic poker framework. """

import itertools
import random
import re
from typing import Union, List, Iterable, Sequence, Optional

import more_itertools
import numpy as np


class NotEnoughCardsError(Exception):
    """ Raise when the deck runs out of cards. """


class SeatOccupiedError(Exception):
    """ Raise when trying to put a player in an already occupied seat. """


class Card:
    """ French-style deck card."""

    def __init__(self, abbreviation: str):
        self._rank = self._abbreviation_to_rank(abbreviation)
        self._suit = self._abbreviation_to_suit(abbreviation)
        self._numerical_rank = self._rank_to_numerical(self._rank)

    def __repr__(self):
        return self.rank + self.suit

    def __str__(self):
        return self.__repr__()

    def __eq__(self, other):
        return self.rank == other.rank and self.suit == other.suit

    @property
    def rank(self) -> str:
        """ Get the card rank. """
        return self._rank

    @property
    def suit(self) -> str:
        """ Get the card suit. """
        return self._suit

    @property
    def numerical_rank(self) -> int:
        """ Get the card numerical rank. """
        return self._numerical_rank

    @property
    def hex_rank(self) -> str:
        """ Get the card alpha numerical rank (hexadecimal) """
        return np.base_repr(self.numerical_rank, 16)

    @staticmethod
    def _abbreviation_to_rank(card_abbreviation: str) -> str:
        """ Get the rank from the card abbreviation. """
        rank = card_abbreviation[0].upper()
        valid = ["2", "3", "4", "5", "6", "7", "8", "9", "T", "J", "Q", "K", "A"]
        if rank not in valid:
            raise ValueError(f"'{card_abbreviation}' is not a valid card abbreviation.")
        return rank

    @staticmethod
    def _abbreviation_to_suit(card_abbreviation: str) -> str:
        """ Get the suit from the card abbreviation. """
        if len(card_abbreviation) > 2:
            raise ValueError(f"'{card_abbreviation}' is not a valid card abbreviation.")
        suit = card_abbreviation[-1].lower()
        valid = ["s", "h", "c", "d"]
        if suit not in valid:
            raise ValueError(f"'{card_abbreviation}' is not a valid card abbreviation.")
        return suit

    @staticmethod
    def _rank_to_numerical(rank: str) -> int:
        """ Get the numerical rank from an alpha-numerical rank. """
        numbers = {"T": "10", "J": "11", "Q": "12", "K": "13", "A": "14"}
        if rank in numbers:
            for key, value in numbers.items():
                if key in rank:
                    rank = rank.replace(key, value)
        return int(rank)


class Deck:
    """ French-style deck. """

    ranks: Sequence[str] = [
        "2",
        "3",
        "4",
        "5",
        "6",
        "7",
        "8",
        "9",
        "T",
        "J",
        "Q",
        "K",
        "A",
    ]
    suits: Sequence[str] = ["s", "h", "c", "d"]

    def __init__(self, random_state=None):
        self._cards: List[Card] = []
        self.set_and_shuffle(random_state)

    def __len__(self):
        return len(self._cards)

    def __iter__(self):
        return self._cards

    @property
    def cards(self):
        """ Get deck cards. """
        return self._cards

    def set_and_shuffle(self, random_state=None):
        """ Set the deck cards and shuffle. """
        self._cards = [
            Card(rank + suit)
            for rank, suit in itertools.product(self.ranks, self.suits)
        ]
        # pylint: disable=E1101
        random_state = np.random.RandomState(random_state)
        random_state.shuffle(self._cards)

    def draw(self) -> Card:
        """ Draw a card. """
        try:
            return self._cards.pop(-1)
        except IndexError:
            raise NotEnoughCardsError("There are no cards left in the deck.")


class Hand:
    """ Poker hand. Formed by Card objects. """

    def __init__(self, *args: Union[Card, str]):
        self._ranks: List[str] = []
        self._suits: List[str] = []
        self._numerical_ranks: List[int] = []
        self._hex_ranks: List[str] = []
        self._cards: List[Card] = []

        self.add(*args)

    def __repr__(self):
        return " ".join(sorted([str(card) for card in self.cards]))

    def __str__(self):
        return self.__repr__()

    def __len__(self):
        return len(self.cards)

    def __getitem__(self, item):
        return self.cards[item]

    def __setitem__(self, key, value: Card):
        self._cards[key] = value
        self._ranks[key] = value.rank
        self._suits[key] = value.suit
        self._numerical_ranks[key] = value.numerical_rank
        self._hex_ranks[key] = value.hex_rank

    def __delitem__(self, key):
        self.cards.pop(key)
        self.ranks.pop(key)
        self.suits.pop(key)
        self.numerical_ranks.pop(key)
        self.hex_ranks.pop(key)

    def __contains__(self, item):
        return item in self._cards

    @property
    def ranks(self) -> List[str]:
        """ Get hand ranks. """
        return self._ranks

    @property
    def suits(self) -> List[str]:
        """ Get hand suits. """
        return self._suits

    @property
    def numerical_ranks(self) -> List[int]:
        """ Get hand numerical ranks. """
        return self._numerical_ranks

    @property
    def hex_ranks(self) -> List[str]:
        """ Get hand alpha-numerical ranks (hexadecimal). """
        return self._hex_ranks

    @property
    def cards(self) -> List[Card]:
        """ Get hand cards. """
        return self._cards

    @property
    def value(self) -> int:
        """
        Get the numerical value of the hand. The bigger the value, the better the hand.
        """
        value = ""

        value = self._high_card() + value
        value = self._pair() + value
        value = self._two_pairs() + value
        value = self._three_of_a_kind() + value
        value = self._straight() + value
        value = self._flush() + value
        value = self._full_house() + value
        value = self._four_of_a_kind() + value
        value = self._straight_flush() + value

        value = self._compensate_missing_cards_value(len(self), value)
        value = self._compensate_extra_cards_value(len(self), value)

        return int(value, 16)

    @property
    def name(self) -> str:
        """ Get ranking name of the hand. """

        names = [
            "high_card",
            "pair",
            "two_pairs",
            "three_of_a_kind",
            "straight",
            "flush",
            "full_house",
            "four_of_a_kind",
            "straight_flush",
            "royal_straight_flush",
        ]
        for name in names:
            if getattr(self, f"is_{name}")():
                return name
        raise ValueError("Hand has unexpected value.")

    def _args_to_cards(self, *args: Union[Card, str]) -> List[Card]:
        """ Parse class arguments to Cards instances. """
        # Separate args if the user used a concatenated argument.
        cards = self._separate_concatenated_cards(*args)
        # Create cards instances if the user used string arguments.
        return [Card(card) if isinstance(card, str) else card for card in cards]

    def _separate_concatenated_cards(self, *args: Union[Card, str]) -> Iterable[str]:
        """ Separate concatenated cards repr in a argument. """
        nested = [
            re.findall(r"[2-9TJQKA][shcd]", card) if isinstance(card, str) else card
            for card in args
        ]
        flat = self._flatten(nested)
        return flat

    @staticmethod
    def _flatten(i: Iterable) -> Iterable:
        """ Flatten an irregular iterable. """
        for val in i:
            # pylint: disable=W1116
            if isinstance(val, Iterable):
                yield from val
            else:
                yield val

    def add(self, *args: Union[Card, str]):
        """ Add cards to the hands. """
        cards = self._args_to_cards(*args)
        self._ranks += [card.rank for card in cards]
        self._suits += [card.suit for card in cards]
        self._numerical_ranks += [card.numerical_rank for card in cards]
        self._hex_ranks += [
            np.base_repr(card.numerical_rank, 16)
            if card.numerical_rank > 9
            else card.rank
            for card in cards
        ]
        self._cards += cards

    @staticmethod
    def _find_repeated_ranks(ranks: Sequence, reps: int) -> set:
        """ Find ranks that are repeated a certain number of times in a hand. """
        return {rank for rank in ranks if ranks.count(rank) == reps}

    # The next methods are useful for the value property only. They
    # work by transforming a hand in a huge integer number. The bigger
    # the number, the stronger the hand. Bellow the construction of this
    # number is better explained.

    # Each pair of letter bellow represent a numerical rank. For
    # example: 02 stands for the deuce, while 11 stands for the Jack.

    # In order to have only 1-dig numbers I'll work with hexadecimal.

    # Every type of hand takes its magnitude multiplied for the
    # numerical rank. The integer formation is bellow.
    # ABCCDEFGGHIIIII
    # A - Straight Flush
    # B - Quads
    # C - Full House
    # D - Flush
    # E - Straight
    # F - Trips
    # G - Two Pair
    # H - Pair
    # I - High Card (Actually, the rank of every card)

    # In a nutshell, the next methods return a code used to form the
    # hand value. This is also where all the logic for deciding the hand
    # level lies.

    def _high_card(self) -> str:
        """ Hand value code for a high card."""
        # Concatenate each cards value in a string, from the biggest to
        # the smallest.
        return "".join(sorted(self.hex_ranks, reverse=True))

    def _pair(self) -> str:
        """ Hand value code for a pair."""
        pairs = list(self._find_repeated_ranks(self.hex_ranks, 2))
        if len(pairs) == 1:
            return pairs[0]
        return "0"

    def _two_pairs(self) -> str:
        """ Hand value code for a two pair."""
        pairs = list(self._find_repeated_ranks(self.hex_ranks, 2))
        if len(pairs) == 2:
            return max(pairs) + min(pairs)
        return "00"

    def _three_of_a_kind(self) -> str:
        """ Hand value code for a three of a kind."""
        trips = list(self._find_repeated_ranks(self.hex_ranks, 3))
        if trips:
            return trips[0]
        return "0"

    def _straight(self) -> str:
        """ Hand value code for a straight."""
        # Work with base 10 numbers because more_itertools.consecutive_groups do work
        # with hexadecimals.
        aces_count = self.numerical_ranks.count(14)
        hand = set(self.numerical_ranks + [1] * aces_count)

        # This next comparisons only work when the Hand is not empty.
        # When the list is empty, it should return no value.
        if not hand:
            return "0"

        groups = [list(group) for group in more_itertools.consecutive_groups(hand)]
        longest_sequence = max([group[-1] - group[0] for group in groups]) + 1

        if longest_sequence >= 5:
            largest_value = max([max(group) for group in groups if len(group) >= 5])
            return np.base_repr(largest_value, 16)  # Convert to hex.
        return "0"

    def _flush(self) -> str:
        """ Hand value code for a flush."""
        suits = {suit for suit in self.suits if self.suits.count(suit) >= 5}
        if len(suits) == 0:
            return "0"
        ranks = [r for r, s in zip(self.numerical_ranks, self.suits) if s in suits]
        return np.base_repr(max(ranks), 16)

    def _full_house(self) -> str:
        """ Hand value code for a full house."""
        trips = list(self._find_repeated_ranks(self.hex_ranks, 3))
        pair = list(self._find_repeated_ranks(self.hex_ranks, 2))
        if trips and pair:
            return trips[0] + pair[0]
        return "00"

    def _four_of_a_kind(self) -> str:
        """ Hand value code for a four of a kind."""
        quads = list(self._find_repeated_ranks(self.hex_ranks, 4))
        if quads:
            return quads[0]
        return "0"

    def _straight_flush(self) -> str:
        """ Hand value code for a straight flush."""
        flush = self._flush()
        if flush == "0":
            return "0"
        straight = self._straight()
        if straight == "0":
            return "0"
        return straight

    @staticmethod
    def _compensate_missing_cards_value(n_cards: int, value: str) -> str:
        """ Add trailing zeros to the value in order to compensate missing cards. """
        if n_cards < 5:
            missing = 5 - n_cards
            return value + "0" * missing
        return value

    @staticmethod
    def _compensate_extra_cards_value(n_cards: int, value: str) -> str:
        """ Remove trailing zeros to the value in order to compensate extra cards. """
        if n_cards > 5:
            extras = n_cards - 5
            return value[: -1 * extras]
        return value

    def is_high_card(self) -> bool:
        """ Check if the hand is a high card. """
        return self.value < int("E" * 5, 16)

    def is_pair(self) -> bool:
        """ Check if the hand is a pair. """
        return int("E" * 5, 16) < self.value < int("E" * 6, 16)

    def is_two_pairs(self) -> bool:
        """ Check if the hand is a two pair. """
        return int("E" * 6, 16) < self.value < int("E" * 8, 16)

    def is_three_of_a_kind(self) -> bool:
        """ Check if the hand is a three of a kind. """
        return int("E" * 8, 16) < self.value < int("E" * 9, 16)

    def is_straight(self) -> bool:
        """ Check if the hand is a straight. """
        return int("E" * 9, 16) < self.value < int("E" * 10, 16)

    def is_flush(self) -> bool:
        """ Check if the hand is a flush. """
        return int("E" * 10, 16) < self.value < int("E" * 11, 16)

    def is_full_house(self) -> bool:
        """ Check if the hand is a full house. """
        return int("E" * 11, 16) < self.value < int("E" * 13, 16)

    def is_four_of_a_kind(self) -> bool:
        """ Check if the hand is a four of a kind. """
        return int("E" * 13, 16) < self.value < int("E" * 14, 16)

    def is_straight_flush(self) -> bool:
        """ Check if the hand is a straight flush. """
        return int("E" * 14, 16) < self.value < int("D" * 15, 16)

    def is_royal_straight_flush(self) -> bool:
        """ Check if the hand is a royal straight flush. """
        return self.value > int("D" * 15, 16)


class Player:
    """ Poker player. """

    def __init__(self, name: str, chips: float):
        self._name: str = name
        self._chips: float = self._validate_chips(chips)
        self._hand: Hand = Hand()

    def __repr__(self):
        return self._name

    @property
    def name(self) -> str:
        """ Get player name. """
        return self._name

    @property
    def chips(self) -> float:
        """ Get or set player chips amount. """
        return self._chips

    @chips.setter
    def chips(self, value: float):
        value = self._validate_chips(value)
        self._chips = value

    @property
    def hand(self) -> Hand:
        """ Get or set player hand. """
        return self._hand

    @hand.setter
    def hand(self, value: Hand):
        self._hand = value

    @staticmethod
    def _validate_chips(chips: float) -> float:
        """ Validate player chips amount. """
        if chips < 0:
            raise ValueError("Chips must equal or greater to zero.")
        return chips

    def add_cards(self, cards: Iterable[Card]):
        """ Add cards to a player hand. """
        for card in cards:
            self.hand.add(card)

    def clear_hand(self):
        """" Clear a player hand"""
        self.hand = Hand()


class Round:
    """ Poker game round. """

    def __init__(self, players: Sequence[Player], n_starting_cards: int = 5):
        self._players = players
        self._deck = Deck()
        self._n_starting_cards = n_starting_cards
        self.new()

    @property
    def players(self) -> Sequence[Player]:
        """ Get or set round players. """
        return self._players

    @players.setter
    def players(self, value: Sequence[Player]):
        self._players = value

    @property
    def deck(self) -> Deck:
        """ Get round deck. """
        return self._deck

    @property
    def n_starting_cards(self) -> int:
        """ Get round number of starting cards. """
        return self._n_starting_cards

    def deal_cards(self, player: Player, n_cards: int):
        """ Deal a number of cards to a single players. """
        cards = [self.deck.draw() for _ in range(n_cards)]
        player.add_cards(cards)

    def deal_cards_to_all(self, n_cards: int):
        """ Deal cards to all players. """
        for player in self.players:
            self.deal_cards(player=player, n_cards=n_cards)

    def new(self):
        """ Start a new round. """
        for player in self.players:
            player.clear_hand()
        self.deck.set_and_shuffle()
        self.deal_cards_to_all(self.n_starting_cards)

    def winner(self) -> np.ndarray:
        """ Evaluate the winner player. """
        return np.argmax([player.hand.value for player in self.players])


class Poker:
    """ Abstract class for a bluff game. """

    _N_STARTING_CARDS: int = 5

    def __init__(self, n_seats: int = 9):
        self._seats: List[Optional[Player]] = [None] * n_seats
        self._dealer = random.choice(range(n_seats))

    @property
    def seats(self) -> List[Optional[Player]]:
        """ Get list of seats. """
        return self._seats

    @property
    def dealer(self) -> int:
        """ Get dealer position. """
        return self._dealer

    @dealer.setter
    def dealer(self, value: int):
        if value >= len(self.seats):
            raise ValueError("Dealer must be set to an existing seat.")
        self._dealer = value

    def add_player(self, player: Player, seat: int):
        """ Add a player to a seat. """
        if self.seats[seat] is None:
            self.seats[seat] = player
        else:
            raise SeatOccupiedError(f"The seat {seat} is already occupied.")

    def add_players(
        self, players: Iterable[Player], seats: Optional[Iterable[int]] = None,
    ):
        """
        Add players to their seats. Use seats=None to choose seats
        randomly.
        """
        # When no seats are passed, chooses randomly.
        if seats is None:
            free_seats = [seat for seat, player in enumerate(self.seats) if not player]
            seats = [self._random_pop(free_seats) for _ in players]
        for player, seat in zip(players, seats):
            self.add_player(player=player, seat=seat)

    @staticmethod
    def _random_pop(lst: list):
        """ Randomly pop an item from a list."""
        return lst.pop(random.randrange(len(lst)))

    def remove_player(self, seat: int):
        """ Remove a player from a seat. """
        self.seats[seat] = None

    @staticmethod
    def _item_to_beginning(list_: list, index: int) -> List:
        """ Move an item to the beginning of a list. """
        return list_[index:] + list_[:index]

    def _validate_dealer(self):
        """ Find a valid position for the dealer. """
        # I sort the seats to put the dealer in the beginning so then I
        # only have to add values to the seat number until I find a
        # valid player. The move variable represents how  many seats the
        # dealer button must move until it finds a valid player.
        seats = self._item_to_beginning(self.seats, self.dealer)
        move = 0
        while seats[move] is None:
            move += 1
        self.dealer += move

    def new_round(self) -> Round:
        """ Start a new round with available players. """
        # Firstly, organize players list so it is passed to the Round
        # class in the playing order.
        self._validate_dealer()
        ordered_seats = self._item_to_beginning(self.seats, self.dealer)
        players = [seat for seat in ordered_seats if seat is not None]

        # Start a round
        rnd = Round(players=players, n_starting_cards=self._N_STARTING_CARDS)
        rnd.new()

        return rnd

Sub-modules

bluff.chinese

Bluff's Chinese Poker sub-module.

bluff.holdem

Bluff's Texas Hold'em sub-module.

Classes

class Card (abbreviation: str)

French-style deck card.

Expand source code
class Card:
    """ French-style deck card."""

    def __init__(self, abbreviation: str):
        self._rank = self._abbreviation_to_rank(abbreviation)
        self._suit = self._abbreviation_to_suit(abbreviation)
        self._numerical_rank = self._rank_to_numerical(self._rank)

    def __repr__(self):
        return self.rank + self.suit

    def __str__(self):
        return self.__repr__()

    def __eq__(self, other):
        return self.rank == other.rank and self.suit == other.suit

    @property
    def rank(self) -> str:
        """ Get the card rank. """
        return self._rank

    @property
    def suit(self) -> str:
        """ Get the card suit. """
        return self._suit

    @property
    def numerical_rank(self) -> int:
        """ Get the card numerical rank. """
        return self._numerical_rank

    @property
    def hex_rank(self) -> str:
        """ Get the card alpha numerical rank (hexadecimal) """
        return np.base_repr(self.numerical_rank, 16)

    @staticmethod
    def _abbreviation_to_rank(card_abbreviation: str) -> str:
        """ Get the rank from the card abbreviation. """
        rank = card_abbreviation[0].upper()
        valid = ["2", "3", "4", "5", "6", "7", "8", "9", "T", "J", "Q", "K", "A"]
        if rank not in valid:
            raise ValueError(f"'{card_abbreviation}' is not a valid card abbreviation.")
        return rank

    @staticmethod
    def _abbreviation_to_suit(card_abbreviation: str) -> str:
        """ Get the suit from the card abbreviation. """
        if len(card_abbreviation) > 2:
            raise ValueError(f"'{card_abbreviation}' is not a valid card abbreviation.")
        suit = card_abbreviation[-1].lower()
        valid = ["s", "h", "c", "d"]
        if suit not in valid:
            raise ValueError(f"'{card_abbreviation}' is not a valid card abbreviation.")
        return suit

    @staticmethod
    def _rank_to_numerical(rank: str) -> int:
        """ Get the numerical rank from an alpha-numerical rank. """
        numbers = {"T": "10", "J": "11", "Q": "12", "K": "13", "A": "14"}
        if rank in numbers:
            for key, value in numbers.items():
                if key in rank:
                    rank = rank.replace(key, value)
        return int(rank)

Instance variables

var hex_rank : str

Get the card alpha numerical rank (hexadecimal)

Expand source code
@property
def hex_rank(self) -> str:
    """ Get the card alpha numerical rank (hexadecimal) """
    return np.base_repr(self.numerical_rank, 16)
var numerical_rank : int

Get the card numerical rank.

Expand source code
@property
def numerical_rank(self) -> int:
    """ Get the card numerical rank. """
    return self._numerical_rank
var rank : str

Get the card rank.

Expand source code
@property
def rank(self) -> str:
    """ Get the card rank. """
    return self._rank
var suit : str

Get the card suit.

Expand source code
@property
def suit(self) -> str:
    """ Get the card suit. """
    return self._suit
class Deck (random_state=None)

French-style deck.

Expand source code
class Deck:
    """ French-style deck. """

    ranks: Sequence[str] = [
        "2",
        "3",
        "4",
        "5",
        "6",
        "7",
        "8",
        "9",
        "T",
        "J",
        "Q",
        "K",
        "A",
    ]
    suits: Sequence[str] = ["s", "h", "c", "d"]

    def __init__(self, random_state=None):
        self._cards: List[Card] = []
        self.set_and_shuffle(random_state)

    def __len__(self):
        return len(self._cards)

    def __iter__(self):
        return self._cards

    @property
    def cards(self):
        """ Get deck cards. """
        return self._cards

    def set_and_shuffle(self, random_state=None):
        """ Set the deck cards and shuffle. """
        self._cards = [
            Card(rank + suit)
            for rank, suit in itertools.product(self.ranks, self.suits)
        ]
        # pylint: disable=E1101
        random_state = np.random.RandomState(random_state)
        random_state.shuffle(self._cards)

    def draw(self) -> Card:
        """ Draw a card. """
        try:
            return self._cards.pop(-1)
        except IndexError:
            raise NotEnoughCardsError("There are no cards left in the deck.")

Class variables

var ranks : Sequence[str]
var suits : Sequence[str]

Instance variables

var cards

Get deck cards.

Expand source code
@property
def cards(self):
    """ Get deck cards. """
    return self._cards

Methods

def draw(self) ‑> Card

Draw a card.

Expand source code
def draw(self) -> Card:
    """ Draw a card. """
    try:
        return self._cards.pop(-1)
    except IndexError:
        raise NotEnoughCardsError("There are no cards left in the deck.")
def set_and_shuffle(self, random_state=None)

Set the deck cards and shuffle.

Expand source code
def set_and_shuffle(self, random_state=None):
    """ Set the deck cards and shuffle. """
    self._cards = [
        Card(rank + suit)
        for rank, suit in itertools.product(self.ranks, self.suits)
    ]
    # pylint: disable=E1101
    random_state = np.random.RandomState(random_state)
    random_state.shuffle(self._cards)
class Hand (*args: Union[Card, str])

Poker hand. Formed by Card objects.

Expand source code
class Hand:
    """ Poker hand. Formed by Card objects. """

    def __init__(self, *args: Union[Card, str]):
        self._ranks: List[str] = []
        self._suits: List[str] = []
        self._numerical_ranks: List[int] = []
        self._hex_ranks: List[str] = []
        self._cards: List[Card] = []

        self.add(*args)

    def __repr__(self):
        return " ".join(sorted([str(card) for card in self.cards]))

    def __str__(self):
        return self.__repr__()

    def __len__(self):
        return len(self.cards)

    def __getitem__(self, item):
        return self.cards[item]

    def __setitem__(self, key, value: Card):
        self._cards[key] = value
        self._ranks[key] = value.rank
        self._suits[key] = value.suit
        self._numerical_ranks[key] = value.numerical_rank
        self._hex_ranks[key] = value.hex_rank

    def __delitem__(self, key):
        self.cards.pop(key)
        self.ranks.pop(key)
        self.suits.pop(key)
        self.numerical_ranks.pop(key)
        self.hex_ranks.pop(key)

    def __contains__(self, item):
        return item in self._cards

    @property
    def ranks(self) -> List[str]:
        """ Get hand ranks. """
        return self._ranks

    @property
    def suits(self) -> List[str]:
        """ Get hand suits. """
        return self._suits

    @property
    def numerical_ranks(self) -> List[int]:
        """ Get hand numerical ranks. """
        return self._numerical_ranks

    @property
    def hex_ranks(self) -> List[str]:
        """ Get hand alpha-numerical ranks (hexadecimal). """
        return self._hex_ranks

    @property
    def cards(self) -> List[Card]:
        """ Get hand cards. """
        return self._cards

    @property
    def value(self) -> int:
        """
        Get the numerical value of the hand. The bigger the value, the better the hand.
        """
        value = ""

        value = self._high_card() + value
        value = self._pair() + value
        value = self._two_pairs() + value
        value = self._three_of_a_kind() + value
        value = self._straight() + value
        value = self._flush() + value
        value = self._full_house() + value
        value = self._four_of_a_kind() + value
        value = self._straight_flush() + value

        value = self._compensate_missing_cards_value(len(self), value)
        value = self._compensate_extra_cards_value(len(self), value)

        return int(value, 16)

    @property
    def name(self) -> str:
        """ Get ranking name of the hand. """

        names = [
            "high_card",
            "pair",
            "two_pairs",
            "three_of_a_kind",
            "straight",
            "flush",
            "full_house",
            "four_of_a_kind",
            "straight_flush",
            "royal_straight_flush",
        ]
        for name in names:
            if getattr(self, f"is_{name}")():
                return name
        raise ValueError("Hand has unexpected value.")

    def _args_to_cards(self, *args: Union[Card, str]) -> List[Card]:
        """ Parse class arguments to Cards instances. """
        # Separate args if the user used a concatenated argument.
        cards = self._separate_concatenated_cards(*args)
        # Create cards instances if the user used string arguments.
        return [Card(card) if isinstance(card, str) else card for card in cards]

    def _separate_concatenated_cards(self, *args: Union[Card, str]) -> Iterable[str]:
        """ Separate concatenated cards repr in a argument. """
        nested = [
            re.findall(r"[2-9TJQKA][shcd]", card) if isinstance(card, str) else card
            for card in args
        ]
        flat = self._flatten(nested)
        return flat

    @staticmethod
    def _flatten(i: Iterable) -> Iterable:
        """ Flatten an irregular iterable. """
        for val in i:
            # pylint: disable=W1116
            if isinstance(val, Iterable):
                yield from val
            else:
                yield val

    def add(self, *args: Union[Card, str]):
        """ Add cards to the hands. """
        cards = self._args_to_cards(*args)
        self._ranks += [card.rank for card in cards]
        self._suits += [card.suit for card in cards]
        self._numerical_ranks += [card.numerical_rank for card in cards]
        self._hex_ranks += [
            np.base_repr(card.numerical_rank, 16)
            if card.numerical_rank > 9
            else card.rank
            for card in cards
        ]
        self._cards += cards

    @staticmethod
    def _find_repeated_ranks(ranks: Sequence, reps: int) -> set:
        """ Find ranks that are repeated a certain number of times in a hand. """
        return {rank for rank in ranks if ranks.count(rank) == reps}

    # The next methods are useful for the value property only. They
    # work by transforming a hand in a huge integer number. The bigger
    # the number, the stronger the hand. Bellow the construction of this
    # number is better explained.

    # Each pair of letter bellow represent a numerical rank. For
    # example: 02 stands for the deuce, while 11 stands for the Jack.

    # In order to have only 1-dig numbers I'll work with hexadecimal.

    # Every type of hand takes its magnitude multiplied for the
    # numerical rank. The integer formation is bellow.
    # ABCCDEFGGHIIIII
    # A - Straight Flush
    # B - Quads
    # C - Full House
    # D - Flush
    # E - Straight
    # F - Trips
    # G - Two Pair
    # H - Pair
    # I - High Card (Actually, the rank of every card)

    # In a nutshell, the next methods return a code used to form the
    # hand value. This is also where all the logic for deciding the hand
    # level lies.

    def _high_card(self) -> str:
        """ Hand value code for a high card."""
        # Concatenate each cards value in a string, from the biggest to
        # the smallest.
        return "".join(sorted(self.hex_ranks, reverse=True))

    def _pair(self) -> str:
        """ Hand value code for a pair."""
        pairs = list(self._find_repeated_ranks(self.hex_ranks, 2))
        if len(pairs) == 1:
            return pairs[0]
        return "0"

    def _two_pairs(self) -> str:
        """ Hand value code for a two pair."""
        pairs = list(self._find_repeated_ranks(self.hex_ranks, 2))
        if len(pairs) == 2:
            return max(pairs) + min(pairs)
        return "00"

    def _three_of_a_kind(self) -> str:
        """ Hand value code for a three of a kind."""
        trips = list(self._find_repeated_ranks(self.hex_ranks, 3))
        if trips:
            return trips[0]
        return "0"

    def _straight(self) -> str:
        """ Hand value code for a straight."""
        # Work with base 10 numbers because more_itertools.consecutive_groups do work
        # with hexadecimals.
        aces_count = self.numerical_ranks.count(14)
        hand = set(self.numerical_ranks + [1] * aces_count)

        # This next comparisons only work when the Hand is not empty.
        # When the list is empty, it should return no value.
        if not hand:
            return "0"

        groups = [list(group) for group in more_itertools.consecutive_groups(hand)]
        longest_sequence = max([group[-1] - group[0] for group in groups]) + 1

        if longest_sequence >= 5:
            largest_value = max([max(group) for group in groups if len(group) >= 5])
            return np.base_repr(largest_value, 16)  # Convert to hex.
        return "0"

    def _flush(self) -> str:
        """ Hand value code for a flush."""
        suits = {suit for suit in self.suits if self.suits.count(suit) >= 5}
        if len(suits) == 0:
            return "0"
        ranks = [r for r, s in zip(self.numerical_ranks, self.suits) if s in suits]
        return np.base_repr(max(ranks), 16)

    def _full_house(self) -> str:
        """ Hand value code for a full house."""
        trips = list(self._find_repeated_ranks(self.hex_ranks, 3))
        pair = list(self._find_repeated_ranks(self.hex_ranks, 2))
        if trips and pair:
            return trips[0] + pair[0]
        return "00"

    def _four_of_a_kind(self) -> str:
        """ Hand value code for a four of a kind."""
        quads = list(self._find_repeated_ranks(self.hex_ranks, 4))
        if quads:
            return quads[0]
        return "0"

    def _straight_flush(self) -> str:
        """ Hand value code for a straight flush."""
        flush = self._flush()
        if flush == "0":
            return "0"
        straight = self._straight()
        if straight == "0":
            return "0"
        return straight

    @staticmethod
    def _compensate_missing_cards_value(n_cards: int, value: str) -> str:
        """ Add trailing zeros to the value in order to compensate missing cards. """
        if n_cards < 5:
            missing = 5 - n_cards
            return value + "0" * missing
        return value

    @staticmethod
    def _compensate_extra_cards_value(n_cards: int, value: str) -> str:
        """ Remove trailing zeros to the value in order to compensate extra cards. """
        if n_cards > 5:
            extras = n_cards - 5
            return value[: -1 * extras]
        return value

    def is_high_card(self) -> bool:
        """ Check if the hand is a high card. """
        return self.value < int("E" * 5, 16)

    def is_pair(self) -> bool:
        """ Check if the hand is a pair. """
        return int("E" * 5, 16) < self.value < int("E" * 6, 16)

    def is_two_pairs(self) -> bool:
        """ Check if the hand is a two pair. """
        return int("E" * 6, 16) < self.value < int("E" * 8, 16)

    def is_three_of_a_kind(self) -> bool:
        """ Check if the hand is a three of a kind. """
        return int("E" * 8, 16) < self.value < int("E" * 9, 16)

    def is_straight(self) -> bool:
        """ Check if the hand is a straight. """
        return int("E" * 9, 16) < self.value < int("E" * 10, 16)

    def is_flush(self) -> bool:
        """ Check if the hand is a flush. """
        return int("E" * 10, 16) < self.value < int("E" * 11, 16)

    def is_full_house(self) -> bool:
        """ Check if the hand is a full house. """
        return int("E" * 11, 16) < self.value < int("E" * 13, 16)

    def is_four_of_a_kind(self) -> bool:
        """ Check if the hand is a four of a kind. """
        return int("E" * 13, 16) < self.value < int("E" * 14, 16)

    def is_straight_flush(self) -> bool:
        """ Check if the hand is a straight flush. """
        return int("E" * 14, 16) < self.value < int("D" * 15, 16)

    def is_royal_straight_flush(self) -> bool:
        """ Check if the hand is a royal straight flush. """
        return self.value > int("D" * 15, 16)

Subclasses

Instance variables

var cards : List[Card]

Get hand cards.

Expand source code
@property
def cards(self) -> List[Card]:
    """ Get hand cards. """
    return self._cards
var hex_ranks : List[str]

Get hand alpha-numerical ranks (hexadecimal).

Expand source code
@property
def hex_ranks(self) -> List[str]:
    """ Get hand alpha-numerical ranks (hexadecimal). """
    return self._hex_ranks
var name : str

Get ranking name of the hand.

Expand source code
@property
def name(self) -> str:
    """ Get ranking name of the hand. """

    names = [
        "high_card",
        "pair",
        "two_pairs",
        "three_of_a_kind",
        "straight",
        "flush",
        "full_house",
        "four_of_a_kind",
        "straight_flush",
        "royal_straight_flush",
    ]
    for name in names:
        if getattr(self, f"is_{name}")():
            return name
    raise ValueError("Hand has unexpected value.")
var numerical_ranks : List[int]

Get hand numerical ranks.

Expand source code
@property
def numerical_ranks(self) -> List[int]:
    """ Get hand numerical ranks. """
    return self._numerical_ranks
var ranks : List[str]

Get hand ranks.

Expand source code
@property
def ranks(self) -> List[str]:
    """ Get hand ranks. """
    return self._ranks
var suits : List[str]

Get hand suits.

Expand source code
@property
def suits(self) -> List[str]:
    """ Get hand suits. """
    return self._suits
var value : int

Get the numerical value of the hand. The bigger the value, the better the hand.

Expand source code
@property
def value(self) -> int:
    """
    Get the numerical value of the hand. The bigger the value, the better the hand.
    """
    value = ""

    value = self._high_card() + value
    value = self._pair() + value
    value = self._two_pairs() + value
    value = self._three_of_a_kind() + value
    value = self._straight() + value
    value = self._flush() + value
    value = self._full_house() + value
    value = self._four_of_a_kind() + value
    value = self._straight_flush() + value

    value = self._compensate_missing_cards_value(len(self), value)
    value = self._compensate_extra_cards_value(len(self), value)

    return int(value, 16)

Methods

def add(self, *args: Union[Card, str])

Add cards to the hands.

Expand source code
def add(self, *args: Union[Card, str]):
    """ Add cards to the hands. """
    cards = self._args_to_cards(*args)
    self._ranks += [card.rank for card in cards]
    self._suits += [card.suit for card in cards]
    self._numerical_ranks += [card.numerical_rank for card in cards]
    self._hex_ranks += [
        np.base_repr(card.numerical_rank, 16)
        if card.numerical_rank > 9
        else card.rank
        for card in cards
    ]
    self._cards += cards
def is_flush(self) ‑> bool

Check if the hand is a flush.

Expand source code
def is_flush(self) -> bool:
    """ Check if the hand is a flush. """
    return int("E" * 10, 16) < self.value < int("E" * 11, 16)
def is_four_of_a_kind(self) ‑> bool

Check if the hand is a four of a kind.

Expand source code
def is_four_of_a_kind(self) -> bool:
    """ Check if the hand is a four of a kind. """
    return int("E" * 13, 16) < self.value < int("E" * 14, 16)
def is_full_house(self) ‑> bool

Check if the hand is a full house.

Expand source code
def is_full_house(self) -> bool:
    """ Check if the hand is a full house. """
    return int("E" * 11, 16) < self.value < int("E" * 13, 16)
def is_high_card(self) ‑> bool

Check if the hand is a high card.

Expand source code
def is_high_card(self) -> bool:
    """ Check if the hand is a high card. """
    return self.value < int("E" * 5, 16)
def is_pair(self) ‑> bool

Check if the hand is a pair.

Expand source code
def is_pair(self) -> bool:
    """ Check if the hand is a pair. """
    return int("E" * 5, 16) < self.value < int("E" * 6, 16)
def is_royal_straight_flush(self) ‑> bool

Check if the hand is a royal straight flush.

Expand source code
def is_royal_straight_flush(self) -> bool:
    """ Check if the hand is a royal straight flush. """
    return self.value > int("D" * 15, 16)
def is_straight(self) ‑> bool

Check if the hand is a straight.

Expand source code
def is_straight(self) -> bool:
    """ Check if the hand is a straight. """
    return int("E" * 9, 16) < self.value < int("E" * 10, 16)
def is_straight_flush(self) ‑> bool

Check if the hand is a straight flush.

Expand source code
def is_straight_flush(self) -> bool:
    """ Check if the hand is a straight flush. """
    return int("E" * 14, 16) < self.value < int("D" * 15, 16)
def is_three_of_a_kind(self) ‑> bool

Check if the hand is a three of a kind.

Expand source code
def is_three_of_a_kind(self) -> bool:
    """ Check if the hand is a three of a kind. """
    return int("E" * 8, 16) < self.value < int("E" * 9, 16)
def is_two_pairs(self) ‑> bool

Check if the hand is a two pair.

Expand source code
def is_two_pairs(self) -> bool:
    """ Check if the hand is a two pair. """
    return int("E" * 6, 16) < self.value < int("E" * 8, 16)
class NotEnoughCardsError (...)

Raise when the deck runs out of cards.

Expand source code
class NotEnoughCardsError(Exception):
    """ Raise when the deck runs out of cards. """

Ancestors

  • builtins.Exception
  • builtins.BaseException
class Player (name: str, chips: float)

Poker player.

Expand source code
class Player:
    """ Poker player. """

    def __init__(self, name: str, chips: float):
        self._name: str = name
        self._chips: float = self._validate_chips(chips)
        self._hand: Hand = Hand()

    def __repr__(self):
        return self._name

    @property
    def name(self) -> str:
        """ Get player name. """
        return self._name

    @property
    def chips(self) -> float:
        """ Get or set player chips amount. """
        return self._chips

    @chips.setter
    def chips(self, value: float):
        value = self._validate_chips(value)
        self._chips = value

    @property
    def hand(self) -> Hand:
        """ Get or set player hand. """
        return self._hand

    @hand.setter
    def hand(self, value: Hand):
        self._hand = value

    @staticmethod
    def _validate_chips(chips: float) -> float:
        """ Validate player chips amount. """
        if chips < 0:
            raise ValueError("Chips must equal or greater to zero.")
        return chips

    def add_cards(self, cards: Iterable[Card]):
        """ Add cards to a player hand. """
        for card in cards:
            self.hand.add(card)

    def clear_hand(self):
        """" Clear a player hand"""
        self.hand = Hand()

Subclasses

Instance variables

var chips : float

Get or set player chips amount.

Expand source code
@property
def chips(self) -> float:
    """ Get or set player chips amount. """
    return self._chips
var handHand

Get or set player hand.

Expand source code
@property
def hand(self) -> Hand:
    """ Get or set player hand. """
    return self._hand
var name : str

Get player name.

Expand source code
@property
def name(self) -> str:
    """ Get player name. """
    return self._name

Methods

def add_cards(self, cards: Iterable[Card])

Add cards to a player hand.

Expand source code
def add_cards(self, cards: Iterable[Card]):
    """ Add cards to a player hand. """
    for card in cards:
        self.hand.add(card)
def clear_hand(self)

" Clear a player hand

Expand source code
def clear_hand(self):
    """" Clear a player hand"""
    self.hand = Hand()
class Poker (n_seats: int = 9)

Abstract class for a bluff game.

Expand source code
class Poker:
    """ Abstract class for a bluff game. """

    _N_STARTING_CARDS: int = 5

    def __init__(self, n_seats: int = 9):
        self._seats: List[Optional[Player]] = [None] * n_seats
        self._dealer = random.choice(range(n_seats))

    @property
    def seats(self) -> List[Optional[Player]]:
        """ Get list of seats. """
        return self._seats

    @property
    def dealer(self) -> int:
        """ Get dealer position. """
        return self._dealer

    @dealer.setter
    def dealer(self, value: int):
        if value >= len(self.seats):
            raise ValueError("Dealer must be set to an existing seat.")
        self._dealer = value

    def add_player(self, player: Player, seat: int):
        """ Add a player to a seat. """
        if self.seats[seat] is None:
            self.seats[seat] = player
        else:
            raise SeatOccupiedError(f"The seat {seat} is already occupied.")

    def add_players(
        self, players: Iterable[Player], seats: Optional[Iterable[int]] = None,
    ):
        """
        Add players to their seats. Use seats=None to choose seats
        randomly.
        """
        # When no seats are passed, chooses randomly.
        if seats is None:
            free_seats = [seat for seat, player in enumerate(self.seats) if not player]
            seats = [self._random_pop(free_seats) for _ in players]
        for player, seat in zip(players, seats):
            self.add_player(player=player, seat=seat)

    @staticmethod
    def _random_pop(lst: list):
        """ Randomly pop an item from a list."""
        return lst.pop(random.randrange(len(lst)))

    def remove_player(self, seat: int):
        """ Remove a player from a seat. """
        self.seats[seat] = None

    @staticmethod
    def _item_to_beginning(list_: list, index: int) -> List:
        """ Move an item to the beginning of a list. """
        return list_[index:] + list_[:index]

    def _validate_dealer(self):
        """ Find a valid position for the dealer. """
        # I sort the seats to put the dealer in the beginning so then I
        # only have to add values to the seat number until I find a
        # valid player. The move variable represents how  many seats the
        # dealer button must move until it finds a valid player.
        seats = self._item_to_beginning(self.seats, self.dealer)
        move = 0
        while seats[move] is None:
            move += 1
        self.dealer += move

    def new_round(self) -> Round:
        """ Start a new round with available players. """
        # Firstly, organize players list so it is passed to the Round
        # class in the playing order.
        self._validate_dealer()
        ordered_seats = self._item_to_beginning(self.seats, self.dealer)
        players = [seat for seat in ordered_seats if seat is not None]

        # Start a round
        rnd = Round(players=players, n_starting_cards=self._N_STARTING_CARDS)
        rnd.new()

        return rnd

Subclasses

Instance variables

var dealer : int

Get dealer position.

Expand source code
@property
def dealer(self) -> int:
    """ Get dealer position. """
    return self._dealer
var seats : List[Union[Player, NoneType]]

Get list of seats.

Expand source code
@property
def seats(self) -> List[Optional[Player]]:
    """ Get list of seats. """
    return self._seats

Methods

def add_player(self, player: Player, seat: int)

Add a player to a seat.

Expand source code
def add_player(self, player: Player, seat: int):
    """ Add a player to a seat. """
    if self.seats[seat] is None:
        self.seats[seat] = player
    else:
        raise SeatOccupiedError(f"The seat {seat} is already occupied.")
def add_players(self, players: Iterable[Player], seats: Union[Iterable[int], NoneType] = None)

Add players to their seats. Use seats=None to choose seats randomly.

Expand source code
def add_players(
    self, players: Iterable[Player], seats: Optional[Iterable[int]] = None,
):
    """
    Add players to their seats. Use seats=None to choose seats
    randomly.
    """
    # When no seats are passed, chooses randomly.
    if seats is None:
        free_seats = [seat for seat, player in enumerate(self.seats) if not player]
        seats = [self._random_pop(free_seats) for _ in players]
    for player, seat in zip(players, seats):
        self.add_player(player=player, seat=seat)
def new_round(self) ‑> Round

Start a new round with available players.

Expand source code
def new_round(self) -> Round:
    """ Start a new round with available players. """
    # Firstly, organize players list so it is passed to the Round
    # class in the playing order.
    self._validate_dealer()
    ordered_seats = self._item_to_beginning(self.seats, self.dealer)
    players = [seat for seat in ordered_seats if seat is not None]

    # Start a round
    rnd = Round(players=players, n_starting_cards=self._N_STARTING_CARDS)
    rnd.new()

    return rnd
def remove_player(self, seat: int)

Remove a player from a seat.

Expand source code
def remove_player(self, seat: int):
    """ Remove a player from a seat. """
    self.seats[seat] = None
class Round (players: Sequence[Player], n_starting_cards: int = 5)

Poker game round.

Expand source code
class Round:
    """ Poker game round. """

    def __init__(self, players: Sequence[Player], n_starting_cards: int = 5):
        self._players = players
        self._deck = Deck()
        self._n_starting_cards = n_starting_cards
        self.new()

    @property
    def players(self) -> Sequence[Player]:
        """ Get or set round players. """
        return self._players

    @players.setter
    def players(self, value: Sequence[Player]):
        self._players = value

    @property
    def deck(self) -> Deck:
        """ Get round deck. """
        return self._deck

    @property
    def n_starting_cards(self) -> int:
        """ Get round number of starting cards. """
        return self._n_starting_cards

    def deal_cards(self, player: Player, n_cards: int):
        """ Deal a number of cards to a single players. """
        cards = [self.deck.draw() for _ in range(n_cards)]
        player.add_cards(cards)

    def deal_cards_to_all(self, n_cards: int):
        """ Deal cards to all players. """
        for player in self.players:
            self.deal_cards(player=player, n_cards=n_cards)

    def new(self):
        """ Start a new round. """
        for player in self.players:
            player.clear_hand()
        self.deck.set_and_shuffle()
        self.deal_cards_to_all(self.n_starting_cards)

    def winner(self) -> np.ndarray:
        """ Evaluate the winner player. """
        return np.argmax([player.hand.value for player in self.players])

Instance variables

var deckDeck

Get round deck.

Expand source code
@property
def deck(self) -> Deck:
    """ Get round deck. """
    return self._deck
var n_starting_cards : int

Get round number of starting cards.

Expand source code
@property
def n_starting_cards(self) -> int:
    """ Get round number of starting cards. """
    return self._n_starting_cards
var players : Sequence[Player]

Get or set round players.

Expand source code
@property
def players(self) -> Sequence[Player]:
    """ Get or set round players. """
    return self._players

Methods

def deal_cards(self, player: Player, n_cards: int)

Deal a number of cards to a single players.

Expand source code
def deal_cards(self, player: Player, n_cards: int):
    """ Deal a number of cards to a single players. """
    cards = [self.deck.draw() for _ in range(n_cards)]
    player.add_cards(cards)
def deal_cards_to_all(self, n_cards: int)

Deal cards to all players.

Expand source code
def deal_cards_to_all(self, n_cards: int):
    """ Deal cards to all players. """
    for player in self.players:
        self.deal_cards(player=player, n_cards=n_cards)
def new(self)

Start a new round.

Expand source code
def new(self):
    """ Start a new round. """
    for player in self.players:
        player.clear_hand()
    self.deck.set_and_shuffle()
    self.deal_cards_to_all(self.n_starting_cards)
def winner(self) ‑> numpy.ndarray

Evaluate the winner player.

Expand source code
def winner(self) -> np.ndarray:
    """ Evaluate the winner player. """
    return np.argmax([player.hand.value for player in self.players])
class SeatOccupiedError (...)

Raise when trying to put a player in an already occupied seat.

Expand source code
class SeatOccupiedError(Exception):
    """ Raise when trying to put a player in an already occupied seat. """

Ancestors

  • builtins.Exception
  • builtins.BaseException