Hold'em Hand Evaluator

lrh9 0 Tallied Votes 1K Views Share

Hand evaluator for Texas Hold'em. If a "hand" has five or more cards, hand.rank will find the best five card hand the hand can form. Two hands can be compared using the comparison operators. The final hand can be gotten from the "hand" with hand.rank.hand. The value used for comparisons can be retrieved with hand.rank.value. Hand value is determined by the hand's Hold'em rank and the card values.

I welcome questions, comments, and bugfixes and other improvements to the code.

import collections
import math

##Copyright © 2010 Larry Haskins
##This program is free software: you can redistribute it and/or modify
##it under the terms of the GNU General Public License as published by
##the Free Software Foundation, either version 3 of the License, or
##(at your option) any later version.

##This program is distributed in the hope that it will be useful,
##but WITHOUT ANY WARRANTY; without even the implied warranty of
##MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
##GNU General Public License for more details.

##You should have received a copy of the GNU General Public License
##along with this program.  If not, see <http://www.gnu.org/licenses/>.


class Base:

    """Helper for classes with simple attribute comparisons.
    Inherit and initialize with the attribute for comparison."""

    def __init__(self, attr):
        self.attr = attr
    
    def __eq__(self, other):
        if self.attr == other.attr:
            return True
        else:
            return False

    def __ge__(self, other):
        if self.attr >= other.attr:
            return True
        else:
            return False

    def __gt__(self, other):
        if self.attr > other.attr:
            return True
        else:
            return False

    def __le__(self, other):
        if self.attr <= other.attr:
            return True
        else:
            return False

    def __lt__(self, other):
        if self.attr < other.attr:
            return True
        else:
            return False

    def __ne__(self, other):
        if self.attr != other.attr:
            return True
        else:
            return False

def _pluralize(word):
    if word[-1] in 'xX':
        ending = 'es'
    else:
        ending = 's'
    return ''.join((word, ending))

class Standard:

    """Namespace for standard card data structures."""
            
    class Card(Base):

        class Suit:

            names = ('spade', 'heart', 'diamond', 'club')
            symbols = ('♠', '♥', '♦', '♣')

            def __init__(self, symbol, name, short=None):
                self.symbol = symbol
                self.name = name
                self.short = short
                if self.short is None:
                    self.short = self.name[0]

            def __repr__(self):
                return self.symbol

            def __str__(self):
                return self.name

        suits = []
        for i in range(4):
            suits.append(Suit(Suit.symbols[i], Suit.names[i]))
        
        class Rank:

            names = ('two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', 'jack', 'queen', 'king', 'ace')

            def __init__(self, value, name, short=None):
                self.value = value
                self.name = name
                self.short = short
                if self.short is None:
                    self.short = str(self.value)

            def __repr__(self):
                return self.value

            def __str__(self):
                return self.name

        ranks = []
        for i, name in enumerate(Rank.names):
            short = None
            if i is 0:
                ranks.append(Rank(i + 1, 'ace', 'a'))
            elif i in range(8, 13):
                short = name[0]
            ranks.append(Rank(i + 2, name, short))
            
        def __init__(self, rank, suit):
            self.rank = rank
            self.value = self.rank.value
            Base.__init__(self, self.value)
            self.suit = suit

        def __str__(self):
            #Return a string representation of the card. Example: Ace of Spades
            rank = self.rank.name.capitalize()
            suit = _pluralize(self.suit.name).capitalize()
            return ' '.join([rank, 'of', suit])

        def __repr__(self):
            #Return a symbolic representation of the card. Example: Aâ™ 
            rank = self.rank.short.capitalize()
            suit = self.suit.symbol
            return ''.join([rank, suit])

        def __hash__(self):
            return hash((self.value, self.suit.symbol))

    class Deck(collections.deque):

        def __init__(self):
            _ = Standard.Card
            temp = []
            for suit in _.suits:
                #Don't include the dummy ace with a value of 1 (Standard.Card.ranks[0]) which is a helper for ranking a straight ace to five.
                for rank in _.ranks[1:]:
                    temp.append(_(rank, suit))
            collections.deque.__init__(self, temp)
            
        def __str__(self):
            #Return a string representation of the deck.
            return '\n'.join([str(card) for card in self])

        def __repr__(self):
            #Return a symbolic representation of the deck.
            return ', '.join([repr(card) for card in self])

#
            
class Holdem:

    "Namespace for Holdem specific aspects of cards.
    Contains some methods that don't belong here."""

    _ = Standard.Card

    @classmethod
    def sort_by_suit(cls, cards):
        """Given cards, compose dictionary with suits as keys
        and the cards of each suit as values.
        suits = {}
        for suit in cls._.suits:
            suits[suit] = []
        for card in cards:
            suits[card.suit].append(card)
        return suits

    @classmethod
    def sort_by_rank(cls, cards):
        """Given cards, compose dictionary with ranks as keys
        and the cards of each rank as values.
        ranks = collections.OrderedDict()
        for rank in cls._.ranks:
            ranks[rank] = []
        for card in cards:
            ranks[card.rank].append(card)
        return ranks

    @classmethod
    def _add_dummy_aces(cls, cards):
        """Add dummy aces for cards.
        Needed when checking for a straight ace to 5."""
        result = list(cards) + [cls._(cls._.ranks[0], card.suit) for card in cards if card.rank == cls._.ranks[-1]]
        return result
    
    @classmethod
    def get_connected(cls, cards):
        """Return list of connected cards."""
        result = []
        temp = cls._add_dummy_aces(cards)
        temp.sort()
        cutoff = range(len(temp) - 5)
        for i, card in enumerate(temp):
            if i is 0:
                result.append(card)
            else:
                diff = card.value - result[-1].value
                if diff in range(2):
                    result.append(card)
                else:
                    if i - 1 in cutoff:
                        result = [card]
        return result

    @staticmethod
    def _sort_and_reverse(cards):
        return tuple(reversed(sorted(cards)))

    @staticmethod
    def _reverse_values(values):
        return reversed(tuple(values))
    
    @classmethod
    def straight_flush(cls, c_arg, s_arg):
        for cards in s_arg:
            temp = cls._add_dummy_aces(cards)
            if len(temp) >= 5:
                in_suited_and_connected = c_arg.intersection(set(temp))
                suited_connected = cls.get_connected(in_suited_and_connected)
                if len(suited_connected) >= 5:
                    hand = cls._sort_and_reverse(suited_connected)[:5]
                    numeric_rank = 8
                    if max(hand).rank == cls._.ranks[-1]:
                        description = 'Royal Flush'
                    else:
                        p1 = max(hand).rank.name.capitalize()
                        description = ' '.join([p1, 'High Straight Flush'])
                    rank = cls.Hand.Rank(hand, numeric_rank, description)
                    return rank
        return False

    @classmethod
    def four_of_a_kind(cls, hand, r_arg):
        for item in r_arg:
            if len(item) == 4:
                kicker = max(set(hand).difference(set(item)))
                hand_ = tuple(item + [kicker])
                p1 = _pluralize(item[0].rank.name).capitalize()
                p2 = kicker.rank.name.capitalize()
                article = _article(p2[0])
                description = ' '.join(['Four', p1, 'with', article, p2, 'Kicker'])
                rank = cls.Hand.Rank(hand_, 7, description)
                return rank
        return False

    @classmethod
    def full_house(cls, r_arg):
        for cards in r_arg:
            if len(cards) == 3:
                for item in tuple(r_arg):
                    if cards != item:
                        if len(item) >= 2:
                            hand = tuple(cards + item[:2])
                            p1 = _pluralize(cards[0].rank.name).capitalize()
                            p2 = _pluralize(item[0].rank.name).capitalize()
                            description = ' '.join([p1, 'Full of', p2])
                            rank = cls.Hand.Rank(hand, 6, description)
                            return rank
        return False

    @classmethod
    def flush(cls, s_arg):
        for cards in s_arg:
            if len(cards) >= 5:
                hand = cls._sort_and_reverse(cards)[:5]
                p1 = ', '.join([card.rank.name.capitalize() for card in hand])
                description = ' '.join([p1, 'Flush'])
                rank = cls.Hand.Rank(hand, 5, description)
                return rank
        return False

    @classmethod
    def straight(cls, c_arg):
        temp = list(c_arg)
        cards = cls.sort_by_rank(c_arg)
        for item in cards.values():
            for card in item[:-1]:
                temp.remove(card)
        if len(temp) >= 5:
            hand = cls._sort_and_reverse(temp)[:5]
            p1 = max(hand).rank.name.capitalize()
            description = ' '.join([p1, 'High Straight'])
            rank = cls.Hand.Rank(hand, 4, description)
            return rank
        return False

    @classmethod
    def three_of_a_kind(cls, hand, r_arg):
        for cards in r_arg:
            if len(cards) == 3:
                kickers = cls._sort_and_reverse(set(hand).difference(set(cards)))[:2]
                hand_ = tuple(cards + list(kickers))
                p1 = _pluralize(cards[0].rank.name).capitalize()
                p2 = [kicker.rank.name.capitalize() for kicker in kickers]
                description = ' '.join(['Three', p1, 'with', p2[0], 'and', p2[1], 'Kickers'])
                rank = cls.Hand.Rank(hand_, 3, description)
                return rank
        return False

    @classmethod
    def two_pair(cls, hand, r_arg):
        for cards in r_arg:
            if len(cards) == 2:
                for item in tuple(r_arg):
                    if cards != item:
                        if len(item) == 2:
                            kicker = max(set(hand).difference(set(cards + item)))
                            hand_ = tuple(cards + item + [kicker])
                            p1 = _pluralize(cards[0].rank.name).capitalize()
                            p2 = _pluralize(item[0].rank.name).capitalize()
                            p3 = kicker.rank.name.capitalize()
                            article = _article(p3[0])
                            description = ' '.join([p1, 'and', p2, 'with', article, p3, 'Kicker'])
                            rank = cls.Hand.Rank(hand_, 2, description)
                            return rank
        return False


    @classmethod
    def pair(cls, hand, r_arg):
        for cards in tuple(r_arg):
            if len(cards) == 2:
                kickers = cls._sort_and_reverse(set(hand).difference(set(cards)))[:3]
                hand_ = tuple(cards + list(kickers))
                p1 = _pluralize(cards[0].rank.name).capitalize()
                p2 = [kicker.rank.name.capitalize() for kicker in kickers]
                description = ' '.join([p1, 'with', p2[0], p2[1], 'and', p2[2], 'Kickers'])
                rank = cls.Hand.Rank(hand_, 1, description)
                return rank
        return False

    @classmethod
    def high_card(cls, cards):
        hand = cls._sort_and_reverse(cards)[:5]
        p1 = ', '.join([card.rank.name.capitalize() for card in hand])
        description = ' '.join([p1, 'High'])
        rank = cls.Hand.Rank(hand, 0, description)
        return rank
        
    class Hand(Standard.Deck, Base):

        class Rank(Base):

            def __init__(self, hand, rank, description):
                self.hand = tuple(hand)
                print(self.hand)
                self.description = description
                print(self.description)
                values = [rank * math.pow(10, 10)] + [card.value * math.pow(10, 8 - i * 2) for i, card in enumerate(self.hand)]
                self.value = int(sum(values))
                print(self.value)
                Base.__init__(self, self.value)

            def __str__(self):
                return self.description

            def __repr__(self):
                return repr(self.hand)

        def __init__(self, *cards):
            collections.deque.__init__(self, *cards)
            self._rank = None
            Base.__init__(self, self._rank)

        @property
        def rank(self):
            if self._rank is None:
                _ = Holdem
                connected = _.get_connected(self)
                suits = _.sort_by_suit(self)
                ranks = _.sort_by_rank(self)
                s_arg = tuple(suits.values())
                r_arg = tuple(_._reverse_values(ranks.values()))
                c_arg = set(connected)
                functions = ((_.straight_flush, (c_arg, s_arg)),
                             (_.four_of_a_kind, (self, r_arg)),
                             (_.full_house, (r_arg,)),
                             (_.flush, (s_arg,)),
                             (_.straight, (c_arg,)),
                             (_.three_of_a_kind, (self, r_arg)),
                             (_.two_pair, (self, r_arg)),
                             (_.pair, (self, r_arg)),
                             (_.high_card, (self,)))
                for function, args in functions:
                    rank = function(*args)
                    if rank:
                        self._rank = rank
                        break
            return self._rank
TrustyTony 888 pyMod Team Colleague Featured Poster

Even after fixing the docstrings, could not get it to do other than generate one Deck, slicing did not work but dealing with Standard.Deck().popleft() in list comprehension got me some cards from deck.

lrh9 95 Posting Whiz in Training

The structure is admittedly confusing. I need to work on that. I had a Dealer class I forgot to include. Some code for shuffling and dealing from the deck because a deck doesn't really shuffle or deal itself. Was a poor decision that sacrificed practicality for purity. Would be easier if they were methods on the deck.

How are the docstrings broken?

I'll probably remove the class namespaces because they aren't necessary.

I'll post some usage examples later.

TrustyTony 888 pyMod Team Colleague Featured Poster

docstring from line 175 until 186 for example

TrustyTony 888 pyMod Team Colleague Featured Poster

Have you considered splitting code to modules? Just using return self.attr <cond> other.attr would compact the helper class quite a bit. On the other hand I do not like the _ variable.

It should be also possible to evaluate objects repr to recreate the object.

lrh9 95 Posting Whiz in Training

I refactored the code and added an example.

The only problem remaining as far as I know is that if two hands are of equal rank but one has higher kickers, my test code will display text suggesting that they are equal. (Differences in kickers are significant in this program though. If one hand has higher kickers than another it will be greater than the other.)

##Copyright © 2010 Larry Haskins
##This program is free software: you can redistribute it and/or modify
##it under the terms of the GNU General Public License as published by
##the Free Software Foundation, either version 3 of the License, or
##(at your option) any later version.

##This program is distributed in the hope that it will be useful,
##but WITHOUT ANY WARRANTY; without even the implied warranty of
##MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
##GNU General Public License for more details.

##You should have received a copy of the GNU General Public License
##along with this program.  If not, see <http://www.gnu.org/licenses/>.

import collections
import math
import random

_vowels = 'aeiou'

def _pluralize(word):   #The bird is indeed equal to or greater than the word!
    if word[-1] in 'xX':
        ending = 'es'
    else:
        ending = 's'
    return ''.join((word, ending))

class Suit:

    names = ('spade',
             'heart',
             'diamond',
             'club')
    symbols = ('♠',
               '♥',
               '♦',
               '♣')

    def __init__(self, symbol, name, short=None):
        self.symbol = symbol
        self.name = name
        self.short = short
        if self.short is None:
            self.short = self.name[0]

    def __repr__(self):
        return "Suit({symbol}, {name}, {short})".format(symbol=self.symbol,
                                                        name=self.name,
                                                        short=self.short)

    def __str__(self):
        return self.symbol

suits = []
for i in range(4):
    suits.append(Suit(Suit.symbols[i], Suit.names[i]))

class Rank:

    names = ('two',
             'three',
             'four',
             'five',
             'six',
             'seven',
             'eight',
             'nine',
             'ten',
             'jack',
             'queen',
             'king',
             'ace')

    def __init__(self, value, name, short=None):
        self.value = value
        self.name = name
        self.short = short
        if self.short is None:
            self.short = str(self.value)

    def __repr__(self):
        return "Rank({value}, {name}, {short})".format(value=self.value,
                                                       name=self.name,
                                                       short=self.short)

    def __str__(self):
        return self.short.capitalize()

ranks = []
for i, name in enumerate(Rank.names):
    short = None
    if i is 0:
        ranks.append(Rank(i + 1, 'ace', 'a'))
    elif i in range(8, 13):
        short = name[0]
    ranks.append(Rank(i + 2, name, short))

del i

class Card:
        
    def __init__(self, rank, suit):
        self.rank = rank
        self.value = self.rank.value
        self.suit = suit

    def __hash__(self):
        return hash((self.value, self.suit.symbol))

    def __str__(self):
        return ''.join((str(self.rank), str(self.suit)))

    def __repr__(self):
        return "Card({rank}, {suit})".format(rank=repr(self.rank),
                                             suit=repr(self.suit))

    def __eq__(self, other):
        return self.value == other.value

    def __ge__(self, other):
        return self.value >= other.value

    def __gt__(self, other):
        return self.value > other.value

    def __le__(self, other):
        return self.value <= other.value

    def __lt__(self, other):
        return self.value < other.value

    def __ne__(self, other):
        return self.value != other.value

class Cards(list):

    def __init__(self, cards=None):
        if cards is None:
            cards = ()
        list.__init__(self, cards)

    def shuffle(self):
        random.shuffle(self)

    def deal(self, num_cards=1):
        return Cards((self.pop() for i in range(num_cards)))

    def __str__(self):
        return ', '.join((str(card) for card in self))

class Deck(Cards):

    def __init__(self):
        cards = []
        for suit in suits:
            for rank in ranks[1:]:
                cards.append(Card(rank, suit))
        Cards.__init__(self, cards)

class Hand(Cards):

    def __init__(self, cards, rank, description):
        self.description = description
        Cards.__init__(self, cards)
        self.value = get_value(rank, cards)

    def __str__(self):
        return self.description

    def __eq__(self, other):
        return self.value == other.value

    def __ge__(self, other):
        return self.value >= other.value

    def __gt__(self, other):
        return self.value > other.value

    def __le__(self, other):
        return self.value <= other.value

    def __lt__(self, other):
        return self.value < other.value

    def __ne__(self, other):
        return self.value != other.value

def sort_by_suit(cards):
    global suits
    suits_ = {}
    for suit in suits:
        suits_[suit] = []
    for card in cards:
        suits_[card.suit].append(card)
    return suits_

def sort_by_rank(cards):
    global ranks
    ranks_ = collections.OrderedDict()
    for rank in ranks:
        ranks_[rank] = []
    for card in cards:
        ranks_[card.rank].append(card)
    return ranks_

def add_dummy_aces(cards):
    global ranks
    return list(cards) + [Card(ranks[0], card.suit) for card in cards if card.rank == ranks[-1]]

def sort_and_reverse(cards):
    return tuple(reversed(sorted(cards)))

def reverse_values(values):
    return tuple(reversed(tuple(values)))

def get_connected(cards):
    connected = []
    cards_ = add_dummy_aces(cards)
    cards_.sort()
    cutoff = range(len(cards_) - 5)
    for i, card in enumerate(cards_):
        if i == 0:
            connected.append(card)
        else:
            diff = card.value - connected[-1].value
            if diff in range(2):
                connected.append(card)
            else:
                if i - 1 in cutoff:
                   connected = [card]
    return connected

def straight_flush(connected, suits):
    global ranks
    for cards in suits:
        cards_ = add_dummy_aces(cards)
        if len(cards_) >= 5:
            in_suited_and_connected = connected.intersection(set(cards_))
            suited_connected = get_connected(in_suited_and_connected)
            if len(suited_connected) >= 5:
                five = sort_and_reverse(suited_connected)[:5]
                if max(five).rank == ranks[-1]:
                    description = 'Royal Flush'
                else:
                    p1 = max(five).rank.name.capitalize()
                    description = ' '.join((p1, 'High Straight Flush'))
                hand = Hand(five, 8, description)
                return hand
    return False

def four_of_a_kind(cards, ranks):
    for item in ranks:
        if len(item) == 4:
            kicker = max(set(cards).difference(set(item)))
            five = item
            five.append(kicker)
            p1 = _pluralize(item[0].rank.name).capitalize()
            description = ' '.join(('Quad', p1))
            hand = Hand(five, 7, description)
            return hand
    return False

def full_house(ranks):
    for cards in ranks:
        if len(cards) == 3:
            for item in tuple(ranks):
                if cards != item:
                    if len(item) >= 2:
                        five = cards
                        five.extend(item[:2])
                        p1 = _pluralize(cards[0].rank.name).capitalize()
                        p2 = _pluralize(item[0].rank.name).capitalize()
                        description = ' '.join((p1, 'Full of', p2))
                        hand = Hand(five, 6, description)
                        return hand
    return False

def flush(suits):
    for cards in suits:
        if len(cards) >= 5:
            five = sort_and_reverse(cards)[:5]
            p1 = max(five).rank.name.capitalize()
            description = ' '.join((p1, 'High Flush'))
            hand = Hand(five, 5, description)
            return hand
    return False

def straight(connected):
    cards = list(connected)
    cards_ = sort_by_rank(connected)
    for item in cards_.values():
        for card in item[:-1]:
            cards.remove(card)
    if len(cards) >= 5:
        five = sort_and_reverse(cards)[:5]
        p1 = max(five).rank.name.capitalize()
        description = ' '.join((p1, 'High Straight'))
        hand = Hand(five, 4, description)
        return hand
    return False

def three_of_a_kind(cards, ranks):
    for item in ranks:
        if len(item) == 3:
            kickers = sort_and_reverse(set(cards).difference(set(item)))[:2]
            five = item
            five.extend(kickers)
            p1 = _pluralize(item[0].rank.name).capitalize()
            description = ' '.join(('Trip', p1))
            hand = Hand(five, 3, description)
            return hand
    return False

def two_pair(cards, ranks):
    for item in ranks:
        if len(item) == 2:
            five = item
            for item_ in tuple(ranks):
                if item != item_:
                    if len(item_) == 2:
                        five.extend(item_)
                        kicker = max(set(cards).difference(set(tuple(five))))
                        five.append(kicker)
                        p1 = _pluralize(item[0].rank.name).capitalize()
                        p2 = _pluralize(item_[0].rank.name).capitalize()
                        description = ' '.join([p1, 'and', p2])
                        hand = Hand(five, 2, description)
                        return hand
    return False

def pair(cards, ranks):
    for item in ranks:
        if len(item) == 2:
            five = item
            kickers = sort_and_reverse(set(cards).difference(set(five)))[:3]
            five.extend(kickers)
            p1 = _pluralize(five[0].rank.name).capitalize()
            description = ' '.join(('Pair of', p1))
            hand = Hand(five, 1, description)
            return hand
    return False

def high_card(cards):
    five = sort_and_reverse(cards)[:5]
    p1 = max(five).rank.name.capitalize()
    description = ' '.join((p1, 'High'))
    hand = Hand(five, 0, description)
    return hand

def get_value(rank, cards):
    values = [rank * math.pow(10, 10)] + [card.value * math.pow(10, 8 - i * 2) for i, card in enumerate(cards)]
    return int(sum(values))

def get_hand(cards):
    connected = set(get_connected(cards))
    suits = tuple(sort_by_suit(cards).values())
    ranks = reverse_values(sort_by_rank(cards).values())
    c_and_r = (cards, ranks)
    evaluators = (
        (straight_flush, (connected, suits)),
        (four_of_a_kind, c_and_r),
        (full_house, (ranks,)),
        (flush, (suits,)),
        (straight, (connected,)),
        (three_of_a_kind, c_and_r),
        (two_pair, c_and_r),
        (pair, c_and_r),
        (high_card, (cards,))
        )
    for function, args in evaluators:
        hand = function(*args)
        if hand:
            break
    return hand

if __name__ == '__main__':
    pass
    """
    c1 = Card(ranks[1], suits[0])
    c2 = Card(ranks[2], suits[1])
    c3 = Card(ranks[4], suits[2])
    c4 = Card(ranks[5], suits[3])
    c5 = Card(ranks[7], suits[0])
    c6 = Card(ranks[8], suits[1])
    c7 = Card(ranks[-1], suits[1])
    cards = Cards((c1, c2, c3, c4, c5, c6, c7))
    hand = get_hand(cards)
    print(hand)
    print(hand.value)
    print(hand.description)
    """
    deck = Deck()
    deck.shuffle()
    p1 = deck.deal(2)
    p2 = deck.deal(2)
    board = deck.deal(5)
    c1 = Cards(p1 + board)
    c2 = Cards(p2 + board)
    h1 = get_hand(c1)
    h2 = get_hand(c2)
    if h1 < h2:
        print('Player 2 wins.')
        print(h2, 'beats', h1)
    elif h2 < h1:
        print('Player 1 wins.')
        print(h1, 'beats', h2)
    else:
        print('Player 1 ties player 2.')
        print(h1, 'ties', h2)
lrh9 95 Posting Whiz in Training
# -*- coding: utf-8 -*-

##Copyright © 2010 Larry Haskins
##This program is free software: you can redistribute it and/or modify
##it under the terms of the GNU General Public License as published by
##the Free Software Foundation, either version 3 of the License, or
##(at your option) any later version.

##This program is distributed in the hope that it will be useful,
##but WITHOUT ANY WARRANTY; without even the implied warranty of
##MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
##GNU General Public License for more details.

##You should have received a copy of the GNU General Public License
##along with this program.  If not, see <http://www.gnu.org/licenses/>.

import collections
import copy
import itertools
from random import shuffle

#Lookup tables are faster than functions for string operations.
PLURALS = {'ace': 'aces',
           'king': 'kings',
           'queen': 'queens',
           'jack': 'jacks',
           'ten': 'tens',
           'nine': 'nines',
           'eight': 'eights',
           'seven': 'sevens',
           'six': 'sixes',
           'five': 'fives',
           'four': 'fours',
           'three': 'threes',
           'two': 'twos',
           'spade': 'spades',
           'heart': 'hearts',
           'diamond': 'diamonds',
           'club': 'clubs'}

"""Next three functions use certain attributes as keys to group items
according to the value of that key."""
def pigeonholed_by_suit(cards):
    suits_ = {}
    for suit in suits:
        suits_[suit] = []
    for card in cards:
        suits_[card.suit].append(card)
    return suits_

def pigeonholed_by_rank(cards):
    ranks_ = collections.OrderedDict()
    for rank in ranks:
        ranks_[rank] = []
    for card in cards:
        ranks_[card.rank].append(card)
    return ranks_

def pigeonholed_by_len(values):
    lens = collections.OrderedDict()
    for i in (0, 1, 2, 3, 4):
        lens[i] = []
    for value in values:
        lens[len(value)].append(value)
    return lens    

"""Helper function for checking a wheel.
(Straight Flush or Straight 5, 4, 3, 2, A)"""
def dummy_aces_added(cards):
    return list(cards) + \
        [Card(ranks[0], card.suit) for card in cards if card.rank == ranks[-1]]

def sorted_and_reversed(cards):
    return sorted(cards)[::-1]

def reversed_values(values):
    return tuple(values)[::-1]

def get_connected(cards):
    connected = []
    cards_ = dummy_aces_added(cards)
    cards_.sort()
    cutoff = range(len(cards_) - 5)
    for i, card in enumerate(cards_):
        if i == 0:
            connected.append(card)
        else:
            diff = card.value - connected[-1].value
            if diff in (0, 1):
                connected.append(card)
            else:
                if i - 1 in cutoff:
                   connected = [card]
    return connected

def straight_flush(connected, flush_cards):
    cards = dummy_aces_added(flush_cards)
    in_suited_and_connected = connected.intersection(cards)
    suited_connected = get_connected(in_suited_and_connected)
    if len(suited_connected) >= 5:
        five = sorted_and_reversed(suited_connected)[:5]
        hand = Hand(five, 8)
        return hand
    return False

def quad(four, cards):
        kicker = max(cards.difference(four))
        five = four
        five.append(kicker)
        hand = Hand(five, 7)
        return hand

def boat(three, two):
    five = three + two
    hand = Hand(five, 6)
    return hand

def flush(flush_cards):
    five = sorted_and_reversed(flush_cards)[:5]
    hand = Hand(five, 5)
    return hand

def straight(connected):
    cards = copy.copy(connected)
    cards_ = pigeonholed_by_rank(connected)
    for item in cards_.values():
        for card in item[1:]:
            cards.remove(card)
    if len(cards) >= 5:
        five = sorted_and_reversed(cards)[:5]
        hand = Hand(five, 4)
        return hand
    return False

def trip(three, cards):
    kickers = sorted_and_reversed(cards.difference(three))[:2]
    five = three
    five.extend(kickers)
    hand = Hand(five, 3)
    return hand

def pairs(first, second, cards):
    five = first + second
    kicker = max(cards.difference(five))
    five.append(kicker)
    hand = Hand(five, 2)
    return hand

def pair(two, cards):
    five = two
    kickers = sorted_and_reversed(cards.difference(two))[:3]
    five.extend(kickers)
    hand = Hand(five, 1)
    return hand

def high(cards):
    five = sorted_and_reversed(cards)[:5]
    hand = Hand(five, 0)
    return hand

"""Moving the calculation of the multipliers outside of the function
resulted in a performance increase.
Only needed once."""
multipliers = [(10 ** (8 - i * 2)) for i in range(5)]

"""Computes a value for the hand.
The first digit of the number is the hand rank and every two digits
after represents a card."""
def get_value(rank, cards):
    values = [rank * (10 ** 10)] + \
             [card.value * multipliers[i] for i, card in enumerate(cards)]
    return sum(values)

"""Code may seem unwieldy or counterintuitive,
but provides a performance increase over my original implementation."""
def get_hand(cards):
    connected = set(get_connected(cards))
    length_connected = len(connected)
    suits = pigeonholed_by_suit(cards)
    flush_cards = False
    for suit in suits:
        if len(suits[suit]) >= 5:
            flush_cards = suits[suit]
    if flush_cards:
        if length_connected >= 5:
            hand = straight_flush(connected, flush_cards)
            if hand:
                return hand
    cards_ = set(cards)
    ranks = pigeonholed_by_rank(cards_)
    values = sorted_and_reversed(ranks.values())
    lengths = pigeonholed_by_len(values)
    if lengths[4]:
        return quad(lengths[4][0], cards_)
    three = False
    if lengths[3]:
        three = lengths[3][0]
    two = False
    if len(lengths[3]) >= 2:
        two = lengths[3][1][:2]
    if lengths[2]:
        two = lengths[2][0]
    if three and two:
        return boat(three, two)
    if flush_cards:
        return flush(flush_cards)
    if length_connected >= 5:
        hand = straight(connected)
        if hand:
            return hand
    if three:
        return trip(three, cards_)
    if two:
        if len(lengths[2]) >= 2:
            return pairs(two, lengths[2][1], cards_)
        else:
            return pair(two, cards_)
    return high(cards_)

class Suit:

    symbols = ('♠',
               '♥',
               '♦',
               '♣')

    longs = ('spade',
             'heart',
             'diamond',
             'club')

    def __init__(self, symbol, long, short=None):
        self.symbol = symbol
        self.long = long
        self.short = short
        if self.short is None:
            self.short = self.long[0]

    def __str__(self):
        return self.symbol

suits = [Suit(Suit.symbols[i], Suit.longs[i]) for i in range(4)]

class Rank:

    longs = ('two',
             'three',
             'four',
             'five',
             'six',
             'seven',
             'eight',
             'nine',
             'ten',
             'jack',
             'queen',
             'king',
             'ace')

    def __init__(self, value, long, short=None):
        self.value = value
        self.long = long
        self.short = short
        if self.short is None:
            self.short = str(self.value)
        self.memo = {}

    def __str__(self):
        return self.long

ranks = []
for i, long in enumerate(Rank.longs):
    short = None
    if i is 0:
        ranks.append(Rank(i + 1, 'ace', 'a'))
    elif i in range(8, 13):
        short = long[0]
    ranks.append(Rank(i + 2, long, short))

del long
del short
del i

class Card:
    
    def __init__(self, rank, suit):
        self.rank = rank
        self.suit = suit
        self.value = self.rank.value
        self.memo = {}

    def describe(self, long=False):
        try:
            description = self.memo[long]
        except KeyError:
            self.memo[None] = ''.join((self.rank.short.capitalize(),
                                       self.suit.symbol))
            self.memo[True] = ' '.join((self.rank.long.capitalize(),
                                        'of',
                                        PLURALS[self.suit.long].capitalize()))
            self.memo[False] = ''.join((self.rank.short.capitalize(),
                                        self.suit.short))
            description = self.memo[long]
        return description

    @classmethod
    def from_str(cls, string):
        try:
            string = string.strip()
        except AttributeError:
            raise TypeError('string must be a string or string-like object!')
        string = string.lower()
        if len(string) != 2:
            raise ValueError('len(string) must equal 2!')
        try:
            card = card_lookup[string]
        except KeyError:
            raise ValueError("string[0] must be in '23456789tjqka'!\n" + \
                             "string[1] must be in 'shdc'!")
        return card
    
    def __str__(self):
        return self.describe()

    def __eq__(self, other):
        return self.value == other.value

    def __ge__(self, other):
        return self.value >= other.value

    def __gt__(self, other):
        return self.value > other.value

    def __le__(self, other):
        return self.value <= other.value

    def __lt__(self, other):
        return self.value < other.value

    def __ne__(self, other):
        return self.value != other.value

    def __hash__(self):
        return hash((self.value, self.suit.symbol))

class Cards(list):

    def __init__(self, cards=()):
        list.__init__(self, cards)

    def draw(self, cards=1):
        return Cards((self.pop() for i in range(cards)))

    @classmethod
    def from_str(cls, string):
        string = string.replace(',', '')
        strings = string.split()
        cards = [Card.from_str(string) for string in strings]
        return cls(cards)

    def __str__(self):
        return ', '.join((str(card) for card in self))

#Next dozen or so lines of code deal with decks.
#Only need to create one deck since cards stay the same.
#Shallow copy of deck is faster than creating a new deck, and uses less memory.
class _Deck(Cards):

    def __init__(self):
        Cards.__init__(self, [
                    Card(*item) for item in itertools.product(ranks[1:], suits)
                    ])

_deck = _Deck()

card_lookup = {}
for card in _deck:
    card_lookup[''.join((card.rank.short, card.suit.short))] = card

def Deck():
    return copy.copy(_deck)

class Hand(Cards):

    def __init__(self, cards, rank):
        Cards.__init__(self, cards)
        self.value = get_value(rank, self)

    @classmethod
    def from_str(cls, string):
        return get_hand(Cards.from_str(string))
    
    def __eq__(self, other):
        return self.value == other.value

    def __ge__(self, other):
        return self.value >= other.value

    def __gt__(self, other):
        return self.value > other.value

    def __le__(self, other):
        return self.value <= other.value

    def __lt__(self, other):
        return self.value < other.value

    def __ne__(self, other):
        return self.value != other.value

#Sanity checks to make sure hands evaluate correctly.
def unit_test_straight_flush():
    hand = get_hand(Cards.from_str('as, ks, qs, js, ts, 5d, 6d'))
    assert hand.value == 81413121110
    hand = get_hand(Cards.from_str('td, 9d, 8d, 7d, 6d, 3h, 2h'))
    assert hand.value == 81009080706
    hand = get_hand(Cards.from_str('as, 2s, 3s, td, 5s, 4s, jd'))
    assert hand.value == 80504030201

def unit_test_quads():
    hand = get_hand(Cards.from_str('as, ad, ah, ac, 7h, 3d, tc'))
    assert hand.value == 71414141410
    hand = get_hand(Cards.from_str('ts, td, tc, 9h, th, 2s, 4s'))
    assert hand.value == 71010101009

def unit_test_boat():
    hand = get_hand(Cards.from_str('as, ad, ac, ks, kh, kd, ts'))
    assert hand.value == 61414141313
    hand = get_hand(Cards.from_str('th, tc, td, js, jc, ah, 5h'))
    assert hand.value == 61010101111

def unit_test_flush():
    hand = get_hand(Cards.from_str('as, js, ts, 5s, 5d, 9s, 3c'))
    assert hand.value == 51411100905
    hand = get_hand(Cards.from_str('6d, 4d, 2d, 9d, qd, 3c, th'))
    assert hand.value == 51209060402

def unit_test_straight():
    hand = get_hand(Cards.from_str('as, kd, qc, th, 8s, js, 5c'))
    assert hand.value == 41413121110
    hand = get_hand(Cards.from_str('as, 2c, 3h, 4d, tc, 5s, jh'))
    assert hand.value == 40504030201
    hand = get_hand(Cards.from_str('jc, th, 3s, 9d, 8s, ac, 7d'))
    assert hand.value == 41110090807
    
def unit_test_trips():
    hand = get_hand(Cards.from_str('as, ac, ad, 9h, jc, 4s, 3h'))
    assert hand.value == 31414141109
    hand = get_hand(Cards.from_str('5s, 5h, 4d, 5c, jh, 8c, as'))
    assert hand.value == 30505051411

def unit_test_pairs():
    hand = get_hand(Cards.from_str('5h, 3d, 5s, ac, ad, jh, 8c'))
    assert hand.value == 21414050511
    hand = get_hand(Cards.from_str('3c, 7d, jc, 3s, 7h, 2h, ad'))
    assert hand.value == 20707030314

def unit_test_pair():
    hand = get_hand(Cards.from_str('ac, 5h, 8d, 5s, js, th, 3c'))
    assert hand.value == 10505141110
    hand = get_hand(Cards.from_str('kd, 7h, kc, 6s, 2d, 9c, 8s'))
    assert hand.value == 11313090807

def unit_test_high():
    hand = get_hand(Cards.from_str('4h, qh, 8c, ts, 6d, ac, 2s'))
    assert hand.value == 1412100806
    hand = get_hand(Cards.from_str('ac, 2h, 3d, 4s, 6h, 7c, 8d'))
    assert hand.value == 1408070604
    
unit_test_straight_flush()
unit_test_quads()
unit_test_boat()
unit_test_flush()
unit_test_straight()
unit_test_trips()
unit_test_pairs()
unit_test_pair()
unit_test_high()

"""If the program reaches this point
the program should be evaluating hands correctly."""

if __name__ == '__main__':
    """
    Code to deal a number of hands to
    two "players" and determine and tally winner.
    
    On my 3.2GHz machine:
    500,000 executions complete in about 4 minutes and 40 seconds.
    (1786 to 1.)
    50,000 executions complete in about 29 seconds.
    (1724 to 1.)
    5,000 executions complete in about 3.7 seconds.
    (1351 to 1.)
    500 executions complete in about 0.25 seconds.
    (2000 to 1.)
    
    Sometimes a high variance from one run to another,
    but the average seems to grow fairly linearly with the number of executions.
    (That's a good thing.)
    """
    count = 5000
    current = 0
    p1w = 0
    p2w = 0
    ties = 0
    import time
    start = time.clock()
    while current < count:
        deck = Deck()
        shuffle(deck)
        p1 = deck.draw(2)
        p2 = deck.draw(2)
        board = deck.draw(5)
        c1 = p1 + board
        c2 = p2 + board
        h1 = get_hand(c1)
        h2 = get_hand(c2)
        if h1 < h2:
            p2w += 1
        elif h2 < h1:
            p1w += 1
        else:
            ties += 1
        current += 1
    end = time.clock()
    print('Time run:', end - start)
    print('Player 1 won:', p1w)
    print('Player 2 won:', p2w)
    print('Ties:', ties)
lrh9 95 Posting Whiz in Training

There is a small bug in my code. The __eq__ method for the card should be:

def __eq__(self, other):
    return hash(self) == hash(other)

I know suits aren't an issue when you compare two cards in a standard game of poker, but if you want to use deck.remove to remove a card you need this new method. Don't worry. It doesn't change hand evaluation. Other comparison methods will remain the same unless I find a reason to alter them.

TrustyTony 888 pyMod Team Colleague Featured Poster

Well done, unit test, cleaner code etc. Some places I would have written little differently, but this is much better than the function test jungle you had before!

I would like to mention that this code needs Python 3 to run.

lrh9 95 Posting Whiz in Training

Yes. I'm coding in Python 3.

If you want to propose changes, feel free. You won't be stepping on any toes. I've been collaborating with people on #python on irc.freenode.net. They've given advice and recommendations. Some I've adopted and some I've rejected.

Posting the source in this thread was a mistake. I won't make it again. I've had to split the source and make it a package. I'm going to get project hosting and then post the link.

Right now I'm working on adding code for hole cards in hold'em. My first goal is to create a collection of possible hole cards. Then I want to go through each and simulate a certain number of hands against a range of opponents for each. I will log the result, and this will create a table of percent probabilities for the hand to win/tie/lose.

lrh9 95 Posting Whiz in Training

Long time no update. I've been studying finite state machines and it has me thinking that some of this might be better solved using finite state machines. The basic idea is that you have a collection of cards you want to find a hand for. You can iterate over the cards and update the state of the hand as you go. When you get to the end of the collection you have the rank and value.

The upside to this approach is that it is time complexity O(n) where n is the length of the collection of cards you are trying to rank.

One downside is that the size of the state transition table is large. I don't remember how to calculate how large, but in Hold'em you would be evaluating a collection of 7 cards chosen from a pool of 52 cards. Plus you could have intermediate states. Statistics could tell you if you have enough memory to make this approach feasible.

A second downside is that generating the state transition table may be difficult. It's impossible to do by hand, and you'd have to be pretty clever to generate a correct state transition table (including any intermediate states) programmatically.

Just thought I'd post this so someone reading this might solve the problem another way.

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.