Contents
18. Abbreviations
|
How to program a text adventure in C
by Ruud Helderman
<r.helderman@hccnet.nl>
Licensed under
MIT License
18. Abbreviations
Classic adventures like
Colossal Cave
and
Zork
have more than a hundred locations for the player to discover.
Playing these games involves a lot of “go north/east/south/west”,
so it’s no surprise that from the very beginning,
text adventures accepted abbreviations like n, e, s, w.
Implementing an abbreviation appears to be trivial.
From a coding perspective,
an abbreviation is just a synonym for its full-sized counterpart, right?
So all we have to do is
add an extra row to the list of commands in function parseAndExecute.
There is a downside to that approach. When using
text-to-speech
to read out loud the progress of a game session,
those abbreviations do not come out too well.
How is a
screen reader
supposed to know ‘ne’ means north-east?
So instead, I would recommend a pre-processor that
expands abbreviations before sending the user’s input to the parser.
Implementation-wise,
such a pre-processor is quite similar to the actual parser.
We can even re-use function matchCommand.
It may seem like overkill to use matchCommand
for recognizing a one-letter command like ‘n’,
but why would I want to implement
case insensitivity and whitespace trimming all over again?
One may argue that this approach is suboptimal.
A simple abbreviation is expanded to something more verbose
(e.g. “go north”),
which then has to be broken down again by the parser.
Please feel free to optimize, if it makes you feel better.
Just remember the
rules of optimization
from the previous chapter.
expand.h |
- extern char *expand(char *input, int bufsize);
|
expand.c |
- #include <stdbool.h>
- #include <string.h>
- #include "match.h"
- typedef struct
- {
- const char *abbreviated;
- const char *expanded;
- } SHORTHAND;
- static int minimum(int x, int y)
- {
- return x < y ? x : y;
- }
- char *expand(char *input, int bufsize)
- {
- static const SHORTHAND shorthands[] =
- {
- {"n", "go north"},
- {"s", "go south"},
- {"w", "go west"},
- {"e", "go east"},
- {"inv", "inventory"},
- {"x A", "examine "},
- {"A", NULL}
- };
- const SHORTHAND *sh;
- for (sh = shorthands; !matchCommand(input, sh->abbreviated); sh++);
- if (sh->expanded != NULL)
- {
- const char *moreInput = *paramByLetter('A');
- int lengthOfExpanded = strlen(sh->expanded);
- memmove(input + lengthOfExpanded, moreInput,
- minimum(strlen(moreInput) + 1, bufsize - lengthOfExpanded - 1));
- strncpy(input, sh->expanded, lengthOfExpanded);
- }
- return input;
- }
|
Explanation:
- Line 5-9:
struct SHORTHAND is similar to struct COMMAND
(defined in parsexec.c).
It maps a command pattern not to a function
but to another (more verbose) command string.
- Line 16:
this is the function that expands the abbreviation (if any).
Expansion takes place directly in the input buffer.
To be able to prevent
buffer overflow,
the function must be aware of the size of the buffer
(parameter bufsize).
- Line 25:
an example of an expansion that is slightly less trivial.
When expanding ‘x’ to ‘examine’,
we should preserve whatever follows the verb.
For example,
“x coin” should be expanded to “examine coin”.
The trailing space in the string “examine ” is there on purpose;
it helps to keep the code simple.
- Line 26:
just like in the parser, the final pattern “A”
should catch every command that did not match any of the abbreviations.
- Line 29:
this loop tries to find the abbreviation that matches the user’s input.
- Line 30:
if the loop stopped at the final ‘sure-match’ pattern (line 26),
then there was no matching abbreviation.
We will skip the rest of the function; no expansion will take place.
- Line 32:
retrieves the value of parameter A
(e.g. “coin” if the user entered “x coin”),
or an empty string if the abbreviation is parameterless.
- Line 34:
moving the noun (the parameter A, if any) away from the verb,
to make room for the expanded verb.
Deliberately using memmove instead of strcpy
(from string.h),
as we are (possibly) moving data within the same input buffer.
- Line 35:
here bufsize is taken into account, to prevent
buffer overflow.
For very long commands, this may truncate the string,
but assuming the buffer has a reasonable size,
that is unlikely to bother the player.
- Line 36:
now we can finally copy the expanded command to the front of the input buffer.
Now all we have to do is call function expand
right before calling parseAndExecute.
Sample output |
Welcome to Little Cave Adventure.
You are in an open field.
You see:
a silver coin
a burly guard
a cave entrance to the east
dense forest all around
a lamp
--> e
The guard stops you from walking into the cave.
--> get coin
You pick up a silver coin.
--> inv
You have:
a silver coin
--> x coin
The coin has an eagle on the obverse.
--> give coin
You give a silver coin to a burly guard.
--> e
You walk into the cave.
It is very dark in here.
You see:
an exit to the west
--> n
It's too dark.
--> w
You walk out of the cave.
You are in an open field.
You see:
a burly guard
a cave entrance to the east
dense forest all around
a lamp
--> w
Dense forest is blocking the way.
--> quit
Bye!
|
main.c |
- #include <stdbool.h>
- #include <stdio.h>
- #include <string.h>
- #include "expand.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(expand(input, sizeof input)) && getInput(argv[1]));
- printf("\nBye!\n");
- return 0;
- }
|
Of course, there should be more to an adventure than
traversing dozens of locations just to take object A to location B.
In the next chapter, we will look at ways to bring more depth into the game.
Next chapter: 19. Conversations