5
\$\begingroup\$

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 > 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 > values2)
 return (values1 > values2)
player1_wins = 0
ranks1 = [0]*10
ranks2 = [0]*10
with open("p054_poker.txt") 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 > 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)
asked May 18, 2016 at 21:03
\$\endgroup\$
2
  • 1
    \$\begingroup\$ (i) The code does not handle ace-low straights. Luckily for you there were no ace-low straights in the Project Euler data. (ii) See §3 of this answer for some ideas about how to shorten the code. \$\endgroup\$ Commented May 18, 2016 at 22:21
  • \$\begingroup\$ @GarethRees The question specifies the order of the values, therefore I consider my code to meet the specifications of the problem (but if it were used elsewhere, ace-low would have to be handled) \$\endgroup\$ Commented May 18, 2016 at 23:50

2 Answers 2

3
\$\begingroup\$

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]) > eval_hand(cards[5:])
print(player1_wins)
answered May 31, 2018 at 14:51
\$\endgroup\$
2
\$\begingroup\$

Flush or strait are not common and the sort is relatively expensive.

If you have a pair you cannot have a strait or flush. Check the rank match first.

Would this properly have
A4455
lose to
66772

answered May 31, 2018 at 15:07
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.