close
close

first Drop

Com TW NOw News 2024

Snakes and Ladders by @ellis2013nz
news

Snakes and Ladders by @ellis2013nz

Over five years ago I started a series of posts about gambling, but for some reason I never finished them. To partially rectify that, here is a long lost and now tidied up post about Snakes and Ladders. Read it before you next time Gaze across the smoke-filled casino table at your opponent, a sinister operative of SMERSH, and sip your vodka martini as you decide whether or not to accept his proposed bet, with him on square 90 and you on square 75…

Okay, maybe that scenario is unlikely. But one of my themes in these posts is that even games of pure chance become skill when you gamble on them. Choices like whether to bet, what odds to offer or accept, and (in some games) whether to offer, accept, or decline a doubling cube turn even a game of pure chance like Persian Monarchs into a game where the best player wins (in the long run).

First, a reminder of how Snakes and Ladders works. Here’s a stylized version of a common Snakes and Ladders board, the Milton Bradley 1952 “Chutes and Ladders” (American kids are apparently afraid of snakes) pictured on the Wikipedia page. I’ve used arrows instead of snakes or chutes and ladders, and of course I’ve left out all the extra graphics, which while interesting to a historian of morality and games, are irrelevant to gameplay. I was actually in Sri Lanka in 2018 when I started this Snakes and Ladders work, so I had a particular interest in its origins, but I don’t have time to explain that now.

You start on a virtual space 0, roll a six-sided die, and move your piece that number of spaces. If you end up on a space with a ladder, you move up to where it ends. So if your first roll is a 1, you move to space 1 and immediately climb the ladder to space 38. If you end up on a snake, you move down to the snake’s tail.

Here’s the R code to draw that sign:

library(tidyverse)
library(foreach)
library(doParallel)

# add in the snakes and ladders:
# see https://en.wikipedia.org/wiki/Snakes_and_Ladders 
# "In the original game the squares of virtue are: Faith (12), Reliability (51), Generosity (57), 
# Knowledge (76), and Asceticism (78). The squares of vice or evil are: Disobedience (41), 
# Vanity (44), Vulgarity (49), Theft (52), Lying (58), Drunkenness (62), Debt (69), Murder (73), 
# Rage (84), Greed (92), Pride (95), and Lust (99).(8)


# Milton Bradley version, from image on Wikipedia.  This is identical
# to the 10 snakes and 9 ladders listed in Althoen et al:
snakes_and_ladders %
  mutate(id = 1:n())

# the board is a vector of numbers that basically show where you end up

board %
  mutate(board_row = c(0, rep(1:nrows, each = nrows)),
         board_col = c(0, rep(1:nrows, nrows)),
         sequence = 0:n) %>%
  mutate(board_col = ifelse(board_row %% 2 == 0 & sequence != 0, 
                            11 - board_col, board_col)) %>%
  select(board_row : sequence)

sl_df %
  left_join(board_df( , c("sequence", "board_row", "board_col")), by = c("starting" = "sequence")) %>%
  left_join(board_df( , c("sequence", "board_row", "board_col")), by = c("ending" = "sequence"))

ggplot(board_df, aes(y = board_row, x = board_col)) +
  geom_tile(colour = "white", fill = "steelblue") +
  
  geom_curve(data = sl_df, aes(y = board_row.x, x = board_col.x,
                                 yend = board_row.y, xend = board_col.y,
                               colour = type),
               arrow = arrow(type = "closed", length = unit(0.1, "inches")),
               curvature = 0.2, size = 2) +
  geom_text(aes(label = sequence)) +
  scale_colour_manual(values = c(Ladder = "white", Snake = "orange")) +
  theme_void(base_family = "Roboto")  +
  theme(legend.position = "none", 
        plot.title = element_text(family = heading_font) ) +
  coord_equal() +
  ggtitle("Snakes and ladders board")

Now, the Wikipedia page claims that Snakes and Ladders can be represented exactly as an absorbing Markov chain, since the transition probability from any square to any other square is fixed and easily defined, and does not depend on the path the counter took to get to that square. However, this model is only useful if you drop the general rule that rolling a six gets you a re-roll; and rolling three sixes sends you “back to square one” (in fact, Snakes and Ladders is the origin of this common English expression).

I want a realistic simulation of the game as it is actually played, so after playing around with Markov chains for a while I realized that it would be much easier to write code that mimics how the players do it. That’s what I used to simulate 10,000 solo games to create this animation of a hundred Snakes and Ladders games:

The code for this is below. All the heavy lifting is done by the sl_game() function, which takes the player’s current position as argument (zero by default) – we’ll use this more later.

#---play the game---------

sl_game 
    left_join(board_df, by = c("square" = "sequence"))
  
  p 
  gather(variable, value) |>
  group_by(variable) |>
  summarise(mean(value),
            median(value))

ggplot(number_turns, aes(x= turns)) +
  geom_density(fill = "blue", alpha = 0.2) +
  labs(x = "Number of moves before winning",
       title = "Distribution of length of one player snakes and ladders games",
       subtitle = "6 gets you another role, three 6s is back to square 1.
'Bounce back' rule applies when trying to finish exactly on 100")

Here is the outcome of that analysis of the number of turns and rolls (remember you can get more rolls than turns, if you get sixes)

  variable `mean(value)` `median(value)`
                         
1 rolls             42.7              34
2 turns             35.0              28

It is a bit more complicated than the average 39.2 dice throws that can be calculated analytically as the average number of throws required under the simplified version modeled with a Markov chain. The expected number of throws is higher because of ‘back to square one’; the number of turns is lower because of the bonus throws you get when you get a six.

The distribution of the number of throws required is very skewed with a long tail – it is in fact possible for a game to go on forever (although this is highly unlikely):

Okay, where does gambling come from? Gambling becomes interesting when there is a choice about the time determination or chances of a bet. If all bets were an equal stake wagered for the game, then Snakes and Ladders would remain a game of pure chance. But if it is possible, say at the beginning of your turn, to assess the board and say “I bet on equal odds that I will win,” then you are making a choice based on your knowledge of the game. A naive observer might think that anyone on a higher square has a better chance of winning, and therefore might fall into the trap of making a bet that would only be fair if they were given favorable odds.

To investigate this with Snakes and Ladders, I simulated a thousand solo games from every square that is a valid starting position for a turn (e.g. excluding squares that are at the bottom of a ladder – you cannot end a turn on that square, because if you land there, you immediately go up the ladder). This gives us a probability distribution for how many turns it is expected to take to win from that point. If we do a full join of this distribution to itself, we get a joint probability for how many turns it will take for Player 1 to win from that position and any combination of Player 2’s starting positions.

We can visualize the result in a graph like this. The highlight boxes are where player 1, who is about to roll, has a surprisingly good chance of winning (greater than 0.55), despite being behind player 2 in the race. These are the likely odds of offering a 50:50 bet to your opponent; only an unusually disciplined or knowledgeable player would think he was losing in this position.

.

These are some of those positions:

   start_p1 start_p2    p1    p2 unusual how_surprising
                         
 1       65       81 0.611 0.389 TRUE             0.761
 2       66       81 0.607 0.393 TRUE             0.746
 3       65       82 0.590 0.410 TRUE             0.744
 4       67       81 0.615 0.385 TRUE             0.743
 5       68       81 0.622 0.378 TRUE             0.740
 6       69       81 0.626 0.374 TRUE             0.735
 7       63       81 0.570 0.430 TRUE             0.733
 8       22       29 0.555 0.445 TRUE             0.731
 9       74       81 0.667 0.333 TRUE             0.730
10       66       82 0.587 0.413 TRUE             0.729

The how_surprising column is a metric I came up with that attempts to incorporate both player 1’s high probability of winning and how far along they currently are corpses to be behind. So if you are player 1 on square 65 and you see your opponent on square 81, this is the time to offer them a bet.

Intuitively, why are 81 and 82 bad spaces? It’s because you missed the ladders from 80 straight to 100 (instant win) and from 71 to 90. You still have snakes on 93, 95, and 98 that can trip you up, while your opponent can skip them altogether if they land on space 80 – which is a non-trivial 1/6 chance for them.

Also square 29 is a bad square, because you just missed the big ladder that starts on square 28, while your opponent, who is apparently behind you, still has a chance to get on it.

Here is the code for running the simulations and drawing the graph:

#-----------------chance of winning from different positions------------
# set up parallel processing cluster
cluster 
  group_by(start) |>
  summarise(avg_turns = mean(turns))

# distribution of results - count of number of turns it takes to win
# from each starting position
distrib 
  count(start, turns) |>
  group_by(start) |>
  mutate(prop = n / sum(n)) |>
  ungroup() |>
  mutate(link = 1) |>
  select(start, turns, prop, link)

# for each combination of starting positions and number of turns to win,
# who wins with one player at one position and another player at another
# (caution this makes a very big object because of the full join)
who_wins 
  left_join(distrib, by = "link", 
            suffix = c("_p1", "_p2"), 
            relationship = "many-to-many") |>
  mutate(prob = prop_p1 * prop_p2) |>
  group_by(start_p1, start_p2) |>
  summarise(p1 = sum(prob(turns_p1  turns_p2))) |>
  mutate(unusual = p1 > 0.55 & start_p2 > start_p1)
  
# Visualise the chance of winning from various positions
who_wins |>
  ggplot(aes(x = start_p1, y = start_p2, fill = p1)) +
  geom_tile() +
  geom_tile(data = filter(who_wins, unusual), fill = "white", colour = "black") +
  scale_fill_gradientn(colours = c("red", "white", "blue")) +
  labs(x = "Player one current square",
       y = "Player 2 current square",
       fill = "Probability of player one winning",
       subtitle = "Rules: 6 gets you another roll. Three 6s is back to square 1. 'Bounce back' rule applies if you don't land
exactly on square 100. Player 1 has the dice.
Black squares indicate situations where it is worth betting on Player 1 even though they are behind",
       title = "Chance of winning a standard snakes and ladders game at different positions")

# some examples where it would be worth putting money on player 1 even
# though they are behind
who_wins |>
  filter(unusual) |>
  mutate(how_surprising = start_p2 / start_p1 * p1) |>
  arrange(desc(how_surprising))

Finally, I was wondering if a doubling cube is useful, given my interest in backgammon. In backgammon, at the beginning of your turn (before you roll the dice) you have the opportunity to offer your opponent the doubling cube. They can either accept, in which case the game is now played for twice the points/stakes; or refuse, in which case they immediately lose the game at their current stake. Once they have accepted the cube, only they can offer it again (probably if fate turns in their favor).

Generally speaking – aside from a few backgammon-specific complications – it makes sense to accept the doubling cube if you have a 1 in 4 or better chance of winning. If you have a 0.75 chance of winning and have the cube available, you should absolutely offer the double; and your opponent should decline. Now, I’ve never heard of Snakes and Ladders with a doubling cube, but I’m sure it’s happened (or will happen in the future). So it’s worth highlighting what are the points at which Player 2 should decline the cube if it’s offered and accept the loss at the current stakes? This chart answers that for us (although a table would probably be more useful for actual use):

Made with this little piece of extra code:

who_wins |>
  ggplot(aes(x = start_p1, y = start_p2, fill = p1)) +
  geom_tile() +
  geom_tile(data = filter(who_wins, p2 

Okay guys, that’s it. Remember to gamble responsibly. Especially if you use this post to scam children, their sorrow will rest on your conscience, and I absolutely disclaim all responsibility for any misuse of any kind of the above material.