1d6 more reasons to love Ruby
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.
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 bestn
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!
Reader Comments
27 Sep 2006
27 Sep 2006
27 Sep 2006
27 Sep 2006
27 Sep 2006
27 Sep 2006
27 Sep 2006
27 Sep 2006
27 Sep 2006
27 Sep 2006
27 Sep 2006
27 Sep 2006
However, applying the multiplier to the increment in the multiplication operator would change the way the object is displayed, via to_s, right? Meaning "(1.d6 * 2 + 5).to_s" will be "1d6+10*2"
Yeah, I noticed that after posting the first "patch". It should be fixed in the change to Dice#to_s in the second "patch".
I'll admit that my implementation doesn't cover every possible use of the dice. But pragmatically, I've never seen an RPG say "1d6 * 2 + 5". They always specify the increment first, followed by the multiplier, so I optimized for that case. :)
Good point, but then again, I'm having a hard time remembering a case where I've seen
(1d6 + 4) * 2
. :) The changed behavior of the multiplier and increment still allows(1.d6 + 4) * 2
. Mechanically, you'll get the same numbers. It will stringify as"1d6 * 2 + 8"
, though. The original implementation didn't even allow the reverse however (well, without playing games with the increment to get the expected value).Nice work on the optimized roll implementation. However, I don't believe Array#sum is a standard method, is it?
I don't know if I'd call it optimized... just clarified. And your right, I don't think Array#sum is in 1.8.x. IIRC, it was going to be in 1.9, though. I've got it added into a utility library I frequently use, so I always forget it's not standard. :)
27 Sep 2006
27 Sep 2006
27 Sep 2006
29 Sep 2006
29 Sep 2006
@Albert:
That's a good question. You can look at it this way...
2d6 == 1d6 + 1d6
. For each of the individual 1d6, you can treat it as if it were an integer -- it's non-deterministic, but it's still an integer (since the range is limited to integers). Since we're dealing with integers, even non-deterministic ones, we can apply the distributive property of multiplication on the integers. Thus,2d6 * 4 = 4 * (1d6 + 1d6) = (4 * 1d6) + (4 * 1d6)
. So it's the same either way.2 Oct 2006
Sounds good.. I have looked through your source for the NPC generator, looking to extend and change things. But by C-fu was not up to the challenge..
For the past year or so I have been working on a dynamic npc generator in python. I’m hoping to be able to have a fairly flexible system at the end of it all. I have found it is quite difficult, especially all the niche effects. It is a lot bigger than it seems. Fair juice to you for doing it so well in C…
The one reason that I have started this was because the NPCs that your generator created where naked. I wanted to link it to a equipment gen that would mod AC and the rest.
Pab
26 Oct 2006