The Invent with Python Blog

Wed 17 October 2018

Writing Bots to Play Zombie Dice

Posted by Al Sweigart in python   

Note: I'm looking for feedback for my Zombie Dice simulator, as well as for this set of instructions. If there's any part that you feel could be improved, please email me at [email protected].

Programming games are a games genre where instead of playing the game directly, players write bot programs to play the game autonomously. I've created a Zombie Dice simulator, allowing programmers to practice their skills while having fun making game-playing AIs. Zombie Dice bots can be simple or incredibly complex, and are great for a class exercise or an individual programming challenge.

How to Play Zombie Dice

Zombie Dice is a quick, fun dice game from Steve Jackson Games. The players are zombies trying to eat as many human brains as possible without getting "shotgunned". There is a cup of 13 dice with icons of brains, footsteps, and shotguns on their faces. The dice are colored green (brains are more likely), red (shotguns are more likely), and yellow (evenly split between brains and shotguns).

On their turn:

  1. The player randomly pulls dice from the cup until they have three in their hand, and rolls them. Players always roll exactly three dice.
  2. They set aside any brains (humans whose brains were eaten) and shotguns (humans who fought back).
  3. If the player has set aside three shotguns on this turn, their turn ends with zero points.
  4. The player keeps any dice that came up footsteps in their hand if they decide to roll again.
  5. Otherwise, the player can choose to end their turn, where they'll collect one point per brain and return all dice to the cup for the next player.
  6. Or the player can choose to continue their turn and roll again. Go back to step 1, noting any dice already in hand that came up footsteps will be reused.

Zombie Dice has a "push your luck" game mechanic: the more you re-roll the dice, the more brains you can get but the more likely you'll eventually accrue three shotguns and lose everything. Once a player reaches 13 points, the rest of the players get one more turn (to potentially catch up) and the game ends. The player with the most points wins.

More complete rules for Zombie Dice can be found here:

Bot Programming Challenge

To install the simulator with pip, run pip install zombiedice (on Windows) or pip3 install zombiedice (on macOS and Linux). To run a demo with some pre-made bots, run python -m zombiedice (on Windows) or python3 -m zombiedice (on macOS and Linux) to see an example of the simulator's web gui:

The simulator can quickly run hundreds of games, noting how many wins and ties each bot got.

You'll create bots by writing a class with a turn() method, which is called by the simulator when it's your bot's turn. Inside this turn() method, you'll call the zombiedice.roll() function as often as you want to roll the dice (though you should call it at least once on your turn, of course). Classes are beyond the scope of this article, but you can simply change the turn() code in myzombie.py:

import zombiedice

class MyZombie:
    def __init__(self, name):
        self.name = name

    def turn(self, gameState):
        # gameState is a dict with info about the current state of the game.
        # You can choose to ignore it in your code.

        diceRollResults = zombiedice.roll() # first roll
        # roll() returns a dictionary with keys 'brains', 'shotgun', and
        # 'footsteps' with how many rolls of each type there were.
        # The 'rolls' key is a list of (color, icon) tuples with the
        # exact roll result information.
        # Example of a roll() return value:
        # {'brains': 1, 'footsteps': 1, 'shotgun': 1,
        #  'rolls': [('yellow', 'brains'), ('red', 'footsteps'),
        #            ('green', 'shotgun')]}

        # The code logic for your zombie goes here:
        brains = 0
        while diceRollResults is not None:
            brains += diceRollResults['brains']

            if brains < 2:
                diceRollResults = zombiedice.roll() # roll again
            else:
                break

class AlwaysRollsTwicePerTurn:
    def __init__(self, name):
        self.name = name

    def turn(self, gameState):
        zombiedice.roll()
        zombiedice.roll()


zombies = (
    zombiedice.examples.RandomCoinFlipZombie(name='Random'),
    zombiedice.examples.RollsUntilInTheLeadZombie(name='Until Leading'),
    zombiedice.examples.MinNumShotgunsThenStopsZombie(name='Stop at 2 Shotguns', minShotguns=2),
    zombiedice.examples.MinNumShotgunsThenStopsZombie(name='Stop at 1 Shotgun', minShotguns=1),
    MyZombie(name='My Zombie Bot'),
    AlwaysRollsTwicePerTurn(name='Rolls Twice'),
    # Add any other zombie players here.
)

# Uncomment one of the following lines to run in CLI or Web GUI mode:
#zombiedice.runTournament(zombies=zombies, numGames=1000)
zombiedice.runWebGui(zombies=zombies, numGames=1000)

The code in the MyZombie class's turn() method keeps calling zombiedice.roll() until it has rolled at least two brains. A second bot is implemented in the AlwaysRollsTwicePerTurn class, which has a turn() method that simply rolls the dice twice on every turn. Running this program calls runWebGui() and brings up the web browser as shown in Figure 6-1, ready to run the simulation. You can examine the source code in the zombiedice package's examples.py file to see how the other bots work.

Try writing some of your own bots to play Zombie Dice, and see how they compare against the other bots. And if you find yourself playing this game in the real world, you'll have the benefit of thousands of simulated games telling you that one of the best strategies is to simply stop once you've rolled two shotguns. But you could always try pressing your luck...

Constructing MyZombie, Step by Step

I'll describe the though process of creating the MyZombie class in the previous section. Start with this basic template that imports the zombiedice module, creates a class with a turn() method that calls zombiedice.roll(), and then runs a tournament:

import zombiedice

class MyZombie:
    def __init__(self, name):
        self.name = name

    def turn(self, gameState):
        zombiedice.roll()

zombies = (
    zombiedice.examples.RandomCoinFlipZombie(name='Random'),
    MyZombie(name='My Zombie Bot'),
)

# Uncomment one of the following lines to run in CLI or Web GUI mode:
zombiedice.runWebGui(zombies=zombies, numGames=1000)

The MyZombie so far only rolls the dice one, which is a very conservative strategy that is unlikely to get many points. To decide if it should roll again, we'll need to examine the return value of zombiedice.roll(), which is a dictionary that shows the results of the roll of the three dice. It would look something like this:

{'brains': 1, 'footsteps': 1, 'shotgun': 1,
 'rolls': [('yellow', 'brains'), ('red', 'footsteps'),
           ('green', 'shotgun')]}

This dictionary has a 'brains', 'footsteps', and 'shotgun' key with values that are the number of that kind of roll. The exact rolls are in the 'rolls' key's value: a list of tuples. Each tuple represent a die, composed of a color and the icon that came up.

Let's say we want to program the strategy that our zombie will keep rolling until it has at least two brains rolled. We would need to keep track of how many brains have been rolled so far on this turn (in a brains variable), which starts at 0. If a future call to zombiedice.roll() ever returns None, then we know that we've reached or exceeded three shotguns on this turn and will get zero points. Update turn() so that it looks like this:

    def turn(self, gameState):
        diceRollResults = zombiedice.roll()
        brains = 0
        while diceRollResults is not None:
            diceRollResults = zombiedice.roll()

Now the code will keep rolling the dice until it returns None. But at that point, we've reached three shotguns and lost the turn. We need to add in logic so that at some point the method returns before zombiedice.roll() starts returning None. Let's examine diceRollResults to see how many brains were rolled:

    def turn(self, gameState):
        diceRollResults = zombiedice.roll()
        brains = 0
        while diceRollResults is not None:
            brains += diceRollResults['brains']

            if brains < 2:
                diceRollResults = zombiedice.roll()

That's better, now zombiedice.roll() is only called as long as we've yet to reach two brain rolls on our turn. However, there's no condition where this method changes. It'd just loop forever! Let's add a bit more code:

    def turn(self, gameState):
        diceRollResults = zombiedice.roll()
        brains = 0
        while diceRollResults is not None:
            brains += diceRollResults['brains']

            if brains < 2:
                diceRollResults = zombiedice.roll()
            else:
                break

This new code says that if we've rolled at least two brains, go ahead and break out of the loop and eventually return from the method, which tells the simulator that the bot is done with its turn.

The value passed for the gameState parameter is a dictionary with the following keys:

  • 'CURRENT_ZOMBIE' - string of the zombie whose turn it is currently
  • 'CURRENT_CUP' - list of dice strings (i.e. 'red', 'yellow', 'green') in the cup
  • 'CURRENT_HAND' - list of dice being rolled (should always be length of three)
  • 'SHOTGUNS_ROLLED' - number of shotguns rolled so far on this turn
  • 'BRAINS_ROLLED' - number of brains rolled so far on this turn
  • 'ROLLED_BRAINS_DETAILS' - list of dice strings for each brain rolled, used in the rare event we run out of brain dice
  • 'TURN_START_TIME' - time since unix epoch that the current turn began

Ideas for Other Bots

There's plenty of strategies you can try in Zombie Dice. Try writing bots that do the following:

  • A bot that has a 50% chance of choosing to re-roll. See how this bot compares against a bot with a 25% or 75% chance to re-roll.
  • A bot that stops re-rolling when it reaches two shotguns.
  • A bot that stops re-rolling when it reaches two shotguns, unless it isn't in the lead. (Check the gameState dictionary to find this information.)
  • A bot that rolls as long as there are green dice left in the cup. (Check the gameState dictionary to find this information.)

Be sure to check out the examples.py file in the module for the code to some bots that have already been made. Then load them up in the simulator and see how they perform!