From: Ruby Quiz Date: 2005-10-06T21:36:07+09:00 Subject: [SUMMARY] Lisp Game (#49) 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!("
","") 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...