Hackbright Code Challenges

Poker: Solution

Poker: Solution

Problem

Poker

Challenge

Harder

Concepts

Logic, Data Structures

Download

poker-solution.zip


For our solution, the eval method returns a tuple like this:

(straight_flush, # rank of high card in SF (0 if not SF)
 four_of_kind,   # rank of card in 4-of-kind (0 if none)
 full_house,     # [triple-rank, pair-rank] (0 if none)
 flush,          # 1 if so, 0 if not
 straight,       # rank of high card (0 if not straight)
 three_of_kind,  # rank of triple (0 if none)
 two_pair,       # [hi-pair, lo-pair] (0 if not 2-pair)
 pair,           # rank of pair (0 if none)
 cards           # tuple of cards in descending order
)

Tuples in this format will sort properly.

For example, for an ace-high straight flush:

>>> straight_flush = Hand("As Ks Qs Js 10s")

>>> straight_flush.eval()
(14, 0, 0, 1, 14, 0, 0, 0, [14, 13, 12, 11, 10])

This will beat anything (or tie with another ace-high straight flush).

For two pair:

>>> two_pair1 = Hand("2s 2c 3s 3c As")
>>> two_pair2 = Hand("2s 2c 9s 9c Js"])

>>> two_pair1.eval()
(0, 0, 0, 0, 0, 0, [3, 2], 3, [14, 3, 3, 2, 2])

>>> two_pair2.eval()
(0, 0, 0, 0, 0, 0, [9, 2], 9, [11, 9, 9, 2, 2])

When these get sorted, the [9, 2] sublist will sort higher than the [3, 2] sublist, so the second hand will be stronger:

>>> two_pair2 > two_pair1
True

Since we put all cards at the end, those will break ties where leftover card rank matters:

>>> pair1 = Hand("2s 2c 9s 8s 7s")
>>> pair2 = Hand("2s 2c 9s 8s 6s")

>>> pair1.eval()
(0, 0, 0, 0, 0, 0, 0, 2, [9, 8, 7, 2, 2])

>>> pair2.eval()
(0, 0, 0, 0, 0, 0, 0, 2, [9, 8, 6, 2, 2])

>>> pair1 > pair2
True

Code

poker.py
class Hand(object):  # ...
    def eval(self):
        """Evaluate the value of a hand."""

        # START SOLUTION

        from collections import Counter

        # Do we have flush? Are all suit the same as 1st?

        first_suit = self.cards[0].suit

        if all(c.suit == first_suit for c in self.cards):
            flush = 1
        else:
            flush = 0

        # For everything else, we'll set to 0 until we find we
        # have it

        straight_flush = four = straight = three = pair = 0

        full_house = [0]
        two_pair = [0]

        # Ranks of cards in hand, in descending order
        ranks = sorted([c.rank for c in self.cards],
                       reverse=True)

        # Now, here's the most helpful part
        #
        # Use the Counter() class of collections, which
        # counts how many times something appears in a list.
        # This returns an object with a most_common() method,
        # which gives us a list of tuples of (item, count),
        # and sorted hi-count to low-count.

        # So, for a hand with ranks of 9, 9, 4, 3, 2, this
        # gives us [(9, 2), (4, 1), (3, 1), (2, 1)]
        # Looking at that output, we can tell our best
        # matching is the pair-of-9s (if we had a triple
        # or four-of-kind, those would be sorted earlier
        # in the list)

        rank_counts = Counter(ranks).most_common()

        if rank_counts[0][1] == 4:
            # Most common rank has 4: we have four-of-a-kind
            # Keep track of what our 4-of-kind rank is
            four = rank_counts[0][0]

        elif rank_counts[0][1] == 3:
            # Most common rank has 3: we have three-of-kind
            # Keep track of what our 3-of-kind rank is
            three = rank_counts[0][0]

            if rank_counts[1][1] == 2:
                # We have full house
                # Keep track of [triple-rank, pair-rank]
                full_house = [
                    rank_counts[0][0], rank_counts[1][0]
                ]

        elif rank_counts[0][1] == 2:
            # We have a pair! Keep track of our pair rank
            pair = rank_counts[0][0]

            if rank_counts[1][1] == 2:
                # Second pair; need to know which pair is
                # the higher rank, so let's sort them
                two_pair = sorted(
                    [rank_counts[1][0], rank_counts[0][0]],
                    reverse=True)

                # To ensure entire evaluation tuple is right,
                # use hi-pair in the pair slot in tuple
                pair = two_pair[0]

        else:
            # We have no pairs -- all ranks are different

            # Is this a straight?
            #
            # Most straights: check if high-low rank == 4

            if ranks[0] - ranks[4] == 4:
                straight = ranks[0]

            # Special case: find a A,2,3,4,5 straight
            elif ranks == [14, 5, 4, 3, 2]:
                # Since A used as 1, high card in this is 5
                straight = 5

            # Do we have a straight flush?

            if flush and straight:
                straight_flush = straight

        # Return tuple of findings, most-powerful to least
        #
        # At end, include the card ranks, sorted high-to-low.
        # This allows ranking no-pair hands and is also used
        # to break ties for other things (if both players have
        # pairs of 6s, player with "AQJ" left over will beat
        # player with "KQJ" left over.

        return (straight_flush,
                four,
                full_house,
                flush,
                straight,
                three,
                two_pair,
                pair,
                ranks)

Bonus: Texas Hold ‘Em

In the variant of poker called Texas Hold ‘Em, players have two cards of their own, and all players share 5 community cards. A players “hand strength” is the best hand that can be made out of any combination of 5 cards drawn from the player’s cards and the community cards.

For example:

Player One: 2s 2c
Player Two: 3s 3c

Community Cards: 2d 2h 9s 8s 7s

Player 1 could make a hand of all four twos, giving her four of a kind. Player 2 could make a hand of two pair: threes and twos. So, player 1’s best hand is better than player 2’s best hand.

To begin to solve this problem, you’d want to be able to find the best hand for a player, drawing five cards from seven.

Write a function that takes a simple list of 7 cards and returns the best hand possible from it:

>>> best(["2s", "2c", "2d", "2h", "9s", "8s", "7s"])
<Hand 9s 2c 2d 2h 2s>

(The solution is below, so pause here if you want to try this out).

Solution

poker.py
def best(cards):
    """Find best hand from set of cards:

        >>> c = ["2s", "2c", "2d", "2h", "9s", "8s", "7s"]
        >>> print(best(c))
        <Hand 9s 2c 2d 2h 2s>

    (This isn't used for the main challenge, but is for the
    bonus challenge, for Texas Hold 'Em)
    """

    # itertools make this easy --- it can simply give us
    # an iterable of all 5-card combinations drawn from that
    # set of cards

    from itertools import combinations

    hands = combinations(cards, 5)

    # Find the highest-ranking of them. Note that the `max`
    # function can take a key, like sort, to use when finding
    # the maximum.

    best_hand = max(hands,
                    key=lambda h: Hand(h).eval())

    return Hand(best_hand)

Advanced Further Study

While the approach shown here is reasonable (and relatively easy to understand), there are algorithmic approaches to poker evaluation that are much faster. A good example of a program that optimizes for speed not clarity is at Deuces.