Test coverage -- fix regression
[open-adventure.git] / make_dungeon.py
1 #!/usr/bin/python3
2
3 # This is the new open-adventure dungeon generator. It'll eventually
4 # replace the existing dungeon.c It currently outputs a .h and .c pair
5 # for C code.
6 #
7 # The nontrivial part of this is the compilation of the YAML for
8 # movement rules to the travel array that's actually used by
9 # playermove().  This program first compiles the YAML to a form
10 # identical to the data in section 3 of the old adventure.text file,
11 # then a second stage unpacks that data into the travel array.
12 #
13 # Here are the rules of the intermediate form:
14 #
15 # Each row of data contains a location number (X), a second
16 # location number (Y), and a list of motion numbers (see section 4).
17 # each motion represents a verb which will go to Y if currently at X.
18 # Y, in turn, is interpreted as follows.  Let M=Y/1000, N=Y mod 1000.
19 #               If N<=300       it is the location to go to.
20 #               If 300<N<=500   N-300 is used in a computed goto to
21 #                                       a section of special code.
22 #               If N>500        message N-500 from section 6 is printed,
23 #                                       and he stays wherever he is.
24 # Meanwhile, M specifies the conditions on the motion.
25 #               If M=0          it's unconditional.
26 #               If 0<M<100      it is done with M% probability.
27 #               If M=100        unconditional, but forbidden to dwarves.
28 #               If 100<M<=200   he must be carrying object M-100.
29 #               If 200<M<=300   must be carrying or in same room as M-200.
30 #               If 300<M<=400   game.prop(M % 100) must *not* be 0.
31 #               If 400<M<=500   game.prop(M % 100) must *not* be 1.
32 #               If 500<M<=600   game.prop(M % 100) must *not* be 2, etc.
33 # If the condition (if any) is not met, then the next *different*
34 # "destination" value is used (unless it fails to meet *its* conditions,
35 # in which case the next is found, etc.).  Typically, the next dest will
36 # be for one of the same verbs, so that its only use is as the alternate
37 # destination for those verbs.  For instance:
38 #               15      110022  29      31      34      35      23      43
39 #               15      14      29
40 # This says that, from loc 15, any of the verbs 29, 31, etc., will take
41 # him to 22 if he's carrying object 10, and otherwise will go to 14.
42 #               11      303008  49
43 #               11      9       50
44 # This says that, from 11, 49 takes him to 8 unless game.prop(3)=0, in which
45 # case he goes to 9.  Verb 50 takes him to 9 regardless of game.prop(3).
46
47 import sys, yaml
48
49 yaml_name = "adventure.yaml"
50 h_name = "dungeon.h"
51 c_name = "dungeon.c"
52
53 statedefines = ""
54
55 h_template = """/* Generated from adventure.yaml - do not hand-hack! */
56 #ifndef DUNGEON_H
57 #define DUNGEON_H
58
59 #include <stdio.h>
60 #include <stdbool.h>
61
62 #define SILENT  -1      /* no sound */
63
64 /* Symbols for cond bits */
65 #define COND_LIT        0       /* Light */
66 #define COND_OILY       1       /* If bit 2 is on: on for oil, off for water */
67 #define COND_FLUID      2       /* Liquid asset, see bit 1 */
68 #define COND_NOARRR     3       /* Pirate doesn't go here unless following */
69 #define COND_NOBACK     4       /* Cannot use "back" to move away */
70 #define COND_ABOVE      5
71 #define COND_DEEP       6       /* Deep - e.g where dwarves are active */
72 #define COND_FOREST     7       /* In the forest */
73 #define COND_FORCED     8       /* Only one way in or out of here */
74 /* Bits past 10 indicate areas of interest to "hint" routines */
75 #define COND_HBASE      10      /* Base for location hint bits */
76 #define COND_HCAVE      11      /* Trying to get into cave */
77 #define COND_HBIRD      12      /* Trying to catch bird */
78 #define COND_HSNAKE     13      /* Trying to deal with snake */
79 #define COND_HMAZE      14      /* Lost in maze */
80 #define COND_HDARK      15      /* Pondering dark room */
81 #define COND_HWITT      16      /* At Witt's End */
82 #define COND_HCLIFF     17      /* Cliff with urn */
83 #define COND_HWOODS     18      /* Lost in forest */
84 #define COND_HOGRE      19      /* Trying to deal with ogre */
85 #define COND_HJADE      20      /* Found all treasures except jade */
86
87 typedef struct {{
88   const char** strs;
89   const int n;
90 }} string_group_t;
91
92 typedef struct {{
93   const string_group_t words;
94   const char* inventory;
95   int plac, fixd;
96   bool is_treasure;
97   const char** descriptions;
98   const char** sounds;
99   const char** texts;
100   const char** changes;
101 }} object_t;
102
103 typedef struct {{
104   const char* small;
105   const char* big;
106 }} descriptions_t;
107
108 typedef struct {{
109   descriptions_t description;
110   const long sound;
111   const bool loud;
112 }} location_t;
113
114 typedef struct {{
115   const char* query;
116   const char* yes_response;
117 }} obituary_t;
118
119 typedef struct {{
120   const int threshold;
121   const int point_loss;
122   const char* message;
123 }} turn_threshold_t;
124
125 typedef struct {{
126   const int threshold;
127   const char* message;
128 }} class_t;
129
130 typedef struct {{
131   const int number;
132   const int turns;
133   const int penalty;
134   const char* question;
135   const char* hint;
136 }} hint_t;
137
138 typedef struct {{
139   const string_group_t words;
140 }} motion_t;
141
142 typedef struct {{
143   const string_group_t words;
144   const long message;
145 }} action_t;
146
147 typedef struct {{
148   const long motion;
149   const long dest;
150   const bool stop;
151 }} travelop_t;
152
153 /* Abstract out the encoding of words in the travel array.  Gives us
154  * some hope of getting to a less cryptic representation than we
155  * inherited from FORTRAN, someday. To understand these, read the
156  * encoding description for travel.
157  */
158 #define T_DESTINATION(entry)    MOD((entry).dest, 1000)
159 #define T_CONDITION(entry)      ((entry).dest / 1000)
160 #define T_NODWARVES(entry)      (T_CONDITION(entry) == 100)
161 #define T_HIGH(entry)           ((entry).dest)
162 #define T_TERMINATE(entry)      ((entry).motion == 1)
163 #define L_SPEAK(loc)            ((loc) - 500)
164
165 extern const location_t locations[];
166 extern const object_t objects[];
167 extern const char* arbitrary_messages[];
168 extern const class_t classes[];
169 extern const turn_threshold_t turn_thresholds[];
170 extern const obituary_t obituaries[];
171 extern const hint_t hints[];
172 extern long conditions[];
173 extern const motion_t motions[];
174 extern const action_t actions[];
175 extern const action_t specials[];
176 extern const travelop_t travel[];
177 extern const long tkey[];
178
179 #define NLOCATIONS      {}
180 #define NOBJECTS        {}
181 #define NHINTS          {}
182 #define NCLASSES        {}
183 #define NDEATHS         {}
184 #define NTHRESHOLDS     {}
185 #define NMOTIONS        {}
186 #define NACTIONS        {}
187 #define NSPECIALS       {}
188 #define NTRAVEL         {}
189 #define NKEYS           {}
190
191 enum arbitrary_messages_refs {{
192 {}
193 }};
194
195 enum locations_refs {{
196 {}
197 }};
198
199 enum object_refs {{
200 {}
201 }};
202
203 enum motion_refs {{
204 {}
205 }};
206
207 enum action_refs {{
208 {}
209 }};
210
211 enum special_refs {{
212 {}
213 }};
214
215 /* State definitions */
216
217 {}
218 #endif /* end DUNGEON_H */
219 """
220
221 c_template = """/* Generated from adventure.yaml - do not hand-hack! */
222
223 #include "{}"
224
225 const char* arbitrary_messages[] = {{
226 {}
227 }};
228
229 const class_t classes[] = {{
230 {}
231 }};
232
233 const turn_threshold_t turn_thresholds[] = {{
234 {}
235 }};
236
237 const location_t locations[] = {{
238 {}
239 }};
240
241 const object_t objects[] = {{
242 {}
243 }};
244
245 const obituary_t obituaries[] = {{
246 {}
247 }};
248
249 const hint_t hints[] = {{
250 {}
251 }};
252
253 long conditions[] = {{
254 {}
255 }};
256
257 const motion_t motions[] = {{
258 {}
259 }};
260
261 const action_t actions[] = {{
262 {}
263 }};
264
265 const action_t specials[] = {{
266 {}
267 }};
268
269 {}
270
271 const travelop_t travel[] = {{
272 {}
273 }};
274
275 /* end */
276 """
277
278 def make_c_string(string):
279     """Render a Python string into C string literal format."""
280     if string == None:
281         return "NULL"
282     string = string.replace("\n", "\\n")
283     string = string.replace("\t", "\\t")
284     string = string.replace('"', '\\"')
285     string = string.replace("'", "\\'")
286     string = '"' + string + '"'
287     return string
288
289 def get_refs(l):
290     reflist = [x[0] for x in l]
291     ref_str = ""
292     for ref in reflist:
293         ref_str += "    {},\n".format(ref)
294     ref_str = ref_str[:-1] # trim trailing newline
295     return ref_str
296
297 def get_string_group(strings):
298     template = """{{
299             .strs = {},
300             .n = {},
301         }}"""
302     if strings == []:
303         strs = "NULL"
304     else:
305         strs = "(const char* []) {" + ", ".join([make_c_string(s) for s in strings]) + "}"
306     n = len(strings)
307     sg_str = template.format(strs, n)
308     return sg_str
309
310 def get_arbitrary_messages(arb):
311     template = """    {},
312 """
313     arb_str = ""
314     for item in arb:
315         arb_str += template.format(make_c_string(item[1]))
316     arb_str = arb_str[:-1] # trim trailing newline
317     return arb_str
318
319 def get_class_messages(cls):
320     template = """    {{
321         .threshold = {},
322         .message = {},
323     }},
324 """
325     cls_str = ""
326     for item in cls:
327         threshold = item["threshold"]
328         message = make_c_string(item["message"])
329         cls_str += template.format(threshold, message)
330     cls_str = cls_str[:-1] # trim trailing newline
331     return cls_str
332
333 def get_turn_thresholds(trn):
334     template = """    {{
335         .threshold = {},
336         .point_loss = {},
337         .message = {},
338     }},
339 """
340     trn_str = ""
341     for item in trn:
342         threshold = item["threshold"]
343         point_loss = item["point_loss"]
344         message = make_c_string(item["message"])
345         trn_str += template.format(threshold, point_loss, message)
346     trn_str = trn_str[:-1] # trim trailing newline
347     return trn_str
348
349 def get_locations(loc):
350     template = """    {{ // {}
351         .description = {{
352             .small = {},
353             .big = {},
354         }},
355         .sound = {},
356         .loud = {},
357     }},
358 """
359     loc_str = ""
360     for (i, item) in enumerate(loc):
361         short_d = make_c_string(item[1]["description"]["short"])
362         long_d = make_c_string(item[1]["description"]["long"])
363         sound = item[1].get("sound", "SILENT")
364         loud = "true" if item[1].get("loud") else "false"
365         loc_str += template.format(i, short_d, long_d, sound, loud)
366     loc_str = loc_str[:-1] # trim trailing newline
367     return loc_str
368
369 def get_objects(obj):
370     template = """    {{ // {}
371         .words = {},
372         .inventory = {},
373         .plac = {},
374         .fixd = {},
375         .is_treasure = {},
376         .descriptions = (const char* []) {{
377 {}
378         }},
379         .sounds = (const char* []) {{
380 {}
381         }},
382         .texts = (const char* []) {{
383 {}
384         }},
385         .changes = (const char* []) {{
386 {}
387         }},
388     }},
389 """
390     obj_str = ""
391     for (i, item) in enumerate(obj):
392         attr = item[1]
393         try:
394             words_str = get_string_group(attr["words"])
395         except KeyError:
396             words_str = get_string_group([])
397         i_msg = make_c_string(attr["inventory"])
398         descriptions_str = ""
399         if attr["descriptions"] == None:
400             descriptions_str = " " * 12 + "NULL,"
401         else:
402             labels = []
403             for l_msg in attr["descriptions"]:
404                 if not isinstance(l_msg, str):
405                     labels.append(l_msg)
406                     l_msg = l_msg[1]
407                 descriptions_str += " " * 12 + make_c_string(l_msg) + ",\n"
408             descriptions_str = descriptions_str[:-1] # trim trailing newline
409             if labels:
410                 global statedefines
411                 statedefines += "/* States for %s */\n" % item[0]
412                 for (i, (label, message)) in enumerate(labels):
413                     if len(message) >= 45:
414                         message = message[:45] + "..."
415                     statedefines += "#define %s\t%d /* %s */\n" % (label, i, message)
416                 statedefines += "\n"
417         sounds_str = ""
418         if attr.get("sounds") == None:
419             sounds_str = " " * 12 + "NULL,"
420         else:
421              for l_msg in attr["sounds"]:
422                  sounds_str += " " * 12 + make_c_string(l_msg) + ",\n"
423              sounds_str = sounds_str[:-1] # trim trailing newline
424         texts_str = ""
425         if attr.get("texts") == None:
426             texts_str = " " * 12 + "NULL,"
427         else:
428              for l_msg in attr["texts"]:
429                  texts_str += " " * 12 + make_c_string(l_msg) + ",\n"
430              texts_str = texts_str[:-1] # trim trailing newline
431         changes_str = ""
432         if attr.get("changes") == None:
433             changes_str = " " * 12 + "NULL,"
434         else:
435              for l_msg in attr["changes"]:
436                  changes_str += " " * 12 + make_c_string(l_msg) + ",\n"
437              changes_str = changes_str[:-1] # trim trailing newline
438         locs = attr.get("locations", ["LOC_NOWHERE", "LOC_NOWHERE"])
439         immovable = attr.get("immovable", False)
440         try:
441             if type(locs) == str:
442                 locs = [locnames.index(locs), -1 if immovable else 0]
443             else:
444                 locs = [locnames.index(x) for x in locs]
445         except IndexError:
446             sys.stderr.write("dungeon: unknown object location in %s\n" % locs)
447             sys.exit(1)
448         treasure = "true" if attr.get("treasure") else "false"
449         obj_str += template.format(i, words_str, i_msg, locs[0], locs[1], treasure, descriptions_str, sounds_str, texts_str, changes_str)
450     obj_str = obj_str[:-1] # trim trailing newline
451     return obj_str
452
453 def get_obituaries(obit):
454     template = """    {{
455         .query = {},
456         .yes_response = {},
457     }},
458 """
459     obit_str = ""
460     for o in obit:
461         query = make_c_string(o["query"])
462         yes = make_c_string(o["yes_response"])
463         obit_str += template.format(query, yes)
464     obit_str = obit_str[:-1] # trim trailing newline
465     return obit_str
466
467 def get_hints(hnt, arb):
468     template = """    {{
469         .number = {},
470         .penalty = {},
471         .turns = {},
472         .question = {},
473         .hint = {},
474     }},
475 """
476     hnt_str = ""
477     md = dict(arb)
478     for member in hnt:
479         item = member["hint"]
480         number = item["number"]
481         penalty = item["penalty"]
482         turns = item["turns"]
483         question = make_c_string(item["question"])
484         hint = make_c_string(item["hint"])
485         hnt_str += template.format(number, penalty, turns, question, hint)
486     hnt_str = hnt_str[:-1] # trim trailing newline
487     return hnt_str
488
489 def get_condbits(locations):
490     cnd_str = ""
491     for (name, loc) in locations:
492         conditions = loc["conditions"]
493         hints = loc.get("hints") or []
494         flaglist = []
495         for flag in conditions:
496             if conditions[flag]:
497                 flaglist.append(flag)
498         line = "|".join([("(1<<COND_%s)" % f) for f in flaglist])
499         trail = "|".join([("(1<<COND_H%s)" % f['name']) for f in hints])
500         if trail:
501             line += "|" + trail
502         if line.startswith("|"):
503             line = line[1:]
504         if not line:
505             line = "0"
506         cnd_str += "    " + line + ",\t// " + name + "\n"
507     return cnd_str
508
509 def get_motions(motions):
510     template = """    {{
511         .words = {},
512     }},
513 """
514     mot_str = ""
515     for motion in motions:
516         contents = motion[1]
517         if contents["words"] == None:
518             words_str = get_string_group([])
519         else:
520             words_str = get_string_group(contents["words"])
521         mot_str += template.format(words_str)
522     return mot_str
523
524 def get_actions(actions):
525     template = """    {{
526         .words = {},
527         .message = {},
528     }},
529 """
530     act_str = ""
531     for action in actions:
532         contents = action[1]
533         
534         if contents["words"] == None:
535             words_str = get_string_group([])
536         else:
537             words_str = get_string_group(contents["words"])
538
539         if contents["message"] == None:
540             message = "NO_MESSAGE"
541         else:
542             message = contents["message"]
543             
544         act_str += template.format(words_str, message)
545     act_str = act_str[:-1] # trim trailing newline
546     return act_str
547
548 def bigdump(arr):
549     out = ""
550     for (i, entry) in enumerate(arr):
551         if i % 10 == 0:
552             if out and out[-1] == ' ':
553                 out = out[:-1]
554             out += "\n    "
555         out += str(arr[i]) + ", "
556     out = out[:-2] + "\n"
557     return out
558
559 def buildtravel(locs, objs):
560     ltravel = []
561     verbmap = {}
562     for i, motion in enumerate(db["motions"]):
563         try:
564             for word in motion[1]["words"]:
565                 verbmap[word.upper()] = i
566         except TypeError:
567             pass
568     def dencode(action, name):
569         "Decode a destination number"
570         if action[0] == "goto":
571             try:
572                 return locnames.index(action[1])
573             except ValueError:
574                 sys.stderr.write("dungeon: unknown location %s in goto clause of %s\n" % (cond[1], name))
575         elif action[0] == "special":
576             return 300 + action[1]
577         elif action[0] == "speak":
578             try:
579                 return 500 + msgnames.index(action[1])
580             except ValueError:
581                 sys.stderr.write("dungeon: unknown location %s in carry clause of %s\n" % (cond[1], name))
582         else:
583             print(cond)
584             raise ValueError
585     def cencode(cond, name):
586         if cond is None:
587             return 0;
588         elif cond[0] == "pct":
589             return cond[1]
590         elif cond[0] == "carry":
591             try:
592                 return 100 + objnames.index(cond[1])
593             except ValueError:
594                 sys.stderr.write("dungeon: unknown object name %s in carry clause of %s\n" % (cond[1], name))
595                 sys.exit(1)
596         elif cond[0] == "with":
597             try:
598                 return 200 + objnames.index(cond[1])
599             except IndexError:
600                 sys.stderr.write("dungeon: unknown object name %s in with clause of \n" % (cond[1], name))
601                 sys.exit(1)
602         elif cond[0] == "not":
603             # FIXME: Allow named as well as numbered states
604             try:
605                 obj = objnames.index(cond[1])
606                 if type(cond[2]) == int:
607                     state = cond[2]
608                 else:
609                     for (i, stateclause) in enumerate(objs[obj][1]["descriptions"]):
610                         if type(stateclause) == list:
611                             if stateclause[0] == cond[2]:
612                                 state = i
613                                 break
614                     else:
615                         sys.stderr.write("dungeon: unmatched state symbol %s in not clause of %s\n" % (cond[2], name))
616                         sys.exit(0);
617                 return 300 + obj + 100 * state
618             except ValueError:
619                 sys.stderr.write("dungeon: unknown object name %s in not clause of %s\n" % (cond[1], name))
620                 sys.exit(1)
621         else:
622             print(cond)
623             raise ValueError
624
625     for (i, (name, loc)) in enumerate(locs):
626         if "travel" in loc:
627             for rule in loc["travel"]:
628                 tt = [i]
629                 dest = dencode(rule["action"], name) + 1000 * cencode(rule.get("cond"), name)
630                 tt.append(dest)
631                 tt += [verbmap[e] for e in rule["verbs"]]
632                 if not rule["verbs"]:
633                     tt.append(1)
634                 ltravel.append(tuple(tt))
635
636     # At this point the ltravel data is in the Section 3
637     # representation from the FORTRAN version.  Next we perform the
638     # same mapping into the runtime format.  This was the C translation
639     # of the FORTRAN code:
640     # long loc;
641     # while ((loc = GETNUM(database)) != -1) {
642     #     long newloc = GETNUM(NULL);
643     #     long L;
644     #     if (TKEY[loc] == 0) {
645     #         TKEY[loc] = TRVS;
646     #     } else {
647     #         TRAVEL[TRVS - 1] = -TRAVEL[TRVS - 1];
648     #     }
649     #     while ((L = GETNUM(NULL)) != 0) {
650     #         TRAVEL[TRVS] = newloc * 1000 + L;
651     #         TRVS = TRVS + 1;
652     #         if (TRVS == TRVSIZ)
653     #             BUG(TOO_MANY_TRAVEL_OPTIONS);
654     #     }
655     #     TRAVEL[TRVS - 1] = -TRAVEL[TRVS - 1];
656     # }
657     #
658     # In order to de-crypticize the runtime code, we're going to break these
659     # magic numbers up into a struct.
660     travel = [[0, 0, False]]
661     tkey = [0]
662     oldloc = 0
663     while ltravel:
664         rule = list(ltravel.pop(0))
665         loc = rule.pop(0)
666         newloc = rule.pop(0)
667         if loc != oldloc:
668             tkey.append(len(travel))
669             oldloc = loc 
670         elif travel:
671             travel[-1][2] = not travel[-1][2]
672         while rule:
673             travel.append([rule.pop(0), newloc, False])
674         travel[-1][2] = True
675     return (travel, tkey)
676
677 def get_travel(travel):
678     template = """    {{
679         .motion = {},
680         .dest = {},
681         .stop = {},
682     }},
683 """
684     out = ""
685     for entry in travel:
686         out += template.format(entry[0], entry[1], entry[2]).lower()
687     out = out[:-1] # trim trailing newline
688     return out
689
690 if __name__ == "__main__":
691     with open(yaml_name, "r") as f:
692         db = yaml.load(f)
693
694     locnames = [x[0] for x in db["locations"]]
695     msgnames = [el[0] for el in db["arbitrary_messages"]]
696     objnames = [el[0] for el in db["objects"]]
697
698     (travel, tkey) = buildtravel(db["locations"],
699                                  db["objects"])
700
701     c = c_template.format(
702         h_name,
703         get_arbitrary_messages(db["arbitrary_messages"]),
704         get_class_messages(db["classes"]),
705         get_turn_thresholds(db["turn_thresholds"]),
706         get_locations(db["locations"]),
707         get_objects(db["objects"]),
708         get_obituaries(db["obituaries"]),
709         get_hints(db["hints"], db["arbitrary_messages"]),
710         get_condbits(db["locations"]),
711         get_motions(db["motions"]),
712         get_actions(db["actions"]),
713         get_actions(db["specials"]),
714         "const long tkey[] = {%s};" % bigdump(tkey),
715         get_travel(travel), 
716     )
717
718     h = h_template.format(
719         len(db["locations"])-1,
720         len(db["objects"])-1,
721         len(db["hints"]),
722         len(db["classes"])-1,
723         len(db["obituaries"]),
724         len(db["turn_thresholds"]),
725         len(db["motions"]),
726         len(db["actions"]),
727         len(db["specials"]),
728         len(travel),
729         len(tkey),
730         get_refs(db["arbitrary_messages"]),
731         get_refs(db["locations"]),
732         get_refs(db["objects"]),
733         get_refs(db["motions"]),
734         get_refs(db["actions"]),
735         get_refs(db["specials"]),
736         statedefines,
737     )
738
739     with open(h_name, "w") as hf:
740         hf.write(h)
741
742     with open(c_name, "w") as cf:
743         cf.write(c)
744
745 # end