How Python Proved Me Wrong About Poker
This is my first post. It is 50% an anecdote about an experience that inspired me to start this blog, and 50% a showcase of how a domain can be modelled using a few lines of Python.
I was recently staying at a hotel situated above a casino. Curiosity got the better of me on the last day and I decided to write off what remained of my travel money. Being a gambling novice, I avoided the tables and made a beeline for the cluster of noisy and garish machines. Among various themed slot games, I found something vaguely familiar: Jacks-or-better video poker.
The premise was simple: the player is dealt five cards and can hold onto any of these, the remainder being replaced by new cards. They win money if they get a pair of jacks or higher, or any other poker hand.
Is the machine lying to me?
Despite getting an occasional winning hand, I steadily lost money. I began to suspect this was partly due to the machine hinting that I should hold the wrong cards. Going on my statistical intuition I began ignoring suggestions to hold certain cards.
Thinking on my feet
To me, the most glaringly bad suggestion was to hold a pair of anything from twos to tens. On their own merits, these would be worthless. I was being encouraged to bet on three of a kind or something better.
The probability of getting the same value card for one of the three new cards would be something like 2/50 + 2/49 + 2/48. Let's say 6/50. While holding a pair precludes straights and flushes, there is also a higher-than-normal chance of a full house and four of a kind. Despite this, I was sure that getting a completely new hand would give me a better chance of winning.
Doing my homework
When I got home I decided to crunch some numbers to put my intuition to the test.
Let's say I opted to hold a pair of twos: a two of hearts and a two of diamonds.
>>> SUITS = 'CDHS'
>>> VALUES = '23456789TJQKA'
>>> full_deck = [val + suit for suit in SUITS for val in VALUES]
>>> my_deck = full_deck.copy()
>>> hold = []
>>> hold.append(my_deck.pop(my_deck.index("2H")))
>>> hold.append(my_deck.pop(my_deck.index("2D")))
Bear with me. For a fair comparison, I need to compute all possible hands for two cases:
if I hold the pair of twos
if I don't hold anything
Python's itertools package comes in handy here.
>>> import itertools
>>> threes = list(itertools.combinations(my_deck, 3))
>>> possible_hands_if_i_hold = [[*three, *hold] for three in threes]
>>> len(possible_hands_if_i_hold)
22100
>>> all_possible_hands = list(itertools.combinations(full_deck, 5))
>>> len(all_possible_hands)
2598960
I didn't expect orders of magnitude fewer possibilities when holding, but it makes sense.
Here's the function I devised to determine if a poker hand wins.
def rank_hand(hand):
if poker.is_royal_flush(hand):
return Hands.ROYAL_FLUSH.value
elif poker.is_straight_flush(hand):
return Hands.STRAIGHT_FLUSH.value
elif poker.is_four_of_a_kind(hand):
return Hands.FOUR_OF_KIND.value
elif poker.is_full_house(hand):
return Hands.FULL_HOUSE.value
elif poker.is_flush(hand):
return Hands.FLUSH.value
elif poker.is_straight(hand):
return Hands.STRAIGHT.value
elif poker.is_three_of_a_kind(hand):
return Hands.THREE_OF_A_KIND.value
elif poker.is_two_pair(hand):
return Hands.TWO_PAIR.value
elif is_pair(hand):
return Hands.PAIR_JACKS_OR_BETTER.value
return 0
This function relies on predicates like this one:
def is_pair(hand):
"""Jacks-or-better variant of is_pair"""
hand_values, _ = zip(*hand)
hand_ranks = sorted(poker.VALUES.find(val) for val in hand_values)
rank_counter = collections.Counter(hand_ranks)
for rank, count in rank_counter.items():
if count == 2 and rank >= poker.VALUES.find("J"):
return True
return False
You can find the rest of the predicates here.
Because Python integers can be truthy (and for that matter booleans are integers e.g. True + True == 2
), rank_hand
also functions as a predicate and can be used with filter
to get a winning subset of all hands.
>>> import poker, jacks_or_better
>>> len(list(filter(jacks_or_better.rank_hand, all_possible_hands)))/ len(all_possible_hands)
0.20588235294117646
>>> len(list(filter(jacks_or_better.rank_hand, possible_hands_if_i_hold)))/ len(possible_hands_if_i_hold)
0.2816326530612245
Numbers don't lie! It turns out that I was throwing away an 8% chance of winning something by not holding onto low pairs.
Out of curiosity, what winning hands did I have with a pair of twos?
>>> ranks_if_hold = collections.Counter(sorted(map(jacks_or_better.rank_hand, possible_hands_if_i_hold)))
>>> hands_by_rank = {hand.value: hand.name for hand in jacks_or_better.Hands}
>>> for rank, count in ranks_if_hold.items(): print(hands_by_rank.get(rank), count)
None 14080
FOUR_OF_KIND 48
FULL_HOUSE 192
THREE_OF_A_KIND 2112
TWO_PAIR 3168
These aren't the best hands, but as those are rare, these are still probably the best bet.
Although gambling machines are designed to give the house at least a marginal edge on players, I wonder if I could have won at video poker with an optimal strategy.
Conclusions
There are a couple of things I can take from this:
I made more money playing an Egyptian-themed slot game than poker, although all I did was repeatedly press a button like a rat in some kind of kaleidoscopic Skinner box. I still prefer video poker as it leaves some room for the player's agency.
My intuitive grasp of statistics is shaky. I'll do some research before setting foot in a casino again.
Python is a great tool for quickly modelling a problem domain. However, my laptop takes a few seconds to rank all possible hands. Rust's speed and expressive type system might make it a better fit for further work with modelling Poker. For example, I could enumerate and classify different starting hands and factor in the payout of winning hands.
I hope you enjoyed this little post.
I'll follow up soon with more substantial writing on backend software development and cloud engineering.
Subscribe to my newsletter
Read articles from Simon Crowe directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Simon Crowe
Simon Crowe
I'm a backend engineer currently working in the DevOps space. In addition to cloud-native technologies like Kubernetes, I maintain an active interest in coding, particularly Python, Go and Rust. I started coding over ten years ago with C# and Unity as a hobbyist. Some years later I learned Python and began working as a backend software engineer. This has taken me through several companies and tech stacks and given me a lot of exposure to cloud technologies.