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:
[toggle code]
- #choose a random item from the table
-
def choice(self):
- #generate a number from 1 to 100
- d100 = random.randrange(100)+1
- #find the corresponding line
- line = bisect.bisect_right(self.items, (d100,))
- (high, item, appearing) = self.items[line]
- numberAppearing = ''
-
if appearing == '':
- #is this a reference to another table?
- subtable = tableFactory(item)
-
if subtable:
- return subtable.choice()
-
else:
- numberAppearing = dice.roll(appearing)
- numberAppearing = ' (' + unicode(numberAppearing) + ')'
- item = item + numberAppearing
- return item
Now, when an entry is rolled that does not contain a die roll in parentheses, the script will look to see if that entry is itself a table file. If it is, it returns the choice from that table rather than from the current table. Go back to the Deep Forest table from Percentage-based random tables, and you’ll see it has an item called “Mist encounter”.
Add a “Mist encounter.txt” file to the same folder where “Deep Forest.txt” is stored:
- 01-15 Fire Spider
- 16-30 Beaked Sweeper
- 31-45 Crazy Crabs (d2)
- 46-60 Pink Horrors (d6)
- 61-75 Toves (d3)
- 76-84 Giant Venus Flytraps (d4)
- 85-90 Mushroom Walker
- 91-96 Giant Leeches (d8)
- 97-00 Mist Wraith
Now, you will no longer see “Mist encounter (1)” as an option. Instead, you’ll see a random entry from the Mist Encounter table:
./random 3 Deep\ Forest 1. Petraiad (1) 2. Pink Horrors (3) 3. Pixies (17)
The Pink Horrors come from Mist encounters.txt.
Note that nothing in here tells it to stop if, on rolling on the new table, yet another new table is found. Thus, you might (as in my Gods & Monsters adventures) have a basic table of encounter types, breaking out into more specific tables, which occasionally break out into subtables for dragons or the Chaotic Mist.
There’s another change we need to make: in the past, if we asked, on the command line, for a table that didn’t exist, we received a Python error for that table. Now, our error message is a more cryptic “'NoneType' object has no attribute 'choose'”. That’s because, in our initial use of the table, we are calling the choose method regardless of whether tableFactory gave us a table back. We need to check for None there as well:
[toggle code]
- #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 re.match(r'[0-9]*d?[1-9][0-9]*$', firstArgument):
- count = firstArgument
- tableName = args.pop(0)
-
else:
- tableName = firstArgument
- table = tableFactory(tableName)
-
if table:
- table.choose(count)
-
else:
- print 'Cannot find table', tableName
In order to report what the problem is, we need to keep the table’s name; so I renamed the variable for table to tableName so that it doesn’t disappear when we create the table object from tableFactory.
Beyond that, it just does “table.choose(count)” if there is a table, and prints that it cannot find the table if there is no table.
Here is the full script.
[toggle code]
- #!/usr/bin/python
- import random
- import optparse
- import os
- import bisect
- import dice
- import re
- 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
-
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
- #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):
-
if type(count) is not int:
-
if count.isdigit():
- count = int(count)
-
else:
- count = dice.roll(count + 't')
-
if count == 1:
- time = 'time:'
-
else:
- time = 'times:'
- print 'Rolling', count, time
-
if count.isdigit():
- maximumLength = len(unicode(count))
- formatString = '%' + unicode(maximumLength) + 'i. %s'
-
for counter in range(count):
-
if count > 1:
- print formatString % (counter+1, self.choice())
-
else:
- print self.choice()
-
if count > 1:
-
if type(count) is not int:
- #a simple table is a list of items with equal probability
-
class SimpleTable(Table):
- #load the list of items and save it
-
def __init__(self, items):
- self.items = items.splitlines()
- #choose a random item from the table
-
def choice(self):
- return random.choice(self.items)
-
class PercentTable(Table):
- #load the list of items and save it
-
def __init__(self, items):
- self.items = []
-
for line in items.splitlines():
- #generate the range, all we need is the high number
- (dieRange, item) = line.split("\t")
-
if '-' in dieRange:
- (lowDie, highDie) = dieRange.split('-')
-
else:
- highDie = dieRange
- highDie = int(highDie)
- #00 is often used in place of 100 in wandering monster tables
-
if highDie == 0:
- highDie = 100
- #generate the number appearing
- appearing = ''
-
if item.endswith(')'):
- dieStart = item.rindex('(')
- appearing = item[dieStart:]
- appearing = appearing.strip('()')
-
if 'd' in appearing:
- appearing = appearing + 't'
- item = item[:dieStart]
- item = item.strip()
- self.items.append((highDie, item, appearing))
- #choose a random item from the table
-
def choice(self):
- #generate a number from 1 to 100
- d100 = random.randrange(100)+1
- #find the corresponding line
- line = bisect.bisect_right(self.items, (d100,))
- (high, item, appearing) = self.items[line]
- numberAppearing = ''
-
if appearing == '':
- #is this a reference to another table?
- subtable = tableFactory(item)
-
if subtable:
- return subtable.choice()
-
else:
- numberAppearing = dice.roll(appearing)
- numberAppearing = ' (' + unicode(numberAppearing) + ')'
- item = item + numberAppearing
- return item
- count = 1
-
while args:
- 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 re.match(r'[0-9]*d?[1-9][0-9]*$', firstArgument):
- count = firstArgument
- tableName = args.pop(0)
-
else:
- tableName = firstArgument
- table = tableFactory(tableName)
-
if table:
- table.choose(count)
-
else:
- print 'Cannot find table', tableName
I’ve added the files necessary for the Highland Guidebook encounter charts to the Guidebook’s resources archive. I’ve also made some updates to the script to handle ndx+c and ndx-1, as well as following tables with explanatory text (for example, animals might be “angry or hungry”). It also now successfully handles entries with non-ASCII text, such as diacriticals. I’ll keep the version in the Guidebook archive updated with any future changes as well.
In response to Programming for Gamers: Choosing a random item: If you can understand a roleplaying game’s rules, you can understand programming. Programming is a lot easier.
- The World of Highland Guidebook
- Highland provides a context for Gods & Monsters adventures. Highland is designed for the rural adventurer, where characters begin in small villages or remote areas and move in towards civilization as they learn more and more about their world’s past. It was designed as a version of the standard fantasy world imprinted on the American old west.
- ZIP Resources for The World of Highland Guidebook
- Resources for The World of Highland Guidebook, including samples and document graphics.
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.
- Programming for Gamers: Choosing a random item
- If you can understand a roleplaying game’s rules, you can understand programming. Programming is a lot easier.
- 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?
- 12 more pages with the topic Programming for Gamers, and other related pages