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:
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:
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:
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:
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:
walk west
The answer is:
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:
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:
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:
weld( chain(), bucket())
Which we have already seen would get simplified to:
weld( :chain, :bucket)
Brian went one step further though and eliminated the comma:
weld chain bucket
Ruby sees that as:
weld( chain( bucket()))
The last call resolves as we have already seen:
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:
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:
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:
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:
# 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:
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:
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:
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:
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...