In my current 3.5 campaign the players will be shortly chasing after the
Keys of Time, which are spread all over various alternate dimensions. Since I totally got bit with the
old-school bug I've decided one of the keys will be found in an old-style Dungeon Crawl Dimension. The characters will start in a 10'x10' dark room(hope they have someway to make light). In the corner will be a 10' pole, a lantern, bundle of torches and a backpack filled with 3 flasks of oil, 10 spikes, a hammer, 50' rope, flint&steel, 1 wk iron rations, and a potion of healing. In each wall will be a wooden door, stuck of course. From there I planned to use the random dungeon generation tables from the DMG. But they turned out to be too unwieldy for use during game. So, I whipped up this simple program to do all the rolls and table lookups for me.
After doing a couple of test runs, I'm a little disappointed with appendix A. Not enough stuff (monsters/treatures/traps), too many freaking wide passages. Too many levels up/down (for my purposes).
For v2 I'll change passages widths to 50% 5', 30% 10', 20% other. Make Traps/Treasures/Monsters more common. Add in room types and dungeon dressings from the other appendixes. Maybe simplified tables to generate magic items, wandering monsters.
There's probably a million of these, this one is mine. Python source
dmg_dungeon_generator.py
Python is keen, by far my favorite programming language. It's a bit like modern rules lite RPG's, there's very little syntax and "rules" to it and what there is has a consistency. Makes it easier to tinker with. For instance the ability to redefine how objects are converted to strings, __str__, lets me define random tables that "roll" themselves when printed. Like so
Door = table_d20(
( 6, "Door left leading to a", Beyond_Door, ),
(12, "Door right leading to a", Beyond_Door, ),
(20, "Door ahead leading to a", Beyond_Door, ),
)
Beyond_Door = table_d20(
( 4, Passage_Width, "parallel passage extending 30' in both directions. Or 10'x10'.", ),
( 8, Passage_Width, "passage straight ahead.", ),
( 9, Passage_Width, "passage ahead/behind 45deg.", ),
(10, Passage_Width, "passage behind/ahead 45deg.", ),
(18, Room, Contents, ),
(20, Chamber, Contents, ),
)
The simple python statement
print Door will randomly roll a d20, say 11, look up matching row in Door table print "Door right leading to a ". Then roll another d20 this time on Beyond_Door table, which leads to other tables and text and so on.
Those are interpreted (along with a few helper classes/functions) by this relatively short class. Not stupendous but I like how simple/clean/flexible the table definitions are.
class Table(object):
"""Object that when evaluated into string will return random result from table"""
def __init__(self, dice, *table):
"""@param dice: callable that returns something comparable to x.
@param *table: list of tuples (x,foo1,foo2,fooN) where x is comparable to return value of dice and foo? are evaluatable into strings.
"""
self.dice = dice
self.table = table
def __str__(self):
"""@return: foo1,foo2,fooN stringified and space separated, from row in table where x >= return value of dice.
"""
return " ".join(str(s) for s in self.get_result())
def get_result(self):
roll = self.dice()
for row in self.table:
if row[0] >= roll:
return row[1:]
return self.table[-1][1:]
def table_d20(*table):
return Table(lambda: random.randint(1, 20), *table)
The table_d20 is a helper function to return a Table object preset to use d20 for its dice rolls. It takes a list of arguments, *table, which in our case are the rows of the table. It passes those along with a lambda function that returns a number between 1 and 20 (our d20 die). We need to wrap the random.randint in a lambda so it's callable, since we want a new random number every time we "roll" on the table.
The Table classes __init__ is nothing special. And the __str__ function just applies the str builtin to each item in the list that self.get_result() returns. It then compiles them all into one string, added a space between each one. If you did now know __str__ gets called whenever the object is converted to a string. e.g str(obj) is basically obj.__str__()
The get_result has all the action. It "rolls the dice" then iterates over the table rows looking at the first item until it finds a match. It then returns all but the first of that row's items. There's a fail safe, if no matching row is found it returns items from the last row.