patternpythonMinor
Poker Hands in Python
Viewed 0 times
pokerpythonhands
Problem
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:
```
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)
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_handreturn 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)
Solution
First impressions - this code is very clear. I like the variable names - that makes lines like
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
Then we can return as much as is necessary for the rank:
Counting pairs/trips/quads can be simplified, given that we have the values in sorted order. We can use
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:
-
We don't test for an ace-low straight:
-
Flushes should be comparable - highest differing card still tie-breaks between flushes of different suits.
And some simplifications:
Applying the above, I get the following modified code:
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 belowThen 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], valuesCounting 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_valueorpairsis 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_dictwith 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)Code Snippets
values = sorted([c[0] for c in hand], reverse=True)
straight = (values == range(max(values), min(values)-1, -1)) # but see belowif 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], valuestrips = []
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)straight = values == list(range(values[0], values[-1]-1, -1))Context
StackExchange Code Review Q#128702, answer score: 2
Revisions (0)
No revisions yet.