Improve test coverage.
[open-adventure.git] / dungeon.c
1 /*
2  * The dungeon compiler. Turns adventure.text into a set of C initializers
3  * defining (mostly) invariant state.  A couple of slots are messed with
4  * at runtime.
5  */
6 #include "common.h"
7
8 #define LINESIZE 100
9 #define RTXSIZ 277
10 #define CLSMAX 12
11 #define LINSIZ 12600
12 #define TRNSIZ 5
13 #define TABSIZ 330
14 #define VRBSIZ 35
15 #define TRVSIZ 885
16 #define TOKLEN 5
17 #define HINTLEN 5
18
19 #include <stdio.h>
20 #include <stdlib.h>
21 #include <stdbool.h>
22 #include <unistd.h>
23
24 // Global variables for use in functions below that can gradually disappear as code is cleaned up
25 static long LNLENG;
26 static long LNPOSN;
27 static char INLINE[LINESIZE + 1];
28 static long OLDLOC;
29
30 // Storage for what comes out of the database
31 long LINUSE;
32 long TRVS;
33 long CLSSES;
34 long TRNVLS;
35 long TABNDX;
36 long HNTMAX;
37 long PTEXT[NOBJECTS + 1];
38 long RTEXT[RTXSIZ + 1];
39 long CTEXT[CLSMAX + 1];
40 long OBJSND[NOBJECTS + 1];
41 long OBJTXT[NOBJECTS + 1];
42 long STEXT[LOCSIZ + 1];
43 long LTEXT[LOCSIZ + 1];
44 long COND[LOCSIZ + 1];
45 long KEY[LOCSIZ + 1];
46 long LOCSND[LOCSIZ + 1];
47 long LINES[LINSIZ + 1];
48 long CVAL[CLSMAX + 1];
49 long TTEXT[TRNSIZ + 1];
50 long TRNVAL[TRNSIZ + 1];
51 long TRAVEL[TRVSIZ + 1];
52 long KTAB[TABSIZ + 1];
53 long ATAB[TABSIZ + 1];
54 long PLAC[NOBJECTS + 1];
55 long FIXD[NOBJECTS + 1];
56 long ACTSPK[VRBSIZ + 1];
57 long HINTS[HNTSIZ + 1][HINTLEN];
58
59
60 static bool is_set(long var, long position)
61 {
62     long mask = 1l << position;
63     bool result = (var & mask) == mask;
64     return (result);
65 }
66
67 static long GETTXT(long SKIP, long ONEWRD, long UPPER)
68 {
69     /*  Take characters from an input line and pack them into 30-bit words.
70      *  Skip says to skip leading blanks.  ONEWRD says stop if we come to a
71      *  blank.  UPPER says to map all letters to uppercase.  If we reach the
72      *  end of the line, the word is filled up with blanks (which encode as 0's).
73      *  If we're already at end of line when GETTXT is called, we return -1. */
74
75     long TEXT;
76     static long SPLITTING = -1;
77
78     if (LNPOSN != SPLITTING)
79         SPLITTING = -1;
80     TEXT = -1;
81     while (true) {
82         if (LNPOSN > LNLENG)
83             return (TEXT);
84         if ((!SKIP) || INLINE[LNPOSN] != 0)
85             break;
86         LNPOSN = LNPOSN + 1;
87     }
88
89     TEXT = 0;
90     for (int I = 1; I <= TOKLEN; I++) {
91         TEXT = TEXT * 64;
92         if (LNPOSN > LNLENG || (ONEWRD && INLINE[LNPOSN] == 0))
93             continue;
94         char current = INLINE[LNPOSN];
95         if (current < 63) {
96             SPLITTING = -1;
97             if (UPPER && current >= 37)
98                 current = current - 26;
99             TEXT = TEXT + current;
100             LNPOSN = LNPOSN + 1;
101             continue;
102         }
103         if (SPLITTING != LNPOSN) {
104             TEXT = TEXT + 63;
105             SPLITTING = LNPOSN;
106             continue;
107         }
108
109         TEXT = TEXT + current - 63;
110         SPLITTING = -1;
111         LNPOSN = LNPOSN + 1;
112     }
113
114     return (TEXT);
115 }
116
117 static void BUG(long NUM)
118 {
119
120     /*  The following conditions are currently considered fatal bugs.  Numbers < 20
121      *  are detected while reading the database; the others occur at "run time".
122      *  0       Message line > 70 characters
123      *  1       Null line in message
124      *  2       Too many words of messages
125      *  3       Too many travel options
126      *  4       Too many vocabulary words
127      *  5       Required vocabulary word not found
128      *  6       Too many RTEXT messages
129      *  7       Too many hints
130      *  8       Location has cond bit being set twice
131      *  9       Invalid section number in database
132      *  10      Too many locations
133      *  11      Too many class or turn messages
134      *  20      Special travel (500>L>300) exceeds goto list
135      *  21      Ran off end of vocabulary table
136      *  22      Vocabulary type (N/1000) not between 0 and 3
137      *  23      Intransitive action verb exceeds goto list
138      *  24      Transitive action verb exceeds goto list
139      *  25      Conditional travel entry with no alternative
140      *  26      Location has no travel entries
141      *  27      Hint number exceeds goto list
142      *  28      Invalid month returned by date function
143      *  29      Too many parameters given to SETPRM */
144
145     fprintf(stderr, "Fatal error %ld.  See source code for interpretation.\n", NUM);
146     exit(EXIT_FAILURE);
147 }
148
149 static void MAPLIN(FILE *OPENED)
150 {
151     /*  Read a line of input, from the specified input source,
152      *  translate the chars to integers in the range 0-126 and store
153      *  them in the common array "INLINE".  Integer values are as follows:
154      *     0   = space [ASCII CODE 40 octal, 32 decimal]
155      *    1-2  = !" [ASCII 41-42 octal, 33-34 decimal]
156      *    3-10 = '()*+,-. [ASCII 47-56 octal, 39-46 decimal]
157      *   11-36 = upper-case letters
158      *   37-62 = lower-case letters
159      *    63   = percent (%) [ASCII 45 octal, 37 decimal]
160      *   64-73 = digits, 0 through 9
161      *  Remaining characters can be translated any way that is convenient;
162      *  The "TYPE" routine below is used to map them back to characters when
163      *  necessary.  The above mappings are required so that certain special
164      *  characters are known to fit in 6 bits and/or can be easily spotted.
165      *  Array elements beyond the end of the line should be filled with 0,
166      *  and LNLENG should be set to the index of the last character.
167      *
168      *  If the data file uses a character other than space (e.g., tab) to
169      *  separate numbers, that character should also translate to 0.
170      *
171      *  This procedure may use the map1,map2 arrays to maintain static data for
172      *  the mapping.  MAP2(1) is set to 0 when the program starts
173      *  and is not changed thereafter unless the routines on this page choose
174      *  to do so. */
175
176     do {
177         if (NULL == fgets(INLINE + 1, sizeof(INLINE) - 1, OPENED)) {
178             printf("Failed fgets()\n");
179         }
180     } while (!feof(OPENED) && INLINE[1] == '#');
181
182     LNLENG = 0;
183     for (size_t i = 1; i < sizeof(INLINE) && INLINE[i] != 0; ++i) {
184         char val = INLINE[i];
185         INLINE[i] = ascii_to_advent[(unsigned)val];
186         if (INLINE[i] != 0)
187             LNLENG = i;
188     }
189     LNPOSN = 1;
190 }
191
192 static long GETNUM(FILE *source)
193 {
194     /*  Obtain the next integer from an input line.  If K>0, we first read a
195      *  new input line from a file; if K<0, we read a line from the keyboard;
196      *  if K=0 we use a line that has already been read (and perhaps partially
197      *  scanned).  If we're at the end of the line or encounter an illegal
198      *  character (not a digit, hyphen, or blank), we return 0. */
199
200     long DIGIT, GETNUM, SIGN;
201
202     if (source != NULL) MAPLIN(source);
203     GETNUM = 0;
204
205     while (INLINE[LNPOSN] == 0) {
206         if (LNPOSN > LNLENG) return (GETNUM);
207         ++LNPOSN;
208     }
209
210     if (INLINE[LNPOSN] != 9) {
211         SIGN = 1;
212     } else {
213         SIGN = -1;
214         LNPOSN = LNPOSN + 1;
215     }
216     while (!(LNPOSN > LNLENG || INLINE[LNPOSN] == 0)) {
217         DIGIT = INLINE[LNPOSN] - 64;
218         if (DIGIT < 0 || DIGIT > 9) {
219             GETNUM = 0;
220             break;
221         }
222         GETNUM = GETNUM * 10 + DIGIT;
223         LNPOSN = LNPOSN + 1;
224     }
225
226     GETNUM = GETNUM * SIGN;
227     LNPOSN = LNPOSN + 1;
228     return (GETNUM);
229 }
230
231 /*  Sections 1, 2, 5, 6, 10, 14.  Read messages and set up pointers. */
232 static void read_messages(FILE* database, long sect)
233 {
234     long KK = LINUSE;
235     while (true) {
236         long loc;
237         LINUSE = KK;
238         loc = GETNUM(database);
239         if (LNLENG >= LNPOSN + 70)BUG(0);
240         if (loc == -1) return;
241         if (LNLENG < LNPOSN)BUG(1);
242         do {
243             KK = KK + 1;
244             if (KK >= LINSIZ)BUG(2);
245             LINES[KK] = GETTXT(false, false, false);
246         } while (LINES[KK] != -1);
247         LINES[LINUSE] = KK;
248         if (loc == OLDLOC) continue;
249         OLDLOC = loc;
250         LINES[LINUSE] = -KK;
251         if (sect == 14) {
252             TRNVLS = TRNVLS + 1;
253             if (TRNVLS > TRNSIZ)BUG(11);
254             TTEXT[TRNVLS] = LINUSE;
255             TRNVAL[TRNVLS] = loc;
256             continue;
257         }
258         if (sect == 10) {
259             CLSSES = CLSSES + 1;
260             if (CLSSES > CLSMAX)BUG(11);
261             CTEXT[CLSSES] = LINUSE;
262             CVAL[CLSSES] = loc;
263             continue;
264         }
265         if (sect == 6) {
266             if (loc > RTXSIZ)BUG(6);
267             RTEXT[loc] = LINUSE;
268             continue;
269         }
270         if (sect == 5) {
271             if (loc > 0 && loc <= NOBJECTS)PTEXT[loc] = LINUSE;
272             continue;
273         }
274         if (loc > LOCSIZ)BUG(10);
275         if (sect == 1) {
276             LTEXT[loc] = LINUSE;
277             continue;
278         }
279
280         STEXT[loc] = LINUSE;
281     }
282 }
283
284 /*  The stuff for section 3 is encoded here.  Each "from-location" gets a
285  *  contiguous section of the "TRAVEL" array.  Each entry in travel is
286  *  newloc*1000 + KEYWORD (from section 4, motion verbs), and is negated if
287  *  this is the last entry for this location.  KEY(N) is the index in travel
288  *  of the first option at location N. */
289 static void read_section3_stuff(FILE* database)
290 {
291     long loc;
292     while ((loc = GETNUM(database)) != -1) {
293         long newloc = GETNUM(NULL);
294         long L;
295         if (KEY[loc] == 0) {
296             KEY[loc] = TRVS;
297         } else {
298             TRAVEL[TRVS - 1] = -TRAVEL[TRVS - 1];
299         }
300         while ((L = GETNUM(NULL)) != 0) {
301             TRAVEL[TRVS] = newloc * 1000 + L;
302             TRVS = TRVS + 1;
303             if (TRVS == TRVSIZ)BUG(3);
304         }
305         TRAVEL[TRVS - 1] = -TRAVEL[TRVS - 1];
306     }
307 }
308
309 /*  Here we read in the vocabulary.  KTAB(N) is the word number, ATAB(N) is
310  *  the corresponding word.  The -1 at the end of section 4 is left in KTAB
311  *  as an end-marker. */
312 static void read_vocabulary(FILE* database)
313 {
314     for (TABNDX = 1; TABNDX <= TABSIZ; TABNDX++) {
315         KTAB[TABNDX] = GETNUM(database);
316         if (KTAB[TABNDX] == -1) return;
317         ATAB[TABNDX] = GETTXT(true, true, true);
318     } /* end loop */
319     BUG(4);
320 }
321
322 /*  Read in the initial locations for each object.  Also the immovability info.
323  *  plac contains initial locations of objects.  FIXD is -1 for immovable
324  *  objects (including the snake), or = second loc for two-placed objects. */
325 static void read_initial_locations(FILE* database)
326 {
327     long OBJ;
328     while ((OBJ = GETNUM(database)) != -1) {
329         PLAC[OBJ] = GETNUM(NULL);
330         FIXD[OBJ] = GETNUM(NULL);
331     }
332 }
333
334 /*  Read default message numbers for action verbs, store in ACTSPK. */
335 static void read_action_verb_message_nr(FILE* database)
336 {
337     long verb;
338     while ((verb = GETNUM(database)) != -1) {
339         ACTSPK[verb] = GETNUM(NULL);
340     }
341 }
342
343 /*  Read info about available liquids and other conditions, store in COND. */
344 static void read_conditions(FILE* database)
345 {
346     long K;
347     while ((K = GETNUM(database)) != -1) {
348         long loc;
349         while ((loc = GETNUM(NULL)) != 0) {
350             if (is_set(COND[loc], K)) BUG(8);
351             COND[loc] = COND[loc] + (1l << K);
352         }
353     }
354 }
355
356
357 /*  Read data for hints. */
358 static void read_hints(FILE* database)
359 {
360     long K;
361     HNTMAX = 0;
362     while ((K = GETNUM(database)) != -1) {
363         if (K <= 0 || K > HNTSIZ)BUG(7);
364         for (int I = 1; I <= 4; I++) {
365             HINTS[K][I] = GETNUM(NULL);
366         } /* end loop */
367         HNTMAX = (HNTMAX > K ? HNTMAX : K);
368     }
369 }
370
371 /*  Read the sound/text info, store in OBJSND, OBJTXT, LOCSND. */
372 static void read_sound_text(FILE* database)
373 {
374     long K;
375     while ((K = GETNUM(database)) != -1) {
376         long KK = GETNUM(NULL);
377         long I = GETNUM(NULL);
378         if (I != 0) {
379             OBJSND[K] = (KK > 0 ? KK : 0);
380             OBJTXT[K] = (I > 0 ? I : 0);
381             continue;
382         }
383
384         LOCSND[K] = KK;
385     }
386 }
387
388
389 static int read_database(FILE* database)
390 {
391
392     /*  Clear out the various text-pointer arrays.  All text is stored in array
393      *  lines; each line is preceded by a word pointing to the next pointer (i.e.
394      *  the word following the end of the line).  The pointer is negative if this is
395      *  first line of a message.  The text-pointer arrays contain indices of
396      *  pointer-words in lines.  STEXT(N) is short description of location N.
397      *  LTEXT(N) is long description.  PTEXT(N) points to message for game.prop(N)=0.
398      *  Successive prop messages are found by chasing pointers.  RTEXT contains
399      *  section 6's stuff.  CTEXT(N) points to a player-class message.  TTEXT is for
400      *  section 14.  We also clear COND (see description of section 9 for details). */
401
402     for (int I = 1; I <= NOBJECTS; I++) {
403         PTEXT[I] = 0;
404         OBJSND[I] = 0;
405         OBJTXT[I] = 0;
406     }
407     for (int I = 1; I <= RTXSIZ; I++) {
408         RTEXT[I] = 0;
409     }
410     for (int I = 1; I <= CLSMAX; I++) {
411         CTEXT[I] = 0;
412     }
413     for (int I = 1; I <= LOCSIZ; I++) {
414         STEXT[I] = 0;
415         LTEXT[I] = 0;
416         COND[I] = 0;
417         KEY[I] = 0;
418         LOCSND[I] = 0;
419     }
420
421     LINUSE = 1;
422     TRVS = 1;
423     CLSSES = 0;
424     TRNVLS = 0;
425
426     /*  Start new data section.  Sect is the section number. */
427
428     while (true) {
429         long sect = GETNUM(database);
430         OLDLOC = -1;
431         switch (sect) {
432         case 0:
433             return (0);
434         case 1:
435             read_messages(database, sect);
436             break;
437         case 2:
438             read_messages(database, sect);
439             break;
440         case 3:
441             read_section3_stuff(database);
442             break;
443         case 4:
444             read_vocabulary(database);
445             break;
446         case 5:
447             read_messages(database, sect);
448             break;
449         case 6:
450             read_messages(database, sect);
451             break;
452         case 7:
453             read_initial_locations(database);
454             break;
455         case 8:
456             read_action_verb_message_nr(database);
457             break;
458         case 9:
459             read_conditions(database);
460             break;
461         case 10:
462             read_messages(database, sect);
463             break;
464         case 11:
465             read_hints(database);
466             break;
467         case 12:
468             break;
469         case 13:
470             read_sound_text(database);
471             break;
472         case 14:
473             read_messages(database, sect);
474             break;
475         default:
476             BUG(9);
477         }
478     }
479 }
480
481 /*  Finish constructing internal data format */
482
483 /*  Having read in the database, certain things are now constructed.
484  *  game.propS are set to zero.  We finish setting up COND by checking for
485  *  forced-motion travel entries.  The PLAC and FIXD arrays are used
486  *  to set up game.atloc(N) as the first object at location N, and
487  *  game.link(OBJ) as the next object at the same location as OBJ.
488  *  (OBJ>NOBJECTS indicates that game.fixed(OBJ-NOBJECTS)=LOC; game.link(OBJ) is
489  *  still the correct link to use.)  game.abbrev is zeroed; it controls
490  *  whether the abbreviated description is printed.  Counts modulo 5
491  *  unless "LOOK" is used. */
492
493 static void write_0d(FILE* header_file, long single, const char* varname)
494 {
495     fprintf(header_file, "LOCATION long %s INITIALIZE(= %ld);\n", varname, single);
496 }
497
498 static void write_1d(FILE* header_file, long array[], long dim, const char* varname)
499 {
500     fprintf(header_file, "LOCATION long %s[] INITIALIZE(= {\n", varname);
501     for (int i = 0; i < dim; ++i) {
502         if (i % 10 == 0) {
503             if (i > 0)
504                 fprintf(header_file, "\n");
505             fprintf(header_file, "  ");
506         }
507         fprintf(header_file, "%ld, ", array[i]);
508     }
509     fprintf(header_file, "\n});\n");
510 }
511
512 static void write_hints(FILE* header_file, long matrix[][HINTLEN], long dim1, long dim2, const char* varname)
513 {
514     fprintf(header_file, "LOCATION long %s[][%ld] INITIALIZE(= {\n", varname, dim2);
515     for (int i = 0; i < dim1; ++i) {
516         fprintf(header_file, "  {");
517         for (int j = 0; j < dim2; ++j) {
518             fprintf(header_file, "%ld, ", matrix[i][j]);
519         }
520         fprintf(header_file, "},\n");
521     }
522     fprintf(header_file, "});\n");
523 }
524
525 static void write_file(FILE* header_file)
526 {
527     int MAXDIE;
528     for (int i = 0; i <= 4; i++) {
529         long x = 2 * i + 81;
530         if (RTEXT[x] != 0)
531             MAXDIE = i + 1;
532     }
533
534
535     fprintf(header_file, "#ifndef DATABASE_H\n");
536     fprintf(header_file, "#define DATABASE_H\n");
537     fprintf(header_file, "\n");
538
539     fprintf(header_file, "#include \"common.h\"\n");
540     fprintf(header_file, "#define TABSIZ 330\n");
541     fprintf(header_file, "#define HNTSIZ 20\n");
542     fprintf(header_file, "#define TOKLEN %d\n", TOKLEN);
543     fprintf(header_file, "#define MAXDIE %d\n", MAXDIE);
544     fprintf(header_file, "\n");
545
546     fprintf(header_file, "\n");
547     fprintf(header_file, "#ifdef DEFINE_GLOBALS_FROM_INCLUDES\n");
548     fprintf(header_file, "#define LOCATION\n");
549     fprintf(header_file, "#define INITIALIZE(...) __VA_ARGS__\n");
550     fprintf(header_file, "#else\n");
551     fprintf(header_file, "#define LOCATION extern\n");
552     fprintf(header_file, "#define INITIALIZE(...)\n");
553     fprintf(header_file, "#endif\n");
554     fprintf(header_file, "\n");
555
556     // content variables
557     write_0d(header_file, TRNVLS, "TRNVLS");
558     write_0d(header_file, HNTMAX, "HNTMAX");
559     write_1d(header_file, OBJSND, NOBJECTS + 1, "OBJSND");
560     write_1d(header_file, OBJTXT, NOBJECTS + 1, "OBJTXT");
561     write_1d(header_file, COND, LOCSIZ + 1, "COND");
562     write_1d(header_file, KEY, LOCSIZ + 1, "KEY");
563     write_1d(header_file, LOCSND, LOCSIZ + 1, "LOCSND");
564     write_1d(header_file, CVAL, CLSMAX + 1, "CVAL");
565     write_1d(header_file, TRNVAL, TRNSIZ + 1, "TRNVAL");
566     write_1d(header_file, TRAVEL, TRVSIZ + 1, "TRAVEL");
567     write_1d(header_file, KTAB, TABSIZ + 1, "KTAB");
568     write_1d(header_file, ATAB, TABSIZ + 1, "ATAB");
569     write_1d(header_file, PLAC, NOBJECTS + 1, "PLAC");
570     write_1d(header_file, FIXD, NOBJECTS + 1, "FIXD");
571     write_1d(header_file, ACTSPK, VRBSIZ + 1, "ACTSPK");
572     write_hints(header_file, HINTS, HNTSIZ + 1, 5, "HINTS");
573
574     fprintf(header_file, "#undef LOCATION\n");
575     fprintf(header_file, "#undef INITIALIZE\n");
576     fprintf(header_file, "#endif\n");
577 }
578
579 int main(void)
580 {
581     FILE* database = fopen("adventure.text", "r");
582     read_database(database);
583     fclose(database);
584
585     FILE* header_file = fopen("database.h", "w");
586     write_file(header_file);
587     fclose(header_file);
588
589     return (EXIT_SUCCESS);
590 }