Created
June 6, 2010 05:03
-
-
Save aquasync/427312 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
require 'yaml' | |
class Game | |
Room = Struct.new :title, :description, :exits, :objects | |
Object = Struct.new :terms, :description | |
Action = Struct.new :terms, :code | |
attr_reader :blackboard | |
def initialize filename, options={} | |
@input = options.fetch(:input) { $stdin } | |
@output = options.fetch(:output) { $stdout } | |
# game state | |
@actions = {} | |
@actsyns = {} | |
@objects = {} | |
@objsyns = {} | |
@rooms = {} | |
@seen = {} | |
@inventory = [] | |
@blackboard = {} | |
data = File.read filename | |
load data | |
# yaml hash will be unordered | |
@room = @rooms[data[/^Room (@\w+):$/, 1]] | |
# set up action terms as synonyms, then rekey off primary term | |
@actions.values.each { |act| @actsyns[act.terms[0]] = act.terms[1..-1] } | |
@actions = @actions.inject({}) { |h, (k, v)| h.update v.terms[0] => v } | |
# install the default actions | |
%w[enter exit north south east west].each { |dir| @actions[dir] = Action.new [dir], proc { || move dir } } | |
%w[look inventory take drop].each { |name| @actions[name] = Action.new [name], method(name) } | |
@actsyns.each { |k, v| v.each { |syn| @actions[syn] = @actions[k] } } | |
@objects.each { |k, v| v.terms.each { |n| @objsyns[n.downcase] = k } } | |
# longest matches first so that more specific actions wins | |
@inputrx = /^(#{@actions.keys.sort_by { |k| k.length }.reverse.map { |s| Regexp.quote s }.join('|')})(?:\s+(.+))?$/ | |
end | |
def load data | |
# offload parsing to YAML by convert to a valid YAML document. | |
# regexps rely on indentation to avoid restricting contents of code blocks | |
# (eg allow a string containing '}}}' in a code block) | |
yaml = data.gsub(/^ Code:/, "\\0 |-").gsub(/^ (\S+):\n((?: .*\n)+)/) do | |
next " #$1: >\n#$2" if $1 == 'Description' | |
" #$1:\n" + $2.map { |l| l.sub(/^ (?= )/, "\\0 ").sub(/^ (?=\S)/, "\\0- ").sub(/^ \S.* guarded by:$/, "\\0 |-") }.join | |
end | |
doc = YAML.load yaml | |
# could use YAML aliases to build the full object graph, | |
# but for now do it separately | |
doc.each do |key, value| | |
case key | |
when /^(Room) (@\w+)$/ | |
value['Objects'] ||= [] | |
value['Exits'] = value['Exits'].inject({}) do |h, s| | |
s, code = Hash === s ? [s.keys[0].sub(/ guarded by$/, ''), s.values[0]] : [s, nil] | |
code = eval code.sub(/\A\{\{\{\s*\n/, "proc do\nThread.new do\n$SAFE = 4\n").sub(/\n\}\}\}\s*\Z/, "\nend.value\nend") if code | |
k, room = s.match(/^(\w+) to (@\w+)$/)[1..-1] | |
h.update k => [room, code] | |
end | |
when /^(Object) (\$\w+)$/ | |
value['Terms'] = value['Terms'].split(/,\s+/) | |
when /^(Action) (\!\w+)$/ | |
value['Terms'] = value['Terms'].split(/,\s+/) | |
value['Code'] = eval value['Code'].sub(/\A\{\{\{\s*\n/, "proc do || text, updates = Thread.new do\n$SAFE = 4\n").sub(/\n\}\}\}\s*\Z/, "\nend.value\nputs text unless text.empty?\nblackboard.update updates\nend") | |
when 'Synonyms' | |
@actsyns = value.inject({}) { |h, (k, v)| h.update k => v.split(/,\s+/) } | |
next | |
else | |
raise 'invalid construct - %p' % key | |
end | |
type, name = key.split ' ', 2 | |
klass = self.class.const_get type | |
instance_variable_get("@#{type.downcase}s").update name => klass.new(*value.values_at(*klass.members.map { |s| s.capitalize })) | |
end | |
end | |
def player_in? room | |
@rooms[room] == @room | |
end | |
def player_has? obj | |
@inventory.include? obj | |
end | |
def look | |
puts [@room.description, *@room.objects.map { |obj| @objects[obj].description }] | |
@seen[@room] = true | |
end | |
def move dir | |
dest, guard = @room.exits[dir] | |
return puts('There is no way to go in that direction.') unless dest | |
ok, text = guard ? guard.call : [true, ''] | |
puts text unless text.empty? | |
return unless ok | |
@room = @rooms[dest] | |
@seen[@room] ? puts("You're #{@room.title}.") : look | |
end | |
def inventory | |
return puts("You're not carrying anything") if @inventory.empty? | |
puts ['You are currently holding the following:', *@inventory.map { |obj| @objects[obj].terms.first }] | |
end | |
def take obj | |
return puts("There is no `#{obj}' here.") unless found = @room.objects.delete(@objsyns[obj.downcase]) | |
puts 'OK' | |
@inventory << found | |
end | |
def drop obj | |
return puts("You don't have a `#{obj}'.") unless found = @inventory.delete(@objsyns[obj.downcase]) | |
@room.objects << found | |
end | |
def play! | |
start! | |
execute_one_command! until ended? | |
end | |
def execute_one_command! | |
if @output == $stdout | |
@output.print '> ' | |
@output.flush | |
end | |
case s = @input.gets | |
when nil | |
puts | |
exit | |
when /^\s*$/ | |
when @inputrx | |
act = @actions[$1] | |
unless ($2 ? 1 : 0) == act.code.arity | |
puts 'what you sayin?' | |
else | |
act.code.call(*[$2].compact) | |
end | |
else | |
puts 'wtf are you talking about? (%s)' % s | |
end | |
end | |
alias start! look | |
def ended? | |
false | |
end | |
private | |
def puts(*args) | |
@output.puts(*args) | |
end | |
end | |
if $0 == __FILE__ | |
unless story_path = ARGV[0] | |
warn "Usage: #{File.basename $0} STORY_FILE" | |
exit 1 | |
end | |
Game.new(story_path).play! | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment