Poker Hands¶

No real interesting mathematical concepts to apply in this one, more just a straight programming problem. Here's a (slightly overengineered) solution.

First we'll define enums for the rank and suit of a card.

In [1]:
from enum import Enum, IntEnum

class Rank(IntEnum):
    TWO = 2
    THREE = 3
    FOUR = 4
    FIVE = 5
    SIX = 6
    SEVEN = 7
    EIGHT = 8
    NINE = 9
    TEN = 10
    JACK = 11
    QUEEN = 12
    KING = 13
    ACE = 14
    
class Suit(Enum):
    CLUBS = "C"
    DIAMONDS = "D"
    HEARTS = "H"
    SPADES = "S"

We'll use these enums to implement a card dataclass. We define a few comparison methods so we can say what cards rank above others in certain hands. The total_ordering decorator defines the comparison methods that are left out. These other methods aren't strictly necessary, but it's a PEP 8 recommendation.

In [2]:
from dataclasses import dataclass
from functools import total_ordering

@dataclass
@total_ordering
class Card:
    rank: Rank
    suit: Suit
    
    def __init__(self, s):
        x, y = s
        
        lookup = {
            "2": Rank.TWO,
            "3": Rank.THREE,
            "4": Rank.FOUR,
            "5": Rank.FIVE,
            "6": Rank.SIX,
            "7": Rank.SEVEN,
            "8": Rank.EIGHT,
            "9": Rank.NINE,
            "T": Rank.TEN,
            "J": Rank.JACK,
            "Q": Rank.QUEEN,
            "K": Rank.KING,
            "A": Rank.ACE,
        }
        self.rank = lookup[x]
        self.suit = Suit(y)
    
    def __eq__(self, other):
        return self.rank == other.rank
    
    def __lt__(self, other):
        return self.rank < other.rank

Now we'll make an enum for comparing hand ranks.

In [3]:
class HandRank(IntEnum):
    HIGHCARD = 1
    ONEPAIR = 2
    TWOPAIR = 3
    THREEOFAKIND = 4
    STRAIGHT = 5
    FLUSH = 6
    FULLHOUSE = 7
    FOUROFAKIND = 8
    STRAIGHTFLUSH = 9

Most of the magic happens in the Hand class. We determine a hand's rank with the rank method. Then we define comparison methods to determine whether one hand beats another. If two hands have the same rank, we group the cards in each hand by each card's rank, and sort the groups primarily by frequency and secondarily by rank. We then compare these special group orderings to determine which hand wins. This allows for proper handling of situations like example 4 in the problem statement, since it will compare the ranks of the largest groups of cards first - for example, we don't want a pair of kings to lose to a pair of nines just because the latter hand also has an ace.

In [4]:
from collections import Counter

@total_ordering
class Hand:
    def __init__(self, cards):
        self.cards = cards
        
    def __eq__(self, other):
        return sorted(self.cards) == sorted(other.cards)
    
    def __lt__(self, other):
        if self.rank == other.rank:
            return self.rank_groups() < other.rank_groups()
            
        return self.rank < other.rank
    
    def rank_groups(self):
        ranks = Counter(card.rank for card in self.cards)
        groups = ((v, k) for (k, v) in ranks.most_common())
        return sorted(groups, reverse=True)
    
    @property
    def is_straight(self):
        ranks = {card.rank for card in self.cards}
        return len(ranks) == 5 and max(ranks) - min(ranks) == 4
    
    @property
    def is_flush(self):
        return len({card.suit for card in self.cards}) == 1
        
    @property
    def rank(self):
        is_straight = self.is_straight
        is_flush = self.is_flush
        
        if is_straight and is_flush:
            return HandRank.STRAIGHTFLUSH
        
        ranks = Counter(card.rank for card in self.cards)
        if 4 in ranks.values():
            return HandRank.FOUROFAKIND
        elif 3 in ranks.values() and 2 in ranks.values():
            return HandRank.FULLHOUSE
        elif is_flush:
            return HandRank.FLUSH
        elif is_straight:
            return HandRank.STRAIGHT
        elif 3 in ranks.values():
            return HandRank.THREEOFAKIND
        elif list(ranks.values()).count(2) == 2:
            return HandRank.TWOPAIR
        elif 2 in ranks.values():
            return HandRank.ONEPAIR
        else:
            return HandRank.HIGHCARD

With all these classes, all that's left is to read the file and compare each hand.

In [5]:
hands = []
with open("txt/0054_poker.txt") as f:
    for line in f:
        cards = tuple(Card(c) for c in line.split())
        hand1, hand2 = Hand(cards[:5]), Hand(cards[5:])
        hands.append((hand1, hand2))
In [6]:
wins = [i for (i, (x, y)) in enumerate(hands) if x > y]
len(wins)
Out[6]:
376

Copyright (C) 2025 filifa¶

This work is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license and the BSD Zero Clause license.