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…
- Record a block of code to be run when the command is dispatched
- Exclude the command based upon roles of players
- Facilitate nested (or namespaced) commands
- 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…
- input: “area delete 1”
- dispatch calls “area”
- input: “delete 1”
- area command calls its own dispatch
- dispatch calls “delete”
- 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?