Navigation is the most fundamental mechanic of an adventure game. Generally, we think of the player’s character being in a room and having exits to other rooms. Using rudimentary commands, e.g. “go north”, “go east”, “go up”, the player can explore the game.
Abstractly, what we have are a collection of nodes (which we generically refer to as rooms) and edges between those nodes (exits). This collection of nodes and edges form a directed graph (map).
Some adventure games are effectively just maps. Look at the famous Choose Your Own Adventure series of books. Here the rooms are passages of text with the exits being the page numbers you could jumpt to at the end of each passage. This concept is most popularly executed today as Twine hypertext games.
We construct a map by first creating a list of rooms. Each room has a description (which may include images), maybe a title or label, and a list of exits. Each exit has a label, e.g. “north”, “south”, “up”, and a reference to the next room that the player would arrive in if they took that particular exit.
In the original BASIC, maps were represented as a NxM array of integers, N rooms x M possible exits. The possible exits were fixed: north, south, east, west, up, and down. If movement in that direction was not possible then the value stored would be 0. Otherwise, the value stored was the index of the new room. Thus, newRoom = map[currentRoom][chosenDirection]
, where chosenDirection
would be an enum for the cardianl directions. In modern languages we can make things a bit less rigid by leveraging the fast, first-class hashes.
In our version, rooms aren’t identified by index, they’re identified by name (which is really just any unique key). And instead of being tied to a fixed set of directions, we can have an unlimited number of “directions” by simply using any uniquely named key for the direction as well. That gives us a data structure that looks like this:
{
"outside-house": {
"door": "inside-house",
"south": "field"
},
"inside-house": {
"door": "outside-house"
},
"field": {
"north": "outside-house"
}
}
To find a new room, the lookup looks the same: newRoom = map[currentRoom][chosenDirection]
, but this time all of those variables are strings, not integers. Much easier to reason about, debug, and author.
Rooms need more than exits though. Our rooms need descriptions. So let’s adjust our data structure to have a description field and an exits field:
{
"outside-house": {
"description": "You are standing outside a white house.",
"exits": {
"door": "inside-house",
"south": "field"
}
},
"inside-house": {
"description": "You are standing inside an old house.",
"exits": {
"door": "outside-house"
}
},
"field": {
"description": "You are standing out in a field.",
"exits": {
"north": "outside-house"
}
}
}
When you arrive in a room you display the description and (optionally) a list of available exits. You now have a map you can explore!
A runnable example will be coming soon.