How to program a text adventure in C
by Ruud Helderman
<r.helderman@hccnet.nl>
Licensed under
MIT License
16. Savegame
An adventure with any degree of difficulty
should give the player the opportunity to save his progress,
so he can resume the game at a later time.
Typically, adventure games simply save their state to a file on disk.
Basically this means: write every (relevant) variable to a file,
and read them back in again to resume the game.
For reasons of portability and security, it would be wise to
serialize
the data.
For a traditional single-player adventure,
an alternative would be for the game to log the player’s input.
When the player wants to resume, do a ‘roll-forward’;
starting from the initial game state, replay every command.
Unusual as it may be, it brings along a few nice advantages.
- The player can browse back through the entire transcript of the game.
It can help a player get over that feeling of
“It has been a while since I last played, what was I doing here?”
- Makes it easier for the player to ‘undo’ a command.
For example when stuck in the dark:
exit the game, edit the log file, resume the game.
It would be cruel to demand the player to start all over again.
- Makes it harder for the player to cheat.
There simply is no advantage in hacking the log file;
without the right clue (or a friend’s savegame),
you will never make it to the other side of that locked door.
- Implementation is simple and generic.
We only have to adjust one function: getInput (see chapter 2).
- Portable by nature.
The log file is a straightforward text file; one command per line.
Do be careful with software updates that alter the game’s behavior;
these might invalidate log files created in earlier versions of the game.
- It can help the developer to analyze problems.
After an application crash,
it will be possible to retrace the steps that led to the situation.
- It can help the developer with test automation.
This is explained in the next chapter.
Of course, this technique also comes with some challenges:
- As the player spends more time playing the game,
the log file will grow, and the time it takes to resume the game will increase.
This is discussed in chapter 17.
- Games that use a random generator to bring in an element of surprise,
may go in a totally different direction when resuming a game saved earlier.
This is discussed in chapter 20.
-
This technique is less suitable for online multi-player games.
For those, it is better to use a database.
This will be discussed in chapter 23.
main.c |
- #include <stdbool.h>
- #include <stdio.h>
- #include <string.h>
- #include "parsexec.h"
- static char input[100] = "look around";
- static bool getFromFP(FILE *fp)
- {
- bool ok = fgets(input, sizeof input, fp) != NULL;
- if (ok) input[strcspn(input, "\n")] = '\0';
- return ok;
- }
- static bool getInput(const char *filename)
- {
- static FILE *fp = NULL;
- bool ok;
- if (fp == NULL)
- {
- if (filename != NULL) fp = fopen(filename, "rt");
- if (fp == NULL) fp = stdin;
- }
- else if (fp == stdin && filename != NULL)
- {
- FILE *out = fopen(filename, "at");
- if (out != NULL)
- {
- fprintf(out, "%s\n", input);
- fclose(out);
- }
- }
- printf("\n--> ");
- ok = getFromFP(fp);
- if (fp != stdin)
- {
- if (ok)
- {
- printf("%s\n", input);
- }
- else
- {
- fclose(fp);
- ok = getFromFP(fp = stdin);
- }
- }
- return ok;
- }
- int main(int argc, char *argv[])
- {
- (void)argc;
- printf("Welcome to Little Cave Adventure.\n");
- while (parseAndExecute(input) && getInput(argv[1]));
- printf("\nBye!\n");
- return 0;
- }
|
Explanation:
- Line 11:
fgets
has the nasty habit of storing a trailing
newline
character in buffer input.
I never mentioned it, as the parser was smart enough to ignore trailing
whitespace.
But in this chapter, we are logging the contents of input,
and in chapter 18, we will be manipulating the contents,
making this newline character more of a nuisance.
So from now on, we will be stripping it off.
Credits to
Tim Čas
for the clever use of strcspn.
- Line 17:
the
static variable
fp represents the source of input; it is either
stdin
(the program takes commands from the keyboard)
or a file containing the list of commands entered in earlier sessions.
- Line 19-23:
the first time getInput is called (fp is still NULL),
fp will be set,
either to a file or (if no filename was specified) to stdin.
In case of a file, it will be opened now for reading.
- Line 24-32:
when reading input from stdin,
each new command entered by the user will be logged
(if a filename was specified).
Notes:
- ‘Old’ commands read from file
do not need to be written to file again.
- The command logged here is the one entered by the user
during the previous call to getInput.
We deliberately do this
so we will not log commands that cause the program to terminate
(both ‘quit’ and any commands that trigger a crash).
- The ‘else’ at the start of line 24
causes logging to be skipped during the first call of getInput.
This prevents the initial ‘look around’ (line 6)
from being logged.
- We are writing to
the same file that was opened earlier to read input from (line 21).
But writing never starts until
after the file was closed after having been read completely (line 43).
So we need to open it again; this time for ‘appending’
(i.e. writing at the end of the file).
- The file is closed each time we have written a command;
no need to keep it open while the user is thinking about his next move.
If you insist on keeping it open
(to prevent other processes from messing with the file),
then I suggest you flush after each command written.
- Line 34:
here we try to read a command, either from file or from keyboard.
- Line 37-40:
after sucessfully reading a command from file,
we echo the command to screen.
That way, the user will get to see the full dialog of earlier sessions.
- Line 41-45:
once the input file reaches
EOF,
we switch over to manual input.
We close the file, set fp to stdin,
and read again using the new file pointer.
- Lines 50 and 54:
the name of the file must be passed as the first argument
when calling the program.
If no name is given, then argv[1] will be NULL.
- Line 52:
dummy statement
to suppress a compiler warning on unused parameter argc.
And in case you hadn't noticed:
this is the first time since chapter 2,
that we are making changes to main.c!
Next chapter: 17. Test automation