Logic, Data Structures
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
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)
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).
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)
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.