Years ago, I wrote some code in Perl for the Chutes and Ladders game. I re-wrote the code here in Python.
The code simulates one player in a game to see how many spins of the dial
are needed to win the game. Many games can be simulated, set by the
-games command line option.
Here is the output for a typical run for 5 games:
game=1 spins=63
game=2 spins=25
game=3 spins=68
game=4 spins=29
game=5 spins=36
Observations based on 1 million games:
| Metric | Number of spins |
|---|---|
| Mean | 39 |
| Median | 33 |
| Mode | 22 |
| Minimum | 7 |
You'll most likely win in 22 spins.
This code merely dumps out raw data. Statistical analysis software sold separately.
I feel like I am violating
DRY
in the special_square assignment with all those 'up ladder' and 'down chute' strings.
Any feedback is welcome.
#!/usr/bin/env python3
"""
Generate statistics for the Chutes and Ladders game.
The game board has 100 squares at positions numbered 1 to 100.
There are 10 chutes and 9 ladders.
A spin of the dial results in a random number between 1 and 6.
It is not possible to climb ladder1 or ladder80 more than once in a game.
There are no such limits on the other ladders or any of the chutes.
"""
import argparse
import random
def game(game_num: int, verbose: bool=0):
"""
Run a simulation of one game.
Prints statistics for the game.
"""
# Define the squares on the board which have special behavior,
# namely a ladder or a chute.
# The keys to the outer dictionary represent
# the starting position of a chute or ladder.
special_square = {
# Ladders
1 : {'end' : 38, 'action' : 'up ladder' },
4 : {'end' : 14, 'action' : 'up ladder' },
9 : {'end' : 31, 'action' : 'up ladder' },
21 : {'end' : 42, 'action' : 'up ladder' },
28 : {'end' : 84, 'action' : 'up ladder' },
36 : {'end' : 44, 'action' : 'up ladder' },
51 : {'end' : 67, 'action' : 'up ladder' },
71 : {'end' : 91, 'action' : 'up ladder' },
80 : {'end' : 100, 'action' : 'up ladder' },
# Chutes
16 : {'end' : 6, 'action' : 'down chute'},
48 : {'end' : 26, 'action' : 'down chute'},
49 : {'end' : 11, 'action' : 'down chute'},
56 : {'end' : 53, 'action' : 'down chute'},
62 : {'end' : 19, 'action' : 'down chute'},
64 : {'end' : 60, 'action' : 'down chute'},
87 : {'end' : 24, 'action' : 'down chute'},
93 : {'end' : 73, 'action' : 'down chute'},
95 : {'end' : 75, 'action' : 'down chute'},
98 : {'end' : 78, 'action' : 'down chute'},
}
# The starting position is the imaginary location of 0,
# which is off the board
position = 0
# Spin the dial and move until you reach the top of the board
spin_count = 0
while position != 100:
# Simulate a spin of the dial, from 1 to 6
dial = random.randrange(1, 7)
spin_count += 1
# Calculate the new position based on the spin value
next_position = position + dial
# Update the position, if needed.
# Also determine what action was taken for verbose output.
if next_position > 100:
# position does not change
action = 'stay'
elif next_position in special_square:
position = special_square[next_position]['end' ]
action = special_square[next_position]['action']
else:
position = next_position
action = 'advance'
if verbose:
print(f'spin_count={spin_count:3d} dial={dial} end_position={position:3d} {action}')
print(f'game={game_num} spins={spin_count}')
parser = argparse.ArgumentParser(description = 'Chutes and Ladders stats')
parser.add_argument('-g', '--games', type=int, help='Number of games to simulate', default=1)
parser.add_argument('-v', '--verbose', help='Display of all spins', action='store_true')
args = parser.parse_args()
for i in range(1, args.games+1):
game(i, args.verbose)