A Statistical Look at Liar's Dice

The Gambler vs The Mathematician

October 9, 2017

Liar’s dice is a great game. Easy to learn, difficult to master. A good balance of luck and strategy. If you don’t know the game, I encourage you to check it out, it’s great for a group.

In our last session, we encountered an interesting situation when we got down to the final two. One player had all 6 of his remaining dice while his opponent was down to one. Of course this led to much discussion and side betting. Mainly, we set out (read: argued) to answer the following questions:

  1. Can the player with all six dice devise an optimal strategy to guarantee a victory?
  2. If not, what are the odds for the player with only one dice?

And just like that, a bet was born. A bet not even involving the two players actually playing the game.

The Strategy

Before we get to the bet, we needed to agree on what the strategy of the player with all 6 dice would do. We pretty quickly arrived that, in order to avoid ever “bluffing,” the person with 6 should just call the highest thing they can possibly call and hope that the opponent with 1 die won’t be able to beat it. So, given dice 1, 1, 3, 4, 4, 5, he should call four 4s (1s are wild).

The Bettors

After taking minimal game theory courses in college, I tend to assume there is always some kind of strange optimal strategy. I was unable to find said strategy but I of course immediately started trying. My goal was to find some consistent strategy (one that did not allow for variable bluffing, despite it being a key component of the game) and then calculate the odds of how often it would win. A statistical (and thus, sure fire, right?) approach to the problem.

On the other side of this bet was my friend Kurt. Kurt, a long time card-player, doesn’t think about the math right away. Instead, he rapidly starts running through thousands of iterations in his head of how the game might play out. His intuition about odds and seeing the game so many times allows him to determine how often each player will win.

The Bet

My quick math showed me that the person with all 6 dice would only lose if the opponent had the same number they called, or had a 1 (wild card). Given 1, 1, 3, 4, 4, 5 and a call of four 4s, the underdog only wins when he has a 4 or a 1, thus allowing him to call five 4s and win. Rolling a 1 and another specific roll will happen 1 in 3 times and it needs to happen 6 times in a row in order to lose. In other words, only 1 in 729 times will the underdog win.

Kurt did some other kind of gambler’s voodoo magic in his head and boldly exclaimed “if you give me 1 dice against your 6 15 times in a row, I bet you I win one.” Me having just calculated the odds with 100% certainty (maybe not…) saw an opportunity to strike. I replied “you’re on” so quickly that Kurt second-guessed himself based on my confidence. I balked a bit at his request for 2:1 odds (he’s already getting odds by running it 15 times) but eventually gave in, thinking I was still a statistical lock.

6 dice vs 1 die. 15 times in a row.

Time to play.

The Result

The first few turns went exactly according to plan. I’d call something like three 5s, Kurt has a 3, game over.

For the most part, the first 10 games went great. Kurt rolled a couple 1s in there but would pretty much lose on the next turn every time. But then, somewhere around the 10th game, something happened.

I rolled 1 1 3 4 4 5 and, per the strategy, called four 4s, the most I could safely call. Kurt then comes back with four 5s, having rolled his own 5.

Oh, crap.

I lost, and he didn’t roll a duplicate or a 1. I ended up winning that game anyways, but he did get me down to three dice. I went through the next few games no problem and won the bet. But my interest had been piqued. My assumptions, and thus my odds, weren’t correct. I must find out what they actually are.

Simulation

Now realizing the problem is possibly more complicated than simple statistics can solve, I set out to write a simple simulation script that could run many many trials to get an estimate of the odds. It also seemed to be a good opportunity to test out Jupyter Notebooks, something I had been meaning to do for a while. All of the code and iPython notebooks for this project is available on GitHub.

Building out the dice randomization, general gameplay flow, and my simple rule-based-no-bluffing strategy wasn’t too tricky. The real variable in this situation is the strategy that Kurt should use.

Run 1 - The One-Upper

As a sanity check, I wanted to check my original odds and give Kurt a basic strategy. Whatever I bid, Kurt bids one more of that and hopes that he has a 1 or whatever number I called. Not smart, but a good starting point.

def get_kurts_bid(my_dice, num_dice_matt_has, matts_bid=None):  
    if matts_bid is None:
        # If Matt has not bid yet start off with 1 of my dice
        return (1, my_dice)
    else:
        # Return one more of what Matt said
        return (matts_bid[0] + 1, matts_bid[1])
Simulation Complete
-------------------
Num Simulations: 10,000,000
Took:            247.9 seconds
Kurt Wins:       13,742
Win Percent:     0.14%
Wins 1 in 727.70 times

Sure enough, 10 million simulations show that my odds of 1 in 729 were likely right. It also, perhaps more importantly, shows that the code is working. Time to start optimizing.

Run 2 - The Good Guesser

Maybe Kurt can do better by trying to trap Matt on the initial bid. Rather than letting Matt drive the action, Kurt can try and guess the number that Matt would have called anyway. An initial guess of three of whatever die Kurt rolls seems like a good guess.

def get_kurts_bid(my_dice, num_dice_matt_has, matts_bid=None):
    if my_dice == 1:
        if matts_bid is None:
            return (1, 2) 
        else:
            return (matts_bid[0] + 1, matts_bid[1])
    else:
        if matts_bid is None:
            return (3, my_dice) 
        else:
            return (matts_bid[0] + 1, matts_bid[1])
Simulation Complete
-------------------
Num Simulations: 10,000,000
Took:            243.0 seconds
Kurt Wins:       14,365
Win Percent:     0.14%
Wins 1 in 696.14 times

Not much improvement, dropped to 1 in 696 times for Kurt to win. This actually makes sense, mostly because of gameplay. Once Kurt wins the first turn, Matt gets to go first in subsequent turns. So really this strategy only affects the very first turn of the game.

Run 3 - The End Gamer

Since the previous strategy only affected the first turn, let’s see if Kurt can be smarter towards the end of the game, if he can get there.

When it gets down to Matt having two or less dice Kurt wants to guess two of whatever his dice is. The hope is Matt rolls 1, 3 and Kurt rolls 4. Matt guesses two 3s, Kurt comes back with two 4s and wins.

def get_kurts_bid(my_dice, num_dice_matt_has, matts_bid=None):
    # When Matt has 2 or less dice, guess 2 of whatever I have if I can, otherwise guess 3 of them
    if num_dice_matt_has <= 2:
        if (2, my_dice) > matts_bid:
            return (2, my_dice)
        return (3, my_dice)
    if my_dice == 1:
        if matts_bid is None:
            return (1, 2) 
        else:
            return (matts_bid[0] + 1, matts_bid[1])
    else:
        if matts_bid is None:
            return (3, my_dice) 
        else:
            return (matts_bid[0] + 1, matts_bid[1])
Simulation Complete
-------------------
Num Simulations: 10,000,000
Took:            243.5 seconds
Kurt Wins:       14,831
Win Percent:     0.15%
Wins 1 in 674.26 times

A little better, but we’re still not doing that much damage. Kurt is only getting to the final two dice about 1 in 80 times anyway. Any strategy we apply there doesn’t do too much to help overall.

Run 4 - The Finger Crosser

Let’s apply the situation that happened during the real game. As a refresher, Matt rolled 1 1 3 4 4 5 and called four 4s. Then Kurt then came back with four 5s, having rolled his own 5. So Kurt’s strategy is to guess the same quantity that Matt bids, but of the number he has instead. The hope is that Matt has only a few natural rolls of his number and the rest 1s.

def get_kurts_bid(my_dice, num_dice_matt_has, matts_bid=None):
    # When Matt has 2 or less dice, guess 2 of whatever I have if I can, otherwise guess 3 of them
    if num_dice_matt_has <= 2:
        if (2, my_dice) > matts_bid:
            return (2, my_dice)
        return (3, my_dice)
    if my_dice == 1:
        if matts_bid is None:
            return (1, 2) 
        else:
            return (matts_bid[0] + 1, matts_bid[1])
    else:
        if matts_bid is None:
            return (3, my_dice)  # Might as well try and trap him
        elif (matts_bid[0], my_dice) > matts_bid:
            return (matts_bid[0], my_dice)  # Hoping for [1, 1, 3] against [4]
        else:
            return (matts_bid[0] + 1, my_dice) # Hope he has all 1s?
Simulation Complete
-------------------
Num Simulations: 10,000,000
Took:            294.1 seconds
Kurt Wins:       89,389
Win Percent:     0.89%
Wins 1 in 111.87 times

Ooooh, progress. Kurt wins 1 in 112 times now. This situation of having all 1s and one natural actually happens a lot, especially towards the end of the game when there are fewer dice.

In the last case, if Matt calls three 4s and I have a 3, for example, Kurt would call four 3s. From what I can tell, this only happens when Matt rolls all 1s, so probably not a whole lot.

Run 5 - The Adjuster

Upon thinking more about the last strategy, I realize it’s essentially the same as the original end game strategy. Actually, it’s an even better end game strategy! The previous end game strategy of guessing 2 of whatever Kurt has (if possible) doesn’t account for a Matt roll of 3, 4 and a Kurt roll of 5. Matt guesses one 4, Kurt should guess one 5 but would previously guess two 5s.

Let’s apply the general game strategy to the end game also.

def get_kurts_bid(my_dice, num_dice_matt_has, matts_bid=None):
    if my_dice == 1:
        if matts_bid is None:
            return (1, 2) 
        else:
            return (matts_bid[0] + 1, matts_bid[1])
    else:
        if matts_bid is None:
            return (3, my_dice)  # Might as well try and trap him
        elif (matts_bid[0], my_dice) > matts_bid:
            return (matts_bid[0], my_dice)  # Hoping for [1, 1, 3] against [4]
        else:
            return (matts_bid[0] + 1, my_dice) # Hope he has all 1s
Simulation Complete
-------------------
Num Simulations: 10,000,000
Took:            301.5 seconds
Kurt Wins:       354,633
Win Percent:     3.55%
Wins 1 in 28.20 times

Whaaa!! Kurt wins this way 1 in 28 times!

It kind of makes sense maybe. Kurt just needs to get through a couple turns by rolling a 1 or the same as me and then has to count on me rolling 1s. It definitely happens, and it turns out it happens about 3.5% of the time.

Outcome

So, 15 runs at 2:1 odds are actually looking pretty good right now.

Even more importantly, it shows that Kurt’s intuition was nearly spot on. This is truly fascinating to me, just by drawing on past experiences and seeing so many dice rolls, Kurt was able to almost exactly predict the odds of this complex scenario.

At least I still won the bet.


This post has also been cross-posted to Medium. You can share and continue the conversation on the Medium post as well.