You are likely to be eaten by a bot: Behind the scenes with Zork.
January 11, 2017Last week, we introduced a bot that allows you to play the classic text adventure game Zork in Cisco Spark rooms or over SMS through Tropo.
Here’s how the bot works (links to the source code repo are at the end of this post).
First, a little history. Back in the 1980s, Infocom created a virtual machine called a Z-machine that Zork would run on. Writing new games was a matter of creating a game file that the virtual machine could read. This made porting the game to various computer platforms simpler, as they’d only have to port the virtual machine, not the entire library of games.
Once Infocom closed down and had their assets absorbed into Activision, game enthusiasts reverse engineered the Z-machine internals by studying the game files and ultimately created replacement Z-machines. One of these, Frotz, is the Z-machine driving this bot. Eventually, Infocom’s owner made Zork 1-3 available for free download, and the Zork 1 game file is installed in the bot.
Frotz runs as a Linux command-line process, and has a mode where you can pipe game commands on stdin and get the results on stdout. The zmachine-api project, created by some engineers at OpenDNS, uses node.js to wrap a REST API around this project and manages starting and stopping the Frotz processes as needed. The Spark bot uses this API.
The Spark bot relies on two Webhooks. One on message creation, is set up without a roomId filter, so all messages that @mention the bot or are in a direct conversation with the bot are delivered to this webhook. The second is a memberships created webhook, also without any filters, so that the bot is notified any time it is added to a Spark room.
When the bot is notified by the Memberships webhook that it’s been added to a room, it immediately posts to the room, introducing itself. It explains what it is, and reminds room participants that they need to @mention the bot if they want to play. This small on boarding mechanism can help room participants understand why a bot was just added to a room and what it can do. This exact on boarding method isn’t appropriate for all bots, but when you are creating a bot, you should think about what a bot should do when it first becomes alive in a room.
Each Spark room is a separate instance of the game. Move around the Zork world in one room, and then go to a different room, and you’ll find your Zork game there is still in the same place it was when you left it.
Another bit of Membership webhook magic is used when the bot is added to a room that already has a game running. If you started playing, remove the bot, and then add it back in again, the game should pick up right where you left off. But the bot should still introduce itself, and also remind the players where they are in the game. Zork has a command called “look” that causes it to tell you what your current surroundings are. So when the bot is added to a room and sees there’s already a game for that room ID, it issues the Look command and shows the players what is going on.
The messages Webhook is how the bot gets the commands from players in the game. When the bot is mentioned, or when someone speaks to it in a one-on-one room, the bot takes the exact input from the player and sends it to Zork. The Webhook triggers the bot, the bot uses the message ID from the webhook to fetch the message content, and then that message content is sent to the game.
One thing that isn’t immediately obvious when creating a Spark bot is how to strip the bot’s name from messages when it’s mentioned. For many bots, this can be as simple as a string match on the bot name and strip that string out. But because this bot has a two word name (“Text Adventure”), the mechanics of Spark mentions means that other people in the room could affect how the mention appears in a message. Spark puts the person’s first name in the message when you mention them, unless there are several people in the room with the same first name. Then to avoid ambiguity, Spark puts the full name in. For a bot like Text Adventure, the presence of another bot in the room called “Text Mom” could mean that sometimes the @mentioned name is “Text” and sometimes it’s “Text Adventure”.
To handle this issue, when the bot server first starts, the bot gets /people/me to find out who it is. It then stores its own personID and uses that to remove mentions by running a regular expression on the HTML markup that wraps a mention.
if (message.html) {
// strip the mention & HTML from the message
var pattern = new RegExp('\]\*data-object-id="' + sparkbotself + '"\[^>\]\*>\[^');
action = message.html.replace(pattern,'');
action = action.replace(/\]*>/g,'');
} else {
action = message.text;
}
Another thing the bot needs to do when processing the message webhook is ignore its own messages. A bot’s message to a room will trigger the webhook from Spark, and you don’t want the bot getting caught in a loop where it is answering its own message, with each answer triggering a new message. When the message webhook arrives, the bot compares the personId that sent the message to its own personId and ignores it if they match.
To keep different instances of the game separate, zmachine-api uses a “label” to tag a Frotz process and later find the process ID that a command should be sent to. Each label becomes a different instance of the game. The bot uses the Spark room ID as the label, regardless if you’re playing in a direct or group room.
Because we’re starting lots of games, in order to keep from having thousands of idle Frotz processes running, the bot starts a new process for every in-game command, then saves the game and shuts down the process when the command is completed. This also ensures that if the server is restarted, all of the games can pick up where they left off. This has the side effect of the “moves” counter in the game appearing to increment by two on every in-game move, since Zork considers saving the game to be a move.
Here’s a flowchart of the bot’s logic. Spark-specific bits are in green, Tropo-specific operations for SMS are in blue, and interactions with the Zork engine and zmachine-api are in orange.
And finally, the code behind the Zork bot is available for your own use. Grab it from my GitHub repository. It’s all written for Node.js, using the Express framework. Included in the repository is a Dockerfile for spinning up a container with the game, and a docker-compose example that will start up multiple containers with the game and all its dependencies.