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