Programming for Gamers: Choosing a random item
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.
- 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.
- re.match matches text patterns from the beginning of the text.
- The last character is a dollar sign, which requires that the pattern match the entire piece of text instead of just the beginning.
- 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.
- 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.
- 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.
- 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.
- 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.
-
if firstArgument.isdigit():
- 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-15 Orcs (d6) 16-25 Goblins (2d20) 26-00 Astertarte 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()
-
for counter in range(count):
- #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)
-
for counter in range(count):
- 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.
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.
↑This assumes you named the file “suns” when you saved it.
↑Computer languages almost always count starting from zero rather than starting from one.
↑
- Fraise: Jean-François Moy and Peter Borg
- Fraise is the successor to the great text editor Smultron. It’s an easy-to-use, powerful, free, text editor with tabs, split windows, syntax coloring, and more.
- Programming help: James Maliszewski at Grognardia
- “How much effort would it require to slap together a program or applet that does about 10 random rolls in succession, with a handful of modifiers, and spit out the results in a specific format?”
More Programming for Gamers
- Are my dice random?
- My d20 appears to have been rolling a lot of ones, a disaster if I were playing D&D but a boon for Gods & Monsters. Is my die really random, or is it skewed towards a particular result? Use the ‘R’ open source statistics tool to find out.
- Easier random tables
- Rather than having to type --table and --count, why not just type the table name and an optional count number?
- Programming a Roman thumb
- Before we move on to more complex stuff with the “random” script, how about something even simpler? Choose or die, Bezonian!
- Multiple tables on the same command
- The way the “random” script currently stands, it does one table at a time. Often, however, you have more than one table you know you’re going to need. Why not use one command to rule them all?
- How random is Python’s random?
- Can you trust a script’s random number generator to actually be random?
- 12 more pages with the topic Programming for Gamers, and other related pages
More Python
- Quick-and-dirty old-school island script
- Here’s a Python-based island generator using the tables from the Judges Guild Island Book 1.
- Astounding Scripts on Monterey
- Monterey removes Python 2, which means that you’ll need to replace it if you’re still using any Python 2 scripts; there’s also a minor change with Layer Windows and GraphicConverter.
- Goodreads: What books did I read last week and last month?
- I occasionally want to look in Goodreads for what I read last month or last week, and that currently means sorting by date read and counting down to the beginning and end of the period in question. This Python script will do that search on an exported Goodreads csv file.
- Test classes and objects in python
- One of the advantages of object-oriented programming is that objects can masquerade as each other.
- Timeout class with retry in Python
- In Paramiko’s ssh client, timeouts don’t seem to work; a signal can handle this—and then can also perform a retry.
- 30 more pages with the topic Python, and other related pages
More random tables
- Island Book 1 and old-school tables
- Judges Guild Island Book 1 is a fascinating playground on which to place a sea-going adventure or campaign. It’s also a great example of the usefulness and wildness of old-school encounter tables.
- Random table rolls
- As often as not, when you roll on a random table you are rolling a random number of times. Now that we have a dice library, we can turn the roll count into a die roll.
- 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. But AD&D uses percentage dice to weight toward some monsters and items more than others.
- Wandering monster chart assistant
- Use the encounter chart assistant to create wandering monster charts using percentages rather than ranges. Copy your tables into this tool to make sure they add up, adjust them, then copy them back out to your word processor. Never worry about missing or overlapping ranges again!
- Multiple tables on the same command
- The way the “random” script currently stands, it does one table at a time. Often, however, you have more than one table you know you’re going to need. Why not use one command to rule them all?
- Three more pages with the topic random tables, and other related pages
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.