Improve test coverage.
[open-adventure.git] / misc.c
1 #include <unistd.h>
2 #include <stdlib.h>
3 #include <stdio.h>
4 #include <string.h>
5 #include <sys/time.h>
6 #include <ctype.h>
7
8 #include "advent.h"
9 #include "database.h"
10 #include "linenoise/linenoise.h"
11 #include "newdb.h"
12
13 char* xstrdup(const char* s)
14 {
15     char* ptr = strdup(s);
16     if (ptr == NULL) {
17         fprintf(stderr, "Out of memory!\n");
18         exit(EXIT_FAILURE);
19     }
20     return (ptr);
21 }
22
23 void packed_to_token(long packed, char token[6])
24 {
25     // Unpack and map back to ASCII.
26     for (int i = 0; i < 5; ++i) {
27         char advent = (packed >> i * 6) & 63;
28         token[4 - i] = advent_to_ascii[(int) advent];
29     }
30
31     // Ensure the last character is \0.
32     token[5] = '\0';
33
34     // Replace trailing whitespace with \0.
35     for (int i = 4; i >= 0; --i) {
36         if (token[i] == ' ' || token[i] == '\t')
37             token[i] = '\0';
38         else
39             break;
40     }
41 }
42
43 /*  I/O routines (SPEAK, PSPEAK, RSPEAK, SETPRM, GETIN, YES) */
44
45 void newspeak(const char* msg)
46 {
47     // Do nothing if we got a null pointer.
48     if (msg == NULL)
49         return;
50
51     // Do nothing if we got an empty string.
52     if (strlen(msg) == 0)
53         return;
54
55     // Print a newline if the global game.blklin says to.
56     if (game.blklin == true)
57         printf("\n");
58
59     // Create a copy of our string, so we can edit it.
60     char* copy = xstrdup(msg);
61
62     // Staging area for stringified parameters.
63     char parameters[5][100]; // FIXME: to be replaced with dynamic allocation
64
65     // Handle format specifiers (including the custom %C, %L, %S) by adjusting the parameter accordingly, and replacing the specifier with %s.
66     int pi = 0; // parameter index
67     for (int i = 0; i < (int)strlen(msg); ++i) {
68         if (msg[i] == '%') {
69             ++pi;
70
71             // Integer specifier. In order to accommodate the fact that PARMS can have both legitimate integers *and* packed tokens, stringify everything. Future work may eliminate the need for this.
72             if (msg[i + 1] == 'd') {
73                 copy[i + 1] = 's';
74                 sprintf(parameters[pi], "%ld", PARMS[pi]);
75             }
76
77             // Unmodified string specifier.
78             if (msg[i + 1] == 's') {
79                 packed_to_token(PARMS[pi], parameters[pi]);
80             }
81
82             // Singular/plural specifier.
83             if (msg[i + 1] == 'S') {
84                 copy[i + 1] = 's';
85                 if (PARMS[pi - 1] > 1) { // look at the *previous* parameter (which by necessity must be numeric)
86                     sprintf(parameters[pi], "%s", "s");
87                 } else {
88                     sprintf(parameters[pi], "%s", "");
89                 }
90             }
91
92             // All-lowercase specifier.
93             if (msg[i + 1] == 'L') {
94                 copy[i + 1] = 's';
95                 packed_to_token(PARMS[pi], parameters[pi]);
96                 for (int j = 0; j < (int)strlen(parameters[pi]); ++j) {
97                     parameters[pi][j] = tolower(parameters[pi][j]);
98                 }
99             }
100
101             // First char uppercase, rest lowercase.
102             if (msg[i + 1] == 'C') {
103                 copy[i + 1] = 's';
104                 packed_to_token(PARMS[pi], parameters[pi]);
105                 for (int j = 0; j < (int)strlen(parameters[pi]); ++j) {
106                     parameters[pi][j] = tolower(parameters[pi][j]);
107                 }
108                 parameters[pi][0] = toupper(parameters[pi][0]);
109             }
110         }
111     }
112
113     // Render the final string.
114     char rendered[2000]; // FIXME: to be replaced with dynamic allocation
115     sprintf(rendered, copy, parameters[1], parameters[2], parameters[3], parameters[4]); // FIXME: to be replaced with vsprintf()
116
117     // Print the message.
118     printf("%s\n", rendered);
119
120     free(copy);
121 }
122
123 void PSPEAK(vocab_t msg, int skip)
124 /*  Find the skip+1st message from msg and print it.  msg should be
125  *  the index of the inventory message for object.  (INVEN+N+1 message
126  *  is game.prop=N message). */
127 {
128     if (skip >= 0)
129         newspeak(object_descriptions[msg].longs[skip]);
130     else
131         newspeak(object_descriptions[msg].inventory);
132 }
133
134 void RSPEAK(vocab_t i)
135 /* Print the i-th "random" message (section 6 of database). */
136 {
137     newspeak(arbitrary_messages[i]);
138 }
139
140 void SETPRM(long first, long p1, long p2)
141 /*  Stores parameters into the PRMCOM parms array for use by speak.  P1 and P2
142  *  are stored into PARMS(first) and PARMS(first+1). */
143 {
144     if (first >= MAXPARMS)
145         BUG(29);
146     else {
147         PARMS[first] = p1;
148         PARMS[first + 1] = p2;
149     }
150 }
151
152 bool GETIN(FILE *input,
153            long *pword1, long *pword1x,
154            long *pword2, long *pword2x)
155 /*  Get a command from the adventurer.  Snarf out the first word, pad it with
156  *  blanks, and return it in WORD1.  Chars 6 thru 10 are returned in WORD1X, in
157  *  case we need to print out the whole word in an error message.  Any number of
158  *  blanks may follow the word.  If a second word appears, it is returned in
159  *  WORD2 (chars 6 thru 10 in WORD2X), else WORD2 is -1. */
160 {
161     long junk;
162
163     for (;;) {
164         if (game.blklin)
165             TYPE0();
166         if (!MAPLIN(input))
167             return false;
168         *pword1 = GETTXT(true, true, true);
169         if (game.blklin && *pword1 < 0)
170             continue;
171         *pword1x = GETTXT(false, true, true);
172         do {
173             junk = GETTXT(false, true, true);
174         } while
175         (junk > 0);
176         *pword2 = GETTXT(true, true, true);
177         *pword2x = GETTXT(false, true, true);
178         do {
179             junk = GETTXT(false, true, true);
180         } while
181         (junk > 0);
182         if (GETTXT(true, true, true) <= 0)
183             return true;
184         RSPEAK(TWO_WORDS);
185     }
186 }
187
188 long YES(FILE *input, vocab_t x, vocab_t y, vocab_t z)
189 /*  Print message X, wait for yes/no answer.  If yes, print Y and return true;
190  *  if no, print Z and return false. */
191 {
192     token_t reply, junk1, junk2, junk3;
193
194     for (;;) {
195         RSPEAK(x);
196         GETIN(input, &reply, &junk1, &junk2, &junk3);
197         if (reply == MAKEWD(250519) || reply == MAKEWD(25)) {
198             RSPEAK(y);
199             return true;
200         }
201         if (reply == MAKEWD(1415) || reply == MAKEWD(14)) {
202             RSPEAK(z);
203             return false;
204         }
205         RSPEAK(PLEASE_ANSWER);
206     }
207 }
208
209 /*  Line-parsing routines (GETTXT, MAKEWD, PUTTXT, SHFTXT, TYPE0) */
210
211 long GETTXT(bool skip, bool onewrd, bool upper)
212 /*  Take characters from an input line and pack them into 30-bit words.
213  *  Skip says to skip leading blanks.  ONEWRD says stop if we come to a
214  *  blank.  UPPER says to map all letters to uppercase.  If we reach the
215  *  end of the line, the word is filled up with blanks (which encode as 0's).
216  *  If we're already at end of line when TEXT is called, we return -1. */
217 {
218     long text;
219     static long splitting = -1;
220
221     if (LNPOSN != splitting)
222         splitting = -1;
223     text = -1;
224     while (true) {
225         if (LNPOSN > LNLENG)
226             return (text);
227         if ((!skip) || INLINE[LNPOSN] != 0)
228             break;
229         ++LNPOSN;
230     }
231
232     text = 0;
233     for (int I = 1; I <= TOKLEN; I++) {
234         text = text * 64;
235         if (LNPOSN > LNLENG || (onewrd && INLINE[LNPOSN] == 0))
236             continue;
237         char current = INLINE[LNPOSN];
238         if (current < ascii_to_advent['%']) {
239             splitting = -1;
240             if (upper && current >= ascii_to_advent['a'])
241                 current = current - 26;
242             text = text + current;
243             ++LNPOSN;
244             continue;
245         }
246         if (splitting != LNPOSN) {
247             text = text + ascii_to_advent['%'];
248             splitting = LNPOSN;
249             continue;
250         }
251
252         text = text + current - ascii_to_advent['%'];
253         splitting = -1;
254         ++LNPOSN;
255     }
256
257     return text;
258 }
259
260 token_t MAKEWD(long letters)
261 /*  Combine TOKLEN (currently 5) uppercase letters (represented by
262  *  pairs of decimal digits in lettrs) to form a 30-bit value matching
263  *  the one that GETTXT would return given those characters plus
264  *  trailing blanks.  Caution: lettrs will overflow 31 bits if
265  *  5-letter word starts with V-Z.  As a kludgey workaround, you can
266  *  increment a letter by 5 by adding 50 to the next pair of
267  *  digits. */
268 {
269     long i = 1, word = 0;
270
271     for (long k = letters; k != 0; k = k / 100) {
272         word = word + i * (MOD(k, 50) + 10);
273         i = i * 64;
274         if (MOD(k, 100) > 50)word = word + i * 5;
275     }
276     i = 64L * 64L * 64L * 64L * 64L / i;
277     word = word * i;
278     return word;
279 }
280
281 void TYPE0(void)
282 /*  Type a blank line.  This procedure is provided as a convenience for callers
283  *  who otherwise have no use for MAPCOM. */
284 {
285     long temp;
286
287     temp = LNLENG;
288     LNLENG = 0;
289     TYPE();
290     LNLENG = temp;
291     return;
292 }
293
294 /*  Data structure  routines */
295
296 long VOCAB(long id, long init)
297 /*  Look up ID in the vocabulary (ATAB) and return its "definition" (KTAB), or
298  *  -1 if not found.  If INIT is positive, this is an initialisation call setting
299  *  up a keyword variable, and not finding it constitutes a bug.  It also means
300  *  that only KTAB values which taken over 1000 equal INIT may be considered.
301  *  (Thus "STEPS", which is a motion verb as well as an object, may be located
302  *  as an object.)  And it also means the KTAB value is taken modulo 1000. */
303 {
304     long lexeme;
305
306     for (long i = 1; i <= TABSIZ; i++) {
307         if (KTAB[i] == -1) {
308             lexeme = -1;
309             if (init < 0)
310                 return (lexeme);
311             BUG(5);
312         }
313         if (init >= 0 && KTAB[i] / 1000 != init)
314             continue;
315         if (ATAB[i] == id) {
316             lexeme = KTAB[i];
317             if (init >= 0)
318                 lexeme = MOD(lexeme, 1000);
319             return (lexeme);
320         }
321     }
322     BUG(21);
323 }
324
325 void JUGGLE(long object)
326 /*  Juggle an object by picking it up and putting it down again, the purpose
327  *  being to get the object to the front of the chain of things at its loc. */
328 {
329     long i, j;
330
331     i = game.place[object];
332     j = game.fixed[object];
333     MOVE(object, i);
334     MOVE(object + NOBJECTS, j);
335 }
336
337 void MOVE(long object, long where)
338 /*  Place any object anywhere by picking it up and dropping it.  May
339  *  already be toting, in which case the carry is a no-op.  Mustn't
340  *  pick up objects which are not at any loc, since carry wants to
341  *  remove objects from game.atloc chains. */
342 {
343     long from;
344
345     if (object > NOBJECTS)
346         from = game.fixed[object - NOBJECTS];
347     else
348         from = game.place[object];
349     if (from != NOWHERE && from != CARRIED && !SPECIAL(from))
350         CARRY(object, from);
351     DROP(object, where);
352 }
353
354 long PUT(long object, long where, long pval)
355 /*  PUT is the same as MOVE, except it returns a value used to set up the
356  *  negated game.prop values for the repository objects. */
357 {
358     MOVE(object, where);
359     return (-1) - pval;;
360 }
361
362 void CARRY(long object, long where)
363 /*  Start toting an object, removing it from the list of things at its former
364  *  location.  Incr holdng unless it was already being toted.  If object>NOBJECTS
365  *  (moving "fixed" second loc), don't change game.place or game.holdng. */
366 {
367     long temp;
368
369     if (object <= NOBJECTS) {
370         if (game.place[object] == CARRIED)
371             return;
372         game.place[object] = CARRIED;
373         ++game.holdng;
374     }
375     if (game.atloc[where] == object) {
376         game.atloc[where] = game.link[object];
377         return;
378     }
379     temp = game.atloc[where];
380     while (game.link[temp] != object) {
381         temp = game.link[temp];
382     }
383     game.link[temp] = game.link[object];
384 }
385
386 void DROP(long object, long where)
387 /*  Place an object at a given loc, prefixing it onto the game.atloc list.  Decr
388  *  game.holdng if the object was being toted. */
389 {
390     if (object > NOBJECTS)
391         game.fixed[object - NOBJECTS] = where;
392     else {
393         if (game.place[object] == CARRIED)
394             --game.holdng;
395         game.place[object] = where;
396     }
397     if (where <= 0)
398         return;
399     game.link[object] = game.atloc[where];
400     game.atloc[where] = object;
401 }
402
403 long ATDWRF(long where)
404 /*  Return the index of first dwarf at the given location, zero if no dwarf is
405  *  there (or if dwarves not active yet), -1 if all dwarves are dead.  Ignore
406  *  the pirate (6th dwarf). */
407 {
408     long at;
409
410     at = 0;
411     if (game.dflag < 2)
412         return (at);
413     at = -1;
414     for (long i = 1; i <= NDWARVES - 1; i++) {
415         if (game.dloc[i] == where)
416             return i;
417         if (game.dloc[i] != 0)
418             at = 0;
419     }
420     return (at);
421 }
422
423 /*  Utility routines (SETBIT, TSTBIT, set_seed, get_next_lcg_value,
424  *  randrange, RNDVOC, BUG) */
425
426 long SETBIT(long bit)
427 /*  Returns 2**bit for use in constructing bit-masks. */
428 {
429     return (1 << bit);
430 }
431
432 bool TSTBIT(long mask, int bit)
433 /*  Returns true if the specified bit is set in the mask. */
434 {
435     return (mask & (1 << bit)) != 0;
436 }
437
438 void set_seed(long seedval)
439 /* Set the LCG seed */
440 {
441     game.lcg_x = (unsigned long) seedval % game.lcg_m;
442 }
443
444 unsigned long get_next_lcg_value(void)
445 /* Return the LCG's current value, and then iterate it. */
446 {
447     unsigned long old_x = game.lcg_x;
448     game.lcg_x = (game.lcg_a * game.lcg_x + game.lcg_c) % game.lcg_m;
449     return old_x;
450 }
451
452 long randrange(long range)
453 /* Return a random integer from [0, range). */
454 {
455     return range * get_next_lcg_value() / game.lcg_m;
456 }
457
458 long RNDVOC(long second, long force)
459 /*  Searches the vocabulary ATAB for a word whose second character is
460  *  char, and changes that word such that each of the other four
461  *  characters is a random letter.  If force is non-zero, it is used
462  *  as the new word.  Returns the new word. */
463 {
464     long rnd = force;
465
466     if (rnd == 0) {
467         for (int i = 1; i <= 5; i++) {
468             long j = 11 + randrange(26);
469             if (i == 2)
470                 j = second;
471             rnd = rnd * 64 + j;
472         }
473     }
474
475     long div = 64L * 64L * 64L;
476     for (int i = 1; i <= TABSIZ; i++) {
477         if (MOD(ATAB[i] / div, 64L) == second) {
478             ATAB[i] = rnd;
479             break;
480         }
481     }
482
483     return rnd;
484 }
485
486 void BUG(long num)
487 /*  The following conditions are currently considered fatal bugs.  Numbers < 20
488  *  are detected while reading the database; the others occur at "run time".
489  *      0       Message line > 70 characters
490  *      1       Null line in message
491  *      2       Too many words of messages
492  *      3       Too many travel options
493  *      4       Too many vocabulary words
494  *      5       Required vocabulary word not found
495  *      6       Too many RTEXT messages
496  *      7       Too many hints
497  *      8       Location has cond bit being set twice
498  *      9       Invalid section number in database
499  *      10      Too many locations
500  *      11      Too many class or turn messages
501  *      20      Special travel (500>L>300) exceeds goto list
502  *      21      Ran off end of vocabulary table
503  *      22      Vocabulary type (N/1000) not between 0 and 3
504  *      23      Intransitive action verb exceeds goto list
505  *      24      Transitive action verb exceeds goto list
506  *      25      Conditional travel entry with no alternative
507  *      26      Location has no travel entries
508  *      27      Hint number exceeds goto list
509  *      28      Invalid month returned by date function
510  *      29      Too many parameters given to SETPRM */
511 {
512
513     printf("Fatal error %ld.  See source code for interpretation.\n", num);
514     exit(0);
515 }
516
517 /*  Machine dependent routines (MAPLIN, TYPE, SAVEIO) */
518
519 bool MAPLIN(FILE *fp)
520 {
521     bool eof;
522
523     /* Read a line of input, from the specified input source.
524      * This logic is complicated partly because it has to serve
525      * several cases with different requirements and partly because
526      * of a quirk in linenoise().
527      *
528      * The quirk shows up when you paste a test log from the clipboard
529      * to the program's command prompt.  While fgets (as expected)
530      * consumes it a line at a time, linenoise() returns the first
531      * line and discards the rest.  Thus, there needs to be an
532      * editline (-s) option to fall back to fgets while still
533      * prompting.  Note that linenoise does behave properly when
534      * fed redirected stdin.
535      *
536      * The logging is a bit of a mess because there are two distinct cases
537      * in which you want to echo commands.  One is when shipping them to
538      * a log under the -l option, in which case you want to suppress
539      * prompt generation (so test logs are unadorned command sequences).
540      * On the other hand, if you redirected stdin and are feeding the program
541      * a logfile, you *do* want prompt generation - it makes checkfiles
542      * easier to read when the commands are marked by a preceding prompt.
543      */
544     do {
545         if (!editline) {
546             if (prompt)
547                 fputs("> ", stdout);
548             IGNORE(fgets(rawbuf, sizeof(rawbuf) - 1, fp));
549             eof = (feof(fp));
550         } else {
551             char *cp = linenoise("> ");
552             eof = (cp == NULL);
553             if (!eof) {
554                 strncpy(rawbuf, cp, sizeof(rawbuf) - 1);
555                 linenoiseHistoryAdd(rawbuf);
556                 strncat(rawbuf, "\n", sizeof(rawbuf) - strlen(rawbuf) - 1);
557                 linenoiseFree(cp);
558             }
559         }
560     } while
561     (!eof && rawbuf[0] == '#');
562     if (eof) {
563         if (logfp && fp == stdin)
564             fclose(logfp);
565         return false;
566     } else {
567         FILE *efp = NULL;
568         if (logfp && fp == stdin)
569             efp = logfp;
570         else if (!isatty(0))
571             efp = stdout;
572         if (efp != NULL) {
573             if (prompt && efp == stdout)
574                 fputs("> ", efp);
575             IGNORE(fputs(rawbuf, efp));
576         }
577         strcpy(INLINE + 1, rawbuf);
578         /*  translate the chars to integers in the range 0-126 and store
579          *  them in the common array "INLINE".  Integer values are as follows:
580          *     0   = space [ASCII CODE 40 octal, 32 decimal]
581          *    1-2  = !" [ASCII 41-42 octal, 33-34 decimal]
582          *    3-10 = '()*+,-. [ASCII 47-56 octal, 39-46 decimal]
583          *   11-36 = upper-case letters
584          *   37-62 = lower-case letters
585          *    63   = percent (%) [ASCII 45 octal, 37 decimal]
586          *   64-73 = digits, 0 through 9
587          *  Remaining characters can be translated any way that is convenient;
588          *  The "TYPE" routine below is used to map them back to characters when
589          *  necessary.  The above mappings are required so that certain special
590          *  characters are known to fit in 6 bits and/or can be easily spotted.
591          *  Array elements beyond the end of the line should be filled with 0,
592          *  and LNLENG should be set to the index of the last character.
593          *
594          *  If the data file uses a character other than space (e.g., tab) to
595          *  separate numbers, that character should also translate to 0.
596          *
597          *  This procedure may use the map1,map2 arrays to maintain static data for
598          *  the mapping.  MAP2(1) is set to 0 when the program starts
599          *  and is not changed thereafter unless the routines on this page choose
600          *  to do so. */
601         LNLENG = 0;
602         for (long i = 1; i <= (long)sizeof(INLINE) && INLINE[i] != 0; i++) {
603             long val = INLINE[i];
604             INLINE[i] = ascii_to_advent[val];
605             if (INLINE[i] != 0)
606                 LNLENG = i;
607         }
608         LNPOSN = 1;
609         return true;
610     }
611 }
612
613 void TYPE(void)
614 /*  Type the first "LNLENG" characters stored in inline, mapping them
615  *  from integers to text per the rules described above.  INLINE
616  *  may be changed by this routine. */
617 {
618     long i;
619
620     if (LNLENG == 0) {
621         printf("\n");
622         return;
623     }
624
625     for (i = 1; i <= LNLENG; i++) {
626         INLINE[i] = advent_to_ascii[(int) INLINE[i]];
627     }
628     INLINE[LNLENG + 1] = 0;
629     printf("%s\n", INLINE + 1);
630     return;
631 }
632
633 void DATIME(long* d, long* t)
634 {
635     struct timeval tv;
636     gettimeofday(&tv, NULL);
637     *d = (long) tv.tv_sec;
638     *t = (long) tv.tv_usec;
639 }
640
641 /* end */