Designing an Adventure Game API
Recently, I started work on a client-server adventure game API, written in node.js on the server side and Vuejs on the client side. And more recently, my interest in the project waned. Here, I’m going to discuss some of what I learned, and what my conclusions are.
What was the basic concept?
At the time, I had taken up a strong interest in the Infocom text adventure games; and when I get interested in a game type I want to make my own. I was really interested in describing interactive worlds solely through text; in addition to my game projects, I occasionally try to write fiction, though nothing too serious.
At the same time, though, the parser felt like an achille’s heel of the whole model. The occasional wordplay puzzle vindicates it, but for the most part, and especially early on, it’s just a struggle to figure out how to interact “properly”. (You can see some of this in my complaining about Mystery House in the Apple ][ article, though that is a graphical adventure game)
So I plotted out a basic concept. The game would be primarily text-based, and be controlled by a limited selection of verbs: LOOK
, TALK
, MOVE
, GET
, USE
, and STATUS
. These verbs wouldn’t be parsed, but instead would use a simple system of icons and modal dialogues.
Why a client/server model?
That’s the first question here to be sure. Adventure games have been written for pretty much anything capable of displaying text; there’s no need for such a fancy model, along with needing now to maintain a server. But I was really interested in doing something online for a few reasons.
- I wanted to learn nodejs and component-based frontend JS design. This goes first because it’s the real reason.
- I’ve written roguelikes before; in that community the concept of deterring “save-scumming” is often brought up. In this case, it could be trivially made impossible.
- Similarly, a local game can be torn apart, its secrets revealed without being ‘solved’. The puzzlebox of an online game can hide its secrets much better.
- I hoped there’d be an easy player-player interaction that would make sense in primarily single-player adventures. Unfortunately, this never really happened.
How is it structured?
Each verb can be thought of a function that acts on a state, and returns an output, and optionally, a modified state. Dialogue segments can have “effects” that change something in the game world; move an NPC, set a flag, etc. (These segments don’t merely need to be triggered by dialogue either, but can be triggered by using an item, looking at something, or others)
The functions are not “pure functions” because the state is modified, rather than being replaced, but I think it comes much closer to a functional model than many of my previous real-time games, that relied on many objects acting on global state.
A major goal in the API was restricting information leaks. When you look in a room, you get a list of API endpoints for all of the possible commands. But I wanted to make sure that these endpoints didn’t give away secrets. For example, a maze could reveal which rooms had been seen already, by using the same room ids. A user who wrote those down could figure out a pattern much quicker than one doing it “properly” and only looking at the labels. So each exit has a unique GUID, even if it goes the same place as another.
Similarly, the GUIDs are shuffled every time the server is rebooted. (If a client is already connected, merely looking at the room again will get the newest state, or refreshing) I considered making them unique per user as well, but decided that was a step too far.
How is that data stored?
The data is stored in MongoDB. Both game-state and immutable facts about the world, though they’re stored separately, with separate schema. From the designer’s perspective, though, the game is a JSON document containing lists of rooms, NPCs, and dialogues, with various links between them. I wrote a simple, but clunky, Electron app (not published online at the moment) that generated the proper documents and provided some sort of GUI editor.
This method has some clear limitations. I’ll get to those in a bit.
Save restrictions are dumb
At first, I wanted to make saving and loading unnecessary. The game would be designed to never allow the player to fall into an unwinnable state, and by nature of the state being kept on a server, your current status would never be lost. You could come back days or years later, and as long as the database was still around, your player would be just where you left them.
But eventually I realized the title, and added /game/save
and /game/load
endpoints that could save and load the game. And that’s the first point where things went wrong; there are really not that many advantages to the client-server model to begin with, and now I had just gotten rid of one I had thought was important.
What else went wrong?
So, the game engine is in a pretty decent state; it needs more conditions and effects, but I always planned to add those in as the game was developed and they were needed; that’s the best way to find out what you actually need.
Unfortunately, I learned there were other things I actually needed too. After setting up a set of limitations I thought I could live with, I realized they were actually much more limiting than I thought. There was no way to have small variations in dialogue, unless I copy-pasted the whole dialogue with the slight change. All lists were permanently unordered. And editing was being done by a clunky Electron app I wrote without the benefit of any modern component-based design. There wasn’t even string interpolation!
But the approach itself was also flawed. Zork and its Infocom successors was written in an actual programming language; making a world dynamic is exactly the benefit of this approach. A database-driven world is by definition (pun intended) more static. This was good enough on the TRS-80, but play Adventureland today and you’ll see its limitations.
Conclusions
In the end the model wasn’t bad, but there was a mismatch with what I wanted to achieve. It was suited to simple item-puzzles, but I wanted to tell stories; simple item-puzzles benefit the most from the parser, being able to use different verbs. My limited interaction demanded a more fleshed-out world, but I had created an engine so minimalist than it didn’t have the capability.
I’m not completely down on amoskeag. I may pick it up again; I may not. I had some ideas for a more puzzle-themed game in the Space Ava universe that might make better use of it; I might also just go through and extend it into something closer to the fiction engine I really wanted to make originally.
In fact, more than anything else, I’d say this was a project undone by its editor and data format. JSON documents are something I’m comfortable with, but both Aspect Star 2 (actually XML rather than JSON) and Aspect Legend had ways to include code as well as just raw data in the game data. In the end programmability is key to a dynamic experience; that more than anything else was silly of me to overlook.
I’m just so reluctant to use eval
…