Metaprogramming MUD Commands

About a year and a half ago I threw together a “mud” in the space of a few days. I use the word mud loosely. Really, I just threw together the parts to make a basic world (editable only by changing the database entries the program used by hand), allow multiple people to log on, and talk. It worked and I was proud of my Ruby skills, as I hadn’t done too much with Ruby outside of the Rails context at that point.

Of course the problem with just throwing the parts in a bin and shaking it is that when you want to add new things, it gets harder and harder as you go. So, I quickly lost interest, as doing it right would have required a complete rewrite.

Fast forward to a few days ago, and I’ve decided to start working on a MUD again… and do it right. I’ve spent the last couple years doing a ton of Ruby work. So, I’m more confident now that I can do it right.

One thing that is now pretty thoroughly etched into my head, that wasn’t a year ago, is metaprogramming with Ruby. I understood how to do it back then… and how to use DSLs in previous years, however, now I have a better grip on just how freaking great it is. (Partially thanks to Paul Graham’s writings convincing me that bottom-up is awesome.)

I worked on the mud code quite a bit on the days I had off for the holidays, and then most everything evening since. So, I’ve got a pretty good base built up. One part of that base is the “command system”.

The command system is what the mud uses to identify and dispatch commands sent by the players. Things like:

say Hey whats up man
eq sword
score
area list
area delete 1
area create New Area
area set 1 room_start=1000 room_end=1100

Theres a few things that are probably immediately noticeable about this list of commands. First, some of these commands should not be usable by the average player. (Allowing ‘area delete’ for normal players would be a disaster.) Secondly, some commands have sub commands.

The naive (and sad) way to handle this would just be nested case statements. Which, of course, would lead to a tremendous amount of boilerplate code and repetition. Not acceptable… and certainly not worthy of a blog post. However, I think I’ve come up with a pretty slick way of handling this situation, and since it involves metaprogramming, which I love, I thought I’d share it. So, first off, what does the code need to do? It needs to…

  1. Record a block of code to be run when the command is dispatched
  2. Exclude the command based upon roles of players
  3. Facilitate nested (or namespaced) commands
  4. Easily create reusable methods (helpers)

So what would code that accomplishes all those goals look like? Well… Heres what I came up with:

module Roles
  extend RoleBuilder

  role(:player) { |c| true }
  role(:immortal) { |c| c.level > 100 }
  role(:admin) { |c| c.level > 105 }
end

class Command < CommandBase
  player(:say) { |input| message_room input }

  namespace(:area) do |area|
    area.admin(:create) do |i|
      # blah blah blah
    end

    area.immortal(:info) do |i|
      # etc etc etc
    end

    area.player(:list) do |i|
      # pretend theres real code here
    end
  end
end

module Helpers
  def current_char
    @client.character
  end
end


Hey! Thats not too bad. So in the previous code we’ve defined three roles (player, immortal, admin) and four commands (say, area create, area info, area list). In the Roles module, we define the different roles… which are then used in the Command class. Those roles are defined as methods in the Roles module. That module is included in CommandBase (which Command inherits from). Those dynamically generated methods, in turn, define methods which are called by a dispatch method (which is outside the scope of this article, but suffice it to say dispatch looks at “area info 1” and says “okay, call the ‘area’ command”... more on this later). The way namespacing works is pretty self explanatory as well. You might guess that the code that lets all this happen is a bit gnarly. Actually, its not bad. Its been through several revisions, the first of which were fairly nasty… but the “final” version is short and pretty simple. This was aided, in part, by Ruby 1.9. (Bonus points if you can point out the 1.9 feature in use in the code below.)

module RoleBuilder
  def role(name, &role_block)
    define_method(name) do |command,&command_block|
      define_method(command) do |input|
        command_block.call(input) if role_block.call(current_char)
      end
    end
  end
end

So, lets start with the RoleBuilder module. Really quite simple, we have defined a method “role” which creates a function, which in turn will create the functions which actually run commands. So in this case, use of the RoleBuilder module might look like this:

module Roles
  extend RoleBuilder

  role(:player) { |c| true }
  role(:admin) { |c| c.level > 100 }
end

In the above example, we end up defining two new methods: player and admin. The “c” being passed into each of the blocks is the logged in character we’re dealing with. That code is outside the scope of this post (and not interesting). So, just pretend that its obvious that a Character object should be passed into those. Then as long as the role block evaluates to true eventually the resulting command will be called (line 5 of the previous code). Moving on…

Alright, it gets a bit more interesting here. If we want to namespace a command (area list, area create, area delete) we use the “namespace” method defined here. Looking back at the code that uses this code (the first code sample) we see that namespace is used like this:

namespace(:area) do |area|
  area.admin(:create) do |i|
    # code to create a new area here
  end

  area.immortal(:info) do |i|
    # code to display area info here
  end
end

class CommandBase
  extend Roles
  extend Namespace
  include Helpers
end

module Namespace
  def namespace(name,&block)
    ns_class = Class.new(CommandBase)
    yield ns_class
    ns = ns_class.new

    define_method(name) do |input|
      m = input.match(/([^\s]+)\s/)
      ns.send(m[1],m.post_match)
    end
  end
end

So what happens is that “namespace” creates a temporary class which inherits from CommandBase, just like Command, as well as a typical command method, like player or admin. When that method gets called (in this case “area”), it then re-dispatches the command, using the next argument. So, its like this…

  1. input: “area delete 1”
  2. dispatch calls “area”
  3. input: “delete 1”
  4. area command calls its own dispatch
  5. dispatch calls “delete”
  6. input: “1”

Pretty neat. Its almost, sorta, recursive. (Except that its not calling the same code, but rather, very similar code)

Theres at least one other good way I can think of to deal with writing a command system. That way involves pattern matching. If I was going to go back and do this again, I might try that, just for my own amusement, but I think the way I just described wins in practicality and “coolness”. And really… whats programming without coolness?

blog comments powered by Disqus