Work the Shell

Simulating Dice Rolls with Zombie Dice

Dave Taylor

Issue #239, March 2014

Dave rolls the dice to see if he's getting brains for lunch or a gunshot as he tries to flee—well, at least within his shell script programming project, that is.

I've spent the past few articles describing how to work with ImageMagick, watermarking, resizing and adding 3-D frames to images from within shell scripts. Interesting stuff, but I feel the need to move back to some game programming, because it's more fun to write than something practical and actually useful. See? That's how this year's going to go in my column. I'm going to start out by enjoying programming rather than carrying it as a burden while we Accomplish Important Things. You with me? I thought you would be.

So in this article, I'm going to start writing a multi-dice-rolling game program based on the fun, lightweight game Zombie Dice. In the game, you're a zombie trying to accumulate brains without getting shot down by vengeful humans. The game comes with 13 dice: six green, four yellow and three red. Each die has three possible elements: brains (good), gunshots (bad) and feet, which denote the person represented by that particular die has “done a runner” and you'll need to roll that particular die again next turn.

Out of the 13 dice, you randomly pick three each time, then roll them, accumulating both brains and gunshots. If you get to three gunshots, you're dead. If you get to 13 brains, you win. That's about it. In the real game, you take turns with someone else, but I'll get into the nuances a bit further in the program. For now, let's start by looking at how to simulate each individual die. Remember, there are three different colors, the green being the “easiest” humans to defeat, and the red being the “hardest”.

In programmatic terms, and using some additional variables to make the notation easier to understand, here's how I am going to simulate the different sides of these dice:

BRAIN=1; SHOT=2; RUNNER=3
gdie[1]=$BRAIN; gdie[2]=$BRAIN; gdie[3]=$BRAIN; 
gdie[4]=$SHOT; gdie[5]=$RUNNER; gdie[6]=$RUNNER;
ydie[1]=$BRAIN; ydie[2]=$BRAIN; ydie[3]=$SHOT;  
ydie[4]=$SHOT; ydie[5]=$RUNNER; ydie[6]=$RUNNER;
rdie[1]=$BRAIN; rdie[2]=$SHOT;  rdie[3]=$SHOT;  
rdie[4]=$SHOT; rdie[5]=$RUNNER; rdie[6]=$RUNNER;

As you can see, each color die has six options—since they're all six-sided dice—and the differences in color are reflected in the number of brains, shots and runners each has specified.

Using all-caps variables as placeholders for numeric values is a bit of an old-school programming technique, but the script will run 99% as fast, and the value of the mnemonics makes writing a lot of the later code far easier and helps avoid errors.

I also start my indexing at 1, not zero. Why, when Bash and other shells use zero-index arrays? Um, I don't know, but likely it's just because it makes more sense to me to refer to “faces one through six” on a die than “zero through five”. Since I'm using the mnemonic names rather than just numeric values, however, I ostensibly could change some of this as desired if it really bugs you. The first actual script segment requires more mnemonic variables so I can stick with the desire to write a very clear script:

GREEN=1; YELLOW=2; RED=3
function pick_color
{
  # returns 0 for green, 1 for yellow and 2 for red
  case $( expr $RANDOM % 13 + 1 ) in
    1|2|3|4|5|6  ) color=$GREEN  ;;
    7|8|9|10     ) color=$YELLOW ;;
    11|12|13     ) color=$RED    ;;
  esac
}

As with just about every game, randomness is important. That's why so many games have decks to shuffle, dice to roll and so on. Totally deterministic games are generally a lot less fun (although not always—neither Chess nor Go have any randomness to them).

Bash makes working with random numbers super easy. The special variable $RANDOM is assigned a random integer value between 1-MAXINT every time you reference it—no need to “seed” it.

Here's an easy and interesting little test:

$ while [ $i -lt 10000 ] ; do 
  echo $RANDOM ; i=$(( $i + 1 )); 
done | sort | uniq -c | sort -rn | head -5
   4 7281
   4 6947
   4 6309
   4 31328
   4 28955

This spits out 10,000 random numbers, then analyzes how frequently each occurs, displaying the five most common. In this case, you can see that the most commonly occurring number appears only four times out of 10,000 numbers. Try this on your system: if you see a number appearing significantly more, you might have a less random number system than you want.

Now where was I? Oh yeah, rolling dice. Since I'm working in a shell script, the idea of a function “returning” a value is pretty non-existent, so instead I'll be setting global variables within local function scope. It's sloppy programming, but a constraint of shell scripting, so bear with it and know that if you want to write this in Ruby, Perl or similar, you'll be able to have a more rigorous policy with your own variables.

Six out of 13 dice are green, four are yellow, and three are red. You can see how that's easily implemented with the case statement.

In a very similar spirit, rolling an individual die is easily accomplished, particularly because of the array of dice sides by color set up earlier in the script. Here's all that's needed:

function roll_die
{
  # returns 1 for a brain, 2 for gunshot, 3 for a runner

  dieroll=$( expr $RANDOM % 6 + 1 )

  case $1 in
    $GREEN  ) roll=${gdie[$dieroll]} ;;
    $YELLOW ) roll=${ydie[$dieroll]} ;;
    $RED    ) roll=${rdie[$dieroll]} ;;
  esac
}

A two-dimensional array would make this even easier, with a single line:

  
roll=${die[$COLOR][$dieroll]}

But in the interest of legibility, I've slightly complicated things with the three different arrays. Still, the function's delightfully small and easily understood.

Armed with both of these, it's easy to pick a color randomly and then roll that die:

pick_color
roll_die $color

Notice that the function roll_die is written to expect the die color to be specified as its parameter. Since we are using global variables, I suppose I should just refer to $color in the function, but that makes me cringe just a little bit. Know what I mean?

Let's add some code to make it easy to display what I've “rolled” too. This is easily done with arrays again:

colorname[$GREEN]="green"; colorname[$YELLOW]="yellow";
 ↪colorname[$RED]="red";
nameof[$BRAIN]="brain"; nameof[$SHOT]="gunshot";
 ↪nameof[$RUNNER]="runner";

Now it's this easy to show what's been rolled:

echo "Rolled ${colorname[$color]} die: ${nameof[$roll]}"

So let's roll a few dice:

$ ./zdice.sh
    rolled green die: runner
    rolled green die: brain
    rolled red die: runner
$ ./zdice.sh
    rolled green die: brain
    rolled red die: runner
    rolled red die: brain

That's a good start. Next month, I'll look at how to accumulate brains and track those dangerous gunshots. Meanwhile, why not pick up the actual game, for sale at Target and a zillion other places. It's from Steve Jackson Games: www.sjgames.com.

Dave Taylor has been hacking shell scripts for more than 30 years. Really. He's the author of the popular Wicked Cool Shell Scripts and can be found on Twitter as @DaveTaylor and more generally at www.DaveTaylorOnline.com.