Sunday, December 28, 2008

DMG Based Dungeon Generator

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.

2 comments:

  1. Your link to the .py file is dead. Would love to see the code, though, so I don't have to write my own.

    ReplyDelete

All Time Most Popular Posts