Skip to content

Instantly share code, notes, and snippets.

@feltnerm
Created December 8, 2012 01:43
Show Gist options
  • Save feltnerm/4238103 to your computer and use it in GitHub Desktop.
Save feltnerm/4238103 to your computer and use it in GitHub Desktop.
Psuedo-Interactive Fiction Interpreter. Done via recursive descent.
#!/usr/local/bin/ruby
#
# dungeon.rb: dungeon state + operations to add/remove objs, open doors, etc.
# - Each object is represented as a simple string, and the intent is that
# each object would be in just one location.
#
# This file contains unit test code; to run it, type
# ruby dungeon.rb
# @author: Rob Hasker
# uncomment following require to enable debugging
#require 'ruby-debug'
# Captures state of the dungeon
class Dungeon
attr_reader :rooms # type: Hash(String -> Room)
attr_reader :pack # type: BackPack
attr_accessor :current_room # type: Room - room character is in
# initialize: init dungeon w/ no rooms and an empty backpack
def initialize
@rooms = {}
@pack = BackPack.new
end
# returns the item's location, either a room or the backpack
# If the item is not in the dungeon, returns nil.
def location_of(item)
return @pack if @pack.has?(item)
@rooms.each { |id, r|
return r if r.has?(item)
}
return nil
end
# Add the room to the dungeon
def add(room)
raise "Illegal room: #{room.inspect}" unless room.class == Room
puts "Room #{room.id} already exists." if @rooms[room.id]
@rooms[room.id] = room
@current_room = room unless @current_room
end
# removes item from dungeon, effectively destroying it
def remove(item)
loc = location_of(item)
loc.remove(item)
end
end
######################################################################
# places where objects can be
class Location
attr_accessor :contents # type: Hash(String -> Boolean)
# (effectively a set)
# initialize so the list of objects is empty
def initialize
@contents = {}
end
# does this location have the specified object?
def has?(item)
! @contents[item].nil?
end
# add object to this location if it is not already present
def add(item)
if ! has?(item)
@contents[item] = true
end
end
# remove object from this location
def remove(item)
@contents.delete(item)
end
protected
# return list of contents for status reports
def sorted_contents
@contents.keys.sort
end
end
# information about a particular room
class Room < Location
attr_reader :id # string identifier for room
attr_reader :exits # hash from directions to room names
# giving open doors out of the room
attr_accessor :entry, :action # source code to execute on entry
# and after executing certain actions
# room identifier + code to execute
def initialize(id, code = [])
super()
@id = id
@exits = {}
action_index = code.index('action:')
if action_index
@entry = code.shift(action_index)
code.shift # skip the action: label
@action = code
else
@entry = code
@action = []
end
end
# list all objects in the room
def describe_objects
sorted_contents.each { |obj|
article = 'aeiou'.include?(obj[0..0].downcase) ? 'an' : 'a'
puts "There is #{article} #{obj} on the ground."
}
end
# display status of room as specified in writeup
def show_status
puts "Status for room #{id}:"
if @contents.empty?
puts " The room is empty."
else
puts " Contents: #{sorted_contents.join(', ')}"
end
if @exits.empty?
puts " No exits."
else
doors = []
doors << 'north' if @exits[:north]
doors << 'south' if @exits[:south]
doors << 'east' if @exits[:east]
doors << 'west' if @exits[:west]
puts " There are exits to the #{doors.join(', ')}."
end
end
# returns true iff there is a door in the specified direction
# Direction must be :north, :south, :east, or ;west
def open?(direction)
! @exits[direction].nil?
end
# open a door leading to the given destination
def open(direction, destination)
@exits[direction] = destination
end
end
# the status of the character's backpack
class BackPack < Location
# show status of backpack by listing its contents
# The list is in alphabetical order for concreteness.
def show_status
if @contents.empty?
puts "Your backpack is empty."
else
puts "Backpack contents:"
puts " #{sorted_contents.join(', ')}"
end
end
end
# class for exception to raise when the character dies
class Death < Exception
end
if __FILE__ == $0
# code to test the dungeon classes; to execute, type
# ruby dungeon.rb
class AssertionError < StandardError
end
def assert a
raise AssertionError, "#{a.inspect}" unless a
end
# uncomment following to check assertion works:
#assert false
# test dungeon and related classes:
d = Dungeon.new
a = Room.new('A')
b = Room.new('B', [:one, :two, 'action:', :three, :four])
assert b.entry == [:one, :two]
assert b.action == [:three, :four]
d.add(a)
d.add(b)
assert d.current_room == a
assert d.location_of('pear').nil?
a.add('pear')
assert d.location_of('rock').nil?
d.current_room.add('rock')
b.add('hammer')
assert a.has? 'pear'
assert a.has? 'rock'
assert b.has? 'hammer'
assert !a.has?('hammer')
d.pack.add('wine')
assert d.location_of('hammer') == b
assert d.location_of('rock') == a
assert d.location_of('wine') == d.pack
assert !a.open?(:east)
a.open(:east, b)
assert a.open?(:east)
d.remove('wine')
assert d.location_of('wine').nil?
assert !d.pack.has?('wine')
d.remove('rock')
assert !a.has?('rock')
assert d.location_of('rock').nil?
puts "All tests pass."
end
<story> -> <room_list> do: <command_list>
<room_list> -> <room_declaration> { <room_declaration> }
<room_declaration> -> <room_name> '{' <instruction_list> '}'
<instruction_list> -> { <instruction> }
<instruction> -> add <object>
-> open <direction> to <room_name>
-> print '(' <message_text> ')
-> if <test> then <instruction_list> [ else <instruction_list> ] end
<direction> -> north | south | east | west
<test> -> <object> in pack
<command_list> -> { <command> }
<command> -> take <object>
-> go <direction>
-> status
-> inventory
#!/usr/local/bin/ruby
#############################################################################
#
# ifi.rb: interactive fiction intepreter
#
# @author: Mark Feltner
#############################################################################
require 'dungeon'
# print an error message and quit - useful for reporting parse erros
# rest_of_code is an array of strings and helps in knowing what
# part of the input has not been parsed yet. Note that you do not
# have to call this function - reporting errors can be helpful
# but is not required for the assignment.
def report_error(message, rest_of_code = [])
abort "Error: #{message}\n -- remaining code: #{rest_of_code.join(' ')}"
end
# class to hold the results of parsing the input file
class Story
def initialize
@dungeon = Dungeon.new
end
# Divides input code into two segments. The first is room definitions
# and instructions, the second is the command list.
def split_code(code)
index = code.index("do:")
return code.slice(0..index-1), code.slice(index+1..code.length)
end
# Parses the room definitions list and executes the commands of the story
# <story> -> <room_list> do: <command_list>
def do_story(code)
room_list, command_list = split_code(code)
load_room_list(room_list)
do_command_list(command_list)
end
protected
# run the instructions for a room.
# This needs to create a copy of the code so that any changes
# to the list (such as removing items that have been parsed)
# will not result in permanent changes to the code for the room.
def run(code)
# create copy of the room
copy = code.clone
# execute the code
do_instruction_list(copy)
end
######################
# ROOM LIST
######################
# Iterate over the list of room defintions until the end
# <room_list> -> <room_declaration> { <room_declaration> }
def load_room_list(code)
while !code.empty? and code.first != "do:"
parse_room_declaration(code)
end
end
# Parse a room declaration
# <room_declaration> -> <room_name> '{' <instruction_list> '}'
def parse_room_declaration(code)
room_name = code.shift
if code.first == '{'
room = Room.new(room_name, code[1...code.index("}")])
@dungeon.add(room)
end
end
# Iterate over the list of instructions
# Execute is recursively passed down to each function and controls
# whether or not that command is executed rather than just syntactically
# parsed.
# <instruction_list> -> { <instruction> }
def do_instruction_list(code, execute = true)
while !code.empty? and code.first != '}'
do_instruction(code, execute)
end
end
# Handles a single instruction
# <instruction> -> add <object>
# -> open <direction> to <room_name>
# -> print '(' <message_text> ')
# -> if <test> then <instruction_list>
# [ else <instruction_list> ] end
def do_instruction(code, execute)
instruction = code.shift
if instruction == "add"
do_instruction_add(code, execute)
elsif instruction == "open"
do_instruction_open(code, execute)
elsif instruction == "print"
do_instruction_print(code, execute)
elsif instruction == "if"
do_instruction_if(code, execute)
end
end
# Add an object to the room
# add <object>
def do_instruction_add(code, execute)
object = code.shift
if execute
if @dungeon.location_of(object) == nil
@dungeon.current_room.add(object)
puts "The #{object} is on the ground."
end
end
end
# Open a door to another room
# open <direction> to <room_name>
def do_instruction_open(code, execute)
direction = code.shift.to_sym
room_name = code.shift(2)[1]
if execute
if !@dungeon.current_room.open?(direction)
@dungeon.current_room.open(direction, room_name)
puts "A door to the #{direction} opens."
end
end
end
# Print something
# print '(' <message_text> ')'
def do_instruction_print(code, execute)
message_text = ""
code.shift
while code.first != ')'
message_text << code.shift
message_text << " "
end
if execute
puts message_text.chomp " "
end
code.shift
end
# Evaluates a test condition for the 'if' command and determines whether
# the following <instruction_list> should be executed rather than just
# syntactically parsed.
# if <test> then <instruction_list> [ else <instruction_list> ] end
def do_instruction_if(code, execute)
if execute
condition = test_condition(code)
if condition
do_instruction_then(code, true, false)
else
do_instruction_then(code, false, true)
end
else
condition = test_condition(code)
if condition
do_instruction_then(code, false, false)
else
do_instruction_then(code, false, false)
end
end
end
# Parses and potentially executes the <instruction_list> when a 'then'
# has been found.
def do_instruction_then(code, execute_then, execute_else)
code.shift # shift over 'then'
while !code.empty?
if code.first == "else"
do_instruction_else(code, execute_else)
elsif code.first == "end"
code.shift
return
else
do_instruction(code, execute_then)
end
end
end
# Parses and potentially executes the <instruction_list> when an 'else'
# has been found.
def do_instruction_else(code, execute)
while !code.empty? and code.first != "end"
do_instruction(code, execute)
end
end
# Test the condition following an 'if'
def test_condition(code)
object = code.first
code.shift(3)
return @dungeon.pack.has?(object)
end
######################
# COMMAND LIST
######################
# Descends into each command found in the <command_list>
# <command_list> -> { <command> }
def do_command_list(code)
run(@dungeon.current_room.entry)
while !code.empty?
do_command(code)
end
end
# Handles a single command
def do_command(code)
command = code.shift
if command == "status"
do_command_status(code)
elsif command == "take"
do_command_take(code)
elsif command == "go"
do_command_go(code)
elsif command == "inventory"
do_command_inventory(code)
end
end
# Executes the 'status' command to show the current room's stuff and doors
def do_command_status(code)
puts "> status"
@dungeon.current_room.show_status
end
# Executes the 'inventory' command to show the player's current pack
def do_command_inventory(code)
puts "> inventory"
@dungeon.pack.show_status
end
# Executes the 'take' command which puts an object from the room to the
# player's pack
# take <object>
def do_command_take(code)
item = code.shift
puts "> take #{item}"
if @dungeon.current_room.has?(item)
@dungeon.pack.add(item)
@dungeon.current_room.remove(item)
puts "You pick up the #{item}."
else
puts "Cannot find #{item}."
end
end
# Executes the 'go' command which takes the player to anothe room
# go <direction>
def do_command_go(code)
direction = code.shift
puts "> go #{direction}"
if @dungeon.current_room.open?(direction.to_sym)
destination = @dungeon.current_room.exits[direction.to_sym]
@dungeon.current_room = @dungeon.rooms[destination]
run(@dungeon.current_room.entry)
else
puts "You bump your nose on the wall."
end
end
end
# returns an array of words read from stdin
# type: none -> void
def get_input()
input = []
while line = gets
input << line
end
input.each { |line| line.chomp! }
#puts ">>>>>>>>"; input.each { |l| puts l }; puts ">>>>>>>>"
# remove comments
input.delete_if { |line| line =~ /^\s*#/ }
result = input.join(' ').split
#puts ">>>>>>>>"; puts result.join(' '); puts "<<<<<<<<<"
return result
end
# input: array of lines; if empty, reads lines from stdin
# Read input, break it into two components, set up the world,
# parse the story, and run the story.
def main(input = "")
if input.empty?
input = get_input
else
input = input.join(' ').split
end
story = Story.new
story.do_story(input)
end
if __FILE__ == $0
if false # debugging
main(['Minimal { } do: status'])
main(['Single { add crate } do: take crate'])
main(['Single { print ( Hello, world! ) } do: status'])
main(['Single { add crate open north to Single',
' if crate in pack then print ( found ) end }',
' do: take crate go north'])
#main(['Multiple { add crate open north to Single',
# ' if womp in pack then print ( womp ) end }',
# ' do: take womp go north status inventory'])
# expected output:
# > status
# Status for room Minimal:
# The room is empty.
# No exits.
# The crate is on the ground.
# > take crate
# You pick up the crate.
# hello, world!
# > status
# Status for room Single:
# The room is empty.
# No exits.
# The crate is on the ground.
# A door to the north opens.
# > take crate
# You pick up the crate.
# > go north
# found
else
main
end
end
#!/usr/local/bin/ruby
#############################################################################
#
# ifi.rb: interactive fiction intepreter
#
# @author: Mark Feltner
#
# ALL EXTENSIONS WORKING EXCEPT CONJUNCT AND DISJUNCT (although they are
# very, very close)
#############################################################################
require 'dungeon'
# print an error message and quit - useful for reporting parse erros
# rest_of_code is an array of strings and helps in knowing what
# part of the input has not been parsed yet. Note that you do not
# have to call this function - reporting errors can be helpful
# but is not required for the assignment.
def report_error(message, rest_of_code = [])
abort "Error: #{message}\n -- remaining code: #{rest_of_code.join(' ')}"
end
# class to hold the results of parsing the input file
class Story
def initialize
@dungeon = Dungeon.new
end
# Divides input code into two segments. The first is room definitions
# and instructions, the second is the command list.
def split_code(code)
index = code.index("do:")
return code.slice(0..index-1), code.slice(index+1..code.length)
end
# Parses the room definitions list and executes the commands of the story
# <story> -> <room_list> do: <command_list>
def do_story(code)
room_list, command_list = split_code(code)
load_room_list(room_list)
do_command_list(command_list)
rescue Death
end
protected
# run the instructions for a room.
# This needs to create a copy of the code so that any changes
# to the list (such as removing items that have been parsed)
# will not result in permanent changes to the code for the room.
def run(code)
# create copy of the room
copy = code.clone
# execute the code
do_instruction_list(copy)
end
######################
# ROOM LIST
######################
# Iterate over the list of room defintions until the end
# <room_list> -> <room_declaration> { <room_declaration> }
def load_room_list(code)
while !code.empty? and code.first != "do:"
parse_room_declaration(code)
end
end
# Parse a room declaration
# <room_declaration> -> <room_name> '{' <instruction_list> '}'
def parse_room_declaration(code)
room_name = code.shift
if code.first == '{'
room = Room.new(room_name, code[1...code.index("}")])
@dungeon.add(room)
end
end
# Iterate over the list of instructions
# Execute is recursively passed down to each function and controls
# whether or not that command is executed rather than just syntactically
# parsed.
# <instruction_list> -> { <instruction> }
def do_instruction_list(code, execute = true)
while !code.empty? and code.first != '}'
do_instruction(code, execute)
end
end
# Handles a single instruction
# <instruction> -> add <object>
# -> open <direction> to <room_name>
# -> print '(' <message_text> ')
# -> if <test> then <instruction_list>
# [ else <instruction_list> ] end
# ->
def do_instruction(code, execute)
instruction = code.shift
if instruction == "add"
do_instruction_add(code, execute)
elsif instruction == "open"
do_instruction_open(code, execute)
elsif instruction == "print"
do_instruction_print(code, execute)
elsif instruction == "if"
do_instruction_if(code, execute)
elsif instruction == "look"
do_instruction_look(code, execute)
elsif instruction == "move"
do_instruction_move(code, execute)
elsif instruction == "destroy"
do_instruction_destroy(code, execute)
elsif instruction == "die"
do_instruction_die(code, execute)
end
end
def do_instruction_look(code, execute)
if execute
@dungeon.current_room.describe_objects
end
end
def do_instruction_move(code, execute)
object = code.shift
location = code.shift(2)[1]
object_location = @dungeon.location_of(object)
if execute
object_location.remove(object)
@dungeon.rooms[location].add(object)
end
end
def do_instruction_destroy(code, execute)
item = code.shift
if execute
@dungeon.remove(item)
end
end
def do_instruction_die(code, execute)
if execute
puts "You die."
raise Death.new
end
end
# Add an object to the room
# add <object>
def do_instruction_add(code, execute)
object = code.shift
if execute
if @dungeon.location_of(object) == nil
@dungeon.current_room.add(object)
puts "The #{object} is on the ground."
end
end
end
# Open a door to another room
# open <direction> to <room_name>
def do_instruction_open(code, execute)
direction = code.shift.to_sym
room_name = code.shift(2)[1]
if execute
if !@dungeon.current_room.open?(direction)
@dungeon.current_room.open(direction, room_name)
puts "A door to the #{direction} opens."
end
end
end
# Print something
# print '(' <message_text> ')'
def do_instruction_print(code, execute)
message_text = ""
code.shift
while code.first != ')'
message_text << code.shift
message_text << " "
end
if execute
puts message_text.chomp " "
end
code.shift
end
# Evaluates a test condition for the 'if' command and determines whether
# the following <instruction_list> should be executed rather than just
# syntactically parsed.
# if <test> then <instruction_list> [ else <instruction_list> ] end
def do_instruction_if(code, execute)
if execute
condition = do_instruction_test(code)
#condition = test_condition(code)
if condition
do_instruction_then(code, true, false)
else
do_instruction_then(code, false, true)
end
else
condition = do_instruction_test(code)
#condition = test_condition(code)
if condition
do_instruction_then(code, false, false)
else
do_instruction_then(code, false, false)
end
end
end
# Parses and potentially executes the <instruction_list> when a 'then'
# has been found.
def do_instruction_then(code, execute_then, execute_else)
code.shift # shift over 'then'
while !code.empty?
if code.first == "else"
do_instruction_else(code, execute_else)
elsif code.first == "end"
code.shift
return
else
do_instruction(code, execute_then)
end
end
end
# Parses and potentially executes the <instruction_list> when an 'else'
# has been found.
def do_instruction_else(code, execute)
while !code.empty? and code.first != "end"
do_instruction(code, execute)
end
end
# Evaluate a conditional test
def do_instruction_test(code)
condition = do_disjunct(code)
while code.first == "or"
code.shift
if condition
do_disjunct(code)
else
condition = do_disjunct(code)
end
end
return condition
end
# Evaluate a disjunct condition
def do_disjunct(code)
condition = do_conjunct(code)
while code.first == "and"
code.shift
if condition
condition = do_conjunct(code)
else
do_conjunct(code)
end
end
return condition
end
# Evaluate a conjunct condition
def do_conjunct(code)
if code.first == "("
while !code.empty? and !code.first == ')'
code.shift
do_instruction_test(code)
code.shift
end
else
if code.first == "not"
code.shift
result = !do_test(code)
#puts result
return result
else
result = do_test(code)
#puts result
return result
end
end
end
# Parses and runs one of the conditonal tests
def do_test(code)
object = code.first
code.shift
if code.first == "in"
code.shift
if code.first == "pack"
code.shift
return test_in_pack(object)
elsif code.first == "room"
code.shift
return test_in_room(object)
end
elsif code.first == "exists"
code.shift
return test_exists(object)
end
end
# True if the object exists in the dungeon or the player's pack
def test_exists(object)
#puts ">> EXIST? #{object}"
return !@dungeon.location_of(object).nil?
end
# True if the object exists in the current player's pack
def test_in_pack(object)
#puts ">> PACK? #{object}"
return @dungeon.pack.has?(object)
end
# True if the object is in the current room
def test_in_room(object)
#puts ">> ROOM? #{object}"
return @dungeon.current_room.has?(object)
end
######################
# COMMAND LIST
######################
# Descends into each command found in the <command_list>
# <command_list> -> { <command> }
def do_command_list(code)
run(@dungeon.current_room.entry)
while !code.empty?
do_command(code)
end
end
# Handles a single command
def do_command(code)
command = code.shift
if command == "status"
do_command_status(code)
elsif command == "take"
do_command_take(code)
run(@dungeon.current_room.action)
elsif command == "go"
do_command_go(code)
elsif command == "inventory"
do_command_inventory(code)
elsif command == "look"
do_command_look(code)
elsif command == "throw"
do_command_throw(code)
run(@dungeon.current_room.action)
elsif command == "drop"
do_command_drop(code)
run(@dungeon.current_room.action)
end
end
def do_command_drop(code)
object = code.shift
puts "> drop #{object}"
if @dungeon.pack.has?(object)
@dungeon.pack.remove(object)
@dungeon.current_room.add(object)
puts "You drop the #{object}."
else
puts "You do not have the #{object}."
end
end
def do_command_look(code)
puts "> look"
@dungeon.current_room.describe_objects
end
def do_command_throw(code)
obj_to_throw = code.shift
obj_to_hit = code.shift(2)[1]
puts "> throw #{obj_to_throw} at #{obj_to_hit}"
if @dungeon.pack.has?(obj_to_throw)
@dungeon.pack.remove(obj_to_throw)
if @dungeon.current_room.has?(obj_to_hit)
@dungeon.current_room.add(obj_to_throw)
puts "You hit the #{obj_to_hit}."
else
puts "Cannot hit #{obj_to_hit}."
end
else
"Cannot throw #{obj_to_throw}."
end
end
# Executes the 'status' command to show the current room's stuff and doors
def do_command_status(code)
puts "> status"
@dungeon.current_room.show_status
end
# Executes the 'inventory' command to show the player's current pack
def do_command_inventory(code)
puts "> inventory"
@dungeon.pack.show_status
end
# Executes the 'take' command which puts an object from the room to the
# player's pack
# take <object>
def do_command_take(code)
item = code.shift
puts "> take #{item}"
if @dungeon.current_room.has?(item)
@dungeon.pack.add(item)
@dungeon.current_room.remove(item)
puts "You pick up the #{item}."
else
puts "Cannot find #{item}."
end
end
# Executes the 'go' command which takes the player to anothe room
# go <direction>
def do_command_go(code)
direction = code.shift
puts "> go #{direction}"
if @dungeon.current_room.open?(direction.to_sym)
destination = @dungeon.current_room.exits[direction.to_sym]
@dungeon.current_room = @dungeon.rooms[destination]
run(@dungeon.current_room.entry)
else
puts "You bump your nose on the wall."
end
end
end
# returns an array of words read from stdin
# type: none -> void
def get_input()
input = []
while line = gets
input << line
end
input.each { |line| line.chomp! }
#puts ">>>>>>>>"; input.each { |l| puts l }; puts ">>>>>>>>"
# remove comments
input.delete_if { |line| line =~ /^\s*#/ }
result = input.join(' ').split
#puts ">>>>>>>>"; puts result.join(' '); puts "<<<<<<<<<"
return result
end
# input: array of lines; if empty, reads lines from stdin
# Read input, break it into two components, set up the world,
# parse the story, and run the story.
def main(input = "")
if input.empty?
input = get_input
else
input = input.join(' ').split
end
story = Story.new
story.do_story(input)
end
if __FILE__ == $0
if false # debugging
main(['Minimal { } do: status'])
main(['Single { add crate } do: take crate'])
main(['Single { print ( Hello, world! ) } do: status'])
main(['Single { add crate open north to Single',
' if crate in pack then print ( found ) end }',
' do: take crate go north'])
#main(['Multiple { add crate open north to Single',
# ' if womp in pack then print ( womp ) end }',
# ' do: take womp go north status inventory'])
# expected output:
# > status
# Status for room Minimal:
# The room is empty.
# No exits.
# The crate is on the ground.
# > take crate
# You pick up the crate.
# hello, world!
# > status
# Status for room Single:
# The room is empty.
# No exits.
# The crate is on the ground.
# A door to the north opens.
# > take crate
# You pick up the crate.
# > go north
# found
else
main
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment