Poker Hands in Python Project Euler problem 54 asks:

The file, poker.txt, contains one-thousand random hands dealt to two players. Each line of the file contains ten cards (separated by a single space): the first five are Player 1's cards and the last five are Player 2's cards. You can assume that all hands are valid (no invalid characters or repeated cards), each player's hand is in no specific order, and in each hand there is a clear winner.
How many hands does Player 1 win?

I wrote this program rather hastily, but I also tried to make use of Python features. Here are my questions:

Are the functions clear and does eval_hand return too much (or too little) info?
Would this benefit from rewriting with OOP?
Is the code easy to understand, and how can the code be shortened or clarified? Performance is not a concern.


value_dict = {'T': 10, 'J': 11, 'Q': 12, 'K': 13, 'A': 14}

def eval_hand(hand):
    # Return ranking: high card = 0, ... royal flush = 9
    # Also return high card(s) of rank

    values = sorted([c[0] for c in hand])
    suits = [c[1] for c in hand]
    straight = (values == range(min(values), max(values)+1))
    flush = all(s == suits[0] for s in suits)

    # Should not occur (too rare)
    if straight and flush:
        if values[0] == 10:
            return 9, None
        else: return 8, max(values)

    pairs = []
    pair_present = False
    three_of_a_kind = False
    three_value = None
    for v in set(values):
        if values.count(v) == 4:
            return 7, v
        if values.count(v) == 3:
            three_of_a_kind = True
            three_value = v
        if values.count(v) == 2:
            pair_present = True
            pairs.append(v)

    if three_of_a_kind and pair_present: return 6, (three_value, pairs[0])
    if flush: return 5, None
    if straight: return 4, max(values)
    if three_of_a_kind: return 3, three_value
    if len(pairs) == 2: return 2, pairs
    if len(pairs) == 1: return 1, pairs[0]
    return 0, max(values)

def tiebreaker(hand1, hand2, hand1_info, hand2_info):
    # Return True if player 1 wins
    #print(hand1, hand1_info, hand2, hand2_info)
    assert(type(hand1_info) != list) # Shortcut, no identical Two Pairs
    assert(type(hand1_info) == int) # Flushes (None type) can't be compared
    if hand1_info != hand2_info:
        return (hand1_info &gt; hand2_info)

    values1 = sorted((c[0] for c in hand1), reverse=True)
    values2 = sorted((c[0] for c in hand2), reverse=True)
    print(values1, values2, values1 &gt; values2)
    return (values1 &gt; values2)


player1_wins = 0
ranks1 = [0]*10
ranks2 = [0]*10
with open(&quot;p054_poker.txt&quot;) as f:
    for line in f:
        s = line.split(' ')
        line_pairs = []
        for card in s:
            try:
                value = int(card[0])
            except:
                value = value_dict[card[0]]

            line_pairs.append((value, card[1]))

        hand1 = line_pairs[:5]
        hand2 = line_pairs[5:]
        hand1_rank, hand1_info = eval_hand(hand1)
        hand2_rank, hand2_info = eval_hand(hand2)

        ranks1[hand1_rank] += 1
        ranks2[hand2_rank] += 1

        if hand1_rank &gt; hand2_rank:
            player1_wins += 1

        elif hand1_rank == hand2_rank and tiebreaker(hand1, hand2, hand1_info, hand2_info):
            player1_wins += 1


#print(eval_hand([(2,'D'), (2,'D'), (1,'H'), (4,'D'), (2,'D')]))
print(ranks1)
print(ranks2)
print(player1_wins)
First impressions - this code is very clear.  I like the variable names - that makes lines like if straight and flush read very naturally.



I don't think there's a need to treat a Royal Flush separately - this is the same rank as Straight Flush, just with the largest possible high-card.  The "should not occur" comment is irrelevant - no matter how rare, we must correctly score Straight Flush (and we would expect a thorough test set to include some).



Instead of needing a separate tiebreaker function to evaluate the kickers, we could return a tuple from eval_hand, with the kickers following the rank+value.  We're already doing something like that for Full House; it's easy to extend that throughout.  I'd collect values in descending order:

values = sorted([c[0] for c in hand], reverse=True)
straight = (values == range(max(values), min(values)-1, -1)) # but see below


Then we can return as much as is necessary for the rank:

if straight and flush:
    return 8, values[0]




if three_of_a_kind and pair_present: return 6, three_value, pairs[0], values
if flush: return 5, values
if straight: return 4, values[0]
if three_of_a_kind: return 3, three_value, values
if len(pairs) == 2: return 2, pairs, values
if len(pairs) == 1: return 1, pairs[0], values




Counting pairs/trips/quads can be simplified, given that we have the values in sorted order.  We can use itertools.groupby() to get the count of identical values, rather than needing to search with values.count():

trips = []
pairs = []
for v, group in itertools.groupby(values):
    count = sum(1 for _ in group)
    if count == 4:
        return 7, v, values
    if count == 3:
        trips.append(v)
    elif count == 2:
        pairs.append(v)




Some additional test cases indicated a few problems:


The test for a straight doesn't work when I compare a list against a range (Python 3.6.5).  I needed to materialise it as a list first:

straight = values == list(range(values[0], values[-1]-1, -1))

We don't test for an ace-low straight:

straight = (values == list(range(values[0], values[0]-5, -1))
            or values == [14, 5, 4, 3, 2])

Flushes should be comparable - highest differing card still tie-breaks between flushes of different suits.


And some simplifications:


Straights and flushes can both be returned before checking for pairs/trips/quads
We don't need boolean flags to say that three_value or pairs is populated - we can directly test those variables are truthy or not.
For ranks 0, 1, 2, the rank is exactly the number of pairs found.
Instead of try/except, we can populate value_dict with the digits 2 to 9.
We're not required to find anything other than the number of Player 1 wins, so remove the code that was useful while debugging.




Applying the above, I get the following modified code:

import itertools

value_dict = {'T': 10, 'J': 11, 'Q': 12, 'K': 13, 'A': 14}
value_dict.update((str(x), x) for x in range(2,10))

def eval_hand(hand):
    # Return ranking followed by tie-break information.
    # 8. Straight Flush
    # 7. Four of a Kind
    # 6. Full House
    # 5. Flush
    # 4. Straight
    # 3. Three of a Kind
    # 2. Two pair
    # 1. One pair
    # 0. High card

    values = sorted([c[0] for c in hand], reverse=True)
    suits = [c[1] for c in hand]
    straight = (values == list(range(values[0], values[0]-5, -1))
                or values == [14, 5, 4, 3, 2])
    flush = all(s == suits[0] for s in suits)

    if straight and flush: return 8, values[1]
    if flush: return 5, values
    if straight: return 4, values[1]

    trips = []
    pairs = []
    for v, group in itertools.groupby(values):
        count = sum(1 for _ in group)
        if count == 4: return 7, v, values
        elif count == 3: trips.append(v)
        elif count == 2: pairs.append(v)

    if trips: return (6 if pairs else 3), trips, pairs, values
    return len(pairs), pairs, values


player1_wins = 0
with open("p054_poker.txt") as f:
    for line in f:
        cards = [(value_dict[x[0]], x[1]) for x in line.split(' ')]
        player1_wins += eval_hand(cards[:5]) &gt; eval_hand(cards[5:])

print(player1_wins)