Biblyon the Great

This zine is dedicated to articles about the fantasy role-playing game Gods & Monsters, and other random musings.

Gods & Monsters Fantasy Role-Playing

Beyond here lie dragons

Programming for Gamers: Choosing a random item

Jerry Stratton, February 26, 2011

Recently, on Grognardia, James Maliszewski asked how hard it would be to write a program to pull a random item from a table. The answer is, not hard at all.

I use Mac OS X, so my solutions will be ones that work on Mac OS X and Linux. I tend to prefer Python, so that’s what I’m going to use. But for the purpose of pulling from random tables, Perl and PHP both can do the same thing.

This was the example I gave to James:

[toggle code]

  • #!/usr/bin/python
  • import random
  • table = [
    • 'Yellow Sun',
    • 'Red Sun',
    • 'Blue Sun',
    • 'Paisley Sun',
  • ]
  • print random.choice(table)

You need to copy this and put it into a text editor, such as Fraise. Save it, giving it a filename like “suns”, and then run it from the command line. In Mac OS X, you can find the command line in “Terminal” in the Utilities folder in your Applications folder.

You need to be in the same directory as the file you just saved1, and then type “python suns” to get a random sun.2

  • $ python suns
  • Red Sun
  • $ python suns
  • Blue Sun
  • $ python suns
  • Paisley Sun

If you want more suns in the list, add new ones just like the old ones, one per line, surrounded by straight apostrophes, and ending in a comma. Go ahead and add a White Sun and an Orange Sun to the list.

[toggle code]

  • table = [
    • 'Yellow Sun',
    • 'Red Sun',
    • 'Blue Sun',
    • 'Paisley Sun',
    • 'White Sun',
    • 'Orange Sun',
  • ]

Run the program again a couple of times, and you should see your new sun types show up.

Do it more than once

Often when you’re asking for random stuff, you need more than one random item. You might, for example, want to get two suns, or three suns.

There are two things your script needs for this: it needs to be able to know how many suns you want; and it needs to be able to repeat itself that many times.

The first step is to give your script the ability to know what you want. You do that with “parameter” or “command-line arguments”. In python, this is handled with an OptionParser.

In python, you grab extra functionality by importing it. So we need to import the “option parser” into our script, just like we imported “random” functionality. Add the emphasized new lines to the “suns” script:

[toggle code]

  • #!/usr/bin/python
  • import random
  • import optparse
  • table = [
    • 'Yellow Sun',
    • 'Red Sun',
    • 'Blue Sun',
    • 'Paisley Sun',
    • 'White Sun',
    • 'Orange Sun',
  • ]
  • parser = optparse.OptionParser()
  • parser.add_option('-c', '--count', type='int', default=1)
  • (options, args) = parser.parse_args()
  • for counter in range(options.count):
    • print counter+1, random.choice(table)

Make sure that you also indent the “print” line using one tab. Python knows what part of the script gets repeated based on which lines directly below the repeat line (“for counter in range”) are indented. Until you’re comfortable typing it yourself, you can hit “toggle code” above any of these examples to get something you can copy directly into your text editor.

Now, you can run the command with the --count option:

  • $ python suns --count 3
  • 1 White Sun
  • 2 Blue Sun
  • 3 Orange Sun

The line “for counter in range(options.count)” repeats the subsequent indented lines options.count times. The “range(options.count)” is giving you every number from 0 to one less than options.count3. If options.count is 3, then range(options.count) will be 0, 1, and 2. The “for” line repeats itself once for 0, once for 1, and once for 2.

Each time it repeats it puts the current range number in “counter”. So that when we “print counter+1”, it prints out whatever number we’re currently at, with one added to it (because computers count from zero and humans count from 1).

Simplify the suns

The list of suns is currently actual python code. When it comes to the things you’re making lists of you’ll probably be changing those lists a lot. Having to edit code whenever you add a new type of sun means that you have to test your code whenever you add a new type of sun—because you’re changing the actual code of your script.

It would be a lot easier to make a text file of suns, and have the script look into that file to create its table. Make a file, call it “suns.txt”, and put these lines in it:

  • Yellow Sun
  • Red Sun
  • Blue Sun
  • Paisley Sun
  • White Sun
  • Orange Sun
  • White Dwarf
  • Black Hole

Save that file in the same directory you’re running the script from.

Remove the “table” lines from the script, and replace it with the emphasized lines:

[toggle code]

  • #!/usr/bin/python
  • import random
  • import optparse
  • parser = optparse.OptionParser()
  • parser.add_option('-c', '--count', type='int', default=1)
  • (options, args) = parser.parse_args()
  • table = open('suns.txt').read()
  • table = table.splitlines()
  • for counter in range(options.count):
    • print counter+1, random.choice(table)

Now, when you run the script, it should do exactly what it did before. But when you change a line in “suns.txt”, the script will use those changes, without your having to worry about the syntax of the list—beyond “one line is one sun”.

Why does it have to be suns?

“But wait!” you say. “The script doesn’t have anything to do with suns now. It’s reading a file I created. Can I create other files?”

Why yes. Yes you can. Rename the script from “suns” to “random”.

Create a new file, call it “snakes.txt”:

  • asp
  • boa
  • coral
  • python
  • viper
  • cobra
  • rattlesnake
  • constrictor
  • huge snake
  • giant snake

Add a new option to the list of options in the script, and change the “open” line to open based on that option:

[toggle code]

  • #!/usr/bin/python
  • import random
  • import optparse
  • parser = optparse.OptionParser()
  • parser.add_option('-c', '--count', type='int', default=1)
  • parser.add_option('-t', '--table', default='suns.txt')
  • (options, args) = parser.parse_args()
  • table = open(options.table).read()
  • table = table.splitlines()
  • for counter in range(options.count):
    • print counter+1, random.choice(table)

Okay? Now, that option defaults to random suns, but if you type “--table snakes.txt” it should give you a random snake instead.

  • $ python random
  • 1 Yellow Sun
  • $ python random --table snakes.txt
  • 1 rattlesnake
  • $ python random --table suns.txt --count 3
  • 1 Red Sun
  • 2 Orange Sun
  • 3 Red Sun
  • $ python random --table snakes.txt --count 3
  • 1 viper
  • 2 rattlesnake
  • 3 boa

There you go: any file you can create, you can pull random items from it on the command line. Play around with it. Make more tables; if you can think of other options to add to the script, see if you can add them yourself.

Future installments in this series might be a random die roller, or pulling random items from more complex tables, such as AD&D’s d100 wandering monster charts.

April 5, 2014: Automatically roll subtables

Now that the “random” script handles percentage tables for wandering encounters, it’s very close to being able to handle the hierarchical encounter charts I use in Gods & Monsters. All that remains is for it to detect that an entry on the table is itself another table.

In order to do this, we need to be able to detect whether an entry matches a table file. We already have a function, tableFactory, that returns a table based on the filename; currently, however, it fails if the filename does not exist as a file. What we can do is make it return “no file” if the file doesn’t exist. In Python terms, we want it to return None instead of returning a Table object.

Before the “open” line in “def tableFactory(name):”, check to see that the filepath exists:

[toggle code]

  • #load the table into the appropriate class
  • def tableFactory(name):
    • filename = name + '.txt'
    • filepath = filename
    • if options.locale:
      • localepath = os.path.join(options.locale, filename)
      • if os.path.exists(localepath):
        • filepath = localepath
    • if not os.path.exists(filepath):
      • return None
    • items = open(filepath).read()
    • if "\t" in items:
      • table = PercentTable(items)
    • else:
      • table = SimpleTable(items)
    • return table

If the path represented by the filepath variable does not exist, the function returns None. This means we can use it for checking to see if a table file exists. Currently, if there is no “number appearing”, the system assumes “1”. Let’s change it to assume nothing. Change “appearing = '1'” to:

  • #generate the number appearing
  • appearing = ''

Now, we can detect this on making our random choice:

March 29, 2014: Random table rolls

Our random table script is doing a lot for us now. We can roll any arbitrary number of times on any text file. But often, we aren’t rolling a known number of times: our number of rolls on the random table is itself random. So it would be nice to, in addition to ./random 3 gems we could also do ./random d4 gems.

Now that we have the dice library installed, we can do this. The main issue is that the script currently knows whether to roll more than once by looking to see if the first item on the command line is all digits:

[toggle code]

  • if firstArgument.isdigit():
    • count = int(firstArgument)

If we are going to accept die rolls, then we need to accept another character as well as digits: the letter ‘d’. There does, however, have to be at least one number. Basically, we need our script to recognize a die roll.

When we need to recognize text patterns, the most common tool is a regular expression. In Python, we use regular expressions by importing the re library. Add this to the top of the script with the rest of the imports:

  • import re

Replace the if/count lines above with:

[toggle code]

  • if re.match(r'[0-9]*d?[1-9][0-9]*$', firstArgument):
    • count = firstArgument

That looks complicated—and regular expressions can certainly become complicated—but in this case it isn’t.

  1. re.match matches text patterns from the beginning of the text.
  2. The last character is a dollar sign, which requires that the pattern match the entire piece of text instead of just the beginning.
  3. The first set of characters are in square brackets: [0-9]. Items in square brackets mean, match any character in that range. For example, [a-m] would mean any character from lower-case ‘a’ through lower-case ‘m’. In this case, it’s any number.
  4. The next character immediately following the first set is an asterisk. The asterisk means any number of the preceding character. The preceding character is any digit. So, “[0-9]*” means any number of digits. Any number includes zero, so this means zero or more digits at the start of the text.
  5. The next portion is the letter ‘d’ followed by a question mark. The letter ‘d’ is needed to show that this is a die roll, and the question mark means that we need zero or one of the previous characters. If this is not a die roll, there will be no ‘d’. If this is a die roll, there will be only one ‘d’. The question mark handles this for us.
  6. The next portion is “[1-9]”. There must be at least one digit from 1 to 9 in either a die roll or an arbitrary number.
  7. And the final portion is “[0-9]*”. We have already seen this; it means from zero to any amount of digits.

You can construct most of your regular expression search patterns using those rules.

March 19, 2014: Percentage-based random tables

Our current random item generator assumes that each item shows up as often as any other item. That’s very OD&D-ish; as often as not, there will be six items per table. But AD&D game masters often use percentage dice to generate some monsters and items more than others. Their tables look like:

01-15Orcs (d6)
16-25Goblins (2d20)
26-00Astertarte the Red Dragon

In this table, most likely for the red dragon’s lair, the red dragon shows up far more often than orcs and goblins. You can see more about this kind of table using the Wandering monster chart assistant.

We can make our random item generator recognize these tables and choose appropriately from them. The main difference is that, instead of creating a list of items and choosing from the list, we will need to generate a number from 1 to 100, and determine where that number falls in the list.

For test purposes, I’m going to use a modification of the default table from the wandering monster assistant:

  • 01-30 Orcs (d12)
  • 31-50 Mushroom People (2d20)
  • 51-70 Pixies (d100)
  • 71-83 Goblins (d20)
  • 84-95 Giant cucumbers (d4)
  • 96-00 Astertarte the Red Dragon

And I’m going to modify the “random” script to separate the parts where the script checks for the file and the part that loads the items in the file:

[toggle code]

  • #!/usr/bin/python
  • import random
  • import optparse
  • import os
  • parser = optparse.OptionParser()
  • parser.add_option('-l', '--locale')
  • (options, args) = parser.parse_args()
  • #load the table into the appropriate class
  • def tableFactory(name):
    • filename = name + '.txt'
    • filepath = filename
    • if options.locale:
      • localepath = os.path.join(options.locale, filename)
      • if os.path.exists(localepath):
        • filepath = localepath
    • items = open(filepath).read()
    • table = SimpleTable(items)
    • return table
  • #a table is a list of items that we choose randomly from
  • class Table(object):
    • #print some random items from the table
    • def choose(self, count):
      • for counter in range(count):
        • print counter+1, self.choice()
  • #a simple table is a list of items with equal probability

The part that looks for the appropriate file based on the given file name (“monsters” or “dragons” or “gems”) is now in the function called “tableFactory”. This function generates a table for us depending on the contents of the file—although at this point it only knows about one type of file. At the location where it says “table = SimpleTable(items)” we will eventually have it look in the file and create either a SimpleTable or a PercentTable object, depending on the contents.

The part that shows the random item has been broken from one class into two classes. The basic class is “Table”. All it knows to do is print out however many choices that we ask for. It doesn’t even know how to make the choice: it calls a method called “choice” that needs to be on the subclass.

SimpleTable is the subclass: because it has “Table” in parentheses, it inherits everything the Table class knows and also adds its own. SimpleTable has two methods: one to load the list of items into an array, and one to make a choice from that array.

Make these changes and run the script, and it should work exactly as it did before.

So, now that we can have a class for each type of table, let’s make one for percent tables:

March 14, 2011: Multiple tables on the same command

So now, what about doing more than one table at a time? Getting three dragons and one snake from the same command line? As soon as you start thinking about using a type of “thing” more than once with different properties, you’re talking about a class of item. Classes are used to take some pieces of code and variables, and put them all together to define that kind of thing. Once we do that, we can use them a lot like we have been using lists and randoms already.

So the first step is to separate the table code into a class:

[toggle code]

  • #!/usr/bin/python
  • import random
  • import optparse
  • parser = optparse.OptionParser()
  • (options, args) = parser.parse_args()
  • #a table is a file of items that we choose randomly from
  • class Table(object):
    • #load the table and save it
    • def __init__(self, name):
      • filename = name + '.txt'
      • items = open(filename).read()
      • self.items = items.splitlines()
    • #print some random items from the table
    • def choose(self, count):
      • for counter in range(count):
        • print counter+1, random.choice(self.items)
  • firstArgument = args.pop(0)
  • #if the first argument is a number, it's the number of random items we want
  • #otherwise, it is the table and we want one item from it
  • if firstArgument.isdigit():
    • count = int(firstArgument)
    • table = args.pop(0)
  • else:
    • table = firstArgument
    • count = 1
  • table = Table(table)
  • table.choose(count)

If you run this from the command line now, it will do exactly what it did before. You should test it to make sure.

What’s new? The word “def” stands for “define function”. We’ve created a class and defined some functionality on it. The “__init__()” function is a special function that runs every time you create a new object from a class of something; in this case, every time we create a new table it will run—initialize itself using—that code. So we use that to load the file as a list of items and save it on the object (“self”) for later use.

Now that we’ve separated the table code into a class, however, we can make a more readable multiple-table command line.

March 3, 2011: Easier random tables

As it currently stands, our script lets us easily get a random item from a “table” of items. But not as easily as it could let us do it.

  • python random --table suns.txt --count 3

The script looks for a “table” option and a “count” option, both of which are optional; leave the table out and it defaults to “suns.txt”. Options are very useful, but they’re probably not appropriate in this case. Once we have a hundred or so tables set up, how often, really, are we going to not have to specify a table? Why require typing --table and --count every time?

It would be a lot easier to be able to type:

  • python random 3 snakes

Or even:

  • ./random 2 dragons

Let’s take it a step at a time.

Arguments, not options

If you look at the current version of the script, we have:

  • (options, args) = parser.parse_args()

We’ve used the options—we used options.table and options.count—but we haven’t used args. Options are very precise. We say “--table” and specify a filename, “--count” and specify a number. Arguments are more freeform. Let’s switch this around to use arguments instead of options.

[toggle code]

  • #!/usr/bin/python
  • import random
  • import optparse
  • parser = optparse.OptionParser()
  • (options, args) = parser.parse_args()
  • count = args.pop(0)
  • count = int(count)
  • table = args.pop(0)
  • table = open(table).read()
  • table = table.splitlines()
  • for counter in range(count):
    • print counter+1, random.choice(table)

Now we don’t have any options (don’t worry, we’ll have one by the end of this series). The new syntax of the command is “python random number table”:

  • $ python random 2 snakes.txt
  • 1 coral
  • 2 constrictor
  • $ python random 3 suns.txt
  • 1 Red Sun
  • 2 Red Sun
  • 3 Blue Sun
  • $ python random 1 dragons.txt
  • 1 firestorm dragon

Dragons? Create a dragons.txt file with these lines:

  • fire dragon
  • water dragon
  • storm dragon
  • forest dragon
  • mud dragon
  • rotting dragon
  • albino dragon
  • laughing dragon
  • mist dragon
  • firestorm dragon
  • salt dragon
  • amethyst dragon
  • Sun Dragon
  • Night Dragon
  • Cloud Dragon
  • Rainbow Dragon

Because dragons are cool. But now, back to the code.

  1. In Mac OS X, type the letters “cd”, a space, and then drag the folder into the terminal window; then press return to “run” the change directory command.

  2. This assumes you named the file “suns” when you saved it.

  3. Computer languages almost always count starting from zero rather than starting from one.

  1. <- The tablets of Enki
  2. Roman thumb ->