First cut at compiling travel rules from YAML.
[open-adventure.git] / newdungeon.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 import sys, yaml
8
9 yaml_name = "adventure.yaml"
10 h_name = "newdb.h"
11 c_name = "newdb.c"
12
13 statedefines = ""
14
15 h_template = """/* Generated from adventure.yaml - do not hand-hack! */
16 #ifndef NEWDB_H
17 #define NEWDB_H
18
19 #include <stdio.h>
20 #include <stdbool.h>
21
22 #define SILENT  -1      /* no sound */
23
24 /* Symbols for cond bits */
25 #define COND_LIT        0       /* Light */
26 #define COND_OILY       1       /* If bit 2 is on: on for oil, off for water */
27 #define COND_FLUID      2       /* Liquid asset, see bit 1 */
28 #define COND_NOARRR     3       /* Pirate doesn't go here unless following */
29 #define COND_NOBACK     4       /* Cannot use "back" to move away */
30 #define COND_ABOVE      5
31 #define COND_DEEP       6       /* Deep - e.g where dwarves are active */
32 #define COND_FOREST     7       /* In the forest */
33 #define COND_FORCED     8       /* Only one way in or out of here */
34 /* Bits past 10 indicate areas of interest to "hint" routines */
35 #define COND_HBASE      10      /* Base for location hint bits */
36 #define COND_HCAVE      11      /* Trying to get into cave */
37 #define COND_HBIRD      12      /* Trying to catch bird */
38 #define COND_HSNAKE     13      /* Trying to deal with snake */
39 #define COND_HMAZE      14      /* Lost in maze */
40 #define COND_HDARK      15      /* Pondering dark room */
41 #define COND_HWITT      16      /* At Witt's End */
42 #define COND_HCLIFF     17      /* Cliff with urn */
43 #define COND_HWOODS     18      /* Lost in forest */
44 #define COND_HOGRE      19      /* Trying to deal with ogre */
45 #define COND_HJADE      20      /* Found all treasures except jade */
46
47 typedef struct {{
48   const char* inventory;
49   int plac, fixd;
50   bool is_treasure;
51   const char** longs;
52   const char** sounds;
53   const char** texts;
54 }} object_t;
55
56 typedef struct {{
57   const char* small;
58   const char* big;
59 }} descriptions_t;
60
61 typedef struct {{
62   descriptions_t description;
63   const long sound;
64   const bool loud;
65 }} location_t;
66
67 typedef struct {{
68   const char* query;
69   const char* yes_response;
70 }} obituary_t;
71
72 typedef struct {{
73   const int threshold;
74   const int point_loss;
75   const char* message;
76 }} turn_threshold_t;
77
78 typedef struct {{
79   const int threshold;
80   const char* message;
81 }} class_t;
82
83 typedef struct {{
84   const int number;
85   const int turns;
86   const int penalty;
87   const char* question;
88   const char* hint;
89 }} hint_t;
90
91 typedef struct {{
92   const char* word;
93   const int type;
94   const int value;
95 }} vocabulary_t;
96
97 extern const location_t locations[];
98 extern const object_t objects[];
99 extern const char* arbitrary_messages[];
100 extern const class_t classes[];
101 extern const turn_threshold_t turn_thresholds[];
102 extern const obituary_t obituaries[];
103 extern const hint_t hints[];
104 extern long conditions[];
105 extern const vocabulary_t vocabulary[];
106 extern const long actspk[];
107
108 #define NLOCATIONS      {}
109 #define NOBJECTS        {}
110 #define NHINTS          {}
111 #define NCLASSES        {}
112 #define NDEATHS         {}
113 #define NTHRESHOLDS     {}
114 #define NVERBS          {}
115 #define NVOCAB          {}
116 #define NTRAVEL         {}
117
118 enum arbitrary_messages_refs {{
119 {}
120 }};
121
122 enum locations_refs {{
123 {}
124 }};
125
126 enum object_refs {{
127 {}
128 }};
129
130 /* State definitions */
131
132 {}
133 #endif /* end NEWDB_H */
134 """
135
136 c_template = """/* Generated from adventure.yaml - do not hand-hack! */
137
138 #include "common.h"
139 #include "{}"
140
141 const char* arbitrary_messages[] = {{
142 {}
143 }};
144
145 const class_t classes[] = {{
146 {}
147 }};
148
149 const turn_threshold_t turn_thresholds[] = {{
150 {}
151 }};
152
153 const location_t locations[] = {{
154 {}
155 }};
156
157 const object_t objects[] = {{
158 {}
159 }};
160
161 const obituary_t obituaries[] = {{
162 {}
163 }};
164
165 const hint_t hints[] = {{
166 {}
167 }};
168
169 long conditions[] = {{
170 {}
171 }};
172
173 const vocabulary_t vocabulary[] = {{
174 {}
175 }};
176
177 const long actspk[] = {{
178     NO_MESSAGE,
179 {}
180 }};
181
182 /* end */
183 """
184
185 def make_c_string(string):
186     """Render a Python string into C string literal format."""
187     if string == None:
188         return "NULL"
189     string = string.replace("\n", "\\n")
190     string = string.replace("\t", "\\t")
191     string = string.replace('"', '\\"')
192     string = string.replace("'", "\\'")
193     string = '"' + string + '"'
194     return string
195
196 def get_refs(l):
197     reflist = [x[0] for x in l]
198     ref_str = ""
199     for ref in reflist:
200         ref_str += "    {},\n".format(ref)
201     ref_str = ref_str[:-1] # trim trailing newline
202     return ref_str
203
204 def get_arbitrary_messages(arb):
205     template = """    {},
206 """
207     arb_str = ""
208     for item in arb:
209         arb_str += template.format(make_c_string(item[1]))
210     arb_str = arb_str[:-1] # trim trailing newline
211     return arb_str
212
213 def get_class_messages(cls):
214     template = """    {{
215         .threshold = {},
216         .message = {},
217     }},
218 """
219     cls_str = ""
220     for item in cls:
221         threshold = item["threshold"]
222         message = make_c_string(item["message"])
223         cls_str += template.format(threshold, message)
224     cls_str = cls_str[:-1] # trim trailing newline
225     return cls_str
226
227 def get_turn_thresholds(trn):
228     template = """    {{
229         .threshold = {},
230         .point_loss = {},
231         .message = {},
232     }},
233 """
234     trn_str = ""
235     for item in trn:
236         threshold = item["threshold"]
237         point_loss = item["point_loss"]
238         message = make_c_string(item["message"])
239         trn_str += template.format(threshold, point_loss, message)
240     trn_str = trn_str[:-1] # trim trailing newline
241     return trn_str
242
243 def get_locations(loc):
244     template = """    {{ // {}
245         .description = {{
246             .small = {},
247             .big = {},
248         }},
249         .sound = {},
250         .loud = {},
251     }},
252 """
253     loc_str = ""
254     for (i, item) in enumerate(loc):
255         short_d = make_c_string(item[1]["description"]["short"])
256         long_d = make_c_string(item[1]["description"]["long"])
257         sound = item[1].get("sound", "SILENT")
258         loud = "true" if item[1].get("loud") else "false"
259         loc_str += template.format(i, short_d, long_d, sound, loud)
260     loc_str = loc_str[:-1] # trim trailing newline
261     return loc_str
262
263 def get_objects(obj):
264     template = """    {{ // {}
265         .inventory = {},
266         .plac = {},
267         .fixd = {},
268         .is_treasure = {},
269         .longs = (const char* []) {{
270 {}
271         }},
272         .sounds = (const char* []) {{
273 {}
274         }},
275         .texts = (const char* []) {{
276 {}
277         }},
278     }},
279 """
280     obj_str = ""
281     for (i, item) in enumerate(obj):
282         attr = item[1]
283         i_msg = make_c_string(attr["inventory"])
284         longs_str = ""
285         if attr["longs"] == None:
286             longs_str = " " * 12 + "NULL,"
287         else:
288             labels = []
289             for l_msg in attr["longs"]:
290                 if not isinstance(l_msg, str):
291                     labels.append(l_msg)
292                     l_msg = l_msg[1]
293                 longs_str += " " * 12 + make_c_string(l_msg) + ",\n"
294             longs_str = longs_str[:-1] # trim trailing newline
295             if labels:
296                 global statedefines
297                 statedefines += "/* States for %s */\n" % item[0]
298                 for (i, (label, message)) in enumerate(labels):
299                     if len(message) >= 45:
300                         message = message[:45] + "..."
301                     statedefines += "#define %s\t%d /* %s */\n" % (label, i, message)
302                 statedefines += "\n"
303         sounds_str = ""
304         if attr.get("sounds") == None:
305             sounds_str = " " * 12 + "NULL,"
306         else:
307              for l_msg in attr["sounds"]:
308                  sounds_str += " " * 12 + make_c_string(l_msg) + ",\n"
309              sounds_str = sounds_str[:-1] # trim trailing newline
310         texts_str = ""
311         if attr.get("texts") == None:
312             texts_str = " " * 12 + "NULL,"
313         else:
314              for l_msg in attr["texts"]:
315                  texts_str += " " * 12 + make_c_string(l_msg) + ",\n"
316              texts_str = texts_str[:-1] # trim trailing newline
317         locs = attr.get("locations", ["LOC_NOWHERE", "LOC_NOWHERE"])
318         immovable = attr.get("immovable", False)
319         try:
320             if type(locs) == str:
321                 locs = [locnames.index(locs), -1 if immovable else 0]
322             else:
323                 locs = [locnames.index(x) for x in locs]
324         except IndexError:
325             sys.stderr.write("dungeon: unknown object location in %s\n" % locs)
326             sys.exit(1)
327         treasure = "true" if attr.get("treasure") else "false"
328         obj_str += template.format(i, i_msg, locs[0], locs[1], treasure, longs_str, sounds_str, texts_str)
329     obj_str = obj_str[:-1] # trim trailing newline
330     return obj_str
331
332 def get_obituaries(obit):
333     template = """    {{
334         .query = {},
335         .yes_response = {},
336     }},
337 """
338     obit_str = ""
339     for o in obit:
340         query = make_c_string(o["query"])
341         yes = make_c_string(o["yes_response"])
342         obit_str += template.format(query, yes)
343     obit_str = obit_str[:-1] # trim trailing newline
344     return obit_str
345
346 def get_hints(hnt, arb):
347     template = """    {{
348         .number = {},
349         .penalty = {},
350         .turns = {},
351         .question = {},
352         .hint = {},
353     }},
354 """
355     hnt_str = ""
356     md = dict(arb)
357     for member in hnt:
358         item = member["hint"]
359         number = item["number"]
360         penalty = item["penalty"]
361         turns = item["turns"]
362         question = make_c_string(item["question"])
363         hint = make_c_string(item["hint"])
364         hnt_str += template.format(number, penalty, turns, question, hint)
365     hnt_str = hnt_str[:-1] # trim trailing newline
366     return hnt_str
367
368 def get_condbits(locations):
369     cnd_str = ""
370     for (name, loc) in locations:
371         conditions = loc["conditions"]
372         hints = loc.get("hints") or []
373         flaglist = []
374         for flag in conditions:
375             if conditions[flag]:
376                 flaglist.append(flag)
377         line = "|".join([("(1<<COND_%s)" % f) for f in flaglist])
378         trail = "|".join([("(1<<COND_H%s)" % f['name']) for f in hints])
379         if trail:
380             line += "|" + trail
381         if line.startswith("|"):
382             line = line[1:]
383         if not line:
384             line = "0"
385         cnd_str += "    " + line + ",\t// " + name + "\n"
386     return cnd_str
387
388 def recompose(type_word, value):
389     "Compose the internal code for a vocabulary word from its YAML entry"
390     parts = ("motion", "action", "object", "special")
391     try:
392         return value + 1000 * parts.index(type_word)
393     except KeyError:
394         sys.stderr.write("dungeon: %s is not a known word\n" % word)
395         sys.exit(1)
396     except IndexError:
397         sys.stderr.write("%s is not a known word classifier\n" % attrs["type"])
398         sys.exit(1)
399
400 def get_vocabulary(vocabulary):
401     template = """    {{
402         .word = {},
403         .type = {},
404         .value = {},
405     }},
406 """
407     voc_str = ""
408     for vocab in vocabulary:
409         word = make_c_string(vocab["word"])
410         type_code = recompose(vocab["type"], vocab["value"])
411         value = vocab["value"]
412         voc_str += template.format(word, type_code, value)
413     voc_str = voc_str[:-1] # trim trailing newline
414     return voc_str
415
416 def get_actspk(actspk):
417     res = ""
418     for (i, word) in actspk.items():
419         res += "    %s,\n" % word
420     return res
421
422 def buildtravel(locs, objs, voc):
423     ltravel = []
424     lkeys = []
425     verbmap = {}
426     for entry in db["vocabulary"]:
427         if entry["type"] == "motion" and entry["value"] not in verbmap:
428             verbmap[entry["word"]] = entry["value"]
429     def dencode(action, name):
430         "Decode a destination number"
431         if action[0] == "goto":
432             try:
433                 return locnames.index(action[1])
434             except ValueError:
435                 sys.stderr.write("dungeon: unknown location %s in goto clause of %s\n" % (cond[1], name))
436         elif action[0] == "special":
437             return 300 + action[1]
438         elif action[0] == "speak":
439             try:
440                 return 500 + msgnames.index(action[1])
441             except ValueError:
442                 sys.stderr.write("dungeon: unknown location %s in carry clause of %s\n" % (cond[1], name))
443         else:
444             print(cond)
445             raise ValueError
446     def cencode(cond, name):
447         if cond is None:
448             return 0;
449         elif cond[0] == "pct":
450             return cond[1]
451         elif cond[0] == "carry":
452             try:
453                 return 100 + objnames.index(cond[1])
454             except ValueError:
455                 sys.stderr.write("dungeon: unknown object name %s in carry clause of %s\n" % (cond[1], name))
456                 sys.exit(1)
457         elif cond[0] == "with":
458             try:
459                 return 200 + objnames.index(cond[1])
460             except IndexError:
461                 sys.stderr.write("dungeon: unknown object name %s in with clause of \n" % (cond[1], name))
462                 sys.exit(1)
463         elif cond[0] == "not":
464             # FIXME: Allow named as well as numbered states
465             try:
466                 return 300 + objnames.index(cond[1]) + 100 * cond[2]
467             except ValueError:
468                 sys.stderr.write("dungeon: unknown object name %s in not clause of %s\n" % (cond[1], name))
469                 sys.exit(1)
470         else:
471             print(cond)
472             raise ValueError
473     # Much more to be done here
474     for (i, (name, loc)) in enumerate(locs):
475         if "travel" in loc:
476             for rule in loc["travel"]:
477                 tt = [i]
478                 dest = dencode(rule["action"], name) + 1000 * cencode(rule.get("cond"), name)
479                 tt.append(dest)
480                 tt += [verbmap[e] for e in rule["verbs"]]
481                 if not rule["verbs"]:
482                     tt.append(1)
483                 #print(tuple(tt))
484     return (ltravel, lkeys)
485
486 if __name__ == "__main__":
487     with open(yaml_name, "r") as f:
488         db = yaml.load(f)
489
490     locnames = [x[0] for x in db["locations"]]
491     msgnames = [el[0] for el in db["arbitrary_messages"]]
492     objnames = [el[0] for el in db["objects"]]
493     (travel, key) = buildtravel(db["locations"], db["objects"], db["vocabulary"])
494
495     c = c_template.format(
496         h_name,
497         get_arbitrary_messages(db["arbitrary_messages"]),
498         get_class_messages(db["classes"]),
499         get_turn_thresholds(db["turn_thresholds"]),
500         get_locations(db["locations"]),
501         get_objects(db["objects"]),
502         get_obituaries(db["obituaries"]),
503         get_hints(db["hints"], db["arbitrary_messages"]),
504         get_condbits(db["locations"]),
505         get_vocabulary(db["vocabulary"]),
506         get_actspk(db["actspk"]),
507     )
508
509     h = h_template.format(
510         len(db["locations"])-1,
511         len(db["objects"])-1,
512         len(db["hints"]),
513         len(db["classes"])-1,
514         len(db["obituaries"]),
515         len(db["turn_thresholds"]),
516         len(db["actspk"]),
517         len(db["vocabulary"]),
518         len(travel),
519         get_refs(db["arbitrary_messages"]),
520         get_refs(db["locations"]),
521         get_refs(db["objects"]),
522         statedefines,
523     )
524
525     with open(h_name, "w") as hf:
526         hf.write(h)
527
528     with open(c_name, "w") as cf:
529         cf.write(c)
530
531 # end