The maze book for programmers!
mazesforprogrammers.com

Algorithms, circle mazes, hex grids, masking, weaving, braiding, 3D and 4D grids, spheres, and more!

DRM-Free Ebook

The Buckblog

assorted ramblings by Jamis Buck

1d6 more reasons to love Ruby

27 September 2006 — 5-minute read

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!

Reader Comments

All that talk of dice awoke loads of memories of playing D n D from when I was a kid. I remember labouring over creating characters for up to a day for some weird reason I never applied programming to the problem. It would seem a bit weird everyone sitting around with laptops though, I suppose I'm just a luddite.
Dan, I totally agree that laptops are inappropriate at the gaming table. The programming I did for D&D was intended to help DM's prepare for a game, not necessarily to be used in-game.
Jamis, Thanks for taking the time to start blogging again. Your last few posts (OK, most of your posts!) have been very informative and engaging. Keep up the good work!
Thanks for the kind words, Kevin! I've made a goal to try and blog every day for a week (excepting weekends), and I've just about met that goal. I'm going to see how long I can keep it up, without resorting to banal entries. :) If anyone has any Ruby- or Rails-related topics they would like to see discussed, I'm open to suggestions!
Thanks for sharing Jamis! I personally like reading up on other people's methods of writing their own Ruby DSL's and what they are doing with Ruby.
Great article!
Nice work with the DSL. As the language progresses, please do keep us informed. I've been working on a similar thing in python. Just trying to give myself some nice DM tools. At any rate, seeing different implementations is always good for the creative juices. Thanks, e.
Excellent article. I also want to encourage those who tend to refer to D&D playing as the "good old days", to pick it up again. Playing it as an adult is just as fun.
A patch to your Dice class. :) I think the change to Dice#* is significant, but the other two are just aesthetic -- definitely subject to taste. class Dice # Consider 1.d6 * 2 + 5 # The original will range from 12..22 # This new one will range from 7..17 (not multiplying # 5 by 2) def *(n) Dice.new(count, sides, increment*n, multiplier*n) end # Similar to yours but IMO slightly clearer. The only # difference is the existence of the intermediate hash # and extra iteration (in sum) in the case where # collect is false. Unless count is significantly large # this should have minimal impact. Also removed the # application of multiplier from the increment due to # the above change. def roll(collect=false) result = (1..count).collect{ roll_one * multiplier } result << increment unless increment.zero? collect ? result : result.sum end private # factored out for clarity, not really significant def roll_one rand(sides) + 1 end end Jacob Fugal
Sorry... maybe formatting will work this time.
  class Dice
    # Consider 1.d6 * 2 + 5
    # The original will range from 12..22
    # This new one will range from  7..17 (not multiplying
    # 5 by 2)
    def *(n)
      Dice.new(count, sides, increment*n, multiplier*n)
    end

    # Similar to yours but IMO slightly clearer. The only
    # difference is the existence of the intermediate hash
    # and extra iteration (in sum) in the case where
    # collect is false. Unless count is significantly large
    # this should have minimal impact. Also removed the
    # application of multiplier from the increment due to
    # the above change.
    def roll(collect=false)
      result = (1..count).collect{ roll_one * multiplier }
      result << increment unless increment.zero?
      collect ? result : result.sum
    end

    private
    # factored out for clarity, not really significant
    def roll_one
      rand(sides) + 1
    end
  end
Sorry to be spamming your blog... more changes to go along with the above.
  class Dice
    # added bit to keep only the actual dice
    def best(n, collect=false)
      list = to_a.first(count).sort.last(n)
      collect ? list : list.sum
    end

    # Changed to pull increment outside the multiplier
    def max
      (count * sides) * multiplier + increment
    end

    # Changed to pull increment outside the multiplier
    def min
      count * multiplier + increment
    end

    # Changed to print the multiplier and increment in the
    # same order they are applied
    def to_s
      s = "#{count}d#{sides}"
      s << "*%d" % multiplier if multiplier != 1
      s << "%+d" % increment if increment != 0
      s
    end
  end
Jacob, Thanks! 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". 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. :) Nice work on the optimized roll implementation. However, I don't believe Array#sum is a standard method, is it?

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. :)

Greetings to Caldwell from South of Lake Lowell! Nice article. I played the Warlock variant of D&D, so things were a bit different (lo, those many years ago), but still a cool idea to do a DSL for this domain. I messed around just a bit, and came up with a REBOL dialect that looks like this: ; test case result [roll [D6]] [3] [roll [2 D6]] [1 6] [roll [3 D4]] [4 3 1] [roll [4 D8]] [4 8 4 2] [sum roll [3 D6]] 7 [avg roll [5 D20]] 14.2 [10 * sum roll [3 D4]] 50 [roll [4 D6 drop lowest]] [2 4 6] [roll [4 D6 keep best]] [4] [roll [the best 3 of 4 D6]] [4 3 3] [roll [worst 3 of 4 D6]] [1 1 3] [roll [4 D6 drop the worst 2]] [3 5]
Argh. Need to use
 I guess?

;test case                      result
[roll [D6]]                     [3]
[roll [2 D6]]                   [1 6]
[roll [3 D4]]                   [4 3 1]
[roll [4 D8]]                   [4 8 4 2]
[sum roll [3 D6]]               7
[avg roll [5 D20]]              14.2
[10 * sum roll [3 D4]]          50
[roll [4 D6 drop lowest]]       [2 4 6]
[roll [4 D6 keep best]]         [4]
[roll [the best 3 of 4 D6]]     [4 3 3]
[roll [worst 3 of 4 D6]]        [1 1 3]
[roll [4 D6 drop the worst 2]]  [3 5]
Am I totally off here, but... as I can recall I havent seen any use of the multiplier as described here... I mean: 3d6 * 2 isnt 3(d6*2) right? I cant recall ever that i got to multiply each roll... Or have I just been cheated out of a vast amount of gold? :<
albert, it's not generally something the players get to do. When a DM rolls for random treasure, or when he wants to know what the age of some elf is, the multipler comes into play. Also, as I mentioned above, starting gold for a particular character class is determined as 3d4x10 (meaning "roll 3d4, then multiply the result by 10").

@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.

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