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)
-
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\$Gareth Rees– Gareth Rees2016年05月18日 22:21:15 +00:00Commented 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\$qwr– qwr2016年05月18日 23:50:20 +00:00Commented May 18, 2016 at 23:50
2 Answers 2
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
orpairs
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 populatevalue_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)
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
Explore related questions
See similar questions with these tags.