Computational Dungeons & Dragons
I play Dungeons and Dragons (shocker, I know) - as fun as D&D is however, the flow of gameplay is often interrupted with extended periods of tedious calculation, particularly if you’re the DM. You should then find it no surprise that I’ve built up quite an array of programs and workflows to streamline the D&D experience so that I can spend more time playing and less time calculating.
Rolling physical dice is fun. I try to roll physical dice whenever I can, however some calculations with dice are just plain tedious, and that’s where the computer comes in. Take for example, the process of rolling the ability score values of a new character in D&D 5E:
Roll six sets of 4d6, summing the highest three numbers of each set.
I’ve written numerous programs and scripts to facilitate the dice-rolling process, but the latest iteration was a Python script I wrote called roll.
roll essentially works by first concatenating all positional arguments into a single string and then replacing all substrings of the form
\d*[dD]\d+ with their “rolled” results. I distinguish
XDY such that
XdY will compute the sum of the results, whereas
XDY will only return the list of results. The resulting string itself is then computed via the Python interpreter via an
eval() call. This is so the user can embed valid Python into their “roll string”. For example:
$ roll 'sum(h(3D8, 2))' + 3d6 + 2
which essentially flows like so:
roll_str = 'sum(h(3D8, 2)) + 3d6 + 2' # ... replacement code here ... repl_roll_str = 'sum(h([3, 6, 2], 2)) + (11) + 2' print(str(eval(repl_roll_str)))
In the above example,
h(l, n) is an alias for
highest(l, n), which is a function that returns a list containing the highest
n integers in the list
roll even allows some customization by providing the script with a YAML configuration file containing key-value maps between substrings in the given roll string, and the effective roll string to replace. Coming back to our tedious ability score example, this lets us define
ability_scores: >- [sum(h(4D6,3)), sum(h(4D6,3)), sum(h(4D6,3)), sum(h(4D6,3)), sum(h(4D6,3)), sum(h(4D6,3))]
in a file called
roll.yaml and then execute
$ roll -c roll.yaml 'ability_scores'
which might return something like
[10, 11, 10, 16, 13, 18]
If we plan on using the same configuration file most of the time, we can just set the
ROLL_CONFIG_FILE environment variable instead of passing
-c roll.yaml to the script. There are a few other convenient things the script can do as well, like displaying verbose and colored output.
Loot tables are a pain, which is why I created a sort-of sister program to
roll called loot. Like
roll (and like 90% of the other CLI programs I write for some reason),
loot makes use of YAML configuration files, however they are much more front-and-center to the script’s functionality. Let’s first take a look at an example configuration file to make sense of what the script does:
# Example Loot Configuration File # ------------------------------- # The "quality" of the item quality: - name: "shoddy quality" weight: 2 - name: "mundane quality" weight: 3 - "fine quality" # ---------- Loot Table ---------- loot: # ----- Gold Coins ----- - name: "gold" weight: 3 loot: - name: "Gold Coins (1d6 + 1)" weight: 2 - name: "Gold Coins (1d8 + 2)" weight: 3 - name: "Gold Coins (1d10 + 4)" weight: 2 - "Gold Coins (1d12 + 7)" # ----- Weapons ----- - name: "weapon" types: quality loot: - name: "A dagger" weight: 2 - "A longsword"
Essentially, the script crawls through the various layers of items within the
loot key, randomly selecting which path it takes at each layer, which is augmented by assigning an optional
weight to the entry. When it reaches an entry which is just a string or doesn’t contain its own
loot key, it prints the item. If the parent of the item specifies a
types key, the script will also roll for the specified type, appending the result to the end of the selected item in parentheses. An output of a run on the above configuration file may look something like:
$ loot -C 3 Gold Coins (1d6 + 1) A dagger (shoddy quality) Gold Coins (1d8 + 2)
Note that the
-C argument specifies the number of times to roll for loot. The script also allows you to specify a particular sub-table via the
$ loot -C 3 -t weapon A dagger (fine quality) A dagger (mundane quality) A longsword (mundane quality)
Take a look at the full example configuration file (and the one that I use for my campaigns) for a better idea.
As a frequent DM, I often have to generate statistics and maps for the worlds that I create, which is another task I find tedious. I won’t go that in-depth here, as I am still in the process of building a new set of tools to facilitate world generation, but I will talk a bit about what I have built in the past and what online tools I currently use.
As far as map generation is concerned, back in 2017 I wrote a simple terrain generation notebook in Mathematica that utilized weighted edge Voronoi meshes, partly inspired by this blog post, as well as this one:
I suggest reading the above posts for more information on procedural map generation. Perhaps in the future I’ll update this section when I develop a new version of the above tool.
Another fantastic tool for generating maps, factions, plots, items, and pretty much everything else you can think of, is Dwarf Fortress. In fact, it’s such a good tool for this that I’ll probably write about it in its own blog post.
Here’s a random assortment of useful links that I frequently use.
Misc Random Generators & Tools