import numpy as np
# initial state: [1, 0, 0, ...] (start on space 0: 100%)
= np.repeat(0, 101)
state0 0] = 1
state0[
# transition matrix. T_matrix[i,j] gives the probability
# of moving from space j to space i.
= np.zeros((101, 101))
T_matrix for i in range(101):
for j in range(6):
= i + j + 1
next_space if next_space > 100: # must land on last space exactly
= i
next_space += 1/6
T_matrix[next_space, i]
# list of special transitions: (type, from, to)
# for `type`: "L" is ladder and "C" is chute (not that it matters here)
# the `from` and `to` positions are 1-based, just as they appear on the board
= [
specials "L", 1, 38],
["L", 4, 14],
["L", 9, 31],
["L", 21, 42],
["L", 28, 84],
["L", 36, 44],
["L", 51, 67],
["L", 71, 91],
["L", 80, 100],
["C", 16, 6],
["C", 47, 26],
["C", 49, 11],
["C", 56, 53],
["C", 62, 19],
["C", 64, 60],
["C", 87, 24],
["C", 93, 73],
["C", 95, 75],
["C", 98, 78]
[
]
for _, start, end in specials:
# row-based changes because anyone who thought they were going to land
# on one of these spaces is actually going to land somewhere else.
# there is zero probability of remaining on a chute or ladder space
+= T_matrix[start,:]
T_matrix[end,:] = 0 T_matrix[start,:]
10 Chutes and Ladders
10.1 Overview
Chutes and Ladders is one of my least favorite games to play. Even though the dice introduce randomness into the game, it still feels like you are just going through the motions of a deterministic process, waiting to see who comes out the winner. There are no decisions to make and no way that you can impact the game or improve your odds of winning. It feels very passive.
I remember when I was about 22, working at the University of Cincinnati, and thinking about how much I hate this game. I wondered how long the game takes to play on average. With the unfettered access university employees get to academic journals, I came across an article:
“How Long Is a Game of Snakes and Ladders?” (S.C. Althoen, L. King and K.Schilling, The Mathematical Gazette, Vol. 77, No. 478, pp. 71-76, Mar. 1993). linke here
Not only did this article provide an answer: 39.2 turns on average, it also provided an exact fractional representation of the expected game length:
\[ \frac{225837582538403273407117496273279920181931269186581786048583}{5757472998140039232950575874628786131130999406013041613400} \]
I thought that was pretty bad-ass! The paper also explained how the entire game could be modeled as a “state-absorbing Markov chain”, which sounded fun to work with. Of course, it wasn’t enough just to get the answer. I wanted to reproduce it myself and learn more about these Markov chains. So that’s exactly what I did.
10.2 Markov Processes and Chains
A Markov process is a process whose future state is entirely determined by its current state. It does not matter what has come before. The current state is enough to know what will come next. All you really need is some way to represent the current state, and then some kind of transition function you apply to the current state to get the next state. This can then be iterated forward in discrete steps to discover future states. A “Markov chain” refers to the model you build to describe the particular process.
In Chutes-and-Ladders the current state can be modeled by a 101 element vector. This vector represents the 100 positions on the board (labeled 1 to 100) and the starting position (position 0) which is off the board. The player begins in position 0, so that we initialize our state to a vector of all zeroes except for a 1 in the first element.
The transition matrix represents the roll of a die and what can happen to your player when that happens. With a 6-sided die there is a 1/6 chance of rolling each number, and each number indicates the number of states you will advance. Thus, if you are on space #10 you have a 1/6 chance of being on space #11 the next round, a 1/6 chance of being on space #12, and so on through space #16. This would be true if there were no chutes or ladders. Actually, there is a chute on space #16 that takes you back to space #6. So the transition matrix would need to be adjusted to show that you actually have a 1/6 chance of going to space #6 and a zero chance of going to #16. The R code below builds the appropriate initial state vector and transition matrix for the game:
We can then simulate a turn in the game simply by multiplying our state vector by the transition matrix. After one turn we have the following:
= state0
state = T_matrix @ state
state
def print_state(state):
"""Print non-zero entries of state vector"""
for i in range(len(state)):
if state[i] > 0:
print(f"{i}: {state[i]}")
print_state(state)
2: 0.16666666666666666
3: 0.16666666666666666
5: 0.16666666666666666
6: 0.16666666666666666
14: 0.16666666666666666
38: 0.16666666666666666
We have a 1/6 chance of ending up in spaces: 2, 3, 5, 14, 38.
When we take the next step, these probabilities start to spread out:
= T_matrix @ state
state print_state(state)
3: 0.027777777777777776
5: 0.05555555555555555
6: 0.1111111111111111
7: 0.1111111111111111
8: 0.1111111111111111
10: 0.05555555555555555
11: 0.05555555555555555
12: 0.027777777777777776
14: 0.05555555555555555
15: 0.027777777777777776
17: 0.027777777777777776
18: 0.027777777777777776
19: 0.027777777777777776
20: 0.027777777777777776
31: 0.08333333333333333
39: 0.027777777777777776
40: 0.027777777777777776
41: 0.027777777777777776
42: 0.027777777777777776
43: 0.027777777777777776
44: 0.027777777777777776
They start to reflect the various ways the game can play out based upon our first roll and second roll. And of course, we could find the probabilities describing the board after N turns with the simple equation:
\[ x_N = T^N x_0 \]
10.3 Statistics
We can now ask ourselves questions such as: how many turns will it take before we have a 50% chance of ending the game? (i.e. what is the median length of a 1 player game?)
= state0
state = 0
n_steps
while True:
= T_matrix @ state
state += 1
n_steps
if state[100] >= 0.5:
break
print(f"Median Game Length = {n_steps}") # prints 32
Median Game Length = 32
If you play the game by yourself, you will have a 50% chance of finishing the game after 32 turns.
While it is possible in theory for games to go on forever (you can always keep hitting those slides), we can put an upper bound on what is practical by measuring how many turns before we have a 99.9999% chance of finishing the game:
= state0
state = 0
n_steps
while True:
= T_matrix @ state
state += 1
n_steps
if state[100] >= 0.999999:
break
print(f"Max Game Length = {n_steps}") # prints 352
Max Game Length = 352
It is very unlikely that a 1-player game would go beyond 300 turns.
With this upper bound in mind lets build a vector containing the probabilities of ending the game at each turn:
= state0
state
= 1000
max_turns
# cumulative probability distribution where index i is the
# probability of winning after i turns
= np.zeros(max_turns + 1)
cdf_finish
for i in range(max_turns):
= T_matrix @ state
state +1] = state[100]
cdf_finish[i
= np.concat(([0], np.diff(cdf_finish)))
pdf_finish
import matplotlib.pyplot as plt
def plot_pmf(pmf, title, xlabel):
"""Plots a discrete probability mass function. `pmf` is an array
of probability where index `i` represents the probability of `i`.
"""
=(8, 4))
plt.figure(figsize#plt.stem(np.arange(len(pmf)), pmf, markerfmt='none')
len(pmf)), pmf, facecolors='none', edgecolors='black')
plt.scatter(np.arange(
plt.title(title)
plt.xlabel(xlabel)"probability")
plt.ylabel(
plt.show()
"Probability of finishing a 1-player game in a specific number of turns", "turns") plot_pmf(pdf_finish,
We can now answer all kinds of fun questions. What is the average length of a 1-player game:
= np.average(np.arange(len(pdf_finish)), weights=pdf_finish)
result print(f"Average Length of Game = {result}")
Average Length of Game = 39.22512230818299
The average game length is calculated to be 39.2 turns. This is exactly the answer that was given in the paper. (Hoo-Wah!) Actually, we match the exact fraction they give out to 9 decimal places: 39.225122308. After this, their answer is slightly higher than ours. This is likely due to us truncating our games after 1000 turns instead of extending them towards infinity. It may also be influenced by computational rounding.
10.4 Multi-Player Games
But how many times do you play Chutes and Ladders by yourself? Let’s tackle a topic that wasn’t covered in the paper: the expected length of multi-player games. We can calculate this by producing a probability distribution indicating the probability that none of the players have finished the game and then taking the complement of this:
= 1 - cdf_finish
cdf_still_playing = cdf_still_playing ** 2
cdf_still_playing2 = 1 - cdf_still_playing2
cdf_finish2 = np.concat(([0], np.diff(cdf_finish2)))
pdf_finish2
"Probability of finishing a 2-player game in a specific number of turns", "turns") plot_pmf(pdf_finish2,
As before, we can use this probability distribution to calculate the expected length of the game:
= np.average(np.arange(len(pdf_finish2)), weights=pdf_finish2)
result print(f"Average Length of Game = {result}")
Average Length of Game = 26.330956642047315
We find this to be 26.3 turns.
We can also do the same for any number of players:
def get_average_game_length(n_players, cdf_finish):
"""Calculate the average number of turns a game will last with
`n_players` playing. `cdf_finish` gives the CDF for the number
of turns it will take one player to finish the game.
"""
= 1 - cdf_finish
cdf_still_playing = cdf_still_playing ** n_players
cdf_still_playing_n = 1 - cdf_still_playing_n
cdf_finish_n = np.concat(([0], np.diff(cdf_finish_n)))
pdf_finish_n return np.average(np.arange(len(pdf_finish_n)), weights=pdf_finish_n)
for n_players in range(1, 5):
= get_average_game_length(n_players, cdf_finish)
avg_game_length = avg_game_length * n_players * 10 / 60
est_minutes print(f"| {n_players} | {avg_game_length:.6f} | {est_minutes:.6f} |")
n_players | avg_game_turns | avg_game_minutes |
---|---|---|
1 | 39.225122 | 6.537520 |
2 | 26.330957 | 8.776986 |
3 | 21.728051 | 10.864025 |
4 | 19.266975 | 12.844650 |
In the table above we have added a column for the expected game length in minutes. This is assuming that each player takes 10 seconds to move. Notice that even though the game with more players ends in fewer turns, these games actually take longer because more people have to move on each turn.
So next time someone asks if you want to play Chutes and Ladders, pull up these figures and you’ll know what you’re signing up for (on average). Let’s just hope you don’t get stuck in one of those rare 300+ turn games!