Like any self-respecting geek, I can claim countless hours spent poring over Dungeons & Dragons manuals. After high school I more-or-less stopped gaming (for various reasons), but I rediscovered the game shortly after the 3rd edition came out. About five years ago (coincidentally about the same time that I discovered Ruby) I actually wrote a suite of utilities to aid Dungeon Masters in creating random non-player characters, treasure hoards, towns and cities, and dungeon maps. In those days, C was my language of choice, so all of those were done “the hard way”. (But boy, was it ever fun!)
Now, looking back (and with a bit of experience with dynamic langauges under my belt) I’ve been wondering how I might have done those generators differently. Most notably, the NPC generator uses all hard-coded data and rules, which makes it quite difficult to extend with new data and rules as more D&D supplements are published. (In fact, it is so hard to extend that I haven’t touched it in about 4 years.)
Well, Ruby is all dynamic and stuff, right? Domain-specific languages, etc, etc?
Right. So I began tinkering. I figured if I could come up with DSL that would let me represent the data for the D&D game, the rest should (more or less) fall into place.
This article isn’t about that DSL, though. That particular DSL is still under construction, as I tinker on it a little bit at a time. However, one particular aspect of that DSL has matured nicely, and I wanted to share.
Just a quick aside for the uninitiated: dice in roll-playing games are described both by how many dice you need to roll, and by the number of sides of the die you’re rolling. Thus, “2d4” means “roll two 4-sided dice”, and “4d8” means “roll four 8-sided dice”.
I think every gamer-programmer in existence has written a “dice roller” app. I’m embarrassed to admit that I’ve done it myself. They are ridiculously simple to write, and all but impossible to use at the gaming table. Face it: real, physical dice are where it’s at.
However, in my DSL, I wanted to be able to define things like hit-dice for character classes, or starting wealth, or the rules for height/weight for the different races, all of which are described in terms of dice rolls.
Originally, I tried defining hit-dice like this:
1
2
3
4
|
character_class :wizard do
hit_die 4
...
end |
Sadly, that didn’t scale well to specifying things like the starting wealth of a character, which is often defined in terms like “3d4 * 10”. My DSL wasn’t rich enough! I considered just using strings ("3d4*10") and parsing them on demand to determine the dice to roll. Yucky. Then, I considered introducing a “dice” helper method (dice(3,4)*10). Also yucky. I needed a way to represent dice, using the language that gamers use. My first attempt was as follows:
1
2
3
4
5
6
7
|
class Integer
def d4
sum = 0
times { sum += (rand(4) + 1) }
sum
end
end |
Using the above, I could say things like 3.d4 and have it return the simulated result. Slick! In my DSL, I could now do:
1
2
3
4
5
|
character_class :wizard do
hit_die 4
gold { 3.d4 * 10 }
...
end |
Here, gold represents the starting wealth of a new wizard. The intention is that when a new character is created, the gold block is evaluated to determine the starting wealth.
All well and good, except there is now this inconsistency with how the hit dice are described. I could describe hit dice the same way, but then I lose the ability to keep track of what the hit die actually was—I only know how many hit points are gained at each level. (I may appear to be splitting the proverbial hair rather fine here, but trust me, to a DM, the difference matters.)
So, the second revision became something like this:
1
2
3
4
5
|
class Integer
def d4
Dice.new(self, 4)
end
end |
The 3.d4 invocation now returns a Dice instance (described below). This new object lets me encapsulate all kinds of nifty functionality. For instance, I can represent 3.d4 * 10 and 1.d8 + 1 and so forth, because I can override the multiplication and addition operators on the Dice class. Using that, I can unify all of my dice-references in the DSL:
1
2
3
4
5
|
character_class :wizard do
hit_die 1.d4
gold 3.d4 * 10
...
end |
Then, in the code that actually generates an NPC using this data, I can do:
1
2
3
|
character.hit_points += cclass.hit_die.roll
character.hit_dice += cclass.hit_die
character.gold += cclass.gold.roll |
Awesome! Things are looking good. However, ability score generation (for determining how smart, strong, wise, etc. a character is) uses some tricky rules, like “roll 4d6 and discard the lowest, summing the rest”. Well, this kind of rule becomes easy to encapsulate with the Dice class:
1
2
3
4
5
|
# the "long" way
score = 4.d6.to_a.sort.last(3).inject(0) { |n,v| n + v }
# after encapsulating the above in a "best" method
score = 4.d6.best(3) |
(Note that Dice#to_a returns an array containing the result of each rolled die. It comes in very handy!)
So, conclusion: Ruby is awesome. Comparing this to the mess of dice rolling routines in my C-implemented utilites, this DSL is wonderful.
Below, you’ll find the link to the Dice implementation I’m using, but I’d encourage you to try implementing your own before you go peeking—it’s quite fun! Consider implementing the following interface:
Dice#*(n): create a new dice instance that represents self multiplied by some integer value.
Dice#+(n): create a new dice instance that represents self incremented by some integer value.
Dice#roll: roll the dice and return the result. Consider making it return an integer or an array, depending on an optional parameter.
Dice#best(n): return the sum (or array) of the best n dice after rolling
Dice#max: return the highest possible value the dice object could return
Dice#min: return the lowest possible value the dice object could return
Dice#average: return the average value of the dice
Dice#to_s: return a string that represents the dice in a nice, readable format (like “4d6+2”).
Once you’ve got your Dice object, try to monkeypatch Integer to give you the nice “4.d6” DSL I described, for each of the standard die types (d4, d6, d8, d10, d12, and d20).
Finally, my implementation: dice.rb
Enjoy!