class ScottKit::Game

Public Class Methods

new(options) click to toggle source

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), and set and clear (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

compile_to_stdout(filename, fh = nil) click to toggle source

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
decompile(f) click to toggle source
# 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
finished?() click to toggle source
# File lib/scottkit/play.rb, line 77
def finished?
  !@finished.nil?
end
load(str) click to toggle source

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
need_to_look(val = :always) click to toggle source
# File lib/scottkit/play.rb, line 296
def need_to_look(val = :always)
  @need_to_look = val
end
play() click to toggle source

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
prepare_to_play() click to toggle source
# 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
process_turn(line) click to toggle source
# 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
prompt_for_turn() click to toggle source
# 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

autodrop(nindex) click to toggle source
# 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
autoget(nindex) click to toggle source
# 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
decompile_wordgroup(f, list, label) click to toggle source
# 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
entityname(i, caption, list, index, register) click to toggle source
# 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
execute_command(verb, noun) click to toggle source
# 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
findword(word, list) click to toggle source

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
gets() click to toggle source

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
is_dark() click to toggle source
# 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
process_lighting() click to toggle source
# 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
restore(name) click to toggle source
# 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
run_matching_actions(vindex, nindex) click to toggle source
# 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
save(name) click to toggle source
# 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
wizard_command(verb, noun) click to toggle source
# 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