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