Lisp Game (#49)

There's been quite a bit of discussion lately about how much Ruby is and isn't like Lisp. While I've known of Lisp for some time now, I have never really taken the time to play with it until just recently. I started with this great tutorial:

Casting SPELs in Lisp

Of course, just reading it didn't tell me all about the differences between Ruby and Lisp, so I decided to translate it to Ruby. That taught me a lot, so I thought I would share.

This week's Ruby Quiz is to translate the game built in the above tutorial into the equivalent Ruby.

The tutorial is very interactive, so you should probably target irb as your platform. (I did.) Don't worry about the prose here, we're just translating the code from the tutorial.

I would say there are probably two extremes for how this can be done. You can basically choose a direct command-for-command translation or decide to keep the premise but translate the code to typical Ruby constructs. I think there's merit to both approaches. I took the direct translation route for instance, and I had to do a fair bit of thinking when I hit the section on Lisp's macros. On the flip side, I think it would be great too wrap all the data in Ruby's objects and use the tutorial to show off concepts like Ruby's open class system. So aim for whatever approach interests you.


Quiz Summary

I'm not going to show one entire solution this week, but instead try to hit some highlights from many. We received a variety of solutions with impressive insights and downright scary hacks. Let's take the tour.

Cleaning Up the Data

The tutorials data format, a nested group of lists, may be very Lispish, but it's not how we do things in Rubyland. Dominik Bathon did a subtle but effective translation that started with the definition of two simple Structs:

ruby
class TextAdventureEngine

Location = Struct.new(:description, :paths)
Path = Struct.new(:description, :destination)

# ...

end

Mix in a few Hashes and already the data has a lot more structure:

ruby
class WizardGame < TextAdventureEngine

def initialize
@objects = [:whiskey_bottle, :bucket, :frog, :chain]

@map = {
:living_room => Location.new(
"you are in the living-room of a wizard's house. " +
"there is a wizard snoring loudly on the couch.",
{ :west => Path.new("door", :garden),
:upstairs => Path.new("stairway", :attic) }
),
:garden => Location.new(
"you are in a beautiful garden. " +
"there is a well in front of you.",
{:east => Path.new("door", :living_room)}
),
:attic => Location.new(
"you are in the attic of the abandoned house. " +
"there is a giant welding torch in the corner.",
{:downstairs => Path.new("stairway", :living_room)}
)
}

@object_locations = {
:whiskey_bottle => :living_room,
:bucket => :living_room,
:chain => :garden,
:frog => :garden
}

@location = :living_room
end

# ...

end

The rest of Dominik's translation came out quite nice and it's not a lot of code. Do look it over.

One thing I would really want to see in a Ruby version of this tutorial is the use of Ruby's open class system to slowly build up a data solution. Kero's code was working on this:

ruby
class Area
def initialize(descr, *elsewhere)
@descr = descr
@elsewhere = elsewhere
end
end

# ...

class Area
attr_reader :descr
end

class Area
attr_reader :elsewhere
def Area::path(ary)
"there is a #{ary[1]} going #{ary[0]} from here."
end
end

class Area
def paths
elsewhere.collect {|path|
Area::path path
}
end
end

I think that would be a great way to slowly unfold the tutorial.

Kero also figured out the obvious way to eliminate the very first command in the tutorial, be sure to look that up.

Replacing the Macros

The interesting part of the tutorial in question is when it begins using macros to redefine the interface. In Lisp, that allows the tutorial to go from using code like:

(walk-direction 'west)

To:

(walk west)

Of course, Ruby doesn't have macros. (That's the discussion that inspired this quiz!) So, most of the people who solved it handled the interface with a different Ruby idiom. The solution we'll examine here doesn't replace all instances of Lisp macro usage. Different applications would require different Ruby idioms to deal with, but the moral (to me, anyway) is use the tools your language provides. Macros are one of the things that make Lisp act like Lisp. On the other hand, method_missing() is a Ruby tool:

ruby
module Kernel
def method_missing(method_id, *args)
if args.empty?
method_id
else
[method_id] + args
end
end
end

That's some code from Brian Schroeder's direct translation of the tutorial. The key insight at work here is how Ruby would see a line like:

ruby
walk west

The answer is:

ruby
walk( west())

Now we can understand Brian's method_missing() hack. If a method isn't defined, like west(), method_missing() will be called and Brian just has it return the method name, so other methods will get it as an argument. In other words, the above call sequence is simplified to:

ruby
walk( :west)

The walk() method is defined and knows how to handle a :west parameter.

The second half of method missing does one more trick. To understand it, we need to look at a different example. Imagine the following call sequence from later in the game:

ruby
weld chain, bucket

That will work as I've shown it, assuming weld() is a real method and knows what to do with a :chain and :bucket, because Ruby sees the call as:

ruby
weld( chain(), bucket())

Which we have already seen would get simplified to:

ruby
weld( :chain, :bucket)

Brian went one step further though and eliminated the comma:

ruby
weld chain bucket

Ruby sees that as:

ruby
weld( chain( bucket()))

The last call resolves as we have already seen:

ruby
weld( chain( :bucket))

But chain() is also handled by method_missing() and now it has an argument. That's what the second part of method_missing() is for. It adds the method name to the argument list and returns it, which leaves us with:

ruby
weld( [:chain, :bucket])

As long as weld() knows how to handle the Array, you can do without the comma.

Brian uses a different set of Ruby tools, define_method() and instance_eval(), to replace the game action macro. I'm not going to show it here in the interests of space and time, but do take a peek at the code. It's fancy stuff.

A Warning

Use a global method_missing() hack like the above, only when you really know what you are doing. When we're just fooling with irb like this, it is pretty harmless, but it still tripped me up a few times. Many Ruby errors are hidden under the rug when you define a global catch-all like this. That can make it tough to bug hunt.

Some solutions restricted the method_missing() hack to irb only and/or reduced the amount of things method_missing() was allowed to handle. These are good cautionary measures to take, when using a hack like this.

Reversing the Problem

A couple of people tried bringing Lisp to Ruby, instead of Rubifying Lisp. Watch how irb is responding to Dave Burt's solution:

irb(main):001:0> require 'lisperati'
(YOU ARE IN THE LIVING_ROOM OF A WIZARDS HOUSE. THERE IS A WIZARD SNORING
LOUDLY ON THE COUCH. THERE IS A DOOR GOING WEST FROM HERE. THERE IS A
STAIRWAY GOING UPSTAIRS FROM HERE. YOU SEE A WHISKEY_BOTTLE ON THE FLOOR.
YOU SEE A BUCKET ON THE FLOOR.)
=> true
irb(main):002:0> pickup bucket
=> (YOU ARE NOW CARRYING THE BUCKET)
irb(main):003:0> walk west
=> (YOU ARE IN A BEAUTIFUL GARDEN. THERE IS A WELL IN FRONT OF YOU. THERE IS
A DOOR GOING EAST FROM HERE. YOU SEE A FROG ON THE FLOOR. YOU SEE A CHAIN ON
THE FLOOR.)
irb(main):004:0> inventory[]
=> (BUCKET)

I can't decide if that's unholy or not, but it sure is cool. Here's the code Lispifying the Arrays:

ruby
class Array
def inspect # (JUST FOR FUN, MAKE ARRAYS LOOK LIKE LISP LISTS)
'(' + map{|x| x.upcase }.join(" ") + ')'
end
end

One simple override on inspect() gives us Lisp style output. Yikes.

There's more Lisp goodness hiding in Dave's code, so be sure and give it a look.

Daniel Sheppard also took a very Lispish approach, building a Lisp interpreter and then feeding in the Lisp code directly from the web site:

ruby
require 'lisp'

lisp = Object.new
lisp.extend(Lisp)
lisp.extend(Lisp::StandardFunctions)

require 'open-uri'
require 'fix_proxy.rb'

open("http://www.lisperati.com/code.html") { |f|
input = f.readlines.join.gsub(/<[^>]*>/, "")
#puts input
lisp.lisp(input)
}

commands = [
[ "(pickup whiskey-bottle)",
"(YOU ARE NOW CARRYING THE WHISKEY-BOTTLE)" ]
]
open("http://www.lisperati.com/cheat.html") { |f|
f.each { |line|
line.chomp!
line.gsub!("<br>","")
if /^>(.*)/ === line
line = $1
line.gsub!("Walk", "walk") #bug in input
commands << [line, ""]
else
#bugs in input
line.gsub!("WIZARDS", "WIZARD'S")
line.gsub!("ATTIC OF THE WIZARD'S", "ATTIC OF THE ABANDONED")
commands[-1][1] << line
end
}
}
commands.each do |c|
puts c[0]
result = lisp.lisp(c[0])
result = result.to_lisp.upcase
unless result == c[1]
puts "Wrong!"
p result
p c[1]
break
end
end

Here you can see that openuri is used to load pages from the tutorial site, which are parsed for code and fed straight to the Lisp interpreter. I must admit that I never expected to see a solution like that!

I won't show the lisp.rb file here in the interests of time and space, but hopefully the above has you curious enough to take a peek on your own. You won't be sorry you did.

Domain Specific Languages (DSLs)

I'm told Jim Weirich is giving a talk on DSLs at RubyConf, and I believe he actually intends to use this very problem area to discuss them. Some of you have a head start on that now.

Both Jim Menard and Sean O'Halpin sent in the beginning of text adventure frameworks for Ruby. Their goal seemed to be to create reasonable syntax for using Ruby in the creation of such games and there are interesting aspects to each approach.

Let's look at a little bit of Sean's code first:

ruby
# Game definition

game "Ruby Adventure" do

directions :east, :west, :north, :south, :up, :down,
:upstairs, :downstairs

room :living_room do
name 'Living Room'
description "You are in the living-room of a wizard's house. " +
"There is a wizard snoring loudly on the couch."
exits :west => [:door, :garden],
:upstairs => [:stairway, :attic]
end

room :garden do
name 'Garden'
description "You are in a beautiful garden. " +
"There is a well in front of you."
exits :east => [:door, :living_room]
end

room :attic do
name "Attic"
description "You are in the attic of the wizard's house. " +
"There is a giant welding torch in the corner."
exits :downstairs => [:stairway, :living_room]
end

thing :whiskey_bottle do
name 'whiskey bottle'
description 'half-empty whiskey bottle'
location :living_room
end

thing :bucket do
name 'bucket'
description 'rusty bucket'
location :living_room
end

thing :chain do
name 'chain'
description 'sturdy iron chain'
location :garden
end

thing :frog do
name 'frog'
description 'green frog'
location :garden
end

start :living_room

end

Interesting use of blocks and method calls there, isn't it? What's really neat is that under the hood this is a fully object oriented system. The method calls just simplify it for you. Have a look at the game() method implementation, for example:

ruby
def game(name, &block)
g = Game.new(name, &block)
g.look
g.main_loop
end

I love this synthesis of Ruby objects with trivial interface code.

Going a step further, it should be possible to derive the name() attribute from the Symbol parameter to room() and thing(), shaving off some more redundancy.

As you can see, these method don't quite use the typical Ruby syntax. Why is it `name 'frog'` and not `name = 'frog'`, for example? The reason is that the blocks in this code are instance_eval()ed, to adjust self for the call. Unfortunately, because of the way Ruby syntax is interpreted, `name = 'frog'` would be assumed to be a local variable assignment instead of a method call. That forced Sean to use this more Perlish syntax.

To follow up on that, let's see how those attribute methods are implemented:

ruby
class GameObject
extend Attributes
has :identifier, :name, :description
def initialize(identifier, &block)
@identifier = identifier
instance_eval &block
end
end

class Thing < GameObject
has :location
end

class Room < GameObject
has :exits

def initialize(identifier, &block)
# put defaults before super - they will be overridden in block
# (if at all)
super
end

end

Looks like we need to see the magic has() method:

ruby
module Attributes
def has(*names)
self.class_eval {
names.each do |name|
define_method(name) {|*args|
if args.size > 0
instance_variable_set("@#{name}", *args)
else
instance_variable_get("@#{name}")
end
}
end
}
end

end

Notice that the defined attribute methods have different behavior depending on the presence of any arguments in their call. Omit the arguments and you're calling a getter. Add an argument to set the attribute instead.

For more typical Ruby idioms, we turn to Jim's code:

ruby
require 'rads'

$world.player.names = ['me', 'myself']
$world.player.long_desc = 'You look down at yourself. Plugh.'

living_room = Room.new(:living_room) { | r |
r.short_desc = "The living room."
r.names = ['living room', 'parlor']
r.long_desc = "You are in the living-room of a wizard's house."
r.west :garden, "door"
r.up :attic, "stairway"
}

wizard = Decoration.new { | o |
o.location = living_room
o.short_desc = 'There is a wizard snoring loudly on the couch.'
o.names = %w(wizard)
o.long_desc = "The wizard's robe and beard are unkempt. He sleeps " +
"the sleep of the dead. OK, the sleep of the really, " +
"really sleepy."
}

# ...

whiskey_bottle = Thing.new { | o |
o.location = living_room
o.short_desc = "whiskey bottle"
o.names = ['whiskey bottle', 'whiskey', 'bottle']
o.long_desc = "A half-full bottle of Old Throat Ripper. The label " +
"claims it's \"the finest whiskey sold\" and warns " +
"that \"mulitple applications may be required for " +
"more than three layers of paint\"."
}

bucket = Container.new { | o |
o.location = living_room
o.short_desc = "bucket"
o.long_desc = "A wooden bucket, its bottom damp with a slimy sheen."
}

# ...

$chain_welded = false
$bucket_filled = false

class << $world

def have?(obj)
obj.location == player
end

# ...

end

# ================================================================

startroom living_room

play_game

In that code you can see Rooms being built, Decorations added, Things created and even custom methods added to the $world. If you have any experience with Interactive Fiction (IF--a fancy name for these text adventure game), this declarative style code is probably looking pretty familiar.

Jim went so far as to do a minimal port of TADS (Text ADventure System). You can see the Ruby version, RADS, pulled in on the first line.

The main difference you see here is the use of object constructors and that the blocks are passed the objects to configure, allowing the use of standard Ruby attribute methods.

Both solutions are very interesting and worth digging deeper into, when you have some time.

Wrap Up

Just because I didn't mention a solution does not mean it wasn't interesting, especially this week. A lot of code came in and there were great tidbits all around. If you want to learn the great Ruby Voodoo, start reading now!

Thanks so much to all who played with this problem or even just discussed variations on Ruby Talk. As always, you taught me a lot.

Tomorrow's Ruby Quiz: Automated ASCII Art...