A couple of evenings ago, after I wrote about
how I got involved in programming and helped a friend with some C++ (he's a historian), I got inspired to start writing a scripting engine for a text-based adventure game. Maybe it will evolve into something, but I wanted to share it in its infancy right now.
My goal was to easily create different types of objects in the game without needing to know much about programming. In other words, I needed a declarative way to create objects in the game. I could just go the easy route and create new types of weapons like this:
short_sword = create_weapon(name="short sword", size="small", description="shiny and metallic with a black leather hilt", damage="1d6+1", quantity_in_game=10, actions="swing, stab, thrust, parry")
But that's not much fun. So I started thinking about
how I'd like to let the game system know about new types of weapons. A
DSL, perhaps. Eventually, I settled on this syntax:
short_sword =
create_small_shiny_and_metallic_with_a_black_leather_hilt_weapon.
named "Short Sword" do
damage_of 1.d6 + 1
with_actions :swing, :stab, :thrust, :parry
and_there_are 10.in_the_world
end
Then, when you do the following print-outs
puts "Name: " + short_sword.name
puts "Size: " + short_sword.size
puts "Description: " + short_sword.description
puts "Damage: " + short_sword.damage.to_s
puts "Actions: " + short_sword.actions.inspect
puts "Quantity in game: " + short_sword.quantity_existing.to_s
You should end up with output like this:
Name: Short Sword
Size: small
Description: shiny and metallic with a black leather hilt
Damage: 1d6 + 1
Actions: [:swing, :stab, :thrust, :parry]
Quantity in game: 10
We could create just about any game object like that, but I've yet to do so, and I don't think
adding it here would do much of anything besides add to the length of the post.
Ideally, I'd want to remove some of those dots and just keep spaces between the words, but then Ruby
wouldn't know which arguments belonged to which methods. I could use a preprocessor that would allow
me to use spaces only and put dots in the appropriate places, but that would needlessly complicate things
for right now. I'll consider it later.
The first thing I noticed about the syntax I wanted was that the
Integer
class would
need some changes. In particular, the methods
in_the_world
and
d6
(along with other dice methods) would need to be added:
class Integer
def in_the_world
self
end
def d6
DieRoll.new(self, 6)
end
end
The method
in_the_world
doesn't really need to do anything aside from return the object it
is called upon, so that the number can be a parameter to
and_there_are
. In fact, we
could do away with it, but I think its presence adds to the readability. If we kept it at
and_there_are 10
, the code wouldn't make much sense.
On top of that, we might decide that
other methods like
in_the_room
or
in_the_air
should be added. At that point
we could have each return some other object that
and_there_are
could use to determine
where the objects are. Upon making that determination, it would place them in the game accordingly.
Then we see the
d6
method. At first I tried the simple route using what was available and
had
d6
return
self + 0.6
. Then, damage_of could figure it out from there.
However, aside from not liking that solution because of magic numbers, it wouldn't work for weapons with
bonuses or penalties (i.e., a weapon that does 1d6+1 points of damage). Therefore, we need to introduce
the
DieRoll
class:
class DieRoll
def initialize(dice, type)
@dice = dice
@type = type
@bonus = 0
end
def +(other)
@bonus = other
self
end
def to_s
droll = @dice.to_s + "d" + @type.to_s
droll += @bonus.to_s if @bonus < 0
droll += " + " + @bonus.to_s if @bonus > 0
droll
end
end
The
initialize
and
to_s
methods aren't anything special.
We see that
initialize
simply takes its arguments and sets up the
DieRoll
while
to_s
just formats the output when we want to display a
DieRoll
as a string. I'm not too thrilled about the name of the class, so if you've got something better,
please let me know!
The
+
method is the only real interesting bit here. It's what allows us to set the bonus
or penalty to the roll.
Finally, we'll need to define
named
,
damage_of
,
with_actions
,
and_there_are
,
and
create_small_shiny_..._with_a_black_leather_hilt_weapon
. I've put them in a
module
now for no other reason than to have easy packaging. I'd revisit
that decision if I were to do something more with this.
In any case, it turns out most these methods are just cleverly named setter functions,
with not much to them. The two notable exceptions are
create\w*weapon
and
named
. You can see all of them below:
module IchabodScript
attr_reader :name, :damage, :actions, :quantity_existing, :size, :description
def named(name)
@name = name
yield
self
end
def damage_of(dmg)
@damage = dmg
end
def with_actions(*action_list)
@actions = action_list
end
def method_missing(method_id, *args)
create_weapon_methods = /create_(\w*)_weapon/
if method_id.to_s =~ create_weapon_methods
@description = method_id.to_s.gsub(create_weapon_methods, '\1')
@size = @description.split('_')[0]
@description.gsub!("_", " ")
@description.gsub!(@size,"")
else
raise method_id.to_s + " is not a valid method."
end
self
end
def and_there_are(num)
@quantity_existing = num
end
alias there_are and_there_are
end
Although it is slightly more than a setter,
named
is still a simple function. The only
thing it does besides set the
name
attribute is
yield
to a block that is passed to it.
That's the block we see in the original syntax beginning with
do
and ending (surprisingly)
with
end
.
The last thing is
create_size_description_weapon
. We use
method_missing
to
allow for any
size
and
description
, and check that the method matches our
regex
/create_(\w*)_weapon/
before extracting that data. If it doesn't match, we just raise an
exception that tells us the requested method is not defined.
If I were to take this further, I would
also check if the method called matched one of the actions available for the weapon. If so, we'd
probably find a way to classify actions as offensive or defensive. We could then print something like
"You #{method_id.to_s} your sword for #{damage.roll} points of damage" (assuming we had a
roll
method on
DieRoll
).
As always, any thoughts, questions, comments, and critcisms are appreciated. Just let me know below.
Hey! Why don't you make your life easier and subscribe to the full post
or short blurb RSS feed? I'm so confident you'll love my smelly pasta plate
wisdom that I'm offering a no-strings-attached, lifetime money back guarantee!