class ScottKit::Game
Public Class Methods
Creates a new game, with no room, items or actions – load must be called to make the game ready for playing, or compile_to_stdout
can be called to generate a new game-file. The options hash affects various aspects of how the game will be loaded, played and compiled. The following symbols are recognised as keys in the options hash:
wizard_mode
-
If specified, then the player can use “wizard commands”, prefixed with a hash, as well as the usual commands: these include
sg
(superget, to take any item whose number is specified),go
(teleport to the room whose number is specified),where
(to find the location of the item whose number is specified), andset
andclear
(to set and clear verbosity flags). restore_file
-
If specified, the name of a saved-game file to restore before starting to play.
read_file
-
If specified, the name of a file of game commands to be run after restoring s saved game (if any) and before starting to read commands from the user.
echo_input
-
If true, then game commands are echoed before being executed. This is useful primarily if input is being redirected from a pipe or a file, so that it's possible to see what the game's responses are in response to. (This is not needed when
:read_file
is used.) random_seed
-
If a number is specified, it is used as the random seed before starting to run the game. This is useful to get random events happening at the same time every time, for example when regression-testing a solution.
bug_tolerant
-
If true, then the game tolerates out-of-range room-numbers as the locations of items, and also compiles such room-named using special names of the form
_ROOMnumber
. (This is not necessary when dealing with well-formed games, but Buckaroo Banzai is not well-formed.) no_wait
-
If true, then the game does not pause when running a
pause
instruction, nor at the end of the game. This is useful to speed up regression tests. show_tokens
-
The compiler shows the tokens it is encountering as it lexically analyses the text of a game source.
show_random
-
Notes when a random occurrence is tested to see whether it fires or not.
show_parse
-
Shows the parsed verb and noun from each game command. (Note that this does not emit information about parsing game source.)
show_conditions
-
Shows each condition that is tested when determining whether to run an action, indicating whether it is true or not.
show_instructions
-
Shows each instruction executed as part of an action.
The show_random
, show_parse
, show_conditions
and show_conditions
flags can be set and cleared on the fly if the game is being played in wizard mode, using the set
and clear
wizard commnds with the arguments r
, p
, c
and i
respectively.
# File lib/scottkit/game.rb, line 104 def initialize(options) @options = options @rooms, @items, @actions, @nouns, @verbs, @messages = [], [], [], [], [], [] end
Public Instance Methods
Compiles the specified game-source file, writing the resulting object file to stdout, whence it should be redirected into a file so that it can be played. Yes, this API is sucky: it would be better if we had a simple compile method that builds the game in memory in a form that can by played, and which can then also be saved as an object file by some other method – but that would have been more work for little gain.
The input file may be specified either as a filename or a filehandle, or both. If both are given, then the filename is used only in reporting to help locate errors. Some value must be given for the filename: an empty string is OK.
(In case you're wondering, the main reason this has to be an instance method of the Game
class rather than a standalone function is that its behaviour is influenced by the game's options.)
# File lib/scottkit/game.rb, line 285 def compile_to_stdout(filename, fh = nil) compiler = ScottKit::Game::Compiler.new(self, filename, fh) compiler.compile_to_stdout end
# File lib/scottkit/decompile.rb, line 14 def decompile(f) f << "# #{@rooms.size} rooms, " f << "#{@items.size} items, " f << "#{@actions.size} actions\n" f << "# #{@messages.size} messages, " f << "#{defined?(@ntreasures) ? @ntreasures : "UNDEFINED"} treasures, " f << "#{@verbs.size} verbs/nouns\n" f.puts "ident #{@id}" if defined? @id f.puts "version #{@version}" if defined? @version f.puts "wordlen #{@wordlen}" if defined? @wordlen f.puts "maxload #{@maxload}" if defined? @maxload f.puts "lighttime #{@lamptime}" if defined? @lamptime f.puts "unknown1 #{@unknown1}" if defined? @unknown1 f.puts "unknown2 #{@unknown2}" if defined? @unknown2 f.puts "start #{quote roomname @startloc}" if defined? @startloc ### Do NOT change the nested if's to a single &&ed one: for # reasons that I do not at all understand, doing so results in # the protected statement being executed when @treasury is 0 if defined? @treasury if @treasury != 0 f.puts "treasury #{quote roomname @treasury}" end end f.puts decompile_wordgroup(f, @verbs, "verb") decompile_wordgroup(f, @nouns, "noun") @rooms.each.with_index do |room, i| next if i == 0 f.puts "room " << quote(roomname(i)) << " \"#{room.desc}\"" room.exits.each.with_index do |exit, j| if exit != 0 f.puts "\texit #{dirname(j)} #{quote roomname(exit)}" end end f.puts end @items.each.with_index do |item, i| f.puts "item #{quote itemname(i)} \"#{item.desc}\"" f.puts "\tcalled #{quote item.name}" if item.name f.puts case item.startloc when ROOM_CARRIED then "\tcarried" when ROOM_NOWHERE then "\tnowhere" else "\tat #{quote roomname(item.startloc)}" end f.puts end @actions.each { |action| action.decompile(f) } end
# File lib/scottkit/play.rb, line 77 def finished? !@finished.nil? end
Loads the game-file specified by str. Note that this must be the content of the game-file, not its name.
# File lib/scottkit/game.rb, line 121 def load(str) @roombynumber = [ "_ROOM0" ] @roomregister = Hash.new(0) # name-stem -> number of registered instances @itembynumber = [] @itemregister = Hash.new(0) # name-stem -> number of registered instances lexer = Fiber.new do while str != "" do if match = str.match(/^\s*(-?\d+|"(.*?)")\s*/m) dputs(:show_tokens, "token " + (match[2] ? "\"#{match[2]}\"" : match[1])) Fiber.yield match[2] || Integer(match[1]) str = match.post_match else raise "bad token: #{str}" end end end (@unknown1, nitems, nactions, nwords, nrooms, @maxload, @startloc, @ntreasures, @wordlen, @lamptime, nmessages, @treasury) = 12.times.map { lexer.resume } @actions = 0.upto(nactions).map do verbnoun = lexer.resume conds, args = [], [] 5.times do n = lexer.resume cond, value = n%20, n/20 if cond == 0 args << value else conds << Condition.new(self, cond, value) end end instructions = [] 2.times do n = lexer.resume [ n/150, n%150 ].each { |val| instructions << Instruction.new(self, val) if val != 0 } end Action.new(self, verbnoun/150, verbnoun%150, conds, instructions, args) end @verbs, @nouns = [], [] 0.upto(nwords) do @verbs << lexer.resume @nouns << lexer.resume end @rooms = 0.upto(nrooms).map do exits = 6.times.map { lexer.resume } desc = lexer.resume Room.new(desc, exits) end @messages = 0.upto(nmessages).map { lexer.resume } @items = 0.upto(nitems).map do desc, name = lexer.resume, nil if match = desc.match(/^(.*)\/(.*)\/$/) desc, name = match[1], match[2] end startloc = lexer.resume startloc = ROOM_CARRIED if startloc == ROOM_OLDCARRIED Item.new(desc, name, startloc) end 0.upto(nactions) do |i| @actions[i].comment =lexer.resume end @version, @id, @unknown2 = 3.times.map { lexer.resume } raise "extra text in adventure file" if lexer.resume end
# File lib/scottkit/play.rb, line 296 def need_to_look(val = :always) @need_to_look = val end
Returns 1 if the game was won, 0 otherwise
# File lib/scottkit/play.rb, line 4 def play prepare_to_play while !finished? prompt_for_turn if !(line = gets) # End of file -- we're done puts break end process_turn(line) end return @finished end
# File lib/scottkit/play.rb, line 22 def prepare_to_play @finished = nil @items.each { |x| x.loc = x.startloc } @flags = Array.new(NFLAGS) { false } # weird way to set a default @counters = Array.new(NFLAGS) { 0 } @saved_rooms = Array.new(NFLAGS) { 0 } @counter = 0 @saved_room = 0 @loc = defined?(@startloc) ? @startloc : 1 @lampleft = defined?(@lamptime) ? @lamptime : 0 @need_to_look = true; puts "ScottKit, a Scott Adams game toolkit in Ruby." puts "(C) 2010-2017 Mike Taylor <mike@miketaylor.org.uk>" puts "Distributed under the GNU GPL version 2 license," if file = options[:restore_file] restore(file) puts "Restored saved game #{file}" end if seed = options[:random_seed] puts "Setting random seed #{seed}" srand(seed) end @fh = nil if file = options[:read_file] @fh = File.new(file) raise "#$0: can't read input file '#{file}': #$!" if !@fh end actually_look end
# File lib/scottkit/play.rb, line 65 def process_turn(line) words = line.chomp.split if words.length == 0 puts "I don't understand your command." return end execute_command(words[0], words[1]) process_lighting end
# File lib/scottkit/play.rb, line 57 def prompt_for_turn run_matching_actions(0, 0) actually_look if @need_to_look print "Tell me what to do ? " end
Private Instance Methods
# File lib/scottkit/play.rb, line 285 def autodrop(nindex) return puts "What ?" if nindex == 0 noun = @nouns[nindex].upcase[0, @wordlen] if !(item = @items.find { |x| x.name == noun && x.loc == ROOM_CARRIED }) puts "It's beyond my power to do that." else item.loc = @loc puts "O.K." end end
# File lib/scottkit/play.rb, line 272 def autoget(nindex) return puts "What ?" if nindex == 0 noun = @nouns[nindex].upcase[0, @wordlen] if !(item = @items.find { |x| x.name == noun && x.loc == @loc }) puts "It's beyond my power to do that." elsif ncarried == @maxload puts "I've too much to carry!" else item.loc = ROOM_CARRIED puts "O.K." end end
# File lib/scottkit/decompile.rb, line 66 def decompile_wordgroup(f, list, label) canonical = nil synonyms = [] printed = false list.each.with_index do |word, i| if (word =~ /^\*/) synonyms << word.sub(/^\*/, "") end if (word !~ /^\*/ || i == list.size-1) if synonyms.size > 0 f.print "#{label}group #{quote canonical} " f.puts synonyms.map { |token| quote token }.join(" ") printed = true end canonical = word synonyms = [] end end f.puts if printed end
# File lib/scottkit/game.rb, line 206 def entityname(i, caption, list, index, register) if i < 0 || i > list.size-1 return "_#{caption.upcase}#{i}" if options[:bug_tolerant] raise "#{caption} ##{i} out of range 0..#{list.size-1}" end if name = index[i] return name end stem = list[i].desc stem = "VOID" if stem =~ /^\s*$/ stem = stem.split.last.sub(/[^a-z]*$/i, "").sub(/.*?([a-z]+)$/i, '\1') count = register[stem] register[stem] += 1 index[i] = count == 0 ? stem : "#{stem}#{count}" end
# File lib/scottkit/play.rb, line 130 def execute_command(verb, noun) if (options[:wizard_mode] && verb) if wizard_command(verb, noun) return end end if verb.upcase == "#LOAD" restore(noun) return end verb = "inventory" if verb == "i" verb = "look" if verb == "l" @noun = noun vindex = findword(verb, verbs) nindex = findword(noun, nouns) if !vindex && !noun if tmp = findword(verb, nouns) || findword(verb, %w{XXX n s e w u d}) vindex, nindex = VERB_GO, tmp end end if !vindex || !nindex puts "You use word(s) I don't know!" return end dputs :show_parse, "vindex=#{vindex}, nindex=#{nindex}" case run_matching_actions(vindex, nindex) when :success then return when :failconds then recognised_command = true when :nomatch then recognised_command = false end # Automatic GO 1.upto 6 do |i| if vindex == VERB_GO && nindex == i puts "Dangerous to move in the dark!" if is_dark newloc = @rooms[@loc].exits[i-1] if newloc != 0 @loc = newloc need_to_look elsif is_dark puts "I fell down and broke my neck." finish(0) else puts "I can't go in that direction." end return end end # Automatic GET/DROP if (vindex == VERB_GET) return autoget(nindex) elsif (vindex == VERB_DROP) return autodrop(nindex) end if (recognised_command) puts "I can't do that yet." else puts "I don't understand your command." end end
Returns index of word in list, or nil if not in vocab Word may be undefined, in which case 0 is returned
# File lib/scottkit/play.rb, line 113 def findword(word, list) return 0 if !word word = (word || "").upcase[0, @wordlen] list.each.with_index do |junk, index| target = list[index].upcase if word == target[0, @wordlen] return index elsif target[0] == "*" && word == target[1, @wordlen+1] while list[index][0] == "*" index -= 1 end return index end end return nil end
Get a line from @fh if defined, otherwise $stdin
# File lib/scottkit/play.rb, line 99 def gets line = nil if (@fh) line = @fh.gets @fh = nil if !line end line = $stdin.gets if !line return nil if !line puts line if @fh || options[:echo_input] line end
# File lib/scottkit/play.rb, line 364 def is_dark return @flags[15] if @items.size <= ITEM_LAMP loc = @items[ITEM_LAMP].loc #puts "dark_flag=#{@flags[15]}, lamp(#{ITEM_LAMP}) at #{loc}" @flags[15] && loc != ROOM_CARRIED && loc != @loc end
# File lib/scottkit/play.rb, line 83 def process_lighting if items.size > ITEM_LAMP && items[ITEM_LAMP].loc != ROOM_NOWHERE && @lampleft > 0 @lampleft -= 1 if @lampleft == 0 puts "Your light has run out" @flags[FLAG_LAMPDEAD] = true if is_dark need_to_look end elsif @lampleft < 25 && @lampleft % 5 == 0 puts("Your light is growing dim."); end end end
# File lib/scottkit/game.rb, line 245 def restore(name) f = File.new(name) or raise "#$0: can't restore game from #{name}: #$!" 0.upto(NFLAGS-1) do |i| @counters[i], @saved_rooms[i] = f.gets.chomp.split.map(&:to_i) end # The variable _ in the next line is the unused one that holds # the redundant dark-flag from the save file. Some versions of # Ruby emit an irritating warning for this if the variable name # is anything else. tmp, _, @loc, @counter, @saved_room, @lampleft = f.gets.chomp.split.map(&:to_i) 0.upto(NFLAGS-1) do |i| @flags[i] = (tmp & 1 << i) != 0 end @items.each { |item| item.loc = f.gets.to_i } end
# File lib/scottkit/play.rb, line 196 def run_matching_actions(vindex, nindex) recognised_command = false @actions.each_index do |i| action = @actions[i] if vindex == action.verb && (vindex == 0 || (nindex == action.noun || action.noun == 0)) recognised_command = true case action.execute(vindex == 0) when :failconds # Do nothing when :success return :success if vindex != 0 when :continue while true action = @actions[i += 1] break if !action || action.verb != 0 || action.noun != 0 action.execute(false) end return :success if vindex != 0 end end end return recognised_command ? :failconds : :nomatch end
# File lib/scottkit/game.rb, line 227 def save(name) f = File.new(name, "w") or raise "#$0: can't save game to #{name}: #$!" f.print(0.upto(NFLAGS-1).map { |i| String(@counters[i]) + " " + String(@saved_rooms[i]) + "\n" }.join) f.print(0.upto(NFLAGS-1).reduce(0) { |acc, i| acc | (@flags[i] ? 1 : 0) << i }) f.print " ", dark_flag ? 1 : 0 f.print " ", @loc f.print " ", @counter f.print " ", @saved_room f.print " ", @lampleft, "\n" f.print @items.map { |item| "#{item.loc}\n" }.join f.close puts "Saved to #{name}" end
# File lib/scottkit/play.rb, line 221 def wizard_command(verb, noun) optnames = { "c" => :show_conditions, "i" => :show_instructions, "r" => :show_random, "p" => :show_parse, } if verb.upcase == "#SG" # superget i = Integer(noun) if (i < 0 || i > @items.count) puts "#{i} out of range 0..#{@items.count}" else @items[i].loc = ROOM_CARRIED puts "Supergot #{@items[i].desc}" end elsif verb.upcase == "#GO" # teleport i = Integer(noun) if (i < 0 || i > @rooms.count) puts "#{i} out of range 0..#{@rooms.count}" else @loc = i need_to_look end elsif verb.upcase == "#WHERE" # find an item i = Integer(noun) if (i < 0 || i > @items.count) puts "#{i} out of range 0..#{@items.count}" else item = @items[i] loc = item.loc puts "#Item #{i} (#{item.desc}) at room #{loc} (#{rooms[loc].desc})" end elsif verb.upcase == "#SET" if (sym = optnames[noun]) @options[sym] = true else puts "Option '#{noun}' unknown" end elsif verb.upcase == "#CLEAR" if (sym = optnames[noun]) @options[sym] = false else puts "Option '#{noun}' unknown" end else return false end true end