Play With Lua!

Interactive Fiction (part 2)

without comments

I decided to clean up and refactor the code some from part one, although it still works the same way: we still have behaviors that various objects can become. I’m not going to go through every part of the code, but I will talk about how behaviors are designed, explain the parser, how commands are handled, and how a prop (the lamp) is implemented.

Behaviors

First, here’s how you create a behavior:

Something = behavior()
function Something.methods.whatever(self) ... end

Behaviors are now tables that contain a table of methods (called methods), a table of every object tied to that behavior (called all), and functions called to initialize or tear down objects that are gaining or resigning that behavior (on_become and on_resign). To create a new object:

blah = new({ a=5 }, Something)

This creates an object called blah that has a field “a” equal to 5, and two behaviors: Something and Object. That’s right, Object is a behavior, containing methods like become and resign, and every object we create behaves like an Object (unless they resign the Object behavior). We can resign things like so:

blah:resign(Something)

Now blah behaves like an Object but not like a Something, so we can still call become (because that method is how an Object behaves) but not whatever.

Rulebooks

In order to respond to commands, we will have this idea of “rules”. A rule is simply a function that takes a command and returns either a response (a string to give the user) or nil. Rooms have rules, props have rules, the game has global rules, but they all behave the same way, so, let’s make a behavior for them:

Rulebook = behavior()
 
function Rulebook.on_become(obj)
   obj.rules = obj.rules or {}
end

Every object that behaves like a Rulebook must contain a list of rules, so we’ll add an empty one to it when it becomes a Rulebook. Now, a Rulebook has to be able to handle a command, by letting the rules that apply to it give their response:

function Rulebook.methods.handle(rulebook, command)
   local message = nil
 
   for _, rule in ipairs(rulebook.rules) do
	  message = rule(rulebook, command) or message
   end
 
   return message
end

Note that we keep going even after a rule has responded; a well-designed Rulebook will only have one rule that applies to each command but more that one might need to be informed (think about a rule that increments a timer every turn but never gives a response, say). Later on we may need to let commands return more than just a response (like a flag saying whether any other rules should run) but this is fine for now.

Rooms

Rooms are about the same as before. However, we no longer keep the big global list of rooms, because the Room behavior does that itself, we just need to write a function to look up rooms by name:

function Room.find_by_name(_, name)
   return Room.all:select(function(room) return room.name == name end)[1]
end

And tell the Room behavior to use that for its __index, so we can say something like Room.Kitchen:

setmetatable(Room, {__index = Room.find_by_name})

Since all Rooms are also Rulebooks, we’ll make a quick function to make it easier to create them:

function Room.new(attrs)
   return new(attrs, Room, Rulebook)
end

Props

Now we’re ready to do something that’s not just reorganization of the old code: props. “Prop” will be a behavior, obviously, and I’ve already said that all Props are Rulebooks. All Props also have a name, an article (which defaults to “a”, and is used in constructing messages, like “a boat and some water”), and a container. The container is the thing (either a Room or a Prop) that this Prop is inside, and it will be used later on to find all the props we can see. So, creating the Prop behavior:

Prop = behavior()
Prop.methods.article = "a"
 
function Prop.new(name, container)
   return new({ name = name, container = container }, Rulebook, Prop)
end

Note that we’re a Rulebook first and a Prop second. Since behaviors are applied in order, and the most-recently-applied takes precedence, this enables us to have Prop change how props handle commands. Also, notice that the default article “a” is put in methods. methods holds not just functions but anything that you want each object to be able to override.

Parser

Now, the parser. This is actually pretty simple. We want to take a string, like “give gold coin to merchant”, and turn it into a command. So, we have to decide what kind of grammar we’ll accept. The tricky part is that some things can be multiple words, like “gold coin”, and there are different forms of sentences that are valid, like we could also understand “walk north” or “drop brick”.

There are four parts of speech we’ll try to capture:

  1. Verb: always in the first position, always one word, not optional.
  2. Preposition: can only be one of about 70 words, which we have a list of (like “under” and “to”). Marks the end of the second term, if there is one.
  3. Subject: everything between the verb and the preposition. This is what the verb will be done to.
  4. Object: everything after the preposition. This is more information that the verb/subject needs to run.

There are, of course, as many weird edge cases as you can think of, but what we’ll do is cheat: have rules in the game to rewrite these perverse commands into something that makes sense, like, if the verb is “turn” or “switch”, then the preposition (which is “on” or “off”) gets stored as the object. Anyway, here’s the little bit of code to do the parsing, courtesy of Lua’s string.gmatch:

function Game.methods.parse(game, str)
   str = str:lower()
   local words, term = table.new(), nil
   for word in str:gmatch("%w+") do words:insert(word) end
   local command = {game = game, verb = words:remove(1)}
 
   for i, word in ipairs(words) do
	  if Game.prepositions[word] then
		 command.preposition = word
		 command.subject = term
		 term = nil
	  elseif term then
		 term = term .. " " .. word
	  else
		 term = word
	  end
   end
 
   if command.preposition then
	  command.object = term
   else
	  command.subject = term
   end
 
   return command
end

Game command handling

After much thought, I decided that we needed multiple rulebooks for handling each command. We’ll try each rulebook in a certain order, and that should be sufficient to handle most of the cases of rules conflicting. So, there are five layers:

  1. Game.system_rules: This holds things like “exit” and “save”, that you really don’t want to ever override. It goes first.
  2. Game.current_room: The current room the player is in might have special conditions that should override everything else, like if it’s dark.
  3. Game.global_rules: The game has global rules for handling things like movement, inventory, etc.
  4. subject: The subject of the command gets to determine how it handles commands, if nothing else overrides it.
  5. Game.last_chance: The game also has a set of last chance rules; if the command gets to here then it’s probably an error, so we’ll detect the common cases here and try to give a helpful error message.

It seems a little over-complicated but in practice, most rules will go in the props or the room. The author should never have to touch system or last chance rules, and very rarely global rules.

Props will do their command-handling a little differently: first they’ll try a list of rules, just like normal, but if that doesn’t yield a response they’ll send the command to a method named the same as the verb. This means that most of our code can be written with just Prop.whatever.verbname:

function Prop.methods.handle(prop, command)
   local resp = Rulebook.methods.handle(prop, command)
 
   if resp then return resp
   elseif prop[command.verb] then
	  return prop[command.verb](prop, command)
   else
	  return nil
   end
end

A prop

So now that that’s out of the way, let’s implement a simple prop, the lamp. It will support two verbs, “examine” and “turn”, so you can “turn on lamp” or you can “turn off lamp”, and you can see if it’s on or off with “examine lamp”. First make a prop:

Prop.new("lamp", Room.Bedroom) -- It starts out in the bedroom
Prop.lamp.turned_on = false -- Record whether it's on or off

Now handle turning it on and off:

function Prop.lamp.turn(lamp, command)
   if command.preposition == "on" then
	  lamp.turned_on = true
	  return "You turn the lamp on."
   elseif command.preposition == "off" then
	  lamp.turned_on = false
	  return "You turn the lamp off."
   end
end

And then a function to examine it:

function Prop.lamp.examine(lamp, command)
   if lamp.turned_on then
	  return "The lamp is glowing brightly."
   else
	  return "The lamp is dark."
   end
end

That actually looks really straightforward, right? All the code to handle this is in one place, and it doesn’t need to worry about knowing about any other objects. If we really wanted to, we could make Lamp into a behavior even, just by making it define these two methods. Let’s try it out:

> return game:input("examine lamp")
    "The lamp is dark."
> return game:input("turn lamp on")
    "You turn the lamp on."
> return game:input("examine lamp")
    "The lamp is glowing brightly."

Very simple.

What’s next?

The game engine works but it only really responds to those two commands. All the basic commands like walk, get, say, etc. need to be implemented, as well as concepts like darkness, open / locked doors, and so on. So there’s still a lot of work making an interactive fiction standard library, but the basic scaffolding of parsing and handling commands is there, as well as describing items and locations.

If you want to write up any of that standard library, here’s the code so far. Have fun!

Written by randrews

May 30th, 2011 at 3:39 pm

Posted in Uncategorized