Description
So far, the adventure has 10 objects. Each object consists of 5 attributes (the fields in the struct OBJECT). It is likely that a real-life adventure has hundreds, even thousands of objects, and the number of attributes per object increases. Keeping such a large list of objects and attributes in its current form would be difficult.
For example, when I added the wallField and wallCave objects previously, I had to do it in three different places: once in object.h (like #define) and twice in object.c (an element in the array objs, and a separate matrix for the tags). This is clumsy and error-prone.
Instead of keeping object.h and object.c by hand, I will start generating the files from a single source that best suits my needs. This new source file could be in any language I prefer (usually a domain-specific language), as long as I have the tools to convert it back to C. The following is a simple example. Consider the following layout to organize objects:
+----------------------------------------------------+ | | | +--------------------------------------------+ | | | | | | | Raw C code (declarations) | | | | | | | +--------------------------------------------+ | | +--------------------------------------------+ | | | | | | | | | | | - ObjectName | | | | AttributeName AttributeValue | | | | AttributeName AttributeValue | | | | ... | | | | - ObjectName | | | | AttributeName AttributeValue | | | | AttributeName AttributeValue | | | | ... | | | | - ... | | | | | | | +--------------------------------------------+ | | | +----------------------------------------------------+
Based on the objects I've collected so far, I can build the following source file. The file name does not matter much; I simply called object.txt, to make it clear that it is related to object.h and object.c.
+------------+ | object.txt | +------------+
#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 | +------------+
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++;
}
}
We actually need to call this AWK script three times to generate the C sources:
awk -v pass=h -f object.awk object.txt > ../include/object.h
awk -v pass=c1 -f object.awk object.txt > ../src/object.c
awk -v pass=c2 -f object.awk object.txt >> ../src/object.c
This will generate a new object.h and object.c, which should be identical (save for layout).
object.c is generated in two steps; for object.h, a single pass is sufficient. I could have created three separate AWK scripts, one for each pass, but instead I created one big script that combined all three, which seemed like the right thing to do considering the many similarities.
The code generation script is very simple; does not check the syntax on attribute values. Most typos made in object.txt pass through the generator without errors. This is not a problem: the syntax checks made later by the C compiler are sufficient. When compiling fails, the trick is to recognize errors in the C code, then find and correct the original source in object.txt. To make this task a little easier, the least I could do was to let the code generator add some comments in the generated C code (see object.awk).
The AWK script can also pass errors to the C compiler, issuing a #error directive as part of the generated code.
Notes:
-
Important: at the time, it did not make any manual changes to object.h and object.c; these would only be
discarded by the code generation process. -
This combination of languages (C, AWK and a domain-specific language) may seem initially confusing. Moreover,
this is still relatively simple compared to the combination of server-side and client-side techniques with
which the average web developer is confronted. -
Because object.txt is converted to simple C code, compiled and linked with the other modules, all of its data
will be part of the final executable. Like any source file, object.txt should not be present when the
executable is running (for example, when someone is playing). This, of course, is just a matter of choice. It
is very well possible to keep object.txt regardless of the executable and make the executable import data from
object.txt at runtime. This is particularly interesting when you build an adventures development system. Keep
in mind that it will make the code slightly more complicated; requires more effort in analyzing object.txt,
since there will not be any C compiler to support me.
Visualization
When it comes to choosing a domain-specific language, keep in mind that code generation is not its only advantage. A simple AWK script, similar to the one above, can be used to display a virtual world map by drawing a chart.
Currently under development...