L
Leslie Viljoen
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, layer_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 layer
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 [email protected]_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 [email protected]_reach?(dobj)
return "The #{words[2]} is not here" if [email protected]_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
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, layer_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 layer
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 [email protected]_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 [email protected]_reach?(dobj)
return "The #{words[2]} is not here" if [email protected]_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