Wizard quiz


#1

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


#2

On Nov 28, 2005, at 2:10 PM, Leslie V. wrote:

Comments very welcome!

I haven’t had a chance to dig through your code yet, but I just
wanted to stress that you look through all the solutions to that quiz
if you haven’t already. A lot of good ideas in there. Jim M.
also built a RADS library, for example…

James Edward G. II


#3

James Edward G. II wrote:

On Nov 28, 2005, at 2:10 PM, Leslie V. wrote:

Comments very welcome!

I haven’t had a chance to dig through your code yet, but I just
wanted to stress that you look through all the solutions to that quiz
if you haven’t already. A lot of good ideas in there. Jim M.
also built a RADS library, for example…

I saw that, and I looked at many of the solutions. AFAIR Jim’s parser
allowed running arbitrary methods
in Object - so I wanted to take that out, and also bend a solution to
work a bit more like TADS.
In my program the “kick” verb consults the object (bucket) to see if
it’s “kickable”. This is very similar to
how TADS works with objects.

The real challenge will be the parser - in fact, that might be a good
quiz too. TADS has thousands of lines
dedicated to tokenising and parsing english - breaking sentences into
phrases and phrases into
adverb + (noun phrase) where (noun phrase is adverb + noun-phrase).
Almost a regex pattern matcher
for word types. It has a ranking system where it tries to figure out the
most likely thing you are trying
to say instead of just taking the first possible meaning. I’m still
going through the TADS3 parsing code
trying to understand it.

Les


#4

Leslie V. removed_email_address@domain.invalid wrote:

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

Offtopic, but I was wondering what made you prefer TADS to Inform.

martin


#5

On Nov 29, 2005, at 1:12 AM, Leslie V. wrote:

The real challenge will be the parser - in fact, that might be a
good quiz too.

I’ve been considering this for a quiz. I’m currently in the process
of solving it myself, to prove it’s not too much work. My free time
is scarce currently though, so if you beat me to a solution mail it
to me, along with a count of how many hours it took to build…

James Edward G. II


#6

On Nov 29, 2005, at 8:15 AM, David B. wrote:

James Edward G. II

Parsing English is what I wrote my thesis on at university. I wonder
if a general purpose English tokenizing and parsing library would be
of any use to anyone. I’m thinking of taking it up again.

I would LOVE to see a pure Ruby library for something like this. I
can’t imagine it wouldn’t get used.

James Edward G. II


#7

On 11/29/05, James Edward G. II removed_email_address@domain.invalid wrote:

James Edward G. II
Parsing English is what I wrote my thesis on at university. I wonder
if a general purpose English tokenizing and parsing library would be
of any use to anyone. I’m thinking of taking it up again.

Dave


#8

Martin DeMello wrote:

Offtopic, but I was wondering what made you prefer TADS to Inform.

I can’t actually remember why. Way back when I started with IF I made
an effort to compare the two and TADS came out ahead. I do remember
making use of the HTML features of TADS… since I’m always getting
lost, my game had an automatic graphical map builder which grew as you
went along, a lot like the Magnetic Scrolls games’ map window.

L


#9

David B. wrote:

if a general purpose English tokenizing and parsing library would be
of any use to anyone. I’m thinking of taking it up again.

Take it up! You could use something like this in all sorts of games,
text-to-speech systems, perhaps even voice recognition. Even
if you don’t take it up, some ideas and algorithms would be
greatly appreciated.

Make a topic on my wiki!
http://mobeus.homelinux.org/eclectica

BTW: My wiki has two Ruby script sections where I dump useful
“nuggets” (tiny bits of useful code) and longer scripts. I’d be grateful
to anyone who contributes.

Les


#10

On 11/29/05, David B. removed_email_address@domain.invalid wrote:

Parsing English is what I wrote my thesis on at university. I wonder
if a general purpose English tokenizing and parsing library would be
of any use to anyone. I’m thinking of taking it up again.

Wow! Excellent!.. this could have great use to implement DSLs and
of course for the obvious gaming fun :slight_smile:


#11

James Edward G. II wrote:

James Edward G. II

For a quiz, perhaps you could provide a selection of say 10 or so
sentences that a parser would
need to “understand” and convert to a range of method calls such as
get(obj), light(dobj, iobj),
feed(dobj, iobj) etc.

TO would need to be understood to get the direct-object/indirect-object
right:
feed bird to lion vs. feed bird lion.

I’ll start on such a solution and let you know how it goes…

Les


#12

Hello all,
I’m learning ruby recently and one thing makes me think. is it possible
to
pass more than one block to methods? also is it necessary or is there
need
for that?

niyo


#13

BTW, for simple english parsing I found this to be a great resource:
http://www.mud.co.uk/richard/cgtaghtw.htm

Particularly this BNF is interesting:

command ::= basic-command [adverb]
basic-command ::= predicate object |
predicate indirect-object object |
predicate object preposition indirect-object
predicate :: = verb | verb short-adverb
indirect-object ::= object
object ::= {adjective} noun | object AND {adjective} noun


#14

On Nov 29, 2005, at 7:04 PM, Niyazi Toytok wrote:

Hello all,
I’m learning ruby recently and one thing makes me think. is it
possible to pass more than one block to methods?

It’s not what I would call easy. You can make lambdas and pass those
in. Same effect, but no syntactic sugar.

also is it necessary or is there need for that?

I’ve actually found myself wanting this twice in the last week or
two. I can think of at least some uses.

James Edward G. II


#15

Hi –

On Wed, 30 Nov 2005, Niyazi Toytok wrote:

Hello all,
I’m learning ruby recently and one thing makes me think. is it possible to
pass more than one block to methods? also is it necessary or is there need
for that?

You can pass lambda objects around. For every method call, there’s
only one “the” block, though. It’s a syntactic construct, analogous
to the argument list.

David


#16

Niyazi Toytok wrote:

Hello all,
I’m learning ruby recently and one thing makes me think. is it possible to
pass more than one block to methods? also is it necessary or is there need
for that?

Not in the sense of xyz {|a| …} {|b| …} but I’ve had the
occasional need to yield more than once from the same method.

Like:

def xyz
val = ‘build :a’
yield [1, val]
val = ‘build :b’
yield [2, val]
nil
end

xyz do |i, arg|
case i
when 1
# stuff #
puts arg
when 2
puts “do something with #{arg}”
# stuff #
end
end

#-> build :a
#-> do something with build :b

Flexible enough ?

daz


#17

Not very nice but…

def a(&block)
puts “A”

return block
end

def b(b2)
puts “B”
b2.call
yield
end

b(a { puts “HELLO 1” }) { puts “HELLO 2” }


#18

I’ve checked lambdas and I think it’s not possible also to pass more
than
one proc as parameter to a method also? I used hash instead

p = lambda { |name|
puts “Hello1” + " " + name
}

z = lambda { |name2|
puts “Hello2” + " " + name2
}

class Test
def blockTest(name,procHash)

procHash['test1'].call(name)
procHash['test2'].call(name)

end

end

test = Test.new
procs = { ‘test1’ => p, ‘test2’ => z }
test.blockTest(“niyo”,procs)

niyo

----- Original Message -----
From: “David A. Black” removed_email_address@domain.invalid
To: “ruby-talk ML” removed_email_address@domain.invalid
Sent: Wednesday, November 30, 2005 4:21 AM
Subject: Re: blocks


#19

I was putting & sign before parameters. that’s why I thought it does not
work. thank you
----- Original Message -----
From: “David A. Black” removed_email_address@domain.invalid
To: “ruby-talk ML” removed_email_address@domain.invalid
Sent: Wednesday, November 30, 2005 5:14 AM
Subject: Re: blocks


#20

Hi –

On Wed, 30 Nov 2005, Niyazi Toytok wrote:

I’ve checked lambdas and I think it’s not possible also to pass more than
one proc as parameter to a method also? I used hash instead

You can pass any number of them as normal params:

def block_test(b1,b2)
b1.call
b2.call
end

b1 = lambda { puts “one” }
b2 = lambda { puts “two” }

block_test(b1,b2)

=> one
two

David