Making better text adventures in C

2025-11-27


Howdy, writers and readers!

Today it's more of a technical article, sorry. It follows one of my earliest posts about making text games in C. This post requires prior knowledge of that old dusty programming language.

The single-pass state machine game

A few months ago I rewrote Plouffe: the Game (a wacky adventure feat my former linear algebra teacher). The original had a lot of switch statements in the main loop:

switch (room) {
case ROOM_ENTRANCE:
	puts("You are in front of a door.\n"
	"1 Open door\n"
	"2 Dance the macarena\n"
	// other options...
	);
	switch(get_input()) {
	case '1':
		puts("It's locked!");
		return ROOM_ENTRANCE;
	case '2':
		puts("You successfully clip through the wall.");
		return ROOM_AMPHITHEATER;
	// other actions for this room...
	}
case ROOM_AMPHITHEATER:
	puts("You see a lot of students.\n"
	"Oh no, not those again, you think to yourself.");
	// other rooms etc...
}

As you can feel, it's quite a lot of code for a minimal "state machine game".

Since then, I discovered the full potential of structs. Here is the game data, now that it sits outside the main loop:

struct action { char from, to, *label, *text; } actions = {
	{0,0,"1 Open door","It's locked!"},
	{0,1,"2 Dance the macarena","You clip through, yay"},
	// etc
};

There is only one line per action, very compact! And most interestingly, here is the entire main loop itself:

int main() {
	int state = 0, input = '\n';
	while (input != 'q') {
		for (struct action *a = actions; a->label; a++) {
			if (a->from == state) {
				if (input == '\n') puts(a->label);
				if (input == a->label[0]) {
					puts(a->text);
					state = a->to;
					break;
				}
			}
		}
		if (input == '\n') printf("> "); fflush(stdout);
		input = getchar();
	}
}

It takes advantage of the terminal, which inputs a cute little newline character when you press Enter. That's when the game has to display your current options. Other characters in the input are actions. Now a single code block remains.

If you want clean input when you use an alternative standard library like Musl, you will need to add "fflush(stdout)" after every print without a new line.

Example session:

  Welcome to Plouffe the Game. You are in front of a door.
1 Open door
2 Dance the macarena
> 1
  It's locked!
1 Open door
2 Dance the macarena
> 2
  You clip through, yay!
  In class, you see a lot of students.
  Oh no, not those again, you think to yourself.
1 Teach
2 Swing various fresh fruit around
3 Go to sleep
> q
Bye!

That's great but not the only thing I found recently...

Raw mode

Ever wanted to just skip the silly "press Enter" requirement to your text game? Enter Raw mode. I do it like this, but there are other ways. Instead of just "input = getchar()":

system("stty raw");
input = getchar();
system("stty cooked");

What does it mean?

Example of a main loop:

while (input != 'q') {
	// list options
	for (struct action *a = actions; a->label; a++)
		if (a->from == state) puts(a->label);

	// handle input
	system("stty raw");
	input = getchar();
	system("stty cooked");

	// handle choice
	for (struct action *a = actions; a->label; a++) {
		if (a->from == state && input == a->label[0]) {
			puts(a->text);
			state = a->to;
			break;
		}
	}
}

The tree machine
(for the lack of a better term)

Why stop at state machines? They're quite a bit limited, you can't put a world/inventory model in them, or just a very simple one. That was the main subject of my "making text adventures" article, and I came up with a complicated bunch of code, but since then I found a dark and mysterious data structure.

The tree machine. Here it is:

enum { YOU = 1, FIELD, TAVERN, APPLE, NB_OBJECTS };
char where[NB_OBJECTS] = {
	[YOU]	= FIELD,
	[APPLE]	= FIELD,
};
struct act { char from[4], to[4], *label, *text; } acts[] = {
{{YOU, FIELD, APPLE, FIELD}, {APPLE, YOU}, "take apple", "You take it."},
{{YOU, FIELD, APPLE, YOU}, {APPLE, FIELD}, "drop apple", "Bye bye apple!"},
{{YOU, FIELD}, {YOU, TAVERN}, "enter tavern", "What a jolly place! You sit down."},

{{YOU, TAVERN}, {YOU, FIELD}, "leave tavern", "You go back outside."},
{{YOU, TAVERN}, {}, "order drink", "It'll take a while."},
{{YOU, TAVERN, APPLE, YOU}, {APPLE, 0}, "eat apple", "Omnomnomnom!"},
{0}};

As you can see:

This tree machine can simulate a complex world model with rooms, inventory, and containers.

It's still very basic. There is no support for generic actions like "the apple is at the same place as you", logic like "the apple is NOT on you", or overrides like "the apple is on you and that other option isn't available". But for simple games (already far more complex than state machine games!) it works, and it's kinda elegant.

If you know an official term for that particular data structure, let me know!

And now, let's step out of the Unix world...

Behind the mirror slash
(a figure of speech because, you know, backslash in Windows. anyway)

To change the scenery from the Unix ubiquity of my PC, I installed both FreeDOS (an operating system) and DOSBox (an emulator). DOS is a very old system that works very well as a bedrock platform. Since it's "dead", it's very mature and stable for long-term software.

The first compiler I found for it is Bruce's C Compiler. Even its documentation says it's a bit buggy lol. Remember what I said about old software? Anyway, it's a no-effort way to cross-compile to DOS, so I choose you, Pikachu- I mean bcc.

Conclusion

I really like text games, because unlike other video games they can last decades without maintenance and you can still discover tricks about them. But often they're more like math tricks (called theorems by normal people).

On that, keep text-gaming and see ya in the next one!