Skip to content

Instantly share code, notes, and snippets.

@eregon

eregon/README Secret

Created May 17, 2010 19:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eregon/56114e98b2109df8865a to your computer and use it in GitHub Desktop.
Save eregon/56114e98b2109df8865a to your computer and use it in GitHub Desktop.
This is the gist for my solution to the 9th RPCFN: Interactive Fiction
See http://rubylearning.com/blog/2010/04/29/rpcfn-interactive-fiction-9
(There is also a git repository: http://github.com/eregon/rpcfn-interactive-fiction)
A gist can be used as a git repository,
but it doesn't accept sub-directories
So,
play.rb is in bin/
all others are in lib/
Tree:
bin/
play.rb
lib/
abstract_object.rb
action.rb
main.rb
module.rb
object.rb
parser.rb
player.rb
room.rb
string.rb
synonyms.rb
world.rb
module InteractiveFiction
class AbstractObject
attr_reader :name
def initialize(name, description)
@name = name
description.each_pair { |key, value|
instance_variable_set :"@#{key}", value
self.class.send(:attr_reader, key) unless self.respond_to?(key)
}
end
def inspect
"#{self.class.simple_name} #{@name}"
end
alias :to_s :inspect
class << self
def find(search, objects, method)
objects.find { |object|
case criteria = object.send(method)
when Array
criteria.include? search
when String
criteria == search
end
}
end
end
end
end
require_relative "abstract_object"
module InteractiveFiction
class Action < AbstractObject
class << self
def find(action_name, actions)
super(action_name, actions, :commands)
end
end
end
end
if RUBY_VERSION < "1.9.2"
# Backports: http://github.com/marcandre/backports
begin
# Gem load
require 'rubygems'
require 'backports/1.9'
rescue LoadError
puts $!
puts "You need to install backports(http://github.com/marcandre/backports) `gem install backports`"
puts "Notice: this cannot be considered as an external gem to help the challenge, it's just there to fill the gap between ruby versions ;)"
end
end
Dir[File.join(File.dirname(__FILE__), "*.rb")].each { |f|
require f unless f == __FILE__
}
if __FILE__ == $0
puts InteractiveFiction::Parser.new(File.expand_path("../../data/petite_cave.if", __FILE__)).parse
end
class Module
def simple_name
name.split('::').last
end
end
require_relative "abstract_object"
module InteractiveFiction
class Object < AbstractObject
NAME_TOKEN = "$"
def Object.find(name, objects)
objects.find { |o|
o.name == name ||
o.name == "#{NAME_TOKEN}#{name}" ||
o.terms.any? { |term|
term.casecmp(name).zero?
}
}
end
end
end
module InteractiveFiction
class Parser
INDENT = " "*2
NO_INDENT = /\A(?!#{INDENT})/
KEY_VALUE_SEPARATOR = /:(?=\s)/
LIST_SEPARATOR = ", "
ROOM_EXIT_SEPARATOR = " "
GROUP_TITLE = /\A(\w+)(?:\s(.\w+))?:\z/
BEGIN_CODE = /^#{Regexp.escape("{{{")}/
END_CODE = /#{Regexp.escape("}}}")}$/
def initialize(file)
@file = file
end
def parse
IO.read(@file).lines.map(&:chomp).
slice_before(NO_INDENT).reject { |o| o.all?(&:empty?) }.inject([]) { |objects, lines|
lines.shift =~ GROUP_TITLE
type, name = $~.captures # With 1.8, we can't use Regexp named groups
objects << send("parse_#{type.downcase}", name, parse_contents(lines))
}
end
def parse_contents(lines)
lines.map(&:unindent).slice_before(NO_INDENT).each_with_object({}) { |key_value, contents|
key, *value = key_value.map(&:strip).join("\n").split(KEY_VALUE_SEPARATOR)
contents[key] = value.join.lstrip.unindent
}
end
def parse_code(code)
code =~ /#{BEGIN_CODE}(.+)#{END_CODE}/m
$1.gsub(/\bblackboard\b/, 'self.blackboard')
# Very weird bug, the method call fail if we add actions ???
# Else we get: "undefined method `[]' for nil:NilClass (NoMethodError)"
# Apparently instance_eval consider blackboard as a local var (with a nil value) in this case
end
def parse_room_exits(text)
text.lines.slice_before(/\A\w+ to/).each_with_object({}) { |exit, h|
if exit.size > 1 and code = exit.join and code =~ BEGIN_CODE and code =~ END_CODE
# enter to @grate_chamber guarded by:
code =~ /(\w+) to (.+) guarded by\n/
h[$1] = [$2, parse_code($')]
else
dir, to, room = exit.first.split(ROOM_EXIT_SEPARATOR)
h[dir] = room
end
}
end
def parse_room(name, desc)
Room.new name,
:exits => parse_room_exits(desc["Exits"]),
:title => desc["Title"].rstrip.end_with!("."),
:long_description => desc["Description"],
:objects_names => (desc["Objects"] || "").split("\n")
end
def parse_object(name, desc)
terms = desc["Terms"].split(LIST_SEPARATOR)
Object.new name,
:terms => terms,
:small_description => terms.first,
:long_description => desc["Description"]
end
def parse_action(name, desc)
Action.new name,
:commands => desc["Terms"].split(LIST_SEPARATOR),
:code => parse_code(desc["Code"])
end
def parse_synonyms(name, desc)
synonyms = desc.each_pair.with_object({}) { |(full, synonym), synonyms|
synonyms[full] = synonym.split(LIST_SEPARATOR)
}
Synonyms.new name,
:synonyms => synonyms,
:all_synonyms => synonyms.values.reduce(:+)
end
end
end
#!/usr/bin/env ruby
require File.expand_path("../../lib/main", __FILE__)
module InteractiveFiction
class Game
def initialize(story_path, options={})
@input = options.fetch(:input) { $stdin }
@output = options.fetch(:output) { $stdout }
objects = Parser.new(story_path).parse
@world = World.new(objects, @input, @output)
# The tests keep ~182000 objects! (on 390660 total)
# This can be reduced to ~140000 objects by adding "@description = nil"
# at the end of the constructor of every AbstractObject's subclass
# This is, however, slower due to GC
# GC.start
# p ObjectSpace.each_object {}
end
def play!
start!
execute_one_command! until ended?
end
def start!
@world.start!
end
def execute_one_command!
print "> " if __FILE__ == $0
@world.execute_one_command!
end
def ended?
false # The game never ends :)
end
end
end
Game = InteractiveFiction::Game
if $0 == __FILE__
story_path = ARGV[0] || File.expand_path("../../data/petite_cave.if", __FILE__)
unless story_path
warn "Usage: #{$0} STORY_FILE"
exit 1
end
game = Game.new(story_path)
game.play!
end
module InteractiveFiction
class Player
def initialize(world)
@world = world
@inventory = []
@blackboard = {}
end
def << object
if object
@inventory << object
"OK"
end
end
def >> object
@inventory.delete(object)
end
def show_inventory
if @inventory.empty?
"You're not carrying anything"
else
@inventory.map { |object|
object.small_description
}.join("\n")
end
end
# block code methods
attr_accessor :blackboard
def player_in?(room_name)
@world.current_room == Room.find(room_name, @world.rooms)
end
def player_has?(object_name)
@inventory.include? Object.find(object_name, @world.objects)
end
end
end
require_relative "abstract_object"
module InteractiveFiction
class Room < AbstractObject
attr_writer :world
def objects
@objects ||= @objects_names.map { |name| Object.find(name, @world.objects) }
end
def enter
@seen = true
end
def description
(@seen ||= false) ? "You're #{@title}" : look
end
def look
[self, *objects].map(&:long_description).join("\n")
end
def << object
objects << object if object
end
def >> object
objects.delete object
end
def Room.find(room_name, rooms)
super(room_name, rooms, :name)
end
end
end
class String
def end_with!(suffix)
end_with?(suffix) ? self : self << suffix
end
def unindent
sub(/\A#{InteractiveFiction::Parser::INDENT}/, '')
end
end
require_relative "abstract_object"
module InteractiveFiction
class Synonyms < AbstractObject
def get_full_command(synonym)
if @all_synonyms.include? synonym
@synonyms.keys.find { |key| @synonyms[key].include?(synonym) }
else
synonym
end
end
end
end
module InteractiveFiction
class World
INPUT_SEPARATOR = " "
MOVES = %w[north east south west]
QUIT = %w[q quit]
attr_reader :current_room
def initialize(objects, input, output)
@input, @output = input, output
objects.group_by(&:class).each_pair { |klass, value|
name = klass.simple_name.downcase.end_with!("s")
instance_variable_set "@#{name}", value
self.class.send(:attr_reader, name) unless self.respond_to?(name)
}
@player = Player.new(self)
@rooms.each { |room| room.world = self }
change_room @rooms.first
end
def puts(*args)
@output.puts(*args)
end
def start!
puts @current_room.long_description
end
def change_room(room)
@current_room = room
room.enter
end
def enter_room(room)
room = Room.find(room, @rooms)
puts room.description
change_room(room)
end
def execute_one_command!
if input = @input.gets and input.chomp! and !QUIT.include?(input)
input = input.split(INPUT_SEPARATOR)
command, args = input.shift, input
@synonyms.each { |synonym| command = synonym.get_full_command(command) }
input = args.unshift(command).join(INPUT_SEPARATOR)
if dir = @current_room.exits[input]
case dir
when Array # With Proc
room, code = dir
allow, message = @player.instance_eval(code) # [ALLOW, MESSAGE]
enter_room(room) if allow
puts message
when String
enter_room dir
end
elsif MOVES.include? input
puts "There is no way to go in that direction"
elsif action = Action.find(input, @actions)
message, blackboard = @player.instance_eval(action.code) # [MESSAGE, BLACKBOARD]
puts message
@player.blackboard.merge!(blackboard)
else
case input
when "look"
puts @current_room.look
when "inventory"
puts @player.show_inventory
when /^take (.+)$/
take_object($1)
when /^drop (.+)$/
drop_object($1)
# EXTRA COMMANDS
when "dirs" # get available directions
puts @current_room.exits.keys.join(", ")
else
puts "Unknown command #{input}"
end
end
else
exit
end
end
def take_object(name)
puts @player << (@current_room >> Object.find(name, @objects))
end
def drop_object(name)
@current_room << (@player >> Object.find(name, @objects))
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment