#!/usr/bin/env python3 """ This is the open-adventure dungeon generator. It consumes a YAML description of the dungeon and outputs a dungeon.h and dungeon.c pair of C code files. The nontrivial part of this is the compilation of the YAML for movement rules to the travel array that's actually used by playermove(). Copyright (c) 2017 by Eric S. Raymond SPDX-License-Identifier: BSD-2-clause """ import sys, yaml YAML_NAME = "adventure.yaml" H_NAME = "dungeon.h" C_NAME = "dungeon.c" H_TEMPLATE_PATH = "templates/dungeon.h.tpl" C_TEMPLATE_PATH = "templates/dungeon.c.tpl" DONOTEDIT_COMMENT = "/* Generated from adventure.yaml - do not hand-hack! */\n\n" statedefines = "" def make_c_string(string): """Render a Python string into C string literal format.""" if string is None: return "NULL" string = string.replace("\n", "\\n") string = string.replace("\t", "\\t") string = string.replace('"', '\\"') string = string.replace("'", "\\'") string = '"' + string + '"' return string def get_refs(l): reflist = [x[0] for x in l] ref_str = "" for ref in reflist: ref_str += " {},\n".format(ref) ref_str = ref_str[:-1] # trim trailing newline return ref_str def get_string_group(strings): template = """{{ .strs = {}, .n = {}, }}""" if strings == []: strs = "NULL" else: strs = "(const char* []) {" + ", ".join([make_c_string(s) for s in strings]) + "}" n = len(strings) sg_str = template.format(strs, n) return sg_str def get_arbitrary_messages(arb): template = """ {}, """ arb_str = "" for item in arb: arb_str += template.format(make_c_string(item[1])) arb_str = arb_str[:-1] # trim trailing newline return arb_str def get_class_messages(cls): template = """ {{ .threshold = {}, .message = {}, }}, """ cls_str = "" for item in cls: threshold = item["threshold"] message = make_c_string(item["message"]) cls_str += template.format(threshold, message) cls_str = cls_str[:-1] # trim trailing newline return cls_str def get_turn_thresholds(trn): template = """ {{ .threshold = {}, .point_loss = {}, .message = {}, }}, """ trn_str = "" for item in trn: threshold = item["threshold"] point_loss = item["point_loss"] message = make_c_string(item["message"]) trn_str += template.format(threshold, point_loss, message) trn_str = trn_str[:-1] # trim trailing newline return trn_str def get_locations(loc): template = """ {{ // {}: {} .description = {{ .small = {}, .big = {}, }}, .sound = {}, .loud = {}, }}, """ loc_str = "" for (i, item) in enumerate(loc): short_d = make_c_string(item[1]["description"]["short"]) long_d = make_c_string(item[1]["description"]["long"]) sound = item[1].get("sound", "SILENT") loud = "true" if item[1].get("loud") else "false" loc_str += template.format(i, item[0], short_d, long_d, sound, loud) loc_str = loc_str[:-1] # trim trailing newline return loc_str def get_objects(obj): template = """ {{ // {}: {} .words = {}, .inventory = {}, .plac = {}, .fixd = {}, .is_treasure = {}, .descriptions = (const char* []) {{ {} }}, .sounds = (const char* []) {{ {} }}, .texts = (const char* []) {{ {} }}, .changes = (const char* []) {{ {} }}, }}, """ obj_str = "" for (i, item) in enumerate(obj): attr = item[1] try: words_str = get_string_group(attr["words"]) except KeyError: words_str = get_string_group([]) i_msg = make_c_string(attr["inventory"]) descriptions_str = "" if attr["descriptions"] is None: descriptions_str = " " * 12 + "NULL," else: labels = [] for l_msg in attr["descriptions"]: descriptions_str += " " * 12 + make_c_string(l_msg) + ",\n" for label in attr.get("states", []): labels.append(label) descriptions_str = descriptions_str[:-1] # trim trailing newline if labels: global statedefines statedefines += "/* States for %s */\n" % item[0] for (n, label) in enumerate(labels): statedefines += "#define %s\t%d\n" % (label, n) statedefines += "\n" sounds_str = "" if attr.get("sounds") is None: sounds_str = " " * 12 + "NULL," else: for l_msg in attr["sounds"]: sounds_str += " " * 12 + make_c_string(l_msg) + ",\n" sounds_str = sounds_str[:-1] # trim trailing newline texts_str = "" if attr.get("texts") is None: texts_str = " " * 12 + "NULL," else: for l_msg in attr["texts"]: texts_str += " " * 12 + make_c_string(l_msg) + ",\n" texts_str = texts_str[:-1] # trim trailing newline changes_str = "" if attr.get("changes") is None: changes_str = " " * 12 + "NULL," else: for l_msg in attr["changes"]: changes_str += " " * 12 + make_c_string(l_msg) + ",\n" changes_str = changes_str[:-1] # trim trailing newline locs = attr.get("locations", ["LOC_NOWHERE", "LOC_NOWHERE"]) immovable = attr.get("immovable", False) try: if isinstance(locs, str): locs = [locs, -1 if immovable else 0] except IndexError: sys.stderr.write("dungeon: unknown object location in %s\n" % locs) sys.exit(1) treasure = "true" if attr.get("treasure") else "false" obj_str += template.format(i, item[0], words_str, i_msg, locs[0], locs[1], treasure, descriptions_str, sounds_str, texts_str, changes_str) obj_str = obj_str[:-1] # trim trailing newline return obj_str def get_obituaries(obit): template = """ {{ .query = {}, .yes_response = {}, }}, """ obit_str = "" for o in obit: query = make_c_string(o["query"]) yes = make_c_string(o["yes_response"]) obit_str += template.format(query, yes) obit_str = obit_str[:-1] # trim trailing newline return obit_str def get_hints(hnt): template = """ {{ .number = {}, .penalty = {}, .turns = {}, .question = {}, .hint = {}, }}, """ hnt_str = "" for member in hnt: item = member["hint"] number = item["number"] penalty = item["penalty"] turns = item["turns"] question = make_c_string(item["question"]) hint = make_c_string(item["hint"]) hnt_str += template.format(number, penalty, turns, question, hint) hnt_str = hnt_str[:-1] # trim trailing newline return hnt_str def get_condbits(locations): cnd_str = "" for (name, loc) in locations: conditions = loc["conditions"] hints = loc.get("hints") or [] flaglist = [] for flag in conditions: if conditions[flag]: flaglist.append(flag) line = "|".join([("(1<500 message N-500 from section 6 is printed, # and he stays wherever he is. # Meanwhile, M specifies the conditions on the motion. # If M=0 it's unconditional. # If 0 500: desttype = "dest_speak" destval = msgnames[dest - 500] else: desttype = "dest_special" destval = locnames[dest - 300] travel.append([len(tkey)-1, locnames[len(tkey)-1], rule.pop(0), condtype, condarg1, condarg2, desttype, destval, "true" if nodwarves else "false", "false"]) travel[-1][-1] = "true" return (travel, tkey) def get_travel(travel): template = """ {{ // from {}: {} .motion = {}, .condtype = {}, .condarg1 = {}, .condarg2 = {}, .desttype = {}, .destval = {}, .nodwarves = {}, .stop = {}, }}, """ out = "" for entry in travel: out += template.format(*entry) out = out[:-1] # trim trailing newline return out if __name__ == "__main__": with open(YAML_NAME, "r") as f: db = yaml.safe_load(f) locnames = [x[0] for x in db["locations"]] msgnames = [el[0] for el in db["arbitrary_messages"]] objnames = [el[0] for el in db["objects"]] motionnames = [el[0] for el in db["motions"]] (travel, tkey) = buildtravel(db["locations"], db["objects"]) ignore = "" try: with open(H_TEMPLATE_PATH, "r") as htf: # read in dungeon.h template h_template = DONOTEDIT_COMMENT + htf.read() with open(C_TEMPLATE_PATH, "r") as ctf: # read in dungeon.c template c_template = DONOTEDIT_COMMENT + ctf.read() except IOError as e: print('ERROR: reading template failed ({})'.format(e.strerror)) sys.exit(-1) c = c_template.format( h_file = H_NAME, arbitrary_messages = get_arbitrary_messages(db["arbitrary_messages"]), classes = get_class_messages(db["classes"]), turn_thresholds = get_turn_thresholds(db["turn_thresholds"]), locations = get_locations(db["locations"]), objects = get_objects(db["objects"]), obituaries = get_obituaries(db["obituaries"]), hints = get_hints(db["hints"]), conditions = get_condbits(db["locations"]), motions = get_motions(db["motions"]), actions = get_actions(db["actions"]), tkeys = bigdump(tkey), travel = get_travel(travel), ignore = ignore ) # 0-origin index of birds's last song. Bird should # die after player hears this. deathbird = len(dict(db["objects"])["BIRD"]["sounds"]) - 1 h = h_template.format( num_locations = len(db["locations"])-1, num_objects = len(db["objects"])-1, num_hints = len(db["hints"]), num_classes = len(db["classes"])-1, num_deaths = len(db["obituaries"]), num_thresholds = len(db["turn_thresholds"]), num_motions = len(db["motions"]), num_actions = len(db["actions"]), num_travel = len(travel), num_keys = len(tkey), bird_endstate = deathbird, arbitrary_messages = get_refs(db["arbitrary_messages"]), locations = get_refs(db["locations"]), objects = get_refs(db["objects"]), motions = get_refs(db["motions"]), actions = get_refs(db["actions"]), state_definitions = statedefines ) with open(H_NAME, "w") as hf: hf.write(h) with open(C_NAME, "w") as cf: cf.write(c) # end