-
-
Save eregon/56114e98b2109df8865a to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Module | |
def simple_name | |
name.split('::').last | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class String | |
def end_with!(suffix) | |
end_with?(suffix) ? self : self << suffix | |
end | |
def unindent | |
sub(/\A#{InteractiveFiction::Parser::INDENT}/, '') | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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