The birth of text adventures coincided with the birth of 8-bit home computers. Trouble is, early text adventures were developed on mainframes and needed megabytes of memory to run, while home computers had kilobytes — a thousand times less.

Creative people made it work anyway. The first to succeed was Scott Adams, founder of Adventure International. He only made a dozen games and change, but all became cult classics. Better yet, some years later he published the source code to his engine in Byte Magazine. It became the basis for a line of bigger, better authoring systems that continues to this day: look up The Quill, The PAW, DAAD or ThinBasic Adventure Builder.

All are based on the same idea at the core: a database format encoding the map, game objects, and events, interpreted by a little program which acts as game master to the player at the keyboard. Simple in concept but cryptic in practice due to constraints that no longer apply. For one thing, programming languages tend to have associative arrays now, even Basic dialects like BaCon and sdlBasic!

This is important, because all modern game engines follow the same pattern, whether you're making a first-person shooter or epic roleplaying game: an editor ouputs a big pile of data placed in a folder together with an executable (the engine proper) and archived together to make the exported game.

It shouldn't be a mystery how game engines work, and how best to learn if not with the most low-tech kind of game there is?

Now, it's easy enough to code a simple text adventure in your language of choice; in fact, it used to be a common programming exercise. For my first attempt, made all the way back in 2010, I went with Javascript. (Look up Jaiffa if you want to see it.) Python is just as good too, and the Cloak of Darkness site even has an example in Awk!

Wouldn't it be nice however if you could pick Perl or Ruby while I stick to Python, and we could run all the same games regardless? We can, too, and the secret, like I said above, is... data. So instead of rushing to code our text adventure, first let's see how to store it in memory.

I assume you know what text adventures are like: you have rooms connected by exits, populated by items and NPCs. The good news is, all of them have similar properties: a name and description, for one thing.


	name = {}
	desc = {}
	spot = {}
	
	name["cloakroom"] = "Cloakroom"
	desc["cloakroom"] = "This room, once opulent, is now bare."
	
	desc["hero"] = "As good-looking as ever."
	spot["hero"] = "cloakroom"
	
	name["hook"] = "small brass hook"
	spot["hook"] = "cloakroom"
	
	name["cloak"] = "velvet cloak"
	desc["cloak"] = "It's of a black so deep it darkens the room."
	spot["cloak"] = "hook"

Wait, why not make everything in the game a dictionary, or for that matter an object? Because this way we don't have to deal with complex data structures, or advanced programming techniques. No scaring away the beginners! Also, because those could differ a lot between languages: exactly what we want to avoid here.

Looking at the data, notice how most objects only need some properties. The brass hook is nondescript for instance, and rooms aren't contained in anything else since they contain all other game objects. Speaking of which, notice how the data doesn't say what is a room, or an actor: the hero's ID is "hero", wherever they are must be a room, and anything else in there is an item until further notice.

What about exits? The simplest way would be to do something like this:


	link = {}
	
	name["foyer"] = "Foyer of the Opera House"
	desc["foyer"] = "A cavern of marble, crystal and gold."

	link["west","foyer"] = "cloakroom"
	link["east","cloakroom"] = "foyer"

Neat trick, isn't it? Trouble is, popular languages like Lua or Javascript can't do that, so you need another solution. Say, create a link dictionary pre-filled with all possible directions:


	link = {
		"north": {},
		"south": {},
		"east": {},
		"west": {},
		"up": {},
		"down": {},
		"in": {},
		"out": {}
	}
	
	link["west"]["foyer"] = "cloakroom"
	link["east"]["cloakroom"] = "foyer"

So much for keeping it simple, but oh well. And either way, we can't use exit because that's a keyword or else a built-in function in many languages.

But naming is a detail. What matters is, now we have enough data to demonstrate a working engine:


	compass = ["north", "south", "east", "west", "up", "down", "in", "out"]

	def look():
		here = spot["hero"]

		print("---")
		print(name[here])
		print("---")
		print(desc[here])
		
		print("\nObvious exits: ", end='')
		for i in compass:
			if (i,here) in link:
				print(i, end='')
				
		print("\n\nYou also see:")
		for i in name:
			if spot.get(i) == here:
				print(" -", name[i])

	def go(noun):
		here = spot["hero"]
		if (noun,here) in link:
			spot["hero"] = link[noun,here]
			look()
		else:
			print("You can't go that way.")

	look()

	while True:
		command = input("\n> ").lower().split()
		verb = command.pop(0) if len(command) > 0 else ""
		noun = command.pop(0) if len(command) > 0 else ""
		if verb == "go":
			go(noun)
		elif verb in compass:
			go(verb)
		else:
			print("I don't know this word:", verb)

Yes, that's Python 3. Sorry about that, I had to pick one language. And yes, the code barely works. The point is to show you how a game engine can be driven by data.

Which was easy so far. Not so much when it describes changing conditions.

How so? Next you'll want a "get" verb. How do you tell the engine what the player can and can't pick up? Perhaps like this:


	lock = {}
	succ = {}
	fail = {}
	
	lock["hook"] = True
	fail["hook"] = "It's screwed to the wall. Quite firmly in fact."
	
	succ["cloak"] = "You don the cloak with a dramatic flourish."

That works, because the hook never moves, while the cloak always can (let an item's lock default to false). In the process, also give each item a suitable message to signal the success or failure of the action.

But what if the player shouldn't be able to walk out the door without the cloak? The obvious way would be like this:


	lock["east","cloakroom"] = lambda: spot["cloak"] != "hero"
	fail["east","cloakroom"] = "Not without proper attire!"
	succ["east","cloakroom"] = "You storm out, cloak billowing."

Trouble is, once we have Python code in the game description, the engine is tied to this one language. The typical solution is to have a custom scripting language, but even a very simple one would be way outside the scope of this article.

(As an aside, we again reuse dictionaries for different types of objects; they can all have slightly different meanings based on context.)

So my newer engine Adventure Prompt instead settles for locks of the form lock["east","cloakroom"] = "-cloak"; just an object ID with a one-character prefix (in this case, meaning "the player isn't carrying the cloak"). You'll need a cheatsheet when there's a dozen of them, but that's a lot of different conditions to test for: a surprisingly flexible system. After all, in the same way you can say when a game object should appear and disappear, or when a dark room becomes lit and so on.

Doing thing this way accomplishes two important things:

  1. the engine only has to be coded once, then many more games can be made just by changing the data;
  2. porting the engine to a new type of computer brings all those games at once to a new audience;

That's how in 2020 we can play so many games in a browser that only used to be available on old computers, or worse, only on consoles and in arcades.

There's just one more thing to take care of, but it's pretty important: until now, we've embedded the game data into the source code. Not very convenient, for several reasons. Look how much redundancy there is, for one thing. And then, imagine writing the engine in a compiled language. Hence why all the talk about databases early on.

Well, "database". It doesn't have to be an SQLite file, really, or for that matter require a specialized GUI to make the game. It would also be way too much for this article, and besides, all we need here is a plain text file:


	room foyer
	name Foyer of the Opera House
	desc A cavern of marble, crystal and gold.
	exit west cloakroom
	
	room cloakroom
	name Cloakroom
	desc This room, once opulent, is now bare.
	
	exit east foyer
	lock -cloak
	fail Not without proper attire!
	succ You storm out, cloak billowing.
	
	item hook
	lock hook
	name small brass hook
	fail It's screwed to the wall. Quite firmly in fact.
	
	item cloak
	spot hook
	name velvet cloak
	desc It's of a black so deep it darkens the room.
	succ You don the cloak with a dramatic flourish.
	
	item hero
	desc As good-looking as ever.

Yes, yes, I said the engine doesn't care what each object is, but we do. Moreover, this way we can make all kinds of data implicit, for example items start out by default in the most recent room defined. As a bonus, we can use the word "exit" just fine in out own text files.

The point however was to show you how the data ends up from that into our engine's internal dictionaries:


	here = ""
	what = ""

	try:
		while True:
			line = input().strip()
			if line == "": continue
			field, data = line.split(maxsplit=1)
			if field == "room":
				here = data
				what = data
			if field == "item":
				what = data
				spot[what] = here
			elif field == "name":
				name[what] = data
			elif field == "desc":
				desc[what] = data
			elif field == "spot":
				spot[what] = data
			elif field == "lock":
				lock[what] = data
			elif field == "succ":
				succ[what] = data
			elif field == "fail":
				fail[what] = data
			elif field == "exit":
				head, dest = data.split(maxsplit=1)
				what = head,here
				link[what] = dest
	except EOFError:
		print("End of game data reached.\n")

There you go! A little bit of context is all we need. Note how an explicit spot neatly overrides the implicit one, and how exits are treated like any other game object for most purposes (they're a "what"). And in the same way we could easily tell the engine to load assets, or whatever else we need.

Printing out the data after load to double-check it is left as an exercise for the reader. Just in case, here's the full code to load the data and play the game, respectively.