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

How to program a text adventure in C

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 rise 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 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 #include <stdio.h> #include "object.h" typedef struct object { const char *description; const char **tags; struct object *location; struct object *destination; } OBJECT; extern OBJECT objs[]; - field description "an open field" tags "field" - cave description "a little cave" tags "cave" - silver description "a silver coin" tags "silver", "coin", "silver coin" location field - gold description "a gold coin" tags "gold", "coin", "gold coin" location cave - guard description "a burly guard" tags "guard", "burly guard" location field - player description "yourself" tags "yourself" location field - intoCave description "a cave entrance to the east" tags "east", "entrance" location field destination cave - exitCave description "a way out to the west" tags "west", "out" location cave destination field - wallField description "dense forest all around" tags "west", "north", "south", "forest" location field - wallCave description "solid rock all around" tags "east", "north", "south", "rock" 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 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 BEGIN { count = 0; obj = ""; if (pass == "c2") { print "\nOBJECT objs[] = {"; } } /^- / { outputRecord(","); obj = $2; prop["description"] = "NULL"; prop["tags"] = ""; prop["location"] = "NULL"; prop["destination"] = "NULL"; } obj && /^[ \t]+[a-z]/ { name = $1; $1 = ""; if (name in prop) { prop[name] = $0; } else if (pass == "c2") { print "#error \"" FILENAME " line " NR ": unknown attribute '" name "'\""; } } !obj && pass == (/^#include/ ? "c1" : "h") { print; } END { outputRecord("\n};"); if (pass == "h") { print "\n#define endOfObjs\t(objs + " count ")"; } } function outputRecord(separator) { if (obj) { if (pass == "h") { print "#define " obj "\t(objs + " count ")"; } else if (pass == "c1") { print "static const char *tags" count "[] = {" prop["tags"] ", NULL};"; } else if (pass == "c2") { print "\t{\t/* " count " = " obj " */"; print "\t\t" prop["description"] ","; print "\t\ttags" count ","; print "\t\t" prop["location"] ","; print "\t\t" prop["destination"]; print "\t}" separator; delete prop; } count++; } }

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 52). 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 2 3 4 5 6 7 8 9 10 BEGIN { print "digraph map {"; } /^- / { outputEdge(); location = destination = ""; } $1 == "location" { location = $2; } $1 == "destination" { destination = $2; } END { outputEdge(); print "}"; } function outputEdge() { if (location && destination) print "\t" location " -> " destination; }

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 not something the player will (should) ever see. It is merely a developer's tool, making it easier to 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 all: lilcave map.png C = object.c misc.c inventory.c location.c main.c H = object.h misc.h inventory.h location.h lilcave: $(C) $(H) gcc $(C) -o $@ object.h: object.awk object.txt awk -v pass=h -f object.awk object.txt > $@ object.c: object.awk object.txt awk -v pass=c1 -f object.awk object.txt > $@ awk -v pass=c2 -f object.awk object.txt >> $@ map.png: map.gv dot -Tpng -o $@ $< map.gv: map.awk object.txt awk -f map.awk object.txt > $@

Now a single command make will do everything that is necessary to construct a new executable (named lilcave here) and a new map image. Notice I put not only object.txt, but also object.awk in the dependency rules (lines 9 and 12). Although technically, object.awk is more of a development tool than it is a source file, it is still subject to change; already so in the next three chapters.


Next chapter: 10. More attributes