Quick-and-dirty old-school island script
As I wrote in the first installment, this island generator (Zip file, 6.9 KB) is far from an example of well-written, organized code. It’s as messy as the old-school tables from Island Book 1 it encapsulates. The data is not separate from the code, and the code is filled with special cases.
If this had been a serious effort, I would have flowcharted the tables, organized them into kind of table, and probably made a series of table classes and subclasses for the different kinds of information in the tables—whether they call for die rolls to determine number, or whether the call for rolling on a further table, for example.
For something like this, that’s a recipe for never getting the script done. So I went along table by table and created code for what each table needed. If it was similar to the code that a previous table had needed, I modified that code, perhaps adding a new, optional, function parameter.
I could almost certainly have used the file-based table script I wrote about in my Programming for Gamers series. It would have been cleaner and smoother. But part of what drew me to these three pages of tables were all of the rough edges. I had to sand some of them off just because it’s a computer program—decisions have to be made—but I wanted to keep as many of the dangerous bits as I could.
Most of the tables are simply a list of 20 items, to be generated using a d20. Those are simple. The list of items is a Python list, and item = random.choice(list)
pulls an item randomly from the list.
Some of the 20-item lists also have options within the list. For those, I made the simple 20-item list a slightly less simple 20-item list of lists. The top table, the list of twenty types of islands, includes, in some entries, a range of numbers and/or a list of island features types.
[toggle code]
-
types = [
- ['barren rocks'],
- ['basalt cay', 'c'],
- ['sparse key', 'c', 't'],
- ['sparse ait', 'c', 'r', 't', 'p'],
- ['sparse isle', 2, 'h', 'c', 'r', 'p'],
- …
- ['monstrous island', 2, 6, 'v', 'h', 'p', 'c', 't'],
- …
- ]
The first item, barren rocks, has no options; so it’s a list of one item. The second item, basalt cay, has one option: there might be a creature on the island. Sparse keys might have a creature or a trap. Sparse isles, on the other hand, might have one or two features, hills, creatures, mineable resources, and/or provisions.
Monstrous islands have two to twelve, which I interpreted as 2d6, volcanoes, hills, provisions, creatures, and/or traps. And, as I later learned, the number rolled is also relevant, so that barren rocks are island size 1, and monstrous islands are island size 10.
I used a Python dict to correspond the feature abbreviations to the function that generates that particular feature.
[toggle code]
-
featureKeys = {
- 'c': generateCreature,
- 'f': generateFeature,
- 'h': 'is hilly',
- 'm': 'is mountainous',
- 'p': generateProvision,
- 'r': 'contains a mineable resource',
- 's': 'contains a stream',
- 't': generateTrap,
- 'v': generateVolcano,
- }
-
def randomIsland():
- index = random.choice(range(len(types)))
- islandInfo = types[index]
- index += 1
- island = islandInfo.pop(0)
- return index, island, islandInfo
- typeNumber, islandType, features = randomIsland()
To generate a random island type, then, the randomIsland
function gets the index number first, then the line from the table using that index number; it adds 1 to the index number because Python list indices are zero-based, but d20 rolls are 1-based. Thus, the indices into the Python list go from 0 to 19, but the d20 rolls should go from 1 to 20.
The island type is always the first item in the list, so that gets popped out. The rest of the list is left for later, to interpret as needed.
The size of the island is simple enough. It’s the index number times d20, in hundreds.
The very next table used generates the island’s elevation. This is provided as a d100 table in the Island Book, so I already need a new function. Percentage tables are very different from d20 tables. Instead of a one-to-one correspondence between the roll and the table item, each table line is a range of results. So instead of making the table generator a class and subclass it for d20 and then d100 rolls, it was easier to make an entirely new function.
- islandElevationString, islandElevation = die100(islandElevations, needNumber=True)
The die100
function takes a list of tuples; each tuple has four items in itL the cutoff percentile for that item, the low and high ranges for that item, and the measurement for that item.
[toggle code]
-
islandElevations = [
- (5, 0, -500, 'foot'),
- (40, 1, 500, 'foot'),
- (60, 501, 1000, 'foot'),
- (70, 1001, 2000, 'foot'),
- (80, 2001, 5000, 'foot'),
- (90, 5001, 10000, 'foot'),
- (99, 10001, 20000, 'foot'),
- (100, 'over 20,000 feet'),
- ]
Underwater islands happen five percent of the time, for d100 rolls ranging from 01 through 05. Underwater islands can go down to 500 feet below sea level. The plurality of islands (25%, or 06 through 40) range from 1 to 500 feet above sea level, and so on.
Later on, whether the island is above sea level or below it will be important, so the script needs to remember that elevation number.
Most of the d100 tables in this book only require the text result, that is, “655 feet” or “130 days”, which means that the next several lines of island description can be created directly:
- sentence('The island is', numberedThing(islandSize, 'foot'), 'wide')
- sentence('It has a general elevation of', islandElevationString)
- sentence('It receives', die100(islandPrecipitation), 'of precipitation per year')
- sentence('It has a growing season of', die100(islandGrowingSeason))
- sentence('Temperatures on the island range from', die100(islandTemperatures))
- sentence('It is recognizable by a unique', random.choice(islandLandmarks))
- sentence('The weather is currently', random.choice(islandWeathers))
Some of those descriptors use d100 tables, some use d20 tables, but none of the results need to be remembered by the script, so they are rolled and printed immediately to the terminal.
Several of the shore outcomes involve finding something. If that something isn’t there, the shore party finds provisions, unless they’re also unavailable. Rather than reproduce that text in each entry, I asterisked them, and append the text in a special case after choosing the random shore outcome.
[toggle code]
- shore = random.choice(shoreOutcomes)
-
if shore.endswith('*'):
- shore = shore[:-1]
-
if shore == 'find provisions':
- shore += ', unless unavailable'
-
else:
- shore += '; if none on island, find provisions, unless also not available'
-
elif shore == 'a passing ship':
- shore += ' (' + random.choice(passingShips) + ')'
- sentence('Outcome of shore party:', shore)
There’s also a special case for passing ships: if that’s the shore outcome, the script rolls for a passing ship.
The coastal encounter code is more complicated. The information about the coastal encounter contains either one item (the name of the encounter), two items (the encounter name and the die to be rolled for how many), three items (the encounter name, the number of dice, and the die to be rolled), or four items (all of that plus an add, that is, 1d8+4 for sea lions). I could have made every item be four items, but that increases the chance of having typos that don’t show up when running the code.
For the island features, the script first determines the die roll for the number of features. This is either the number of possible features or the die roll provided in the d20 table entry.
[toggle code]
-
if features and type(features[0]) == int:
- dieSize = features.pop(0)
-
if features and type(features[0]) == int:
- dieCount = dieSize
- dieSize = features.pop(0)
-
else:
- dieCount = 1
-
else:
- dieSize = len(features) or 1
- dieCount = 1
The script then generates the number of features from that die roll and loops until that many features have been generated to make a list of what kind of features are present.
Most of the entries have a range of numbers listed that I am assuming is the result of a die roll. I’m also assuming that the die roll determines the total number of features. Arguably, it could also have been meant to determine the number of each individual feature.
Similarly, if there is no die roll specified, I assume that the die roll should generate a number of from one to the number of possible features. That is, sparse ait has no die roll listed but has four possible features.1 I assume a die roll of d4. I could easily be convinced that there should be only one item total, or that there should be one of each of those four items.
[toggle code]
- featureList = {}
- featureCount = rollDie(dieCount, dieSize)
-
while features and featureCount > 0:
- newFeature = random.choice(features)
-
if newFeature not in featureList:
- featureList[newFeature] = 0
- featureList[newFeature] += 1
- featureCount -= 1
Finally, the script loops through each kind of feature and generates that feature.
[toggle code]
-
for feature in featureList:
- generator = featureKeys[feature]
-
if type(generator) == str:
- sentence('The island', generator)
-
else:
- featureCount = featureList[feature]
-
if featureCount > 1:
- title(numberedThing(featureCount, generator.title))
- increaseIndentation()
-
for index in range(1, featureCount+1):
- thing = generator()
- sentence(str(index) + '.', thing)
- decreaseIndentation()
- paragraph()
-
else:
- thing = generator()
- sentence(generator.title + ':', thing)
-
if hasattr(generator, 'message') and generator.message not in additionalMessages:
- additionalMessages.append(generator.message)
The key letter is turned into the string describing that feature or the function that will generate the feature using the featureKeys
dict mentioned above. If it’s a string, it gets output immediately.
If it’s a function, the function is called however many times that feature was generated. That is, if there are two volcanoes, the generateVolcano
function is called twice. Single items are presented as a sentence, and multiple items as a numbered list.
Rather than being a serious project, I wrote this script over several days, probably about half an hour or so a day, each day being dedicated to a handful of the tables.
Sparse Key
The island is 1,800 feet wide. It has a general elevation of 998 feet. It receives 59 inches of precipitation per year. It has a growing season of five days. Temperatures on the island range from 81 to 100° F. It is recognizable by a unique cul-de-sac. The weather is currently drizzle.
- The island is approachable through shear cliffs.
- Noise: howling.
- Outcome of shore party: boat sinks.
- If an encounter is needed near the coast, consider one giant squid.
- Dominant creature: will-o-wisp.
- Multiply average precipitation by three if within 150 miles of equator.
- Reduce temperatures by 10° for every 200 miles north of the equator, and by 5° for every 1,500 feet above sea level; in the winter subtract 30%, in the spring subtract 20%, in the summer add 10%, and in the fall subtract 25%.
That is a fascinating little island, a lot like Alien or other haunted house adventures. They’re trapped on an island with a mysterious creature that can draw them out one-by-one, appear out of nowhere and disappear. Their boat was probably capsized by the giant squid, the sheer cliffs make it difficult to get to and from shore. Nice little one-shot for a sea-faring game.
And developing this script was a fascinating, and fun, look at a very early use of a series of tables as a random generator for random, weird, and detailed encounters, specialized for a very specific task.
In response to 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.
I had to look this up. An “ait” or “eyot” is a small island, often one found in a river and caused by sediment.
↑
- Island generator (Zip file, 6.9 KB)
- A script to generate islands using the tables from the Judges Guild Island Book 1 as a guide.
- The Adventure Guide’s Handbook
- Weave fantasy stories around characters that you and your friends create. As a Gods & Monsters Adventure Guide you will present a fantastic world to your players’ characters: all of its great cities, lost ruins, deep forests, and horrendous creatures.
- Alien
- A classic space horror film directed by Ridley Scott and featuring a lot of design by H. R. Giger. The original tagline was “In Space, No One Can Hear You Scream”. Giger’s alien did a pretty good job of making sure that in the movie theater, everyone could.
- Automatically roll subtables
- The final version of the table roller will automatically roll subtables, handling the wandering encounter chart style of Gods & Monsters.
- Island Book I at The Acaeum
- “This booklet includes 48 pages of small island groups and atolls for adventuring: passing ships, damaged by storms, may drop anchor here for repairs or be breached on the rocks, marooning their crewman; or it may be the stronghold of pirates, the sanctuary of a secret religion, or a coven of witches.”
- 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.
More Judges Guild
- 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.
- Skin a module 3: Thracia to The Lost City
- The Judges Guild module Caverns of Thracia is one of the classics of the old-school. It’s also eminently reskinnable by changing the names of gods and expanding on some of the magic items hidden inside.
- Knee deep in monster frogs: A Judges Guild history
- Bill Owen, one of the early members/employees of Judges Guild, has created an amazing color collection of old Judges Guild artifacts: maps, designs, and more from the early days of JG.
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
More Python
- 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.
- 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.
- 30 more pages with the topic Python, and other related pages