Run the full house simulation for 10 million hands (and 100 million hands if it doesn’t take too long) and determine the probability of a full house. Did you get a better result?
Chapter 20 Simulating Poker Hands
This chapter will analyze poker hands using the idea of the last two chapters. If you are unfamiliar with poker or the hands of poker, here’s a quick synopsis and the Wikipedia page on Poker Hands is quite helpful. And recall from Chapter 12 that a card has both a rank (Ace, 2--10, Jack, Queen, King) and a suit (hearts, diamonds, clubs and spades).
In this chapter, we are only concerned with 5 cards with no jokers and just determining if a hand satisfies one of the following:
-
Royal Flush: the ranks of the cards are 10, J, Q, K, A and all cards have the same suit.
-
Straight Flush: the ranks are sequential and all cards have the same suit. We will allow Ace to be both high (as in a Royal Flush) and low, like, A, 2, 3, 4, 5.
-
Flush: All cards have the same suit. We will exclude straight flushes, but ace can be high or low.
-
Straight: The ranks of the 5 cards are sequential. Again, we will exclude straight flushes.
-
Four of a kind: four of the cards have the same rank
-
Full House: two cards have the same rank, the other three cards have the same rank. The suit doesn’t matter.
-
Three of a kind: three of the cards have the same rank. The other two cards do not have the same rank. The suit of the cards doesn’t matter and also, make sure that the other two cards are not a pair or that would be a full house.
-
Two pairs: Two cards have the same rank. Two of the remaining cards have the same rank, but different than the first two pair. The 5th card does not make it a full house.
-
One pair: two cards have the same rank. The remaining cards do not make it a different type of hand (full house, three of a kind, etc.)
-
No pair or nothing: the cards don’t form any other hand. This is also called High Card, in that if comparing hands, the highest card in the hand is important.
Section 20.1 A user-defined package
In Chapter 23, we will create a module, but it is helpful for the topics in this chapter to use that module. Download
PlayingCards.jl from ???. This module contains the Card and Hand types we developed in Chapter 12.
This is a module/package, which like other packages, need to be loaded with either the
using or import keyword. Since this is just a file in the current directory, we first run the file and then load it
include("PlayingCards.jl")
using .PlayingCards
where the
. represents a local (current directory) module. Note that when running a module that has been added (downloaded), no . is needed before its name.
Once we have the function written, we should test it on a few known and unknown full house hands. Try testing:
fh1 = Hand([Card(4,1),Card(4,3),Card(4,4),Card(7,1),Card(7,2)]) fh2 = Hand([Card(4,1),Card(4,3),Card(7,4),Card(7,1),Card(7,2)]) fh3 = Hand([Card(2,1),Card(4,3),Card(4,4),Card(7,1),Card(7,2)])
As entering these, you will see The first 2 are full house hands and the last is not.
Subsection 20.1.1 Writing a Full House function
To use simulation, we will need to write a function that will determine if a hand is a full house. First of all, a function template for this will look like:
function isFullHouse(h::Hand) end
We are using the naming convention that if the function returns a boolean then start with
is or has.
Here’s some other things to think about:
-
Recall that the individual cards are stored in the
cardsfield of theHandstruct. So within the function, we can access the cards withh.cardsand this is an array ofCards. -
We can access the individual cards using array notation, so the first card in the hand is
h.cards[1]. -
Recall that the rank of a card is with the
rankfield and the suit is in thesuitfield. So the rank of the 3rd card could be accessed withh.cards[3].rank.
Now before just diving into this function, if we try to determine all the possible ways to have a full house that’s a lot. That is, if the full house has sevens and threes, the sevens could be in the first card, fourth card and fifth card. But there are many other combinations. The order of the cards doesn’t matter (and often if you are playing a game with cards in that you can see your hand, you move them around), so we will first sort the cards to help us.
Within the function, if we call
local r = sort(map(c -> c.rank, h.cards))
This will result in a sorted array of the ranks of each of the cards in your hand. If we have the hand
fh1 above, the r array will be [4 4 4 7 7].
We have now reduced the problem, to a much more manageable set of options. First, if there is a full house on a hand, there are two possibilities: 1) the first three ranks are equal and the last two ranks are equal or 2) the first two ranks are equal and the last three ranks are equal.
We could put this in code, however, there’s even a bit easier way. For any full house, the first two cards are equal and the last two are equal. Then either the 2nd and 3rd card or 3rd and 4th cards’ ranks are equal, but not both. We can write this in the following way:
function isFullHouse(h::Hand)
local r=sort(map(c -> c.rank,h.cards))
r[2]==r[1] && r[5]==r[4] && r[2] != r[4] && (r[3]==r[2] || r[4]==r[3])
end
Let’s test this function on these three hands with:
isFullHouse(fh1),isFullHouse(fh2),isFullHouse(fh3)
which returns
(true, true, false) indicating that the first two are full houses and the last is not.
Section 20.2 Simulating Poker Hands
Now that we have a function to test if a hand is a full house, we want to perform a Monte Carlo simulation on a large number of poker hands and test if this gives the result we want. An easier way to do this is to develop a function that tests many different hands. In general, testing each is called a trial, so we’ll have following which passes in a function that takes a hand and a number of trials and returns the fraction of times that hand satisfies that function. First, here’s the
runTrial function.
using Random
function runTrials(f::Function, trials::Integer)
local deck=collect(1:52) # creates the array [1,2,3,...,52]
local num_hands=0
for i=1:trials
shuffle!(deck)
h = Hand(map(Card,deck[1:5])) # creates a hand of the first five cards of the shuffled deck
if(f(h))
num_hands+=1
end
end
num_hands/trials
end
Here’s a few ideas about this function:
-
The argument of the function includes a function. We will pass the
isFullHousefunction into therunTrialfunction. -
Line 3 creates an array from 1 to 52. We will use this to create a hand later.
-
The variable
num_handswill store how many hands will result in passed in function being true. -
The function
shuffle!shuffles the array calleddeckin line 6. Recall that any julia function with a ! modifies the function arguments, so this modifies the deck array instead. -
Line 7 creates a hand from the first five values of the array in
deck. This uses the constructor for a card based on an integer between 1 and 52.
If we run this with the command:
runTrials(isFullHouse,1_000_000)
This returns
0.001408, which means that the probability of drawing a full house is 0.1408%.
Subsection 20.2.1 Results of the simulation
Is the result of this simulation accurate? Similar to some of the counting techniques that we saw in Chapter 18, we can determine the probability of a full house by counting all poker hands that are full houses and dividing by the total number of poker hands. This is explained in the Wikipedia site for Poker Probabilities. Looking at full house line in the table, the probability of a full house in 0.1441\%, so this isn’t bad. If we increased the number of hands, we would expect to get closer to the true answer.
Check Your Understanding 20.1.
Section 20.3 Probabilities of Other Hands
Ultimately, it would be nice to have functions that determine if a hand is any of the ones listed at the top. We will cover two others, two pair and a flush, however the hardest part of doing this is making sure that functions aren’t double counted. For example, if we have an
isFlush function, then we need to make sure that straight flushes and royal flushes aren’t counted. Within the isFlush function, we could call isRoyalFlush and isStraightFlush however, it’s easy to get into a circular call if either of these call isFlush.
Subsection 20.3.1 Helping Functions for the Poker Hands
To help out with this problem, we will first create two functions
isOneSuit, which tests if all cards have the same suit and hasRun to test if a hand has a run of five cards. Each of the functions we’ve discussed will then call them without getting into a circular problem.
The first function is fairly straightforward with:
function isOneSuit(h::Hand)
local s = map(c -> c.suit,h.cards)
s[1]==s[2]==s[3]==s[4]==s[5]
end
Notice that first, if we are testing all suits are equal, there is no reason to sort like we did in the previous two functions. Also, the line 3 of the function is a shortcut for
s[1]==s[2] && s[2]==s[3] && s[3]==s[4] && s[4]==s[5]
which would work fine as well, but the above is clear and shorter to write.
Similarly, the following will test for a run:
function hasRun(h::Hand)
local r = sort(map(c->c.rank,h.cards))
r[2]==r[1]+1 && r[3]==r[2]+1 && r[4]==r[3]+1 && r[5]==r[4]+1 ||
r[1]==1 && r[2]==10 && r[3]==11 && r[4]==12 && r[5]==13 ## ace high run
end
The 2nd line of the function tests a run by checking if each element of the array is one more than the previous one.
Check Your Understanding 20.2.
Test the two functions we just wrote to make sure that they work.
(a)
Test
isOneSuit by creating two hands that is only one suit and two hands that are multiple suits. Make sure the function returns the correct answer.
(b)
Test
HasRun by creating two hands that has a run and two hands that don’t. Make sure the function returns the correct answer.
Subsection 20.3.2 Building a Royal Flush function
Now that we have a
isOneSuit and an hasRun functions, we can more easily build the following functions: isRoyalFlush, isStraightFlush and isStraight
We’ll start with checking for a royal flush. Simply we need to check if it is one of suit and has a run where it is an ace-high run. The following will do this:
function isRoyalFlush(h::Hand)
local r = sort(map(c -> c.rank,h.cards))
r[1]==1 && r[2]==10 && r[3]==11 && r[4]==12 && r[5]==13 && isOneSuit(h)
end
Even though we could call the
hasRun function, since we are only looking for an ace-high run, we copy the rank information over and then test additionally if it is one suit. Because there are so few royal flushes, we need to do many trials to detect. If we enter
runTrials(isRoyalFlush,10_000_000)
then we get
7.0e-7, which is the floating point version of \(7 \times 10^{-7}\) which is close to the actual probability.
Check Your Understanding 20.3.
(a)
Write the
isStraightFlush function first, by 1) checking if it has a run, 2) checking that is is one one suit and 3) checking that it is not a royal flush. This can be done in one compound boolean statement.
(b)
Write the
isStraight function by 1) checking it has a run 2) checking it is not a royal flush 3) checking it is not a straight flush. Again, this can be done in one compound boolean statement.
Subsection 20.3.3 Building a Two Pair function
Similar to the full house function, we will pull out the ranks of the hand since that is all that matters and sort the results. The we have three possibilities:
-
The first and second card ranks and 3rd and 4th card ranks are equal.
-
The first and second card ranks and 4th and 5th card ranks are equal.
-
The second and third card ranks and 4th and 5th card ranks are equal.
We can build a compound boolean statement with ors between these. The following is a possibility:
function isTwoPair(h::Hand)
local r = sort(map(c -> c.rank,h.cards))
(r[1]==r[2] && r[3] == r[4]) ||
(r[1]==r[2] && r[4] == r[5]) ||
(r[2]==r[3] && r[4] == r[5])
end
and note that we have split the statement over the last three lines just for readability. If we run this with
two_pair = runTrials(isTwoPair,1_000_000)
the result is
0.049242. Looking at the results from the Wikipedia Poker Probability page, which says that the probability is 4.7539\%, this looks high. You could try to run this again or with higher number of trials, but there’s another reason this could be high.
Since the results are larger than expected, we are probability counting other hands that we shouldn’t. It seems that the other hands we are counting could include 4 of a kind and full house. The best way to handle this would to eventually build up all of the various types of hands. Inside the
isTwoPair function we could then test for these three and return false if any are true.
function isTwoPair(h::Hand)
local r = sort(map(c -> c.rank,h.cards))
! isFullHouse(h) &&
# ! isFourOfAKind(h) ## remove the # at the beginning of the line if you have a isFourOfAKind function
( (r[1]==r[2] && r[3] == r[4]) ||
(r[1]==r[2] && r[4] == r[5]) ||
(r[2]==r[3] && r[4] == r[5]) )
end
Check Your Understanding 20.4.
Write an
isFourOfAKind function, test it and then update the isTwoPair function to exclude any four of a kind hands from being counted.
Section 20.4 Summary of Simulation
Take this chapter as an example of how to use simulation to solve problems. In short, if there is any randomization that occurs in a problem, using techniques as shown here might be a way to solve them.
