From: Leslie Viljoen Date: 2005-11-29T05:10:27+09:00 Subject: Wizard quiz I have written a few text adventures in TADS, the Texts Adventure Development System. The system is excellent and I highly recommend it, but since learning Ruby I've wanted something similar to TADS in Ruby. The only reference to such a project I could find was the Quiz 49: http://www.rubyquiz.com/quiz49.html Here's my late solution, if anyone is interested - but with the intention of building a library that allows near-TADS in Ruby. I've just put in the 'invisible' water object so you can "throw water at wizard". The parser is still very simple but I want to develop it to TADS' level. (TADS: http://www.tads.org/) Comments very welcome! -------------------------- rads.rb module Util Map_syns = [%w(n north), %w(s south), %w(e east), %w(w west), %w(ne northeast), %w(se southeast), %w(sw southwest), %w(nw northwest), %w(u up), %w(d down)] def Util.map_short(long) Map_syns.each do |syn| return syn[0] if syn[1] == long.to_s return long if syn[0] == long.to_s #If long is actually a short, return it end raise "map_short can't find short name for #{long}" end def Util.map_long(short) Map_syns.each do |syn| return syn[1] if syn[0] == short.to_s return short if syn[1] == short.to_s #If short is actually a long, return it end raise "map_long can't find long name for #{short}" end def Util.is_direction?(word) Map_syns.each do |syn| return true if word == syn[0] || word == syn[1] end return false end end class ThingError < Exception attr_reader :noun def initialize(noun) @noun = noun super end end class DirectionError < Exception attr_reader :dir def initialize(dir) @dir = dir super end end class Thing attr_accessor :name, :sdesc, :ldesc, :adesc, :location, :immobile, :invisible def initialize(name, sdesc, ldesc, adesc, location) @name = name @sdesc = sdesc @ldesc = ldesc @adesc = adesc @location = location @immobile = false @invisible = false end end class KickableThing < Thing def do_kick "You kick the #{name} like you mean it" end end class ImmobileThing < Thing def initialize(name, sdesc, ldesc, adesc, location) super @immobile = true end end class InvisibleThing < Thing def initialize(name, sdesc, ldesc, adesc, location) super @invisible = true end end class Map attr_reader :start, :player_room def initialize(rooms, start) @start = start @rooms = rooms #Add a player_room if there wasn't one got_player = false @rooms.each do |room| got_player = true if room.name == "player" end if !got_player @rooms.push(Room.new("player", "")) end @player_room = self["player"] end def find(name) @rooms.each do |room| return room if room.name == name end return nil end alias [] find end class Room attr_accessor :name, :sdesc, :exits def initialize(name, sdesc) @name = name @sdesc = sdesc end def go(dir) return @exits[dir] if @exits[dir] raise DirectionError.new(dir) end def fmt_exits list = [] @exits.keys.each do |short_exit| list << Util.map_long(short_exit) end return list.join(", ") end def fmt_desc sdesc + "\nexits are: " + fmt_exits end end class World attr_accessor :map, :things def initialize(map, things) @map = map @things = things end def find(thing_name) @things.each do |thing| return thing if thing.name == thing_name end return nil end alias [] find def noun_to_thing(noun) @things.each do |thing| return thing if thing.name == noun end raise ThingError.new(noun) end def fmt_list(list) #list of strings -> this, that and last return "" if list.empty? n = list.length if n == 1 return list.first end "#{list[0..n-2].join(", ")} and #{list.last}" end def things_present(location) list = [] @things.each do |thing| list << thing if thing.location == location end list end def things_visible(location) list = [] @things.each do |thing| list << thing if thing.location == location && !thing.invisible end list end def fmt_things(location) list = things_visible(location) return "" if list.empty? alist = list.collect do |thing| thing.adesc end "You see #{fmt_list(alist)} here" end end #TODO: The player object must be carefully spliced to include only # standard things - so it can be nicely overridden for any game class Player attr_reader :world attr_accessor :location def initialize(world) @world = world @location = @world.map.start @player_room = @world.map.player_room end def inventory @world.things_present(@player_room) end def inventory_visible @world.things_visible(@player_room) end def can_reach?(thing) thing.location == @player_room || thing.location == @location end #com_xxx methods implement verbs without objects def com_quit exit end alias com_q com_quit def com_look @location.fmt_desc + "\n" + @world.fmt_things(@location) end alias com_l com_look def com_inventory list = inventory_visible return "You have nothing" if list.empty? names = [] list.each do |item| names << item.adesc end "You have #{world.fmt_list(names)}" end alias com_i com_inventory #do_xxx methods implement verbs with single objects def do_get(thing) return "No matter how hard you try, you cannot move the #{thing.name}" if thing.immobile thing.location = @player_room "You take the #{thing.name}" end alias do_take do_get def do_drop(thing) thing.location = @location "Dropped" end def do_kick(thing) if thing.respond_to?("do_kick") thing.do_kick else "That's not kickable!" end end def do_examine(thing) thing.ldesc end alias do_x do_examine alias do_look do_examine alias do_l do_examine #io_xxx methods implement verbs with two objects #def io_weld(dobj, iobj) #end end class Parser attr_reader :player def initialize(player) @player = player end def parse(line) words = line.downcase.split(" ") %w(the to with in on at).each do |w| words.delete(w) end return if words.empty? verb = words.first ### direction if Util.is_direction?(verb) dir = Util.map_short(verb) #Convert any long direction names to short ones begin @player.location = @player.location.go(dir.to_sym) rescue DirectionError => err return "Sorry, there's nothing in that direction" else return @player.com_look end end ### verb if words.length == 1 method = "com_"+verb if @player.respond_to?(method) return @player.send(method.to_sym) else return "Sorry, I don't know how to #{verb}" end end ### verb + direct object if words.length == 2 method = "do_" + verb begin dobj = player.world.noun_to_thing(words[1]) rescue ThingError => err return "The #{words[1]} is not here" end return "The #{words[1]} is not here" if !@player.can_reach?(dobj) if @player.respond_to?(method) return @player.send(method.to_sym, dobj) else return "Sorry, I don't know how to #{verb} #{dobj.adesc}" end end ### verb + direct obj + indirect obj if words.length == 3 method = "io_" + verb begin dobj = @player.world.noun_to_thing(words[1]) iobj = @player.world.noun_to_thing(words[2]) rescue ThingError => err return "The #{err.noun} is not here" end return "The #{words[1]} is not here" if !@player.can_reach?(dobj) return "The #{words[2]} is not here" if !@player.can_reach?(iobj) if @player.respond_to?(method) return @player.send(method.to_sym, dobj, iobj) else return "Sorry, I don't know how to do that" end end "Sorry, I'm not sure what you mean, try being less wordy" end end -------------------------- wizard.rb require 'rads' ########################################### Class overrides class MyPlayer < Player def initialize(world) super @welded = false @water_filled = false end def io_weld(dobj, iobj) return "There's nothing here to weld with" if @location != @world.map["attic"] if [dobj.name, iobj.name].sort == ["bucket", "chain"] @welded = true "You weld the #{dobj.sdesc} to the #{iobj.sdesc}" else "Welding only really works on metal" end end def do_get(thing) return "He's too heavy" if thing.name == "wizard" super end def io_dunk(dobj, iobj) return "You can't dunk those in this game" if [dobj.name, iobj.name].sort != ["bucket", "well"] return "The water is too deep to reach" if !@welded @world["bucket"].ldesc = "The bucket is full of water" @water_filled = true @world["water"].location = @player_room "You dunk the bucket in the well and fill it with water" end alias io_dip io_dunk def io_splash(dobj, iobj) if [dobj.name, iobj.name].sort != ["bucket", "wizard"] && [dobj.name, iobj.name].sort != ["water", "wizard"] return "You can't splash those in this game" end return "The bucket is empty" if !@water_filled if @world["frog"].location == @world.map["player"] "The wizard awakens but when he sees that you have his pet frog he banishes you to the wild woods!" else "You splash the wizard and he wakes from his slumber! He greets you warmly and gives you a magic wand." + "\nYou win!" end end alias io_pour io_splash alias io_throw io_splash end ########################################### The game - rooms player = Room.new("player", "") garden = Room.new("garden", "The garden is a little overgrown but still lush with plants.") living = Room.new("living", "You are in the living-room of the Wizard's house. There is a wizard snoring loudly on the couch.") attic = Room.new("attic", "You are in the attic of the abandoned house. There is a giant welding torch in the corner.") garden.exits = {:e => living} living.exits = {:w => garden, :u => attic} attic.exits = {:d => living} rooms = [player, garden, living, attic] start = living ########################################### The game - things chain = Thing.new("chain", "chain", "The chain looks quite strong", "a chain", garden) frog = Thing.new("frog", "slimy frog", "The frog is completely unfazed", "a frog", garden) wiz = Thing.new("wizard", "sleeping wizard", "The wizard is dead to the world", "a wizard", living) bucket = KickableThing.new("bucket", "old bucket", "The rusty old bucket looks really old", "a bucket", living) well = ImmobileThing.new("well", "well", "The well is old and the water deep", "a well", garden) water = InvisibleThing.new("water", "water", "The water sloshes as you go", "water", nil) things = [bucket, chain, well, frog, wiz, water] ################################################################# map = Map.new(rooms, start) world = World.new(map, things) player = MyPlayer.new(world) parser = Parser.new(player) puts puts parser.player.com_look loop do print "\n> " line = gets puts parser.parse(line) end