Every text adventure has a parser. But there are parsers and parsers. A simple ‘verb-noun’ parser (like the one we have been using ever since chapter 2) could be sufficient for a carefully designed adventure game. However, Infocom has proven that a more advanced parser really helps to make an enjoyable game. It does not have to pass the Turing test; but it should enable the player to express his intentions in a more or less natural way.
Our parser mainly consists of two lines of code, tucked away in main.c:
char *verb = strtok(input, " \n"); char *noun = strtok(NULL, "\n"); |
Alright, plus the sequence of strcmp calls to match verbs to commands, and a few functions in misc.c to translate nouns to objects (parseObject, nounIsInTags). But that's it. This system has served us well for the past 10 chapters, but it has some major flaws.
Writing a good parser is no trivial task, but here I will give you a relatively simple approach. We will define a grammar that consists of a list of patterns, similar to (but much simpler than) regular expressions. Examples of patterns:
look around | Matches exactly that; double spaces, leading spaces, trailing spaces and case differences are ignored |
go A | Matches the word go followed by the tag of an object (see chapters 4 and 8 for an explanation of ‘tags’) |
put A in B | Matches the word put followed by a tag, the word in, and finally another tag |
To parse the user's input, we will traverse the list from top to bottom, trying to match the user's input with each pattern in turn. We will stop at the first match found. We will not make use of backtracking, though this could be added later.
Uppercase letters are the nonterminal symbols in our grammar; they match any object's tag, and they capture the properties of the associated object. For each nonterminal, the properties are stored in a struct:
typedef struct { const char *tag; OBJECT *object; DISTANCE distance; int count; } PARAM; |
A brief explanation of the struct's members:
This struct could then be passed as a parameter to the function that takes care of execution of the command.
void executeGo(PARAM *param) { OBJECT *obj = param->object; DISTANCE distance = param->distance; ... } |
For simplicity, I will use a global variable instead of parameters (despite the bad reputation of global variables). The global variable will be an array of aforementioned struct.
PARAM params[26]; |
params[0] (the first array element) captures nonterminal ‘A’, params[1] captures ‘B’, and so forth. The total number of elements in the array does not really matter. 26 is sufficient when every uppercase letter qualifies as a nonterminal. In practice, the upper bound can be a lot smaller, but please do some bounds checking to prevent a buffer overrun in case of a malformed pattern.
Now we have to think of a way to handle missing or unrecognized objects. For example, if the user enters “go kave”, none of the patterns will match (not even “go A” because “kave” is not a known tag), and a generic error message (“I don't know how to go kave”) would be returned. This takes away every opportunity to improve these ‘negative responses.’ It would be better to maintain all replies concerning command go inside function executeGo. There are a few ways to achieve this, but the easiest one is to introduce special nonterminals “A?”, “B?” etc. that match anything (including total gibberish). Capturing will be as follows.
In practice, we will use these ‘loose’ nonterminals only at the very end of a pattern. Examples:
You saw us create two patterns for command put. This applies to every command that requires two or more parameters. With n parameters, you need n patterns to handle all possible mismatches. An example with three parameters:
The following module contains a list of patterns that should be able to match every command we have implemented so far (plus a few extras such as look at and go to to demonstrate the correct order of patterns). Each pattern is tied to a function that executes the appropriate command.
TODO: also 'get A from B' and 'put A in B' and 'give A to B' and 'ask B for A'
parsexec.h | |
1 | extern int parseAndExecute(const char *input); |
parsexec.c | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | #include "object.h" #include "misc.h" #include "parsexec.h" #include "location.h" #include "inventory.h" #include "execute.h" typedef struct { int (*function)(void); const char *pattern; } COMMAND; static int executeQuit(void) { return 0; } static int executeNoMatch(void) { PARAM *par = paramByLetter('A'); if (par->distance != distNoObjectSpecified) { printf("I don't know how to %s.\n", par->tag); } return 1; } int parseAndExecute(const char *input) { static const COMMAND commands[] = { {executeQuit , "quit"}, {executeLookAround, "look"}, {executeLookAround, "look around"}, {executeLook , "look at A?"}, {executeLook , "look A?"}, {executeGo , "go to A?"}, {executeGo , "go A?"}, {executeGetFrom , "get A from B?"}, {executeGet , "get A?"}, {executePutIn , "put A in B?"}, {executePutIn , "drop A in B?"}, {executeDrop , "drop A?"}, {executeGive , "give A?"}, {executeAsk , "ask A?"}, {executeInventory , "inventory"}, {executeOpen , "open A?"}, {executeClose , "close A?"}, {executeLock , "lock A?"}, {executeUnlock , "unlock A?"}, {executeNoMatch , "A?"} }; const COMMAND *cmd; for (cmd = commands; !matchCommand(input, cmd->pattern); cmd++); return (*cmd->function)(); } |
Explanation:
The hardest part is the implementation of function matchCommand.
match.h | |
1 2 3 4 5 6 7 8 9 10 11 12 | typedef struct param { const char *tag; OBJECT *object; DISTANCE distance; int count; } PARAM; extern PARAM params[]; #define paramByLetter(letter) (params + (letter) - 'A') extern int matchCommand(const char *src, const char *pattern); |
match.c | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | #include <ctype.h> #include <string.h> #include "object.h" #include "misc.h" #include "match.h" #define MAX_PARAMS 26 PARAM params[MAX_PARAMS]; static const char *skipSpaces(const char *src) { while (isspace(*src)) src++; return src; } static const char *matchSpaces(const char *src) { return *src == '\0' || isspace(*src) ? skipSpaces(src) : NULL; } static const char *matchTerminal(const char *src, char terminal) { return terminal == ' ' ? matchSpaces(src) : tolower(*src) == tolower(terminal) ? src + 1 : NULL; } static const char *matchTag(const char *src, const char *tag) { while (src != NULL && *tag != '\0') src = matchTerminal(src, *tag++); return src; } static int compareWithParam(const char *tag, DISTANCE distance, PARAM *par) { int diff = strlen(tag) - strlen(par->tag); if (diff == 0) diff = par->distance - distance; if (diff == 0) par->count++; return diff; } static const char *matchParam(const char *src, PARAM *par, int loose) { const char *restOfSrc = loose ? src + strlen(src) : NULL; OBJECT *obj; par->tag = src; par->distance = *src == '\0' ? distNoObjectSpecified : distUnknownObject; forEachObject(obj) { DISTANCE distance = distanceTo(obj); const char **tag; for (tag = obj->tags; *tag != NULL; tag++) { const char *behindMatch = matchTag(src, *tag); if (behindMatch != NULL && compareWithParam(*tag, distance, par) > 0 && (!loose || *skipSpaces(behindMatch) == '\0')) { par->tag = *tag; par->object = obj; par->distance = distance; par->count = 1; restOfSrc = behindMatch; } } } return restOfSrc; } int matchCommand(const char *src, const char *pattern) { PARAM *par; for (par = params; par < params + MAX_PARAMS; par++) { par->object = NULL; par->distance = distNoObjectSpecified; par->count = 0; } for (src = skipSpaces(src); src != NULL && *pattern != '\0'; pattern++) { src = isupper(*pattern) ? matchParam(src, paramByLetter(*pattern), pattern[1] == '?') : *pattern == '?' ? src : matchTerminal(src, *pattern); } return src != NULL && *skipSpaces(src) == '\0'; } |
Explanation:
Now we can remove the original implementation of function parseAndExecute from main.c:
main.c | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | #include <stdio.h> #include "match.h" #include "location.h" static char input[100]; static int getInput() { printf("\n--> "); return fgets(input, sizeof(input), stdin) != NULL; } int main() { printf("Welcome to Little Cave Adventure.\n"); executeLook("around"); while (getInput() && parseAndExecute(input)); printf("\nBye!\n"); return 0; } |
The amount of code in misc.c can be reduced as well, since the new parser makes functions parseObject and nounIsInTags obsolete. This is what is left.
misc.h | |
1 2 3 4 | TODO |
misc.c | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | #include <stdio.h> #include <string.h> #include "object.h" #include "misc.h" OBJECT *getPassageTo(OBJECT *targetLocation) { OBJECT *obj; forEachObject(obj) { if (obj->location == player->location && obj->prospect == targetLocation) { return obj; } } return NULL; } DISTANCE distanceTo(OBJECT *obj) { return !validObject(obj) ? distUnknownObject : obj == player ? distPlayer : obj == player->location ? distLocation : obj->location == player ? distHeld : obj->location == player->location ? distHere : getPassageTo(obj) != NULL ? distOverthere : !validObject(obj->location) ? distNotHere : obj->location->location == player ? distHeldContained : obj->location->location == player->location ? distHereContained : distNotHere; } OBJECT *personHere(void) { OBJECT *obj; forEachObject(obj) { if (distanceTo(obj) == distHere && obj->health > 0) { return obj; } } return NULL; } int listObjectsAtLocation(OBJECT *location) { int count = 0; OBJECT *obj; forEachObject(obj) { if (obj != player && obj->location == location) { if (count++ == 0) { printf("%s:\n", location->contents); } printf("%s\n", obj->description); } } return count; } int weightOfContents(OBJECT *container) { int sum = 0; OBJECT *obj; forEachObject(obj) { if (obj->location == container) sum += obj->weight; } return sum; } |
We still need to make some minor adjustments to the existing command-implementing functions. We immediately grab the opportunity to make a check on ambiguous nouns.
execute.h | |
1 2 3 4 | extern int executeOpen(void); extern int executeClose(void); extern int executeLock(void); extern int executeUnlock(void); |
execute.c | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | #include <stdio.h> #include "object.h" #include "misc.h" #include "match.h" static int objectWithinReach(const char *verb, PARAM *par) { int ok = 0; if (par->distance > distNotHere) { printf("I don't understand what you want to %s.\n", verb); } else if (par->distance == distNotHere) { printf("You don't see any %s here.\n", par->tag); } else if (par->distance >= distHereContained) { printf("That is out of reach.\n"); } else if (par->count > 1) { printf("Multiple choices to %s; be more specific.\n", verb); } else { ok = 1; } return ok; } int executeOpen(void) { if (objectWithinReach("open", params)) { printf("%s", (*params->obj->open)(params->obj)); } return 1; } int executeClose(void) { if (objectWithinReach("close", params)) { printf("%s", (*params->obj->close)(params->obj)); } return 1; } int executeLock(void) { if (objectWithinReach("lock", params)) { printf("%s", (*params->obj->lock)(params->obj)); } return 1; } int executeUnlock(void) { if (objectWithinReach("unlock", params)) { printf("%s", (*params->obj->unlock)(params->obj)); } return 1; } |
TODO: location.*
location.h | |
1 2 3 | extern int executeLookAround(void); extern int executeLook(void); extern int executeGo(void); |
location.c | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | #include <stdio.h> #include <string.h> #include "object.h" #include "misc.h" #include "match.h" int executeLookAround(void) { printf("You are in %s.\n", player->location->description); listObjectsAtLocation(player->location); return 1; } int executeLook(void) { if (params->distance >= distUnknownObject) { printf("I don't understand what you want to see.\n"); } else if (params->distance == distNotHere) { printf("You don't see any %s here.\n", params->tag); } else if (params->distance == distOverthere) { printf("Too far away, move closer please.\n"); } else if (params->distance == distHereContained) { printf("Hard to see, try to get it first.\n"); } else { printf("%s", params->obj->details); listObjectsAtLocation(params->obj); } return 1; } static void movePlayer(OBJECT *passage) { printf("%s", passage->textGo); if (passage->destination != NULL) { player->location = passage->destination; printf("\n"); executeLookAround(); } } int executeGo(void) { if (params->distance >= distUnknownObject) { printf("I don't understand where you want to go.\n"); } else if (params->distance == distLocation) { printf("You are already there.\n"); } else if (params->distance == distOverthere) { movePlayer(getPassageTo(params->obj)); } else if (params->distance == distHere) { movePlayer(params->obj); } else if (params->distance < distNotHere) { printf("You can't get any closer than this.\n"); } else { printf("You don't see any %s here.\n", params->tag); } return 1; } |
TODO: inventory.*
inventory.h | |
1 | TODO |
inventory.c | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | #include <stdio.h> #include "object.h" #include "misc.h" #include "match.h" static int moveObject(PARAM *par, OBJECT *from, OBJECT *to) { OBJECT *obj = par->obj; if (obj == NULL) { printf("I don't understand what item you mean.\n"); } else if (from != obj->location) { switch (par->distance) { case distPlayer: printf("You should not be doing that to yourself.\n"); break; case distHeld: printf("You already have %s.\n", obj->description); break; case distLocation: case distOverthere: printf("That's not an item.\n"); break; case distHere: if (from == player) { printf("You have no %s.\n", par->tag); } else { printf("Sorry, %s has no %s.\n", from->description, par->tag); } break; case distHeldContained: case distHereContained: printf("Sorry, %s is holding it.\n", obj->location->description); break; default: printf("You don't see any %s here.\n", par->tag); } } else if (to == NULL) { printf("There is nobody here to give that to.\n"); } else if (obj->weight > to->capacity) { printf("That is way too heavy.\n"); } else if (obj->weight + weightOfContents(to) > to->capacity) { printf("That would become too heavy.\n"); } else { obj->location = to; printf("OK.\n"); } return 1; } int executeGet(void) { return moveObject(params, player->location, player); } int executeDrop(void) { return moveObject(params, player, player->location); } int executeGive(void) { return moveObject(params, player, personHere()); } int executeAsk(void) { return moveObject(params, personHere(), player); } int executeGetFrom(void) { return moveObject(params, params[1].obj, player); } int executePutIn(void) { return moveObject(params, player, player[1].obj); } int executeInventory(void) { if (listObjectsAtLocation(player) == 0) { printf("You are empty-handed.\n"); } return 1; } |
TODO: still no 'drop all', 'drop coin and key', 'get key and go east'; yacc might be better
Next chapter: 14. Light and dark