X-Git-Url: https://jxself.org/git/?p=open-adventure.git;a=blobdiff_plain;f=make_dungeon.py;h=63eee94ed241863772ee71465c6a25b7d07e7c7c;hp=c394b5f516d20c0b4d74017a17b84d12ce1b21d8;hb=eb49f4d0d2f211117ea4da472ccfabb71b9f8689;hpb=f1b37ea1636d84fb253648b71fb2eca4b2b13ea9 diff --git a/make_dungeon.py b/make_dungeon.py index c394b5f..63eee94 100755 --- a/make_dungeon.py +++ b/make_dungeon.py @@ -1,285 +1,26 @@ -#!/usr/bin/python3 +#!/usr/bin/env python -# This is the new open-adventure dungeon generator. It'll eventually -# replace the existing dungeon.c It currently outputs a .h and .c pair -# for C code. +# 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(). This program first compiles the YAML to a form -# identical to the data in section 3 of the old adventure.text file, -# then a second stage unpacks that data into the travel array. +# playermove(). # -# Here are the rules of the intermediate form: -# -# Each row of data contains a location number (X), a second -# location number (Y), and a list of motion numbers (see section 4). -# each motion represents a verb which will go to Y if currently at X. -# Y, in turn, is interpreted as follows. Let M=Y/1000, N=Y mod 1000. -# If N<=300 it is the location to go to. -# If 300500 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 -#include - -#define SILENT -1 /* no sound */ - -/* Symbols for cond bits */ -#define COND_LIT 0 /* Light */ -#define COND_OILY 1 /* If bit 2 is on: on for oil, off for water */ -#define COND_FLUID 2 /* Liquid asset, see bit 1 */ -#define COND_NOARRR 3 /* Pirate doesn't go here unless following */ -#define COND_NOBACK 4 /* Cannot use "back" to move away */ -#define COND_ABOVE 5 -#define COND_DEEP 6 /* Deep - e.g where dwarves are active */ -#define COND_FOREST 7 /* In the forest */ -#define COND_FORCED 8 /* Only one way in or out of here */ -/* Bits past 10 indicate areas of interest to "hint" routines */ -#define COND_HBASE 10 /* Base for location hint bits */ -#define COND_HCAVE 11 /* Trying to get into cave */ -#define COND_HBIRD 12 /* Trying to catch bird */ -#define COND_HSNAKE 13 /* Trying to deal with snake */ -#define COND_HMAZE 14 /* Lost in maze */ -#define COND_HDARK 15 /* Pondering dark room */ -#define COND_HWITT 16 /* At Witt's End */ -#define COND_HCLIFF 17 /* Cliff with urn */ -#define COND_HWOODS 18 /* Lost in forest */ -#define COND_HOGRE 19 /* Trying to deal with ogre */ -#define COND_HJADE 20 /* Found all treasures except jade */ - -typedef struct {{ - const char** strs; - const int n; -}} string_group_t; - -typedef struct {{ - const string_group_t words; - const char* inventory; - int plac, fixd; - bool is_treasure; - const char** descriptions; - const char** sounds; - const char** texts; - const char** changes; -}} object_t; - -typedef struct {{ - const char* small; - const char* big; -}} descriptions_t; - -typedef struct {{ - descriptions_t description; - const long sound; - const bool loud; -}} location_t; - -typedef struct {{ - const char* query; - const char* yes_response; -}} obituary_t; - -typedef struct {{ - const int threshold; - const int point_loss; - const char* message; -}} turn_threshold_t; - -typedef struct {{ - const int threshold; - const char* message; -}} class_t; - -typedef struct {{ - const int number; - const int turns; - const int penalty; - const char* question; - const char* hint; -}} hint_t; - -typedef struct {{ - const string_group_t words; -}} motion_t; - -typedef struct {{ - const string_group_t words; - const long message; -}} action_t; - -typedef struct {{ - const string_group_t words; - const char* message; -}} special_t; - -typedef struct {{ - const long motion; - const long cond; - const long dest; - const bool nodwarves; - const bool stop; -}} travelop_t; - -/* Abstract out the encoding of words in the travel array. Gives us - * some hope of getting to a less cryptic representation than we - * inherited from FORTRAN, someday. To understand these, read the - * encoding description for travel. - */ -#define T_TERMINATE(entry) ((entry).motion == 1) -#define L_SPEAK(loc) ((loc) - 500) - -extern const location_t locations[]; -extern const object_t objects[]; -extern const char* arbitrary_messages[]; -extern const class_t classes[]; -extern const turn_threshold_t turn_thresholds[]; -extern const obituary_t obituaries[]; -extern const hint_t hints[]; -extern long conditions[]; -extern const motion_t motions[]; -extern const action_t actions[]; -extern const special_t specials[]; -extern const travelop_t travel[]; -extern const long tkey[]; -extern const char *ignore; - -#define NLOCATIONS {} -#define NOBJECTS {} -#define NHINTS {} -#define NCLASSES {} -#define NDEATHS {} -#define NTHRESHOLDS {} -#define NMOTIONS {} -#define NACTIONS {} -#define NSPECIALS {} -#define NTRAVEL {} -#define NKEYS {} - -enum arbitrary_messages_refs {{ -{} -}}; - -enum locations_refs {{ -{} -}}; - -enum object_refs {{ -{} -}}; - -enum motion_refs {{ -{} -}}; - -enum action_refs {{ -{} -}}; - -enum special_refs {{ -{} -}}; - -/* State definitions */ - -{} -#endif /* end DUNGEON_H */ -""" - -c_template = """/* Generated from adventure.yaml - do not hand-hack! */ - -#include "{}" - -const char* arbitrary_messages[] = {{ -{} -}}; - -const class_t classes[] = {{ -{} -}}; - -const turn_threshold_t turn_thresholds[] = {{ -{} -}}; - -const location_t locations[] = {{ -{} -}}; - -const object_t objects[] = {{ -{} -}}; - -const obituary_t obituaries[] = {{ -{} -}}; - -const hint_t hints[] = {{ -{} -}}; - -long conditions[] = {{ -{} -}}; - -const motion_t motions[] = {{ -{} -}}; - -const action_t actions[] = {{ -{} -}}; - -const special_t specials[] = {{ -{} -}}; - -const long tkey[] = {{{}}}; +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" -const travelop_t travel[] = {{ -{} -}}; - -const char *ignore = \"{}\"; +DONOTEDIT_COMMENT = "/* Generated from adventure.yaml - do not hand-hack! */\n\n" -/* end */ -""" +statedefines = "" def make_c_string(string): """Render a Python string into C string literal format.""" @@ -353,7 +94,7 @@ def get_turn_thresholds(trn): return trn_str def get_locations(loc): - template = """ {{ // {} + template = """ {{ // {}: {} .description = {{ .small = {}, .big = {}, @@ -368,12 +109,12 @@ def get_locations(loc): 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, short_d, long_d, sound, loud) + 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 = """ {{ // {} + template = """ {{ // {}: {} .words = {}, .inventory = {}, .plac = {}, @@ -407,18 +148,15 @@ def get_objects(obj): else: labels = [] for l_msg in attr["descriptions"]: - if not isinstance(l_msg, str): - labels.append(l_msg) - l_msg = l_msg[1] 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 (i, (label, message)) in enumerate(labels): - if len(message) >= 45: - message = message[:45] + "..." - statedefines += "#define %s\t%d /* %s */\n" % (label, i, message) + for (n, label) in enumerate(labels): + statedefines += "#define %s\t%d\n" % (label, n) statedefines += "\n" sounds_str = "" if attr.get("sounds") == None: @@ -445,14 +183,12 @@ def get_objects(obj): immovable = attr.get("immovable", False) try: if type(locs) == str: - locs = [locnames.index(locs), -1 if immovable else 0] - else: - locs = [locnames.index(x) for x in locs] + 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, words_str, i_msg, locs[0], locs[1], treasure, descriptions_str, sounds_str, texts_str, changes_str) + 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 @@ -470,7 +206,7 @@ def get_obituaries(obit): obit_str = obit_str[:-1] # trim trailing newline return obit_str -def get_hints(hnt, arb): +def get_hints(hnt): template = """ {{ .number = {}, .penalty = {}, @@ -480,7 +216,6 @@ def get_hints(hnt, arb): }}, """ hnt_str = "" - md = dict(arb) for member in hnt: item = member["hint"] number = item["number"] @@ -536,40 +271,12 @@ def get_actions(actions): template = """ {{ .words = {}, .message = {}, + .noaction = {}, }}, """ act_str = "" for action in actions: contents = action[1] - - if contents["words"] == None: - words_str = get_string_group([]) - else: - words_str = get_string_group(contents["words"]) - - if contents["message"] == None: - message = "NO_MESSAGE" - else: - message = contents["message"] - - act_str += template.format(words_str, message) - global ignore - if contents.get("oldstyle", True) == False: - for word in contents["words"]: - if len(word) == 1: - ignore += word.upper() - act_str = act_str[:-1] # trim trailing newline - return act_str - -def get_specials(specials): - template = """ {{ - .words = {}, - .message = {}, - }}, -""" - spc_str = "" - for special in specials: - contents = special[1] if contents["words"] == None: words_str = get_string_group([]) @@ -581,14 +288,19 @@ def get_specials(specials): else: message = make_c_string(contents["message"]) - spc_str += template.format(words_str, message) + if contents.get("noaction") == None: + noaction = "false" + else: + noaction = "true" + + act_str += template.format(words_str, message, noaction) global ignore if contents.get("oldstyle", True) == False: for word in contents["words"]: if len(word) == 1: ignore += word.upper() - spc_str = spc_str[:-1] # trim trailing newline - return spc_str + act_str = act_str[:-1] # trim trailing newline + return act_str def bigdump(arr): out = "" @@ -602,6 +314,51 @@ def bigdump(arr): return out def buildtravel(locs, objs): + assert len(locs) <= 300 + assert len(objs) <= 100 + # THIS CODE IS WAAAY MORE COMPLEX THAN IT NEEDS TO BE. It's the + # result of a massive refactoring exercise that concentrated all + # the old nastiness in one spot. It hasn't been finally simplified + # because there's no need to do it until one of the assertions + # fails. Hint: if you try cleaning this up, the acceptance test is + # simple - the output dungeon.c must not change. + # + # This function first compiles the YAML to a form identical to the + # data in section 3 of the old adventure.text file, then a second + # stage unpacks that data into the travel array. Here are the + # rules of that intermediate form: + # + # Each row of data contains a location number (X), a second + # location number (Y), and a list of motion numbers (see section 4). + # each motion represents a verb which will go to Y if currently at X. + # Y, in turn, is interpreted as follows. Let M=Y/1000, N=Y mod 1000. + # If N<=300 it is the location to go to. + # If 300500 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 = """ {{ + template = """ {{ // from {}: {} .motion = {}, - .cond = {}, - .dest = {}, + .condtype = {}, + .condarg1 = {}, + .condarg2 = {}, + .desttype = {}, + .destval = {}, .nodwarves = {}, .stop = {}, }}, """ out = "" for entry in travel: - out += template.format(*entry).lower() + out += template.format(*entry) out = out[:-1] # trim trailing newline return out if __name__ == "__main__": - with open(yaml_name, "r") as f: + with open(YAML_NAME, "r") as f: db = yaml.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)) + exit(-1) + c = c_template.format( - h_name, - get_arbitrary_messages(db["arbitrary_messages"]), - get_class_messages(db["classes"]), - get_turn_thresholds(db["turn_thresholds"]), - get_locations(db["locations"]), - get_objects(db["objects"]), - get_obituaries(db["obituaries"]), - get_hints(db["hints"], db["arbitrary_messages"]), - get_condbits(db["locations"]), - get_motions(db["motions"]), - get_actions(db["actions"]), - get_specials(db["specials"]), - bigdump(tkey), - get_travel(travel), - ignore, + 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( - len(db["locations"])-1, - len(db["objects"])-1, - len(db["hints"]), - len(db["classes"])-1, - len(db["obituaries"]), - len(db["turn_thresholds"]), - len(db["motions"]), - len(db["actions"]), - len(db["specials"]), - len(travel), - len(tkey), - get_refs(db["arbitrary_messages"]), - get_refs(db["locations"]), - get_refs(db["objects"]), - get_refs(db["motions"]), - get_refs(db["actions"]), - get_refs(db["specials"]), - statedefines, + 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: + with open(H_NAME, "w") as hf: hf.write(h) - with open(c_name, "w") as cf: + with open(C_NAME, "w") as cf: cf.write(c) # end