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

Quick-and-dirty old-school island script

Jerry Stratton, July 5, 2023

Haunted Island Adventure Guide’s Handbook cover: The Haunted Island image for the cover of the Adventure Guide’s Handbook.; Persistence of Vision; povray; Gods & Monsters; Hawaii; islands

Islands are great locations for adventures, especially weird ones or constrained ones, which is why I chose an island for the cover of The Adventure Guide’s Handbook.

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.

Ailsa Craig, Scotland: “The round Ailsa Craig on the Waverley Paddle Steamer returning to Ayr. It is an island in the outer Firth of Clyde, Scotland where granite was quarried to make curling stones.”; islands; Scotland

Do you stop at this island or not? Does it matter if it’s called a “terrible island” as opposed to a “rugged isle”? (Photo by Paul Hart, CC-BY 2.0)

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.

Gilligan’s Island incentives: There’s a reason the professor didn’t fix that boat. Incentives matter.; Gilligan’s Island; incentives

Careful what you put on the island, or your players may never want to leave!

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.

Enjoy! (Zip file, 6.9 KB)

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.

  1. I had to look this up. An “ait” or “eyot” is a small island, often one found in a river and caused by sediment.

  1. Wondrous Weapons ->