My Secret Life as a Spaghetti Coder
home | about | contact | privacy statement
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 =
    named "Short Sword" do
      damage_of 1.d6 + 1
      with_actions :swing, :stab, :thrust, :parry
      and_there_are 10.in_the_world

Then, when you do the following print-outs

puts "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

 def d6, 6)

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

 def +(other)
   @bonus = other

 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

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

 def damage_of(dmg)
   @damage = dmg

 def with_actions(*action_list)
   @actions = action_list

 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!("_", " ")
     raise method_id.to_s + " is not a valid method."

 def and_there_are(num)
   @quantity_existing = num
 alias there_are and_there_are


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!

Leave a comment

Incredible. As I was randomly surfing the web I eventually came across this:

Jamis had the same approach as I did to the die rolling, but almost a year ago. That's not to say I thought it was an original idea on my part - I certainly didn't think so. But to find it completely by chance on the same day I posted this? Surely there's only a 1.d10000000 chance of rolling that outcome!

Posted by Sam on Aug 02, 2007 at 04:24 PM UTC - 5 hrs

Then there's Dwemthy's Array:

Posted by hgs on Aug 31, 2007 at 07:22 AM UTC - 5 hrs

Leave a comment

Leave this field empty
Your Name
Email (not displayed, more info?)


Subcribe to this comment thread
Remember my details

Picture of me

.NET (19)
AI/Machine Learning (14)
Answers To 100 Interview Questions (10)
Bioinformatics (2)
Business (1)
C and Cplusplus (6)
cfrails (22)
ColdFusion (78)
Customer Relations (15)
Databases (3)
DRY (18)
DSLs (11)
Future Tech (5)
Games (5)
Groovy/Grails (8)
Hardware (1)
IDEs (9)
Java (38)
JavaScript (4)
Linux (2)
Lisp (1)
Mac OS (4)
Management (15)
MediaServerX (1)
Miscellany (76)
OOAD (37)
Productivity (11)
Programming (168)
Programming Quotables (9)
Rails (31)
Ruby (67)
Save Your Job (58)
scriptaGulous (4)
Software Development Process (23)
TDD (41)
TDDing xorblog (6)
Tools (5)
Web Development (8)
Windows (1)
With (1)
YAGNI (10)

Agile Manifesto & Principles
Principles Of OOD
Ruby on Rails

RSS 2.0: Full Post | Short Blurb
Subscribe by email:

Delivered by FeedBurner