Skip to content

Instantly share code, notes, and snippets.

@aquasync
Created June 6, 2010 05:03
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 aquasync/427312 to your computer and use it in GitHub Desktop.
Save aquasync/427312 to your computer and use it in GitHub Desktop.
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