HiveBrain v1.2.0
Get Started
← Back to all entries
patternpythonMinor

Find and display best Poker hand

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
pokerhandfindanddisplaybest

Problem

This code is intended to choose the best poker hand of five out of a set of cards. The cards are represented by a list of strings, where each string contains the rank and suit (e.g., 'AC' represents an ace of clubs). The code also handles numerical ranks for the face cards, so the string could also be '14C' for ace of clubs (in Poker, ace trumps all other cards, so it's treated as 14 in the code).

Given a list of cards, such as ['2H', '5C', 'AC', 'AD', '6C', '7C', 'AS'], the code generates all possible combinations of five cards, and determines the type of hand. For example, ['2H', '5C', 'AC', 'AD', '6C'] is a one pair hand. After finding the strongest type of hand available, there may be multiple hands with the same type. Out of these, the code selects the hand with the greatest sum of the card ranks.

```
import itertools

def numeric_ranks(cards):
"""
Changes the input list of card strings to a list of
strings with numbers substituting for face cards.
ex.
numeric_ranks(['AS','3S','4S','5S','JC'])
returns ['14S','3S','4S','5S','11C']
"""
suits = get_suits(cards)
face_numbers = {'A': 14, 'J': 11, 'Q': 12, 'K': 13}
for index, card in enumerate(cards):
rank = card[0:-1]
try:
int(rank)
except:
# Rank is a letter, not a number
cards[index] = str(face_numbers[rank])+suits[index]
return cards

def get_ranks(cards):
"""
Returns a list of ints containing the rank of each card in cards.
ex.
get_ranks(['2S','3C','5C','4D','6D'])
returns [2,3,5,4,6]
"""
cards = numeric_ranks(cards) # Convert rank letters to numbers (e.g. J to 11)
return [int(card[0:-1]) for card in cards]

def get_suits(cards):
"""
Returns a list of strings containing the suit of each card in cards.
ex.
get_ranks(['2S','3C','5C','4D','6D'])
returns ['S','C','C','D','D']
"""
return [card[-1] for card in cards]

def evaluate_hand(hand):

Solution

Overall, this solution is quite good, especially if you are a beginner as you say. Each function has a clear purpose, and the docstrings are helpful. The docstrings could be improved by writing the examples as doctests:

def all_equal(lst):
    """ 
    Return True if all elements of lst are the same, False otherwise 

    >>> all_equal(['S,'S','S'])
    True
    >>> all_equal([])
    False
    """
    return len(set(lst)) == 1


Card representation

The numeric_ranks function would be better if it returned a new list instead of mutating the original list.

def numeric_ranks(cards):
    """
    Represent the card ranks numerically, with 'A' as 14, 'K' as 13,
    'Q' as 12, 'J' as 10.

    >>> numeric_ranks(['AS', '3S', '4S', '5S', 'JC'])
    ['14S', '3S', '4S', '5S', '11C']
    """
    FACE_VALUES = {'A': '14', 'J': '11', 'Q': '12', 'K': '13'}
    return [
        FACE_VALUES.get(c[:-1], c[:-1]) + c[-1:]
        for c in cards
    ]


A bigger problem with numeric_ranks, though, is that its result is "stringly typed". After parsing each card, why stuff not represent it in a data structure that is actually useful? You could write a class, but a simple alternative would be to use a tuple (preferably a namedtuple):

from collections import namedtuple

Card = namedtuple('Card', 'numeric_rank rank suit')

def parse_card(card):
    """
    Interpret the card as a namedtuple with a rank and suit.  The rank is
    represented numerically, with 'A' as 14, 'K' as 13, 'Q' as 12, 'J' as
    10.

    >>> parse_card('AS')
    Card(numeric_rank=14, rank='A', suit='♤')
    >>> parse_card('3S')
    Card(numeric_rank=3, rank='3', suit='♤')
    >>> parse_card('JC')
    Card(numeric_rank=11, rank='J', suit='♧')
    """
    FACE_VALUES = {'A': 14, 'J': 11, 'Q': 12, 'K': 13}
    PRETTY_SUITS = {'C': '\u2667', 'D': '\u2662', 'H': '\u2661', 'S': '\u2664'}
    rank, suit = card[:-1], card[-1:]
    return Card(
        numeric_rank=int(FACE_VALUES.get(rank, rank)),
        rank=rank,
        suit=PRETTY_SUITS[suit]
    )


Better yet, let's have Card take care of the pretty-printing while we're at it:

class Card(namedtuple('Card', 'numeric_rank rank suit')):
    def __str__(self):
        return self.rank + self.suit


A bonus of this tuple representation is that you get sorting by numeric rank for free:

>>> [str(c) for c in sorted(map(parse_card, ['3C', 'JS', '3S', 'AC', '10H']))]
['3♤', '3♧', '10♡', 'J♤', 'A♧']


Hand evaluation

The evaluate_hand function could be a bit more compact.

Detecting if-statements for straight / straight flush / royal flush would be more readable using a conditional expression.

The magic numbers would make more sense if you showed their derivation.

In sum([ranks.count(x) for x in ranks]), you should omit the square brackets, to make a generator expression rather than a list comprehension.

def evaluate_hand(cards):
    ranks = [card.numeric_rank for card in cards]
    suits = [card.suit for card in cards]
    if is_consecutive(ranks):
        return (
            'Straight' if not all_equal(suits) else
            'Straight flush' if max(ranks) < 14 else
            'Royal flush'
        )
    if all_equal(suits):
        return 'Flush'
    return {
        4 + 4 + 4 + 4 + 1: 'Four of a kind',
        3 + 3 + 3 + 2 + 2: 'Full house',
        3 + 3 + 3 + 1 + 1: 'Three of a kind',
        2 + 2 + 2 + 2 + 1: 'Two pair',
        2 + 2 + 1 + 1 + 1: 'One pair',
        1 + 1 + 1 + 1 + 1: 'High card',
    }[sum(ranks.count(r) for r in ranks)]


The get_best_hand function is, in essence, a kind of max() function, and should be written as such.

def best_hand(hand):
    def hand_score(cards):
        type_score = [
            'High card',
            'One pair',
            'Two pair',
            'Three of a kind',
            'Straight',
            'Flush',
            'Full house',
            'Four of a kind',
            'Straight flush',
            'Royal flush',
        ].index(evaluate_hand(cards))
        return (type_score, sum(card.numeric_rank for card in cards))

    if len(set(hand)) != len(hand):
        raise ValueError('Duplicate card in hand')
    return max(itertools.combinations(cards, 5), key=hand_score)


Suggested solution

Putting it all together…

```
from collections import namedtuple
import itertools

def all_equal(lst):
return len(set(lst)) == 1

def is_consecutive(lst):
return len(set(lst)) == len(lst) and max(lst) - min(lst) == len(lst) - 1

class Card(namedtuple('Card', 'numeric_rank rank suit')):
def __str__(self):
return self.rank + self.suit

def parse_card(card):
"""
Interpret the card as a namedtuple with a rank and suit. The rank is
represented numerically, with 'A' as 14, 'K' as 13, 'Q' as 12, 'J' as
10.

>>> parse_card('AS')
Card(numeric_rank=14, rank='A', suit='♤')
>>> parse_card('3S')
Card(numeric_rank=3, rank='3', suit='♤')
>>> parse_card(

Code Snippets

def all_equal(lst):
    """ 
    Return True if all elements of lst are the same, False otherwise 

    >>> all_equal(['S,'S','S'])
    True
    >>> all_equal([])
    False
    """
    return len(set(lst)) == 1
def numeric_ranks(cards):
    """
    Represent the card ranks numerically, with 'A' as 14, 'K' as 13,
    'Q' as 12, 'J' as 10.

    >>> numeric_ranks(['AS', '3S', '4S', '5S', 'JC'])
    ['14S', '3S', '4S', '5S', '11C']
    """
    FACE_VALUES = {'A': '14', 'J': '11', 'Q': '12', 'K': '13'}
    return [
        FACE_VALUES.get(c[:-1], c[:-1]) + c[-1:]
        for c in cards
    ]
from collections import namedtuple

Card = namedtuple('Card', 'numeric_rank rank suit')

def parse_card(card):
    """
    Interpret the card as a namedtuple with a rank and suit.  The rank is
    represented numerically, with 'A' as 14, 'K' as 13, 'Q' as 12, 'J' as
    10.

    >>> parse_card('AS')
    Card(numeric_rank=14, rank='A', suit='♤')
    >>> parse_card('3S')
    Card(numeric_rank=3, rank='3', suit='♤')
    >>> parse_card('JC')
    Card(numeric_rank=11, rank='J', suit='♧')
    """
    FACE_VALUES = {'A': 14, 'J': 11, 'Q': 12, 'K': 13}
    PRETTY_SUITS = {'C': '\u2667', 'D': '\u2662', 'H': '\u2661', 'S': '\u2664'}
    rank, suit = card[:-1], card[-1:]
    return Card(
        numeric_rank=int(FACE_VALUES.get(rank, rank)),
        rank=rank,
        suit=PRETTY_SUITS[suit]
    )
class Card(namedtuple('Card', 'numeric_rank rank suit')):
    def __str__(self):
        return self.rank + self.suit
>>> [str(c) for c in sorted(map(parse_card, ['3C', 'JS', '3S', 'AC', '10H']))]
['3♤', '3♧', '10♡', 'J♤', 'A♧']

Context

StackExchange Code Review Q#144551, answer score: 8

Revisions (0)

No revisions yet.