Interactive Fiction (part 1)
Like it or hate it, and there are plenty of reasons to do both, object-oriented programming is an idea that you can’t escape. Practically every language and every library uses it. Every CS curriculum teaches it. Every learn-to-program tutorial explains it. And so, people have some funny ideas about what it exactly is. If your first language supports it (like Java or Ruby, say) then it becomes very hard to distinguish between object-oriented programming and how your language does object-oriented programming.
Unless you’re using Lua. Because OO in Lua is whatever you write for it. The language doesn’t offer any of what a Java programmer would consider essential features, like classes, instance variables, even types. What it offers is a way to make those things. So that’s what I’m going to do over the next couple posts, in the process of writing a miniature interactive fiction engine. At the end it’ll look a little bit like this except that I’m going to make a bunch of compromises that Zarf never would, because he’s smarter than I am and can foresee how it’ll mess everything up. Also because he intends to use his for longer than a blog post.
Game design
First, let’s define what we’re going to do. We want to have a system that simulates a world, which interacts with the user solely through text. We’ll give it commands like “get lamp” or “walk north” or “put chalice on altar”, and it will change the game state in some way and respond with a message. First, let’s talk about how we’re going to represent the game world.
As far as nouns go, there are two kinds of things in our game: there are rooms, which are the locations the player can be in (“you are in the kitchen”), and there are props, which are the things that are in the rooms (“there is a knife and a pan of brownies here”). Props can also be other places too, like in the player’s inventory or inside other props (“the chest contains a sword and a blue cloak”). So the world is a directed graph of rooms, and each room is the root of a tree of objects. In addition to this, there can be other “global” factors at play, like if it’s dark then all room descriptions are “it’s dark, you may be eaten by a Grue”. We’ll have a list of global rules that aren’t part of the world graph, but get first dibs on any command.
Time for a big helpful picture or two. Here’s how the rooms will be laid out:
And here’s the layout of items within a single room:
This first post, we’re going to lay the groundwork, and talk about how to implement rooms. First though, since this is ostensibly about object-oriented programming, it’s about time we mentioned objects.
Basic objects
An object, in the Java / C++ sense, is an instance of a class. And a class is a user-defined datatype (where you, the programmer, are the user; classes are typically defined in code), just like “string” or “array”. This method of OO is almost totally dominant, to the point where languages like Javascript are thought of as not supporting OO because they use a different model. Really, though, an object is anything that fits these criteria:
- Contains some related state.
- Contains the code to manage that state
Se let’s say that a room has a name (“Kitchen”), a description (“A small apartment kitchen. Nothing fancy but good enough for making brownies and frozen pizza”), and a set of exits (south to the bedroom, west to the living room). That’s its state. We can represent this in Lua with a table:
kitchen = { name = "Kitchen", description = "A small apartment kitchen. Nothing fancy . . .", exits = { w = "LivingRoom" } }
Now we need the operations to manage the data. We could just make these be functions stored in the table, but we have to assume that we’ll have lots of different rooms sharing the same functions (after all, what does print_description
do differently for the bedroom than the kitchen), so we should have a way to share these across different objects. Lua provides a facility called metatables, which control how tables respond to different events, like someone looking for a key that isn’t there. In that case Lua looks in the metatable under __index
, and if that’s a function it calls it (with the table and the missing key). As a shortcut, if __index
is a table it just looks in that table for the missing key.
So let’s take all the code to manage a room’s state and put it in a table, room_methods:
room_methods = { print_description = function(room) print(room.name .. "\n\n" .. room.description) end -- and so on... }
Now we can set it as the metatable’s __index
table, and call things in it:
setmetatable(kitchen, { __index = room_methods }) kitchen.print_description(kitchen)
There’s even syntactic sugar so that we don’t have to pass in the first argument by hand:
kitchen:print_description()
Note that the metatable isn’t what we look for the functions in, the __index
field in the metatable is. We can also put things in other slots in the metatable, like __tostring
. The tree looks like this:
So that accounts for the state and the functions to handle the state. Notice that nowhere in there is there anything that corresponds to a class, or a type. This is just an object. The only primitives used at all are tables. Since we made this system ourselves in the language, instead of it being provided for us by the language, we can mold it some to fit our problem better.
Refining our objects
I think later on we’ll want to have objects that share the characteristics of multiple different kinds of objects, like a glowing sword has all the “weapon” properties as well as all the “light source” properties. We won’t be able to do that if we just shove all the functions into an __index
table, but if we have an __index
function that can look in multiple tables…
So let’s make a couple generic functions for dealing with things like this, and that’ll be the basis of our object system. Start with a become
function that takes a table of state (an object) and a table of functions (some operations), and adds those operations to that object:
function become(obj, operations) local m = getmetatable(obj) if not m then -- No metatable yet; we'll add one m = {behaviors={}, __index=dispatch} setmetatable(obj, m) end table.insert(m.behaviors, 1, operations) end
The dispatch
function in that metatable will look in all the behaviors
tables in order and return the first matching method it sees:
function dispatch(obj, message_name) local m = getmetatable(obj) if not m then return nil end for _, operations in ipairs(m.behaviors) do if operations[message_name] then return operations[message_name] end end return nil end
So now creating our kitchen will look like this:
Room = {} function Room.print_description(room) print(room.name .. "\n\n" .. room.description) end -- and so on... kitchen = { name = "Kitchen" -- And all the rest . . . } become(kitchen, Room)
Okay, so, now we’re talking. We can now represent a single room, so a table of these is a list of the rooms. Let’s make that table (which I won’t do inline because it’ll be long and repetitive), and then start thinking about how to interact with them.
Game engine
What we’ll do is have a function that takes a table representing a command (it will have a verb, and an optional subject and direct object) and then modify some game state based on that. We can pretty easily represent this as an object called “game”. The game object will have a current room and a list of global “rules”:
Game = {} game = new(Game, { current_room = ROOMS.Bedroom, globals = {} })
(new
is a convenience function that takes a type and a table, calls become
, and returns a new object)
A “global rule” is a function that takes a game and a command, and returns either a string to say to the player (if it knows how to interpret that command) or nil (if this rule doesn’t affect that command). This is how we’ll handle all verbs that aren’t tied to a particular room or object:
game:add_global(function(game, command) if command.verb == "walk" then local new_room = game.current_room:room_in_dir(command.subject) if new_room then game.current_room = new_room return new_room.name .. "\n\n" .. new_room.description else return "There is no exit that way" end end end)
So, in order to handle any command (so far, anyway) we just have to have game:input
call each of the global rules in order until one of them returns non-nil, and that’s the response:
function Game.input(game, command) for _, rule in ipairs(game.globals) do local message = rule(game, command) if message then return message end end return "Sorry, I didn't understand that" end
Now we can start this up, walk around the place, and get responses back:
> return game:input{ verb="walk", subject="n" } "Living Room A well-appointed but cluttered living room" > return game:input{ verb="walk", subject="w" } "There is no exit that way" > return game:input{ verb="walk", subject="e" } "Kitchen A simple kitchen." > return game:input{ verb="walk", subject="up" } "There is no exit that way" > return game:input{ verb="yodel" } "Sorry, I didn't understand that"
Now we’re just one parser and some props away from a system that can make interactive fiction. About two pages of code so far, counting the descriptions of the rooms. Next time, we’ll finish it!