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.
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.
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.
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.
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.
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))
wins = [i for (i, (x, y)) in enumerate(hands) if x > y]
len(wins)
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.