Abolish VOCWRD().
[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** words;
93 }} motion_t;
94
95 typedef struct {{
96   const char** words;
97   const long message;
98 }} action_t;
99
100 extern const location_t locations[];
101 extern const object_t objects[];
102 extern const char* arbitrary_messages[];
103 extern const class_t classes[];
104 extern const turn_threshold_t turn_thresholds[];
105 extern const obituary_t obituaries[];
106 extern const hint_t hints[];
107 extern long conditions[];
108 extern const motion_t motions[];
109 extern const action_t actions[];
110
111 #define NLOCATIONS      {}
112 #define NOBJECTS        {}
113 #define NHINTS          {}
114 #define NCLASSES        {}
115 #define NDEATHS         {}
116 #define NTHRESHOLDS     {}
117 #define NACTIONS        {}
118 #define NTRAVEL         {}
119
120 enum arbitrary_messages_refs {{
121 {}
122 }};
123
124 enum locations_refs {{
125 {}
126 }};
127
128 enum object_refs {{
129 {}
130 }};
131
132 enum motion_refs {{
133 {}
134 }};
135
136 enum action_refs {{
137 {}
138 }};
139
140 /* State definitions */
141
142 {}
143 #endif /* end NEWDB_H */
144 """
145
146 c_template = """/* Generated from adventure.yaml - do not hand-hack! */
147
148 #include "common.h"
149 #include "{}"
150
151 const char* arbitrary_messages[] = {{
152 {}
153 }};
154
155 const class_t classes[] = {{
156 {}
157 }};
158
159 const turn_threshold_t turn_thresholds[] = {{
160 {}
161 }};
162
163 const location_t locations[] = {{
164 {}
165 }};
166
167 const object_t objects[] = {{
168 {}
169 }};
170
171 const obituary_t obituaries[] = {{
172 {}
173 }};
174
175 const hint_t hints[] = {{
176 {}
177 }};
178
179 long conditions[] = {{
180 {}
181 }};
182
183 const motion_t motions[] = {{
184 {}
185 }};
186
187 const action_t actions[] = {{
188 {}
189 }};
190
191 /* end */
192 """
193
194 def make_c_string(string):
195     """Render a Python string into C string literal format."""
196     if string == None:
197         return "NULL"
198     string = string.replace("\n", "\\n")
199     string = string.replace("\t", "\\t")
200     string = string.replace('"', '\\"')
201     string = string.replace("'", "\\'")
202     string = '"' + string + '"'
203     return string
204
205 def get_refs(l):
206     reflist = [x[0] for x in l]
207     ref_str = ""
208     for ref in reflist:
209         ref_str += "    {},\n".format(ref)
210     ref_str = ref_str[:-1] # trim trailing newline
211     return ref_str
212
213 def get_arbitrary_messages(arb):
214     template = """    {},
215 """
216     arb_str = ""
217     for item in arb:
218         arb_str += template.format(make_c_string(item[1]))
219     arb_str = arb_str[:-1] # trim trailing newline
220     return arb_str
221
222 def get_class_messages(cls):
223     template = """    {{
224         .threshold = {},
225         .message = {},
226     }},
227 """
228     cls_str = ""
229     for item in cls:
230         threshold = item["threshold"]
231         message = make_c_string(item["message"])
232         cls_str += template.format(threshold, message)
233     cls_str = cls_str[:-1] # trim trailing newline
234     return cls_str
235
236 def get_turn_thresholds(trn):
237     template = """    {{
238         .threshold = {},
239         .point_loss = {},
240         .message = {},
241     }},
242 """
243     trn_str = ""
244     for item in trn:
245         threshold = item["threshold"]
246         point_loss = item["point_loss"]
247         message = make_c_string(item["message"])
248         trn_str += template.format(threshold, point_loss, message)
249     trn_str = trn_str[:-1] # trim trailing newline
250     return trn_str
251
252 def get_locations(loc):
253     template = """    {{ // {}
254         .description = {{
255             .small = {},
256             .big = {},
257         }},
258         .sound = {},
259         .loud = {},
260     }},
261 """
262     loc_str = ""
263     for (i, item) in enumerate(loc):
264         short_d = make_c_string(item[1]["description"]["short"])
265         long_d = make_c_string(item[1]["description"]["long"])
266         sound = item[1].get("sound", "SILENT")
267         loud = "true" if item[1].get("loud") else "false"
268         loc_str += template.format(i, short_d, long_d, sound, loud)
269     loc_str = loc_str[:-1] # trim trailing newline
270     return loc_str
271
272 def get_objects(obj):
273     template = """    {{ // {}
274         .inventory = {},
275         .plac = {},
276         .fixd = {},
277         .is_treasure = {},
278         .longs = (const char* []) {{
279 {}
280         }},
281         .sounds = (const char* []) {{
282 {}
283         }},
284         .texts = (const char* []) {{
285 {}
286         }},
287     }},
288 """
289     obj_str = ""
290     for (i, item) in enumerate(obj):
291         attr = item[1]
292         i_msg = make_c_string(attr["inventory"])
293         longs_str = ""
294         if attr["longs"] == None:
295             longs_str = " " * 12 + "NULL,"
296         else:
297             labels = []
298             for l_msg in attr["longs"]:
299                 if not isinstance(l_msg, str):
300                     labels.append(l_msg)
301                     l_msg = l_msg[1]
302                 longs_str += " " * 12 + make_c_string(l_msg) + ",\n"
303             longs_str = longs_str[:-1] # trim trailing newline
304             if labels:
305                 global statedefines
306                 statedefines += "/* States for %s */\n" % item[0]
307                 for (i, (label, message)) in enumerate(labels):
308                     if len(message) >= 45:
309                         message = message[:45] + "..."
310                     statedefines += "#define %s\t%d /* %s */\n" % (label, i, message)
311                 statedefines += "\n"
312         sounds_str = ""
313         if attr.get("sounds") == None:
314             sounds_str = " " * 12 + "NULL,"
315         else:
316              for l_msg in attr["sounds"]:
317                  sounds_str += " " * 12 + make_c_string(l_msg) + ",\n"
318              sounds_str = sounds_str[:-1] # trim trailing newline
319         texts_str = ""
320         if attr.get("texts") == None:
321             texts_str = " " * 12 + "NULL,"
322         else:
323              for l_msg in attr["texts"]:
324                  texts_str += " " * 12 + make_c_string(l_msg) + ",\n"
325              texts_str = texts_str[:-1] # trim trailing newline
326         locs = attr.get("locations", ["LOC_NOWHERE", "LOC_NOWHERE"])
327         immovable = attr.get("immovable", False)
328         try:
329             if type(locs) == str:
330                 locs = [locnames.index(locs), -1 if immovable else 0]
331             else:
332                 locs = [locnames.index(x) for x in locs]
333         except IndexError:
334             sys.stderr.write("dungeon: unknown object location in %s\n" % locs)
335             sys.exit(1)
336         treasure = "true" if attr.get("treasure") else "false"
337         obj_str += template.format(i, i_msg, locs[0], locs[1], treasure, longs_str, sounds_str, texts_str)
338     obj_str = obj_str[:-1] # trim trailing newline
339     return obj_str
340
341 def get_obituaries(obit):
342     template = """    {{
343         .query = {},
344         .yes_response = {},
345     }},
346 """
347     obit_str = ""
348     for o in obit:
349         query = make_c_string(o["query"])
350         yes = make_c_string(o["yes_response"])
351         obit_str += template.format(query, yes)
352     obit_str = obit_str[:-1] # trim trailing newline
353     return obit_str
354
355 def get_hints(hnt, arb):
356     template = """    {{
357         .number = {},
358         .penalty = {},
359         .turns = {},
360         .question = {},
361         .hint = {},
362     }},
363 """
364     hnt_str = ""
365     md = dict(arb)
366     for member in hnt:
367         item = member["hint"]
368         number = item["number"]
369         penalty = item["penalty"]
370         turns = item["turns"]
371         question = make_c_string(item["question"])
372         hint = make_c_string(item["hint"])
373         hnt_str += template.format(number, penalty, turns, question, hint)
374     hnt_str = hnt_str[:-1] # trim trailing newline
375     return hnt_str
376
377 def get_condbits(locations):
378     cnd_str = ""
379     for (name, loc) in locations:
380         conditions = loc["conditions"]
381         hints = loc.get("hints") or []
382         flaglist = []
383         for flag in conditions:
384             if conditions[flag]:
385                 flaglist.append(flag)
386         line = "|".join([("(1<<COND_%s)" % f) for f in flaglist])
387         trail = "|".join([("(1<<COND_H%s)" % f['name']) for f in hints])
388         if trail:
389             line += "|" + trail
390         if line.startswith("|"):
391             line = line[1:]
392         if not line:
393             line = "0"
394         cnd_str += "    " + line + ",\t// " + name + "\n"
395     return cnd_str
396
397 def recompose(type_word, value):
398     "Compose the internal code for a vocabulary word from its YAML entry"
399     parts = ("motion", "action", "object", "special")
400     try:
401         return value + 1000 * parts.index(type_word)
402     except KeyError:
403         sys.stderr.write("dungeon: %s is not a known word\n" % word)
404         sys.exit(1)
405     except IndexError:
406         sys.stderr.write("%s is not a known word classifier\n" % attrs["type"])
407         sys.exit(1)
408
409 def buildtravel(locs, objs, voc):
410     ltravel = []
411     lkeys = []
412     verbmap = {}
413     for entry in db["vocabulary"]:
414         if entry["type"] == "motion" and entry["value"] not in verbmap:
415             verbmap[entry["word"]] = entry["value"]
416     def dencode(action, name):
417         "Decode a destination number"
418         if action[0] == "goto":
419             try:
420                 return locnames.index(action[1])
421             except ValueError:
422                 sys.stderr.write("dungeon: unknown location %s in goto clause of %s\n" % (cond[1], name))
423         elif action[0] == "special":
424             return 300 + action[1]
425         elif action[0] == "speak":
426             try:
427                 return 500 + msgnames.index(action[1])
428             except ValueError:
429                 sys.stderr.write("dungeon: unknown location %s in carry clause of %s\n" % (cond[1], name))
430         else:
431             print(cond)
432             raise ValueError
433     def cencode(cond, name):
434         if cond is None:
435             return 0;
436         elif cond[0] == "pct":
437             return cond[1]
438         elif cond[0] == "carry":
439             try:
440                 return 100 + objnames.index(cond[1])
441             except ValueError:
442                 sys.stderr.write("dungeon: unknown object name %s in carry clause of %s\n" % (cond[1], name))
443                 sys.exit(1)
444         elif cond[0] == "with":
445             try:
446                 return 200 + objnames.index(cond[1])
447             except IndexError:
448                 sys.stderr.write("dungeon: unknown object name %s in with clause of \n" % (cond[1], name))
449                 sys.exit(1)
450         elif cond[0] == "not":
451             # FIXME: Allow named as well as numbered states
452             try:
453                 return 300 + objnames.index(cond[1]) + 100 * cond[2]
454             except ValueError:
455                 sys.stderr.write("dungeon: unknown object name %s in not clause of %s\n" % (cond[1], name))
456                 sys.exit(1)
457         else:
458             print(cond)
459             raise ValueError
460     # Much more to be done here
461     for (i, (name, loc)) in enumerate(locs):
462         if "travel" in loc:
463             for rule in loc["travel"]:
464                 tt = [i]
465                 dest = dencode(rule["action"], name) + 1000 * cencode(rule.get("cond"), name)
466                 tt.append(dest)
467                 tt += [verbmap[e] for e in rule["verbs"]]
468                 if not rule["verbs"]:
469                     tt.append(1)
470                 ltravel.append(tuple(tt))
471     return (tuple(ltravel), lkeys)
472
473 def get_motions(motions):
474     template = """    {{
475         .words = {},
476     }},
477 """
478     mot_str = ""
479     for motion in motions:
480         contents = motion[1]
481         if contents["words"] == None:
482             mot_str += template.format("NULL")
483             continue
484         c_words = [make_c_string(s) for s in contents["words"]]
485         words_str = "(const char* []) {" + ", ".join(c_words) + "}"
486         mot_str += template.format(words_str)
487     return mot_str
488
489 def get_actions(actions):
490     template = """    {{
491         .words = {},
492         .message = {},
493     }},
494 """
495     act_str = ""
496     for action in actions:
497         contents = action[1]
498         
499         if contents["words"] == None:
500             words_str = "NULL"
501         else:
502             c_words = [make_c_string(s) for s in contents["words"]]
503             words_str = "(const char* []) {" + ", ".join(c_words) + "}"
504
505         if contents["message"] == None:
506             message = "NO_MESSAGE"
507         else:
508             message = contents["message"]
509             
510         act_str += template.format(words_str, message)
511     act_str = act_str[:-1] # trim trailing newline
512     return act_str
513
514 if __name__ == "__main__":
515     with open(yaml_name, "r") as f:
516         db = yaml.load(f)
517
518     locnames = [x[0] for x in db["locations"]]
519     msgnames = [el[0] for el in db["arbitrary_messages"]]
520     objnames = [el[0] for el in db["objects"]]
521     (travel, key) = buildtravel(db["locations"], db["objects"], db["vocabulary"])
522     # FIXME: pack the Section 3 representation into the runtime format.
523
524     c = c_template.format(
525         h_name,
526         get_arbitrary_messages(db["arbitrary_messages"]),
527         get_class_messages(db["classes"]),
528         get_turn_thresholds(db["turn_thresholds"]),
529         get_locations(db["locations"]),
530         get_objects(db["objects"]),
531         get_obituaries(db["obituaries"]),
532         get_hints(db["hints"], db["arbitrary_messages"]),
533         get_condbits(db["locations"]),
534         get_motions(db["motions"]),
535         get_actions(db["actions"]),
536     )
537
538     h = h_template.format(
539         len(db["locations"])-1,
540         len(db["objects"])-1,
541         len(db["hints"]),
542         len(db["classes"])-1,
543         len(db["obituaries"]),
544         len(db["turn_thresholds"]),
545         len(db["actions"]),
546         len(travel),
547         get_refs(db["arbitrary_messages"]),
548         get_refs(db["locations"]),
549         get_refs(db["objects"]),
550         get_refs(db["motions"]),
551         get_refs(db["actions"]),
552         statedefines,
553     )
554
555     with open(h_name, "w") as hf:
556         hf.write(h)
557
558     with open(c_name, "w") as cf:
559         cf.write(c)
560
561 # end