In Part 1 of this series we used the itertools module to explore poker combinatorics: the number of ways each hand can be dealt, which determines the relative likelihood of different hands. In Part 2, we'll expand on combinatorics to understand how well our hand does against our opponent's likely holdings.

from itertools import combinations, filterfalse, product, starmap

We were considering a hypothetial scenario where you hold pocket deuces and you believe your opponent has either pocket aces, pocket kings, or ace-king. In poker terminology, the list of all the hands you think someone could have based on their actions is called a range. So in this case, your opponent's range is [AA, KK, AK]. Normally it's not possible to narrow down someone's range to just a few hands - this is a simplified example for illustration.

To put a finer point on it, let's say the following action has taken place. You're playing 1-2 No Limit Hold-em. Your opponent raised to 10, you re-raised to 30, and they went all-in for 100. Should you call or fold?

The code below computes the number of combinations of pocket aces, pocket kings, ace-king suited, and ace-king offsuit respectively. For an explanation of how the code works, see Part 1.

suits = 'spades hearts diamonds clubs'.split()

aces = [('A', s) for s in suits]
kings = [('K', s) for s in suits]

AA = list(combinations(aces, 2))
KK = list(combinations(kings, 2))

AK = list(product(aces, kings))

def suited(hand):
    return hand[0][1] == hand[1][1]

AKs = list(filter(suited, AK))
AKo = list(filterfalse(suited, AK))
combos = [
    len(AA),
    len(KK),
    len(AKs),
    len(AKo)
]
combos
[6, 6, 4, 12]

We left off Part 1 with the possibly surprising observation that given two facts...

  • There are 6 combos each of pocket aces and pocket kings, compared to 16 total combos of ace-king
  • Pocket deuces is a favorite against ace-king

...you are actually a favorite more often than not in this scenario. That seems encouraging! However, you are either slightly ahead or way behind, which might put a damper on your enthusiasm.

Let's try to quantify this. To do so, you'll need to know your chances of winning against each hand in your opponent's range. In poker, this number is called equity. Calculating the equities is outside the scope of this notebook, but you can look them up with a poker equity calculator. Here's the equity of your hand, 22, against each hand in your opponent's range:

equities = [
    17.78, # AA
    18.19, # KK
    50.11, # AKs
    52.65, # AKo
]

A quick aside on equity: Analytical-minded people who don't play a lot of poker often ask some version of the question, "Isn't poker just math?" By math, I think they usually mean something like equity. Basically, how often would your hand win if you just dealt out the rest of the cards. This does seem extremely relevant, so why isn't it the whole story? Well, if you or your opponent is all-in, it is nearly the whole story. But if you're not all-in, you don't just deal out the cards and see who wins. You engage in a series of strategic interactions where you and your opponent try to outmaneuver each other. In this struggle, hands can perform better or worse than their equity would suggest. This part of poker is described by an entirely different branch of math: game theory.

Anyway, back to the situation at hand. When deciding whether to call or fold, what really matters is your equity against your opponent's range. This is the average of your equity against each hand in their range, weighted by the number of combinations. (Remember, combinations determine the relative likelihood of the hands.)

To calculate that you can use another function from the itertools module, starmap, which applies a function to each item of an iterable. In this case, to get the equity against their range, for each hand you can multiply the number of combos by your equity against that hand, and finally divide by the total number of combos to get the weighted average.

equity_vs_range = sum(starmap(operator.mul, zip(combos, equities))) / sum(combos)
equity_vs_range
37.43071428571428

So it turns out that even though you're a favorite more often than not, you only have 37% equity against their range. Not great. So you should fold, right?

The thing is, there's already a lot of money in the pot. There's the blinds of 1 and 2, plus your raise to 30, and finally your opponent's 100. You only have to call another 70 to have a chance of winning all of that.

In poker, maximizing expected value is the name of the game. By definition, the expected value of folding is zero. If you fold, you can neither win nor lose any more money. So, you should call if the expected value of calling is greater than zero. The expected value of the call is -70 (what you need to invest to call) plus what you stand to win, which is your equity against your opponent's range times the size of the final pot.

(
    -70 # what it costs to call
    + equity_vs_range / 100 # your equity vs. their range (divide by 100 because it's a percent)
    * (1 + 2 + 100 + 100) # final size of the pot
)
5.984349999999978

Even though you only have 37% equity against your opponent's range, you stand to make 6 dollars by calling because there's already so much money in the pot. Admittedly, this isn't a great spot for you - on average you'll finish the hand with less money than you started with - but maybe you should have thought of that before raising to 30.