#!/usr/bin/env python3 # SPDX-FileCopyrightText: Eric S. Raymond # SPDX-License-Identifier: BSD-2-Clause """ 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(). """ # pylint: disable=consider-using-f-string,line-too-long,invalid-name,missing-function-docstring,too-many-branches,global-statement,multiple-imports,too-many-locals,too-many-statements,too-many-nested-blocks,no-else-return,raise-missing-from,redefined-outer-name 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", encoding='ascii', errors='surrogateescape') 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", encoding='ascii', errors='surrogateescape') as htf: # read in dungeon.h template h_template = DONOTEDIT_COMMENT + htf.read() with open(C_TEMPLATE_PATH, "r", encoding='ascii', errors='surrogateescape') 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", encoding='ascii', errors='surrogateescape') as hf: hf.write(h) with open(C_NAME, "w", encoding='ascii', errors='surrogateescape') as cf: cf.write(c) # end