Saturday 3 September 2011

Hello World. With dice.

Hello. From now on, I probably, maybe, will post some more-or-less interesting things related to programming and/or gaming here.

Let me start with the first gem, namely Ruby.
I'm writing something that could be called a "game", and it makes heavy use of die rolls. First, I of course needed a way to store and roll dice, so a dice class was the obvious first choice.

class Dice
  attr_accessor :amount, :sides

  def initialize(amount, sides)
    @amount, @sides = amount, sides
  end

  def roll
    result = 0
    @amount.times { |i| result += Random.rand(@sides) + 1 }
    return result
  end
end

Pretty straightforward. Minor additions include a to_s functions that outputs the object in the common dice nomination ("xdy", where x is the amount of dice, and y how many sides each die has) and a class method that parses such a string back into a Dice object.

This class is a nice thing to have, probably reusable, and so on. But it's far from done.
Now, you see, when you use dice rolls a lot, you'll probably also want to roll some dice without unnecessarily creating Dice objects every time. Doing
Dice.new(2, 6).roll
is not only ugly, but also creates redundant objects. So, we'll also want Dice::roll.

Now,
Dice.roll(2, 6)
is much better. But still pretty verbose.

You see, Ruby is very flexible and has a lot of wonderful features that allow for really cool code. I thought, hey, it would be awesome to just write 2d6 and have it roll 2d6. Then I came across method_missing. It is defined in BasicObject and called whenever an object is sent a message it cannot process. For example, Dice.foo would cause a call to method_missing, which by default (per definition in BasicObject) raises an exception.

We can intercept that. See where I'm going?

I promptly wrote a neat module. Look:

module DiceRolls
  def method_missing(sym, *args, &block)
    sym.to_s.match(/^_(\d+)d(\d+)$/) do |md|
      result = 0
      md[1].to_i.times { |i| result += Random.rand(md[2].to_i) + 1 }
      return result
    end
    super
  end
end

This, I am proud of.
Let me explain. The sym argument is the name of the method we tried to call.

(By the way, you'll recognize the three innermost lines as duplicate code from the above Dice#roll, which actually rolls the required amount of dice. They can be substituted with the previously defined Dice::roll, with md[1] and md[2] as arguments. I just wrote it that way to prevent any dependencies.)

The regexp matches strings of the form "_[x]d[y]", where x and y are... well, you should know by now. The underscore is there because Ruby function names can't start with a digit. Basically, whenever a function of that form is called, it is intercepted and processed by method_missing (unless it IS defined... but what are the odds? And even if, it's such a rare occurrence that you'd probably be aware of it).

If a class includes that module, you could just call _2d6  from anywhere in that class and get the ready random result. This is much more graceful than the clunky
Dice.roll(2, 6)
used before. The chance of name clashes (IE. a method of that form already defined) is, again, negligibly small, since... I mean, seriously. Who would call a method _2d6?
The super at the end passes the message on to the ancestors method_missing, to deal with any messages we didn't process.

Now. You could go one step further. I personally use it for my project, since I know it won't cause any harm, but I'm not sure it's always entirely safe.
That is, include the module in Object. Then, you can use this trick ANYWHERE, including top-level code.

I also later defined a similar match block that catches methods of the form "c[x]d[y]" and returns a new, matching Dice object:

module DiceRolls
    def method_missing(sym, *args, &block)
        sym.to_s.match(/^([_c])(\d+)d(\d+)$/) do |md|
            case md[1]
                when "_"
                    return Dice.roll(md[2].to_i, md[3].to_i)
                when "c"
                    return Dice.new(md[2].to_i, md[3].to_i)
            end
        end
        super
    end
end

(I keep the Dice class and this module together in one file, which I have on my load path, so I finally decided to use Dice::roll and Dice::new.)

I'm sure there are other applications for scenarios where similar notations (like [x]d[y]) exist, I can't think of any for now, though.

I love Ruby.

No comments: