public
Last active

  • Download Gist
game.rb
Ruby
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
 
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

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.