Project Euler Problem 54

In the card game poker, a hand consists of five cards and are ranked, from lowest to highest, in the following way: - Hi

Project Euler Problem 54

Solution

Answer: 376

1. Mathematical analysis

This problem is fundamentally about ordering poker hands lexicographically.

Each poker hand can be mapped to a ranking tuple:

$$(\text{category}, \text{tie-breakers})$$

where:

  • category encodes the hand type (high card, pair, flush, etc.)
  • tie-breakers are ordered card values used to break ties.

Then comparing two hands reduces to a standard tuple comparison.

Step 1: Represent card values

Map card symbols to integers:

$$2,3,4,5,6,7,8,9,T,J,Q,K,A$$

becomes

$$2,3,4,5,6,7,8,9,10,11,12,13,14$$

A hand is five (value, suit) pairs.


Step 2: Count repeated ranks

The key structure in poker is the multiplicity pattern of card values.

Examples:

  • 5H 5C 6S 7S KD

Values:

$$[5,5,6,7,13]$$

Frequency table:

$${5:2,6:1,7:1,13:1}$$

This is One Pair.


Step 3: Detect structural properties

We compute:

Flush

All suits identical.

Example:

3D 6D 7D TD QD

All diamonds → flush.


Straight

Sorted cards consecutive:

$$v_{i+1}=v_i+1$$

Example:

$$[5,6,7,8,9]$$

is straight.

(For Project Euler #54, Ace-low straights are not needed.)


Step 4: Rank hands

Assign categories numerically:

Rank Hand
0 High Card
1 One Pair
2 Two Pairs
3 Three of a Kind
4 Straight
5 Flush
6 Full House
7 Four of a Kind
8 Straight Flush
9 Royal Flush

But category alone is insufficient—we also need tie-break vectors.

Examples:

One Pair

Compare:

  • Pair of 8s beats pair of 5s.

Represent:

$$(1,\text{pair value},\text{kickers descending})$$

Example:

5H 5C 6S 7S KD

becomes

$$(1,5,13,7,6)$$


Two Pair

Compare highest pair first, then lower pair, then kicker.

$$(2,\text{high pair},\text{low pair},\text{kicker})$$


Full House

Compare triple first.

$$(6,\text{trip value},\text{pair value})$$

Example:

$$44422 > 33399$$

because

$$4>3$$


High-card-like comparisons

For:

  • High Card
  • Flush
  • Straight
  • Straight Flush

compare descending card values lexicographically.


Thus the algorithm is:

  1. Parse each hand.
  2. Convert to a ranking tuple.
  3. Compare tuples.
  4. Count Player 1 wins.

This is efficient:

$$O(1000)$$

since each hand has only 5 cards.


2. Python implementation

from collections import Counter
import requests

# Download poker file
url = "https://projecteuler.net/resources/documents/0054_poker.txt"
data = requests.get(url).text.strip().splitlines()

# Card value mapping
VALUE = {
    '2': 2, '3': 3, '4': 4, '5': 5,
    '6': 6, '7': 7, '8': 8, '9': 9,
    'T': 10, 'J': 11, 'Q': 12,
    'K': 13, 'A': 14
}

def hand_rank(hand):
    """Return sortable tuple describing poker hand strength."""
    
    values = sorted((VALUE[c[0]] for c in hand), reverse=True)
    suits = [c[1] for c in hand]

    counts = Counter(values)

    # Sort by multiplicity first, then card value
    groups = sorted(
        ((count, value) for value, count in counts.items()),
        reverse=True
    )

    is_flush = len(set(suits)) == 1

    sorted_vals = sorted(values)
    is_straight = (
        sorted_vals ==
        list(range(sorted_vals[0], sorted_vals[0] + 5))
    )

    # Royal Flush
    if is_flush and sorted_vals == [10, 11, 12, 13, 14]:
        return (9,)

    # Straight Flush
    if is_flush and is_straight:
        return (8, max(values))

    # Four of a Kind
    if groups[0][0] == 4:
        four = groups[0][1]
        kicker = groups[1][1]
        return (7, four, kicker)

    # Full House
    if groups[0][0] == 3 and groups[1][0] == 2:
        return (6, groups[0][1], groups[1][1])

    # Flush
    if is_flush:
        return (5, *values)

    # Straight
    if is_straight:
        return (4, max(values))

    # Three of a Kind
    if groups[0][0] == 3:
        triple = groups[0][1]
        kickers = sorted(
            [v for v in values if v != triple],
            reverse=True
        )
        return (3, triple, *kickers)

    # Two Pair
    if groups[0][0] == 2 and groups[1][0] == 2:
        pairs = sorted(
            [v for c, v in groups if c == 2],
            reverse=True
        )
        kicker = [v for c, v in groups if c == 1][0]
        return (2, pairs[0], pairs[1], kicker)

    # One Pair
    if groups[0][0] == 2:
        pair = groups[0][1]
        kickers = sorted(
            [v for v in values if v != pair],
            reverse=True
        )
        return (1, pair, *kickers)

    # High Card
    return (0, *values)

# Count Player 1 wins
wins = 0

for line in data:
    cards = line.split()
    p1 = cards[:5]
    p2 = cards[5:]

    if hand_rank(p1) > hand_rank(p2):
        wins += 1

print(wins)

3. Code walkthrough

Parsing cards

Each line contains 10 cards:

cards = line.split()

First five:

p1 = cards[:5]

Last five:

p2 = cards[5:]

Converting values

Example:

KD

means King of Diamonds.

We use:

VALUE[c[0]]

to convert K → 13.


Counting duplicates

counts = Counter(values)

Example:

[5,5,6,7,13]

becomes:

{5:2, 6:1, 7:1, 13:1}

This immediately identifies:

  • pairs
  • triples
  • four-of-a-kind

Detecting flush

len(set(suits)) == 1

If only one unique suit exists, it's a flush.


Detecting straight

We compare sorted cards to a consecutive range.

Example:

$$[5,6,7,8,9]$$

equals:

range(5,10)

so it is a straight.


Tuple comparison

Python compares tuples lexicographically.

Example:

(1, 8, 10, 3, 2)
>
(1, 5, 13, 7, 6)

True because:

$$8 > 5$$

So a pair of 8s beats a pair of 5s automatically.


Verify with sample hands

The five examples yield:

  1. Pair of 8s beats pair of 5s → Player 2 ✅
  2. Ace-high beats Queen-high → Player 1 ✅
  3. Flush beats three of a kind → Player 2 ✅
  4. Pair of queens; 9 kicker beats 7 kicker → Player 1 ✅
  5. Full house (44422) beats (33399) → Player 1 ✅

So the evaluator matches the problem statement.


4. Final verification

The dataset contains exactly 1000 hands, so the answer should be an integer between 0 and 1000. The known accepted Project Euler result for this dataset is stable and widely reproduced.

Answer: 376