The Problem with Chutes and Ladders

“Chutes and Ladders” is an old children’s game where players take turns spinning a spinner and moving their pawn between one and six spaces. Throughout the board of 100 spaces are nine ladders and ten slides (chutes?) that transport your pawn up or down. There are little illustrations describing why you were rewarded with a ladder (doing your chores, helping a wounded puppy, etc.) or punished with a slide (skating on thin ice, making a mess, etc.).

I played this game when I was young, and my own son plays it now. When he was three and four, the game helped him learn to count, and perhaps taught him some good behavior.

But when my son turned five, the game just got…boring, for me at least. There is no strategy to “Chutes and Ladders”: All we do is spin, move, spin, move until my son performs his victory dance or, if I am unlucky enough to actually win the game, he demands a rematch. To make things worse, if we are both sufficiently unlucky (by landing on numerous slides), the game can last a very long time.

But just like a senior citizen at the bingo parlor, my son is hooked, and there is no denying a five-year-old when he wants to play “Chutes and Ladders”. So I spin, move, spin, move until my mind wanders and I contemplate the finer points of the game.

For example, I always let my son go first. I wondered what advantage this gives him? (A big one, I hoped!) Calculating the answer requires playing lots of games. Luckily, R can play “Chutes and Ladders” a lot faster than we can.

Simulating the Game

The first step is to create the board. The following matrix is helpful:

chutes_ladders<-matrix(c(1:100,1:100),nrow=100,ncol=2) ##ladders chutes_ladders[1,2]=38 chutes_ladders[4,2]=14 chutes_ladders[9,2]=31 chutes_ladders[21,2]=42 chutes_ladders[28,2]=84 chutes_ladders[36,2]=44 chutes_ladders[51,2]=67 chutes_ladders[71,2]=91 chutes_ladders[80,2]=100 ##slides chutes_ladders[16,2]=6 chutes_ladders[49,2]=11 chutes_ladders[62,2]=19 chutes_ladders[87,2]=24 chutes_ladders[48,2]=26 chutes_ladders[56,2]=53 chutes_ladders[64,2]=60 chutes_ladders[93,2]=73 chutes_ladders[95,2]=75 chutes_ladders[98,2]=78 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 chutes_ladders < - matrix ( c ( 1 : 100 , 1 : 100 ) , nrow = 100 , ncol = 2 ) ##ladders chutes_ladders [ 1 , 2 ] = 38 chutes_ladders [ 4 , 2 ] = 14 chutes_ladders [ 9 , 2 ] = 31 chutes_ladders [ 21 , 2 ] = 42 chutes_ladders [ 28 , 2 ] = 84 chutes_ladders [ 36 , 2 ] = 44 chutes_ladders [ 51 , 2 ] = 67 chutes_ladders [ 71 , 2 ] = 91 chutes_ladders [ 80 , 2 ] = 100 ##slides chutes_ladders [ 16 , 2 ] = 6 chutes_ladders [ 49 , 2 ] = 11 chutes_ladders [ 62 , 2 ] = 19 chutes_ladders [ 87 , 2 ] = 24 chutes_ladders [ 48 , 2 ] = 26 chutes_ladders [ 56 , 2 ] = 53 chutes_ladders [ 64 , 2 ] = 60 chutes_ladders [ 93 , 2 ] = 73 chutes_ladders [ 95 , 2 ] = 75 chutes_ladders [ 98 , 2 ] = 78

Now, R plays the games. 10,000 games run in about 5 minutes on my laptop, and provides plenty of data.

library(data.table) games<-data.table(x=integer(),y=integer(),number=integer(),winner=character(),turns=integer()) total=10000 set.seed(1) pb <- txtProgressBar(min = 0, max = total, style = 3) for (g in 1:total){ x=0 y=0 game<-data.table(x=integer(),y=integer(),number=integer()) while(!(x==100|y==100)){ s<-sample(1:6,1) if (x+s<101){ x=x+s } x<-chutes_ladders[x,2] s<-sample(1:6,1) if(y+s<101){ y=y+s } y<-chutes_ladders[y,2] turn<-data.table(x=x,y=y,number=g) game<-rbindlist(list(game,turn),use.names=FALSE,fill=FALSE) } if(x==100){ game$winner<-"x" } else if(y==100){ game$winner<-"y" } game$turns<-nrow(game) games<-rbindlist(list(games,game),use.names=FALSE,fill=FALSE) setTxtProgressBar(pb, g) } close(pb) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 library ( data . table ) games < - data . table ( x = integer ( ) , y = integer ( ) , number = integer ( ) , winner = character ( ) , turns = integer ( ) ) total = 10000 set . seed ( 1 ) pb < - txtProgressBar ( min = 0 , max = total , style = 3 ) for ( g in 1 : total ) { x = 0 y = 0 game < - data . table ( x = integer ( ) , y = integer ( ) , number = integer ( ) ) while ( ! ( x == 100 | y == 100 ) ) { s < - sample ( 1 : 6 , 1 ) if ( x + s < 101 ) { x = x + s } x < - chutes_ladders [ x , 2 ] s < - sample ( 1 : 6 , 1 ) if ( y + s < 101 ) { y = y + s } y < - chutes_ladders [ y , 2 ] turn < - data . table ( x = x , y = y , number = g ) game < - rbindlist ( list ( game , turn ) , use . names = FALSE , fill = FALSE ) } if ( x == 100 ) { game $ winner < - "x" } else if ( y == 100 ) { game $ winner < - "y" } game $ turns < - nrow ( game ) games < - rbindlist ( list ( games , game ) , use . names = FALSE , fill = FALSE ) setTxtProgressBar ( pb , g ) } close ( pb )

The result is a data table called “games” that contains all the moves for each of the 10,000 games played. I’ve also recorded the number of turns in each game and the winner, either “player x” or “player y.”

First-Mover Advantage

What advantage is afforded the player who goes first? There are no “ties” in “Chutes and Ladders”, so the first-player advantage lies in situations where the second player could have tied on the last turn. The simulation above gives each player an equal number of turns, but awards the first player (player x) the victory in the case of a tie. In a real game, the second player (player y) wouldn’t even spin because the game would be over, but the result is the same.

ties<-games[x==100&y==100] nrow(ties) 1 2 ties < - games [ x == 100 & y == 100 ] nrow ( ties )

156 games were awarded to player x that could have ended in a tie. Assuming that player x should only have been given credit for half those games, player x has a 50.78% chance of winning each game. Not much of an edge, there.

Do Cheaters Prosper?

My son tries to give himself an edge sometimes by, ahem, cheating. At the beginning of the game, he’ll control the spinner so that it lands on “one” so he is awarded a ladder that starts on space one and transports his pawn to space 38. Another of his strategies is to (ahem) “make sure” he lands on the biggest ladder, which stretches from space 28 all the way to space 84. He is convinced that landing on the biggest ladder is a sure path to victory and he’ll “miscount” his pawn’s moves in order to experience the sublime thrill of ascending to the upper reaches of the board. Having played many games of “Chutes and Ladders”, I know that a player who is far behind can actually have a run of luck and win, so I sometimes wonder what kind of advantage those two cheats really give my son. First, let’s consider spinning a one to start the game:

games_won<-games[!duplicated(number)] x_spins_1<-games_won[x==38&!y==38] x_spins_1_count<-x_spins_1[,.N,by=winner] x_spins_1_count[,percent:=N/sum(N)] x_spins_1_count 1 2 3 4 5 games_won < - games [ ! duplicated ( number ) ] x_spins_1 < - games_won [ x == 38 & ! y == 38 ] x_spins_1_count < - x_spins_1 [ , . N , by = winner ] x_spins_1_count [ , percent : = N / sum ( N ) ] x_spins_1_count

If player x spins a one to start the game and the other player does not, player x wins 55.5% of the time.

And the biggest ladder? This code is a bit more complicated because we subset moves that landed on space 84, and then determine which of these turns were the result of the biggest ladder by looking at the previous turn:

x_space_84<-games[,previous:=shift(x,n=1,type="lag")][x==84] biggest_ladder<-x_space_84[previous<78] biggest_ladder_count<-biggest_ladder[,.N,by=winner][,percent:=N/sum(N)] biggest_ladder_count 1 2 3 4 x_space_84 < - games [ , previous : = shift ( x , n = 1 , type = "lag" ) ] [ x == 84 ] biggest_ladder < - x_space_84 [ previous < 78 ] biggest_ladder_count < - biggest_ladder [ , . N , by = winner ] [ , percent : = N / sum ( N ) ] biggest_ladder_count

If player x lands on the biggest ladder at some point in the game, he wins 60% of the time.

So, my son’s “cheating” is helping him, but less than he realizes. I think this is because the creators of the game placed enough random slides and ladders throughout the board to keep any one move from ensuring victory. I’m told this game is 1000 years old, and I would wager that kids back then cheated too.

Root Cause

The game wouldn’t be very exciting for my son if he won every time, anyway. The real cause of my boredom is the length of some games. Inevitably, the clock is drifting past bedtime, and there we are – spin, move, spin, move – all the while I am hoping I don’t win. (I really shouldn’t complain, my son is great and I love to spend time with him. But spin, move, spin, move is not exactly a bonding moment.)

The following code creates descriptive statistics about the length of the games:

mean(games_won$turns) hist(games_won$turns,breaks=100) min(games_won$turns) max(games_won$turns) 1 2 3 4 mean ( games_won $ turns ) hist ( games_won $ turns , breaks = 100 ) min ( games_won $ turns ) max ( games_won $ turns )

In the simulation, an average game lasts 26.5 turns, but it is a right-tailed distribution, so the longest game lasted 146 turns!

A few notes for the mathematically inclined: 1) The length of the game is theoretically unbounded because there is a chance, no matter how small, that both players will be cursed and land on slides forever. 2) I ran this simulation 10 times to make sure that 10,000 games produced a representative average length. It did: The mean of means was 26.49909 with a standard deviation of 0.1533848 3) There are some more sophisticated measurements of the average game length that employ Markov Chains here and here.

If only there was a way to give each game a more consistent and shorter length.

Then, it hit me. There are nine ladders and ten slides. There is a ladder missing! What if I placed a new tenth ladder on the board that consistently shortened the game? Yes, but where?!

Creating a New Ladder

The following code counts all the times each space of the board was landed on in the simulation.

All_moves<-data.table(all=c(games$x,games$y)) All_moves_count<-All_moves[,.N,by=all][order(-N)] 1 2 All_moves < - data . table ( all = c ( games $ x , games $ y ) ) All_moves_count < - All_moves [ , . N , by = all ] [ order ( - N ) ]

Reviewing the 15 most popular spaces, most are either on a ladder or a slide. This makes sense, as there are two ways to land on each of these spaces. But one space is not, space 47. This space precedes the dreaded “double slide” of spaces 48 and 49, the two spaces every child yearns desperately to skip over with a high spin.

Googling images of “Chutes and Ladders” game boards, I have noticed that on older versions of the game, sometimes called “Snakes and Ladders”, the slide on space 48 on my version starts on space 47. If this is the case on your board, replace the line chutesladders[48,2]=26 with the following: chutes_ladders[47,2]=26.

What if space 47 was the beginning of a ladder that stretched all the way to space 100?

Before you object, consider that that space 80 has just such a ladder, and players win that way all the time. Run the following code to confirm that about half of all games end with the winner landing on space 80:

x_wins<-games[,previous:=shift(x,n=1,type="lag")][x==100] x_wins[,space_80:=previous<94] x_wins_count<-x_wins[,.N,by=space_80][,percent:=N/sum(N)] 1 2 3 x_wins < - games [ , previous : = shift ( x , n = 1 , type = "lag" ) ] [ x == 100 ] x_wins [ , space_80 : = previous < 94 ] x_wins_count < - x_wins [ , . N , by = space_80 ] [ , percent : = N / sum ( N ) ]

So, adding another winning ladder on space 47 isn’t such a big deal.

The following line of code creates the ladder:

chutes_ladders[47,2]=100 1 chutes_ladders [ 47 , 2 ] = 100

Again, if your version already has a ladder on space 47, use the following code: chutes_ladders[48,2]=100

Now, re-run the simulation and produce the same descriptive statistics on the length of the game. Now, an average game lasted only 15.2 turns, and the longest game in the simulation lasted 106 turns.

games_won<-games[duplicated(number)] mean(games_won$turns) hist(games_won$turns,breaks=100) min(games_won$turns) max(games_won$turns) 1 2 3 4 5 games_won < - games [ duplicated ( number ) ] mean ( games_won $ turns ) hist ( games_won $ turns , breaks = 100 ) min ( games_won $ turns ) max ( games_won $ turns )



Unfortunately, this hack creates an explosion of games that end in three turns (see the histogram). Here’s how it happens: a player spins a one to start the game and ends up on space 38. Two lucky spins later, he lands on space 47 and wins. (My son’s cheating would pay off handsomely in this version of the game). The other problem is purely aesthetic: a ladder stretching across the board in this way would cover at least two of the illustrations.

To make a more subtle change to the game, have the new “space 47 ladder” end at space 72. The player who climbs this ladder has 1/6 chance of winning the game within two turns by landing on space 80. The mean in the simulation decreased to 22.4, the longest game was 110 turns, there are no more three-turn games, and the ladder doesn’t even cover any illustrations. Most importantly, there would be on average of 15% more “game time” we can spend playing Legos!

Conclusion

In the end, the solution to making “Chutes and Ladders” more enjoyable for adults is to spend a little time creating a paper ladder and taping it to the board. And your kids might get a kick out of the new ladder as well.