Contents

1. Introduction
2. The main loop
3. Locations
4. Objects
5. Inventory
6. Passages
7. Distance
8. North, east, south, west
9. Code generation
10. More attributes
11. Conditions
12. Open and close
13. The parser
14. Multiple nouns
15. Light and dark
16. Savegame
17. Test automation
18. Abbreviations
19. Conversations
20. Combat
21. Multi-player
22. Client-server
23. Database
24. Speech
25. JavaScript

How to program a text adventure in C

by Ruud Helderman <r.helderman@hccnet.nl>

Licensed under MIT License

9. Code generation

So far, our adventure has 10 objects. Each object consists of 5 attributes (the fields in struct OBJECT). A real text adventure is likely to have hundreds, even thousands of objects, and the number of attributes per object is likely to grow as well (see the next chapter). In its current form, maintaining such a big list of objects and attributes would be hard.

For example, when we added objects wallField and wallCave in the previous chapter, we had to do so in three different places: once in object.h (as a #define), and twice in object.c (an element in array objs, and a separate array for the tags). This is clumsy and error-prone.

Instead of maintaining object.h and object.c by hand, we will start generating the files from a single source that is more suited to our needs. This new source file could be in any language you like (typically some domain-specific language), as long as you have the tools to convert it back to C. Below is a simple example. Consider the following layout to organize our objects:

Raw C code (declarations)
- ObjectName
      AttributeName AttributeValue
      AttributeName AttributeValue
      ...
- ObjectName
      AttributeName AttributeValue
      AttributeName AttributeValue
      ...
- ...

Based on the objects we have gathered so far, we could construct the following source file. The file name does not matter much; I simply named it object.txt, to make it clear it is related to object.h and object.c.

object.txt
  1. #include <stdio.h>
  2. #include "object.h"
  3. typedef struct object {
  4. const char *description;
  5. const char **tags;
  6. struct object *location;
  7. struct object *destination;
  8. } OBJECT;
  9. extern OBJECT objs[];
  10. - field
  11. description "an open field"
  12. tags "field"
  13. - cave
  14. description "a little cave"
  15. tags "cave"
  16. - silver
  17. description "a silver coin"
  18. tags "silver", "coin", "silver coin"
  19. location field
  20. - gold
  21. description "a gold coin"
  22. tags "gold", "coin", "gold coin"
  23. location cave
  24. - guard
  25. description "a burly guard"
  26. tags "guard", "burly guard"
  27. location field
  28. - player
  29. description "yourself"
  30. tags "yourself"
  31. location field
  32. - intoCave
  33. description "a cave entrance to the east"
  34. tags "east", "entrance"
  35. location field
  36. destination cave
  37. - exitCave
  38. description "an exit to the west"
  39. tags "west", "exit"
  40. location cave
  41. destination field
  42. - wallField
  43. description "dense forest all around"
  44. tags "west", "north", "south", "forest"
  45. location field
  46. - wallCave
  47. description "solid rock all around"
  48. tags "east", "north", "south", "rock"
  49. location cave

I made up the syntax myself, so it is safe to assume there are no standard tools to translate it to C. We will have to write our own code generator! Since this code generator will be a separate program, completely independent of our adventure program, we can write it in any language we like - not necessarily C. Here is one possible implementation, written in AWK:

object.awk
  1. BEGIN {
  2. count = 0;
  3. obj = "";
  4. if (pass == "c2") {
  5. print "\nOBJECT objs[] = {";
  6. }
  7. }
  8. /^- / {
  9. outputRecord(",");
  10. obj = $2;
  11. prop["description"] = "NULL";
  12. prop["tags"] = "";
  13. prop["location"] = "NULL";
  14. prop["destination"] = "NULL";
  15. }
  16. obj && /^[ \t]+[a-z]/ {
  17. name = $1;
  18. $1 = "";
  19. if (name in prop) {
  20. prop[name] = $0;
  21. }
  22. else if (pass == "c2") {
  23. print "#error \"" FILENAME " line " NR ": unknown attribute '" name "'\"";
  24. }
  25. }
  26. !obj && pass == (/^#include/ ? "c1" : "h") {
  27. print;
  28. }
  29. END {
  30. outputRecord("\n};");
  31. if (pass == "h") {
  32. print "\n#define endOfObjs\t(objs + " count ")";
  33. }
  34. }
  35. function outputRecord(separator)
  36. {
  37. if (obj) {
  38. if (pass == "h") {
  39. print "#define " obj "\t(objs + " count ")";
  40. }
  41. else if (pass == "c1") {
  42. print "static const char *tags" count "[] = {" prop["tags"] ", NULL};";
  43. }
  44. else if (pass == "c2") {
  45. print "\t{\t/* " count " = " obj " */";
  46. print "\t\t" prop["description"] ",";
  47. print "\t\ttags" count ",";
  48. print "\t\t" prop["location"] ",";
  49. print "\t\t" prop["destination"];
  50. print "\t}" separator;
  51. delete prop;
  52. }
  53. count++;
  54. }
  55. }

Explanation:

We actually need to call this AWK script three times to generate the C sources:

awk -v pass=h -f object.awk object.txt > object.h awk -v pass=c1 -f object.awk object.txt > object.c awk -v pass=c2 -f object.awk object.txt >> object.c

This will generate a new object.h and object.c, which should be identical (save for the layout) to the ones I wrote myself in the previous chapter.

As you can see, object.c is generated in two passes; for object.h, a single pass is sufficient. I could have made three separate AWK scripts, one for each pass, but instead I made a single big script combining all three, which seemed like the right thing to do considering the many similarities.

Our code generator script is very basic; it does no syntax checking on the attribute values. Most typos made in object.txt will pass through the generator without any errors. This is not a problem though: the syntax checks performed afterwards by the C compiler are sufficient. When compilation fails, the trick is to recognize your mistakes in the C code, then find and fix the original source in object.txt. To make this task just a little bit easier, the least we can do is let the code generator add some comments in the generated C code (see object.awk line 50). The AWK script may also pass errors over to the C compiler, by outputting a #error directive as part of the generated code (see line 25).

Notes:

Visualization

When it comes to choosing a domain-specific language, keep in mind that code generation is not its only benefit. A simple AWK script, similar to the one above, can be used to visualize a map of your virtual world by drawing a graph.

map.awk
  1. BEGIN { print "digraph map {"; }
  2. /^- / { outputEdges(); delete a; }
  3. /^[ \t]/ { a[$1] = $2; }
  4. END { outputEdges(); print "}"; }
  5. function outputEdges()
  6. {
  7. outputEdge(a["location"], a["destination"]);
  8. }
  9. function outputEdge(from, to)
  10. {
  11. if (from && to) print "\t" from " -> " to;
  12. }

Explanation:

Execute this script with the commands below, and object.txt will be converted into map.png; the picture you see on the right. Please note that this picture is not part of the game; it is a spoiler and should not be revealed to the player. It‘s here to help the developer spot mistakes in object.txt.

awk -f map.awk object.txt > map.gv dot -Tpng -o map.png map.gv

Notes:

Makefile

Calling AWK manually each time object.txt has been modified, soon becomes tedious. It is best to make these calls part of your build process. For example, a simple makefile for our adventure might look like this:

makefile
  1. all: success.txt src.zip map.png
  2. C = object.c misc.c noun.c location.c move.c inventory.c parsexec.c main.c
  3. H = object.h misc.h noun.h location.h move.h inventory.h parsexec.h
  4. success.txt: lilcave testscript.txt baseline.txt
  5. ./test.sh
  6. mv -f transcript.txt $@
  7. lilcave: $(C) $(H)
  8. gcc -Wall -Wextra -Wpedantic -Werror $(C) -o $@
  9. object.h: object.awk object.txt
  10. awk -v pass=h -f object.awk object.txt > $@
  11. object.c: object.awk object.txt
  12. awk -v pass=c1 -f object.awk object.txt > $@
  13. awk -v pass=c2 -f object.awk object.txt >> $@
  14. map.png: map.gv
  15. dot -Tpng -o $@ $<
  16. map.gv: map.awk object.txt
  17. awk -f map.awk object.txt > $@
  18. src.zip: $(C) $(H) object.txt makefile testscript.txt baseline.txt
  19. zip -rq $@ $^
  20. clean:
  21. $(RM) object.c object.h lilcave map.gv map.png transcript.txt success.txt src.zip

Explanation:

Now a single command make will do everything that is necessary to construct an executable and a map image.


⭳   Download source code 🌀   Run on Repl.it

Next chapter: 10. More attributes