In the previous post, we considered the probabilities of making one specific hand with the turn/river card. This can be rather useful in specific situations, but still cannot apply thoughout a game. Poker is essentially an incomplete information game. Different from Go, where you can see all stones placed on the chessboard and thereby "solve" an optimal move, you never know you opponents' pocket cards until showdown (yet even then, people mucks). Also, you have little clue on the unshown community cards. Therefore, in order to evaluate a hand during a poker game, we'd better opt for a online evaluation algorithm instead of considering this as a DP-like problem.

# Hand Values

For the sake of convenience, the table of hand values is shown again below.

Name Description Example
High Card Simple value of the card. Lowest: deuce; highest: ace. As 4s 7h Td 2c
Pair Two cards with the same value. As 4s 7h Td Ac
Two Pairs Two pairs where each pair of cards have the same value. As 4s 4h Td Ac
Three of a Kind Three cards with the same value. As 4s 4h 4d 2c
Straight Five cards in consecutive values (ace can precede deuce or follow up king). 9s Ts Jh Qd Kc
Flush Five cards of the same suit. Ah 4h 7h Th 2h
Full House Three of a kind with the rest two making a pair. As 4s 4h 4d Ac
Four of a Kind Four cards of the same value. As 4s 4h 4d 4c
Straight Flush Straight of the same suit. 9h Th Jh Qh Kh
Royal Flush Straight flush from ten to ace. Th Jh Qh Kh Ah

# Evaluation

Our ultimate goal is to be able to evaluate the probability of a win (and tie), i.e. the relative strength of our hand. However, let's just try to get one step back and consider evaluating the absolute strengths. Here we denote it as the hand value. Ideally, there are $\binom{52}{5}=2,598,960$ possible hands but much less valid values. An intuitive idea to match all these hands to their values, which is also what I did, is to first generate a sparse mapping from hands to values, and then condense it. First we need a function that identifies hand types. Then, for hands within the same type, we encode them like a carry system (e.g. decimal); for hands across different types, we manually add offsets so that higher hand types always yield higher values. The final results are stored in the dictionary hv and serialized locally. The highest value is 6144 and here is a glance of hv:

# Simulation

Now that we have the full mapping from hands to values, there are two things we can do to calculate the probabilities:

1. Enumerate all opponent hands exhaustively, and compare hand values
2. Simulate certain number of opponent hands, and then compare

Things are gonna be much easier when we only consider the two-player case (a.k.a. heads-up games). In that case, we can literally count and evaluate all scenarios — when there're 2 pocket cards for me and 3 community cards shown, we only need to enumerate $\binom{52-5}{2}=1,081$ opponent hands, which is lightning fast to finish with modern programming languages like Python or C++. However, when we have more players, like 5, the first method gets nasty. The hands of each opponent are not independent, so we have to go through $\binom{52-5}{2\times 4} > 3\times 10^8$ situations and that, different to the heads-up scenario, would be unacceptably slow no matter which language we use. Therefore, we opt for the second method at some cost of precision. The code (partial) is shared below.

# Example

Below are two example tests.

P(win) = 5.4688%
P(tie) = 0.4883%
P(win) = 1.9531%
P(tie) = 0.7812%

Note here I also implement an interesting parameter called ranges which represents the opponents prior ranges at pre-flop. When passing an empty value, all combinations of two cards are considered. When we specify a list of ranges (numbers from 0 to 1, say $x\%$), then the opponents are assumed to only play when their pocket cards are at least in the top $x\%$ pairs of all pairs. See this table for more reasoning.