Created
May 8, 2024 04:20
-
-
Save Myriachan/083b2a9d45f3b3a630f27643168ead0e to your computer and use it in GitHub Desktop.
Script to see whether a Beyond Chaos seed allows getting every item
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import sys | |
| import re | |
| # Flags for the item map | |
| FLAG_SAFE = 1 << 0 | |
| FLAG_STEAL = 1 << 1 | |
| FLAG_DROP = 1 << 2 | |
| FLAG_MORPH = 1 << 3 | |
| FLAG_TREASURE = 1 << 4 | |
| FLAG_SHOP = 1 << 5 | |
| FLAG_UNCURSE = 1 << 6 | |
| FLAG_COLISEUM = 1 << 7 | |
| FLAG_SAFE_VIA_UNCURSE = 1 << 8 | |
| SAFE_FLAGS = FLAG_SAFE | FLAG_SAFE_VIA_UNCURSE | |
| PROPAGATE_FLAGS = SAFE_FLAGS | FLAG_COLISEUM | |
| # Locations accessible indefinitely | |
| WOR_SAFE_LOCATIONS = """ | |
| Ancient Castle | |
| Cave on the Veldt | |
| Daryl's Tomb | |
| Fanatics Tower | |
| Figaro Castle Basement | |
| Final Dungeon | |
| Hidon's Cave | |
| Mt. Zozo | |
| Narshe (WoR) | |
| Owzer's Mansion | |
| Phoenix Cave | |
| South Figaro Cave (WoR) | |
| South Figaro Basement | |
| Yeti's Lair | |
| Zone Eater's Belly | |
| Zozo | |
| """ | |
| # Shops accessible indefinitely | |
| WOR_SAFE_SHOPS = """ | |
| ALBROOK (WOR) ARMOR | |
| ALBROOK (WOR) ITEMS | |
| ALBROOK (WOR) RELICS | |
| ALBROOK (WOR) WEAPONS | |
| FIGARO CASTLE (WOR) ITEMS | |
| JIDOOR (WOR) ARMOR | |
| JIDOOR (WOR) ITEMS | |
| JIDOOR (WOR) RELICS | |
| JIDOOR (WOR) WEAPONS | |
| KOHLINGEN (WOR) ARMOR | |
| KOHLINGEN (WOR) ITEMS | |
| KOHLINGEN (WOR) WEAPONS | |
| MARANDA (WOR) ARMOR | |
| MARANDA (WOR) WEAPONS | |
| NIKEAH (WOR) ARMOR | |
| NIKEAH (WOR) ITEMS | |
| NIKEAH (WOR) RELICS | |
| NIKEAH (WOR) WEAPONS | |
| SOUTH FIGARO (WOR) ARMOR | |
| SOUTH FIGARO (WOR) ITEMS | |
| SOUTH FIGARO (WOR) RELICS | |
| SOUTH FIGARO (WOR) WEAPONS | |
| THAMASA (WOR) ARMOR | |
| THAMASA (WOR) ITEMS | |
| THAMASA (WOR) RELICS | |
| THAMASA (WOR) WEAPONS | |
| TZEN (WOR) ARMOR | |
| TZEN (WOR) ITEMS | |
| TZEN (WOR) RELICS | |
| TZEN (WOR) WEAPONS | |
| """ | |
| # Final bosses, whose items can't be kept. | |
| # Kefka and Fake Kefka are in code. | |
| MONSTER_EXCLUDE = """ | |
| Face | |
| Girl | |
| Hit | |
| Magic | |
| Long Arm | |
| Short Arm | |
| Sleep | |
| Tiger | |
| Tools | |
| """ | |
| # Monster parameters that should be considered lists. | |
| MONSTER_PARAM_LISTS = """ | |
| OTHER | |
| SKILLS | |
| SKETCH | |
| STEAL | |
| DROPS | |
| MORPH | |
| LOCATION | |
| """ | |
| # Monster parameter lists that can contain item names. | |
| # Each entry must also be in MONSTER_PARAM_LISTS. | |
| MONSTER_PARAM_ITEMS = { | |
| "STEAL" : FLAG_STEAL, | |
| "DROPS" : FLAG_DROP, | |
| "MORPH" : FLAG_MORPH, | |
| } | |
| # Cursed Shield possible names. | |
| CURSED_SHIELD_NAMES = """ | |
| Cursed Shld | |
| Cursed Shld? | |
| """ | |
| # Paladin Shield possible names. | |
| PALADIN_SHIELD_NAMES = """ | |
| Paladin Shld | |
| Paladin Shl? | |
| """ | |
| def strings_to_lookup_dict(data): | |
| temp = list(filter(lambda x: x != "", data.split("\n"))) | |
| return {value : None for dontcare, value in enumerate(temp)} | |
| WOR_SAFE_LOCATIONS = strings_to_lookup_dict(WOR_SAFE_LOCATIONS) | |
| WOR_SAFE_SHOPS = strings_to_lookup_dict(WOR_SAFE_SHOPS) | |
| MONSTER_EXCLUDE = strings_to_lookup_dict(MONSTER_EXCLUDE) | |
| MONSTER_PARAM_LISTS = strings_to_lookup_dict(MONSTER_PARAM_LISTS) | |
| CURSED_SHIELD_NAMES = strings_to_lookup_dict(CURSED_SHIELD_NAMES) | |
| PALADIN_SHIELD_NAMES = strings_to_lookup_dict(PALADIN_SHIELD_NAMES) | |
| def is_monster_excluded(monster): | |
| if monster["NAME"] in MONSTER_EXCLUDE: | |
| return True | |
| # Don't exclude Kefka at Narshe. | |
| if monster["NAME"] == "Kefka" and monster["LEVEL"] >= 40: | |
| return True | |
| # Fake Kefka should be excluded, too, but how? | |
| return False | |
| def is_location_safe(location): | |
| if location in WOR_SAFE_LOCATIONS: | |
| return True | |
| if location[0:14] == "World of Ruin ": | |
| return True | |
| return False | |
| def flags_to_string(flags): | |
| l = [] | |
| if (flags & SAFE_FLAGS) != 0: | |
| l += ["safe"] | |
| if (flags & FLAG_TREASURE) != 0: | |
| l += ["treasure"] | |
| if (flags & FLAG_SHOP) != 0: | |
| l += ["shop"] | |
| if (flags & FLAG_STEAL) != 0: | |
| l += ["steal"] | |
| if (flags & FLAG_DROP) != 0: | |
| l += ["drop"] | |
| if (flags & FLAG_MORPH) != 0: | |
| l += ["morph"] | |
| if (flags & FLAG_UNCURSE) != 0: | |
| l += ["uncurse"] | |
| if (flags & FLAG_COLISEUM) != 0: | |
| l += ["coliseum"] | |
| return ", ".join(l) | |
| class Reader: | |
| def __init__(self, lines): | |
| self.lines = lines | |
| self.lineno = 1 | |
| def next(self): | |
| if self.lineno - 1 >= len(self.lines): | |
| raise IndexError("read past end of file") | |
| self.lineno += 1 | |
| return self.lines[self.lineno - 2] | |
| def at_end(self): | |
| return self.lineno - 1 == len(self.lines) | |
| def process(document): | |
| item_list = [] | |
| item_map = {} | |
| item_junction_fixup = {} | |
| coliseum_map = {} | |
| regex_section = re.compile(r"^-\d+- (.*)$") | |
| def is_section_name(l, n): | |
| m = regex_section.match(l) | |
| if m == None: | |
| return False | |
| return m.group(1) == n | |
| while True: | |
| if document.next() == "============================================================": | |
| if is_section_name(document.next(), "COLOSSEUM"): | |
| if document.next() != "------------------------------------------------------------": | |
| raise ValueError("bad coliseum block") | |
| break | |
| while True: | |
| line = document.next() | |
| if line == "": | |
| break | |
| # Wall Ring? -> Back Guard? : LV 26 Migril | |
| if line[12:16] != " -> ": | |
| raise ValueError("invalid coliseum line (1)") | |
| if line[28:36] != " : LV ": | |
| raise ValueError("invalid coliseum line (2)") | |
| from_item = line[0:12].strip() | |
| to_item = line[16:28].strip() | |
| if from_item in coliseum_map: | |
| raise ValueError("duplicate item %s in coliseum" % from_item) | |
| item_list += [from_item] | |
| coliseum_map[from_item] = to_item | |
| item_map[from_item] = 0 | |
| # Build mapping table for junctioned item names, ugh. | |
| # Need this because coliseum and shop lists don't have | |
| # the exclamation points added. | |
| for item in item_list: | |
| item_junction_fixup[item] = item | |
| if item[-1] == "?": | |
| if len(item) - 1 <= 10: | |
| new_item = item[:-1] + "?!" | |
| else: | |
| new_item = item[:-1] + "!" | |
| else: | |
| new_item = item[:11] + "!" | |
| item_junction_fixup[new_item] = item | |
| # Wait for monster list. | |
| while True: | |
| if document.next() == "============================================================": | |
| if is_section_name(document.next(), "MONSTERS"): | |
| if document.next() != "------------------------------------------------------------": | |
| raise ValueError("bad monster block") | |
| break | |
| line = document.next() | |
| if line != "": | |
| raise ValueError("missing blank before monster blocks") | |
| # Abidettu (Level 23) | |
| regex_monster_name = re.compile(r"^(.*) \(Level (\d+)\)$") | |
| # \--------------------------------/ | |
| regex_monster_stats_end = re.compile(r"^\\-+\/$") | |
| # SKETCH: Battle, Cling Ball | |
| regex_monster_param = re.compile(r"^([^:]+): *(.*)$") | |
| # MORPH (50%) | |
| regex_monster_morph = re.compile(r"^MORPH \((\d+)%\)$") | |
| while True: | |
| line = document.next() | |
| if line == "": | |
| break | |
| elif line == "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~": | |
| pass | |
| elif line == "============================================================": | |
| break | |
| else: | |
| raise ValueError("bad monster entry: " + line) | |
| line = document.next() | |
| match = regex_monster_name.match(line) | |
| if match == None: | |
| raise ValueError("bad monster name line: " + line) | |
| monster = { "NAME" : match.group(1), "LEVEL" : int(match.group(2)) } | |
| while True: | |
| line = document.next() | |
| if regex_monster_stats_end.match(line) != None: | |
| break | |
| # Parse the monster information. | |
| while True: | |
| line = document.next() | |
| if line == "": | |
| break | |
| match = regex_monster_param.match(line) | |
| if match == None: | |
| raise ValueError("bad monster parameter for " + monster["NAME"]) | |
| # Check for "MORPH (3%)" and change to MORPH plus MORPH%. | |
| param_name = match.group(1) | |
| param_value = match.group(2) | |
| match = regex_monster_morph.match(param_name) | |
| if match != None: | |
| param_name = "MORPH" | |
| monster["MORPH%"] = int(match.group(1)) | |
| # Check for a parameter type that should be interpreted as a list. | |
| if param_name in MONSTER_PARAM_LISTS: | |
| param_value = param_value.split(", ") | |
| # Turn empty string into [] rather than [""] | |
| if len(param_value) == 1: | |
| if param_value[0] == "": | |
| param_value = [] | |
| # Remove exclamation marks, since the Coliseum list doesn't have them! | |
| monster[param_name] = param_value | |
| # Is this a monster we don't want to consider? These are final bosses. | |
| # Sadly, excluding Fake Kefka would be complicated. | |
| if is_monster_excluded(monster): | |
| #print("%s (%d): on excluded list" % (monster["NAME"], monster["LEVEL"])) | |
| continue | |
| # Is this monster present in a safe location? | |
| safe_monster = False | |
| if "LOCATION" in monster: | |
| for location in monster["LOCATION"]: | |
| if is_location_safe(location): | |
| safe_monster = True | |
| break | |
| # Flag all the items available from this monster. | |
| for param in monster.keys(): | |
| # Skip morph for unsafe locations, because maybe no Ragnarok yet. | |
| if (param == "MORPH") and (not safe_monster): | |
| continue | |
| if param in MONSTER_PARAM_ITEMS: | |
| flags_to_set = MONSTER_PARAM_ITEMS[param] | |
| if safe_monster: | |
| flags_to_set |= FLAG_SAFE | |
| for item in monster[param]: | |
| # Monster info has the junction !. | |
| item = item_junction_fixup[item] | |
| item_map[item] |= flags_to_set | |
| # Wait for shop list. | |
| while True: | |
| if document.next() == "============================================================": | |
| if is_section_name(document.next(), "SHOPS"): | |
| if document.next() != "------------------------------------------------------------": | |
| raise ValueError("bad shop block") | |
| if document.next() != "": | |
| raise ValueError("bad shop block") | |
| break | |
| # Shop processing. | |
| # ThunderBlad? 18000 | |
| regex_shop_item = re.compile(r"^(.{12}) +\d+$") | |
| while True: | |
| shop_name = document.next() | |
| if shop_name == "": | |
| break | |
| # Shop item list. | |
| while True: | |
| line = document.next() | |
| if line == "": | |
| break | |
| if line == "Discounts for female characters.": | |
| continue | |
| match = regex_shop_item.match(line) | |
| if match == None: | |
| raise ValueError("bad shop item") | |
| item = match.group(1).rstrip() | |
| flags_to_set = FLAG_SHOP | |
| if shop_name in WOR_SAFE_SHOPS: | |
| flags_to_set |= FLAG_SAFE | |
| #print("shop: %s (%d)" % (item, flags_to_set)) | |
| item_map[item] |= flags_to_set | |
| # Wait for treasure list. | |
| while True: | |
| if document.next() == "============================================================": | |
| if is_section_name(document.next(), "TREASURE CHESTS"): | |
| if document.next() != "------------------------------------------------------------": | |
| raise ValueError("bad treasure chest block") | |
| if document.next() != "": | |
| raise ValueError("bad treasure chest block") | |
| break | |
| # Treasure processing. We skip lines with * because they | |
| # are not necessarily available. | |
| # Empty! (238) | |
| regex_treasure_empty = re.compile(r"^Empty! \(\d+\)$") | |
| # Treasure 227: Air Anchor | |
| regex_treasure_entry = re.compile(r"^(\*?(?:Treasure|Enemy)) \d+: (.*)$") | |
| # 20400 GP | |
| regex_treasure_money = re.compile(r"^\d+ GP$") | |
| while True: | |
| area_name = document.next() | |
| if area_name == "": | |
| break | |
| # Treasure chest list. | |
| while True: | |
| line = document.next() | |
| if line == "": | |
| break | |
| # Skip "Empty!" | |
| if regex_treasure_empty.match(line) != None: | |
| continue | |
| # Skip Enemy, *Enemy, *Treasure. | |
| match = regex_treasure_entry.match(line) | |
| if match == None: | |
| raise ValueError("bad treasure chest: " + line) | |
| type = match.group(1) | |
| item = match.group(2) | |
| if type != "Treasure": | |
| continue | |
| # Skip money | |
| if regex_treasure_money.match(item) != None: | |
| continue | |
| flags_to_set = FLAG_TREASURE | |
| #print("treasure: %s %s %s (%d)" % (area_name, type, item, flags_to_set)) | |
| # Monster info has the junction !. | |
| item = item_junction_fixup[item] | |
| item_map[item] |= flags_to_set | |
| # Wait for secret item list. | |
| while True: | |
| if document.next() == "============================================================": | |
| if is_section_name(document.next(), "SECRET ITEMS"): | |
| if document.next() != "------------------------------------------------------------": | |
| raise ValueError("bad secret item block") | |
| break | |
| # Secret item list. | |
| while True: | |
| if document.at_end(): | |
| break | |
| line = document.next() | |
| if line == "": | |
| continue | |
| if line == "============================================================": | |
| break | |
| # If not previously encountered, add this item to the list. | |
| if line not in item_map: | |
| item_list += [line] | |
| item_map[line] = 0 | |
| coliseum_map[line] = line # Map it to itself I guess | |
| # Finally, we're done reading the file. | |
| item_list = sorted(item_list) | |
| # Consider uncursing the Cursed Shield. | |
| cursed_shld_name = None | |
| for item in CURSED_SHIELD_NAMES: | |
| if item in item_map: | |
| cursed_shld_name = item | |
| break | |
| paladin_shld_name = None | |
| for item in PALADIN_SHIELD_NAMES: | |
| if item in item_map: | |
| paladin_shld_name = item | |
| break | |
| item_map[paladin_shld_name] |= FLAG_UNCURSE | item_map[cursed_shld_name] | |
| # Compute the transitive closure using the coliseum. | |
| more = True | |
| loop_count = 0 | |
| while more: | |
| more = False | |
| loop_count += 1 | |
| # Process coliseum | |
| for from_item in item_list: | |
| to_item = coliseum_map[from_item] | |
| if to_item == from_item: | |
| continue | |
| from_flags = item_map[from_item] | |
| to_flags = item_map[to_item] | |
| if from_flags == 0: | |
| continue | |
| new_flags = (from_flags & PROPAGATE_FLAGS) | FLAG_COLISEUM | |
| if (to_flags & new_flags) == new_flags: | |
| continue | |
| item_map[to_item] |= new_flags | |
| more = True | |
| # If Cursed Shld is safe, flag Paladin Shld as safe-via-uncurse. | |
| if (item_map[cursed_shld_name] & SAFE_FLAGS) != 0: | |
| if (item_map[paladin_shld_name] & FLAG_SAFE_VIA_UNCURSE) == 0: | |
| item_map[paladin_shld_name] |= FLAG_SAFE_VIA_UNCURSE | |
| more = True | |
| #print("loop count: %d" % loop_count) | |
| # Print results! | |
| #print("") | |
| #print("*** SAFE ITEMS ***") | |
| #for item in item_list: | |
| # if (item_map[item] & FLAG_SAFE) == FLAG_SAFE: | |
| # print(" " + item) | |
| print("") | |
| print("*** SAFE VIA UNCURSE ITEMS ***") | |
| for item in item_list: | |
| if (item_map[item] & SAFE_FLAGS) == FLAG_SAFE_VIA_UNCURSE: | |
| print(" %s (%s)" % (item, flags_to_string(item_map[item]))) | |
| print("") | |
| print("*** UNSAFE ITEMS ***") | |
| for item in item_list: | |
| if ((item_map[item] & SAFE_FLAGS) == 0) and (item_map[item] != 0): | |
| print(" %s (%s)" % (item, flags_to_string(item_map[item]))) | |
| print("") | |
| print("*** MISSING ITEMS ***") | |
| for item in item_list: | |
| if item_map[item] == 0: | |
| print(" %s (%s)" % (item, flags_to_string(item_map[item]))) | |
| def main(argv): | |
| for filename in argv[1:]: | |
| with open(filename, "r") as file: | |
| lines = file.readlines() | |
| for i in range(len(lines)): | |
| if lines[i][-1] == "\n": | |
| lines[i] = lines[i][:-1] | |
| process(Reader(lines)) | |
| return 0 | |
| if __name__ == "__main__": | |
| sys.exit(main(sys.argv)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment