Decoupling State from Functionality

A Case Against Classes for Text Adventure Games

Rob Muhlestein
Aug 23, 2017 · 9 min read

Learn or teach programming fundamentals in any language by creating a single-file , sharable text adventure (aka interactive fiction, aka story game) using a simple linked subroutine (aka function) for each part. One part calls another—no classes needed. Store everything about the game worth saving in a global dictionary (aka map, aka object, aka associative array). Save the game by saving the dictionary to JSON. Beginning students will break out of linear thinking gracefully and be better prepared for event-based programming concepts later.

Ever save a game? Of course you have. When you did you saved the state of the game, not its functionality, which is tucked away in the code.

State contains all the properties of a thing at a certain moment in time that potentially change over time. Things like health, mana, inventory, and location are all part of the state. Some properties have other things in them that also have their own state, such as a magic lamp with three wishes.

Let’s imagine you are making your first story game, sometimes also called a text adventure, or interactive fiction. At any point during the story you want the player to be able to save.

Without knowing any code at all you already know you need to have some format to save the game data to disk or the cloud or to send it somewhere. Intuitively you know this should take the least amount of effort. What are you options?

Story Game Stub in Python

A Class for Every Room?

No, no, no. I have read several recommend this to beginner programmers, but even the most advanced programmer never would. Classes define the type and structure of data for each object that is constructed from the class, like from a blueprint or template. You know you are doing it wrong if you are creating classes called MailBox and HouseByTheField. They call classes that are meant to only have a single object singletons but this is the least practical approach. People have been so conditioned to inject classes into everything that often they do this and develop themselves into a corner rather than out of one, which was the original intend of classes and the main reason traditional object oriented programming with inheritance has rounded as a bad practice most of the time.

A Part Class

So rather than have a HouseByTheField you would have a class Area or Part or Chapter and houseByTheField would be just one of objects from that class. But we are straining.

Each part needs to be flexible enough to contain its own code and interaction with the player.

There are ways to inject code into a object constructed from a class, (to use traditional OO parlance), with “first class” functions, which are functions that can be used like any other variable until they are called. The problem with this approach is that—even if you are lucky enough to have first class functions in your language—you have code for a given part that lives all over the place:

  • The class Part definition block
  • The definition of the function to handle the part
  • The object constructed from the Part class
  • The initialization of the part object passing in the handler function
  • The code that calls the handler from the part object

You can kinda combine some of them, but when you do you pretty much blow out strict-typing, (which you do not want to do). Strict typing is always your friend even though it will spit your errors in your face immediately rather than silently killing players with them later.

Using this option every Part has to be initialized and any specific functionality for that Part has to be passed in as a function handler. For example, in order to have a dumb chess board AI you would have to write the code, potentially creating a class for it as well, and then pass it in to the Part object when created.

Then when you want to save out the game that part object would have to be marshaled to some formatted data to be stored or sent.

Thankfully there is a much simpler way.

One Subroutine for Each Part

The best solution is to create a subroutine for each part. The term subroutine is a little old. The gosub statement supplemented the goto statement in early programming languages. This literally jumped the program to the line number that the subroutine was on. Before that goto literally jumped to an exact line number in the code, (which obviously was a huge problem).

These days subroutines are more commonly called functions. For a while languages like Pascal distinguished between procedures and functions, mostly to show that functions have a return value and procedures do not. The term methods is also frequently used here, albeit incorrectly. A method must be associated with a specific thing and is almost always tied to that thing with a dot.

I use the word subroutine on purpose here because it conveys the best meaning. In our text adventure we want to jump to a different part of the story adventure depending on what happens in a given part and what the player responds. There are no classes to create, objects to initialize, extra part handlers to write. The subroutine is the part handler from the beginning.

Within the subroutine if statements or task completion can directly call the next part subroutine.

Each part is linked to one or more other parts through direct calls to the next parts subroutine.

The most powerful thing about this approach is that you can include an entire mini-game in a part subroutine if you wish. “You notice a piece on the chess board in front of you randomly moves beckoning your response,” after which you move your piece and the chess AI reacts all within the part containing the chess board.

One interesting side effect of using linked subroutines is that the call stack, (which is the language’s way of keeping track of the order of subroutine calls), actually represents the path through the story adventure itself. If you do have a bug that causes your game to crash, the stack trace would literally show the exact parts the player did in order. Talk about great debugging!

Capitalize Part Subroutines

If your language allows it, I would break with convention and capitalize each part subroutine to distinguish it from other code in your story. That way you can see the parts right away. Normally initial capitalization is reserved for things like classes, but since we do not use classes we have no worries.

Beware of Recursion Limits

Most languages have limits on the number of times a subroutine can call itself, which is called recursion. You might be tempted to simply call the same part subroutine again if the player doesn’t give a correct command. This risks hitting that recursion barrier eventually (even though it is usually in the hundreds or thousands). A safe way to avoid this is to put an infinite loop inside the part subroutine around the prompt for player action that can only be broken out of with a correct response.

Navigation Considerations

Don’t do a grid system. You do not need it. You are not creating a muli-user-dungeon (MUD) here. In fact, I strongly encourage you to use non-standard directional commands and choice triggers. There are more choices than just North, South, East, West and left and right. You are creating an interactive story, a text adventure, the more possibilities, the more fun, even if back tracing your steps is harder. It forces the player to be present and dwell in imagination instead of rushing through things.

Usually coders get sucked into adding a grid when they want to add wandering monsters or other AI that generally knows about the entire world generated by our interactive text adventure. You know you have hit this when you start coding standard navigation options into your game and justify it saying you want to have consistency. Consistency makes text adventures boring—really boring. Besides, interactive fiction need not have a part that has a specific direction and location. The entire thing can occur from a single place. Text adventures are about the story, not just the navigational location. [By the way, MUDs engines really destroyed this.]

Besides, a true AI would know more about the environment and adapt rather than having knowledge of the entire grid. One way to implement a wandering monster would actually be a fun exercise in machine intelligence. You simply need to create an additional command for each part of the story that gives all the specific options for that part, which are hidden to human players, so that the monster or non-player character can choose different things. Machine learning projects begin with a required number of relatively minimal choices.

Once you add the wiring to your code to support multiple concurrent players you can simple have monsters and other non-player characters enter the game like everyone else, but at different points. Once your game parts are more or less solid you could program a bot to learn by playing the game millions of times, then take that code generated and incorporate it into your game itself.

Global Game State

Starting with the Welcome() part you probably already want to save something about the game that other parts can see, say a name you prompted from the player. The easiest way to keep track of this is to create a game dictionary (here I love the Python word “dictionary” instead of “map”) and add the name to it. Now you instantly have a single variable containing all the data about your game that you can save or send very easily.

Imagine the other things you can save:

  • The player’s current location
  • Rooms visited and even in what order with a sort of breadcrumb trail
  • Items available in different rooms to be picked up
  • Player stats

Indeed there are many things.

But Global is Bad, Right?

You have probably been beaten to death at some point in your life for ever using “global” anything in programming but this is effectively exactly what most games do—especially high performance ones. In a multiplayer game every detail of game state—down to the position of that rocket or AOE effect—is communicated to everyone playing that game hundreds or thousands of times a second.

In fact, these protocols for communicating that state data are the core and everything else somewhat secondary because game play speed completely depends on its efficiency to avoid dreaded lag. Sure somewhere in that C++ code there might be a rocket class probably, but what it is and where are the only bits of data that need be communicated. There is no time for marshaling.

Create a Tell Helper Function (Instead of Print)

Creating a simple tell() function, (which just prints its argument to the screen initially), will allow you to customize how the text of the story is displayed. Perhaps later you want to add color, or wrap it for a different screen width. Using tell() from the beginning instead of the native print() function will save a lot of code rewriting later.

Create a Prompt Helper Function

If you are luck your language has a way of prompting the player for input and returning it so you can put it into a variable and act on it. But even so it is best to create a prompt() helper function so that you can customize how this is done. Eventually you will add game manager functionality to the prompt() function as well since there is no event game loop. Built in actions, such as ‘inventory’ or ‘tell’ can be added to prompt so that every part subroutine does not have to.

The prompt() function should probably take an argument to ask a specific question but just display a basic prompt by default when no arguments are passed to it.

Personal Rant Against Using Classes by Default

After about two years of Go programming and several in JavaScript, C, Ruby, and Perl before that I have come to loathe classes, (even though I created a Perl classes pragma, which I haven’t updated in years). Notice I said classes and not objects.

Traditionally instead of just creating a map/dictionary to store the data about your thing you create a class and only stamp out things from that class by calling a constructor. This is often just plain unnecessary. If you are doing anything in the constructor you likely have side effects and have roundly been identified as implicit bad practice. If you are setting default values you could have just set them to begin with in the map itself or when the thing is used.

One of the best parts about Go is that there are no classes. There are structs, states of things, objects if you insist, that can have methods tacked onto them. This is brilliant.

What goes beyond brilliant is that the data structure representing the thing can be easily converted (called marshaling) into any format, JSON, text, binary and saved or sent somewhere. When you open that data or receive it you just have to tell Go, “hey this is type of foo thing” (called unmarshaling and/or casting) and it just does it. Go was clearly created for a data-centric world we find ourselves in today.

)

Rob Muhlestein

Written by

/^((Found|Teach|Hack)er|(Men|Jani)tor|C\w+O)$/ 🌎 skilstak.io 💥 robs.io

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade