From dfb9ebfbdb64015a58326d6dc2827ba6b3e6b956 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 22 Mar 2022 16:13:31 -0600 Subject: [PATCH 01/12] Customizer main work --- BaseClasses.py | 8 +- Bosses.py | 21 + CLI.py | 25 +- DoorShuffle.py | 264 +- DungeonGenerator.py | 8 + EntranceShuffle.py | 521 +-- Fill.py | 2 +- Gui.py | 2 +- ItemList.py | 139 +- Main.py | 51 +- Mystery.py | 6 +- RELEASENOTES.md | 9 + Rom.py | 108 +- RoomData.py | 12 + Rules.py | 2 +- docs/Customizer.md | 182 + docs/customizer_example.yaml | 108 + docs/multi_mystery_example.yaml | 17 + docs/player1.yml | 19 + docs/player2.yml | 16 + docs/player3.yml | 25 + resources/app/cli/args.json | 8 +- resources/app/cli/lang/en.json | 1 + resources/app/gui/lang/en.json | 7 +- .../app/gui/randomize/entrando/widgets.json | 6 +- .../gui/randomize/generation/checkboxes.json | 1 + source/classes/CustomSettings.py | 329 ++ source/classes/constants.py | 1 + source/gui/randomize/generation.py | 42 + source/overworld/EntranceShuffle2.py | 3003 +++++++++++++++++ source/{test => overworld}/__init__.py | 0 source/tools/MysteryUtils.py | 190 ++ {source/test => test}/MysteryTestSuite.py | 0 test/stats/EntranceShuffleStats.py | 154 + test/stats/__init__.py | 0 35 files changed, 4599 insertions(+), 688 deletions(-) create mode 100644 docs/Customizer.md create mode 100644 docs/customizer_example.yaml create mode 100644 docs/multi_mystery_example.yaml create mode 100644 docs/player1.yml create mode 100644 docs/player2.yml create mode 100644 docs/player3.yml create mode 100644 source/classes/CustomSettings.py create mode 100644 source/overworld/EntranceShuffle2.py rename source/{test => overworld}/__init__.py (100%) create mode 100644 source/tools/MysteryUtils.py rename {source/test => test}/MysteryTestSuite.py (100%) create mode 100644 test/stats/EntranceShuffleStats.py create mode 100644 test/stats/__init__.py diff --git a/BaseClasses.py b/BaseClasses.py index e997054c..b1b58cd9 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -113,7 +113,7 @@ class World(object): set_player_attr('can_access_trock_front', None) set_player_attr('can_access_trock_big_chest', None) set_player_attr('can_access_trock_middle', None) - set_player_attr('fix_fake_world', logic[player] not in ['owglitches', 'nologic'] or shuffle[player] in ['crossed', 'insanity', 'madness_legacy']) + set_player_attr('fix_fake_world', logic[player] not in ['owglitches', 'nologic'] or shuffle[player] in ['crossed', 'insanity']) set_player_attr('mapshuffle', False) set_player_attr('compassshuffle', False) set_player_attr('keyshuffle', False) @@ -2733,8 +2733,8 @@ class Pot(object): # byte 0: DDDE EEEE (DR, ER) dr_mode = {"basic": 1, "crossed": 2, "vanilla": 0} -er_mode = {"vanilla": 0, "simple": 1, "restricted": 2, "full": 3, "crossed": 4, "insanity": 5, "restricted_legacy": 8, - "full_legacy": 9, "madness_legacy": 10, "insanity_legacy": 11, "dungeonsfull": 7, "dungeonssimple": 6} +er_mode = {"vanilla": 0, "simple": 1, "restricted": 2, "full": 3, "crossed": 4, "insanity": 5, 'lite': 8, + 'lean': 9, "dungeonsfull": 7, "dungeonssimple": 6} # byte 1: LLLW WSSR (logic, mode, sword, retro) logic_mode = {"noglitches": 0, "minorglitches": 1, "nologic": 2, "owglitches": 3, "majorglitches": 4} @@ -2753,7 +2753,7 @@ mixed_travel_mode = {"prevent": 0, "allow": 1, "force": 2} # new byte 4: ?DDD PPPP (unused, drop, pottery) # dropshuffle reserves 2 bits, pottery needs 2 but reserves 2 for future modes) -pottery_mode = {"none": 0, "shuffle": 1, "keys": 2, "lottery": 3, 'dungeon': 4, 'cave': 5} +pottery_mode = {"none": 0, "shuffle": 1, "keys": 2, 'lottery': 3, 'dungeon': 4, 'cave': 5} # byte 5: CCCC CTTX (crystals gt, ctr2, experimental) counter_mode = {"default": 0, "off": 1, "on": 2, "pickup": 3} diff --git a/Bosses.py b/Bosses.py index 53393d5f..16ff4f4d 100644 --- a/Bosses.py +++ b/Bosses.py @@ -165,6 +165,22 @@ def place_bosses(world, player): all_bosses = sorted(boss_table.keys()) #s orted to be deterministic on older pythons placeable_bosses = [boss for boss in all_bosses if boss not in ['Agahnim', 'Agahnim2', 'Ganon']] + used_bosses = [] + + if world.customizer and world.customizer.get_bosses(): + custom_bosses = world.customizer.get_bosses() + if player in custom_bosses: + for location, boss in custom_bosses[player].items(): + level = None + if '(' in location: + i = location.find('(') + level = location[i+1:location.find(')')] + location = location[:i-1] + if can_place_boss(world, player, boss, location, level): + loc_text = location + (' ('+level+')' if level else '') + place_boss(boss, level, location, loc_text, world, player) + boss_locations.remove([location, level]) + used_bosses.append((boss, level)) # temporary hack for swordless kholdstare: if world.boss_shuffle[player] in ["simple", "full", "unique"]: @@ -179,6 +195,8 @@ def place_bosses(world, player): bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm'] else: # all bosses present, the three duplicates chosen at random bosses = placeable_bosses + random.sample(placeable_bosses, 3) + for u, level in used_bosses: + placeable_bosses.remove(u) logging.getLogger('').debug('Bosses chosen %s', bosses) @@ -202,6 +220,9 @@ def place_bosses(world, player): place_boss(boss, level, loc, loc_text, world, player) elif world.boss_shuffle[player] == 'unique': bosses = list(placeable_bosses) + for u, level in used_bosses: + if not level: + bosses.remove(u) for [loc, level] in boss_locations: loc_text = loc + (' ('+level+')' if level else '') diff --git a/CLI.py b/CLI.py index a6a88d23..9a2914de 100644 --- a/CLI.py +++ b/CLI.py @@ -9,6 +9,7 @@ import sys from source.classes.BabelFish import BabelFish from Utils import update_deprecated_args +from source.classes.CustomSettings import CustomSettings class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter): @@ -32,11 +33,25 @@ def parse_cli(argv, no_defaults=False): parser = argparse.ArgumentParser(add_help=False) parser.add_argument('--settingsfile', help="input json file of settings", type=str) parser.add_argument('--multi', default=defval(settings["multi"]), type=lambda value: min(max(int(value), 1), 255)) + parser.add_argument('--customizer', help='input yaml file for customizations', type=str) + parser.add_argument('--print_custom_yaml', help='print example yaml for current settings', + default=False, action="store_true") + parser.add_argument('--mystery', dest="mystery", default=False, action="store_true") + multiargs, _ = parser.parse_known_args(argv) if multiargs.settingsfile: settings = apply_settings_file(settings, multiargs.settingsfile) + player_num = multiargs.multi + if multiargs.customizer: + custom = CustomSettings() + custom.load_yaml(multiargs.customizer) + cp = custom.determine_players() + if cp: + player_num = cp + + parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) # get args @@ -78,9 +93,11 @@ def parse_cli(argv, no_defaults=False): parser.add_argument('--securerandom', default=defval(settings["securerandom"]), action='store_true') parser.add_argument('--teams', default=defval(1), type=lambda value: max(int(value), 1)) parser.add_argument('--settingsfile', dest="filename", help="input json file of settings", type=str) + parser.add_argument('--customizer', dest="customizer", help='input yaml file for customizations', type=str) + parser.add_argument('--print_custom_yaml', dest="print_custom_yaml", default=False, action="store_true") - if multiargs.multi: - for player in range(1, multiargs.multi + 1): + if player_num: + for player in range(1, player_num + 1): parser.add_argument(f'--p{player}', default=defval(''), help=argparse.SUPPRESS) ret = parser.parse_args(argv) @@ -92,9 +109,9 @@ def parse_cli(argv, no_defaults=False): ret.dropshuffle = True ret.pottery = 'keys' if ret.pottery == 'none' else ret.pottery - if multiargs.multi: + if player_num: defaults = copy.deepcopy(ret) - for player in range(1, multiargs.multi + 1): + for player in range(1, player_num + 1): playerargs = parse_cli(shlex.split(getattr(ret, f"p{player}")), True) for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', diff --git a/DoorShuffle.py b/DoorShuffle.py index f4ee9cfa..a182304e 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -256,9 +256,23 @@ def convert_key_doors(k_doors, world, player): def connect_custom(world, player): - if hasattr(world, 'custom_doors') and world.custom_doors[player]: - for entrance, ext in world.custom_doors[player]: - connect_two_way(world, entrance, ext, player) + if world.customizer and world.customizer.get_doors(): + custom_doors = world.customizer.get_doors() + if player not in custom_doors: + return + custom_doors = custom_doors[player] + if 'doors' not in custom_doors: + return + for door, dest in custom_doors['doors'].items(): + d = world.get_door(door, player) + if d.type not in [DoorType.Interior, DoorType.Logical]: + if isinstance(dest, str): + connect_two_way(world, door, dest, player) + elif 'dest' in dest: + if 'one-way' in dest and dest['one-way']: + connect_one_way(world, door, dest['dest'], player) + else: + connect_two_way(world, door, dest['dest'], player) def connect_simple_door(world, exit_name, region_name, player): @@ -422,6 +436,9 @@ def choose_portals(world, player): master_door_list = [x for x in world.doors if x.player == player and x.portalAble] portal_assignment = defaultdict(list) shuffled_info = list(info_map.items()) + + custom = customizer_portals(master_door_list, world, player) + if cross_flag: random.shuffle(shuffled_info) for dungeon, info in shuffled_info: @@ -436,17 +453,17 @@ def choose_portals(world, player): info.required_passage[target_region] = [x for x in possible_portals if x != sanc.name] info.required_passage = {x: y for x, y in info.required_passage.items() if len(y) > 0} for target_region, possible_portals in info.required_passage.items(): - candidates = find_portal_candidates(master_door_list, dungeon, need_passage=True, crossed=cross_flag, - bk_shuffle=bk_shuffle, rupee_bow=rupee_bow_flag) - choice, portal = assign_portal(candidates, possible_portals, world, player) + candidates = find_portal_candidates(master_door_list, dungeon, custom, need_passage=True, + crossed=cross_flag, bk_shuffle=bk_shuffle, rupee_bow=rupee_bow_flag) + choice, portal = assign_portal(candidates, possible_portals, custom, world, player) portal.destination = True clean_up_portal_assignment(portal_assignment, dungeon, portal, master_door_list, outstanding_portals) dead_end_choices = info.total - 1 - len(portal_assignment[dungeon]) for i in range(0, dead_end_choices): - candidates = find_portal_candidates(master_door_list, dungeon, dead_end_allowed=True, + candidates = find_portal_candidates(master_door_list, dungeon, custom, dead_end_allowed=True, crossed=cross_flag, bk_shuffle=bk_shuffle, rupee_bow=rupee_bow_flag) possible_portals = outstanding_portals if not info.sole_entrance else [x for x in outstanding_portals if x != info.sole_entrance] - choice, portal = assign_portal(candidates, possible_portals, world, player) + choice, portal = assign_portal(candidates, possible_portals, custom, world, player) if choice.deadEnd: if choice.passage: portal.destination = True @@ -455,9 +472,9 @@ def choose_portals(world, player): clean_up_portal_assignment(portal_assignment, dungeon, portal, master_door_list, outstanding_portals) the_rest = info.total - len(portal_assignment[dungeon]) for i in range(0, the_rest): - candidates = find_portal_candidates(master_door_list, dungeon, crossed=cross_flag, + candidates = find_portal_candidates(master_door_list, dungeon, custom, crossed=cross_flag, bk_shuffle=bk_shuffle, standard=hc_flag, rupee_bow=rupee_bow_flag) - choice, portal = assign_portal(candidates, outstanding_portals, world, player) + choice, portal = assign_portal(candidates, outstanding_portals, custom, world, player) clean_up_portal_assignment(portal_assignment, dungeon, portal, master_door_list, outstanding_portals) for portal in world.dungeon_portals[player]: @@ -495,6 +512,29 @@ def choose_portals(world, player): world.swamp_patch_required[player] = True +def customizer_portals(master_door_list, world, player): + custom_portals = {} + assigned_doors = set() + if world.customizer and world.customizer.get_doors(): + custom_doors = world.customizer.get_doors()[player] + if custom_doors and 'lobbies' in custom_doors: + for portal, assigned_door in custom_doors['lobbies'].items(): + door = next(x for x in master_door_list if x.name == assigned_door) + custom_portals[portal] = door + assigned_doors.add(door) + if custom_doors and 'doors' in custom_doors: + for src_door, dest in custom_doors['doors'].items(): + door = world.get_door(src_door, player) + assigned_doors.add(door) + if isinstance(dest, str): + door = world.get_door(dest, player) + assigned_doors.add(door) + else: + door = world.get_door(dest['dest'], player) + assigned_doors.add(door) + return custom_portals, assigned_doors + + def analyze_portals(world, player): info_map = {} for dungeon, portal_list in dungeon_portals.items(): @@ -576,9 +616,14 @@ def disconnect_portal(portal, world, player): chosen_door.entranceFlag = False -def find_portal_candidates(door_list, dungeon, need_passage=False, dead_end_allowed=False, crossed=False, +def find_portal_candidates(door_list, dungeon, custom, need_passage=False, dead_end_allowed=False, crossed=False, bk_shuffle=False, standard=False, rupee_bow=False): - ret = [x for x in door_list if bk_shuffle or not x.bk_shuffle_req] + custom_portals, assigned_doors = custom + if assigned_doors: + ret = [x for x in door_list if x not in assigned_doors] + else: + ret = door_list + ret = [x for x in ret if bk_shuffle or not x.bk_shuffle_req] if crossed: ret = [x for x in ret if not x.dungeonLink or x.dungeonLink == dungeon or x.dungeonLink.startswith('link')] else: @@ -594,9 +639,13 @@ def find_portal_candidates(door_list, dungeon, need_passage=False, dead_end_allo return ret -def assign_portal(candidates, possible_portals, world, player): - candidate = random.choice(candidates) +def assign_portal(candidates, possible_portals, custom, world, player): + custom_portals, assigned_doors = custom portal_choice = random.choice(possible_portals) + if portal_choice in custom_portals: + candidate = custom_portals[portal_choice] + else: + candidate = random.choice(candidates) portal = world.get_portal(portal_choice, player) while candidate.lw_restricted and not portal.light_world: candidates.remove(candidate) @@ -723,6 +772,7 @@ def within_dungeon(world, player): paths = determine_required_paths(world, player) check_required_paths(paths, world, player) + setup_custom_door_types(world, player) # shuffle_key_doors for dungeons logging.getLogger('').info(world.fish.translate("cli", "cli", "shuffling.keydoors")) start = time.process_time() @@ -994,6 +1044,7 @@ def cross_dungeon(world, player): at.dungeon_items.append(ItemFactory('Compass (Agahnims Tower)', player)) at.dungeon_items.append(ItemFactory('Map (Agahnims Tower)', player)) + setup_custom_door_types(world, player) assign_cross_keys(dungeon_builders, world, player) all_dungeon_items_cnt = len(list(y for x in world.dungeons if x.player == player for y in x.all_items)) target_items = 34 @@ -1058,6 +1109,29 @@ def cross_dungeon(world, player): refine_boss_exits(world, player) +def filter_key_door_pool(pool, selected_custom): + new_pool = [] + for cand in pool: + found = False + for custom in selected_custom: + if isinstance(cand, Door): + if isinstance(custom, Door): + found = cand.name == custom.name + else: + found = cand.name == custom[0].name or cand.name == custom[1].name + else: + if isinstance(custom, Door): + found = cand[0].name == custom.name or cand[1].name == custom.name + else: + found = (cand[0].name == custom[0].name or cand[0].name == custom[1].name + or cand[1].name == custom[0].name or cand[1].name == custom[1].name) + if found: + break + if not found: + new_pool.append(cand) + return new_pool + + def assign_cross_keys(dungeon_builders, world, player): logging.getLogger('').info(world.fish.translate("cli", "cli", "shuffling.keydoors")) start = time.process_time() @@ -1069,9 +1143,13 @@ def assign_cross_keys(dungeon_builders, world, player): remaining += 19 else: remaining = len(list(x for dgn in world.dungeons if dgn.player == player for x in dgn.small_keys)) - total_keys = remaining total_candidates = 0 start_regions_map = {} + if player in world.custom_door_types: + custom_key_doors = world.custom_door_types[player]['Key Door'] + else: + custom_key_doors = defaultdict(list) + key_door_pool, key_doors_assigned = {}, {} # Step 1: Find Small Key Door Candidates for name, builder in dungeon_builders.items(): dungeon = world.get_dungeon(name, player) @@ -1081,22 +1159,27 @@ def assign_cross_keys(dungeon_builders, world, player): dungeon.big_key = ItemFactory(dungeon_bigs[name], player) start_regions = convert_regions(builder.path_entrances, world, player) find_small_key_door_candidates(builder, start_regions, world, player) - builder.key_doors_num = max(0, len(builder.candidates) - builder.key_drop_cnt) + key_door_pool[name] = list(builder.candidates) + if custom_key_doors[name]: + key_door_pool[name] = filter_key_door_pool(key_door_pool[name], custom_key_doors[name]) + remaining -= len(custom_key_doors[name]) + builder.key_doors_num = max(0, len(key_door_pool[name]) - builder.key_drop_cnt) total_candidates += builder.key_doors_num start_regions_map[name] = start_regions + total_keys = remaining # Step 2: Initial Key Number Assignment & Calculate Flexibility for name, builder in dungeon_builders.items(): calculated = int(round(builder.key_doors_num*total_keys/total_candidates)) - max_keys = builder.location_cnt - calc_used_dungeon_items(builder) - cand_len = max(0, len(builder.candidates) - builder.key_drop_cnt) + max_keys = max(0, builder.location_cnt - calc_used_dungeon_items(builder, world, player)) + cand_len = max(0, len(key_door_pool[name]) - builder.key_drop_cnt) limit = min(max_keys, cand_len) suggested = min(calculated, limit) - combo_size = ncr(len(builder.candidates), suggested + builder.key_drop_cnt) + combo_size = ncr(len(key_door_pool[name]), suggested + builder.key_drop_cnt) while combo_size > 500000 and suggested > 0: suggested -= 1 - combo_size = ncr(len(builder.candidates), suggested + builder.key_drop_cnt) - builder.key_doors_num = suggested + builder.key_drop_cnt + combo_size = ncr(len(key_door_pool[name]), suggested + builder.key_drop_cnt) + builder.key_doors_num = suggested + builder.key_drop_cnt + len(custom_key_doors[name]) remaining -= suggested builder.combo_size = combo_size if suggested < limit: @@ -1104,7 +1187,7 @@ def assign_cross_keys(dungeon_builders, world, player): # Step 3: Initial valid combination find - reduce flex if needed for name, builder in dungeon_builders.items(): - suggested = builder.key_doors_num - builder.key_drop_cnt + suggested = builder.key_doors_num - builder.key_drop_cnt - len(custom_key_doors[name]) builder.total_keys = builder.key_doors_num find_valid_combination(builder, start_regions_map[name], world, player) actual_chest_keys = builder.key_doors_num - builder.key_drop_cnt @@ -1161,7 +1244,7 @@ def reassign_boss(boss_region, boss_key, builder, gt, world, player): def check_entrance_fixes(world, player): # I believe these modes will be fine - if world.shuffle[player] not in ['insanity', 'insanity_legacy', 'madness_legacy']: + if world.shuffle[player] not in ['insanity']: checks = { 'Palace of Darkness': 'pod', 'Skull Woods Final Section': 'sw', @@ -1402,6 +1485,39 @@ def valid_region_to_explore(region, world, player): or (region.name == 'Hyrule Castle Ledge' and world.mode[player] == 'standard')) +def setup_custom_door_types(world, player): + if not hasattr(world, 'custom_door_types'): + world.custom_door_types = defaultdict(dict) + if world.customizer and world.customizer.get_doors(): + # type_conv = {'Bomb Door': DoorKind.Bombable , 'Dash Door', DoorKind.Dashable, 'Key Door', DoorKind.SmallKey} + custom_doors = world.customizer.get_doors() + if player not in custom_doors: + return + custom_doors = custom_doors[player] + if 'doors' not in custom_doors: + return + world.custom_door_types[player] = type_map = {'Key Door': defaultdict(list), 'Dash Door': [], 'Bomb Door': []} + for door, dest in custom_doors['doors'].items(): + if isinstance(dest, dict): + if 'type' in dest: + door_kind = dest['type'] + d = world.get_door(door, player) + dungeon = d.entrance.parent_region.dungeon + if d.type == DoorType.SpiralStairs: + type_map[door_kind][dungeon.name].append(d) + elif door_kind == 'Key Door': + # check if the + if d.dest.type in [DoorType.Interior, DoorType.Normal]: + type_map[door_kind][dungeon.name].append((d, d.dest)) + else: + type_map[door_kind][dungeon.name].append(d) + else: + if d.dest.type in [DoorType.Interior, DoorType.Normal]: + type_map[door_kind].append((d, d.dest)) + else: + type_map[door_kind].append(d) + + def shuffle_key_doors(builder, world, player): start_regions = convert_regions(builder.path_entrances, world, player) # count number of key doors - this could be a table? @@ -1458,34 +1574,44 @@ def find_small_key_door_candidates(builder, start_regions, world, player): builder.candidates = paired_candidates -def calc_used_dungeon_items(builder): - base = 4 - if builder.bk_required and not builder.bk_provided: +def calc_used_dungeon_items(builder, world, player): + base = 2 + if not world.bigkeyshuffle[player]: + if builder.bk_required and not builder.bk_provided: + base += 1 + if not world.compassshuffle[player]: + base += 1 + if not world.mapshuffle[player]: base += 1 - # if builder.name == 'Hyrule Castle': - # base -= 1 # Missing compass/map - # if builder.name == 'Agahnims Tower': - # base -= 2 # Missing both compass/map - # gt can lose map once compasses work return base def find_valid_combination(builder, start_regions, world, player, drop_keys=True): logger = logging.getLogger('') + key_door_pool = list(builder.candidates) # can these be a set? + key_doors_needed = builder.key_doors_num + if player in world.custom_door_types: + custom_key_doors = world.custom_door_types[player]['Key Door'][builder.name] + else: + custom_key_doors = [] + if custom_key_doors: # could validate that each custom item is in the candidates + key_door_pool = filter_key_door_pool(key_door_pool, custom_key_doors) + key_doors_needed -= len(custom_key_doors) + # find valid combination of candidates - if len(builder.candidates) < builder.key_doors_num: + if len(key_door_pool) < key_doors_needed: if not drop_keys: logger.info('No valid layouts for %s with %s doors', builder.name, builder.key_doors_num) return False - builder.key_doors_num = len(builder.candidates) # reduce number of key doors - logger.info('%s: %s', world.fish.translate("cli","cli","lowering.keys.candidates"), builder.name) - combinations = ncr(len(builder.candidates), builder.key_doors_num) + builder.key_doors_num -= key_doors_needed - len(key_door_pool) # reduce number of key doors + logger.info('%s: %s', world.fish.translate("cli", "cli", "lowering.keys.candidates"), builder.name) + combinations = ncr(len(key_door_pool), key_doors_needed) itr = 0 start = time.process_time() sample_list = list(range(0, int(combinations))) random.shuffle(sample_list) - proposal = kth_combination(sample_list[itr], builder.candidates, builder.key_doors_num) - + proposal = kth_combination(sample_list[itr], key_door_pool, key_doors_needed) + proposal.extend(custom_key_doors) # eliminate start region if portal marked as destination excluded = {} for region in start_regions: @@ -1509,14 +1635,16 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True return False logger.info('%s: %s', world.fish.translate("cli","cli","lowering.keys.layouts"), builder.name) builder.key_doors_num -= 1 + key_doors_needed -= 1 if builder.key_doors_num < 0: - raise Exception('Bad dungeon %s - 0 key doors not valid' % builder.name) - combinations = ncr(len(builder.candidates), builder.key_doors_num) + raise Exception('Bad dungeon %s - less than 0 key doors not valid' % builder.name) + combinations = ncr(len(key_door_pool), max(0, key_doors_needed)) sample_list = list(range(0, int(combinations))) random.shuffle(sample_list) itr = 0 start = time.process_time() # reset time since itr reset - proposal = kth_combination(sample_list[itr], builder.candidates, builder.key_doors_num) + proposal = kth_combination(sample_list[itr], key_door_pool, key_doors_needed) + proposal.extend(custom_key_doors) key_layout.reset(proposal, builder, world, player) if (itr+1) % 1000 == 0: mark = time.process_time()-start @@ -1710,15 +1838,31 @@ def change_door_to_small_key(d, world, player): d.smallKey = True room = world.get_room(d.roomIndex, player) if room.doorList[d.doorListPos][1] != DoorKind.SmallKey: + verify_door_list_pos(d, room, world, player) room.change(d.doorListPos, DoorKind.SmallKey) +def verify_door_list_pos(d, room, world, player): + if d.doorListPos >= 4: + new_index = room.next_free() + if new_index is not None: + room.swap(new_index, d.doorListPos) + other = next(x for x in world.doors if x.player == player and x.roomIndex == d.roomIndex + and x.doorListPos == new_index) + other.doorListPos = d.doorListPos + d.doorListPos = new_index + else: + raise Exception(f'Invalid stateful door: {d.name}. Only 4 stateful doors per supertile') + + def smooth_door_pairs(world, player): all_doors = [x for x in world.doors if x.player == player] skip = set() bd_candidates = defaultdict(list) for door in all_doors: if door.type in [DoorType.Normal, DoorType.Interior] and door not in skip and not door.entranceFlag: + if not door.dest: + continue partner = door.dest skip.add(partner) room_a = world.get_room(door.roomIndex, player) @@ -1791,15 +1935,33 @@ def stateful_door(door, kind): return False +def custom_door_kind(custom_key, kind, bd_candidates, counts, world, player): + if custom_key in world.custom_door_types[player]: + for door_a, door_b in world.custom_door_types[player][custom_key]: + change_pair_type(door_a, kind, world, player) + d_name = door_a.entrance.parent_region.dungeon.name + bd_list = next(bd_list for dungeon, bd_list in bd_candidates.items() if dungeon.name == d_name) + if door_a in bd_list: + bd_list.remove(door_a) + if door_b in bd_list: + bd_list.remove(door_b) + counts[d_name] += 1 + + def shuffle_bombable_dashable(bd_candidates, world, player): + dash_counts = defaultdict(int) + bomb_counts = defaultdict(int) + if world.custom_door_types[player]: + custom_door_kind('Dash Door', DoorKind.Dashable, bd_candidates, dash_counts, world, player) + custom_door_kind('Bomb Door', DoorKind.Bombable, bd_candidates, bomb_counts, world, player) if world.doorShuffle[player] == 'basic': for dungeon, candidates in bd_candidates.items(): - diff = bomb_dash_counts[dungeon.name][1] + diff = bomb_dash_counts[dungeon.name][1] - dash_counts[dungeon.name] if diff > 0: for chosen in random.sample(candidates, min(diff, len(candidates))): change_pair_type(chosen, DoorKind.Dashable, world, player) candidates.remove(chosen) - diff = bomb_dash_counts[dungeon.name][0] + diff = bomb_dash_counts[dungeon.name][0] - bomb_counts[dungeon.name] if diff > 0: for chosen in random.sample(candidates, min(diff, len(candidates))): change_pair_type(chosen, DoorKind.Bombable, world, player) @@ -1808,21 +1970,27 @@ def shuffle_bombable_dashable(bd_candidates, world, player): remove_pair_type_if_present(excluded, world, player) elif world.doorShuffle[player] == 'crossed': all_candidates = sum(bd_candidates.values(), []) - for chosen in random.sample(all_candidates, min(8, len(all_candidates))): - change_pair_type(chosen, DoorKind.Dashable, world, player) - all_candidates.remove(chosen) - for chosen in random.sample(all_candidates, min(12, len(all_candidates))): - change_pair_type(chosen, DoorKind.Bombable, world, player) - all_candidates.remove(chosen) + desired_dashables = 8 - sum(dash_counts.values(), 0) + desired_bombables = 12 - sum(bomb_counts.values(), 0) + if desired_dashables > 0: + for chosen in random.sample(all_candidates, min(desired_dashables, len(all_candidates))): + change_pair_type(chosen, DoorKind.Dashable, world, player) + all_candidates.remove(chosen) + if desired_bombables > 0: + for chosen in random.sample(all_candidates, min(desired_bombables, len(all_candidates))): + change_pair_type(chosen, DoorKind.Bombable, world, player) + all_candidates.remove(chosen) for excluded in all_candidates: remove_pair_type_if_present(excluded, world, player) def change_pair_type(door, new_type, world, player): room_a = world.get_room(door.roomIndex, player) + verify_door_list_pos(door, room_a, world, player) room_a.change(door.doorListPos, new_type) if door.type != DoorType.Interior: room_b = world.get_room(door.dest.roomIndex, player) + verify_door_list_pos(door.dest, room_b, world, player) room_b.change(door.dest.doorListPos, new_type) add_pair(door, door.dest, world, player) spoiler_type = 'Bomb Door' if new_type == DoorKind.Bombable else 'Dash Door' diff --git a/DungeonGenerator.py b/DungeonGenerator.py index f0038f45..38d47230 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -1286,6 +1286,10 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, complete_dungeons = {x: y for x, y in dungeon_map.items() if sum(len(sector.outstanding_doors) for sector in y.sectors) <= 0} [dungeon_map.pop(key) for key in complete_dungeons.keys()] + if not dungeon_map: + dungeon_map.update(complete_dungeons) + return dungeon_map + # categorize sectors identify_destination_sectors(accessible_sectors, reverse_d_map, dungeon_map, connections, dungeon_entrances, split_dungeon_entrances) @@ -1398,6 +1402,8 @@ def identify_destination_sectors(accessible_sectors, reverse_d_map, dungeon_map, explored = False else: d_name = reverse_d_map[sector] + if d_name not in dungeon_map: + return if d_name not in split_dungeon_entrances: for r_name in dungeon_entrances[d_name]: ent_sector = find_sector(r_name, dungeon_map[d_name].sectors) @@ -2953,6 +2959,8 @@ def split_dungeon_builder(builder, split_list, builder_info): sub_builder.all_entrances = list(split_entrances) for r_name in split_entrances: assign_sector(find_sector(r_name, candidate_sectors), sub_builder, candidate_sectors, global_pole) + if len(candidate_sectors) == 0: + return dungeon_map comb_w_replace = len(dungeon_map) ** len(candidate_sectors) return balance_split(candidate_sectors, dungeon_map, global_pole, builder_info) except (GenerationException, NeutralizingException): diff --git a/EntranceShuffle.py b/EntranceShuffle.py index d1bfdbb8..0d3f502b 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -245,61 +245,6 @@ def link_entrances(world, player): # place remaining doors connect_doors(world, doors, door_targets, player) - elif world.shuffle[player] == 'restricted_legacy': - simple_shuffle_dungeons(world, player) - - lw_entrances = list(LW_Entrances) - dw_entrances = list(DW_Entrances) - dw_must_exits = list(DW_Entrances_Must_Exit) - old_man_entrances = list(Old_Man_Entrances) - caves = list(Cave_Exits) - three_exit_caves = list(Cave_Three_Exits) - single_doors = list(Single_Cave_Doors) - bomb_shop_doors = list(Bomb_Shop_Single_Cave_Doors) - blacksmith_doors = list(Blacksmith_Single_Cave_Doors) - door_targets = list(Single_Cave_Targets) - - # only use two exit caves to do mandatory dw connections - connect_mandatory_exits(world, dw_entrances, caves, dw_must_exits, player) - # add three exit doors to pool for remainder - caves.extend(three_exit_caves) - - # place old man, has limited options - # exit has to come from specific set of doors, the entrance is free to move about - random.shuffle(old_man_entrances) - old_man_exit = old_man_entrances.pop() - lw_entrances.extend(old_man_entrances) - random.shuffle(lw_entrances) - old_man_entrance = lw_entrances.pop() - connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player) - connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) - - # place Old Man House in Light World - connect_caves(world, lw_entrances, [], Old_Man_House, player) - - # connect rest. There's 2 dw entrances remaining, so we will not run into parity issue placing caves - connect_caves(world, lw_entrances, dw_entrances, caves, player) - - # scramble holes - scramble_holes(world, player) - - # place blacksmith, has limited options - random.shuffle(blacksmith_doors) - blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) - bomb_shop_doors.extend(blacksmith_doors) - - # place dam and pyramid fairy, have limited options - random.shuffle(bomb_shop_doors) - bomb_shop = bomb_shop_doors.pop() - connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) - single_doors.extend(bomb_shop_doors) - - # tavern back door cannot be shuffled yet - connect_doors(world, ['Tavern North'], ['Tavern'], player) - - # place remaining doors - connect_doors(world, single_doors, door_targets, player) elif world.shuffle[player] == 'full': skull_woods_shuffle(world, player) @@ -532,324 +477,6 @@ def link_entrances(world, player): # place remaining doors connect_doors(world, entrances, door_targets, player) - elif world.shuffle[player] == 'full_legacy': - skull_woods_shuffle(world, player) - - lw_entrances = list(LW_Entrances + LW_Dungeon_Entrances + Old_Man_Entrances) - dw_entrances = list(DW_Entrances + DW_Dungeon_Entrances) - dw_must_exits = list(DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit) - lw_must_exits = list(LW_Dungeon_Entrances_Must_Exit) - old_man_entrances = list(Old_Man_Entrances + ['Tower of Hera']) - caves = list(Cave_Exits + Dungeon_Exits + Cave_Three_Exits) # don't need to consider three exit caves, have one exit caves to avoid parity issues - single_doors = list(Single_Cave_Doors) - bomb_shop_doors = list(Bomb_Shop_Single_Cave_Doors) - blacksmith_doors = list(Blacksmith_Single_Cave_Doors) - door_targets = list(Single_Cave_Targets) - - if world.mode[player] == 'standard': - # must connect front of hyrule castle to do escape - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) - else: - caves.append(tuple(random.sample(['Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'],3))) - lw_entrances.append('Hyrule Castle Entrance (South)') - - if not world.shuffle_ganon: - connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player) - else: - dw_entrances.append('Ganons Tower') - caves.append('Ganons Tower Exit') - - # we randomize which world requirements we fulfill first so we get better dungeon distribution - if random.randint(0, 1) == 0: - connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player) - connect_mandatory_exits(world, dw_entrances, caves, dw_must_exits, player) - else: - connect_mandatory_exits(world, dw_entrances, caves, dw_must_exits, player) - connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player) - if world.mode[player] == 'standard': - # rest of hyrule castle must be in light world - connect_caves(world, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player) - - # place old man, has limited options - # exit has to come from specific set of doors, the entrance is free to move about - old_man_entrances = [door for door in old_man_entrances if door in lw_entrances] - random.shuffle(old_man_entrances) - old_man_exit = old_man_entrances.pop() - lw_entrances.remove(old_man_exit) - - random.shuffle(lw_entrances) - old_man_entrance = lw_entrances.pop() - connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player) - connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) - - # place Old Man House in Light World - connect_caves(world, lw_entrances, [], list(Old_Man_House), player) #need this to avoid badness with multiple seeds - - # now scramble the rest - connect_caves(world, lw_entrances, dw_entrances, caves, player) - - # scramble holes - scramble_holes(world, player) - - # place blacksmith, has limited options - random.shuffle(blacksmith_doors) - blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) - bomb_shop_doors.extend(blacksmith_doors) - - # place bomb shop, has limited options - random.shuffle(bomb_shop_doors) - bomb_shop = bomb_shop_doors.pop() - connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) - single_doors.extend(bomb_shop_doors) - - # tavern back door cannot be shuffled yet - connect_doors(world, ['Tavern North'], ['Tavern'], player) - - # place remaining doors - connect_doors(world, single_doors, door_targets, player) - elif world.shuffle[player] == 'madness_legacy': - # here lie dragons, connections are no longer two way - lw_entrances = list(LW_Entrances + LW_Dungeon_Entrances + Old_Man_Entrances) - dw_entrances = list(DW_Entrances + DW_Dungeon_Entrances) - dw_entrances_must_exits = list(DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit) - - lw_doors = list(LW_Entrances + LW_Dungeon_Entrances + LW_Dungeon_Entrances_Must_Exit) + ['Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', - 'Lumberjack Tree Cave'] + list(Old_Man_Entrances) - dw_doors = list(DW_Entrances + DW_Dungeon_Entrances + DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit) + ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)'] - - random.shuffle(lw_doors) - random.shuffle(dw_doors) - - dw_entrances_must_exits.append('Skull Woods Second Section Door (West)') - dw_entrances.append('Skull Woods Second Section Door (East)') - dw_entrances.append('Skull Woods First Section Door') - - lw_entrances.extend(['Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave']) - - lw_entrances_must_exits = list(LW_Dungeon_Entrances_Must_Exit) - - old_man_entrances = list(Old_Man_Entrances) + ['Tower of Hera'] - - mandatory_light_world = ['Old Man House Exit (Bottom)', 'Old Man House Exit (Top)'] - mandatory_dark_world = [] - caves = list(Cave_Exits + Dungeon_Exits + Cave_Three_Exits) - - # shuffle up holes - - lw_hole_entrances = ['Kakariko Well Drop', 'Bat Cave Drop', 'North Fairy Cave Drop', 'Lost Woods Hideout Drop', 'Lumberjack Tree Tree', 'Sanctuary Grave'] - dw_hole_entrances = ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'] - - hole_targets = [('Kakariko Well Exit', 'Kakariko Well (top)'), - ('Bat Cave Exit', 'Bat Cave (right)'), - ('North Fairy Cave Exit', 'North Fairy Cave'), - ('Lost Woods Hideout Exit', 'Lost Woods Hideout (top)'), - ('Lumberjack Tree Exit', 'Lumberjack Tree (top)'), - (('Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)'), 'Skull Back Drop')] - - if world.mode[player] == 'standard': - # cannot move uncle cave - connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player) - connect_exit(world, 'Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance Stairs', player) - connect_entrance(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player) - else: - lw_hole_entrances.append('Hyrule Castle Secret Entrance Drop') - hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance')) - lw_doors.append('Hyrule Castle Secret Entrance Stairs') - lw_entrances.append('Hyrule Castle Secret Entrance Stairs') - - if not world.shuffle_ganon: - connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player) - connect_two_way(world, 'Pyramid Entrance', 'Pyramid Exit', player) - connect_entrance(world, 'Pyramid Hole', 'Pyramid', player) - else: - dw_entrances.append('Ganons Tower') - caves.append('Ganons Tower Exit') - dw_hole_entrances.append('Pyramid Hole') - hole_targets.append(('Pyramid Exit', 'Pyramid')) - dw_entrances_must_exits.append('Pyramid Entrance') - dw_doors.extend(['Ganons Tower', 'Pyramid Entrance']) - - random.shuffle(lw_hole_entrances) - random.shuffle(dw_hole_entrances) - random.shuffle(hole_targets) - - # decide if skull woods first section should be in light or dark world - sw_light = random.randint(0, 1) == 0 - if sw_light: - sw_hole_pool = lw_hole_entrances - mandatory_light_world.append('Skull Woods First Section Exit') - else: - sw_hole_pool = dw_hole_entrances - mandatory_dark_world.append('Skull Woods First Section Exit') - for target in ['Skull Left Drop', 'Skull Pinball', 'Skull Pot Circle']: - connect_entrance(world, sw_hole_pool.pop(), target, player) - - # sanctuary has to be in light world - connect_entrance(world, lw_hole_entrances.pop(), 'Sewer Drop', player) - mandatory_light_world.append('Sanctuary Exit') - - # fill up remaining holes - for hole in dw_hole_entrances: - exits, target = hole_targets.pop() - mandatory_dark_world.append(exits) - connect_entrance(world, hole, target, player) - - for hole in lw_hole_entrances: - exits, target = hole_targets.pop() - mandatory_light_world.append(exits) - connect_entrance(world, hole, target, player) - - # hyrule castle handling - if world.mode[player] == 'standard': - # must connect front of hyrule castle to do escape - connect_entrance(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) - connect_exit(world, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player) - mandatory_light_world.append(('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) - else: - lw_doors.append('Hyrule Castle Entrance (South)') - lw_entrances.append('Hyrule Castle Entrance (South)') - caves.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) - - # now let's deal with mandatory reachable stuff - def extract_reachable_exit(cavelist): - random.shuffle(cavelist) - candidate = None - for cave in cavelist: - if isinstance(cave, tuple) and len(cave) > 1: - # special handling: TRock and Spectracle Rock cave have two entries that we should consider entrance only - # ToDo this should be handled in a more sensible manner - if cave[0] in ['Turtle Rock Exit (Front)', 'Spectacle Rock Cave Exit (Peak)'] and len(cave) == 2: - continue - candidate = cave - break - if candidate is None: - raise RuntimeError('No suitable cave.') - cavelist.remove(candidate) - return candidate - - def connect_reachable_exit(entrance, general, worldspecific, worldoors): - # select which one is the primary option - if random.randint(0, 1) == 0: - primary = general - secondary = worldspecific - else: - primary = worldspecific - secondary = general - - try: - cave = extract_reachable_exit(primary) - except RuntimeError: - cave = extract_reachable_exit(secondary) - - exit = cave[-1] - cave = cave[:-1] - connect_exit(world, exit, entrance, player) - connect_entrance(world, worldoors.pop(), exit, player) - # rest of cave now is forced to be in this world - worldspecific.append(cave) - - # we randomize which world requirements we fulfill first so we get better dungeon distribution - if random.randint(0, 1) == 0: - for entrance in lw_entrances_must_exits: - connect_reachable_exit(entrance, caves, mandatory_light_world, lw_doors) - for entrance in dw_entrances_must_exits: - connect_reachable_exit(entrance, caves, mandatory_dark_world, dw_doors) - else: - for entrance in dw_entrances_must_exits: - connect_reachable_exit(entrance, caves, mandatory_dark_world, dw_doors) - for entrance in lw_entrances_must_exits: - connect_reachable_exit(entrance, caves, mandatory_light_world, lw_doors) - - # place old man, has limited options - # exit has to come from specific set of doors, the entrance is free to move about - old_man_entrances = [entrance for entrance in old_man_entrances if entrance in lw_entrances] - random.shuffle(old_man_entrances) - old_man_exit = old_man_entrances.pop() - lw_entrances.remove(old_man_exit) - - connect_exit(world, 'Old Man Cave Exit (East)', old_man_exit, player) - connect_entrance(world, lw_doors.pop(), 'Old Man Cave Exit (East)', player) - mandatory_light_world.append('Old Man Cave Exit (West)') - - # we connect up the mandatory associations we have found - for mandatory in mandatory_light_world: - if not isinstance(mandatory, tuple): - mandatory = (mandatory,) - for exit in mandatory: - # point out somewhere - connect_exit(world, exit, lw_entrances.pop(), player) - # point in from somewhere - connect_entrance(world, lw_doors.pop(), exit, player) - - for mandatory in mandatory_dark_world: - if not isinstance(mandatory, tuple): - mandatory = (mandatory,) - for exit in mandatory: - # point out somewhere - connect_exit(world, exit, dw_entrances.pop(), player) - # point in from somewhere - connect_entrance(world, dw_doors.pop(), exit, player) - - # handle remaining caves - while caves: - # connect highest exit count caves first, prevent issue where we have 2 or 3 exits across worlds left to fill - cave_candidate = (None, 0) - for i, cave in enumerate(caves): - if isinstance(cave, str): - cave = (cave,) - if len(cave) > cave_candidate[1]: - cave_candidate = (i, len(cave)) - cave = caves.pop(cave_candidate[0]) - - place_lightworld = random.randint(0, 1) == 0 - if place_lightworld: - target_doors = lw_doors - target_entrances = lw_entrances - else: - target_doors = dw_doors - target_entrances = dw_entrances - - if isinstance(cave, str): - cave = (cave,) - - # check if we can still fit the cave into our target group - if len(target_doors) < len(cave): - if not place_lightworld: - target_doors = lw_doors - target_entrances = lw_entrances - else: - target_doors = dw_doors - target_entrances = dw_entrances - - for exit in cave: - connect_exit(world, exit, target_entrances.pop(), player) - connect_entrance(world, target_doors.pop(), exit, player) - - # handle simple doors - - single_doors = list(Single_Cave_Doors) - bomb_shop_doors = list(Bomb_Shop_Single_Cave_Doors) - blacksmith_doors = list(Blacksmith_Single_Cave_Doors) - door_targets = list(Single_Cave_Targets) - - # place blacksmith, has limited options - random.shuffle(blacksmith_doors) - blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) - bomb_shop_doors.extend(blacksmith_doors) - - # place dam and pyramid fairy, have limited options - random.shuffle(bomb_shop_doors) - bomb_shop = bomb_shop_doors.pop() - connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) - single_doors.extend(bomb_shop_doors) - - # tavern back door cannot be shuffled yet - connect_doors(world, ['Tavern North'], ['Tavern'], player) - - # place remaining doors - connect_doors(world, single_doors, door_targets, player) elif world.shuffle[player] == 'insanity': # beware ye who enter here @@ -1023,149 +650,6 @@ def link_entrances(world, player): # place remaining doors connect_doors(world, doors, door_targets, player) - elif world.shuffle[player] == 'insanity_legacy': - world.fix_fake_world[player] = False - # beware ye who enter here - - entrances = LW_Entrances + LW_Dungeon_Entrances + DW_Entrances + DW_Dungeon_Entrances + Old_Man_Entrances + ['Skull Woods Second Section Door (East)', 'Skull Woods First Section Door', 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave'] - entrances_must_exits = DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit + LW_Dungeon_Entrances_Must_Exit + ['Skull Woods Second Section Door (West)'] - - doors = LW_Entrances + LW_Dungeon_Entrances + LW_Dungeon_Entrances_Must_Exit + ['Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave'] + Old_Man_Entrances +\ - DW_Entrances + DW_Dungeon_Entrances + DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit + ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)'] - - random.shuffle(doors) - - old_man_entrances = list(Old_Man_Entrances) + ['Tower of Hera'] - - caves = Cave_Exits + Dungeon_Exits + Cave_Three_Exits + ['Old Man House Exit (Bottom)', 'Old Man House Exit (Top)', 'Skull Woods First Section Exit', 'Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)', - 'Kakariko Well Exit', 'Bat Cave Exit', 'North Fairy Cave Exit', 'Lost Woods Hideout Exit', 'Lumberjack Tree Exit', 'Sanctuary Exit'] - - # shuffle up holes - - hole_entrances = ['Kakariko Well Drop', 'Bat Cave Drop', 'North Fairy Cave Drop', 'Lost Woods Hideout Drop', 'Lumberjack Tree Tree', 'Sanctuary Grave', - 'Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'] - - hole_targets = ['Kakariko Well (top)', 'Bat Cave (right)', 'North Fairy Cave', 'Lost Woods Hideout (top)', 'Lumberjack Tree (top)', 'Sewer Drop', 'Skull Back Drop', - 'Skull Left Drop', 'Skull Pinball', 'Skull Pot Circle'] - - if world.mode[player] == 'standard': - # cannot move uncle cave - connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player) - connect_exit(world, 'Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance Stairs', player) - connect_entrance(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player) - else: - hole_entrances.append('Hyrule Castle Secret Entrance Drop') - hole_targets.append('Hyrule Castle Secret Entrance') - doors.append('Hyrule Castle Secret Entrance Stairs') - entrances.append('Hyrule Castle Secret Entrance Stairs') - caves.append('Hyrule Castle Secret Entrance Exit') - - if not world.shuffle_ganon: - connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player) - connect_two_way(world, 'Pyramid Entrance', 'Pyramid Exit', player) - connect_entrance(world, 'Pyramid Hole', 'Pyramid', player) - else: - entrances.append('Ganons Tower') - caves.extend(['Ganons Tower Exit', 'Pyramid Exit']) - hole_entrances.append('Pyramid Hole') - hole_targets.append('Pyramid') - entrances_must_exits.append('Pyramid Entrance') - doors.extend(['Ganons Tower', 'Pyramid Entrance']) - - random.shuffle(hole_entrances) - random.shuffle(hole_targets) - random.shuffle(entrances) - - # fill up holes - for hole in hole_entrances: - connect_entrance(world, hole, hole_targets.pop(), player) - - # hyrule castle handling - if world.mode[player] == 'standard': - # must connect front of hyrule castle to do escape - connect_entrance(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) - connect_exit(world, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player) - caves.append(('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) - else: - doors.append('Hyrule Castle Entrance (South)') - entrances.append('Hyrule Castle Entrance (South)') - caves.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) - - # now let's deal with mandatory reachable stuff - def extract_reachable_exit(cavelist): - random.shuffle(cavelist) - candidate = None - for cave in cavelist: - if isinstance(cave, tuple) and len(cave) > 1: - # special handling: TRock has two entries that we should consider entrance only - # ToDo this should be handled in a more sensible manner - if cave[0] in ['Turtle Rock Exit (Front)', 'Spectacle Rock Cave Exit (Peak)'] and len(cave) == 2: - continue - candidate = cave - break - if candidate is None: - raise RuntimeError('No suitable cave.') - cavelist.remove(candidate) - return candidate - - def connect_reachable_exit(entrance, caves, doors): - cave = extract_reachable_exit(caves) - - exit = cave[-1] - cave = cave[:-1] - connect_exit(world, exit, entrance, player) - connect_entrance(world, doors.pop(), exit, player) - # rest of cave now is forced to be in this world - caves.append(cave) - - # connect mandatory exits - for entrance in entrances_must_exits: - connect_reachable_exit(entrance, caves, doors) - - # place old man, has limited options - # exit has to come from specific set of doors, the entrance is free to move about - old_man_entrances = [entrance for entrance in old_man_entrances if entrance in entrances] - random.shuffle(old_man_entrances) - old_man_exit = old_man_entrances.pop() - entrances.remove(old_man_exit) - - connect_exit(world, 'Old Man Cave Exit (East)', old_man_exit, player) - connect_entrance(world, doors.pop(), 'Old Man Cave Exit (East)', player) - caves.append('Old Man Cave Exit (West)') - - # handle remaining caves - for cave in caves: - if isinstance(cave, str): - cave = (cave,) - - for exit in cave: - connect_exit(world, exit, entrances.pop(), player) - connect_entrance(world, doors.pop(), exit, player) - - # handle simple doors - - single_doors = list(Single_Cave_Doors) - bomb_shop_doors = list(Bomb_Shop_Single_Cave_Doors) - blacksmith_doors = list(Blacksmith_Single_Cave_Doors) - door_targets = list(Single_Cave_Targets) - - # place blacksmith, has limited options - random.shuffle(blacksmith_doors) - blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) - bomb_shop_doors.extend(blacksmith_doors) - - # place dam and pyramid fairy, have limited options - random.shuffle(bomb_shop_doors) - bomb_shop = bomb_shop_doors.pop() - connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) - single_doors.extend(bomb_shop_doors) - - # tavern back door cannot be shuffled yet - connect_doors(world, ['Tavern North'], ['Tavern'], player) - - # place remaining doors - connect_doors(world, single_doors, door_targets, player) else: raise NotImplementedError('Shuffling not supported yet') @@ -1326,7 +810,7 @@ def link_inverted_entrances(world, player): 'Fairy Ascension Cave (Bottom)', 'Fairy Ascension Cave (Top)', 'Spiral Cave (Bottom)', 'Old Man Cave (East)', 'Death Mountain Return Cave (East)', 'Spiral Cave', 'Old Man House (Top)', 'Spectacle Rock Cave', 'Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Bottom)'] - + # place old man, bumper cave bottom to DDM entrances not in east bottom random.shuffle(old_man_entrances) @@ -3059,7 +2543,8 @@ Inverted_Must_Exit_Invalid_Connections = defaultdict(set, { }) -# these are connections that cannot be shuffled and always exist. They link together separate parts of the world we need to divide into regions +# these are connections that cannot be shuffled and always exist. +# They link together separate parts of the world we need to divide into regions mandatory_connections = [('Links House S&Q', 'Links House'), ('Sanctuary S&Q', 'Sanctuary'), ('Old Man S&Q', 'Old Man House'), diff --git a/Fill.py b/Fill.py index 3645e5d9..43a3abc0 100644 --- a/Fill.py +++ b/Fill.py @@ -11,7 +11,7 @@ from source.item.FillUtil import filter_pot_locations, valid_pot_items def get_dungeon_item_pool(world): - return [item for dungeon in world.dungeons for item in dungeon.all_items] + return [item for dungeon in world.dungeons for item in dungeon.all_items if item.location is None] def promote_dungeon_items(world): diff --git a/Gui.py b/Gui.py index 032cce90..9eab4aac 100755 --- a/Gui.py +++ b/Gui.py @@ -179,7 +179,7 @@ def guiMain(args=None): self.pages["startinventory"].content.pack(side=TOP, fill=BOTH, expand=True) # Custom Controls - self.pages["custom"].content = custom_page(self,self.pages["custom"]) + self.pages["custom"].content = custom_page(self, self.pages["custom"]) self.pages["custom"].content.pack(side=TOP, fill=BOTH, expand=True) def validation(P): diff --git a/ItemList.py b/ItemList.py index ea35dd59..da01706f 100644 --- a/ItemList.py +++ b/ItemList.py @@ -6,7 +6,7 @@ import RaceRandom as random from BaseClasses import Region, RegionType, Shop, ShopType, Location, CollectionState, PotItem from EntranceShuffle import connect_entrance from Regions import shop_to_location_table, retro_shops, shop_table_by_location, valid_pot_location -from Fill import FillError, fill_restrictive, fast_fill, get_dungeon_item_pool +from Fill import FillError, fill_restrictive, get_dungeon_item_pool, is_dungeon_item from PotShuffle import vanilla_pots from Items import ItemFactory @@ -254,7 +254,9 @@ def generate_itempool(world, player): world.get_location('Zelda Drop Off', player).locked = True # set up item pool - if world.custom: + if world.customizer and world.customizer.get_item_pool(): + (pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms) = make_customizer_pool(world, player) + elif world.custom: (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) = make_custom_item_pool(world.progressive, world.shuffle[player], world.difficulty[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.bombbag[player], world.customitemarray) world.rupoor_cost = min(world.customitemarray[player]["rupoorcost"], 9999) else: @@ -352,7 +354,9 @@ def generate_itempool(world, player): # rather than making all hearts/heart pieces progression items (which slows down generation considerably) # We mark one random heart container as an advancement item (or 4 heart pieces in expert mode) if world.difficulty[player] in ['normal', 'hard'] and not (world.custom and world.customitemarray[player]["heartcontainer"] == 0): - next(item for item in items if item.name == 'Boss Heart Container').advancement = True + container = next((item for item in items if item.name == 'Boss Heart Container'), None) + if container: + container.advancement = True elif world.difficulty[player] in ['expert'] and not (world.custom and world.customitemarray[player]["heartpiece"] < 4): adv_heart_pieces = (item for item in items if item.name == 'Piece of Heart') for i in range(4): @@ -372,8 +376,19 @@ def generate_itempool(world, player): world.itempool += [beemizer(item) for item in items] # shuffle medallions - mm_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)] - tr_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)] + mm_medallion, tr_medallion = None, None + if world.customizer and world.customizer.get_medallions() and player in world.customizer.get_medallions(): + medal_map = world.customizer.get_medallions() + if player in medal_map: + custom_medallions = medal_map[player] + if 'Misery Mire' in custom_medallions: + mm_medallion = custom_medallions['Misery Mire'] + if 'Turtle Rock' in custom_medallions: + tr_medallion = custom_medallions['Turtle Rock'] + if not mm_medallion: + mm_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)] + if not tr_medallion: + tr_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)] world.required_medallions[player] = (mm_medallion, tr_medallion) # shuffle bottle refills @@ -797,12 +812,8 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, lamps_needed_for_dark_rooms = 1 - # insanity shuffle doesn't have fake LW/DW logic so for now guaranteed Mirror and Moon Pearl at the start - if shuffle == 'insanity_legacy': - place_item('Link\'s House', 'Magic Mirror') - place_item('Sanctuary', 'Moon Pearl') - else: - pool.extend(['Magic Mirror', 'Moon Pearl']) + # old insanity shuffle didn't have fake LW/DW logic so this used to be conditional + pool.extend(['Magic Mirror', 'Moon Pearl']) if timer == 'display': clock_mode = 'stopwatch' @@ -993,15 +1004,8 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s pool.extend(['Fighter Sword'] * customitemarray["sword1"]) pool.extend(['Progressive Sword'] * customitemarray["progressivesword"]) - - if shuffle == 'insanity_legacy': - place_item('Link\'s House', 'Magic Mirror') - place_item('Sanctuary', 'Moon Pearl') - pool.extend(['Magic Mirror'] * max((customitemarray["mirror"] -1 ), 0)) - pool.extend(['Moon Pearl'] * max((customitemarray["pearl"] - 1), 0)) - else: - pool.extend(['Magic Mirror'] * customitemarray["mirror"]) - pool.extend(['Moon Pearl'] * customitemarray["pearl"]) + pool.extend(['Magic Mirror'] * customitemarray["mirror"]) + pool.extend(['Moon Pearl'] * customitemarray["pearl"]) if retro: itemtotal = itemtotal - 28 # Corrects for small keys not being in item pool in Retro Mode @@ -1012,6 +1016,39 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) + +def make_customizer_pool(world, player): + pool = [] + placed_items = {} + precollected_items = [] + clock_mode = None + + def place_item(loc, item): + assert loc not in placed_items + placed_items[loc] = item + + diff = difficulties[world.difficulty[player]] + for item_name, amount in world.customizer.get_item_pool()[player].items(): + if isinstance(amount, int): + if item_name == 'Bottle (Random)': + for _ in range(amount): + pool.append(random.choice(diff.bottles)) + else: + pool.extend([item_name] * amount) + + timer = world.timer[player] + if timer in ['display', 'timed', 'timed-countdown']: + clock_mode = 'countdown' if timer == 'timed-countdown' else 'stopwatch' + elif timer == 'timed-ohko': + clock_mode = 'countdown-ohko' + elif timer == 'ohko': + clock_mode = 'ohko' + + if world.goal[player] == 'pedestal': + place_item('Master Sword Pedestal', 'Triforce') + + return pool, placed_items, precollected_items, clock_mode, 1 + # A quick test to ensure all combinations generate the correct amount of items. def test(): for difficulty in ['normal', 'hard', 'expert']: @@ -1020,7 +1057,7 @@ def test(): for mode in ['open', 'standard', 'inverted', 'retro']: for swords in ['random', 'assured', 'swordless', 'vanilla']: for progressive in ['on', 'off']: - for shuffle in ['full', 'insanity_legacy']: + for shuffle in ['full']: for logic in ['noglitches', 'minorglitches', 'owglitches', 'nologic']: for retro in [True, False]: for bombbag in [True, False]: @@ -1044,24 +1081,48 @@ if __name__ == '__main__': def fill_specific_items(world): - keypool = [item for item in world.itempool if item.smallkey] - cage = world.get_location('Tower of Hera - Basement Cage', 1) - c_dungeon = cage.parent_region.dungeon - key_item = next(x for x in keypool if c_dungeon.name in x.name or (c_dungeon.name == 'Hyrule Castle' and 'Escape' in x.name)) - world.itempool.remove(key_item) - all_state = world.get_all_state(True) - fill_restrictive(world, all_state, [cage], [key_item]) - - location = world.get_location('Tower of Hera - Map Chest', 1) - key_item = next(x for x in world.itempool if 'Byrna' in x.name) - world.itempool.remove(key_item) - fast_fill(world, [key_item], [location]) + if world.customizer: + placements = world.customizer.get_placements() + dungeon_pool = get_dungeon_item_pool(world) + prize_pool = [] + prize_set = {'Red Pendant', 'Blue Pendant', 'Green Pendant', 'Crystal 1', 'Crystal 2', + 'Crystal 3', 'Crystal 4', 'Crystal 7', 'Crystal 5', 'Crystal 6'} + for p in range(1, world.players + 1): + prize_pool.extend(prize_set) + if placements: + for player, placement_list in placements.items(): + for location, item in placement_list.items(): + loc = world.get_location(location, player) + item_parts = item.split('#') + item_player = player if len(item_parts) < 2 else int(item_parts[1]) + item_name = item_parts[0] + event_flag = False + if is_dungeon_item(item_name, world, item_player): + item_to_place = next(x for x in dungeon_pool + if x.name == item_name and x.player == item_player) + dungeon_pool.remove(item_to_place) + event_flag = True + elif item_name in prize_set: + item_player = player # prizes must be for that player + item_to_place = ItemFactory(item_name, item_player) + prize_pool.remove(item_name) + event_flag = True + else: + item_to_place = next((x for x in world.itempool + if x.name == item_name and x.player == item_player), None) + if item_to_place is None: + item_to_place = ItemFactory(item_name, player) + else: + world.itempool.remove(item_to_place) + world.push_item(loc, item_to_place, False) + # track_outside_keys(item_to_place, spot_to_fill, world) + # track_dungeon_items(item_to_place, spot_to_fill, world) + loc.event = event_flag or item_to_place.advancement - # somaria = next(item for item in world.itempool if item.name == 'Cane of Somaria') - # shooter = world.get_location('Palace of Darkness - Shooter Room', 1) - # world.itempool.remove(somaria) - # all_state = world.get_all_state(True) - # fill_restrictive(world, all_state, [shooter], [somaria]) - +def is_dungeon_item(item, world, player): + return ((item.startswith('Small Key') and not world.keyshuffle[player]) + or (item.startswith('Big Key') and not world.bigkeyshuffle[player]) + or (item.startswith('Compass') and not world.compassshuffle[player]) + or (item.startswith('Map') and not world.mapshuffle[player])) diff --git a/Main.py b/Main.py index d9212380..e06d434b 100644 --- a/Main.py +++ b/Main.py @@ -25,11 +25,13 @@ from Rules import set_rules from Dungeons import create_dungeons from Fill import distribute_items_restrictive, promote_dungeon_items, fill_dungeons_restrictive, ensure_good_pots from Fill import sell_potions, sell_keys, balance_multiworld_progression, balance_money_progression, lock_shop_locations -from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops +from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops, fill_specific_items from Utils import output_path, parse_player_names from source.item.FillUtil import create_item_pool_config, massage_item_pool, district_item_pool_config +from source.overworld.EntranceShuffle2 import link_entrances_new from source.tools.BPS import create_bps_from_data +from source.classes.CustomSettings import CustomSettings __version__ = '1.0.1.11v' @@ -57,22 +59,32 @@ def main(args, seed=None, fish=None): if args.securerandom: random.use_secure() - + seeded = False # initialize the world if args.code: for player, code in args.code.items(): if code: Settings.adjust_args_from_code(code, player, args) + customized = None + if args.customizer: + customized = CustomSettings() + customized.load_yaml(args.customizer) + seed = customized.determine_seed() + if seed: + seeded = True + customized.adjust_args(args) world = World(args.multi, args.shuffle, args.door_shuffle, args.logic, args.mode, args.swords, args.difficulty, args.item_functionality, args.timer, args.progressive, args.goal, args.algorithm, args.accessibility, args.shuffleganon, args.retro, args.custom, args.customitemarray, args.hints) + world.customizer = customized if customized else None logger = logging.getLogger('') if seed is None: random.seed(None) world.seed = random.randint(0, 999999999) else: world.seed = int(seed) - random.seed(world.seed) + if not seeded: + random.seed(world.seed) if args.securerandom: world.seed = ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(9)) @@ -128,6 +140,8 @@ def main(args, seed=None, fish=None): for player, name in enumerate(team, 1): world.player_names[player].append(name) logger.info('') + world.settings = CustomSettings() + world.settings.create_from_world(world) for player in range(1, world.players + 1): world.difficulty_requirements[player] = difficulties[world.difficulty[player]] @@ -156,6 +170,15 @@ def main(args, seed=None, fish=None): adjust_locations(world, player) place_bosses(world, player) + if world.customizer and world.customizer.get_start_inventory(): + for p, inv_list in world.customizer.get_start_inventory().items(): + for inv_item in inv_list: + item = ItemFactory(inv_item.strip(), p) + if item: + world.push_precollected(item) + if args.print_custom_yaml: + world.settings.record_info(world) + if any(world.potshuffle.values()): logger.info(world.fish.translate("cli", "cli", "shuffling.pots")) for player in range(1, world.players + 1): @@ -168,15 +191,20 @@ def main(args, seed=None, fish=None): logger.info(world.fish.translate("cli","cli","shuffling.world")) for player in range(1, world.players + 1): - if world.mode[player] != 'inverted': - link_entrances(world, player) + if world.experimental[player] or (world.customizer and world.customizer.get_entrances()): + link_entrances_new(world, player) else: - link_inverted_entrances(world, player) + if world.mode[player] != 'inverted': + link_entrances(world, player) + else: + link_inverted_entrances(world, player) logger.info(world.fish.translate("cli", "cli", "shuffling.prep")) for player in range(1, world.players + 1): link_doors_prep(world, player) + if args.print_custom_yaml: + world.settings.record_entrances(world) create_item_pool_config(world) logger.info(world.fish.translate("cli", "cli", "shuffling.dungeons")) @@ -187,6 +215,8 @@ def main(args, seed=None, fish=None): mark_light_world_regions(world, player) else: mark_dark_world_regions(world, player) + if args.print_custom_yaml: + world.settings.record_doors(world) logger.info(world.fish.translate("cli", "cli", "generating.itempool")) for player in range(1, world.players + 1): @@ -207,13 +237,13 @@ def main(args, seed=None, fish=None): lock_shop_locations(world, player) massage_item_pool(world) + if args.print_custom_yaml: + world.settings.record_item_pool(world) + fill_specific_items(world) logger.info(world.fish.translate("cli", "cli", "placing.dungeon.prizes")) fill_prizes(world) - # used for debugging - # fill_specific_items(world) - logger.info(world.fish.translate("cli","cli","placing.dungeon.items")) if args.algorithm != 'equitable': @@ -258,6 +288,9 @@ def main(args, seed=None, fish=None): ensure_good_pots(world, True) outfilebase = f'DR_{args.outputname if args.outputname else world.seed}' + if args.print_custom_yaml: + world.settings.record_item_placements(world) + world.settings.write_to_file(output_path(f'{outfilebase}_custom.yaml')) rom_names = [] jsonout = {} diff --git a/Mystery.py b/Mystery.py index c8d4da4a..3a33dad2 100644 --- a/Mystery.py +++ b/Mystery.py @@ -1,15 +1,15 @@ import argparse import logging import RaceRandom as random -import urllib.request -import urllib.parse -import yaml from DungeonRandomizer import parse_cli from Main import main as DRMain from source.classes.BabelFish import BabelFish from yaml.constructor import SafeConstructor +from source.tools.MysteryUtils import roll_settings, get_weights + + def add_bool(self, node): return self.construct_scalar(node) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b67ae9bf..4d7688c0 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -35,6 +35,15 @@ The old "Pot Shuffle" option is still available under "Pot Shuffle (Legacy)" or The sram locations for pots and sprite drops are not yet final, please reach out for assistance or investigate the rom changes. +## Customizer + +Please refer to [the documentation](docs/Customizer.md) and examples of customizer [here](docs/customizer_example.yaml) and [here](docs/multi_mystery_example.yaml) +note that entrance customization is only available with experimental features turned on. + +## Experimental Entrance Shuffle + +To support customizer and future entrance shuffle modes (perhaps even customizable ones), the entrance shuffle algorithm has been re-written. It is currently in an unstable state, and will use the old method unless you turn experimental features on. I'm currently in the process of evaluating most modes with different combinations of settings and checking the distribution of entrances. Entrance customization is only supported with this new experimental entrance shuffle. The experimental entrance shuffle includes prototypes of Lean and Lite entrance shuffles from the OWR branch. + ## Restricted Item Placement Algorithm diff --git a/Rom.py b/Rom.py index 2b376e56..2069bb7f 100644 --- a/Rom.py +++ b/Rom.py @@ -681,7 +681,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): # Thanks to Zarby89 for originally finding these values # todo fix screen scrolling - if world.shuffle[player] not in ['insanity', 'insanity_legacy', 'madness_legacy'] and \ + if world.shuffle[player] not in ['insanity'] and \ exit.name in ['Eastern Palace Exit', 'Tower of Hera Exit', 'Thieves Town Exit', 'Ice Palace Exit', 'Misery Mire Exit', 'Palace of Darkness Exit', 'Swamp Palace Exit', 'Ganons Tower Exit', 'Desert Palace Exit (North)', 'Agahnims Tower Exit', 'Spiral Cave Exit (Top)', 'Superbunny Cave Exit (Bottom)', 'Turtle Rock Ledge Exit (East)']: @@ -790,8 +790,8 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_byte(0x13f030+offset, layout.max_chests) builder = world.dungeon_layouts[player][name] valid_cnt = len(valid_loc_by_dungeon[name]) - if valid_cnt > 99: - logging.getLogger('').warning(f'{name} exceeds 99 in locations ({valid_cnt})') + if valid_cnt > 256: + logging.getLogger('').warning(f'{name} exceeds 256 in locations ({valid_cnt})') rom.write_byte(0x13f080+offset, valid_cnt % 10) rom.write_byte(0x13f090+offset, valid_cnt // 10) rom.write_byte(0x13f0a0+offset, valid_cnt) @@ -1644,7 +1644,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_bytes(0x02F539, [0xEA, 0xEA, 0xEA, 0xEA, 0xEA] if world.powder_patch_required[player] else [0xAD, 0xBF, 0x0A, 0xF0, 0x4F]) # allow smith into multi-entrance caves in appropriate shuffles - if world.shuffle[player] in ['restricted', 'full', 'crossed', 'insanity'] or (world.shuffle[player] == 'simple' and world.mode[player] == 'inverted'): + if world.shuffle[player] in ['restricted', 'full', 'lite', 'lean', 'crossed', 'insanity'] or (world.shuffle[player] == 'simple' and world.mode[player] == 'inverted'): rom.write_byte(0x18004C, 0x01) # set correct flag for hera basement item @@ -2133,7 +2133,7 @@ def write_strings(rom, world, player, team): entrances_to_hint.update({'Inverted Ganons Tower': 'The sealed castle door'}) else: entrances_to_hint.update({'Ganons Tower': 'Ganon\'s Tower'}) - if world.shuffle[player] in ['simple', 'restricted', 'restricted_legacy']: + if world.shuffle[player] in ['simple', 'restricted']: for entrance in all_entrances: if entrance.name in entrances_to_hint: this_hint = entrances_to_hint[entrance.name] + ' leads to ' + hint_text(entrance.connected_region) + '.' @@ -2144,22 +2144,22 @@ def write_strings(rom, world, player, team): entrances_to_hint.update(InconvenientOtherEntrances) if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']: hint_count = 0 - elif world.shuffle[player] in ['simple', 'restricted', 'restricted_legacy']: + elif world.shuffle[player] in ['simple', 'restricted']: hint_count = 2 else: hint_count = 4 for entrance in all_entrances: - if entrance.name in entrances_to_hint: - if hint_count > 0: + if hint_count > 0: + if entrance.name in entrances_to_hint: this_hint = entrances_to_hint[entrance.name] + ' leads to ' + hint_text(entrance.connected_region) + '.' tt[hint_locations.pop(0)] = this_hint entrances_to_hint.pop(entrance.name) hint_count -= 1 - else: - break + else: + break #Next we handle hints for randomly selected other entrances, curating the selection intelligently based on shuffle. - if world.shuffle[player] not in ['simple', 'restricted', 'restricted_legacy']: + if world.shuffle[player] not in ['simple', 'restricted']: entrances_to_hint.update(ConnectorEntrances) entrances_to_hint.update(DungeonEntrances) if world.mode[player] == 'inverted': @@ -2168,7 +2168,17 @@ def write_strings(rom, world, player, team): entrances_to_hint.update({'Agahnims Tower': 'The sealed castle door'}) elif world.shuffle[player] == 'restricted': entrances_to_hint.update(ConnectorEntrances) - entrances_to_hint.update(OtherEntrances) + entrances_to_hint.update(ItemEntrances) + if world.shuffle[player] not in ['lite', 'lean']: + entrances_to_hint.update(ShopEntrances) + entrances_to_hint.update(OtherEntrances) + elif world.shopsanity[player]: + entrances_to_hint.update(ShopEntrances) + if world.shufflelinks[player] and world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull']: + if world.mode[player] == 'inverted': + entrances_to_hint.update({'Inverted Links House': 'The hero\'s old residence'}) + else: + entrances_to_hint.update({'Links House': 'The hero\'s old residence'}) if world.mode[player] == 'inverted': entrances_to_hint.update({'Inverted Dark Sanctuary': 'The dark sanctuary cave'}) entrances_to_hint.update({'Inverted Big Bomb Shop': 'The old hero\'s dark home'}) @@ -2176,7 +2186,7 @@ def write_strings(rom, world, player, team): else: entrances_to_hint.update({'Dark Sanctuary Hint': 'The dark sanctuary cave'}) entrances_to_hint.update({'Big Bomb Shop': 'The old bomb shop'}) - if world.shuffle[player] in ['insanity', 'madness_legacy', 'insanity_legacy']: + if world.shuffle[player] in ['insanity']: entrances_to_hint.update(InsanityEntrances) if world.shuffle_ganon: if world.mode[player] == 'inverted': @@ -2802,16 +2812,45 @@ DungeonEntrances = {'Eastern Palace': 'Eastern Palace', 'Desert Palace Entrance (North)': 'The northmost cave in the desert' } -OtherEntrances = {'Blinds Hideout': 'Blind\'s old house', - 'Lake Hylia Fairy': 'A cave NE of Lake Hylia', +ItemEntrances = {'Blinds Hideout': 'Blind\'s old house', + 'Chicken House': 'The chicken lady\'s house', + 'Aginahs Cave': 'The open desert cave', + 'Sahasrahlas Hut': 'The house near armos', + 'Blacksmiths Hut': 'The old smithery', + 'Sick Kids House': 'The central house in Kakariko', + 'Mini Moldorm Cave': 'The cave south of Lake Hylia', + 'Ice Rod Cave': 'The sealed cave SE Lake Hylia', + 'Library': 'The old library', + 'Potion Shop': 'The witch\'s building', + 'Dam': 'The old dam', + 'Waterfall of Wishing': 'Going behind the waterfall', + 'Bonk Rock Cave': 'The rock pile near Sanctuary', + 'Graveyard Cave': 'The graveyard ledge', + 'Checkerboard Cave': 'The NE desert ledge', + 'Cave 45': 'The ledge south of haunted grove', + 'Kings Grave': 'The northeastmost grave', + 'C-Shaped House': 'The NE house in Village of Outcasts', + 'Mire Shed': 'The western hut in the mire', + 'Spike Cave': 'The ledge cave on west dark DM', + 'Hype Cave': 'The cave south of the old bomb shop', + 'Brewery': 'The Village of Outcasts building with no door', + 'Chest Game': 'The westmost building in the Village of Outcasts', + } + +ShopEntrances = {'Cave Shop (Lake Hylia)': 'The cave NW Lake Hylia', + 'Kakariko Shop': 'The old Kakariko shop', + 'Capacity Upgrade': 'The cave on the island', + 'Dark Lake Hylia Shop': 'The building NW dark Lake Hylia', + 'Dark World Shop': 'The hammer sealed building', + 'Red Shield Shop': 'The fenced in building', + 'Cave Shop (Dark Death Mountain)': 'The base of east dark DM', + 'Dark World Potion Shop': 'The building near the catfish', + 'Dark World Lumberjack Shop': 'The northmost Dark World building' + } + +OtherEntrances = {'Lake Hylia Fairy': 'A cave NE of Lake Hylia', 'Light Hype Fairy': 'The cave south of your house', 'Desert Fairy': 'The cave near the desert', - 'Chicken House': 'The chicken lady\'s house', - 'Aginahs Cave': 'The open desert cave', - 'Sahasrahlas Hut': 'The house near armos', - 'Cave Shop (Lake Hylia)': 'The cave NW Lake Hylia', - 'Blacksmiths Hut': 'The old smithery', - 'Sick Kids House': 'The central house in Kakariko', 'Lost Woods Gamble': 'A tree trunk door', 'Fortune Teller (Light)': 'A building NE of Kakariko', 'Snitch Lady (East)': 'A house guarded by a snitch', @@ -2819,49 +2858,24 @@ OtherEntrances = {'Blinds Hideout': 'Blind\'s old house', 'Bush Covered House': 'A house with an uncut lawn', 'Tavern (Front)': 'A building with a backdoor', 'Light World Bomb Hut': 'A Kakariko building with no door', - 'Kakariko Shop': 'The old Kakariko shop', - 'Mini Moldorm Cave': 'The cave south of Lake Hylia', 'Long Fairy Cave': 'The eastmost portal cave', 'Good Bee Cave': 'The open cave SE Lake Hylia', '20 Rupee Cave': 'The rock SE Lake Hylia', '50 Rupee Cave': 'The rock near the desert', - 'Ice Rod Cave': 'The sealed cave SE Lake Hylia', - 'Library': 'The old library', - 'Potion Shop': 'The witch\'s building', - 'Dam': 'The old dam', 'Lumberjack House': 'The lumberjack house', 'Lake Hylia Fortune Teller': 'The building NW Lake Hylia', 'Kakariko Gamble Game': 'The old Kakariko gambling den', - 'Waterfall of Wishing': 'Going behind the waterfall', - 'Capacity Upgrade': 'The cave on the island', - 'Bonk Rock Cave': 'The rock pile near Sanctuary', - 'Graveyard Cave': 'The graveyard ledge', - 'Checkerboard Cave': 'The NE desert ledge', - 'Cave 45': 'The ledge south of haunted grove', - 'Kings Grave': 'The northeastmost grave', 'Bonk Fairy (Light)': 'The rock pile near your home', 'Hookshot Fairy': 'The left paired cave on east DM', - 'Bonk Fairy (Dark)': 'The rock pile near the old bomb shop', + 'Bonk Fairy (Dark)': 'The rock pile near the old bomb shop', 'Dark Lake Hylia Fairy': 'The cave NE dark Lake Hylia', - 'C-Shaped House': 'The NE house in Village of Outcasts', 'Dark Death Mountain Fairy': 'The SW cave on dark DM', - 'Dark Lake Hylia Shop': 'The building NW dark Lake Hylia', - 'Dark World Shop': 'The hammer sealed building', - 'Red Shield Shop': 'The fenced in building', - 'Mire Shed': 'The western hut in the mire', 'East Dark World Hint': 'The dark cave near the eastmost portal', 'Dark Desert Hint': 'The cave east of the mire', - 'Spike Cave': 'The ledge cave on west dark DM', 'Palace of Darkness Hint': 'The building south of Kiki', 'Dark Lake Hylia Ledge Spike Cave': 'The rock SE dark Lake Hylia', - 'Cave Shop (Dark Death Mountain)': 'The base of east dark DM', - 'Dark World Potion Shop': 'The building near the catfish', 'Archery Game': 'The old archery game', - 'Dark World Lumberjack Shop': 'The northmost Dark World building', - 'Hype Cave': 'The cave south of the old bomb shop', - 'Brewery': 'The Village of Outcasts building with no door', 'Dark Lake Hylia Ledge Hint': 'The open cave SE dark Lake Hylia', - 'Chest Game': 'The westmost building in the Village of Outcasts', 'Dark Desert Fairy': 'The eastern hut in the mire', 'Dark Lake Hylia Ledge Fairy': 'The sealed cave SE dark Lake Hylia', 'Fortune Teller (Dark)': 'The building NE the Village of Outcasts' diff --git a/RoomData.py b/RoomData.py index f3c82576..e3f432d0 100644 --- a/RoomData.py +++ b/RoomData.py @@ -316,6 +316,18 @@ class Room(object): byte_array.append(kind.value) return byte_array + def next_free(self): + for i, door in enumerate(self.doorList): + if i >= 4: + return None + pos, kind = door + if kind not in [DoorKind.SmallKey, DoorKind.Dashable, DoorKind.Bombable, DoorKind.TrapTriggerable, + DoorKind.Trap, DoorKind.Trap2, DoorKind.TrapTriggerableLow, DoorKind.TrapLowE3, + DoorKind.BigKey, DoorKind.StairKey, DoorKind.StairKey2, DoorKind.StairKeyLow, + DoorKind.BlastWall, DoorKind.BombableEntrance]: + return i + return None + def __str__(self): return str(self.__unicode__()) diff --git a/Rules.py b/Rules.py index e96a483a..81b419a1 100644 --- a/Rules.py +++ b/Rules.py @@ -63,7 +63,7 @@ def set_rules(world, player): if world.mode[player] != 'inverted': set_big_bomb_rules(world, player) - if world.logic[player] == 'owglitches' and world.shuffle[player] not in ('insanity', 'insanity_legacy'): + if world.logic[player] == 'owglitches' and world.shuffle[player] != 'insanity': path_to_courtyard = mirrorless_path_to_castle_courtyard(world, player) add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.world.get_entrance('Dark Death Mountain Offset Mirror', player).can_reach(state) and all(rule(state) for rule in path_to_courtyard), 'or') else: diff --git a/docs/Customizer.md b/docs/Customizer.md new file mode 100644 index 00000000..af9ae7ed --- /dev/null +++ b/docs/Customizer.md @@ -0,0 +1,182 @@ +## Customizer Files + +A customizer file is [yaml file](https://yaml.org/) with different settings. This documentation is intended to be read with an [example customizer file](customizer_example.yaml). + +This can also be used to roll a mystery or mutli-mystery seed via the GUI. [Example](multi_mystery_example.yaml) + +The cli includes a couple arguments to help: + +`--print_custom_yaml` will create a yaml file based on the seed rolled. Treat it like a spoiler. + +`--customizer` takes a file as an argument. + +Present on the GUI as `Print Customizer File` and `Customizer File` on the Generation Setup tab. + +### meta + +Supported values: + +* `players`: number of players +* `seed`: you can set the seed number for a set experience on things that are randomized instead of specified +* `algorithm`: fill algorithm +* `names`: for naming mutliworld players + +###### Not Yet Implemented + +* `mystery`: indicates the seed should be treated as a mystery (hides total collection_rate) (This may end up player specific) + +### settings + +This must be defined by player. Each player number should be listed with the appropriate settings. + +in each player section you can set up default settings for that player or you can specify a mystery yaml file. + +``` +1: player1.yaml +2: + shuffle: crossed + door_shuffle: basic +``` + +Player 1's settings will be determined by rolling the mystery weights and player 2's setting will be default except for those two specified in his section. Each settings should be consistent with the CLI arguments. + +Start inventory is not supported here. It has a separate section. + +###### Not Yet Implemented + +Rom/Adjust flags like sprite, quickswap are not outputing with the print_custom_yaml settings + +### item_pool + +This must be defined by player. Each player number should be listed with the appropriate pool. + +Then each player can have the entire item pool defined. The name of item should be followed by the number of that item in the pool. All key items need to be listed here for now. + +`Bottle (Random)` is supported to randomize bottle contents according to those allowed by difficulty. Pendants and crystals are supported here. + + +##### Known Issues + +1. Dungeon items amount can be increased but not eliminated (as the amount of each dungeon item is either pre-determined or calculated by door rando) and these extra items may not be confined to the dungeon +2. Door rando removes Red Rupees from the pool to make room for extra dungeon items as needed. +3. Shopsanity appends extra shop items to the pool. +4. Beemizer runs after pool creation changing junk items into bees +5. Retro + Shopsanity adds more items to the pool +6. Retro + either of dropshuffle or pottery adds keys to the pool +7. Most pottery settings add a large amount of junk items to the pool + +### placements + +This must be defined by player. Each player number should be listed with the appropriate placement list. + +You may list each location for a player and the item you wish to place there. A location name requires to be enclosed by single quotes if the location name contains a `#` (Most pot locations have the `#`). (Currently no location names have both a `'` and a `#` so you don't need to worry about escaping the `'`) + + For multiworld you can specify which player the item is for using this syntax: + +`#` + + Example: + `Pegasus Boots#3` means the boots for player 3. + + +### entrances + +This must be defined by player. Each player number should be listed with the appropriate sections. This section has three primary subsections: `entrances`, `exits`, and `two-way`. + +#### two-way + +`two-way` should be used for connectors, dungeons that you wish to couple. (as opposite to decoupled in the insanity shuffle). Links house should be placed using this method as is can be decoupled logically. (Haven't tested to see if it works in game). The overworld entrance is listed first, followed by the interior exit that it leads to. (The exit will then be linked to exit at that entrance). + +`50 Rupee Cave: Desert Palace Exit (North)` The 50 Rupee entrance leads to Desert North entrance, and leaving there will spit you out at the same place. + +#### exits + +`exits` is used for the Chris Houlihan Room Exit and connectors and dungeons that you wish to be decoupled from their entrances. Perhaps counter-intuitively, the exit is listed after the entrance from which it emerges. + +`Light Hype Fairy: Chris Houlihan Room Exit` leaving Chris Houlihan Room will spit you out at the Light Hype Fairy. + +(I can easily switch this syntax around if people would like me too) + +#### entrances + +`entrances` is used for single entrances caves, houses, shops, etc. and drops. Single entrance caves always exit to where you enter, they cannot be decoupled. Dungeons and connectors which are decoupled can also be listed here. + +`Chicken House: Kakariko Shop` if you walk into Chicken House door, you will in the Kakariko Shop. + +##### Known Issues + +Chris Houlihan and Links House should be specified together or not at all. + +### doors + +This must be defined by player. Each player number should be listed with the appropriate sections. This section has three primary subsections: `lobbies` and `doors`. + +`lobbies` lists the doors by which each dungeon is entered + +`: ` Ex. `Turtle Rock Chest: TR Lava Escape SE` + +`doors` lists pairs of doors. The first door name is listed is the key. The value of this object may be the paired door name or optionally it can have two properties: `dest` and `type`. If you want a type, you must use the second option. + +The destination door is listed under `dest` +Supported `type`s are `Key Door`, `Bomb Door`, and `Dash Door` + +Here are the two examples of the syntax: + +```Hyrule Dungeon Guardroom Abyss Edge: Hyrule Castle Lobby W``` +``` +Sewers Rat Path WS: + dest: Sewers Secret Room ES + two-way: true + type: Key Door +``` + +You'll note that sub-tile door do not need to be listed, but if you want them to be key doors you will have to list them. + + ###### Not Yet Implemented + + `one-way` to indicate decoupled doors + + ##### Known Issue + + If you specify a door type and those doors cannot be a stateful door due to the nature of the supertile (or you've placed too many on the supertile) an exception is thrown. + +### medallions + +This must be defined by player. Each player number should be listed with the appropriate info. + +Example: +``` +Misery Mire: Ether +Turtle Rock: Quake +``` + +Leave blank or omit if you wish it to be random. + +### bosses + +This must be defined by player. Each player number should be listed with the appropriate boss list. + +This is done as `: ` + +E.g. `Skull Woods: Helmasaur King` for helmacopter. Be sure to turn on at least one enemizer setting for the bosses to actually be randomized. + +### startinventory + +This must be defined by player. Each player number should be listed with a list of items to start with. + +This is a yaml list (note the hyphens): + +``` +startinventory: + 1: + - Pegasus Boots + - Progressive Sword +``` + +To start with multiple copies of progressive items, list them more than once. + +##### Known Issue + +This conflicts with the mystery yaml, if specified. These start inventory items will be added after those are added. + + diff --git a/docs/customizer_example.yaml b/docs/customizer_example.yaml new file mode 100644 index 00000000..3100e6c2 --- /dev/null +++ b/docs/customizer_example.yaml @@ -0,0 +1,108 @@ +meta: + algorithm: balanced + players: 1 + seed: 42 + names: Lonk +settings: + 1: + door_shuffle: basic + dropshuffle: true + experimental: true + goal: ganon + hints: true + intensity: 3 + overworld_map: compass + pseudoboots: true + pottery: keys + shopsanity: true + shuffle: crossed + shufflelinks: true + shufflebosses: unique +item_pool: + 1: + Blue Boomerang: 3 + Bombos: 3 + Book of Mudora: 3 + Boss Heart Container: 30 + Bottle (Random): 12 + Bug Catching Net: 3 + Cane of Byrna: 3 + Cane of Somaria: 3 + Cape: 3 + Ether: 3 + Fire Rod: 3 + Flippers: 3 + Hammer: 3 + Hookshot: 3 + Ice Rod: 3 + Lamp: 3 + Magic Mirror: 3 + Magic Powder: 3 + Magic Upgrade (1/2): 3 + Moon Pearl: 3 + Mushroom: 3 + Ocarina: 3 + Pegasus Boots: 3 + Piece of Heart: 24 + Progressive Armor: 6 + Progressive Bow: 6 + Progressive Glove: 6 + Progressive Shield: 9 + Progressive Sword: 12 + Quake: 3 + Red Boomerang: 3 + Rupees (300): 10 + Bombs (3): 20 + Sanctuary Heart Container: 3 + Shovel: 3 + Single Arrow: 1 +placements: + 1: + Palace of Darkness - Big Chest: Hammer + Capacity Upgrade - Left: Moon Pearl + Turtle Rock - Pokey 2 Key Drop: Ice Rod +entrances: + 1: + entrances: + Bat Cave Drop: Pyramid + Dam: Chicken House + Snitch Lady (West): Cave 45 + exits: + Misery Mire: Chris Houlihan Room Exit + two-way: + Bat Cave Cave: Pyramid Exit + Bumper Cave (Bottom): Bumper Cave Exit (Bottom) + Bumper Cave (Top): Bumper Cave Exit (Top) + Skull Woods Final Section: Ganons Tower Exit + Misery Mire: Links House Exit +doors: + 1: + lobbies: + Sanctuary: Sewers Pull Switch S + Skull 2 East: Skull 1 Lobby S + Skull 3: Skull 2 West Lobby S + Turtle Rock Lazy Eyes: TR Roller Room SW + doors: + Hyrule Dungeon Guardroom Abyss Edge: Hyrule Castle Lobby W + Sewers Rat Path WS: + dest: Sewers Secret Room ES + type: Key Door + Sewers Rat Path WN: + dest: Sewers Secret Room EN + type: Key Door + Tower Pacifist Run WS: + dest: Tower Circle of Pots ES + type: Key Door + Hera Lobby Up Stairs: Hera 4F Down Stairs +medallions: + 1: + Misery Mire: Bombos +bosses: + 1: + Palace of Darkness: Arrghus + Thieves Town: Blind + Ganons Tower (top): Vitreous +startinventory: + 1: + - Pegasus Boots + diff --git a/docs/multi_mystery_example.yaml b/docs/multi_mystery_example.yaml new file mode 100644 index 00000000..74d6b755 --- /dev/null +++ b/docs/multi_mystery_example.yaml @@ -0,0 +1,17 @@ +meta: + algorithm: balanced + players: 3 + seed: 722495389 + names: Link,Zelda,Ganon +settings: + 1: player1.yml + 2: player2.yml + 3: player3.yml +start_inventory: + 1: + - Progressive Sword + - Pegasus Boots + 2: + - Lamp + 3: + - Flippers \ No newline at end of file diff --git a/docs/player1.yml b/docs/player1.yml new file mode 100644 index 00000000..6c41d350 --- /dev/null +++ b/docs/player1.yml @@ -0,0 +1,19 @@ +description: I like my keysanity distributed weirdly, but no doors please +door_shuffle: vanilla +dungeon_items: + standard: 12 + m: 3 + c: 3 + s: 3 + b: 3 + mc: 2 + ms: 2 + mb: 2 + cs: 2 + cb: 2 + sb: 2 + mcs: 3 + mcb: 3 + msb: 3 + csb: 3 + full: 12 diff --git a/docs/player2.yml b/docs/player2.yml new file mode 100644 index 00000000..a57d1403 --- /dev/null +++ b/docs/player2.yml @@ -0,0 +1,16 @@ +description: Some sort of er with unknown goals +door_shuffle: basic +entrance_shuffle: + dungeonssimple: 3 + dungeonsfull: 2 + simple: 2 + restricted: 2 + full: 2 + crossed: 3 + insanity: 1 +goals: + ganon: 2 + fast_ganon: 2 + dungeons: 1 + pedestal: 2 + triforce-hunt: 1 \ No newline at end of file diff --git a/docs/player3.yml b/docs/player3.yml new file mode 100644 index 00000000..52ab32d3 --- /dev/null +++ b/docs/player3.yml @@ -0,0 +1,25 @@ +description: Door feature fan +door_shuffle: crossed +world_state: + standard: 2 + open: 2 + inverted: 1 +intensity: + 1: 1 + 2: 1 + 3: 2 +keydropshuffle: + on: 1 + off: 1 +shopsanity: + on: 1 + off: 4 +restrict_boss_items: + none: 1 + dungeon: 1 +boss_shuffle: + none: 4 + simple: 1 + full: 1 + unique: 1 + random: 1 \ No newline at end of file diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 70bf5ab0..b2769bbf 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -132,12 +132,10 @@ "full", "crossed", "insanity", - "restricted_legacy", - "full_legacy", - "madness_legacy", - "insanity_legacy", "dungeonsfull", - "dungeonssimple" + "dungeonssimple", + "lite", + "lean" ] }, "door_shuffle": { diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 6ae45ea5..d815cb6d 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -281,6 +281,7 @@ "bombbag": ["Start with 0 bomb capacity. Two capacity upgrades (+10) are added to the pool (default: %(default)s)" ], "startinventory": [ "Specifies a list of items that will be in your starting inventory (separated by commas). (default: %(default)s)" ], "usestartinventory": [ "Toggle usage of Starting Inventory." ], + "customizer": ["Path to a customizer file."], "custom": [ "Not supported." ], "customitemarray": [ "Not supported." ], "accessibility": [ diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index e18f7304..c81dcb61 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -131,15 +131,13 @@ "randomizer.entrance.entranceshuffle": "Entrance Shuffle", "randomizer.entrance.entranceshuffle.vanilla": "Vanilla", + "randomizer.entrance.entranceshuffle.lite": "Lite", + "randomizer.entrance.entranceshuffle.lean": "Lean", "randomizer.entrance.entranceshuffle.simple": "Simple", "randomizer.entrance.entranceshuffle.restricted": "Restricted", "randomizer.entrance.entranceshuffle.full": "Full", "randomizer.entrance.entranceshuffle.crossed": "Crossed", "randomizer.entrance.entranceshuffle.insanity": "Insanity", - "randomizer.entrance.entranceshuffle.restricted_legacy": "Restricted (Legacy)", - "randomizer.entrance.entranceshuffle.full_legacy": "Full (Legacy)", - "randomizer.entrance.entranceshuffle.madness_legacy": "Madness (Legacy)", - "randomizer.entrance.entranceshuffle.insanity_legacy": "Insanity (Legacy)", "randomizer.entrance.entranceshuffle.dungeonsfull": "Dungeons + Full", "randomizer.entrance.entranceshuffle.dungeonssimple": "Dungeons + Simple", @@ -192,6 +190,7 @@ "randomizer.generation.createspoiler": "Create Spoiler Log", "randomizer.generation.createrom": "Create Patched ROM", "randomizer.generation.calcplaythrough": "Calculate Playthrough", + "randomizer.generation.print_custom_yaml": "Print Customizer File", "randomizer.generation.usestartinventory": "Use Starting Inventory", "randomizer.generation.usecustompool": "Use Custom Item Pool", diff --git a/resources/app/gui/randomize/entrando/widgets.json b/resources/app/gui/randomize/entrando/widgets.json index ba9b7fd7..c7dde4f6 100644 --- a/resources/app/gui/randomize/entrando/widgets.json +++ b/resources/app/gui/randomize/entrando/widgets.json @@ -6,15 +6,13 @@ "type": "selectbox", "options": [ "vanilla", + "lite", + "lean", "simple", "restricted", "full", "crossed", "insanity", - "restricted_legacy", - "full_legacy", - "madness_legacy", - "insanity_legacy", "dungeonsfull", "dungeonssimple" ] diff --git a/resources/app/gui/randomize/generation/checkboxes.json b/resources/app/gui/randomize/generation/checkboxes.json index ef37e0c1..9c196402 100644 --- a/resources/app/gui/randomize/generation/checkboxes.json +++ b/resources/app/gui/randomize/generation/checkboxes.json @@ -4,6 +4,7 @@ "bps": { "type": "checkbox" }, "createspoiler": { "type": "checkbox" }, "calcplaythrough": { "type": "checkbox" }, + "print_custom_yaml": { "type": "checkbox" }, "usestartinventory": { "type": "checkbox" }, "usecustompool": { "type": "checkbox" } } diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py new file mode 100644 index 00000000..3b05f7bf --- /dev/null +++ b/source/classes/CustomSettings.py @@ -0,0 +1,329 @@ +import os +import urllib.request +import urllib.parse +import yaml +from yaml.representer import Representer +from collections import defaultdict + +import RaceRandom as random +from BaseClasses import LocationType, DoorType +from source.tools.MysteryUtils import roll_settings, get_weights + + +class CustomSettings(object): + + def __init__(self): + self.file_source = None + self.relative_dir = None + self.world_rep = {} + self.player_range = None + + def load_yaml(self, file): + self.file_source = load_yaml(file) + head, filename = os.path.split(file) + self.relative_dir = head + + def determine_seed(self): + if 'meta' not in self.file_source: + return None + meta = defaultdict(lambda: None, self.file_source['meta']) + seed = meta['seed'] + if seed: + random.seed(seed) + return seed + return None + + def determine_players(self): + if 'meta' not in self.file_source: + return None + meta = defaultdict(lambda: None, self.file_source['meta']) + return meta['players'] + + def adjust_args(self, args): + def get_setting(value, default): + if value: + return value + return default + if 'meta' in self.file_source: + meta = defaultdict(lambda: None, self.file_source['meta']) + args.multi = get_setting(meta['players'], args.multi) + args.algorithm = get_setting(meta['algorithm'], args.algorithm) + args.outputname = get_setting(meta['name'], args.outputname) + args.bps = get_setting(meta['bps'], args.bps) + args.suppress_rom = get_setting(meta['suppress_rom'], args.suppress_rom) + args.names = get_setting(meta['names'], args.names) + self.player_range = range(1, args.multi + 1) + if 'settings' in self.file_source: + for p in self.player_range: + player_setting = self.file_source['settings'][p] + if isinstance(player_setting, str): + weights = get_weights(os.path.join(self.relative_dir, player_setting)) + settings = defaultdict(lambda: None, vars(roll_settings(weights))) + else: + settings = defaultdict(lambda: None, player_setting) + args.shuffle[p] = get_setting(settings['shuffle'], args.shuffle[p]) + args.door_shuffle[p] = get_setting(settings['door_shuffle'], args.door_shuffle[p]) + args.logic[p] = get_setting(settings['logic'], args.logic[p]) + args.mode[p] = get_setting(settings['mode'], args.mode[p]) + args.swords[p] = get_setting(settings['swords'], args.swords[p]) + args.item_functionality[p] = get_setting(settings['item_functionality'], args.item_functionality[p]) + args.goal[p] = get_setting(settings['goal'], args.goal[p]) + args.difficulty[p] = get_setting(settings['difficulty'], args.difficulty[p]) + args.accessibility[p] = get_setting(settings['accessibility'], args.accessibility[p]) + args.retro[p] = get_setting(settings['retro'], args.retro[p]) + args.hints[p] = get_setting(settings['hints'], args.hints[p]) + args.shopsanity[p] = get_setting(settings['shopsanity'], args.shopsanity[p]) + args.dropshuffle[p] = get_setting(settings['dropshuffle'], args.dropshuffle[p]) + args.pottery[p] = get_setting(settings['pottery'], args.pottery[p]) + args.mixed_travel[p] = get_setting(settings['mixed_travel'], args.mixed_travel[p]) + args.standardize_palettes[p] = get_setting(settings['standardize_palettes'], + args.standardize_palettes[p]) + args.intensity[p] = get_setting(settings['intensity'], args.intensity[p]) + args.dungeon_counters[p] = get_setting(settings['dungeon_counters'], args.dungeon_counters[p]) + args.crystals_gt[p] = get_setting(settings['crystals_gt'], args.crystals_gt[p]) + args.crystals_ganon[p] = get_setting(settings['crystals_ganon'], args.crystals_ganon[p]) + args.experimental[p] = get_setting(settings['experimental'], args.experimental[p]) + args.openpyramid[p] = get_setting(settings['openpyramid'], args.openpyramid[p]) + args.bigkeyshuffle[p] = get_setting(settings['bigkeyshuffle'], args.bigkeyshuffle[p]) + args.keyshuffle[p] = get_setting(settings['keyshuffle'], args.keyshuffle[p]) + args.mapshuffle[p] = get_setting(settings['mapshuffle'], args.mapshuffle[p]) + args.compassshuffle[p] = get_setting(settings['compassshuffle'], args.compassshuffle[p]) + args.shufflebosses[p] = get_setting(settings['shufflebosses'], args.shufflebosses[p]) + args.shuffleenemies[p] = get_setting(settings['shuffleenemies'], args.shuffleenemies[p]) + args.enemy_health[p] = get_setting(settings['enemy_health'], args.enemy_health[p]) + args.enemy_damage[p] = get_setting(settings['enemy_damage'], args.enemy_damage[p]) + args.shufflepots[p] = get_setting(settings['shufflepots'], args.shufflepots[p]) + args.bombbag[p] = get_setting(settings['bombbag'], args.bombbag[p]) + args.shufflelinks[p] = get_setting(settings['shufflelinks'], args.shufflelinks[p]) + args.restrict_boss_items[p] = get_setting(settings['restrict_boss_items'], args.restrict_boss_items[p]) + args.overworld_map[p] = get_setting(settings['overworld_map'], args.overworld_map[p]) + args.pseudoboots[p] = get_setting(settings['pseudoboots'], args.pseudoboots[p]) + args.triforce_goal[p] = get_setting(settings['triforce_goal'], args.triforce_goal[p]) + args.triforce_pool[p] = get_setting(settings['triforce_pool'], args.triforce_pool[p]) + args.beemizer[p] = get_setting(settings['beemizer'], args.beemizer[p]) + + # mystery usage + args.usestartinventory[p] = get_setting(settings['usestartinventory'], args.usestartinventory[p]) + args.startinventory[p] = get_setting(settings['startinventory'], args.startinventory[p]) + + # rom adjust stuff + args.sprite[p] = get_setting(settings['sprite'], args.sprite[p]) + args.disablemusic[p] = get_setting(settings['disablemusic'], args.disablemusic[p]) + args.quickswap[p] = get_setting(settings['quickswap'], args.quickswap[p]) + args.reduce_flashing[p] = get_setting(settings['reduce_flashing'], args.reduce_flashing[p]) + args.fastmenu[p] = get_setting(settings['fastmenu'], args.fastmenu[p]) + args.heartcolor[p] = get_setting(settings['heartcolor'], args.heartcolor[p]) + args.heartbeep[p] = get_setting(settings['heartbeep'], args.heartbeep[p]) + args.ow_palettes[p] = get_setting(settings['ow_palettes'], args.ow_palettes[p]) + args.uw_palettes[p] = get_setting(settings['uw_palettes'], args.uw_palettes[p]) + args.shuffle_sfx[p] = get_setting(settings['shuffle_sfx'], args.shuffle_sfx[p]) + + def get_item_pool(self): + if 'item_pool' in self.file_source: + return self.file_source['item_pool'] + return None + + def get_placements(self): + if 'placements' in self.file_source: + return self.file_source['placements'] + return None + + def get_entrances(self): + if 'entrances' in self.file_source: + return self.file_source['entrances'] + return None + + def get_doors(self): + if 'doors' in self.file_source: + return self.file_source['doors'] + return None + + def get_bosses(self): + if 'bosses' in self.file_source: + return self.file_source['bosses'] + return None + + def get_start_inventory(self): + if 'start_inventory' in self.file_source: + return self.file_source['start_inventory'] + return None + + def get_medallions(self): + if 'medallions' in self.file_source: + return self.file_source['medallions'] + return None + + def create_from_world(self, world): + self.player_range = range(1, world.players + 1) + settings_dict, meta_dict = {}, {} + self.world_rep['meta'] = meta_dict + meta_dict['players'] = world.players + meta_dict['algorithm'] = world.algorithm + meta_dict['seed'] = world.seed + self.world_rep['settings'] = settings_dict + for p in self.player_range: + settings_dict[p] = {} + settings_dict[p]['shuffle'] = world.shuffle[p] + settings_dict[p]['door_shuffle'] = world.doorShuffle[p] + settings_dict[p]['intensity'] = world.intensity[p] + settings_dict[p]['logic'] = world.logic[p] + settings_dict[p]['mode'] = world.mode[p] + settings_dict[p]['swords'] = world.swords[p] + settings_dict[p]['difficulty'] = world.difficulty[p] + settings_dict[p]['goal'] = world.goal[p] + settings_dict[p]['accessibility'] = world.accessibility[p] + settings_dict[p]['item_functionality'] = world.difficulty_adjustments[p] + settings_dict[p]['retro'] = world.retro[p] + settings_dict[p]['hints'] = world.hints[p] + settings_dict[p]['shopsanity'] = world.shopsanity[p] + settings_dict[p]['dropshuffle'] = world.dropshuffle[p] + settings_dict[p]['pottery'] = world.pottery[p] + settings_dict[p]['mixed_travel'] = world.mixed_travel[p] + settings_dict[p]['standardize_palettes'] = world.standardize_palettes[p] + settings_dict[p]['dungeon_counters'] = world.dungeon_counters[p] + settings_dict[p]['crystals_gt'] = world.crystals_gt_orig[p] + settings_dict[p]['crystals_ganon'] = world.crystals_ganon_orig[p] + settings_dict[p]['experimental'] = world.experimental[p] + settings_dict[p]['openpyramid'] = world.open_pyramid[p] + settings_dict[p]['bigkeyshuffle'] = world.bigkeyshuffle[p] + settings_dict[p]['keyshuffle'] = world.keyshuffle[p] + settings_dict[p]['mapshuffle'] = world.mapshuffle[p] + settings_dict[p]['compassshuffle'] = world.compassshuffle[p] + settings_dict[p]['shufflebosses'] = world.boss_shuffle[p] + settings_dict[p]['shuffleenemies'] = world.enemy_shuffle[p] + settings_dict[p]['enemy_health'] = world.enemy_health[p] + settings_dict[p]['enemy_damage'] = world.enemy_damage[p] + settings_dict[p]['shufflepots'] = world.potshuffle[p] + settings_dict[p]['bombbag'] = world.bombbag[p] + settings_dict[p]['shufflelinks'] = world.shufflelinks[p] + settings_dict[p]['overworld_map'] = world.overworld_map[p] + settings_dict[p]['pseudoboots'] = world.pseudoboots[p] + settings_dict[p]['triforce_goal'] = world.treasure_hunt_count[p] + settings_dict[p]['triforce_pool'] = world.treasure_hunt_total[p] + settings_dict[p]['beemizer'] = world.beemizer[p] + + # rom adjust stuff + # settings_dict[p]['sprite'] = world.sprite[p] + # settings_dict[p]['disablemusic'] = world.disablemusic[p] + # settings_dict[p]['quickswap'] = world.quickswap[p] + # settings_dict[p]['reduce_flashing'] = world.reduce_flashing[p] + # settings_dict[p]['fastmenu'] = world.fastmenu[p] + # settings_dict[p]['heartcolor'] = world.heartcolor[p] + # settings_dict[p]['heartbeep'] = world.heartbeep[p] + # settings_dict[p]['ow_palettes'] = world.ow_palettes[p] + # settings_dict[p]['uw_palettes'] = world.uw_palettes[p] + # settings_dict[p]['shuffle_sfx'] = world.shuffle_sfx[p] + # more settings? + + def record_info(self, world): + self.world_rep['bosses'] = bosses = {} + self.world_rep['start_inventory'] = start_inv = {} + for p in self.player_range: + bosses[p] = {} + start_inv[p] = [] + for dungeon in world.dungeons: + for level, boss in dungeon.bosses.items(): + location = dungeon.name if level is None else f'{dungeon.name} ({level})' + if boss and 'Agahnim' not in boss.name: + bosses[dungeon.player][location] = boss.name + for item in world.precollected_items: + start_inv[item.player].append(item.name) + + def record_item_pool(self, world): + self.world_rep['item_pool'] = item_pool = {} + self.world_rep['medallions'] = medallions = {} + for p in self.player_range: + item_pool[p] = defaultdict(int) + medallions[p] = {} + for item in world.itempool: + item_pool[item.player][item.name] += 1 + for p, req_medals in world.required_medallions.items(): + medallions[p]['Misery Mire'] = req_medals[0] + medallions[p]['Turtle Rock'] = req_medals[1] + + def record_item_placements(self, world): + self.world_rep['placements'] = placements = {} + for p in self.player_range: + placements[p] = {} + for location in world.get_locations(): + if location.type != LocationType.Logical and not location.skip: + if location.player != location.item.player: + placements[location.player][location.name] = f'{location.item.name}#{location.item.player}' + else: + placements[location.player][location.name] = location.item.name + + def record_entrances(self, world): + self.world_rep['entrances'] = entrances = {} + world.custom_entrances = {} + for p in self.player_range: + connections = entrances[p] = {} + connections['entrances'] = {} + connections['exits'] = {} + connections['two-way'] = {} + for key, data in world.spoiler.entrances.items(): + player = data['player'] if 'player' in data else 1 + connections = entrances[player] + sub = 'two-way' if data['direction'] == 'both' else 'exits' if data['direction'] == 'exit' else 'entrances' + connections[sub][data['entrance']] = data['exit'] + + def record_doors(self, world): + self.world_rep['doors'] = doors = {} + for p in self.player_range: + meta_doors = doors[p] = {} + lobbies = meta_doors['lobbies'] = {} + door_map = meta_doors['doors'] = {} + for portal in world.dungeon_portals[p]: + lobbies[portal.name] = portal.door.name + door_types = {DoorType.Normal, DoorType.SpiralStairs, DoorType.Interior} + if world.intensity[p] > 1: + door_types.update([DoorType.Open, DoorType.StraightStairs, DoorType.Ladder]) + door_kinds, skip = {}, set() + for key, info in world.spoiler.doorTypes.items(): + if key[1] == p: + if ' <-> ' in info['doorNames']: + dns = info['doorNames'].split(' <-> ') + for dn in dns: + door_kinds[dn] = info['type'] # Key Door, Bomb Door, Dash Door + else: + door_kinds[info['doorNames']] = info['type'] + for door in world.doors: + if door.player == p and not door.entranceFlag and door.type in door_types and door not in skip: + if door.type == DoorType.Interior: + if door.name in door_types: + door_value = {'type': door_kinds[door.name]} + door_map[door.name] = door_value # intra-tile note + skip.add(door.dest) + elif door.dest: + if door.dest.dest == door: + door_value = door.dest.name + skip.add(door.dest) + if door.name in door_kinds: + door_value = {'dest': door_value, 'type': door_kinds[door.name]} + if door.name not in door_kinds and door.dest.name in door_kinds: + # tricky swap thing + door_value = {'dest': door.name, 'type': door_kinds[door.dest.name]} + door = door.dest # this is weird + elif door.name in door_kinds: + door_value = {'dest': door.dest.name, 'one-way': True, 'type': door_kinds[door.name]} + else: + door_value = {'dest': door.dest.name, 'one-way': True} + door_map[door.name] = door_value + + def record_medallions(self): + pass + + def write_to_file(self, destination): + yaml.add_representer(defaultdict, Representer.represent_dict) + with open(destination, 'w') as file: + yaml.dump(self.world_rep, file) + + +def load_yaml(path): + try: + if urllib.parse.urlparse(path).scheme: + return yaml.load(urllib.request.urlopen(path), Loader=yaml.FullLoader) + with open(path, 'r', encoding='utf-8') as f: + return yaml.load(f, Loader=yaml.SafeLoader) + except Exception as e: + raise Exception(f'Failed to read customizer file: {e}') + diff --git a/source/classes/constants.py b/source/classes/constants.py index 9199b47a..ca065229 100644 --- a/source/classes/constants.py +++ b/source/classes/constants.py @@ -122,6 +122,7 @@ SETTINGSTOPROCESS = { "createspoiler": "create_spoiler", "createrom": "create_rom", "calcplaythrough": "calc_playthrough", + "print_custom_yaml": "print_custom_yaml", "usestartinventory": "usestartinventory", "usecustompool": "custom", "saveonexit": "saveonexit" diff --git a/source/gui/randomize/generation.py b/source/gui/randomize/generation.py index ea09767f..c66eceb6 100644 --- a/source/gui/randomize/generation.py +++ b/source/gui/randomize/generation.py @@ -79,6 +79,48 @@ def generation_page(parent,settings): # frame: pack self.widgets[widget].pieces["frame"].pack(fill=X) + + self.frames["customizer"] = Frame(self) + self.frames["customizer"].pack(anchor=W, fill=X) + ## Customizer file + # This one's more-complicated, build it and stuff it + # widget ID + widget = "customizer" + + # Empty object + self.widgets[widget] = Empty() + # pieces + self.widgets[widget].pieces = {} + + # frame + self.widgets[widget].pieces["frame"] = Frame(self.frames["customizer"]) + # frame: label + self.widgets[widget].pieces["frame"].label = Label(self.widgets[widget].pieces["frame"], text='Customizer File: ') + # storage var + self.widgets[widget].storageVar = StringVar() + # textbox + self.widgets[widget].pieces["textbox"] = Entry(self.widgets[widget].pieces["frame"], + textvariable=self.widgets[widget].storageVar) + self.widgets[widget].storageVar.set(settings["customizer"]) + + # FIXME: Translate these + def FileSelect(): + file = filedialog.askopenfilename(filetypes=[("Yaml Files", (".yaml", ".yml")), ("All Files", "*")], + initialdir=os.path.join(".")) + self.widgets["customizer"].storageVar.set(file) + # dialog button + self.widgets[widget].pieces["button"] = Button(self.widgets[widget].pieces["frame"], + text='Select File', command=FileSelect) + + # frame label: pack + self.widgets[widget].pieces["frame"].label.pack(side=LEFT) + # textbox: pack + self.widgets[widget].pieces["textbox"].pack(side=LEFT, fill=X, expand=True) + # button: pack + self.widgets[widget].pieces["button"].pack(side=LEFT) + # frame: pack + self.widgets[widget].pieces["frame"].pack(fill=X) + ## Run Diagnostics # This one's more-complicated, build it and stuff it # widget ID diff --git a/source/overworld/EntranceShuffle2.py b/source/overworld/EntranceShuffle2.py new file mode 100644 index 00000000..b46e05e0 --- /dev/null +++ b/source/overworld/EntranceShuffle2.py @@ -0,0 +1,3003 @@ +import RaceRandom as random +import logging +import copy + +from collections import defaultdict + + +class EntrancePool(object): + def __init__(self, world, player): + self.entrances = set() + self.exits = set() + self.inverted = False + self.coupled = True + self.default_map = {} + self.one_way_map = {} + self.skull_handled = False + self.links_on_mountain = False + self.decoupled_entrances = [] + self.decoupled_exits = [] + + self.world = world + self.player = player + + def is_standard(self): + return self.world.mode[self.player] == 'standard' + + +class Restrictions(object): + def __init__(self): + self.size = None + self.must_exit_to_lw = False + self.fixed = False + # must_exit_to_dw = False + # same_world = False + + +def link_entrances_new(world, player): + avail_pool = EntrancePool(world, player) + i_drop_map = {x: y for x, y in drop_map.items() if not x.startswith('Inverted')} + i_entrance_map = {x: y for x, y in entrance_map.items() if not x.startswith('Inverted')} + i_single_ent_map = {x: y for x, y in single_entrance_map.items() if not x.startswith('Inverted')} + + avail_pool.entrances = set(i_drop_map.keys()).union(i_entrance_map.keys()).union(i_single_ent_map.keys()) + avail_pool.exits = set(i_entrance_map.values()).union(i_drop_map.values()).union(i_single_ent_map.values()) + avail_pool.exits.add('Chris Houlihan Room Exit') + avail_pool.inverted = world.mode[player] == 'inverted' + if avail_pool.inverted: + avail_pool.exits.add('Inverted Dark Sanctuary Exit') + inverted_substitution(avail_pool, avail_pool.entrances, True, True) + inverted_substitution(avail_pool, avail_pool.exits, False, True) + default_map = {} + default_map.update(entrance_map) + one_way_map = {} + one_way_map.update(drop_map) + one_way_map.update(single_entrance_map) + if avail_pool.inverted: + default_map['Old Man Cave (West)'] = 'Bumper Cave Exit (Bottom)' + default_map['Death Mountain Return Cave (West)'] = 'Bumper Cave Exit (Top)' + default_map['Bumper Cave (Bottom)'] = 'Old Man Cave Exit (West)' + default_map['Dark Death Mountain Fairy'] = 'Old Man Cave Exit (East)' + del one_way_map['Dark Death Mountain Fairy'] + default_map['Old Man Cave (East)'] = 'Death Mountain Return Cave Exit (West)' + one_way_map['Bumper Cave (Top)'] = 'Dark Death Mountain Healer Fairy' + del default_map['Bumper Cave (Top)'] + avail_pool.default_map = default_map + avail_pool.one_way_map = one_way_map + + # setup mandatory connections + if not avail_pool.inverted: + for exit_name, region_name in mandatory_connections: + connect_simple(world, exit_name, region_name, player) + else: + for exit_name, region_name in inverted_mandatory_connections: + connect_simple(world, exit_name, region_name, player) + + # not randomized at this time + connect_simple(world, 'Tavern North', 'Tavern', player) + + connect_custom(avail_pool, world, player) + + if world.shuffle[player] == 'vanilla': + do_vanilla_connections(avail_pool) + else: + mode = world.shuffle[player] + if mode not in modes: + raise RuntimeError(f'Shuffle mode {mode} is not yet supported') + mode_cfg = copy.deepcopy(modes[mode]) + if avail_pool.is_standard(): + do_standard_connections(avail_pool) + pool_list = mode_cfg['pools'] if 'pools' in mode_cfg else {} + for pool_name, pool in pool_list.items(): + special_shuffle = pool['special'] if 'special' in pool else None + if special_shuffle == 'drops': + holes, targets = find_entrances_and_targets_drops(avail_pool, pool['entrances']) + connect_random(holes, targets, avail_pool) + elif special_shuffle == 'fixed_shuffle': + do_fixed_shuffle(avail_pool, pool['entrances']) + elif special_shuffle == 'same_world': + do_same_world_shuffle(avail_pool, pool) + elif special_shuffle == 'simple_connector': + do_connector_shuffle(avail_pool, pool) + elif special_shuffle == 'old_man_cave_east': + exits = [x for x in pool['entrances'] if x in avail_pool.exits] + cross_world = mode_cfg['cross_world'] == 'on' if 'cross_world' in mode_cfg else False + do_old_man_cave_exit(set(avail_pool.entrances), exits, avail_pool, cross_world) + elif special_shuffle == 'inverted_fixed': + if avail_pool.inverted: + connect_two_way(pool['entrance'], pool['exit'], avail_pool) + elif special_shuffle == 'limited': + do_limited_shuffle(pool, avail_pool) + elif special_shuffle == 'limited_lw': + do_limited_shuffle_exclude_drops(pool, avail_pool) + elif special_shuffle == 'limited_dw': + do_limited_shuffle_exclude_drops(pool, avail_pool, False) + elif special_shuffle == 'vanilla': + do_vanilla_connect(pool, avail_pool) + elif special_shuffle == 'skull': + entrances, exits = find_entrances_and_exits(avail_pool, pool['entrances']) + connect_random(entrances, exits, avail_pool, True) + avail_pool.skull_handled = True + else: + entrances, exits = find_entrances_and_exits(avail_pool, pool['entrances']) + do_main_shuffle(entrances, exits, avail_pool, mode_cfg) + undefined_behavior = mode_cfg['undefined'] + if undefined_behavior == 'vanilla': + do_vanilla_connections(avail_pool) + elif undefined_behavior == 'shuffle': + do_main_shuffle(set(avail_pool.entrances), set(avail_pool.exits), avail_pool, mode_cfg) + + # afterward + + # check for swamp palace fix + if (world.get_entrance('Dam', player).connected_region.name != 'Dam' + or world.get_entrance('Swamp Palace', player).connected_region.name != 'Swamp Portal'): + world.swamp_patch_required[player] = True + + # check for potion shop location + if world.get_entrance('Potion Shop', player).connected_region.name != 'Potion Shop': + world.powder_patch_required[player] = True + + # check for ganon location + pyramid_hole = 'Inverted Pyramid Hole' if avail_pool.inverted else 'Pyramid Hole' + if world.get_entrance(pyramid_hole, player).connected_region.name != 'Pyramid': + world.ganon_at_pyramid[player] = False + + # check for Ganon's Tower location + gt = 'Inverted Ganons Tower' if avail_pool.inverted else 'Ganons Tower' + if world.get_entrance(gt, player).connected_region.name != 'Ganons Tower Portal': + world.ganonstower_vanilla[player] = False + + +def do_vanilla_connections(avail_pool): + if 'Chris Houlihan Room Exit' in avail_pool.exits: + lh = 'Inverted Links House' if avail_pool.inverted else 'Links House' + connect_exit('Chris Houlihan Room Exit', lh, avail_pool) + for ent in list(avail_pool.entrances): + if ent in avail_pool.default_map and avail_pool.default_map[ent] in avail_pool.exits: + connect_vanilla_two_way(ent, avail_pool.default_map[ent], avail_pool) + if ent in avail_pool.one_way_map and avail_pool.one_way_map[ent] in avail_pool.exits: + connect_vanilla(ent, avail_pool.one_way_map[ent], avail_pool) + + +def do_main_shuffle(entrances, exits, avail, mode_def): + # drops and holes + cross_world = mode_def['cross_world'] == 'on' if 'cross_world' in mode_def else False + keep_together = mode_def['keep_drops_together'] == 'on' if 'keep_drops_together' in mode_def else True + avail.coupled = mode_def['decoupled'] != 'on' if 'decoupled' in mode_def else True + do_holes_and_linked_drops(entrances, exits, avail, cross_world, keep_together) + + if not avail.coupled: + avail.decoupled_entrances.extend(entrances) + avail.decoupled_exits.extend(exits) + + if not avail.world.shuffle_ganon: + if avail.inverted and 'Inverted Ganons Tower' in entrances: + connect_two_way('Inverted Ganons Tower', 'Inverted Ganons Tower Exit', avail) + entrances.remove('Inverted Ganons Tower') + exits.remove('Inverted Ganons Tower Exit') + if not avail.coupled: + avail.decoupled_entrances.remove('Inverted Ganons Tower') + avail.decoupled_exits.remove('Inverted Ganons Tower Exit') + elif 'Ganons Tower' in entrances: + connect_two_way('Ganons Tower', 'Ganons Tower Exit', avail) + entrances.remove('Ganons Tower') + exits.remove('Ganons Tower Exit') + if not avail.coupled: + avail.decoupled_entrances.remove('Ganons Tower') + avail.decoupled_exits.remove('Ganons Tower Exit') + + # links house / houlihan + do_links_house(entrances, exits, avail, cross_world) + + # inverted sanc + if avail.inverted and 'Inverted Dark Sanctuary Exit' in exits: + choices = [e for e in Inverted_Dark_Sanctuary_Doors if e in entrances] + choice = random.choice(choices) + entrances.remove(choice) + exits.remove('Inverted Dark Sanctuary Exit') + connect_entrance(choice, 'Inverted Dark Sanctuary', avail) + ext = avail.world.get_entrance('Inverted Dark Sanctuary Exit', avail.player) + ext.connect(avail.world.get_entrance(choice, avail.player).parent_region) + if not avail.coupled: + avail.decoupled_entrances.remove(choice) + + # mandatory exits + rem_entrances, rem_exits = set(), set() + if not cross_world: + mand_exits = figure_out_must_exits_same_world(entrances, exits, avail) + must_exit_lw, must_exit_dw, lw_entrances, dw_entrances, multi_exit_caves, hyrule_forced = mand_exits + if hyrule_forced: + do_mandatory_connections(avail, lw_entrances, hyrule_forced, must_exit_lw) + else: + do_mandatory_connections(avail, lw_entrances, multi_exit_caves, must_exit_lw) + # remove old man house as connector - not valid for dw must_exit if it is a spawn point + if not avail.inverted: + new_mec = [] + for cave_option in multi_exit_caves: + if any('Old Man House' in cave for cave in cave_option): + rem_exits.update([item for item in cave_option]) + else: + new_mec.append(cave_option) + multi_exit_caves = new_mec + do_mandatory_connections(avail, dw_entrances, multi_exit_caves, must_exit_dw) + rem_entrances.update(lw_entrances) + rem_entrances.update(dw_entrances) + else: + # cross world mandantory + entrance_list = list(entrances) + must_exit, multi_exit_caves = figure_out_must_exits_cross_world(entrances, exits, avail) + do_mandatory_connections(avail, entrance_list, multi_exit_caves, must_exit) + rem_entrances.update(entrance_list) + + rem_exits.update([x for item in multi_exit_caves for x in item]) + rem_exits.update(exits) + + # old man cave + do_old_man_cave_exit(rem_entrances, rem_exits, avail, cross_world) + + # blacksmith + if 'Blacksmiths Hut' in rem_exits: + blacksmith_options = [x for x in Blacksmith_Options if x in rem_entrances] + blacksmith_choice = random.choice(blacksmith_options) + connect_entrance(blacksmith_choice, 'Blacksmiths Hut', avail) + rem_entrances.remove(blacksmith_choice) + if not avail.coupled: + avail.decoupled_exits.remove('Blacksmiths Hut') + rem_exits.remove('Blacksmiths Hut') + + # bomb shop + bomb_shop = 'Inverted Big Bomb Shop' if avail.inverted else 'Big Bomb Shop' + if bomb_shop in rem_exits: + bomb_shop_options = Inverted_Bomb_Shop_Options if avail.inverted else Bomb_Shop_Options + bomb_shop_options = [x for x in bomb_shop_options if x in rem_entrances] + bomb_shop_choice = random.choice(bomb_shop_options) + connect_entrance(bomb_shop_choice, bomb_shop, avail) + rem_entrances.remove(bomb_shop_choice) + if not avail.coupled: + avail.decoupled_exits.remove(bomb_shop) + rem_exits.remove(bomb_shop) + + def bonk_fairy_exception(x): # (Bonk Fairy not eligible in standard) + return not avail.is_standard() or x != 'Bonk Fairy (Light)' + if not cross_world: + # OM Cave entrance in lw/dw if cross_world off + if 'Old Man Cave Exit (West)' in rem_exits: + world_limiter = DW_Entrances if avail.inverted else LW_Entrances + om_cave_options = [x for x in rem_entrances if x in world_limiter and bonk_fairy_exception(x)] + om_cave_choice = random.choice(om_cave_options) + if not avail.coupled: + connect_exit('Old Man Cave Exit (West)', om_cave_choice, avail) + avail.decoupled_entrances.remove(om_cave_choice) + else: + connect_two_way(om_cave_choice, 'Old Man Cave Exit (West)', avail) + rem_entrances.remove(om_cave_choice) + rem_exits.remove('Old Man Cave Exit (West)') + # OM House in lw/dw if cross_world off + om_house = ['Old Man House Exit (Bottom)', 'Old Man House Exit (Top)'] + if not avail.inverted: # we don't really care where this ends up in inverted? + for ext in om_house: + if ext in rem_exits: + om_house_options = [x for x in rem_entrances if x in LW_Entrances and bonk_fairy_exception(x)] + om_house_choice = random.choice(om_house_options) + if not avail.coupled: + connect_exit(ext, om_house_choice, avail) + avail.decoupled_entrances.remove(om_house_choice) + else: + connect_two_way(om_house_choice, ext, avail) + rem_entrances.remove(om_house_choice) + rem_exits.remove(ext) + + # the rest of the caves + multi_exit_caves = figure_out_true_exits(rem_exits, avail) + unused_entrances = set() + if not cross_world: + lw_entrances, dw_entrances = [], [] + for x in rem_entrances: + if bonk_fairy_exception(x): + lw_entrances.append(x) if x in LW_Entrances else dw_entrances.append(x) + do_same_world_connectors(lw_entrances, dw_entrances, multi_exit_caves, avail) + unused_entrances.update(lw_entrances) + unused_entrances.update(dw_entrances) + else: + entrance_list = [x for x in rem_entrances if bonk_fairy_exception(x)] + do_cross_world_connectors(entrance_list, multi_exit_caves, avail) + unused_entrances.update(entrance_list) + + if avail.is_standard() and 'Bonk Fairy (Light)' in rem_entrances: + rem_entrances = list(unused_entrances) + ['Bonk Fairy (Light)'] + else: + rem_entrances = list(unused_entrances) + rem_entrances.sort() + rem_exits = list(rem_exits if avail.coupled else avail.decoupled_exits) + rem_exits.sort() + random.shuffle(rem_entrances) + random.shuffle(rem_exits) + placing = min(len(rem_entrances), len(rem_exits)) + for door, target in zip(rem_entrances, rem_exits): + connect_entrance(door, target, avail) + rem_entrances[:] = rem_entrances[placing:] + rem_exits[:] = rem_exits[placing:] + if rem_entrances or rem_exits: + logging.getLogger('').warning(f'Unplaced entrances/exits: {", ".join(rem_entrances + rem_exits)}') + + +def do_old_man_cave_exit(entrances, exits, avail, cross_world): + if 'Old Man Cave Exit (East)' in exits: + om_cave_options = Inverted_Old_Man_Entrances if avail.inverted else Old_Man_Entrances + if avail.inverted and cross_world: + om_cave_options = Inverted_Old_Man_Entrances + Old_Man_Entrances + om_cave_options = [x for x in om_cave_options if x in entrances] + om_cave_choice = random.choice(om_cave_options) + if not avail.coupled: + connect_exit('Old Man Cave Exit (East)', om_cave_choice, avail) + avail.decoupled_entrances.remove(om_cave_choice) + else: + connect_two_way(om_cave_choice, 'Old Man Cave Exit (East)', avail) + entrances.remove(om_cave_choice) + exits.remove('Old Man Cave Exit (East)') + + +def do_standard_connections(avail): + connect_two_way('Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', avail) + # cannot move uncle cave + connect_two_way('Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', avail) + connect_entrance('Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', avail) + connect_two_way('Links House', 'Links House Exit', avail) + connect_exit('Chris Houlihan Room Exit', 'Links House', avail) + + +def remove_from_list(t_list, removals): + for r in removals: + t_list.remove(r) + + +def do_holes_and_linked_drops(entrances, exits, avail, cross_world, keep_together): + holes_to_shuffle = [x for x in entrances if x in drop_map] + + if not avail.world.shuffle_ganon: + if avail.inverted and 'Inverted Pyramid Hole' in holes_to_shuffle: + connect_entrance('Inverted Pyramid Hole', 'Pyramid', avail) + connect_two_way('Pyramid Entrance', 'Pyramid Exit', avail) + holes_to_shuffle.remove('Inverted Pyramid Hole') + remove_from_list(entrances, ['Inverted Pyramid Hole', 'Pyramid Entrance']) + remove_from_list(exits, ['Pyramid', 'Pyramid Exit']) + elif 'Pyramid Hole' in holes_to_shuffle: + connect_entrance('Pyramid Hole', 'Pyramid', avail) + connect_two_way('Pyramid Entrance', 'Pyramid Exit', avail) + holes_to_shuffle.remove('Pyramid Hole') + remove_from_list(entrances, ['Pyramid Hole', 'Pyramid Entrance']) + remove_from_list(exits, ['Pyramid', 'Pyramid Exit']) + + if not keep_together: + targets = [avail.one_way_map[x] for x in holes_to_shuffle] + connect_random(holes_to_shuffle, targets, avail) + remove_from_list(entrances, holes_to_shuffle) + remove_from_list(exits, targets) + return # we're done here + + hole_entrances, hole_targets = [], [] + for hole in drop_map: + if hole in entrances and hole in linked_drop_map: + linked_entrance = linked_drop_map[hole] + if hole in entrances and linked_entrance in entrances: + hole_entrances.append((linked_entrance, hole)) + target_exit = avail.default_map[linked_entrance] + target_drop = avail.one_way_map[hole] + if target_exit in exits and target_drop in exits: + hole_targets.append((target_exit, target_drop)) + + random.shuffle(hole_entrances) + if not cross_world and 'Sanctuary Grave' in holes_to_shuffle: + lw_entrance = next(entrance for entrance in hole_entrances if entrance[0] in LW_Entrances) + hole_entrances.remove(lw_entrance) + sanc_interior = next(target for target in hole_targets if target[0] == 'Sanctuary Exit') + hole_targets.remove(sanc_interior) + connect_two_way(lw_entrance[0], sanc_interior[0], avail) # two-way exit + connect_entrance(lw_entrance[1], sanc_interior[1], avail) # hole + remove_from_list(entrances, [lw_entrance[0], lw_entrance[1]]) + remove_from_list(exits, [sanc_interior[0], sanc_interior[1]]) + + random.shuffle(hole_targets) + for entrance, drop in hole_entrances: + ext, target = hole_targets.pop() + connect_two_way(entrance, ext, avail) + connect_entrance(drop, target, avail) + remove_from_list(entrances, [entrance, drop]) + remove_from_list(exits, [ext, target]) + + +def do_links_house(entrances, exits, avail, cross_world): + lh_exit = 'Inverted Links House Exit' if avail.inverted else 'Links House Exit' + if lh_exit in exits: + if not avail.world.shufflelinks[avail.player]: + links_house = 'Inverted Links House' if avail.inverted else 'Links House' + else: + forbidden = list(Isolated_LH_Doors_Inv + Inverted_Dark_Sanctuary_Doors + if avail.inverted else Isolated_LH_Doors_Open) + # simple shuffle - + if avail.world.shuffle[avail.player] == 'simple': + avail.links_on_mountain = True # taken care of by the logic below + if avail.inverted: # in inverted, links house cannot be on the mountain + forbidden.extend(['Spike Cave', 'Dark Death Mountain Fairy', 'Hookshot Fairy']) + else: + # links house cannot be on dm if there's no way off the mountain + ent = avail.world.get_entrance('Death Mountain Return Cave (West)', avail.player) + if ent.connected_region.name in Simple_DM_Non_Connectors: + forbidden.append('Hookshot Fairy') + # other cases it is fine + # can't have links house on eddm in restricted because Inverted Aga Tower isn't available + # todo: inverted full may have the same problem if both links house and a mandatory connector is chosen + # from the 3 inverted options + if avail.world.shuffle[avail.player] in ['restricted'] and avail.inverted: + avail.links_on_mountain = True + forbidden.extend(['Spike Cave', 'Dark Death Mountain Fairy']) + + # lobby shuffle means you ought to keep links house in the same world + sanc_spawn_can_be_dark = (not avail.inverted and avail.world.doorShuffle[avail.player] == 'crossed' + and avail.world.intensity[avail.player] >= 3) + entrance_pool = entrances if avail.coupled else avail.decoupled_entrances + if cross_world and not sanc_spawn_can_be_dark: + possible = [e for e in entrance_pool if e not in forbidden] + else: + world_list = LW_Entrances if not avail.inverted else DW_Entrances + possible = [e for e in entrance_pool if e in world_list and e not in forbidden] + possible.sort() + links_house = random.choice(possible) + connect_two_way(links_house, lh_exit, avail) + entrances.remove(links_house) + connect_exit('Chris Houlihan Room Exit', links_house, avail) # should match link's house + exits.remove(lh_exit) + exits.remove('Chris Houlihan Room Exit') + if not avail.coupled: + avail.decoupled_entrances.remove(links_house) + avail.decoupled_exits.remove('Links House Exit') + avail.decoupled_exits.remove('Chris Houlihan Room Exit') + # links on dm + dm_spots = LH_DM_Connector_List.union(LH_DM_Exit_Forbidden) + if links_house in dm_spots: + if avail.links_on_mountain: + return # connector is fine + multi_exit_caves = figure_out_connectors(exits) + entrance_pool = entrances if avail.coupled else avail.decoupled_entrances + if cross_world: + possible_dm_exits = [e for e in entrances if e in LH_DM_Connector_List] + possible_exits = [e for e in entrance_pool if e not in dm_spots] + else: + world_list = LW_Entrances if not avail.inverted else DW_Entrances + possible_dm_exits = [e for e in entrances if e in LH_DM_Connector_List and e in world_list] + possible_exits = [e for e in entrance_pool if e not in dm_spots and e in world_list] + chosen_cave = random.choice(multi_exit_caves) + shuffle_connector_exits(chosen_cave) + possible_dm_exits.sort() + possible_exits.sort() + chosen_dm_escape = random.choice(possible_dm_exits) + chosen_landing = random.choice(possible_exits) + if avail.coupled: + connect_two_way(chosen_dm_escape, chosen_cave.pop(0), avail) + connect_two_way(chosen_landing, chosen_cave.pop(), avail) + entrances.remove(chosen_dm_escape) + entrances.remove(chosen_landing) + else: + connect_entrance(chosen_dm_escape, chosen_cave.pop(0), avail) + connect_exit(chosen_cave.pop(), chosen_landing, avail) + entrances.remove(chosen_dm_escape) + avail.decoupled_entrances.remove(chosen_landing) + if len(chosen_cave): + exits.update([x for x in chosen_cave]) + exits.update([x for item in multi_exit_caves for x in item]) + + +def figure_out_connectors(exits): + multi_exit_caves = [] + for item in Connector_List: + if all(x in exits for x in item): + remove_from_list(exits, item) + multi_exit_caves.append(list(item)) + return multi_exit_caves + + +def figure_out_true_exits(exits, avail): + multi_exit_caves = [] + for item in Connector_List: + if all(x in exits for x in item): + remove_from_list(exits, item) + multi_exit_caves.append(list(item)) + for item in avail.default_map.values(): + if item in exits: + multi_exit_caves.append(item) + exits.remove(item) + return multi_exit_caves + + +# todo: figure out hyrule forced better +def figure_out_must_exits_same_world(entrances, exits, avail): + lw_entrances, dw_entrances = [], [] + hyrule_forced = None + check_for_hc = (avail.is_standard() or avail.world.doorShuffle[avail.player] != 'vanilla') + + for x in entrances: + lw_entrances.append(x) if x in LW_Entrances else dw_entrances.append(x) + multi_exit_caves = figure_out_connectors(exits) + if check_for_hc: + for option in multi_exit_caves: + if any(x in option for x in ['Hyrule Castle Exit (South)', 'Hyrule Castle Exit (East)', + 'Hyrule Castle Exit (West)']): + hyrule_forced = [option] + if hyrule_forced: + remove_from_list(multi_exit_caves, hyrule_forced) + + must_exit_lw, must_exit_dw = must_exits_helper(avail, lw_entrances, dw_entrances) + + return must_exit_lw, must_exit_dw, lw_entrances, dw_entrances, multi_exit_caves, hyrule_forced + + +def must_exits_helper(avail, lw_entrances, dw_entrances): + must_exit_lw = Inverted_LW_Must_Exit if avail.inverted else LW_Must_Exit + must_exit_dw = Inverted_DW_Must_Exit if avail.inverted else DW_Must_Exit + if not avail.inverted and not avail.skull_handled: + must_exit_dw.append(('Skull Woods Second Section Door (West)', 'Skull Woods Final Section')) + must_exit_lw = must_exit_filter(avail, must_exit_lw, lw_entrances) + must_exit_dw = must_exit_filter(avail, must_exit_dw, dw_entrances) + return must_exit_lw, must_exit_dw + + +def figure_out_must_exits_cross_world(entrances, exits, avail): + multi_exit_caves = figure_out_connectors(exits) + + must_exit_lw = Inverted_LW_Must_Exit if avail.inverted else LW_Must_Exit + must_exit_dw = Inverted_DW_Must_Exit if avail.inverted else DW_Must_Exit + if not avail.inverted and not avail.skull_handled: + must_exit_dw.append(('Skull Woods Second Section Door (West)', 'Skull Woods Final Section')) + must_exit = must_exit_filter(avail, must_exit_lw + must_exit_dw, entrances) + + return must_exit, multi_exit_caves + + +def do_same_world_connectors(lw_entrances, dw_entrances, caves, avail): + random.shuffle(lw_entrances) + random.shuffle(dw_entrances) + random.shuffle(caves) + while caves: + # connect highest exit count caves first, prevent issue where we have 2 or 3 exits across worlds left to fill + cave_candidate = (None, 0) + for i, cave in enumerate(caves): + if isinstance(cave, str): + cave = (cave,) + if len(cave) > cave_candidate[1]: + cave_candidate = (i, len(cave)) + cave = caves.pop(cave_candidate[0]) + + target = lw_entrances if random.randint(0, 1) == 0 else dw_entrances + if isinstance(cave, str): + cave = (cave,) + + # check if we can still fit the cave into our target group + if len(target) < len(cave): + # need to use other set + target = lw_entrances if target is dw_entrances else dw_entrances + + for ext in cave: + # todo: for decoupled, need to split the avail decoupled entrances into lw/dw + # if decoupled: + # choice = random.choice(avail.decoupled_entrances) + # connect_exit(ext, choice, avail) + # avail.decoupled_entrances.remove() + # else: + connect_two_way(target.pop(), ext, avail) + + +def do_cross_world_connectors(entrances, caves, avail): + random.shuffle(entrances) + random.shuffle(caves) + while caves: + cave_candidate = (None, 0) + for i, cave in enumerate(caves): + if isinstance(cave, str): + cave = (cave,) + if len(cave) > cave_candidate[1]: + cave_candidate = (i, len(cave)) + cave = caves.pop(cave_candidate[0]) + + if isinstance(cave, str): + cave = (cave,) + + for ext in cave: + if not avail.coupled: + choice = random.choice(avail.decoupled_entrances) + connect_exit(ext, choice, avail) + avail.decoupled_entrances.remove(choice) + else: + connect_two_way(entrances.pop(), ext, avail) + + +def do_fixed_shuffle(avail, entrance_list): + max_size = 0 + options = {} + for i, entrance_set in enumerate(entrance_list): + entrances, targets = find_entrances_and_exits(avail, entrance_set) + size = min(len(entrances), len(targets)) + max_size = max(max_size, size) + rules = Restrictions() + rules.size = size + if ('Hyrule Castle Entrance (South)' in entrances and + avail.world.doorShuffle[avail.player] in ['basic', 'crossed']): + rules.must_exit_to_lw = True + if 'Inverted Ganons Tower' in entrances and not avail.world.shuffle_ganon: + rules.fixed = True + option = (i, entrances, targets, rules) + options[i] = option + choices = dict(options) + for i, option in options.items(): + key, entrances, targets, rules = option + if rules.size and rules.size < max_size: + choice = choices[i] + elif rules.fixed: + choice = choices[i] + elif rules.must_exit_to_lw: + filtered_choices = {i: opt for i, opt in choices.items() if all(t in default_lw for t in opt[2])} + index, choice = random.choice(list(filtered_choices.items())) + else: + index, choice = random.choice(list(choices.items())) + del choices[choice[0]] + for t, entrance in enumerate(entrances): + target = choice[2][t] + connect_two_way(entrance, target, avail) + + +def do_same_world_shuffle(avail, pool_def): + single_exit = pool_def['entrances'] + multi_exit = pool_def['connectors'] + # complete_entrance_set = set() + lw_entrances, dw_entrances, multi_exits_caves, other_exits = [], [], [], [] + hyrule_forced = None + check_for_hc = avail.is_standard() or avail.world.doorShuffle[avail.player] != 'vanilla' + + single_entrances, single_exits = find_entrances_and_exits(avail, single_exit) + other_exits.extend(single_exits) + for x in single_entrances: + (dw_entrances, lw_entrances)[x in LW_Entrances].append(x) + # complete_entrance_set.update(single_entrances) + for option in multi_exit: + multi_entrances, multi_exits = find_entrances_and_exits(avail, option) + # complete_entrance_set.update(multi_entrances) + if check_for_hc and any(x in multi_entrances for x in ['Hyrule Castle Entrance (South)', + 'Hyrule Castle Entrance (East)', + 'Hyrule Castle Entrance (West)']): + hyrule_forced = [multi_exits] + else: + multi_exits_caves.append(multi_exits) + for x in multi_entrances: + (dw_entrances, lw_entrances)[x in LW_Entrances].append(x) + + must_exit_lw = Inverted_LW_Must_Exit if avail.inverted else LW_Must_Exit + must_exit_dw = Inverted_DW_Must_Exit if avail.inverted else DW_Must_Exit + must_exit_lw = must_exit_filter(avail, must_exit_lw, lw_entrances) + must_exit_dw = must_exit_filter(avail, must_exit_dw, dw_entrances) + + if hyrule_forced: + do_mandatory_connections(avail, lw_entrances, hyrule_forced, must_exit_lw) + else: + do_mandatory_connections(avail, lw_entrances, multi_exits_caves, must_exit_lw) + do_mandatory_connections(avail, dw_entrances, multi_exits_caves, must_exit_dw) + + # connect caves + random.shuffle(lw_entrances) + random.shuffle(dw_entrances) + random.shuffle(multi_exits_caves) + while multi_exits_caves: + cave_candidate = (None, 0) + for i, cave in enumerate(multi_exits_caves): + if len(cave) > cave_candidate[1]: + cave_candidate = (i, len(cave)) + cave = multi_exits_caves.pop(cave_candidate[0]) + + target = lw_entrances if random.randint(0, 1) == 0 else dw_entrances + if len(target) < len(cave): # swap because we ran out of entrances in that world + target = lw_entrances if target is dw_entrances else dw_entrances + + for ext in cave: + connect_two_way(target.pop(), ext, avail) + # finish the rest + connect_random(lw_entrances+dw_entrances, single_exits, avail, True) + + +def do_connector_shuffle(avail, pool_def): + directional_list = pool_def['directional_inv' if avail.inverted else 'directional'] + connector_list = pool_def['connectors_inv' if avail.inverted else 'connectors'] + option_list = pool_def['options'] + + for connector in directional_list: + chosen_option = random.choice(option_list) + ignored_ent, chosen_exits = find_entrances_and_exits(avail, chosen_option) + if not chosen_exits: + continue # nothing available + # this shuffle ensures directionality + shuffle_connector_exits(chosen_exits) + connector_ent, ignored_exits = find_entrances_and_exits(avail, connector) + for i, ent in enumerate(connector_ent): + connect_two_way(ent, chosen_exits[i], avail) + option_list.remove(chosen_option) + + for connector in connector_list: + chosen_option = random.choice(option_list) + ignored_ent, chosen_exits = find_entrances_and_exits(avail, chosen_option) + # directionality need not be preserved + random.shuffle(chosen_exits) + connector_ent, ignored_exits = find_entrances_and_exits(avail, connector) + for i, ent in enumerate(connector_ent): + connect_two_way(ent, chosen_exits[i], avail) + option_list.remove(chosen_option) + + +def do_limited_shuffle(pool_def, avail): + entrance_pool, ignored_exits = find_entrances_and_exits(avail, pool_def['entrances']) + exit_pool = [x for x in pool_def['options'] if x in avail.exits] + random.shuffle(exit_pool) + for entrance in entrance_pool: + chosen_exit = exit_pool.pop() + connect_two_way(entrance, chosen_exit, avail) + + +def do_limited_shuffle_exclude_drops(pool_def, avail, lw=True): + ignored_entrances, exits = find_entrances_and_exits(avail, pool_def['entrances']) + reserved_drops = set(linked_drop_map.values()) + must_exit_lw, must_exit_dw = must_exits_helper(avail, LW_Entrances, DW_Entrances) + must_exit = set(must_exit_lw if lw else must_exit_dw) + base_set = LW_Entrances if lw else DW_Entrances + entrance_pool = [x for x in base_set if x in avail.entrances and x not in reserved_drops] + random.shuffle(entrance_pool) + for next_exit in exits: + if next_exit not in Connector_Exit_Set: + reduced_pool = [x for x in entrance_pool if x not in must_exit] + chosen_entrance = reduced_pool.pop() + entrance_pool.remove(chosen_entrance) + else: + chosen_entrance = entrance_pool.pop() + connect_two_way(chosen_entrance, next_exit, avail) + + +def do_vanilla_connect(pool_def, avail): + if pool_def['condition'] == 'shopsanity': + if avail.world.shopsanity[avail.player]: + return + defaults = inverted_default_connections if avail.inverted else default_connections + for entrance in pool_def['entrances']: + if entrance in avail.entrances: + target = defaults[entrance] + connect_simple(avail.world, entrance, target, avail.player) + avail.entrances.remove(entrance) + avail.exits.remove(target) + + +def do_mandatory_connections(avail, entrances, cave_options, must_exit): + if len(must_exit) == 0: + return + if not avail.coupled: + do_mandatory_connections_decoupled(avail, cave_options, must_exit) + return + + # Keeps track of entrances that cannot be used to access each exit / cave + if avail.inverted: + invalid_connections = Inverted_Must_Exit_Invalid_Connections.copy() + else: + invalid_connections = Must_Exit_Invalid_Connections.copy() + invalid_cave_connections = defaultdict(set) + + if avail.world.logic[avail.player] in ['owglitches', 'nologic']: + import OverworldGlitchRules + for entrance in OverworldGlitchRules.get_non_mandatory_exits(avail.inverted): + invalid_connections[entrance] = set() + if entrance in must_exit: + must_exit.remove(entrance) + entrances.append(entrance) + entrances.sort() # sort these for consistency + random.shuffle(entrances) + random.shuffle(cave_options) + + if avail.inverted: + at = avail.world.get_region('Agahnims Tower Portal', avail.player) + for entrance in invalid_connections: + if avail.world.get_entrance(entrance, avail.player).connected_region == at: + for ext in invalid_connections[entrance]: + invalid_connections[ext] = invalid_connections[ext].union({'Inverted Ganons Tower', 'Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)'}) + break + + used_caves = [] + required_entrances = 0 # Number of entrances reserved for used_caves + while must_exit: + exit = must_exit.pop() + # find multi exit cave + candidates = [] + for candidate in cave_options: + if not isinstance(candidate, str) and (candidate in used_caves + or len(candidate) < len(entrances) - required_entrances): + candidates.append(candidate) + cave = random.choice(candidates) + if cave is None: + raise RuntimeError('No more caves left. Should not happen!') + + # all caves are sorted so that the last exit is always reachable + rnd_cave = list(cave) + shuffle_connector_exits(rnd_cave) # should be the same as unbiasing some entrances... + entrances.remove(exit) + connect_two_way(exit, rnd_cave[-1], avail) + if len(cave) == 2: + entrance = next(e for e in entrances[::-1] if e not in invalid_connections[exit] + and e not in invalid_cave_connections[tuple(cave)] and e not in must_exit) + entrances.remove(entrance) + connect_two_way(entrance, rnd_cave[0], avail) + if cave in used_caves: + required_entrances -= 2 + used_caves.remove(cave) + if entrance in invalid_connections: + for exit2 in invalid_connections[entrance]: + invalid_connections[exit2] = invalid_connections[exit2].union(invalid_connections[exit]).union(invalid_cave_connections[tuple(cave)]) + elif cave[-1] == 'Spectacle Rock Cave Exit': # Spectacle rock only has one exit + cave_entrances = [] + for cave_exit in rnd_cave[:-1]: + entrance = next(e for e in entrances[::-1] if e not in invalid_connections[exit] and e not in must_exit) + cave_entrances.append(entrance) + entrances.remove(entrance) + connect_two_way(entrance, cave_exit, avail) + if entrance not in invalid_connections: + invalid_connections[exit] = set() + if all(entrance in invalid_connections for entrance in cave_entrances): + new_invalid_connections = invalid_connections[cave_entrances[0]].intersection(invalid_connections[cave_entrances[1]]) + for exit2 in new_invalid_connections: + invalid_connections[exit2] = invalid_connections[exit2].union(invalid_connections[exit]) + else: # save for later so we can connect to multiple exits + if cave in used_caves: + required_entrances -= 1 + used_caves.remove(cave) + else: + required_entrances += len(cave)-1 + cave_options.append(rnd_cave[0:-1]) + random.shuffle(cave_options) + used_caves.append(rnd_cave[0:-1]) + invalid_cave_connections[tuple(rnd_cave[0:-1])] = invalid_cave_connections[tuple(cave)].union(invalid_connections[exit]) + cave_options.remove(cave) + for cave in used_caves: + if cave in cave_options: # check if we placed multiple entrances from this 3 or 4 exit + for cave_exit in cave: + entrance = next(e for e in entrances[::-1] if e not in invalid_cave_connections[tuple(cave)]) + invalid_cave_connections[tuple(cave)] = set() + entrances.remove(entrance) + connect_two_way(entrance, cave_exit, avail) + cave_options.remove(cave) + + +def do_mandatory_connections_decoupled(avail, cave_options, must_exit): + for next_entrance in must_exit: + random.shuffle(cave_options) + candidate = None + for cave in cave_options: + if len(cave) < 2 or (len(cave) == 2 and ('Spectacle Rock Cave Exit (Peak)' in cave + or 'Turtle Rock Ledge Exit (East)' in cave)): + continue + candidate = cave + break + if candidate is None: + raise RuntimeError('No suitable cave.') + cave_options.remove(candidate) + + # all caves are sorted so that the last exit is always reachable + shuffle_connector_exits(candidate) # should be the same as un-biasing some entrances... + chosen_exit = candidate[-1] + cave = candidate[:-1] + connect_exit(chosen_exit, next_entrance, avail) + cave_options.append(cave) + avail.decoupled_entrances.remove(next_entrance) + + +def must_exit_filter(avail, candidates, shuffle_pool): + filtered_list = [] + for cand in candidates: + if isinstance(cand, tuple): + candidates = [x for x in cand if x in avail.entrances and x in shuffle_pool] + if len(candidates) > 1: + filtered_list.append(random.choice(candidates)) + elif len(candidates) == 1: + filtered_list.append(candidates[0]) + elif cand in avail.entrances and cand in shuffle_pool: + filtered_list.append(cand) + return filtered_list + + +def shuffle_connector_exits(connector_choices): + random.shuffle(connector_choices) + # the order matter however, because we assume the last choice is exit-able from the other ways to get in + # the first one is the one where you can assume you access the entire cave from + if 'Paradox Cave Exit (Bottom)' == connector_choices[0]: # Paradox bottom is exit only + i = random.randint(1, len(connector_choices) - 1) + connector_choices[0], connector_choices[i] = connector_choices[i], connector_choices[0] + # east ledge can't fulfill a must_exit condition + if 'Turtle Rock Ledge Exit (East)' in connector_choices and 'Turtle Rock Ledge Exit (East)' != connector_choices[0]: + i = connector_choices.index('Turtle Rock Ledge Exit (East)') + connector_choices[0], connector_choices[i] = connector_choices[i], connector_choices[0] + # these only have one exit (one-way nature) + if 'Spectacle Rock Cave Exit' in connector_choices and connector_choices[-1] != 'Spectacle Rock Cave Exit': + i = connector_choices.index('Spectacle Rock Cave Exit') + connector_choices[-1], connector_choices[i] = connector_choices[i], connector_choices[-1] + if 'Superbunny Cave Exit (Top)' in connector_choices and connector_choices[-1] != 'Superbunny Cave Exit (Top)': + connector_choices[-1], connector_choices[0] = connector_choices[0], connector_choices[-1] + if 'Spiral Cave Exit' in connector_choices and connector_choices[-1] != 'Spiral Cave Exit': + connector_choices[-1], connector_choices[0] = connector_choices[0], connector_choices[-1] + + +def find_entrances_and_targets_drops(avail_pool, drop_pool): + holes, targets = [], [] + inverted_substitution(avail_pool, drop_pool, True) + for item in drop_pool: + if item in avail_pool.entrances: + holes.append(item) + if drop_map[item] in avail_pool.exits: + targets.append(drop_map[item]) + return holes, targets + + +def find_entrances_and_exits(avail_pool, entrance_pool): + entrances, targets = [], [] + inverted_substitution(avail_pool, entrance_pool, True) + for item in entrance_pool: + if item in avail_pool.entrances: + entrances.append(item) + if 'Links House' in item: + targets.append('Chris Houlihan Room Exit') + if item in entrance_map and entrance_map[item] in avail_pool.exits: + targets.append(entrance_map[item]) + elif item in single_entrance_map and single_entrance_map[item] in avail_pool.exits: + targets.append(single_entrance_map[item]) + return entrances, targets + + +inverted_sub_table = { + 'Ganons Tower': 'Inverted Agahnims Tower', + 'Agahnims Tower': 'Inverted Ganons Tower', + 'Dark Sanctuary Hint': 'Inverted Dark Sanctuary', + 'Big Bomb Shop': 'Inverted Links House', + 'Links House': 'Inverted Big Bomb Shop', + 'Pyramid Hole': 'Inverted Pyramid Hole', + 'Pyramid Entrance': 'Inverted Pyramid Entrance' +} + +inverted_exit_sub_table = { + 'Ganons Tower Exit': 'Inverted Agahnims Tower Exit', + 'Agahnims Tower Exit': 'Inverted Ganons Tower Exit', + 'Dark Sanctuary Hint': 'Inverted Dark Sanctuary Exit', + 'Big Bomb Shop': 'Inverted Links House Exit', + 'Links House Exit': 'Inverted Big Bomb Shop', +} + + +def inverted_substitution(avail_pool, collection, is_entrance, is_set=False): + if avail_pool.inverted: + sub_table = inverted_sub_table if is_entrance else inverted_exit_sub_table + for area, sub in sub_table.items(): + if is_set: + if area in collection: + collection.remove(area) + collection.add(sub) + else: + try: + idx = collection.index(area) + collection[idx] = sub + except ValueError: + pass + + +def connect_random(exitlist, targetlist, avail, two_way=False): + targetlist = list(targetlist) + random.shuffle(targetlist) + + for exit, target in zip(exitlist, targetlist): + if two_way: + connect_two_way(exit, target, avail) + else: + connect_entrance(exit, target, avail) + + +def connect_custom(avail_pool, world, player): + if world.customizer and world.customizer.get_entrances(): + custom_entrances = world.customizer.get_entrances() + player_key = player + for ent_name, exit_name in custom_entrances[player_key]['two-way'].items(): + connect_two_way(ent_name, exit_name, avail_pool) + for ent_name, exit_name in custom_entrances[player_key]['entrances'].items(): + connect_entrance(ent_name, exit_name, avail_pool) + for ent_name, exit_name in custom_entrances[player_key]['exits'].items(): + connect_exit(exit_name, ent_name, avail_pool) + + +def connect_simple(world, exit_name, region_name, player): + world.get_entrance(exit_name, player).connect(world.get_region(region_name, player)) + + +def connect_vanilla(exit_name, region_name, avail): + world, player = avail.world, avail.player + world.get_entrance(exit_name, player).connect(world.get_region(region_name, player)) + avail.entrances.remove(exit_name) + avail.exits.remove(region_name) + + +def connect_vanilla_two_way(entrancename, exit_name, avail): + world, player = avail.world, avail.player + + entrance = world.get_entrance(entrancename, player) + exit = world.get_entrance(exit_name, player) + + # if these were already connected somewhere, remove the backreference + if entrance.connected_region is not None: + entrance.connected_region.entrances.remove(entrance) + if exit.connected_region is not None: + exit.connected_region.entrances.remove(exit) + + entrance.connect(exit.parent_region) + exit.connect(entrance.parent_region) + avail.entrances.remove(entrancename) + avail.exits.remove(exit_name) + + +def connect_entrance(entrancename, exit_name, avail): + world, player = avail.world, avail. player + entrance = world.get_entrance(entrancename, player) + # check if we got an entrance or a region to connect to + try: + region = world.get_region(exit_name, player) + exit = None + except RuntimeError: + exit = world.get_entrance(exit_name, player) + region = exit.parent_region + + # if this was already connected somewhere, remove the backreference + if entrance.connected_region is not None: + entrance.connected_region.entrances.remove(entrance) + + target = exit_ids[exit.name][0] if exit is not None else exit_ids.get(region.name, None) + addresses = door_addresses[entrance.name][0] + + entrance.connect(region, addresses, target) + if entrancename != 'Tavern North': + avail.entrances.remove(entrancename) + if avail.coupled: + if exit_name == 'Inverted Dark Sanctuary': + avail.exits.remove('Inverted Dark Sanctuary Exit') + else: + avail.exits.remove(exit_name) + world.spoiler.set_entrance(entrance.name, exit.name if exit is not None else region.name, 'entrance', player) + logging.getLogger('').debug(f'Connected (entr) {entrance.name} to {exit.name if exit is not None else region.name}') + + +def connect_exit(exit_name, entrancename, avail): + world, player = avail.world, avail. player + entrance = world.get_entrance(entrancename, player) + exit = world.get_entrance(exit_name, player) + + # if this was already connected somewhere, remove the backreference + if exit.connected_region is not None: + exit.connected_region.entrances.remove(exit) + + exit.connect(entrance.parent_region, door_addresses[entrance.name][1], exit_ids[exit.name][1]) + if exit_name != 'Chris Houlihan Room Exit' and avail.coupled: + avail.entrances.remove(entrancename) + avail.exits.remove(exit_name) + world.spoiler.set_entrance(entrance.name, exit.name, 'exit', player) + logging.getLogger('').debug(f'Connected (exit) {entrance.name} to {exit.name}') + + +def connect_two_way(entrancename, exit_name, avail): + world, player = avail.world, avail.player + + entrance = world.get_entrance(entrancename, player) + exit = world.get_entrance(exit_name, player) + + # if these were already connected somewhere, remove the backreference + if entrance.connected_region is not None: + entrance.connected_region.entrances.remove(entrance) + if exit.connected_region is not None: + exit.connected_region.entrances.remove(exit) + + entrance.connect(exit.parent_region, door_addresses[entrance.name][0], exit_ids[exit.name][0]) + exit.connect(entrance.parent_region, door_addresses[entrance.name][1], exit_ids[exit.name][1]) + avail.entrances.remove(entrancename) + avail.exits.remove(exit_name) + world.spoiler.set_entrance(entrance.name, exit.name, 'both', player) + logging.getLogger('').debug(f'Connected (2-way) {entrance.name} to {exit.name}') + + +modes = { + 'dungeonssimple': { + 'undefined': 'vanilla', + 'pools': { + 'skull_drops': { + 'special': 'drops', + 'entrances': ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', + 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'] + }, + 'skull_doors': { + 'special': 'skull', + 'entrances': ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', + 'Skull Woods Second Section Door (West)'] + }, + 'single_entrance_dungeon': { + 'entrances': ['Eastern Palace', 'Tower of Hera', 'Thieves Town', 'Skull Woods Final Section', + 'Palace of Darkness', 'Ice Palace', 'Misery Mire', 'Swamp Palace', 'Ganons Tower'] + }, + 'multi_entrance_dungeon': { + 'special': 'fixed_shuffle', + 'entrances': [['Hyrule Castle Entrance (South)', 'Hyrule Castle Entrance (East)', + 'Hyrule Castle Entrance (West)', 'Agahnims Tower'], + ['Desert Palace Entrance (South)', 'Desert Palace Entrance (East)', + 'Desert Palace Entrance (West)', 'Desert Palace Entrance (North)'], + ['Turtle Rock', 'Turtle Rock Isolated Ledge Entrance', + 'Dark Death Mountain Ledge (West)', 'Dark Death Mountain Ledge (East)']] + }, + } + }, + 'dungeonsfull': { + 'undefined': 'vanilla', + 'pools': { + 'skull_drops': { + 'special': 'drops', + 'entrances': ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', + 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'] + }, + 'skull_doors': { + 'special': 'skull', + 'entrances': ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', + 'Skull Woods Second Section Door (West)'] + }, + 'dungeon': { + 'special': 'same_world', + 'sanc_flag': 'light_world', # always light world flag + 'entrances': ['Eastern Palace', 'Tower of Hera', 'Thieves Town', 'Skull Woods Final Section', + 'Agahnims Tower', 'Palace of Darkness', 'Ice Palace', 'Misery Mire', 'Swamp Palace', + 'Ganons Tower'], + 'connectors': [['Hyrule Castle Entrance (South)', 'Hyrule Castle Entrance (East)', + 'Hyrule Castle Entrance (West)'], + ['Desert Palace Entrance (South)', 'Desert Palace Entrance (East)', + 'Desert Palace Entrance (West)', 'Desert Palace Entrance (North)'], + ['Turtle Rock', 'Turtle Rock Isolated Ledge Entrance', + 'Dark Death Mountain Ledge (West)', 'Dark Death Mountain Ledge (East)']] + }, + } + }, + 'lite': { + 'undefined': 'shuffle', + 'keep_drops_together': 'on', + 'cross_world': 'off', + 'pools': { + 'skull_drops': { + 'special': 'drops', + 'entrances': ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', + 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'] + }, + 'skull_doors': { + 'special': 'skull', + 'entrances': ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', + 'Skull Woods Second Section Door (West)'] + }, + 'fixed_non_items': { + 'special': 'vanilla', + 'condition': '', + 'entrances': ['Dark Death Mountain Fairy', 'Dark Desert Fairy', 'Dark Desert Hint', 'Archery Game', + 'Fortune Teller (Dark)', 'Dark Sanctuary Hint', 'Bonk Fairy (Dark)', + 'Dark Lake Hylia Ledge Hint', 'Dark Lake Hylia Ledge Spike Cave', + 'Dark Lake Hylia Ledge Fairy', 'Dark Lake Hylia Fairy', 'Dark Lake Hylia Shop', + 'Palace of Darkness Hint', 'East Dark World Hint', 'Hookshot Fairy', '50 Rupee Cave', + 'Kakariko Gamble Game', '20 Rupee Cave', 'Good Bee Cave', 'Long Fairy Cave', + 'Light World Bomb Hut', 'Tavern (Front)', 'Bush Covered House', 'Snitch Lady (West)', + 'Snitch Lady (East)', 'Fortune Teller (Light)', 'Lost Woods Gamble', 'Desert Fairy', + 'Light Hype Fairy', 'Lake Hylia Fortune Teller', 'Lake Hylia Fairy', 'Bonk Fairy (Light)', + 'Lumberjack House', 'Inverted Dark Sanctuary'], + }, + 'fixed_shops': { + 'special': 'vanilla', + 'condition': 'shopsanity', + 'entrances': ['Cave Shop (Dark Death Mountain)', 'Dark World Potion Shop', 'Dark World Lumberjack Shop', + 'Dark World Shop', 'Red Shield Shop', 'Kakariko Shop', 'Capacity Upgrade', + 'Cave Shop (Lake Hylia)'], + }, + 'item_caves': { # shuffles shops if they weren't fixed in the last one + 'entrances': ['Mimic Cave', 'Spike Cave', 'Mire Shed', 'Dark World Hammer Peg Cave', 'Chest Game', + 'C-Shaped House', 'Brewery', 'Hype Cave', 'Big Bomb Shop', 'Pyramid Fairy', + 'Ice Rod Cave', 'Dam', 'Bonk Rock Cave', 'Library', 'Potion Shop', 'Mini Moldorm Cave', + 'Checkerboard Cave', 'Graveyard Cave', 'Cave 45', 'Sick Kids House', 'Blacksmiths Hut', + 'Sahasrahlas Hut', 'Aginahs Cave', 'Chicken House', 'Kings Grave', 'Blinds Hideout', + 'Waterfall of Wishing', 'Inverted Bomb Shop', 'Cave Shop (Dark Death Mountain)', + 'Dark World Potion Shop', 'Dark World Lumberjack Shop', 'Dark World Shop', + 'Red Shield Shop', 'Kakariko Shop', 'Capacity Upgrade', 'Cave Shop (Lake Hylia)', + 'Links House', 'Inverted Links House'] + }, + 'old_man_cave': { # have to do old man cave first so lw dungeon don't use up everything + 'special': 'old_man_cave_east', + 'entrances': ['Old Man Cave Exit (East)'], + }, + 'lw_dungeons': { + 'special': 'limited_lw', + 'entrances': ['Hyrule Castle Entrance (South)', 'Hyrule Castle Entrance (East)', + 'Hyrule Castle Entrance (West)', 'Agahnims Tower', 'Eastern Palace', 'Tower of Hera', + 'Desert Palace Entrance (South)', 'Desert Palace Entrance (East)', + 'Desert Palace Entrance (West)', 'Desert Palace Entrance (North)'], + }, + 'dw_dungeons': { + 'special': 'limited_dw', + 'entrances': ['Ice Palace', 'Misery Mire', 'Ganons Tower', 'Turtle Rock', + 'Turtle Rock Isolated Ledge Entrance', + 'Dark Death Mountain Ledge (West)', 'Dark Death Mountain Ledge (East)'], + }, + } + }, + 'lean': { + 'undefined': 'shuffle', + 'keep_drops_together': 'on', + 'cross_world': 'on', + 'pools': { + 'skull_drops': { + 'special': 'drops', + 'entrances': ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', + 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'] + }, + 'skull_doors': { + 'special': 'skull', + 'entrances': ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', + 'Skull Woods Second Section Door (West)'] + }, + 'fixed_non_items': { + 'special': 'vanilla', + 'condition': '', + 'entrances': ['Dark Death Mountain Fairy', 'Dark Desert Fairy', 'Dark Desert Hint', 'Archery Game', + 'Fortune Teller (Dark)', 'Dark Sanctuary Hint', 'Bonk Fairy (Dark)', + 'Dark Lake Hylia Ledge Hint', 'Dark Lake Hylia Ledge Spike Cave', + 'Dark Lake Hylia Ledge Fairy', 'Dark Lake Hylia Fairy', 'Dark Lake Hylia Shop', + 'Palace of Darkness Hint', 'East Dark World Hint', 'Hookshot Fairy', '50 Rupee Cave', + 'Kakariko Gamble Game', '20 Rupee Cave', 'Good Bee Cave', 'Long Fairy Cave', + 'Light World Bomb Hut', 'Tavern (Front)', 'Bush Covered House', 'Snitch Lady (West)', + 'Snitch Lady (East)', 'Fortune Teller (Light)', 'Lost Woods Gamble', 'Desert Fairy', + 'Light Hype Fairy', 'Lake Hylia Fortune Teller', 'Lake Hylia Fairy', 'Bonk Fairy (Light)', + 'Lumberjack House', 'Inverted Dark Sanctuary'], + }, + 'fixed_shops': { + 'special': 'vanilla', + 'condition': 'shopsanity', + 'entrances': ['Cave Shop (Dark Death Mountain)', 'Dark World Potion Shop', 'Dark World Lumberjack Shop', + 'Dark World Shop', 'Red Shield Shop', 'Kakariko Shop', 'Capacity Upgrade', + 'Cave Shop (Lake Hylia)'], + }, + 'item_caves': { # shuffles shops if they weren't fixed in the last one + 'entrances': ['Mimic Cave', 'Spike Cave', 'Mire Shed', 'Dark World Hammer Peg Cave', 'Chest Game', + 'C-Shaped House', 'Brewery', 'Hype Cave', 'Big Bomb Shop', 'Pyramid Fairy', + 'Ice Rod Cave', 'Dam', 'Bonk Rock Cave', 'Library', 'Potion Shop', 'Mini Moldorm Cave', + 'Checkerboard Cave', 'Graveyard Cave', 'Cave 45', 'Sick Kids House', 'Blacksmiths Hut', + 'Sahasrahlas Hut', 'Aginahs Cave', 'Chicken House', 'Kings Grave', 'Blinds Hideout', + 'Waterfall of Wishing', 'Inverted Bomb Shop', 'Cave Shop (Dark Death Mountain)', + 'Dark World Potion Shop', 'Dark World Lumberjack Shop', 'Dark World Shop', + 'Red Shield Shop', 'Kakariko Shop', 'Capacity Upgrade', 'Cave Shop (Lake Hylia)', + 'Links House', 'Inverted Links House'] + } + } + }, + 'simple': { + 'undefined': 'shuffle', + 'keep_drops_together': 'on', + 'cross_world': 'off', + 'pools': { + 'skull_drops': { + 'special': 'drops', + 'entrances': ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', + 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'] + }, + 'skull_doors': { + 'special': 'skull', + 'entrances': ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', + 'Skull Woods Second Section Door (West)'] + }, + 'single_entrance_dungeon': { + 'entrances': ['Eastern Palace', 'Tower of Hera', 'Thieves Town', 'Skull Woods Final Section', + 'Palace of Darkness', 'Ice Palace', 'Misery Mire', 'Swamp Palace', 'Ganons Tower'] + }, + 'multi_entrance_dungeon': { + 'special': 'fixed_shuffle', + 'entrances': [['Hyrule Castle Entrance (South)', 'Hyrule Castle Entrance (East)', + 'Hyrule Castle Entrance (West)', 'Agahnims Tower'], + ['Desert Palace Entrance (South)', 'Desert Palace Entrance (East)', + 'Desert Palace Entrance (West)', 'Desert Palace Entrance (North)'], + ['Turtle Rock', 'Turtle Rock Isolated Ledge Entrance', + 'Dark Death Mountain Ledge (West)', 'Dark Death Mountain Ledge (East)']] + }, + 'two_way_entrances': { + 'special': 'simple_connector', + 'directional': [ + ['Bumper Cave (Bottom)', 'Bumper Cave (Top)'], + ['Hookshot Cave', 'Hookshot Cave Back Entrance'], + ], + 'connectors': [ + ['Elder House (East)', 'Elder House (West)'], + ['Two Brothers House (East)', 'Two Brothers House (West)'], + ['Superbunny Cave (Bottom)', 'Superbunny Cave (Top)'] + ], + 'directional_inv': [ + ['Old Man Cave (West)', 'Death Mountain Return Cave (West)'], + ['Two Brothers House (East)', 'Two Brothers House (West)'], + ], + 'connectors_inv': [ + ['Elder House (East)', 'Elder House (West)'], + ['Superbunny Cave (Bottom)', 'Superbunny Cave (Top)'], + ['Hookshot Cave', 'Hookshot Cave Back Entrance'] + ], + 'options': [ + ['Bumper Cave (Bottom)', 'Bumper Cave (Top)'], + ['Hookshot Cave', 'Hookshot Cave Back Entrance'], + ['Elder House (East)', 'Elder House (West)'], + ['Two Brothers House (East)', 'Two Brothers House (West)'], + ['Superbunny Cave (Bottom)', 'Superbunny Cave (Top)'], + ['Death Mountain Return Cave (West)', 'Death Mountain Return Cave (East)'], + ['Fairy Ascension Cave (Bottom)', 'Fairy Ascension Cave (Top)'], + ['Spiral Cave (Bottom)', 'Spiral Cave'] + ] + }, + 'old_man_cave': { + 'special': 'old_man_cave_east', + 'entrances': ['Old Man Cave Exit (East)'], + }, + 'old_man_cave_inverted': { + 'special': 'inverted_fixed', + 'entrance': 'Bumper Cave (Bottom)', + 'exit': 'Old Man Cave Exit (West)' + }, + 'light_death_mountain': { + 'special': 'limited', + 'entrances': ['Old Man Cave (West)', 'Old Man Cave (East)', 'Old Man House (Bottom)', + 'Old Man House (Top)', 'Death Mountain Return Cave (East)', + 'Death Mountain Return Cave (West)', 'Fairy Ascension Cave (Bottom)', + 'Fairy Ascension Cave (Top)', 'Spiral Cave', 'Spiral Cave (Bottom)', + 'Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Bottom)', 'Spectacle Rock Cave', + 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'Paradox Cave (Top)'], + 'options': ['Elder House Exit (East)', 'Elder House Exit (West)', 'Two Brothers House Exit (East)', + 'Two Brothers House Exit (West)', 'Old Man Cave Exit (West)', 'Old Man House Exit (Bottom)', + 'Old Man House Exit (Top)', 'Death Mountain Return Cave Exit (East)', + 'Death Mountain Return Cave Exit (West)', 'Fairy Ascension Cave Exit (Bottom)', + 'Fairy Ascension Cave Exit (Top)', 'Spiral Cave Exit (Top)', 'Spiral Cave Exit', + 'Bumper Cave Exit (Bottom)', 'Bumper Cave Exit (Top)', 'Hookshot Cave Front Exit', + 'Hookshot Cave Back Exit', 'Superbunny Cave Exit (Top)', 'Superbunny Cave Exit (Bottom)', + 'Spectacle Rock Cave Exit (Peak)', 'Spectacle Rock Cave Exit', + 'Spectacle Rock Cave Exit (Top)', 'Paradox Cave Exit (Bottom)', + 'Paradox Cave Exit (Middle)', 'Paradox Cave Exit (Top)'] + } + } + }, + 'restricted': { + 'undefined': 'shuffle', + 'keep_drops_together': 'on', + 'cross_world': 'off', + 'pools': { + 'skull_drops': { + 'special': 'drops', + 'entrances': ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', + 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'] + }, + 'skull_doors': { + 'special': 'skull', + 'entrances': ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', + 'Skull Woods Second Section Door (West)'] + }, + 'single_entrance_dungeon': { + 'entrances': ['Eastern Palace', 'Tower of Hera', 'Thieves Town', 'Skull Woods Final Section', + 'Palace of Darkness', 'Ice Palace', 'Misery Mire', 'Swamp Palace', 'Ganons Tower'] + }, + 'multi_entrance_dungeon': { + 'special': 'fixed_shuffle', + 'entrances': [['Hyrule Castle Entrance (South)', 'Hyrule Castle Entrance (East)', + 'Hyrule Castle Entrance (West)', 'Agahnims Tower'], + ['Desert Palace Entrance (South)', 'Desert Palace Entrance (East)', + 'Desert Palace Entrance (West)', 'Desert Palace Entrance (North)'], + ['Turtle Rock', 'Turtle Rock Isolated Ledge Entrance', + 'Dark Death Mountain Ledge (West)', 'Dark Death Mountain Ledge (East)']] + }, + } + }, + 'full': { + 'undefined': 'shuffle', + 'keep_drops_together': 'on', + 'cross_world': 'off', + 'pools': { + 'skull_drops': { + 'special': 'drops', + 'entrances': ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', + 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'] + }, + 'skull_doors': { + 'special': 'skull', + 'entrances': ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', + 'Skull Woods Second Section Door (West)'] + }, + } + }, + 'crossed': { + 'undefined': 'shuffle', + 'keep_drops_together': 'on', + 'cross_world': 'on', + 'pools': { + 'skull_drops': { + 'special': 'drops', + 'entrances': ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', + 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'] + }, + 'skull_doors': { + 'special': 'skull', + 'entrances': ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', + 'Skull Woods Second Section Door (West)'] + }, + } + }, + 'insanity': { + 'undefined': 'shuffle', + 'keep_drops_together': 'off', + 'cross_world': 'on', + 'decoupled': 'on', + 'pools': {} + } +} + +drop_map = { + 'Skull Woods First Section Hole (East)': 'Skull Pinball', + 'Skull Woods First Section Hole (West)': 'Skull Left Drop', + 'Skull Woods First Section Hole (North)': 'Skull Pot Circle', + 'Skull Woods Second Section Hole': 'Skull Back Drop', + + 'Hyrule Castle Secret Entrance Drop': 'Hyrule Castle Secret Entrance', + 'Kakariko Well Drop': 'Kakariko Well (top)', + 'Bat Cave Drop': 'Bat Cave (right)', + 'North Fairy Cave Drop': 'North Fairy Cave', + 'Lost Woods Hideout Drop': 'Lost Woods Hideout (top)', + 'Lumberjack Tree Tree': 'Lumberjack Tree (top)', + 'Sanctuary Grave': 'Sewer Drop', + 'Pyramid Hole': 'Pyramid', + 'Inverted Pyramid Hole': 'Pyramid' +} + +linked_drop_map = { + 'Hyrule Castle Secret Entrance Drop': 'Hyrule Castle Secret Entrance Stairs', + 'Kakariko Well Drop': 'Kakariko Well Cave', + 'Bat Cave Drop': 'Bat Cave Cave', + 'North Fairy Cave Drop': 'North Fairy Cave', + 'Lost Woods Hideout Drop': 'Lost Woods Hideout Stump', + 'Lumberjack Tree Tree': 'Lumberjack Tree Cave', + 'Sanctuary Grave': 'Sanctuary', + 'Pyramid Hole': 'Pyramid Entrance', + 'Inverted Pyramid Hole': 'Inverted Pyramid Entrance' +} + +entrance_map = { + 'Desert Palace Entrance (South)': 'Desert Palace Exit (South)', + 'Desert Palace Entrance (West)': 'Desert Palace Exit (West)', + 'Desert Palace Entrance (North)': 'Desert Palace Exit (North)', + 'Desert Palace Entrance (East)': 'Desert Palace Exit (East)', + + 'Eastern Palace': 'Eastern Palace Exit', + 'Tower of Hera': 'Tower of Hera Exit', + + 'Hyrule Castle Entrance (South)': 'Hyrule Castle Exit (South)', + 'Hyrule Castle Entrance (West)': 'Hyrule Castle Exit (West)', + 'Hyrule Castle Entrance (East)': 'Hyrule Castle Exit (East)', + 'Agahnims Tower': 'Agahnims Tower Exit', + 'Inverted Agahnims Tower': 'Inverted Agahnims Tower Exit', + + 'Thieves Town': 'Thieves Town Exit', + 'Skull Woods First Section Door': 'Skull Woods First Section Exit', + 'Skull Woods Second Section Door (East)': 'Skull Woods Second Section Exit (East)', + 'Skull Woods Second Section Door (West)': 'Skull Woods Second Section Exit (West)', + 'Skull Woods Final Section': 'Skull Woods Final Section Exit', + 'Ice Palace': 'Ice Palace Exit', + 'Misery Mire': 'Misery Mire Exit', + 'Palace of Darkness': 'Palace of Darkness Exit', + 'Swamp Palace': 'Swamp Palace Exit', + + 'Turtle Rock': 'Turtle Rock Exit (Front)', + 'Dark Death Mountain Ledge (West)': 'Turtle Rock Ledge Exit (West)', + 'Dark Death Mountain Ledge (East)': 'Turtle Rock Ledge Exit (East)', + 'Turtle Rock Isolated Ledge Entrance': 'Turtle Rock Isolated Ledge Exit', + 'Ganons Tower': 'Ganons Tower Exit', + 'Inverted Ganons Tower': 'Inverted Ganons Tower Exit', + + 'Links House': 'Links House Exit', + 'Inverted Links House': 'Inverted Links House Exit', + + + 'Hyrule Castle Secret Entrance Stairs': 'Hyrule Castle Secret Entrance Exit', + 'Kakariko Well Cave': 'Kakariko Well Exit', + 'Bat Cave Cave': 'Bat Cave Exit', + 'North Fairy Cave': 'North Fairy Cave Exit', + 'Lost Woods Hideout Stump': 'Lost Woods Hideout Exit', + 'Lumberjack Tree Cave': 'Lumberjack Tree Exit', + 'Sanctuary': 'Sanctuary Exit', + 'Pyramid Entrance': 'Pyramid Exit', + 'Inverted Pyramid Entrance': 'Pyramid Exit', + + 'Elder House (East)': 'Elder House Exit (East)', + 'Elder House (West)': 'Elder House Exit (West)', + 'Two Brothers House (East)': 'Two Brothers House Exit (East)', + 'Two Brothers House (West)': 'Two Brothers House Exit (West)', + 'Old Man Cave (West)': 'Old Man Cave Exit (West)', + 'Old Man Cave (East)': 'Old Man Cave Exit (East)', + 'Old Man House (Bottom)': 'Old Man House Exit (Bottom)', + 'Old Man House (Top)': 'Old Man House Exit (Top)', + 'Death Mountain Return Cave (East)': 'Death Mountain Return Cave Exit (East)', + 'Death Mountain Return Cave (West)': 'Death Mountain Return Cave Exit (West)', + 'Fairy Ascension Cave (Bottom)': 'Fairy Ascension Cave Exit (Bottom)', + 'Fairy Ascension Cave (Top)': 'Fairy Ascension Cave Exit (Top)', + 'Spiral Cave': 'Spiral Cave Exit (Top)', + 'Spiral Cave (Bottom)': 'Spiral Cave Exit', + 'Bumper Cave (Bottom)': 'Bumper Cave Exit (Bottom)', + 'Bumper Cave (Top)': 'Bumper Cave Exit (Top)', + 'Hookshot Cave': 'Hookshot Cave Front Exit', + 'Hookshot Cave Back Entrance': 'Hookshot Cave Back Exit', + 'Superbunny Cave (Top)': 'Superbunny Cave Exit (Top)', + 'Superbunny Cave (Bottom)': 'Superbunny Cave Exit (Bottom)', + + 'Spectacle Rock Cave Peak': 'Spectacle Rock Cave Exit (Peak)', + 'Spectacle Rock Cave (Bottom)': 'Spectacle Rock Cave Exit', + 'Spectacle Rock Cave': 'Spectacle Rock Cave Exit (Top)', + 'Paradox Cave (Bottom)': 'Paradox Cave Exit (Bottom)', + 'Paradox Cave (Middle)': 'Paradox Cave Exit (Middle)', + 'Paradox Cave (Top)': 'Paradox Cave Exit (Top)', +} + + +single_entrance_map = { + 'Mimic Cave': 'Mimic Cave', 'Dark Death Mountain Fairy': 'Dark Death Mountain Healer Fairy', + 'Cave Shop (Dark Death Mountain)': 'Cave Shop (Dark Death Mountain)', 'Spike Cave': 'Spike Cave', + 'Dark Desert Fairy': 'Dark Desert Healer Fairy', 'Dark Desert Hint': 'Dark Desert Hint', 'Mire Shed': 'Mire Shed', + 'Archery Game': 'Archery Game', 'Dark World Potion Shop': 'Dark World Potion Shop', + 'Dark World Lumberjack Shop': 'Dark World Lumberjack Shop', 'Dark World Shop': 'Village of Outcasts Shop', + 'Fortune Teller (Dark)': 'Fortune Teller (Dark)', 'Dark Sanctuary Hint': 'Dark Sanctuary Hint', + 'Red Shield Shop': 'Red Shield Shop', 'Dark World Hammer Peg Cave': 'Dark World Hammer Peg Cave', + 'Chest Game': 'Chest Game', 'C-Shaped House': 'C-Shaped House', 'Brewery': 'Brewery', + 'Bonk Fairy (Dark)': 'Bonk Fairy (Dark)', 'Hype Cave': 'Hype Cave', + 'Dark Lake Hylia Ledge Hint': 'Dark Lake Hylia Ledge Hint', + 'Dark Lake Hylia Ledge Spike Cave': 'Dark Lake Hylia Ledge Spike Cave', + 'Dark Lake Hylia Ledge Fairy': 'Dark Lake Hylia Ledge Healer Fairy', + 'Dark Lake Hylia Fairy': 'Dark Lake Hylia Healer Fairy', + 'Dark Lake Hylia Shop': 'Dark Lake Hylia Shop', 'Big Bomb Shop': 'Big Bomb Shop', + 'Palace of Darkness Hint': 'Palace of Darkness Hint', 'East Dark World Hint': 'East Dark World Hint', + 'Pyramid Fairy': 'Pyramid Fairy', 'Hookshot Fairy': 'Hookshot Fairy', '50 Rupee Cave': '50 Rupee Cave', + 'Ice Rod Cave': 'Ice Rod Cave', 'Bonk Rock Cave': 'Bonk Rock Cave', 'Library': 'Library', + 'Kakariko Gamble Game': 'Kakariko Gamble Game', 'Potion Shop': 'Potion Shop', '20 Rupee Cave': '20 Rupee Cave', + 'Good Bee Cave': 'Good Bee Cave', 'Long Fairy Cave': 'Long Fairy Cave', 'Mini Moldorm Cave': 'Mini Moldorm Cave', + 'Checkerboard Cave': 'Checkerboard Cave', 'Graveyard Cave': 'Graveyard Cave', 'Cave 45': 'Cave 45', + 'Kakariko Shop': 'Kakariko Shop', 'Light World Bomb Hut': 'Light World Bomb Hut', + 'Tavern (Front)': 'Tavern (Front)', 'Bush Covered House': 'Bush Covered House', + 'Snitch Lady (West)': 'Snitch Lady (West)', 'Snitch Lady (East)': 'Snitch Lady (East)', + 'Fortune Teller (Light)': 'Fortune Teller (Light)', 'Lost Woods Gamble': 'Lost Woods Gamble', + 'Sick Kids House': 'Sick Kids House', 'Blacksmiths Hut': 'Blacksmiths Hut', 'Capacity Upgrade': 'Capacity Upgrade', + 'Cave Shop (Lake Hylia)': 'Cave Shop (Lake Hylia)', 'Sahasrahlas Hut': 'Sahasrahlas Hut', + 'Aginahs Cave': 'Aginahs Cave', 'Chicken House': 'Chicken House', 'Kings Grave': 'Kings Grave', + 'Desert Fairy': 'Desert Healer Fairy', 'Light Hype Fairy': 'Swamp Healer Fairy', + 'Lake Hylia Fortune Teller': 'Lake Hylia Fortune Teller', 'Lake Hylia Fairy': 'Lake Hylia Healer Fairy', + 'Bonk Fairy (Light)': 'Bonk Fairy (Light)', 'Lumberjack House': 'Lumberjack House', 'Dam': 'Dam', + 'Blinds Hideout': 'Blinds Hideout', 'Waterfall of Wishing': 'Waterfall of Wishing', + 'Inverted Bomb Shop': 'Inverted Bomb Shop', 'Inverted Dark Sanctuary': 'Inverted Dark Sanctuary', +} + +default_dw = { + 'Thieves Town Exit', 'Skull Woods First Section Exit', 'Skull Woods Second Section Exit (East)', + 'Skull Woods Second Section Exit (West)', 'Skull Woods Final Section Exit', 'Ice Palace Exit', 'Misery Mire Exit', + 'Palace of Darkness Exit', 'Swamp Palace Exit', 'Turtle Rock Exit (Front)', 'Turtle Rock Ledge Exit (West)', + 'Turtle Rock Ledge Exit (East)', 'Turtle Rock Isolated Ledge Exit', 'Bumper Cave Exit (Top)', + 'Bumper Cave Exit (Bottom)', 'Superbunny Cave Exit (Top)', 'Superbunny Cave Exit (Bottom)', + 'Hookshot Cave Front Exit', 'Hookshot Cave Back Exit', 'Ganons Tower Exit', 'Pyramid Exit', + 'Dark Lake Hylia Healer Fairy', 'Dark Lake Hylia Ledge Healer Fairy', 'Dark Desert Healer Fairy', + 'Dark Death Mountain Healer Fairy', 'Cave Shop (Dark Death Mountain)', 'Pyramid Fairy', 'East Dark World Hint', + 'Palace of Darkness Hint', 'Big Bomb Shop', 'Village of Outcasts Shop', 'Dark Lake Hylia Shop', + 'Dark World Lumberjack Shop', 'Dark World Potion Shop', 'Dark Lake Hylia Ledge Spike Cave', + 'Dark Lake Hylia Ledge Hint', 'Hype Cave', 'Brewery', 'C-Shaped House', 'Chest Game', 'Dark World Hammer Peg Cave', + 'Red Shield Shop', 'Dark Sanctuary Hint', 'Fortune Teller (Dark)', 'Archery Game', 'Mire Shed', 'Dark Desert Hint', + 'Spike Cave', 'Skull Back Drop', 'Skull Left Drop', 'Skull Pinball', 'Skull Pot Circle', 'Pyramid', + 'Inverted Agahnims Tower Exit', 'Inverted Dark Sanctuary Exit', 'Inverted Links House Exit' +} + +default_lw = { + 'Links House Exit', 'Desert Palace Exit (South)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)', + 'Desert Palace Exit (North)', 'Eastern Palace Exit', 'Tower of Hera Exit', 'Hyrule Castle Exit (South)', + 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)', 'Agahnims Tower Exit', + 'Hyrule Castle Secret Entrance Exit', 'Kakariko Well Exit', 'Bat Cave Exit', 'Elder House Exit (East)', + 'Elder House Exit (West)', 'North Fairy Cave Exit', 'Lost Woods Hideout Exit', 'Lumberjack Tree Exit', + 'Two Brothers House Exit (East)', 'Two Brothers House Exit (West)', 'Sanctuary Exit', 'Old Man Cave Exit (East)', + 'Old Man Cave Exit (West)', 'Old Man House Exit (Bottom)', 'Old Man House Exit (Top)', + 'Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)', 'Spectacle Rock Cave Exit', + 'Spectacle Rock Cave Exit (Top)', 'Spectacle Rock Cave Exit (Peak)', 'Paradox Cave Exit (Bottom)', + 'Paradox Cave Exit (Middle)', 'Paradox Cave Exit (Top)', 'Fairy Ascension Cave Exit (Bottom)', + 'Fairy Ascension Cave Exit (Top)', 'Spiral Cave Exit', 'Spiral Cave Exit (Top)', 'Waterfall of Wishing', 'Dam', + 'Blinds Hideout', 'Lumberjack House', 'Bonk Fairy (Light)', 'Bonk Fairy (Dark)', 'Lake Hylia Healer Fairy', + 'Swamp Healer Fairy', 'Desert Healer Fairy', 'Fortune Teller (Light)', 'Lake Hylia Fortune Teller', 'Kings Grave', + 'Chicken House', 'Aginahs Cave', 'Sahasrahlas Hut', 'Cave Shop (Lake Hylia)', 'Capacity Upgrade', 'Blacksmiths Hut', + 'Sick Kids House', 'Lost Woods Gamble', 'Snitch Lady (East)', 'Snitch Lady (West)', 'Bush Covered House', + 'Tavern (Front)', 'Light World Bomb Hut', 'Kakariko Shop', 'Cave 45', 'Graveyard Cave', 'Checkerboard Cave', + 'Mini Moldorm Cave', 'Long Fairy Cave', 'Good Bee Cave', '20 Rupee Cave', '50 Rupee Cave', 'Ice Rod Cave', + 'Bonk Rock Cave', 'Library', 'Kakariko Gamble Game', 'Potion Shop', 'Hookshot Fairy', 'Mimic Cave', + 'Kakariko Well (top)', 'Hyrule Castle Secret Entrance', 'Bat Cave (right)', 'North Fairy Cave', + 'Lost Woods Hideout (top)', 'Lumberjack Tree (top)', 'Sewer Drop', 'Inverted Ganons Tower Exit', + 'Inverted Big Bomb Shop' +} + +LW_Entrances = ['Elder House (East)', 'Elder House (West)', 'Two Brothers House (East)', 'Two Brothers House (West)', + 'Old Man Cave (West)', 'Old Man House (Bottom)', 'Death Mountain Return Cave (West)', + 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'Paradox Cave (Top)', + 'Fairy Ascension Cave (Bottom)', 'Fairy Ascension Cave (Top)', 'Spiral Cave', 'Spiral Cave (Bottom)', + 'Desert Palace Entrance (South)', 'Desert Palace Entrance (West)', 'Desert Palace Entrance (North)', + 'Desert Palace Entrance (East)', 'Eastern Palace', 'Tower of Hera', 'Hyrule Castle Entrance (West)', + 'Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (South)', 'Agahnims Tower', 'Blinds Hideout', + 'Lake Hylia Fairy', 'Light Hype Fairy', 'Desert Fairy', 'Chicken House', 'Aginahs Cave', + 'Sahasrahlas Hut', 'Cave Shop (Lake Hylia)', 'Blacksmiths Hut', 'Sick Kids House', 'Lost Woods Gamble', + 'Fortune Teller (Light)', 'Snitch Lady (East)', 'Snitch Lady (West)', 'Bush Covered House', + 'Tavern (Front)', 'Light World Bomb Hut', 'Kakariko Shop', 'Mini Moldorm Cave', 'Long Fairy Cave', + 'Good Bee Cave', '20 Rupee Cave', '50 Rupee Cave', 'Ice Rod Cave', 'Library', 'Potion Shop', 'Dam', + 'Lumberjack House', 'Lake Hylia Fortune Teller', 'Kakariko Gamble Game', 'Waterfall of Wishing', + 'Capacity Upgrade', 'Bonk Rock Cave', 'Graveyard Cave', 'Checkerboard Cave', 'Cave 45', 'Kings Grave', + 'Bonk Fairy (Light)', 'Hookshot Fairy', 'Mimic Cave', 'Links House', 'Old Man Cave (East)', + 'Old Man House (Top)', 'Death Mountain Return Cave (East)', 'Spectacle Rock Cave', + 'Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Bottom)', 'Hyrule Castle Secret Entrance Stairs', + 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Lost Woods Hideout Stump', + 'Lumberjack Tree Cave', 'Sanctuary', + 'Inverted Ganons Tower', 'Inverted Big Bomb Shop', 'Inverted Pyramid Entrance'] + +DW_Entrances = ['Bumper Cave (Bottom)', 'Superbunny Cave (Top)', 'Superbunny Cave (Bottom)', 'Hookshot Cave', + 'Thieves Town', 'Skull Woods Final Section', 'Ice Palace', 'Misery Mire', 'Palace of Darkness', + 'Swamp Palace', 'Turtle Rock', 'Dark Death Mountain Ledge (West)', 'Dark Death Mountain Ledge (East)', + 'Turtle Rock Isolated Ledge Entrance', 'Bumper Cave (Top)', 'Hookshot Cave Back Entrance', + 'Bonk Fairy (Dark)', 'Dark Sanctuary Hint', 'Dark Lake Hylia Fairy', 'C-Shaped House', 'Big Bomb Shop', + 'Dark Death Mountain Fairy', 'Dark Lake Hylia Shop', 'Dark World Shop', 'Red Shield Shop', 'Mire Shed', + 'East Dark World Hint', 'Dark Desert Hint', 'Spike Cave', 'Palace of Darkness Hint', + 'Dark Lake Hylia Ledge Spike Cave', 'Cave Shop (Dark Death Mountain)', 'Dark World Potion Shop', + 'Pyramid Fairy', 'Archery Game', 'Dark World Lumberjack Shop', 'Hype Cave', 'Brewery', + 'Dark Lake Hylia Ledge Hint', 'Chest Game', 'Dark Desert Fairy', 'Dark Lake Hylia Ledge Fairy', + 'Fortune Teller (Dark)', 'Dark World Hammer Peg Cave', 'Pyramid Entrance', + 'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', + 'Skull Woods Second Section Door (West)', + 'Inverted Dark Sanctuary', 'Inverted Links House', 'Inverted Agahnims Tower'] + +LW_Must_Exit = ['Desert Palace Entrance (East)'] + +DW_Must_Exit = [('Dark Death Mountain Ledge (East)', 'Dark Death Mountain Ledge (West)'), + 'Turtle Rock Isolated Ledge Entrance', 'Bumper Cave (Top)', 'Hookshot Cave Back Entrance', + 'Pyramid Entrance'] + +Inverted_LW_Must_Exit = [('Desert Palace Entrance (North)', 'Desert Palace Entrance (West)'), + 'Desert Palace Entrance (East)', 'Death Mountain Return Cave (West)', + 'Two Brothers House (West)', + ('Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)', 'Inverted Ganons Tower')] + +Inverted_DW_Must_Exit = [] + +Isolated_LH_Doors_Open = ['Mimic Cave', 'Kings Grave', 'Waterfall of Wishing', 'Desert Palace Entrance (South)', + 'Desert Palace Entrance (North)', 'Capacity Upgrade', 'Ice Palace', + 'Skull Woods Final Section', 'Skull Woods Second Section Door (West)', + 'Dark World Hammer Peg Cave', 'Turtle Rock Isolated Ledge Entrance', + 'Dark Death Mountain Ledge (West)', 'Dark Death Mountain Ledge (East)', + 'Dark World Shop', 'Dark World Potion Shop'] + +Isolated_LH_Doors_Inv = ['Kings Grave', 'Waterfall of Wishing', 'Desert Palace Entrance (South)', + 'Desert Palace Entrance (North)', 'Capacity Upgrade', 'Ice Palace', + 'Skull Woods Final Section', 'Skull Woods Second Section Door (West)', + 'Dark World Hammer Peg Cave', 'Turtle Rock Isolated Ledge Entrance', + 'Dark Death Mountain Ledge (West)', 'Dark Death Mountain Ledge (East)', + 'Dark World Shop', 'Dark World Potion Shop'] + +# inverted doesn't like really like - Paradox Top or Tower of Hera +LH_DM_Connector_List = { + 'Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)', 'Death Mountain Return Cave (East)', + 'Fairy Ascension Cave (Bottom)', 'Fairy Ascension Cave (Top)', 'Spiral Cave', 'Spiral Cave (Bottom)', + 'Tower of Hera', 'Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Bottom)', 'Spectacle Rock Cave', + 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'Paradox Cave (Top)', 'Hookshot Fairy', 'Spike Cave', + 'Dark Death Mountain Fairy', 'Inverted Agahnims Tower', 'Superbunny Cave (Top)', 'Superbunny Cave (Bottom)', + 'Hookshot Cave', 'Cave Shop (Dark Death Mountain)', 'Turtle Rock'} + +LH_DM_Exit_Forbidden = { + 'Turtle Rock Isolated Ledge Entrance', 'Mimic Cave', 'Hookshot Cave Back Entrance', + 'Dark Death Mountain Ledge (West)', 'Dark Death Mountain Ledge (East)', 'Desert Palace Entrance (South)', + 'Ice Palace', 'Waterfall of Wishing', 'Kings Grave', 'Dark World Hammer Peg Cave', 'Capacity Upgrade', + 'Skull Woods Final Section', 'Skull Woods Second Section Door (West)' +} # omissions from Isolated Starts: 'Desert Palace Entrance (North)', 'Dark World Shop', 'Dark World Potion Shop' + +# in inverted we put dark sanctuary in west dark world for now +Inverted_Dark_Sanctuary_Doors = [ + 'Inverted Dark Sanctuary', 'Fortune Teller (Dark)', 'Brewery', 'C-Shaped House', 'Chest Game', + 'Dark World Lumberjack Shop', 'Red Shield Shop', 'Bumper Cave (Bottom)', 'Bumper Cave (Top)', 'Thieves Town' +] + +Connector_List = [['Elder House Exit (East)', 'Elder House Exit (West)'], + ['Two Brothers House Exit (East)', 'Two Brothers House Exit (West)'], + ['Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)'], + ['Fairy Ascension Cave Exit (Bottom)', 'Fairy Ascension Cave Exit (Top)'], + ['Bumper Cave Exit (Top)', 'Bumper Cave Exit (Bottom)'], + ['Hookshot Cave Back Exit', 'Hookshot Cave Front Exit'], + ['Superbunny Cave Exit (Bottom)', 'Superbunny Cave Exit (Top)'], + ['Spiral Cave Exit (Top)', 'Spiral Cave Exit'], + ['Old Man House Exit (Bottom)', 'Old Man House Exit (Top)'], + ['Spectacle Rock Cave Exit (Peak)', 'Spectacle Rock Cave Exit (Top)', + 'Spectacle Rock Cave Exit'], + ['Paradox Cave Exit (Top)', 'Paradox Cave Exit (Middle)', 'Paradox Cave Exit (Bottom)'], + ['Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', + 'Hyrule Castle Exit (East)'], + ['Desert Palace Exit (South)', 'Desert Palace Exit (East)', + 'Desert Palace Exit (West)'], + ['Turtle Rock Exit (Front)', 'Turtle Rock Isolated Ledge Exit', + 'Turtle Rock Ledge Exit (West)', 'Turtle Rock Ledge Exit (East)']] + +Connector_Exit_Set = { + 'Elder House Exit (East)', 'Elder House Exit (West)', 'Two Brothers House Exit (East)', + 'Two Brothers House Exit (West)', 'Death Mountain Return Cave Exit (West)', + 'Death Mountain Return Cave Exit (East)', 'Fairy Ascension Cave Exit (Bottom)', 'Fairy Ascension Cave Exit (Top)', + 'Bumper Cave Exit (Top)', 'Bumper Cave Exit (Bottom)', 'Hookshot Cave Back Exit', 'Hookshot Cave Front Exit', + 'Superbunny Cave Exit (Top)', 'Spiral Cave Exit', 'Old Man House Exit (Bottom)', 'Old Man House Exit (Top)', + 'Spectacle Rock Cave Exit', 'Paradox Cave Exit (Bottom)', + 'Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)', + 'Desert Palace Exit (South)', 'Desert Palace Exit (East)', 'Desert Palace Exit (West)', 'Turtle Rock Exit (Front)', + 'Turtle Rock Isolated Ledge Exit', 'Turtle Rock Ledge Exit (West)' +} + +# Entrances that cannot be used to access a must_exit entrance - symmetrical to allow reverse lookups +Must_Exit_Invalid_Connections = defaultdict(set, { + 'Dark Death Mountain Ledge (East)': {'Dark Death Mountain Ledge (West)', 'Mimic Cave'}, + 'Dark Death Mountain Ledge (West)': {'Dark Death Mountain Ledge (East)', 'Mimic Cave'}, + 'Mimic Cave': {'Dark Death Mountain Ledge (West)', 'Dark Death Mountain Ledge (East)'}, + 'Bumper Cave (Top)': {'Death Mountain Return Cave (West)'}, + 'Death Mountain Return Cave (West)': {'Bumper Cave (Top)'}, + 'Skull Woods Second Section Door (West)': {'Skull Woods Final Section'}, + 'Skull Woods Final Section': {'Skull Woods Second Section Door (West)'}, +}) +Inverted_Must_Exit_Invalid_Connections = defaultdict(set, { + 'Bumper Cave (Top)': {'Death Mountain Return Cave (West)'}, + 'Death Mountain Return Cave (West)': {'Bumper Cave (Top)'}, + 'Desert Palace Entrance (North)': {'Desert Palace Entrance (West)'}, + 'Desert Palace Entrance (West)': {'Desert Palace Entrance (North)'}, + 'Inverted Ganons Tower': {'Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)'}, + 'Hyrule Castle Entrance (West)': {'Hyrule Castle Entrance (East)', 'Inverted Ganons Tower'}, + 'Hyrule Castle Entrance (East)': {'Hyrule Castle Entrance (West)', 'Inverted Ganons Tower'}, +}) + +Old_Man_Entrances = ['Old Man Cave (East)', + 'Old Man House (Top)', + 'Death Mountain Return Cave (East)', + 'Spectacle Rock Cave', + 'Spectacle Rock Cave Peak', + 'Spectacle Rock Cave (Bottom)', + 'Tower of Hera'] + +Inverted_Old_Man_Entrances = ['Dark Death Mountain Fairy', 'Spike Cave', 'Inverted Agahnims Tower'] + +Simple_DM_Non_Connectors = {'Old Man Cave Ledge', 'Spiral Cave (Top)', 'Superbunny Cave (Bottom)', + 'Spectacle Rock Cave (Peak)', 'Spectacle Rock Cave (Top)'} + +Blacksmith_Options = [ + 'Blinds Hideout', 'Lake Hylia Fairy', 'Light Hype Fairy', 'Desert Fairy', 'Chicken House', 'Aginahs Cave', + 'Sahasrahlas Hut', 'Cave Shop (Lake Hylia)', 'Blacksmiths Hut', 'Sick Kids House', 'Lost Woods Gamble', + 'Fortune Teller (Light)', 'Snitch Lady (East)', 'Snitch Lady (West)', 'Bush Covered House', 'Tavern (Front)', + 'Light World Bomb Hut', 'Kakariko Shop', 'Mini Moldorm Cave', 'Long Fairy Cave', 'Good Bee Cave', '20 Rupee Cave', + '50 Rupee Cave', 'Ice Rod Cave', 'Library', 'Potion Shop', 'Dam', 'Lumberjack House', 'Lake Hylia Fortune Teller', + 'Kakariko Gamble Game', 'Eastern Palace', 'Elder House (East)', 'Elder House (West)', 'Two Brothers House (East)', + 'Old Man Cave (West)', 'Sanctuary', 'Lumberjack Tree Cave', 'Lost Woods Hideout Stump', 'North Fairy Cave', + 'Bat Cave Cave', 'Kakariko Well Cave', 'Inverted Big Bomb Shop', 'Links House'] + +Bomb_Shop_Options = [ + 'Waterfall of Wishing', 'Capacity Upgrade', 'Bonk Rock Cave', 'Graveyard Cave', 'Checkerboard Cave', 'Cave 45', + 'Kings Grave', 'Bonk Fairy (Light)', 'Hookshot Fairy', 'East Dark World Hint', 'Palace of Darkness Hint', + 'Dark Lake Hylia Fairy', 'Dark Lake Hylia Ledge Fairy', 'Dark Lake Hylia Ledge Spike Cave', + 'Dark Lake Hylia Ledge Hint', 'Hype Cave', 'Bonk Fairy (Dark)', 'Brewery', 'C-Shaped House', 'Chest Game', + 'Dark World Hammer Peg Cave', 'Red Shield Shop', 'Dark Sanctuary Hint', 'Fortune Teller (Dark)', 'Dark World Shop', + 'Dark World Lumberjack Shop', 'Dark World Potion Shop', 'Archery Game', 'Mire Shed', 'Dark Desert Hint', + 'Dark Desert Fairy', 'Spike Cave', 'Cave Shop (Dark Death Mountain)', 'Dark Death Mountain Fairy', 'Mimic Cave', + 'Big Bomb Shop', 'Dark Lake Hylia Shop', 'Bumper Cave (Top)', 'Links House', + 'Hyrule Castle Entrance (South)', 'Misery Mire', 'Thieves Town', 'Bumper Cave (Bottom)', 'Swamp Palace', + 'Hyrule Castle Secret Entrance Stairs', 'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', + 'Skull Woods Second Section Door (West)', 'Skull Woods Final Section', 'Ice Palace', 'Turtle Rock', + 'Dark Death Mountain Ledge (West)', 'Dark Death Mountain Ledge (East)', 'Superbunny Cave (Top)', + 'Superbunny Cave (Bottom)', 'Hookshot Cave', 'Ganons Tower', 'Desert Palace Entrance (South)', 'Tower of Hera', + 'Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)', + 'Death Mountain Return Cave (East)', 'Death Mountain Return Cave (West)', 'Spectacle Rock Cave Peak', + 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'Paradox Cave (Top)', 'Fairy Ascension Cave (Bottom)', + 'Fairy Ascension Cave (Top)', 'Spiral Cave', 'Spiral Cave (Bottom)', 'Palace of Darkness', + 'Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)', 'Agahnims Tower', + 'Desert Palace Entrance (West)', 'Desert Palace Entrance (North)', + 'Spectacle Rock Cave', 'Spectacle Rock Cave (Bottom)', 'Two Brothers House (West)'] + Blacksmith_Options + +Inverted_Bomb_Shop_Options = [ + 'Waterfall of Wishing', 'Capacity Upgrade', 'Bonk Rock Cave', 'Graveyard Cave', 'Checkerboard Cave', 'Cave 45', + 'Kings Grave', 'Bonk Fairy (Light)', 'Hookshot Fairy', 'East Dark World Hint', 'Palace of Darkness Hint', + 'Dark Lake Hylia Fairy', 'Dark Lake Hylia Ledge Fairy', 'Dark Lake Hylia Ledge Spike Cave', + 'Dark Lake Hylia Ledge Hint', 'Hype Cave', 'Bonk Fairy (Dark)', 'Brewery', 'C-Shaped House', 'Chest Game', + 'Dark World Hammer Peg Cave', 'Red Shield Shop', 'Fortune Teller (Dark)', 'Dark World Shop', + 'Dark World Lumberjack Shop', 'Dark World Potion Shop', 'Archery Game', 'Mire Shed', 'Dark Desert Hint', + 'Dark Desert Fairy', 'Spike Cave', 'Cave Shop (Dark Death Mountain)', 'Dark Death Mountain Fairy', 'Mimic Cave', + 'Dark Lake Hylia Shop', 'Bumper Cave (Top)', + 'Hyrule Castle Entrance (South)', 'Misery Mire', 'Thieves Town', 'Bumper Cave (Bottom)', 'Swamp Palace', + 'Hyrule Castle Secret Entrance Stairs', 'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', + 'Skull Woods Second Section Door (West)', 'Skull Woods Final Section', 'Ice Palace', 'Turtle Rock', + 'Dark Death Mountain Ledge (West)', 'Dark Death Mountain Ledge (East)', 'Superbunny Cave (Top)', + 'Superbunny Cave (Bottom)', 'Hookshot Cave', 'Desert Palace Entrance (South)', 'Tower of Hera', + 'Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)', + 'Death Mountain Return Cave (East)', 'Death Mountain Return Cave (West)', 'Spectacle Rock Cave Peak', + 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'Paradox Cave (Top)', 'Fairy Ascension Cave (Bottom)', + 'Fairy Ascension Cave (Top)', 'Spiral Cave', 'Spiral Cave (Bottom)', 'Palace of Darkness', + 'Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)', + 'Desert Palace Entrance (West)', 'Desert Palace Entrance (North)', + 'Inverted Ganons Tower', 'Inverted Agahnims Tower', 'Inverted Dark Sanctuary', 'Inverted Links House', + 'Inverted Big Bomb Shop'] + Blacksmith_Options + + +# these are connections that cannot be shuffled and always exist. +# They link together separate parts of the world we need to divide into regions +mandatory_connections = [('Links House S&Q', 'Links House'), + ('Sanctuary S&Q', 'Sanctuary'), + ('Old Man S&Q', 'Old Man House'), + ('Lake Hylia Central Island Pier', 'Lake Hylia Central Island'), + ('Lake Hylia Central Island Teleporter', 'Dark Lake Hylia Central Island'), + ('Zoras River', 'Zoras River'), + ('Kings Grave Outer Rocks', 'Kings Grave Area'), + ('Kings Grave Inner Rocks', 'Light World'), + ('Kings Grave Mirror Spot', 'Kings Grave Area'), + ('Kakariko Well (top to bottom)', 'Kakariko Well (bottom)'), + ('Kakariko Well (top to back)', 'Kakariko Well (back)'), + ('Master Sword Meadow', 'Master Sword Meadow'), + ('Hobo Bridge', 'Hobo Bridge'), + ('Bat Cave Drop Ledge', 'Bat Cave Drop Ledge'), + ('Bat Cave Door', 'Bat Cave (left)'), + ('Lost Woods Hideout (top to bottom)', 'Lost Woods Hideout (bottom)'), + ('Lumberjack Tree (top to bottom)', 'Lumberjack Tree (bottom)'), + ('Blinds Hideout N', 'Blinds Hideout (Top)'), + ('Desert Palace Stairs', 'Desert Palace Stairs'), + ('Desert Palace Stairs Drop', 'Light World'), + ('Desert Palace Entrance (North) Rocks', 'Desert Palace Entrance (North) Spot'), + ('Desert Ledge Return Rocks', 'Desert Ledge'), + ('Hyrule Castle Ledge Courtyard Drop', 'Hyrule Castle Courtyard'), + ('Hyrule Castle Main Gate', 'Hyrule Castle Courtyard'), + ('Sewer Drop', 'Sewers Rat Path'), + ('Flute Spot 1', 'Death Mountain'), + ('Death Mountain Entrance Rock', 'Death Mountain Entrance'), + ('Death Mountain Entrance Drop', 'Light World'), + ('Spectacle Rock Cave Drop', 'Spectacle Rock Cave (Bottom)'), + ('Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave (Bottom)'), + ('Death Mountain Return Cave E', 'Death Mountain Return Cave (right)'), + ('Death Mountain Return Cave W', 'Death Mountain Return Cave (left)'), + ('Death Mountain Return Ledge Drop', 'Light World'), + ('Old Man Cave Dropdown', 'Old Man Cave'), + ('Old Man House Front to Back', 'Old Man House Back'), + ('Old Man House Back to Front', 'Old Man House'), + ('Broken Bridge (West)', 'East Death Mountain (Bottom)'), + ('Broken Bridge (East)', 'Death Mountain'), + ('East Death Mountain Drop', 'East Death Mountain (Bottom)'), + ('Spiral Cave Ledge Access', 'Spiral Cave Ledge'), + ('Spiral Cave Ledge Drop', 'East Death Mountain (Bottom)'), + ('Spiral Cave (top to bottom)', 'Spiral Cave (Bottom)'), + ('East Death Mountain (Top)', 'East Death Mountain (Top)'), + ('Death Mountain (Top)', 'Death Mountain (Top)'), + ('Death Mountain Drop', 'Death Mountain'), + ('Spectacle Rock Drop', 'Death Mountain (Top)'), + + ('Top of Pyramid', 'East Dark World'), + ('Dark Lake Hylia Drop (East)', 'Dark Lake Hylia'), + ('Dark Lake Hylia Drop (South)', 'Dark Lake Hylia'), + ('Dark Lake Hylia Teleporter', 'Dark Lake Hylia'), + ('Dark Lake Hylia Ledge', 'Dark Lake Hylia Ledge'), + ('Dark Lake Hylia Ledge Drop', 'Dark Lake Hylia'), + ('East Dark World Pier', 'East Dark World'), + ('Lake Hylia Island Mirror Spot', 'Lake Hylia Island'), + ('Lake Hylia Central Island Mirror Spot', 'Lake Hylia Central Island'), + ('Hyrule Castle Ledge Mirror Spot', 'Hyrule Castle Ledge'), + ('South Dark World Bridge', 'South Dark World'), + ('East Dark World Bridge', 'East Dark World'), + ('Maze Race Mirror Spot', 'Maze Race Ledge'), + ('Village of Outcasts Heavy Rock', 'West Dark World'), + ('Village of Outcasts Drop', 'South Dark World'), + ('Village of Outcasts Eastern Rocks', 'Hammer Peg Area'), + ('Village of Outcasts Pegs', 'Dark Grassy Lawn'), + ('Peg Area Rocks', 'West Dark World'), + ('Grassy Lawn Pegs', 'West Dark World'), + ('Bat Cave Drop Ledge Mirror Spot', 'Bat Cave Drop Ledge'), + ('East Dark World River Pier', 'East Dark World'), + ('West Dark World Gap', 'West Dark World'), + ('East Dark World Broken Bridge Pass', 'East Dark World'), + ('Catfish Exit Rock', 'Northeast Dark World'), + ('Catfish Entrance Rock', 'Catfish'), + ('Northeast Dark World Broken Bridge Pass', 'Northeast Dark World'), + ('Bumper Cave Entrance Rock', 'Bumper Cave Entrance'), + ('Bumper Cave Entrance Drop', 'West Dark World'), + ('Bumper Cave Entrance Mirror Spot', 'Death Mountain Entrance'), + ('Bumper Cave Ledge Drop', 'West Dark World'), + ('Bumper Cave Ledge Mirror Spot', 'Death Mountain Return Ledge'), + ('Skull Woods Forest', 'Skull Woods Forest'), + ('Desert Ledge Mirror Spot', 'Desert Ledge'), + ('Desert Ledge (Northeast) Mirror Spot', 'Desert Ledge (Northeast)'), + ('Desert Palace Entrance (North) Mirror Spot', 'Desert Palace Entrance (North) Spot'), + ('Dark Desert Teleporter', 'Dark Desert'), + ('Desert Palace Stairs Mirror Spot', 'Desert Palace Stairs'), + ('East Hyrule Teleporter', 'East Dark World'), + ('South Hyrule Teleporter', 'South Dark World'), + ('Kakariko Teleporter', 'West Dark World'), + ('Death Mountain Teleporter', 'Dark Death Mountain (West Bottom)'), + ('Paradox Cave Push Block Reverse', 'Paradox Cave Chest Area'), + ('Paradox Cave Push Block', 'Paradox Cave Front'), + ('Paradox Cave Chest Area NE', 'Paradox Cave Bomb Area'), + ('Paradox Cave Bomb Jump', 'Paradox Cave'), + ('Paradox Cave Drop', 'Paradox Cave Chest Area'), + ('Light World Death Mountain Shop', 'Light World Death Mountain Shop'), + ('Fairy Ascension Rocks', 'Fairy Ascension Plateau'), + ('Fairy Ascension Mirror Spot', 'Fairy Ascension Plateau'), + ('Fairy Ascension Drop', 'East Death Mountain (Bottom)'), + ('Fairy Ascension Ledge Drop', 'Fairy Ascension Plateau'), + ('Fairy Ascension Ledge', 'Fairy Ascension Ledge'), + ('Fairy Ascension Cave Climb', 'Fairy Ascension Cave (Top)'), + ('Fairy Ascension Cave Pots', 'Fairy Ascension Cave (Bottom)'), + ('Fairy Ascension Cave Drop', 'Fairy Ascension Cave (Drop)'), + ('Spectacle Rock Mirror Spot', 'Spectacle Rock'), + ('Dark Death Mountain Drop (East)', 'Dark Death Mountain (East Bottom)'), + ('Dark Death Mountain Drop (West)', 'Dark Death Mountain (West Bottom)'), + ('East Death Mountain (Top) Mirror Spot', 'East Death Mountain (Top)'), + ('Superbunny Cave Climb', 'Superbunny Cave (Top)'), + ('Hookshot Cave Front to Middle', 'Hookshot Cave (Middle)'), + ('Hookshot Cave Middle to Front', 'Hookshot Cave (Front)'), + ('Hookshot Cave Middle to Back', 'Hookshot Cave (Back)'), + ('Hookshot Cave Back to Middle', 'Hookshot Cave (Middle)'), + ('Hookshot Cave Bonk Path', 'Hookshot Cave (Bonk Islands)'), + ('Hookshot Cave Hook Path', 'Hookshot Cave (Hook Islands)'), + ('Turtle Rock Teleporter', 'Turtle Rock (Top)'), + ('Turtle Rock Drop', 'Dark Death Mountain (Top)'), + ('Floating Island Drop', 'Dark Death Mountain (Top)'), + ('Floating Island Mirror Spot', 'Death Mountain Floating Island (Light World)'), + ('East Death Mountain Teleporter', 'Dark Death Mountain (East Bottom)'), + ('Isolated Ledge Mirror Spot', 'Fairy Ascension Ledge'), + ('Spiral Cave Mirror Spot', 'Spiral Cave Ledge'), + ('Mimic Cave Mirror Spot', 'Mimic Cave Ledge'), + ('Cave 45 Mirror Spot', 'Cave 45 Ledge'), + ('Bombos Tablet Mirror Spot', 'Bombos Tablet Ledge'), + ('Graveyard Ledge Mirror Spot', 'Graveyard Ledge'), + ('Ganon Drop', 'Bottom of Pyramid'), + ('Pyramid Drop', 'East Dark World'), + ('Maze Race Ledge Drop', 'Light World'), + ('Graveyard Ledge Drop', 'Light World'), + ('Cave 45 Ledge Drop', 'Light World'), + ('Checkerboard Ledge Drop', 'Light World'), + ('Desert Ledge Drop', 'Light World'), + ('Hyrule Castle Main Gate (North)', 'Light World'), + ('Hyrule Castle Ledge Drop', 'Light World'), + ] + +inverted_mandatory_connections = [('Links House S&Q', 'Inverted Links House'), + ('Dark Sanctuary S&Q', 'Inverted Dark Sanctuary'), + ('Old Man S&Q', 'Old Man House'), + ('Castle Ledge S&Q', 'Hyrule Castle Ledge'), + ('Lake Hylia Central Island Pier', 'Lake Hylia Central Island'), + ('Lake Hylia Island Pier', 'Lake Hylia Island'), + ('Lake Hylia Warp', 'Northeast Light World'), + ('Northeast Light World Warp', 'Light World'), + ('Zoras River', 'Zoras River'), + ('Waterfall of Wishing Cave', 'Waterfall of Wishing Cave'), + ('Northeast Light World Return', 'Northeast Light World'), + ('Kings Grave Outer Rocks', 'Kings Grave Area'), + ('Kings Grave Inner Rocks', 'Light World'), + ('Kakariko Well (top to bottom)', 'Kakariko Well (bottom)'), + ('Kakariko Well (top to back)', 'Kakariko Well (back)'), + ('Master Sword Meadow', 'Master Sword Meadow'), + ('Hobo Bridge', 'Hobo Bridge'), + ('Bat Cave Drop Ledge', 'Bat Cave Drop Ledge'), + ('Bat Cave Door', 'Bat Cave (left)'), + ('Lost Woods Hideout (top to bottom)', 'Lost Woods Hideout (bottom)'), + ('Lumberjack Tree (top to bottom)', 'Lumberjack Tree (bottom)'), + ('Blinds Hideout N', 'Blinds Hideout (Top)'), + ('Desert Palace Stairs', 'Desert Palace Stairs'), + ('Desert Palace Stairs Drop', 'Light World'), + ('Desert Palace Entrance (North) Rocks', 'Desert Palace Entrance (North) Spot'), + ('Desert Ledge Return Rocks', 'Desert Ledge'), + ('Sewer Drop', 'Sewers Rat Path'), + ('Death Mountain Entrance Rock', 'Death Mountain Entrance'), + ('Death Mountain Entrance Drop', 'Light World'), + ('Death Mountain Return Cave E', 'Death Mountain Return Cave (right)'), + ('Death Mountain Return Cave W', 'Death Mountain Return Cave (left)'), + ('Spectacle Rock Cave Drop', 'Spectacle Rock Cave (Bottom)'), + ('Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave (Bottom)'), + ('Death Mountain Return Ledge Drop', 'Light World'), + ('Old Man Cave Dropdown', 'Old Man Cave'), + ('Old Man House Front to Back', 'Old Man House Back'), + ('Old Man House Back to Front', 'Old Man House'), + ('Broken Bridge (West)', 'East Death Mountain (Bottom)'), + ('Broken Bridge (East)', 'Death Mountain'), + ('East Death Mountain Drop', 'East Death Mountain (Bottom)'), + ('Spiral Cave Ledge Access', 'Spiral Cave Ledge'), + ('Spiral Cave Ledge Drop', 'East Death Mountain (Bottom)'), + ('Spiral Cave (top to bottom)', 'Spiral Cave (Bottom)'), + ('East Death Mountain (Top)', 'East Death Mountain (Top)'), + ('Death Mountain (Top)', 'Death Mountain (Top)'), + ('Death Mountain Drop', 'Death Mountain'), + ('Dark Lake Hylia Drop (East)', 'Dark Lake Hylia'), + ('Dark Lake Hylia Drop (South)', 'Dark Lake Hylia'), + ('Dark Lake Hylia Teleporter', 'Dark Lake Hylia'), + ('Dark Lake Hylia Ledge Pier', 'Dark Lake Hylia Ledge'), + ('Dark Lake Hylia Ledge Drop', 'Dark Lake Hylia'), + ('Ice Palace Missing Wall', 'Dark Lake Hylia Central Island'), + ('Dark Lake Hylia Shallows', 'Dark Lake Hylia'), + ('East Dark World Pier', 'East Dark World'), + ('South Dark World Bridge', 'South Dark World'), + ('East Dark World Bridge', 'East Dark World'), + ('Village of Outcasts Heavy Rock', 'West Dark World'), + ('Village of Outcasts Drop', 'South Dark World'), + ('Village of Outcasts Eastern Rocks', 'Hammer Peg Area'), + ('Village of Outcasts Pegs', 'Dark Grassy Lawn'), + ('Peg Area Rocks', 'West Dark World'), + ('Grassy Lawn Pegs', 'West Dark World'), + ('East Dark World River Pier', 'Northeast Dark World'), + ('West Dark World Gap', 'West Dark World'), + ('East Dark World Broken Bridge Pass', 'East Dark World'), + ('Northeast Dark World Broken Bridge Pass', 'Northeast Dark World'), + ('Catfish Exit Rock', 'Northeast Dark World'), + ('Catfish Entrance Rock', 'Catfish'), + ('Bumper Cave Entrance Rock', 'Bumper Cave Entrance'), + ('Bumper Cave Entrance Drop', 'West Dark World'), + ('Bumper Cave Ledge Drop', 'West Dark World'), + ('Skull Woods Forest', 'Skull Woods Forest'), + ('Paradox Cave Push Block Reverse', 'Paradox Cave Chest Area'), + ('Paradox Cave Push Block', 'Paradox Cave Front'), + ('Paradox Cave Chest Area NE', 'Paradox Cave Bomb Area'), + ('Paradox Cave Bomb Jump', 'Paradox Cave'), + ('Paradox Cave Drop', 'Paradox Cave Chest Area'), + ('Light World Death Mountain Shop', 'Light World Death Mountain Shop'), + ('Fairy Ascension Rocks', 'Fairy Ascension Plateau'), + ('Fairy Ascension Drop', 'East Death Mountain (Bottom)'), + ('Fairy Ascension Ledge Drop', 'Fairy Ascension Plateau'), + ('Fairy Ascension Ledge Access', 'Fairy Ascension Ledge'), + ('Fairy Ascension Cave Climb', 'Fairy Ascension Cave (Top)'), + ('Fairy Ascension Cave Pots', 'Fairy Ascension Cave (Bottom)'), + ('Fairy Ascension Cave Drop', 'Fairy Ascension Cave (Drop)'), + ('Dark Death Mountain Drop (East)', 'Dark Death Mountain (East Bottom)'), + ('Ganon Drop', 'Bottom of Pyramid'), + ('Pyramid Drop', 'East Dark World'), + ('Post Aga Teleporter', 'Light World'), + ('Secret Passage Inner Bushes', 'Light World'), + ('Secret Passage Outer Bushes', 'Hyrule Castle Secret Entrance Area'), + ('Potion Shop Inner Bushes', 'Light World'), + ('Potion Shop Outer Bushes', 'Potion Shop Area'), + ('Potion Shop Inner Rock', 'Northeast Light World'), + ('Potion Shop Outer Rock', 'Potion Shop Area'), + ('Potion Shop River Drop', 'River'), + ('Graveyard Cave Inner Bushes', 'Light World'), + ('Graveyard Cave Outer Bushes', 'Graveyard Cave Area'), + ('Graveyard Cave Mirror Spot', 'West Dark World'), + ('Light World River Drop', 'River'), + ('Light World Pier', 'Light World'), + ('Potion Shop Pier', 'Potion Shop Area'), + ('Hyrule Castle Ledge Courtyard Drop', 'Light World'), + ('Mimic Cave Ledge Access', 'Mimic Cave Ledge'), + ('Mimic Cave Ledge Drop', 'East Death Mountain (Bottom)'), + ('Turtle Rock Tail Drop', 'Turtle Rock (Top)'), + ('Turtle Rock Drop', 'Dark Death Mountain'), + ('Superbunny Cave Climb', 'Superbunny Cave (Top)'), + ('Hookshot Cave Front to Middle', 'Hookshot Cave (Middle)'), + ('Hookshot Cave Middle to Front', 'Hookshot Cave (Front)'), + ('Hookshot Cave Middle to Back', 'Hookshot Cave (Back)'), + ('Hookshot Cave Back to Middle', 'Hookshot Cave (Middle)'), + ('Hookshot Cave Bonk Path', 'Hookshot Cave (Bonk Islands)'), + ('Hookshot Cave Hook Path', 'Hookshot Cave (Hook Islands)'), + ('Desert Ledge Drop', 'Light World'), + ('Floating Island Drop', 'Dark Death Mountain'), + ('Dark Lake Hylia Central Island Teleporter', 'Lake Hylia Central Island'), + ('Dark Desert Teleporter', 'Light World'), + ('East Dark World Teleporter', 'Light World'), + ('South Dark World Teleporter', 'Light World'), + ('West Dark World Teleporter', 'Light World'), + ('Dark Death Mountain Teleporter (West)', 'Death Mountain'), + ('Dark Death Mountain Teleporter (East)', 'East Death Mountain (Top)'), + ('Dark Death Mountain Teleporter (East Bottom)', 'East Death Mountain (Bottom)'), + ('Mire Mirror Spot', 'Dark Desert'), + ('Dark Desert Drop', 'Dark Desert'), + ('Desert Palace Stairs Mirror Spot', 'Dark Desert'), + ('Desert Palace North Mirror Spot', 'Dark Desert'), + ('Maze Race Mirror Spot', 'West Dark World'), + ('Lake Hylia Central Island Mirror Spot', 'Dark Lake Hylia Central Island'), + ('Hammer Peg Area Mirror Spot', 'Hammer Peg Area'), + ('Bumper Cave Ledge Mirror Spot', 'Bumper Cave Ledge'), + ('Bumper Cave Entrance Mirror Spot', 'Bumper Cave Entrance'), + ('Death Mountain Mirror Spot', 'Dark Death Mountain'), + ('East Death Mountain Mirror Spot (Top)', 'Dark Death Mountain'), + ('East Death Mountain Mirror Spot (Bottom)', 'Dark Death Mountain (East Bottom)'), + ('Death Mountain (Top) Mirror Spot', 'Dark Death Mountain'), + ('Dark Death Mountain Ledge Mirror Spot (East)', 'Dark Death Mountain Ledge'), + ('Dark Death Mountain Ledge Mirror Spot (West)', 'Dark Death Mountain Ledge'), + ('Floating Island Mirror Spot', 'Death Mountain Floating Island (Dark World)'), + ('Laser Bridge Mirror Spot', 'Dark Death Mountain Isolated Ledge'), + ('East Dark World Mirror Spot', 'East Dark World'), + ('West Dark World Mirror Spot', 'West Dark World'), + ('South Dark World Mirror Spot', 'South Dark World'), + ('Potion Shop Mirror Spot', 'Northeast Dark World'), + ('Catfish Mirror Spot', 'Catfish'), + ('Shopping Mall Mirror Spot', 'Dark Lake Hylia Ledge'), + ('Skull Woods Mirror Spot', 'Skull Woods Forest (West)'), + ('DDM Flute', 'The Sky'), + ('DDM Landing', 'Dark Death Mountain'), + ('NEDW Flute', 'The Sky'), + ('NEDW Landing', 'Northeast Dark World'), + ('WDW Flute', 'The Sky'), + ('WDW Landing', 'West Dark World'), + ('SDW Flute', 'The Sky'), + ('SDW Landing', 'South Dark World'), + ('EDW Flute', 'The Sky'), + ('EDW Landing', 'East Dark World'), + ('DLHL Flute', 'The Sky'), + ('DLHL Landing', 'Dark Lake Hylia Ledge'), + ('DD Flute', 'The Sky'), + ('DD Landing', 'Dark Desert Ledge'), + ('EDDM Flute', 'The Sky'), + ('Dark Grassy Lawn Flute', 'The Sky'), + ('Hammer Peg Area Flute', 'The Sky'), + ('Bush Covered Lawn Inner Bushes', 'Light World'), + ('Bush Covered Lawn Outer Bushes', 'Bush Covered Lawn'), + ('Bush Covered Lawn Mirror Spot', 'Dark Grassy Lawn'), + ('Bomb Hut Inner Bushes', 'Light World'), + ('Bomb Hut Outer Bushes', 'Bomb Hut Area'), + ('Bomb Hut Mirror Spot', 'West Dark World'), + ('Maze Race Ledge Drop', 'Light World')] + +# non-shuffled entrance links +default_connections = {'Links House': 'Links House', + 'Links House Exit': 'Light World', + 'Waterfall of Wishing': 'Waterfall of Wishing', + 'Blinds Hideout': 'Blinds Hideout', + 'Dam': 'Dam', + 'Lumberjack House': 'Lumberjack House', + 'Hyrule Castle Secret Entrance Drop': 'Hyrule Castle Secret Entrance', + 'Hyrule Castle Secret Entrance Stairs': 'Hyrule Castle Secret Entrance', + 'Hyrule Castle Secret Entrance Exit': 'Hyrule Castle Courtyard', + 'Bonk Fairy (Light)': 'Bonk Fairy (Light)', + 'Lake Hylia Fairy': 'Lake Hylia Healer Fairy', + 'Lake Hylia Fortune Teller': 'Lake Hylia Fortune Teller', + 'Light Hype Fairy': 'Swamp Healer Fairy', + 'Desert Fairy': 'Desert Healer Fairy', + 'Kings Grave': 'Kings Grave', + 'Tavern North': 'Tavern', + 'Chicken House': 'Chicken House', + 'Aginahs Cave': 'Aginahs Cave', + 'Sahasrahlas Hut': 'Sahasrahlas Hut', + 'Cave Shop (Lake Hylia)': 'Cave Shop (Lake Hylia)', + 'Capacity Upgrade': 'Capacity Upgrade', + 'Kakariko Well Drop': 'Kakariko Well (top)', + 'Kakariko Well Cave': 'Kakariko Well (bottom)', + 'Kakariko Well Exit': 'Light World', + 'Blacksmiths Hut': 'Blacksmiths Hut', + 'Bat Cave Drop': 'Bat Cave (right)', + 'Bat Cave Cave': 'Bat Cave (left)', + 'Bat Cave Exit': 'Light World', + 'Sick Kids House': 'Sick Kids House', + 'Elder House (East)': 'Elder House', + 'Elder House (West)': 'Elder House', + 'Elder House Exit (East)': 'Light World', + 'Elder House Exit (West)': 'Light World', + 'North Fairy Cave Drop': 'North Fairy Cave', + 'North Fairy Cave': 'North Fairy Cave', + 'North Fairy Cave Exit': 'Light World', + 'Lost Woods Gamble': 'Lost Woods Gamble', + 'Fortune Teller (Light)': 'Fortune Teller (Light)', + 'Snitch Lady (East)': 'Snitch Lady (East)', + 'Snitch Lady (West)': 'Snitch Lady (West)', + 'Bush Covered House': 'Bush Covered House', + 'Tavern (Front)': 'Tavern (Front)', + 'Light World Bomb Hut': 'Light World Bomb Hut', + 'Kakariko Shop': 'Kakariko Shop', + 'Lost Woods Hideout Drop': 'Lost Woods Hideout (top)', + 'Lost Woods Hideout Stump': 'Lost Woods Hideout (bottom)', + 'Lost Woods Hideout Exit': 'Light World', + 'Lumberjack Tree Tree': 'Lumberjack Tree (top)', + 'Lumberjack Tree Cave': 'Lumberjack Tree (bottom)', + 'Lumberjack Tree Exit': 'Light World', + 'Cave 45': 'Cave 45', + 'Graveyard Cave': 'Graveyard Cave', + 'Checkerboard Cave': 'Checkerboard Cave', + 'Mini Moldorm Cave': 'Mini Moldorm Cave', + 'Long Fairy Cave': 'Long Fairy Cave', # near East Light World Teleporter + 'Good Bee Cave': 'Good Bee Cave', + '20 Rupee Cave': '20 Rupee Cave', + '50 Rupee Cave': '50 Rupee Cave', + 'Ice Rod Cave': 'Ice Rod Cave', + 'Bonk Rock Cave': 'Bonk Rock Cave', + 'Library': 'Library', + 'Kakariko Gamble Game': 'Kakariko Gamble Game', + 'Potion Shop': 'Potion Shop', + 'Two Brothers House (East)': 'Two Brothers House', + 'Two Brothers House (West)': 'Two Brothers House', + 'Two Brothers House Exit (East)': 'Light World', + 'Two Brothers House Exit (West)': 'Maze Race Ledge', + + 'Sanctuary': 'Sanctuary Portal', + 'Sanctuary Grave': 'Sewer Drop', + 'Sanctuary Exit': 'Light World', + + 'Old Man Cave (West)': 'Old Man Cave Ledge', + 'Old Man Cave (East)': 'Old Man Cave', + 'Old Man Cave Exit (West)': 'Light World', + 'Old Man Cave Exit (East)': 'Death Mountain', + 'Old Man House (Bottom)': 'Old Man House', + 'Old Man House Exit (Bottom)': 'Death Mountain', + 'Old Man House (Top)': 'Old Man House Back', + 'Old Man House Exit (Top)': 'Death Mountain', + 'Death Mountain Return Cave (East)': 'Death Mountain Return Cave (right)', + 'Death Mountain Return Cave (West)': 'Death Mountain Return Cave (left)', + 'Death Mountain Return Cave Exit (West)': 'Death Mountain Return Ledge', + 'Death Mountain Return Cave Exit (East)': 'Death Mountain', + 'Spectacle Rock Cave Peak': 'Spectacle Rock Cave (Peak)', + 'Spectacle Rock Cave (Bottom)': 'Spectacle Rock Cave (Bottom)', + 'Spectacle Rock Cave': 'Spectacle Rock Cave (Top)', + 'Spectacle Rock Cave Exit': 'Death Mountain', + 'Spectacle Rock Cave Exit (Top)': 'Death Mountain', + 'Spectacle Rock Cave Exit (Peak)': 'Death Mountain', + 'Paradox Cave (Bottom)': 'Paradox Cave Front', + 'Paradox Cave (Middle)': 'Paradox Cave', + 'Paradox Cave (Top)': 'Paradox Cave', + 'Paradox Cave Exit (Bottom)': 'East Death Mountain (Bottom)', + 'Paradox Cave Exit (Middle)': 'East Death Mountain (Bottom)', + 'Paradox Cave Exit (Top)': 'East Death Mountain (Top)', + 'Hookshot Fairy': 'Hookshot Fairy', + 'Fairy Ascension Cave (Bottom)': 'Fairy Ascension Cave (Bottom)', + 'Fairy Ascension Cave (Top)': 'Fairy Ascension Cave (Top)', + 'Fairy Ascension Cave Exit (Bottom)': 'Fairy Ascension Plateau', + 'Fairy Ascension Cave Exit (Top)': 'Fairy Ascension Ledge', + 'Spiral Cave': 'Spiral Cave (Top)', + 'Spiral Cave (Bottom)': 'Spiral Cave (Bottom)', + 'Spiral Cave Exit': 'East Death Mountain (Bottom)', + 'Spiral Cave Exit (Top)': 'Spiral Cave Ledge', + + 'Pyramid Fairy': 'Pyramid Fairy', + 'East Dark World Hint': 'East Dark World Hint', + 'Palace of Darkness Hint': 'Palace of Darkness Hint', + 'Big Bomb Shop': 'Big Bomb Shop', + 'Dark Lake Hylia Shop': 'Dark Lake Hylia Shop', + 'Dark Lake Hylia Fairy': 'Dark Lake Hylia Healer Fairy', + 'Dark Lake Hylia Ledge Fairy': 'Dark Lake Hylia Ledge Healer Fairy', + 'Dark Lake Hylia Ledge Spike Cave': 'Dark Lake Hylia Ledge Spike Cave', + 'Dark Lake Hylia Ledge Hint': 'Dark Lake Hylia Ledge Hint', + 'Hype Cave': 'Hype Cave', + 'Bonk Fairy (Dark)': 'Bonk Fairy (Dark)', + 'Brewery': 'Brewery', + 'C-Shaped House': 'C-Shaped House', + 'Chest Game': 'Chest Game', + 'Dark World Hammer Peg Cave': 'Dark World Hammer Peg Cave', + 'Bumper Cave (Bottom)': 'Bumper Cave', + 'Bumper Cave (Top)': 'Bumper Cave', + 'Red Shield Shop': 'Red Shield Shop', + 'Dark Sanctuary Hint': 'Dark Sanctuary Hint', + 'Fortune Teller (Dark)': 'Fortune Teller (Dark)', + 'Dark World Shop': 'Village of Outcasts Shop', + 'Dark World Lumberjack Shop': 'Dark World Lumberjack Shop', + 'Dark World Potion Shop': 'Dark World Potion Shop', + 'Archery Game': 'Archery Game', + 'Bumper Cave Exit (Top)': 'Bumper Cave Ledge', + 'Bumper Cave Exit (Bottom)': 'West Dark World', + 'Mire Shed': 'Mire Shed', + 'Dark Desert Hint': 'Dark Desert Hint', + 'Dark Desert Fairy': 'Dark Desert Healer Fairy', + 'Spike Cave': 'Spike Cave', + 'Hookshot Cave': 'Hookshot Cave (Front)', + 'Superbunny Cave (Top)': 'Superbunny Cave (Top)', + 'Cave Shop (Dark Death Mountain)': 'Cave Shop (Dark Death Mountain)', + 'Dark Death Mountain Fairy': 'Dark Death Mountain Healer Fairy', + 'Superbunny Cave (Bottom)': 'Superbunny Cave (Bottom)', + 'Superbunny Cave Exit (Top)': 'Dark Death Mountain (Top)', + 'Superbunny Cave Exit (Bottom)': 'Dark Death Mountain (East Bottom)', + 'Hookshot Cave Front Exit': 'Dark Death Mountain (Top)', + 'Hookshot Cave Back Exit': 'Death Mountain Floating Island (Dark World)', + 'Hookshot Cave Back Entrance': 'Hookshot Cave (Back)', + 'Mimic Cave': 'Mimic Cave', + + 'Pyramid Hole': 'Pyramid', + 'Pyramid Exit': 'Pyramid Ledge', + 'Pyramid Entrance': 'Bottom of Pyramid'} + +inverted_default_connections = {'Waterfall of Wishing': 'Waterfall of Wishing', + 'Blinds Hideout': 'Blinds Hideout', + 'Dam': 'Dam', + 'Lumberjack House': 'Lumberjack House', + 'Hyrule Castle Secret Entrance Drop': 'Hyrule Castle Secret Entrance', + 'Hyrule Castle Secret Entrance Stairs': 'Hyrule Castle Secret Entrance', + 'Hyrule Castle Secret Entrance Exit': 'Light World', + 'Bonk Fairy (Light)': 'Bonk Fairy (Light)', + 'Lake Hylia Fairy': 'Lake Hylia Healer Fairy', + 'Lake Hylia Fortune Teller': 'Lake Hylia Fortune Teller', + 'Light Hype Fairy': 'Swamp Healer Fairy', + 'Desert Fairy': 'Desert Healer Fairy', + 'Kings Grave': 'Kings Grave', + 'Tavern North': 'Tavern', + 'Chicken House': 'Chicken House', + 'Aginahs Cave': 'Aginahs Cave', + 'Sahasrahlas Hut': 'Sahasrahlas Hut', + 'Cave Shop (Lake Hylia)': 'Cave Shop (Lake Hylia)', + 'Capacity Upgrade': 'Capacity Upgrade', + 'Kakariko Well Drop': 'Kakariko Well (top)', + 'Kakariko Well Cave': 'Kakariko Well (bottom)', + 'Kakariko Well Exit': 'Light World', + 'Blacksmiths Hut': 'Blacksmiths Hut', + 'Bat Cave Drop': 'Bat Cave (right)', + 'Bat Cave Cave': 'Bat Cave (left)', + 'Bat Cave Exit': 'Light World', + 'Sick Kids House': 'Sick Kids House', + 'Elder House (East)': 'Elder House', + 'Elder House (West)': 'Elder House', + 'Elder House Exit (East)': 'Light World', + 'Elder House Exit (West)': 'Light World', + 'North Fairy Cave Drop': 'North Fairy Cave', + 'North Fairy Cave': 'North Fairy Cave', + 'North Fairy Cave Exit': 'Light World', + 'Lost Woods Gamble': 'Lost Woods Gamble', + 'Fortune Teller (Light)': 'Fortune Teller (Light)', + 'Snitch Lady (East)': 'Snitch Lady (East)', + 'Snitch Lady (West)': 'Snitch Lady (West)', + 'Bush Covered House': 'Bush Covered House', + 'Tavern (Front)': 'Tavern (Front)', + 'Light World Bomb Hut': 'Light World Bomb Hut', + 'Kakariko Shop': 'Kakariko Shop', + 'Lost Woods Hideout Drop': 'Lost Woods Hideout (top)', + 'Lost Woods Hideout Stump': 'Lost Woods Hideout (bottom)', + 'Lost Woods Hideout Exit': 'Light World', + 'Lumberjack Tree Tree': 'Lumberjack Tree (top)', + 'Lumberjack Tree Cave': 'Lumberjack Tree (bottom)', + 'Lumberjack Tree Exit': 'Light World', + 'Cave 45': 'Cave 45', + 'Graveyard Cave': 'Graveyard Cave', + 'Checkerboard Cave': 'Checkerboard Cave', + 'Mini Moldorm Cave': 'Mini Moldorm Cave', + 'Long Fairy Cave': 'Long Fairy Cave', + 'Good Bee Cave': 'Good Bee Cave', + '20 Rupee Cave': '20 Rupee Cave', + '50 Rupee Cave': '50 Rupee Cave', + 'Ice Rod Cave': 'Ice Rod Cave', + 'Bonk Rock Cave': 'Bonk Rock Cave', + 'Library': 'Library', + 'Kakariko Gamble Game': 'Kakariko Gamble Game', + 'Potion Shop': 'Potion Shop', + 'Two Brothers House (East)': 'Two Brothers House', + 'Two Brothers House (West)': 'Two Brothers House', + 'Two Brothers House Exit (East)': 'Light World', + 'Two Brothers House Exit (West)': 'Maze Race Ledge', + 'Sanctuary': 'Sanctuary Portal', + 'Sanctuary Grave': 'Sewer Drop', + 'Sanctuary Exit': 'Light World', + 'Old Man House (Bottom)': 'Old Man House', + 'Old Man House Exit (Bottom)': 'Death Mountain', + 'Old Man House (Top)': 'Old Man House Back', + 'Old Man House Exit (Top)': 'Death Mountain', + 'Spectacle Rock Cave Peak': 'Spectacle Rock Cave (Peak)', + 'Spectacle Rock Cave (Bottom)': 'Spectacle Rock Cave (Bottom)', + 'Spectacle Rock Cave': 'Spectacle Rock Cave (Top)', + 'Spectacle Rock Cave Exit': 'Death Mountain', + 'Spectacle Rock Cave Exit (Top)': 'Death Mountain', + 'Spectacle Rock Cave Exit (Peak)': 'Death Mountain', + 'Paradox Cave (Bottom)': 'Paradox Cave Front', + 'Paradox Cave (Middle)': 'Paradox Cave', + 'Paradox Cave (Top)': 'Paradox Cave', + 'Paradox Cave Exit (Bottom)': 'East Death Mountain (Bottom)', + 'Paradox Cave Exit (Middle)': 'East Death Mountain (Bottom)', + 'Paradox Cave Exit (Top)': 'East Death Mountain (Top)', + 'Hookshot Fairy': 'Hookshot Fairy', + 'Fairy Ascension Cave (Bottom)': 'Fairy Ascension Cave (Bottom)', + 'Fairy Ascension Cave (Top)': 'Fairy Ascension Cave (Top)', + 'Fairy Ascension Cave Exit (Bottom)': 'Fairy Ascension Plateau', + 'Fairy Ascension Cave Exit (Top)': 'Fairy Ascension Ledge', + 'Spiral Cave': 'Spiral Cave (Top)', + 'Spiral Cave (Bottom)': 'Spiral Cave (Bottom)', + 'Spiral Cave Exit': 'East Death Mountain (Bottom)', + 'Spiral Cave Exit (Top)': 'Spiral Cave Ledge', + 'Pyramid Fairy': 'Pyramid Fairy', + 'East Dark World Hint': 'East Dark World Hint', + 'Palace of Darkness Hint': 'Palace of Darkness Hint', + 'Dark Lake Hylia Shop': 'Dark Lake Hylia Shop', + 'Dark Lake Hylia Fairy': 'Dark Lake Hylia Healer Fairy', + 'Dark Lake Hylia Ledge Fairy': 'Dark Lake Hylia Ledge Healer Fairy', + 'Dark Lake Hylia Ledge Spike Cave': 'Dark Lake Hylia Ledge Spike Cave', + 'Dark Lake Hylia Ledge Hint': 'Dark Lake Hylia Ledge Hint', + 'Hype Cave': 'Hype Cave', + 'Bonk Fairy (Dark)': 'Bonk Fairy (Dark)', + 'Brewery': 'Brewery', + 'C-Shaped House': 'C-Shaped House', + 'Chest Game': 'Chest Game', + 'Dark World Hammer Peg Cave': 'Dark World Hammer Peg Cave', + 'Red Shield Shop': 'Red Shield Shop', + 'Fortune Teller (Dark)': 'Fortune Teller (Dark)', + 'Dark World Shop': 'Village of Outcasts Shop', + 'Dark World Lumberjack Shop': 'Dark World Lumberjack Shop', + 'Dark World Potion Shop': 'Dark World Potion Shop', + 'Archery Game': 'Archery Game', + 'Mire Shed': 'Mire Shed', + 'Dark Desert Hint': 'Dark Desert Hint', + 'Dark Desert Fairy': 'Dark Desert Healer Fairy', + 'Spike Cave': 'Spike Cave', + 'Hookshot Cave': 'Hookshot Cave (Front)', + 'Superbunny Cave (Top)': 'Superbunny Cave (Top)', + 'Cave Shop (Dark Death Mountain)': 'Cave Shop (Dark Death Mountain)', + 'Superbunny Cave (Bottom)': 'Superbunny Cave (Bottom)', + 'Superbunny Cave Exit (Bottom)': 'Dark Death Mountain (East Bottom)', + 'Hookshot Cave Back Exit': 'Death Mountain Floating Island (Dark World)', + 'Hookshot Cave Back Entrance': 'Hookshot Cave (Back)', + 'Mimic Cave': 'Mimic Cave', + 'Inverted Pyramid Hole': 'Pyramid', + 'Inverted Links House': 'Inverted Links House', + 'Inverted Links House Exit': 'South Dark World', + 'Inverted Big Bomb Shop': 'Inverted Big Bomb Shop', + 'Inverted Dark Sanctuary': 'Inverted Dark Sanctuary', + 'Inverted Dark Sanctuary Exit': 'West Dark World', + 'Old Man Cave (West)': 'Bumper Cave', + 'Old Man Cave (East)': 'Death Mountain Return Cave (left)', + 'Old Man Cave Exit (West)': 'West Dark World', + 'Old Man Cave Exit (East)': 'Dark Death Mountain', + 'Dark Death Mountain Fairy': 'Old Man Cave', + 'Bumper Cave (Bottom)': 'Old Man Cave Ledge', + 'Bumper Cave (Top)': 'Dark Death Mountain Healer Fairy', + 'Bumper Cave Exit (Top)': 'Death Mountain Return Ledge', + 'Bumper Cave Exit (Bottom)': 'Light World', + 'Death Mountain Return Cave (West)': 'Bumper Cave', + 'Death Mountain Return Cave (East)': 'Death Mountain Return Cave (right)', + 'Death Mountain Return Cave Exit (West)': 'Death Mountain', + 'Death Mountain Return Cave Exit (East)': 'Death Mountain', + 'Hookshot Cave Front Exit': 'Dark Death Mountain', + 'Superbunny Cave Exit (Top)': 'Dark Death Mountain', + 'Pyramid Exit': 'Light World', + 'Inverted Pyramid Entrance': 'Bottom of Pyramid'} + +# non shuffled dungeons +default_dungeon_connections = [('Desert Palace Entrance (South)', 'Desert South Portal'), + ('Desert Palace Entrance (West)', 'Desert West Portal'), + ('Desert Palace Entrance (North)', 'Desert Back Portal'), + ('Desert Palace Entrance (East)', 'Desert East Portal'), + ('Desert Palace Exit (South)', 'Desert Palace Stairs'), + ('Desert Palace Exit (West)', 'Desert Ledge'), + ('Desert Palace Exit (East)', 'Desert Palace Lone Stairs'), + ('Desert Palace Exit (North)', 'Desert Palace Entrance (North) Spot'), + + ('Eastern Palace', 'Eastern Portal'), + ('Eastern Palace Exit', 'Light World'), + ('Tower of Hera', 'Hera Portal'), + ('Tower of Hera Exit', 'Death Mountain (Top)'), + + ('Hyrule Castle Entrance (South)', 'Hyrule Castle South Portal'), + ('Hyrule Castle Entrance (West)', 'Hyrule Castle West Portal'), + ('Hyrule Castle Entrance (East)', 'Hyrule Castle East Portal'), + ('Hyrule Castle Exit (South)', 'Hyrule Castle Courtyard'), + ('Hyrule Castle Exit (West)', 'Hyrule Castle Ledge'), + ('Hyrule Castle Exit (East)', 'Hyrule Castle Ledge'), + ('Agahnims Tower', 'Agahnims Tower Portal'), + ('Agahnims Tower Exit', 'Hyrule Castle Ledge'), + + ('Thieves Town', 'Thieves Town Portal'), + ('Thieves Town Exit', 'West Dark World'), + ('Skull Woods First Section Hole (East)', 'Skull Pinball'), + ('Skull Woods First Section Hole (West)', 'Skull Left Drop'), + ('Skull Woods First Section Hole (North)', 'Skull Pot Circle'), + ('Skull Woods First Section Door', 'Skull 1 Portal'), + ('Skull Woods First Section Exit', 'Skull Woods Forest'), + ('Skull Woods Second Section Hole', 'Skull Back Drop'), + ('Skull Woods Second Section Door (East)', 'Skull 2 East Portal'), + ('Skull Woods Second Section Door (West)', 'Skull 2 West Portal'), + ('Skull Woods Second Section Exit (East)', 'Skull Woods Forest'), + ('Skull Woods Second Section Exit (West)', 'Skull Woods Forest (West)'), + ('Skull Woods Final Section', 'Skull 3 Portal'), + ('Skull Woods Final Section Exit', 'Skull Woods Forest (West)'), + ('Ice Palace', 'Ice Portal'), + ('Ice Palace Exit', 'Dark Lake Hylia Central Island'), + ('Misery Mire', 'Mire Portal'), + ('Misery Mire Exit', 'Dark Desert'), + ('Palace of Darkness', 'Palace of Darkness Portal'), + ('Palace of Darkness Exit', 'East Dark World'), + ('Swamp Palace', 'Swamp Portal'), # requires additional patch for flooding moat if moved + ('Swamp Palace Exit', 'South Dark World'), + + ('Turtle Rock', 'Turtle Rock Main Portal'), + ('Turtle Rock Exit (Front)', 'Dark Death Mountain (Top)'), + ('Turtle Rock Ledge Exit (West)', 'Dark Death Mountain Ledge'), + ('Turtle Rock Ledge Exit (East)', 'Dark Death Mountain Ledge'), + ('Dark Death Mountain Ledge (West)', 'Turtle Rock Lazy Eyes Portal'), + ('Dark Death Mountain Ledge (East)', 'Turtle Rock Chest Portal'), + ('Turtle Rock Isolated Ledge Exit', 'Dark Death Mountain Isolated Ledge'), + ('Turtle Rock Isolated Ledge Entrance', 'Turtle Rock Eye Bridge Portal'), + + ('Ganons Tower', 'Ganons Tower Portal'), + ('Ganons Tower Exit', 'Dark Death Mountain (Top)') + ] + +inverted_default_dungeon_connections = [('Desert Palace Entrance (South)', 'Desert South Portal'), + ('Desert Palace Entrance (West)', 'Desert West Portal'), + ('Desert Palace Entrance (North)', 'Desert Back Portal'), + ('Desert Palace Entrance (East)', 'Desert East Portal'), + ('Desert Palace Exit (South)', 'Desert Palace Stairs'), + ('Desert Palace Exit (West)', 'Desert Ledge'), + ('Desert Palace Exit (East)', 'Desert Palace Lone Stairs'), + ('Desert Palace Exit (North)', 'Desert Palace Entrance (North) Spot'), + ('Eastern Palace', 'Eastern Portal'), + ('Eastern Palace Exit', 'Light World'), + ('Tower of Hera', 'Hera Portal'), + ('Tower of Hera Exit', 'Death Mountain (Top)'), + ('Hyrule Castle Entrance (South)', 'Hyrule Castle South Portal'), + ('Hyrule Castle Entrance (West)', 'Hyrule Castle West Portal'), + ('Hyrule Castle Entrance (East)', 'Hyrule Castle East Portal'), + ('Hyrule Castle Exit (South)', 'Light World'), + ('Hyrule Castle Exit (West)', 'Hyrule Castle Ledge'), + ('Hyrule Castle Exit (East)', 'Hyrule Castle Ledge'), + ('Thieves Town', 'Thieves Town Portal'), + ('Thieves Town Exit', 'West Dark World'), + ('Skull Woods First Section Hole (East)', 'Skull Pinball'), + ('Skull Woods First Section Hole (West)', 'Skull Left Drop'), + ('Skull Woods First Section Hole (North)', 'Skull Pot Circle'), + ('Skull Woods First Section Door', 'Skull 1 Portal'), + ('Skull Woods First Section Exit', 'Skull Woods Forest'), + ('Skull Woods Second Section Hole', 'Skull Back Drop'), + ('Skull Woods Second Section Door (East)', 'Skull 2 East Portal'), + ('Skull Woods Second Section Door (West)', 'Skull 2 West Portal'), + ('Skull Woods Second Section Exit (East)', 'Skull Woods Forest'), + ('Skull Woods Second Section Exit (West)', 'Skull Woods Forest (West)'), + ('Skull Woods Final Section', 'Skull 3 Portal'), + ('Skull Woods Final Section Exit', 'Skull Woods Forest (West)'), + ('Ice Palace', 'Ice Portal'), + ('Misery Mire', 'Mire Portal'), + ('Misery Mire Exit', 'Dark Desert'), + ('Palace of Darkness', 'Palace of Darkness Portal'), + ('Palace of Darkness Exit', 'East Dark World'), + # requires additional patch for flooding moat if moved + ('Swamp Palace', 'Swamp Portal'), + ('Swamp Palace Exit', 'South Dark World'), + ('Turtle Rock', 'Turtle Rock Main Portal'), + ('Turtle Rock Ledge Exit (West)', 'Dark Death Mountain Ledge'), + ('Turtle Rock Ledge Exit (East)', 'Dark Death Mountain Ledge'), + ('Dark Death Mountain Ledge (West)', 'Turtle Rock Lazy Eyes Portal'), + ('Dark Death Mountain Ledge (East)', 'Turtle Rock Chest Portal'), + ('Turtle Rock Isolated Ledge Exit', 'Dark Death Mountain Isolated Ledge'), + ('Turtle Rock Isolated Ledge Entrance', 'Turtle Rock Eye Bridge Portal'), + ('Inverted Ganons Tower', 'Ganons Tower Portal'), + ('Inverted Ganons Tower Exit', 'Hyrule Castle Ledge'), + ('Inverted Agahnims Tower', 'Agahnims Tower Portal'), + ('Inverted Agahnims Tower Exit', 'Dark Death Mountain'), + ('Turtle Rock Exit (Front)', 'Dark Death Mountain'), + ('Ice Palace Exit', 'Dark Lake Hylia') + ] + +indirect_connections = { + 'Turtle Rock (Top)': 'Turtle Rock', + 'East Dark World': 'Pyramid Fairy', + 'Big Bomb Shop': 'Pyramid Fairy', + 'Dark Desert': 'Pyramid Fairy', + 'West Dark World': 'Pyramid Fairy', + 'South Dark World': 'Pyramid Fairy', + 'Light World': 'Pyramid Fairy', + 'Old Man Cave': 'Old Man S&Q' +} +# format: +# Key=Name +# addr = (door_index, exitdata) # multiexit +# | ([addr], None) # holes +# exitdata = (room_id, ow_area, vram_loc, scroll_y, scroll_x, link_y, link_x, camera_y, camera_x, unknown_1, unknown_2, door_1, door_2) + +# ToDo somehow merge this with creation of the locations +door_addresses = {'Links House': (0x00, (0x0104, 0x2c, 0x0506, 0x0a9a, 0x0832, 0x0ae8, 0x08b8, 0x0b07, 0x08bf, 0x06, 0xfe, 0x0816, 0x0000)), + 'Inverted Big Bomb Shop': (0x00, (0x0104, 0x2c, 0x0506, 0x0a9a, 0x0832, 0x0ae8, 0x08b8, 0x0b07, 0x08bf, 0x06, 0xfe, 0x0816, 0x0000)), + 'Desert Palace Entrance (South)': (0x08, (0x0084, 0x30, 0x0314, 0x0c56, 0x00a6, 0x0ca8, 0x0128, 0x0cc3, 0x0133, 0x0a, 0xfa, 0x0000, 0x0000)), + 'Desert Palace Entrance (West)': (0x0A, (0x0083, 0x30, 0x0280, 0x0c46, 0x0003, 0x0c98, 0x0088, 0x0cb3, 0x0090, 0x0a, 0xfd, 0x0000, 0x0000)), + 'Desert Palace Entrance (North)': (0x0B, (0x0063, 0x30, 0x0016, 0x0c00, 0x00a2, 0x0c28, 0x0128, 0x0c6d, 0x012f, 0x00, 0x0e, 0x0000, 0x0000)), + 'Desert Palace Entrance (East)': (0x09, (0x0085, 0x30, 0x02a8, 0x0c4a, 0x0142, 0x0c98, 0x01c8, 0x0cb7, 0x01cf, 0x06, 0xfe, 0x0000, 0x0000)), + 'Eastern Palace': (0x07, (0x00c9, 0x1e, 0x005a, 0x0600, 0x0ed6, 0x0618, 0x0f50, 0x066d, 0x0f5b, 0x00, 0xfa, 0x0000, 0x0000)), + 'Tower of Hera': (0x32, (0x0077, 0x03, 0x0050, 0x0014, 0x087c, 0x0068, 0x08f0, 0x0083, 0x08fb, 0x0a, 0xf4, 0x0000, 0x0000)), + 'Hyrule Castle Entrance (South)': (0x03, (0x0061, 0x1b, 0x0530, 0x0692, 0x0784, 0x06cc, 0x07f8, 0x06ff, 0x0803, 0x0e, 0xfa, 0x0000, 0x87be)), + 'Hyrule Castle Entrance (West)': (0x02, (0x0060, 0x1b, 0x0016, 0x0600, 0x06ae, 0x0604, 0x0728, 0x066d, 0x0733, 0x00, 0x02, 0x0000, 0x8124)), + 'Hyrule Castle Entrance (East)': (0x04, (0x0062, 0x1b, 0x004a, 0x0600, 0x0856, 0x0604, 0x08c8, 0x066d, 0x08d3, 0x00, 0xfa, 0x0000, 0x8158)), + 'Inverted Pyramid Entrance': (0x35, (0x0010, 0x1b, 0x0418, 0x0679, 0x06b4, 0x06c6, 0x0728, 0x06e6, 0x0733, 0x07, 0xf9, 0x0000, 0x0000)), + 'Agahnims Tower': (0x23, (0x00e0, 0x1b, 0x0032, 0x0600, 0x0784, 0x0634, 0x07f8, 0x066d, 0x0803, 0x00, 0x0a, 0x0000, 0x82be)), + 'Inverted Ganons Tower': (0x23, (0x00e0, 0x1b, 0x0032, 0x0600, 0x0784, 0x0634, 0x07f8, 0x066d, 0x0803, 0x00, 0x0a, 0x0000, 0x82be)), + 'Thieves Town': (0x33, (0x00db, 0x58, 0x0b2e, 0x075a, 0x0176, 0x07a8, 0x01f8, 0x07c7, 0x0203, 0x06, 0xfa, 0x0000, 0x0000)), + 'Skull Woods First Section Door': (0x29, (0x0058, 0x40, 0x0f4c, 0x01f6, 0x0262, 0x0248, 0x02e8, 0x0263, 0x02ef, 0x0a, 0xfe, 0x0000, 0x0000)), + 'Skull Woods Second Section Door (East)': (0x28, (0x0057, 0x40, 0x0eb8, 0x01e6, 0x01c2, 0x0238, 0x0248, 0x0253, 0x024f, 0x0a, 0xfe, 0x0000, 0x0000)), + 'Skull Woods Second Section Door (West)': (0x27, (0x0056, 0x40, 0x0c8e, 0x01a6, 0x0062, 0x01f8, 0x00e8, 0x0213, 0x00ef, 0x0a, 0x0e, 0x0000, 0x0000)), + 'Skull Woods Final Section': (0x2A, (0x0059, 0x40, 0x0282, 0x0066, 0x0016, 0x00b8, 0x0098, 0x00d3, 0x00a3, 0x0a, 0xfa, 0x0000, 0x0000)), + 'Ice Palace': (0x2C, (0x000e, 0x75, 0x0bc6, 0x0d6a, 0x0c3e, 0x0db8, 0x0cb8, 0x0dd7, 0x0cc3, 0x06, 0xf2, 0x0000, 0x0000)), + 'Misery Mire': (0x26, (0x0098, 0x70, 0x0414, 0x0c79, 0x00a6, 0x0cc7, 0x0128, 0x0ce6, 0x0133, 0x07, 0xfa, 0x0000, 0x0000)), + 'Palace of Darkness': (0x25, (0x004a, 0x5e, 0x005a, 0x0600, 0x0ed6, 0x0628, 0x0f50, 0x066d, 0x0f5b, 0x00, 0xfa, 0x0000, 0x0000)), + 'Swamp Palace': (0x24, (0x0028, 0x7b, 0x049e, 0x0e8c, 0x06f2, 0x0ed8, 0x0778, 0x0ef9, 0x077f, 0x04, 0xfe, 0x0000, 0x0000)), + 'Turtle Rock': (0x34, (0x00d6, 0x47, 0x0712, 0x00da, 0x0e96, 0x0128, 0x0f08, 0x0147, 0x0f13, 0x06, 0xfa, 0x0000, 0x0000)), + 'Dark Death Mountain Ledge (West)': (0x14, (0x0023, 0x45, 0x07ca, 0x0103, 0x0c46, 0x0157, 0x0cb8, 0x0172, 0x0cc3, 0x0b, 0x0a, 0x0000, 0x0000)), + 'Dark Death Mountain Ledge (East)': (0x18, (0x0024, 0x45, 0x07e0, 0x0103, 0x0d00, 0x0157, 0x0d78, 0x0172, 0x0d7d, 0x0b, 0x00, 0x0000, 0x0000)), + 'Turtle Rock Isolated Ledge Entrance': (0x17, (0x00d5, 0x45, 0x0ad4, 0x0164, 0x0ca6, 0x01b8, 0x0d18, 0x01d3, 0x0d23, 0x0a, 0xfa, 0x0000, 0x0000)), + 'Hyrule Castle Secret Entrance Stairs': (0x31, (0x0055, 0x1b, 0x044a, 0x067a, 0x0854, 0x06c8, 0x08c8, 0x06e7, 0x08d3, 0x06, 0xfa, 0x0000, 0x0000)), + 'Kakariko Well Cave': (0x38, (0x002f, 0x18, 0x0386, 0x0665, 0x0032, 0x06b7, 0x00b8, 0x06d2, 0x00bf, 0x0b, 0xfe, 0x0000, 0x0000)), + 'Bat Cave Cave': (0x10, (0x00e3, 0x22, 0x0412, 0x087a, 0x048e, 0x08c8, 0x0508, 0x08e7, 0x0513, 0x06, 0x02, 0x0000, 0x0000)), + 'Elder House (East)': (0x0D, (0x00f3, 0x18, 0x02c4, 0x064a, 0x0222, 0x0698, 0x02a8, 0x06b7, 0x02af, 0x06, 0xfe, 0x05d4, 0x0000)), + 'Elder House (West)': (0x0C, (0x00f2, 0x18, 0x02bc, 0x064c, 0x01e2, 0x0698, 0x0268, 0x06b9, 0x026f, 0x04, 0xfe, 0x05cc, 0x0000)), + 'North Fairy Cave': (0x37, (0x0008, 0x15, 0x0088, 0x0400, 0x0a36, 0x0448, 0x0aa8, 0x046f, 0x0ab3, 0x00, 0x0a, 0x0000, 0x0000)), + 'Lost Woods Hideout Stump': (0x2B, (0x00e1, 0x00, 0x0f4e, 0x01f6, 0x0262, 0x0248, 0x02e8, 0x0263, 0x02ef, 0x0a, 0x0e, 0x0000, 0x0000)), + 'Lumberjack Tree Cave': (0x11, (0x00e2, 0x02, 0x0118, 0x0015, 0x04c6, 0x0067, 0x0548, 0x0082, 0x0553, 0x0b, 0xfa, 0x0000, 0x0000)), + 'Two Brothers House (East)': (0x0F, (0x00f5, 0x29, 0x0880, 0x0b07, 0x0200, 0x0b58, 0x0238, 0x0b74, 0x028d, 0x09, 0x00, 0x0b86, 0x0000)), + 'Two Brothers House (West)': (0x0E, (0x00f4, 0x28, 0x08a0, 0x0b06, 0x0100, 0x0b58, 0x01b8, 0x0b73, 0x018d, 0x0a, 0x00, 0x0bb6, 0x0000)), + 'Sanctuary': (0x01, (0x0012, 0x13, 0x001c, 0x0400, 0x06de, 0x0414, 0x0758, 0x046d, 0x0763, 0x00, 0x02, 0x0000, 0x01aa)), + 'Old Man Cave (West)': (0x05, (0x00f0, 0x0a, 0x03a0, 0x0264, 0x0500, 0x02b8, 0x05a8, 0x02d3, 0x058d, 0x0a, 0x00, 0x0000, 0x0000)), + 'Old Man Cave (East)': (0x06, (0x00f1, 0x03, 0x1402, 0x0294, 0x0604, 0x02e8, 0x0678, 0x0303, 0x0683, 0x0a, 0xfc, 0x0000, 0x0000)), + 'Old Man House (Bottom)': (0x2F, (0x00e4, 0x03, 0x181a, 0x031e, 0x06b4, 0x03a7, 0x0728, 0x038d, 0x0733, 0x00, 0x0c, 0x0000, 0x0000)), + 'Old Man House (Top)': (0x30, (0x00e5, 0x03, 0x10c6, 0x0224, 0x0814, 0x0278, 0x0888, 0x0293, 0x0893, 0x0a, 0x0c, 0x0000, 0x0000)), + 'Death Mountain Return Cave (East)': (0x2E, (0x00e7, 0x03, 0x0d82, 0x01c4, 0x0600, 0x0218, 0x0648, 0x0233, 0x067f, 0x0a, 0x00, 0x0000, 0x0000)), + 'Death Mountain Return Cave (West)': (0x2D, (0x00e6, 0x0a, 0x00a0, 0x0205, 0x0500, 0x0257, 0x05b8, 0x0272, 0x058d, 0x0b, 0x00, 0x0000, 0x0000)), + 'Spectacle Rock Cave Peak': (0x22, (0x00ea, 0x03, 0x092c, 0x0133, 0x0754, 0x0187, 0x07c8, 0x01a2, 0x07d3, 0x0b, 0xfc, 0x0000, 0x0000)), + 'Spectacle Rock Cave': (0x21, (0x00fa, 0x03, 0x0eac, 0x01e3, 0x0754, 0x0237, 0x07c8, 0x0252, 0x07d3, 0x0b, 0xfc, 0x0000, 0x0000)), + 'Spectacle Rock Cave (Bottom)': (0x20, (0x00f9, 0x03, 0x0d9c, 0x01c3, 0x06d4, 0x0217, 0x0748, 0x0232, 0x0753, 0x0b, 0xfc, 0x0000, 0x0000)), + 'Paradox Cave (Bottom)': (0x1D, (0x00ff, 0x05, 0x0ee0, 0x01e3, 0x0d00, 0x0237, 0x0da8, 0x0252, 0x0d7d, 0x0b, 0x00, 0x0000, 0x0000)), + 'Paradox Cave (Middle)': (0x1E, (0x00ef, 0x05, 0x17e0, 0x0304, 0x0d00, 0x0358, 0x0dc8, 0x0373, 0x0d7d, 0x0a, 0x00, 0x0000, 0x0000)), + 'Paradox Cave (Top)': (0x1F, (0x00df, 0x05, 0x0460, 0x0093, 0x0d00, 0x00e7, 0x0db8, 0x0102, 0x0d7d, 0x0b, 0x00, 0x0000, 0x0000)), + 'Fairy Ascension Cave (Bottom)': (0x19, (0x00fd, 0x05, 0x0dd4, 0x01c4, 0x0ca6, 0x0218, 0x0d18, 0x0233, 0x0d23, 0x0a, 0xfa, 0x0000, 0x0000)), + 'Fairy Ascension Cave (Top)': (0x1A, (0x00ed, 0x05, 0x0ad4, 0x0163, 0x0ca6, 0x01b7, 0x0d18, 0x01d2, 0x0d23, 0x0b, 0xfa, 0x0000, 0x0000)), + 'Spiral Cave': (0x1C, (0x00ee, 0x05, 0x07c8, 0x0108, 0x0c46, 0x0158, 0x0cb8, 0x0177, 0x0cc3, 0x06, 0xfa, 0x0000, 0x0000)), + 'Spiral Cave (Bottom)': (0x1B, (0x00fe, 0x05, 0x0cca, 0x01a3, 0x0c56, 0x01f7, 0x0cc8, 0x0212, 0x0cd3, 0x0b, 0xfa, 0x0000, 0x0000)), + 'Bumper Cave (Bottom)': (0x15, (0x00fb, 0x4a, 0x03a0, 0x0263, 0x0500, 0x02b7, 0x05a8, 0x02d2, 0x058d, 0x0b, 0x00, 0x0000, 0x0000)), + 'Bumper Cave (Top)': (0x16, (0x00eb, 0x4a, 0x00a0, 0x020a, 0x0500, 0x0258, 0x05b8, 0x0277, 0x058d, 0x06, 0x00, 0x0000, 0x0000)), + 'Superbunny Cave (Top)': (0x13, (0x00e8, 0x45, 0x0460, 0x0093, 0x0d00, 0x00e7, 0x0db8, 0x0102, 0x0d7d, 0x0b, 0x00, 0x0000, 0x0000)), + 'Superbunny Cave (Bottom)': (0x12, (0x00f8, 0x45, 0x0ee0, 0x01e4, 0x0d00, 0x0238, 0x0d78, 0x0253, 0x0d7d, 0x0a, 0x00, 0x0000, 0x0000)), + 'Hookshot Cave': (0x39, (0x003c, 0x45, 0x04da, 0x00a3, 0x0cd6, 0x0107, 0x0d48, 0x0112, 0x0d53, 0x0b, 0xfa, 0x0000, 0x0000)), + 'Hookshot Cave Back Entrance': (0x3A, (0x002c, 0x45, 0x004c, 0x0000, 0x0c56, 0x0038, 0x0cc8, 0x006f, 0x0cd3, 0x00, 0x0a, 0x0000, 0x0000)), + 'Ganons Tower': (0x36, (0x000c, 0x43, 0x0052, 0x0000, 0x0884, 0x0028, 0x08f8, 0x006f, 0x0903, 0x00, 0xfc, 0x0000, 0x0000)), + 'Inverted Agahnims Tower': (0x36, (0x000c, 0x43, 0x0052, 0x0000, 0x0884, 0x0028, 0x08f8, 0x006f, 0x0903, 0x00, 0xfc, 0x0000, 0x0000)), + 'Pyramid Entrance': (0x35, (0x0010, 0x5b, 0x0b0e, 0x075a, 0x0674, 0x07a8, 0x06e8, 0x07c7, 0x06f3, 0x06, 0xfa, 0x0000, 0x0000)), + 'Skull Woods First Section Hole (West)': ([0xDB84D, 0xDB84E], None), + 'Skull Woods First Section Hole (East)': ([0xDB84F, 0xDB850], None), + 'Skull Woods First Section Hole (North)': ([0xDB84C], None), + 'Skull Woods Second Section Hole': ([0xDB851, 0xDB852], None), + 'Pyramid Hole': ([0xDB854, 0xDB855, 0xDB856], None), + 'Inverted Pyramid Hole': ([0xDB854, 0xDB855, 0xDB856, 0x180340], None), + 'Waterfall of Wishing': (0x5B, (0x0114, 0x0f, 0x0080, 0x0200, 0x0e00, 0x0207, 0x0e60, 0x026f, 0x0e7d, 0x00, 0x00, 0x0000, 0x0000)), + 'Dam': (0x4D, (0x010b, 0x3b, 0x04a0, 0x0e8a, 0x06fa, 0x0ed8, 0x0778, 0x0ef7, 0x077f, 0x06, 0xfa, 0x0000, 0x0000)), + 'Blinds Hideout': (0x60, (0x0119, 0x18, 0x02b2, 0x064a, 0x0186, 0x0697, 0x0208, 0x06b7, 0x0213, 0x06, 0xfa, 0x0000, 0x0000)), + 'Hyrule Castle Secret Entrance Drop': ([0xDB858], None), + 'Bonk Fairy (Light)': (0x76, (0x0126, 0x2b, 0x00a0, 0x0a0a, 0x0700, 0x0a67, 0x0788, 0x0a77, 0x0785, 0x06, 0xfa, 0x0000, 0x0000)), + 'Lake Hylia Fairy': (0x5D, (0x0115, 0x2e, 0x0016, 0x0a00, 0x0cb6, 0x0a37, 0x0d28, 0x0a6d, 0x0d33, 0x00, 0x00, 0x0000, 0x0000)), + 'Light Hype Fairy': (0x6B, (0x0115, 0x34, 0x00a0, 0x0c04, 0x0900, 0x0c58, 0x0988, 0x0c73, 0x0985, 0x0a, 0xf6, 0x0000, 0x0000)), + 'Desert Fairy': (0x71, (0x0115, 0x3a, 0x0000, 0x0e00, 0x0400, 0x0e26, 0x0468, 0x0e6d, 0x0485, 0x00, 0x00, 0x0000, 0x0000)), + 'Kings Grave': (0x5A, (0x0113, 0x14, 0x0320, 0x0456, 0x0900, 0x04a6, 0x0998, 0x04c3, 0x097d, 0x0a, 0xf6, 0x0000, 0x0000)), + 'Tavern North': (0x42, (0x0103, 0x18, 0x1440, 0x08a7, 0x0206, 0x08f9, 0x0288, 0x0914, 0x0293, 0xf7, 0x09, 0xFFFF, 0x0000)), # do not use, buggy + 'Chicken House': (0x4A, (0x0108, 0x18, 0x1120, 0x0837, 0x0106, 0x0888, 0x0188, 0x08a4, 0x0193, 0x07, 0xf9, 0x1530, 0x0000)), + 'Aginahs Cave': (0x70, (0x010a, 0x30, 0x0656, 0x0cc6, 0x02aa, 0x0d18, 0x0328, 0x0d33, 0x032f, 0x08, 0xf8, 0x0000, 0x0000)), + 'Sahasrahlas Hut': (0x44, (0x0105, 0x1e, 0x0610, 0x06d4, 0x0c76, 0x0727, 0x0cf0, 0x0743, 0x0cfb, 0x0a, 0xf6, 0x0000, 0x0000)), + 'Cave Shop (Lake Hylia)': (0x57, (0x0112, 0x35, 0x0022, 0x0c00, 0x0b1a, 0x0c26, 0x0b98, 0x0c6d, 0x0b9f, 0x00, 0x00, 0x0000, 0x0000)), + 'Capacity Upgrade': (0x5C, (0x0115, 0x35, 0x0a46, 0x0d36, 0x0c2a, 0x0d88, 0x0ca8, 0x0da3, 0x0caf, 0x0a, 0xf6, 0x0000, 0x0000)), + 'Kakariko Well Drop': ([0xDB85C, 0xDB85D], None), + 'Blacksmiths Hut': (0x63, (0x0121, 0x22, 0x010c, 0x081a, 0x0466, 0x0868, 0x04d8, 0x0887, 0x04e3, 0x06, 0xfa, 0x041A, 0x0000)), + 'Bat Cave Drop': ([0xDB859, 0xDB85A], None), + 'Sick Kids House': (0x3F, (0x0102, 0x18, 0x10be, 0x0826, 0x01f6, 0x0877, 0x0278, 0x0893, 0x0283, 0x08, 0xf8, 0x14CE, 0x0000)), + 'North Fairy Cave Drop': ([0xDB857], None), + 'Lost Woods Gamble': (0x3B, (0x0100, 0x00, 0x004e, 0x0000, 0x0272, 0x0008, 0x02f0, 0x006f, 0x02f7, 0x00, 0x00, 0x0000, 0x0000)), + 'Fortune Teller (Light)': (0x64, (0x0122, 0x11, 0x060e, 0x04b4, 0x027d, 0x0508, 0x02f8, 0x0523, 0x0302, 0x0a, 0xf6, 0x0000, 0x0000)), + 'Snitch Lady (East)': (0x3D, (0x0101, 0x18, 0x0ad8, 0x074a, 0x02c6, 0x0798, 0x0348, 0x07b7, 0x0353, 0x06, 0xfa, 0x0DE8, 0x0000)), + 'Snitch Lady (West)': (0x3E, (0x0101, 0x18, 0x0788, 0x0706, 0x0046, 0x0758, 0x00c8, 0x0773, 0x00d3, 0x08, 0xf8, 0x0B98, 0x0000)), + 'Bush Covered House': (0x43, (0x0103, 0x18, 0x1156, 0x081a, 0x02b6, 0x0868, 0x0338, 0x0887, 0x0343, 0x06, 0xfa, 0x1466, 0x0000)), + 'Tavern (Front)': (0x41, (0x0103, 0x18, 0x1842, 0x0916, 0x0206, 0x0967, 0x0288, 0x0983, 0x0293, 0x08, 0xf8, 0x1C50, 0x0000)), + 'Light World Bomb Hut': (0x49, (0x0107, 0x18, 0x1800, 0x0916, 0x0000, 0x0967, 0x0068, 0x0983, 0x008d, 0x08, 0xf8, 0x9C0C, 0x0000)), + 'Kakariko Shop': (0x45, (0x011f, 0x18, 0x16a8, 0x08e7, 0x0136, 0x0937, 0x01b8, 0x0954, 0x01c3, 0x07, 0xf9, 0x1AB6, 0x0000)), + 'Lost Woods Hideout Drop': ([0xDB853], None), + 'Lumberjack Tree Tree': ([0xDB85B], None), + 'Cave 45': (0x50, (0x011b, 0x32, 0x0680, 0x0cc9, 0x0400, 0x0d16, 0x0438, 0x0d36, 0x0485, 0x07, 0xf9, 0x0000, 0x0000)), + 'Graveyard Cave': (0x51, (0x011b, 0x14, 0x0016, 0x0400, 0x08a2, 0x0446, 0x0918, 0x046d, 0x091f, 0x00, 0x00, 0x0000, 0x0000)), + 'Checkerboard Cave': (0x7D, (0x0126, 0x30, 0x00c8, 0x0c0a, 0x024a, 0x0c67, 0x02c8, 0x0c77, 0x02cf, 0x06, 0xfa, 0x0000, 0x0000)), + 'Mini Moldorm Cave': (0x7C, (0x0123, 0x35, 0x1480, 0x0e96, 0x0a00, 0x0ee8, 0x0a68, 0x0f03, 0x0a85, 0x08, 0xf8, 0x0000, 0x0000)), + 'Long Fairy Cave': (0x54, (0x011e, 0x2f, 0x06a0, 0x0aca, 0x0f00, 0x0b18, 0x0fa8, 0x0b37, 0x0f85, 0x06, 0xfa, 0x0000, 0x0000)), + 'Good Bee Cave': (0x6A, (0x0120, 0x37, 0x0084, 0x0c00, 0x0e26, 0x0c36, 0x0e98, 0x0c6f, 0x0ea3, 0x00, 0x00, 0x0000, 0x0000)), + '20 Rupee Cave': (0x7A, (0x0125, 0x37, 0x0200, 0x0c23, 0x0e00, 0x0c86, 0x0e68, 0x0c92, 0x0e7d, 0x0d, 0xf3, 0x0000, 0x0000)), + '50 Rupee Cave': (0x78, (0x0124, 0x3a, 0x0790, 0x0eea, 0x047a, 0x0f47, 0x04f8, 0x0f57, 0x04ff, 0x06, 0xfa, 0x0000, 0x0000)), + 'Ice Rod Cave': (0x7F, (0x0120, 0x37, 0x0080, 0x0c00, 0x0e00, 0x0c37, 0x0e48, 0x0c6f, 0x0e7d, 0x00, 0x00, 0x0000, 0x0000)), + 'Bonk Rock Cave': (0x79, (0x0124, 0x13, 0x0280, 0x044a, 0x0600, 0x04a7, 0x0638, 0x04b7, 0x067d, 0x06, 0xfa, 0x0000, 0x0000)), + 'Library': (0x48, (0x0107, 0x29, 0x0100, 0x0a14, 0x0200, 0x0a67, 0x0278, 0x0a83, 0x0285, 0x0a, 0xf6, 0x040E, 0x0000)), + 'Potion Shop': (0x4B, (0x0109, 0x16, 0x070a, 0x04e6, 0x0c56, 0x0538, 0x0cc8, 0x0553, 0x0cd3, 0x08, 0xf8, 0x0A98, 0x0000)), + 'Sanctuary Grave': ([0xDB85E], None), + 'Hookshot Fairy': (0x4F, (0x010c, 0x05, 0x0ee0, 0x01e3, 0x0d00, 0x0236, 0x0d78, 0x0252, 0x0d7d, 0x0b, 0xf5, 0x0000, 0x0000)), + 'Pyramid Fairy': (0x62, (0x0116, 0x5b, 0x0b1e, 0x0754, 0x06fa, 0x07a7, 0x0778, 0x07c3, 0x077f, 0x0a, 0xf6, 0x0000, 0x0000)), + 'East Dark World Hint': (0x68, (0x010e, 0x6f, 0x06a0, 0x0aca, 0x0f00, 0x0b18, 0x0fa8, 0x0b37, 0x0f85, 0x06, 0xfa, 0x0000, 0x0000)), + 'Palace of Darkness Hint': (0x67, (0x011a, 0x5e, 0x0c24, 0x0794, 0x0d12, 0x07e8, 0x0d90, 0x0803, 0x0d97, 0x0a, 0xf6, 0x0000, 0x0000)), + 'Dark Lake Hylia Fairy': (0x6C, (0x0115, 0x6e, 0x0016, 0x0a00, 0x0cb6, 0x0a36, 0x0d28, 0x0a6d, 0x0d33, 0x00, 0x00, 0x0000, 0x0000)), + 'Dark Lake Hylia Ledge Fairy': (0x80, (0x0115, 0x77, 0x0080, 0x0c00, 0x0e00, 0x0c37, 0x0e48, 0x0c6f, 0x0e7d, 0x00, 0x00, 0x0000, 0x0000)), + 'Dark Lake Hylia Ledge Spike Cave': (0x7B, (0x0125, 0x77, 0x0200, 0x0c27, 0x0e00, 0x0c86, 0x0e68, 0x0c96, 0x0e7d, 0x09, 0xf7, 0x0000, 0x0000)), + 'Dark Lake Hylia Ledge Hint': (0x69, (0x010e, 0x77, 0x0084, 0x0c00, 0x0e26, 0x0c36, 0x0e98, 0x0c6f, 0x0ea3, 0x00, 0x00, 0x0000, 0x0000)), + 'Hype Cave': (0x3C, (0x011e, 0x74, 0x00a0, 0x0c0a, 0x0900, 0x0c58, 0x0988, 0x0c77, 0x097d, 0x06, 0xfa, 0x0000, 0x0000)), + 'Bonk Fairy (Dark)': (0x77, (0x0126, 0x6b, 0x00a0, 0x0a05, 0x0700, 0x0a66, 0x0788, 0x0a72, 0x0785, 0x0b, 0xf5, 0x0000, 0x0000)), + 'Brewery': (0x47, (0x0106, 0x58, 0x16a8, 0x08e4, 0x013e, 0x0938, 0x01b8, 0x0953, 0x01c3, 0x0a, 0xf6, 0x1AB6, 0x0000)), + 'C-Shaped House': (0x53, (0x011c, 0x58, 0x09d8, 0x0744, 0x02ce, 0x0797, 0x0348, 0x07b3, 0x0353, 0x0a, 0xf6, 0x0DE8, 0x0000)), + 'Chest Game': (0x46, (0x0106, 0x58, 0x078a, 0x0705, 0x004e, 0x0758, 0x00c8, 0x0774, 0x00d3, 0x09, 0xf7, 0x0B98, 0x0000)), + 'Dark World Hammer Peg Cave': (0x7E, (0x0127, 0x62, 0x0894, 0x091e, 0x0492, 0x09a6, 0x0508, 0x098b, 0x050f, 0x00, 0x00, 0x0000, 0x0000)), + 'Red Shield Shop': (0x74, (0x0110, 0x5a, 0x079a, 0x06e8, 0x04d6, 0x0738, 0x0548, 0x0755, 0x0553, 0x08, 0xf8, 0x0AA8, 0x0000)), + 'Dark Sanctuary Hint': (0x59, (0x0112, 0x53, 0x001e, 0x0400, 0x06e2, 0x0446, 0x0758, 0x046d, 0x075f, 0x00, 0x00, 0x0000, 0x0000)), + 'Inverted Dark Sanctuary': (0x59, (0x0112, 0x53, 0x001e, 0x0400, 0x06e2, 0x0446, 0x0758, 0x046d, 0x075f, 0x00, 0x00, 0x0000, 0x0000)), + 'Fortune Teller (Dark)': (0x65, (0x0122, 0x51, 0x0610, 0x04b4, 0x027e, 0x0507, 0x02f8, 0x0523, 0x0303, 0x0a, 0xf6, 0x091E, 0x0000)), + 'Dark World Shop': (0x5F, (0x010f, 0x58, 0x1058, 0x0814, 0x02be, 0x0868, 0x0338, 0x0883, 0x0343, 0x0a, 0xf6, 0x0000, 0x0000)), + 'Dark World Lumberjack Shop': (0x56, (0x010f, 0x42, 0x041c, 0x0074, 0x04e2, 0x00c7, 0x0558, 0x00e3, 0x055f, 0x0a, 0xf6, 0x0000, 0x0000)), + 'Dark World Potion Shop': (0x6E, (0x010f, 0x56, 0x080e, 0x04f4, 0x0c66, 0x0548, 0x0cd8, 0x0563, 0x0ce3, 0x0a, 0xf6, 0x0000, 0x0000)), + 'Archery Game': (0x58, (0x0111, 0x69, 0x069e, 0x0ac4, 0x02ea, 0x0b18, 0x0368, 0x0b33, 0x036f, 0x0a, 0xf6, 0x09AC, 0x0000)), + 'Mire Shed': (0x5E, (0x010d, 0x70, 0x0384, 0x0c69, 0x001e, 0x0cb6, 0x0098, 0x0cd6, 0x00a3, 0x07, 0xf9, 0x0000, 0x0000)), + 'Dark Desert Hint': (0x61, (0x0114, 0x70, 0x0654, 0x0cc5, 0x02aa, 0x0d16, 0x0328, 0x0d32, 0x032f, 0x09, 0xf7, 0x0000, 0x0000)), + 'Dark Desert Fairy': (0x55, (0x0115, 0x70, 0x03a8, 0x0c6a, 0x013a, 0x0cb7, 0x01b8, 0x0cd7, 0x01bf, 0x06, 0xfa, 0x0000, 0x0000)), + 'Spike Cave': (0x40, (0x0117, 0x43, 0x0ed4, 0x01e4, 0x08aa, 0x0236, 0x0928, 0x0253, 0x092f, 0x0a, 0xf6, 0x0000, 0x0000)), + 'Cave Shop (Dark Death Mountain)': (0x6D, (0x0112, 0x45, 0x0ee0, 0x01e3, 0x0d00, 0x0236, 0x0daa, 0x0252, 0x0d7d, 0x0b, 0xf5, 0x0000, 0x0000)), + 'Dark Death Mountain Fairy': (0x6F, (0x0115, 0x43, 0x1400, 0x0294, 0x0600, 0x02e8, 0x0678, 0x0303, 0x0685, 0x0a, 0xf6, 0x0000, 0x0000)), + 'Mimic Cave': (0x4E, (0x010c, 0x05, 0x07e0, 0x0103, 0x0d00, 0x0156, 0x0d78, 0x0172, 0x0d7d, 0x0b, 0xf5, 0x0000, 0x0000)), + 'Big Bomb Shop': (0x52, (0x011c, 0x6c, 0x0506, 0x0a9a, 0x0832, 0x0ae7, 0x08b8, 0x0b07, 0x08bf, 0x06, 0xfa, 0x0816, 0x0000)), + 'Inverted Links House': (0x52, (0x011c, 0x6c, 0x0506, 0x0a9a, 0x0832, 0x0ae7, 0x08b8, 0x0b07, 0x08bf, 0x06, 0xfa, 0x0816, 0x0000)), + 'Dark Lake Hylia Shop': (0x73, (0x010f, 0x75, 0x0380, 0x0c6a, 0x0a00, 0x0cb8, 0x0a58, 0x0cd7, 0x0a85, 0x06, 0xfa, 0x0000, 0x0000)), + 'Lumberjack House': (0x75, (0x011f, 0x02, 0x049c, 0x0088, 0x04e6, 0x00d8, 0x0558, 0x00f7, 0x0563, 0x08, 0xf8, 0x07AA, 0x0000)), + 'Lake Hylia Fortune Teller': (0x72, (0x0122, 0x35, 0x0380, 0x0c6a, 0x0a00, 0x0cb8, 0x0a58, 0x0cd7, 0x0a85, 0x06, 0xfa, 0x0000, 0x0000)), + 'Kakariko Gamble Game': (0x66, (0x0118, 0x29, 0x069e, 0x0ac4, 0x02ea, 0x0b18, 0x0368, 0x0b33, 0x036f, 0x0a, 0xf6, 0x09AC, 0x0000))} + +# format: +# Key=Name +# value = entrance # +# | (entrance #, exit #) +exit_ids = {'Links House Exit': (0x01, 0x00), + 'Inverted Links House Exit': (0x01, 0x00), + 'Chris Houlihan Room Exit': (None, 0x3D), + 'Desert Palace Exit (South)': (0x09, 0x0A), + 'Desert Palace Exit (West)': (0x0B, 0x0C), + 'Desert Palace Exit (East)': (0x0A, 0x0B), + 'Desert Palace Exit (North)': (0x0C, 0x0D), + 'Eastern Palace Exit': (0x08, 0x09), + 'Tower of Hera Exit': (0x33, 0x2D), + 'Hyrule Castle Exit (South)': (0x04, 0x03), + 'Hyrule Castle Exit (West)': (0x03, 0x02), + 'Hyrule Castle Exit (East)': (0x05, 0x04), + 'Agahnims Tower Exit': (0x24, 0x25), + 'Inverted Agahnims Tower Exit': (0x24, 0x25), + 'Thieves Town Exit': (0x34, 0x35), + 'Skull Woods First Section Exit': (0x2A, 0x2B), + 'Skull Woods Second Section Exit (East)': (0x29, 0x2A), + 'Skull Woods Second Section Exit (West)': (0x28, 0x29), + 'Skull Woods Final Section Exit': (0x2B, 0x2C), + 'Ice Palace Exit': (0x2D, 0x2E), + 'Misery Mire Exit': (0x27, 0x28), + 'Palace of Darkness Exit': (0x26, 0x27), + 'Swamp Palace Exit': (0x25, 0x26), + 'Turtle Rock Exit (Front)': (0x35, 0x34), + 'Turtle Rock Ledge Exit (West)': (0x15, 0x16), + 'Turtle Rock Ledge Exit (East)': (0x19, 0x1A), + 'Turtle Rock Isolated Ledge Exit': (0x18, 0x19), + 'Hyrule Castle Secret Entrance Exit': (0x32, 0x33), + 'Kakariko Well Exit': (0x39, 0x3A), + 'Bat Cave Exit': (0x11, 0x12), + 'Elder House Exit (East)': (0x0E, 0x0F), + 'Elder House Exit (West)': (0x0D, 0x0E), + 'North Fairy Cave Exit': (0x38, 0x39), + 'Lost Woods Hideout Exit': (0x2C, 0x36), + 'Lumberjack Tree Exit': (0x12, 0x13), + 'Two Brothers House Exit (East)': (0x10, 0x11), + 'Two Brothers House Exit (West)': (0x0F, 0x10), + 'Sanctuary Exit': (0x02, 0x01), + 'Old Man Cave Exit (East)': (0x07, 0x08), + 'Old Man Cave Exit (West)': (0x06, 0x07), + 'Old Man House Exit (Bottom)': (0x30, 0x31), + 'Old Man House Exit (Top)': (0x31, 0x32), + 'Death Mountain Return Cave Exit (West)': (0x2E, 0x2F), + 'Death Mountain Return Cave Exit (East)': (0x2F, 0x30), + 'Spectacle Rock Cave Exit': (0x21, 0x22), + 'Spectacle Rock Cave Exit (Top)': (0x22, 0x23), + 'Spectacle Rock Cave Exit (Peak)': (0x23, 0x24), + 'Paradox Cave Exit (Bottom)': (0x1E, 0x1F), + 'Paradox Cave Exit (Middle)': (0x1F, 0x20), + 'Paradox Cave Exit (Top)': (0x20, 0x21), + 'Fairy Ascension Cave Exit (Bottom)': (0x1A, 0x1B), + 'Fairy Ascension Cave Exit (Top)': (0x1B, 0x1C), + 'Spiral Cave Exit': (0x1C, 0x1D), + 'Spiral Cave Exit (Top)': (0x1D, 0x1E), + 'Bumper Cave Exit (Top)': (0x17, 0x18), + 'Bumper Cave Exit (Bottom)': (0x16, 0x17), + 'Superbunny Cave Exit (Top)': (0x14, 0x15), + 'Superbunny Cave Exit (Bottom)': (0x13, 0x14), + 'Hookshot Cave Front Exit': (0x3A, 0x3B), + 'Hookshot Cave Back Exit': (0x3B, 0x3C), + 'Ganons Tower Exit': (0x37, 0x38), + 'Inverted Ganons Tower Exit': (0x37, 0x38), + 'Pyramid Exit': (0x36, 0x37), + 'Waterfall of Wishing': 0x5C, + 'Dam': 0x4E, + 'Blinds Hideout': 0x61, + 'Lumberjack House': 0x6B, + 'Bonk Fairy (Light)': 0x71, + 'Bonk Fairy (Dark)': 0x71, + 'Lake Hylia Healer Fairy': 0x5E, + 'Swamp Healer Fairy': 0x5E, + 'Desert Healer Fairy': 0x5E, + 'Dark Lake Hylia Healer Fairy': 0x5E, + 'Dark Lake Hylia Ledge Healer Fairy': 0x5E, + 'Dark Desert Healer Fairy': 0x5E, + 'Dark Death Mountain Healer Fairy': 0x5E, + 'Fortune Teller (Light)': 0x65, + 'Lake Hylia Fortune Teller': 0x65, + 'Kings Grave': 0x5B, + 'Tavern': 0x43, + 'Chicken House': 0x4B, + 'Aginahs Cave': 0x4D, + 'Sahasrahlas Hut': 0x45, + 'Cave Shop (Lake Hylia)': 0x58, + 'Cave Shop (Dark Death Mountain)': 0x58, + 'Capacity Upgrade': 0x5D, + 'Blacksmiths Hut': 0x64, + 'Sick Kids House': 0x40, + 'Lost Woods Gamble': 0x3C, + 'Snitch Lady (East)': 0x3E, + 'Snitch Lady (West)': 0x3F, + 'Bush Covered House': 0x44, + 'Tavern (Front)': 0x42, + 'Light World Bomb Hut': 0x4A, + 'Kakariko Shop': 0x46, + 'Cave 45': 0x51, + 'Graveyard Cave': 0x52, + 'Checkerboard Cave': 0x72, + 'Mini Moldorm Cave': 0x6C, + 'Long Fairy Cave': 0x55, + 'Good Bee Cave': 0x56, + '20 Rupee Cave': 0x6F, + '50 Rupee Cave': 0x6D, + 'Ice Rod Cave': 0x84, + 'Bonk Rock Cave': 0x6E, + 'Library': 0x49, + 'Kakariko Gamble Game': 0x67, + 'Potion Shop': 0x4C, + 'Hookshot Fairy': 0x50, + 'Pyramid Fairy': 0x63, + 'East Dark World Hint': 0x69, + 'Palace of Darkness Hint': 0x68, + 'Big Bomb Shop': 0x53, + 'Inverted Big Bomb Shop': 0x53, + 'Village of Outcasts Shop': 0x60, + 'Dark Lake Hylia Shop': 0x60, + 'Dark World Lumberjack Shop': 0x60, + 'Dark World Potion Shop': 0x60, + 'Dark Lake Hylia Ledge Spike Cave': 0x70, + 'Dark Lake Hylia Ledge Hint': 0x6A, + 'Hype Cave': 0x3D, + 'Brewery': 0x48, + 'C-Shaped House': 0x54, + 'Chest Game': 0x47, + 'Dark World Hammer Peg Cave': 0x83, + 'Red Shield Shop': 0x57, + 'Dark Sanctuary Hint': 0x5A, + 'Inverted Dark Sanctuary': 0x5A, + 'Fortune Teller (Dark)': 0x66, + 'Archery Game': 0x59, + 'Mire Shed': 0x5F, + 'Dark Desert Hint': 0x62, + 'Spike Cave': 0x41, + 'Mimic Cave': 0x4F, + 'Kakariko Well (top)': 0x80, + 'Hyrule Castle Secret Entrance': 0x7D, + 'Bat Cave (right)': 0x7E, + 'North Fairy Cave': 0x7C, + 'Lost Woods Hideout (top)': 0x7A, + 'Lumberjack Tree (top)': 0x7F, + 'Sewer Drop': 0x81, + 'Skull Back Drop': 0x79, + 'Skull Left Drop': 0x77, + 'Skull Pinball': 0x78, + 'Skull Pot Circle': 0x76, + 'Pyramid': 0x7B} + +ow_prize_table = {'Links House': (0x8b1, 0xb2d), 'Inverted Big Bomb Shop': (0x8b1, 0xb2d), + 'Desert Palace Entrance (South)': (0x108, 0xd70), 'Desert Palace Entrance (West)': (0x031, 0xca0), + 'Desert Palace Entrance (North)': (0x0e1, 0xba0), 'Desert Palace Entrance (East)': (0x191, 0xca0), + 'Eastern Palace': (0xf31, 0x620), 'Tower of Hera': (0x8D0, 0x080), + 'Hyrule Castle Entrance (South)': (0x7b0, 0x730), 'Hyrule Castle Entrance (West)': (0x700, 0x640), + 'Hyrule Castle Entrance (East)': (0x8a0, 0x640), 'Inverted Pyramid Entrance': (0x720, 0x700), + 'Agahnims Tower': (0x7e0, 0x640), 'Inverted Ganons Tower': (0x7e0, 0x640), + 'Thieves Town': (0x1d0, 0x780), 'Skull Woods First Section Door': (0x240, 0x280), + 'Skull Woods Second Section Door (East)': (0x1a0, 0x240), + 'Skull Woods Second Section Door (West)': (0x0c0, 0x1c0), 'Skull Woods Final Section': (0x082, 0x0b0), + 'Ice Palace': (0xca0, 0xda0), + 'Misery Mire': (0x100, 0xca0), + 'Palace of Darkness': (0xf40, 0x620), 'Swamp Palace': (0x759, 0xED0), + 'Turtle Rock': (0xf11, 0x103), + 'Dark Death Mountain Ledge (West)': (0xb80, 0x180), + 'Dark Death Mountain Ledge (East)': (0xc80, 0x180), + 'Turtle Rock Isolated Ledge Entrance': (0xc00, 0x240), + 'Hyrule Castle Secret Entrance Stairs': (0x850, 0x700), + 'Kakariko Well Cave': (0x060, 0x680), + 'Bat Cave Cave': (0x540, 0x8f0), + 'Elder House (East)': (0x2b0, 0x6a0), + 'Elder House (West)': (0x230, 0x6a0), + 'North Fairy Cave': (0xa80, 0x440), + 'Lost Woods Hideout Stump': (0x240, 0x280), + 'Lumberjack Tree Cave': (0x4e0, 0x004), + 'Two Brothers House (East)': (0x200, 0x0b60), + 'Two Brothers House (West)': (0x180, 0x0b60), + 'Sanctuary': (0x720, 0x4a0), + 'Old Man Cave (West)': (0x580, 0x2c0), + 'Old Man Cave (East)': (0x620, 0x2c0), + 'Old Man House (Bottom)': (0x720, 0x320), + 'Old Man House (Top)': (0x820, 0x220), + 'Death Mountain Return Cave (East)': (0x600, 0x220), + 'Death Mountain Return Cave (West)': (0x500, 0x1c0), + 'Spectacle Rock Cave Peak': (0x720, 0x0a0), + 'Spectacle Rock Cave': (0x790, 0x1a0), + 'Spectacle Rock Cave (Bottom)': (0x710, 0x0a0), + 'Paradox Cave (Bottom)': (0xd80, 0x180), + 'Paradox Cave (Middle)': (0xd80, 0x380), + 'Paradox Cave (Top)': (0xd80, 0x020), + 'Fairy Ascension Cave (Bottom)': (0xcc8, 0x2a0), + 'Fairy Ascension Cave (Top)': (0xc00, 0x240), + 'Spiral Cave': (0xb80, 0x180), + 'Spiral Cave (Bottom)': (0xb80, 0x2c0), + 'Bumper Cave (Bottom)': (0x580, 0x2c0), + 'Bumper Cave (Top)': (0x500, 0x1c0), + 'Superbunny Cave (Top)': (0xd80, 0x020), + 'Superbunny Cave (Bottom)': (0xd00, 0x180), + 'Hookshot Cave': (0xc80, 0x0c0), + 'Hookshot Cave Back Entrance': (0xcf0, 0x004), + 'Ganons Tower': (0x8D0, 0x080), + 'Inverted Agahnims Tower': (0x8D0, 0x080), + 'Pyramid Entrance': (0x640, 0x7c0), + 'Skull Woods First Section Hole (West)': None, + 'Skull Woods First Section Hole (East)': None, + 'Skull Woods First Section Hole (North)': None, + 'Skull Woods Second Section Hole': None, + 'Pyramid Hole': None, + 'Inverted Pyramid Hole': None, + 'Waterfall of Wishing': (0xe80, 0x280), + 'Dam': (0x759, 0xED0), + 'Blinds Hideout': (0x190, 0x6c0), + 'Hyrule Castle Secret Entrance Drop': None, + 'Bonk Fairy (Light)': (0x740, 0xa80), + 'Lake Hylia Fairy': (0xd40, 0x9f0), + 'Light Hype Fairy': (0x940, 0xc80), + 'Desert Fairy': (0x420, 0xe00), + 'Kings Grave': (0x920, 0x520), + 'Tavern North': None, # can't mark this one technically + 'Chicken House': (0x120, 0x880), + 'Aginahs Cave': (0x2e0, 0xd00), + 'Sahasrahlas Hut': (0xcf0, 0x6c0), + 'Cave Shop (Lake Hylia)': (0xbc0, 0xc00), + 'Capacity Upgrade': (0xca0, 0xda0), + 'Kakariko Well Drop': None, + 'Blacksmiths Hut': (0x4a0, 0x880), + 'Bat Cave Drop': None, + 'Sick Kids House': (0x220, 0x880), + 'North Fairy Cave Drop': None, + 'Lost Woods Gamble': (0x240, 0x080), + 'Fortune Teller (Light)': (0x2c0, 0x4c0), + 'Snitch Lady (East)': (0x310, 0x7a0), + 'Snitch Lady (West)': (0x800, 0x7a0), + 'Bush Covered House': (0x2e0, 0x880), + 'Tavern (Front)': (0x270, 0x980), + 'Light World Bomb Hut': (0x070, 0x980), + 'Kakariko Shop': (0x170, 0x980), + 'Lost Woods Hideout Drop': None, + 'Lumberjack Tree Tree': None, + 'Cave 45': (0x440, 0xca0), 'Graveyard Cave': (0x8f0, 0x430), + 'Checkerboard Cave': (0x260, 0xc00), + 'Mini Moldorm Cave': (0xa40, 0xe80), + 'Long Fairy Cave': (0xf60, 0xb00), + 'Good Bee Cave': (0xec0, 0xc00), + '20 Rupee Cave': (0xe80, 0xca0), + '50 Rupee Cave': (0x4d0, 0xed0), + 'Ice Rod Cave': (0xe00, 0xc00), + 'Bonk Rock Cave': (0x5f0, 0x460), + 'Library': (0x270, 0xaa0), + 'Potion Shop': (0xc80, 0x4c0), + 'Sanctuary Grave': None, + 'Hookshot Fairy': (0xd00, 0x180), + 'Pyramid Fairy': (0x740, 0x740), + 'East Dark World Hint': (0xf60, 0xb00), + 'Palace of Darkness Hint': (0xd60, 0x7c0), + 'Dark Lake Hylia Fairy': (0xd40, 0x9f0), + 'Dark Lake Hylia Ledge Fairy': (0xe00, 0xc00), + 'Dark Lake Hylia Ledge Spike Cave': (0xe80, 0xca0), + 'Dark Lake Hylia Ledge Hint': (0xec0, 0xc00), + 'Hype Cave': (0x940, 0xc80), + 'Bonk Fairy (Dark)': (0x740, 0xa80), + 'Brewery': (0x170, 0x980), 'C-Shaped House': (0x310, 0x7a0), 'Chest Game': (0x800, 0x7a0), + 'Dark World Hammer Peg Cave': (0x4c0, 0x940), + 'Red Shield Shop': (0x500, 0x680), + 'Dark Sanctuary Hint': (0x720, 0x4a0), + 'Inverted Dark Sanctuary': (0x720, 0x4a0), + 'Fortune Teller (Dark)': (0x2c0, 0x4c0), + 'Dark World Shop': (0x2e0, 0x880), + 'Dark World Lumberjack Shop': (0x4e0, 0x0d0), + 'Dark World Potion Shop': (0xc80, 0x4c0), + 'Archery Game': (0x2f0, 0xaf0), + 'Mire Shed': (0x060, 0xc90), + 'Dark Desert Hint': (0x2e0, 0xd00), + 'Dark Desert Fairy': (0x1c0, 0xc90), + 'Spike Cave': (0x860, 0x180), + 'Cave Shop (Dark Death Mountain)': (0xd80, 0x180), + 'Dark Death Mountain Fairy': (0x620, 0x2c0), + 'Mimic Cave': (0xc80, 0x180), + 'Big Bomb Shop': (0x8b1, 0xb2d), 'Inverted Links House': (0x8b1, 0xb2d), + 'Dark Lake Hylia Shop': (0xa40, 0xc40), + 'Lumberjack House': (0x4e0, 0x0d0), + 'Lake Hylia Fortune Teller': (0xa40, 0xc40), + 'Kakariko Gamble Game': (0x2f0, 0xaf0)} diff --git a/source/test/__init__.py b/source/overworld/__init__.py similarity index 100% rename from source/test/__init__.py rename to source/overworld/__init__.py diff --git a/source/tools/MysteryUtils.py b/source/tools/MysteryUtils.py new file mode 100644 index 00000000..6cc1d4ff --- /dev/null +++ b/source/tools/MysteryUtils.py @@ -0,0 +1,190 @@ +import argparse +import RaceRandom as random + +import urllib.request +import urllib.parse +import yaml + + +def get_weights(path): + try: + if urllib.parse.urlparse(path).scheme: + return yaml.load(urllib.request.urlopen(path), Loader=yaml.FullLoader) + with open(path, 'r', encoding='utf-8') as f: + return yaml.load(f, Loader=yaml.SafeLoader) + except Exception as e: + raise Exception(f'Failed to read weights file: {e}') + + +def roll_settings(weights): + def get_choice(option, root=None): + root = weights if root is None else root + if option not in root: + return None + if type(root[option]) is not dict: + return root[option] + if not root[option]: + return None + return random.choices(list(root[option].keys()), weights=list(map(int, root[option].values())))[0] + + def get_choice_default(option, root=weights, default=None): + choice = get_choice(option, root) + if choice is None and default is not None: + return default + return choice + + while True: + subweights = weights.get('subweights', {}) + if len(subweights) == 0: + break + chances = ({k: int(v['chance']) for (k, v) in subweights.items()}) + subweight_name = random.choices(list(chances.keys()), weights=list(chances.values()))[0] + subweights = weights.get('subweights', {}).get(subweight_name, {}).get('weights', {}) + subweights['subweights'] = subweights.get('subweights', {}) + weights = {**weights, **subweights} + + ret = argparse.Namespace() + + ret.algorithm = get_choice('algorithm') + ret.mystery = get_choice_default('mystery', default=True) + + glitch_map = {'none': 'noglitches', 'no_logic': 'nologic', 'owglitches': 'owglitches', + 'minorglitches': 'minorglitches'} + glitches_required = get_choice('glitches_required') + if glitches_required is not None: + if glitches_required not in glitch_map.keys(): + print(f'Logic did not match one of: {", ".join(glitch_map.keys())}') + glitches_required = 'none' + ret.logic = glitch_map[glitches_required] + + # item_placement = get_choice('item_placement') + # not supported in ER + + dungeon_items = get_choice('dungeon_items') + dungeon_items = '' if dungeon_items == 'standard' or dungeon_items is None else dungeon_items + dungeon_items = 'mcsb' if dungeon_items == 'full' else dungeon_items + ret.mapshuffle = get_choice('map_shuffle') == 'on' if 'map_shuffle' in weights else 'm' in dungeon_items + ret.compassshuffle = get_choice('compass_shuffle') == 'on' if 'compass_shuffle' in weights else 'c' in dungeon_items + ret.keyshuffle = get_choice('smallkey_shuffle') == 'on' if 'smallkey_shuffle' in weights else 's' in dungeon_items + ret.bigkeyshuffle = get_choice('bigkey_shuffle') == 'on' if 'bigkey_shuffle' in weights else 'b' in dungeon_items + + ret.accessibility = get_choice('accessibility') + ret.restrict_boss_items = get_choice('restrict_boss_items') + + entrance_shuffle = get_choice('entrance_shuffle') + ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla' + overworld_map = get_choice('overworld_map') + ret.overworld_map = overworld_map if overworld_map != 'default' else 'default' + door_shuffle = get_choice('door_shuffle') + ret.door_shuffle = door_shuffle if door_shuffle != 'none' else 'vanilla' + ret.intensity = get_choice('intensity') + ret.experimental = get_choice('experimental') == 'on' + ret.collection_rate = get_choice('collection_rate') == 'on' + + ret.dungeon_counters = get_choice('dungeon_counters') if 'dungeon_counters' in weights else 'default' + if ret.dungeon_counters == 'default': + ret.dungeon_counters = 'pickup' if ret.door_shuffle != 'vanilla' or ret.compassshuffle == 'on' else 'off' + + ret.shufflelinks = get_choice('shufflelinks') == 'on' + ret.pseudoboots = get_choice('pseudoboots') == 'on' + ret.shopsanity = get_choice('shopsanity') == 'on' + keydropshuffle = get_choice('keydropshuffle') == 'on' + ret.dropshuffle = get_choice('dropshuffle') == 'on' or keydropshuffle + ret.pottery = get_choice('pottery') if 'pottery' in weights else 'none' + ret.pottery = 'keys' if ret.pottery == 'none' and keydropshuffle else ret.pottery + ret.shufflepots = get_choice('pot_shuffle') == 'on' + ret.mixed_travel = get_choice('mixed_travel') if 'mixed_travel' in weights else 'prevent' + ret.standardize_palettes = (get_choice('standardize_palettes') if 'standardize_palettes' in weights + else 'standardize') + + goal = get_choice('goals') + if goal is not None: + ret.goal = {'ganon': 'ganon', + 'fast_ganon': 'crystals', + 'dungeons': 'dungeons', + 'pedestal': 'pedestal', + 'triforce-hunt': 'triforcehunt' + }[goal] + ret.openpyramid = goal == 'fast_ganon' if ret.shuffle in ['vanilla', 'dungeonsfull', 'dungeonssimple'] else False + + ret.crystals_gt = get_choice('tower_open') + + ret.crystals_ganon = get_choice('ganon_open') + + goal_min = get_choice_default('triforce_goal_min', default=20) + goal_max = get_choice_default('triforce_goal_max', default=20) + pool_min = get_choice_default('triforce_pool_min', default=30) + pool_max = get_choice_default('triforce_pool_max', default=30) + ret.triforce_goal = random.randint(int(goal_min), int(goal_max)) + min_diff = get_choice_default('triforce_min_difference', default=10) + ret.triforce_pool = random.randint(max(int(pool_min), ret.triforce_goal + int(min_diff)), int(pool_max)) + + ret.mode = get_choice('world_state') + if ret.mode == 'retro': + ret.mode = 'open' + ret.retro = True + ret.retro = get_choice('retro') == 'on' # this overrides world_state if used + + ret.bombbag = get_choice('bombbag') == 'on' + + ret.hints = get_choice('hints') == 'on' + + swords = get_choice('weapons') + if swords is not None: + ret.swords = {'randomized': 'random', + 'assured': 'assured', + 'vanilla': 'vanilla', + 'swordless': 'swordless' + }[swords] + + ret.difficulty = get_choice('item_pool') + + ret.item_functionality = get_choice('item_functionality') + + old_style_bosses = {'basic': 'simple', + 'normal': 'full', + 'chaos': 'random'} + boss_choice = get_choice('boss_shuffle') + if boss_choice in old_style_bosses.keys(): + boss_choice = old_style_bosses[boss_choice] + ret.shufflebosses = boss_choice + + enemy_choice = get_choice('enemy_shuffle') + if enemy_choice == 'chaos': + enemy_choice = 'random' + ret.shuffleenemies = enemy_choice + + old_style_damage = {'none': 'default', + 'chaos': 'random'} + damage_choice = get_choice('enemy_damage') + if damage_choice in old_style_damage: + damage_choice = old_style_damage[damage_choice] + ret.enemy_damage = damage_choice + + ret.enemy_health = get_choice('enemy_health') + + ret.beemizer = get_choice('beemizer') if 'beemizer' in weights else '0' + + inventoryweights = weights.get('startinventory', {}) + startitems = [] + for item in inventoryweights.keys(): + if get_choice(item, inventoryweights) == 'on': + startitems.append(item) + ret.startinventory = ','.join(startitems) + if len(startitems) > 0: + ret.usestartinventory = True + + if 'rom' in weights: + romweights = weights['rom'] + ret.sprite = get_choice('sprite', romweights) + ret.disablemusic = get_choice('disablemusic', romweights) == 'on' + ret.quickswap = get_choice('quickswap', romweights) == 'on' + ret.reduce_flashing = get_choice('reduce_flashing', romweights) == 'on' + ret.fastmenu = get_choice('menuspeed', romweights) + ret.heartcolor = get_choice('heartcolor', romweights) + ret.heartbeep = get_choice('heartbeep', romweights) + ret.ow_palettes = get_choice('ow_palettes', romweights) + ret.uw_palettes = get_choice('uw_palettes', romweights) + ret.shuffle_sfx = get_choice('shuffle_sfx', romweights) == 'on' + + return ret diff --git a/source/test/MysteryTestSuite.py b/test/MysteryTestSuite.py similarity index 100% rename from source/test/MysteryTestSuite.py rename to test/MysteryTestSuite.py diff --git a/test/stats/EntranceShuffleStats.py b/test/stats/EntranceShuffleStats.py new file mode 100644 index 00000000..2be02071 --- /dev/null +++ b/test/stats/EntranceShuffleStats.py @@ -0,0 +1,154 @@ +import RaceRandom as random +import logging +import time + +from collections import Counter, defaultdict + +from source.overworld.EntranceShuffle2 import link_entrances_new +from EntranceShuffle import link_entrances, link_inverted_entrances +from BaseClasses import World +from Regions import create_regions, create_dungeon_regions +from InvertedRegions import create_inverted_regions + + +# tested: open + crossed (lh) Mar. 17 (made changes) +# tested: open + simple (lh) Mar. 22 + +def run_stats(): + # logging.basicConfig(format='%(message)s', level=logging.DEBUG) + random.seed(None) + tests = 10000 + + for main_mode in ['open', 'standard', 'inverted']: + for shuffle_mode in ['dungeonssimple', 'dungeonsfull', + 'simple', 'restricted', 'full', 'crossed']: + for ls in [True, False]: + if ls and (main_mode == 'standard' or shuffle_mode in ['dungeonssimple', 'dungeonsfull']): + continue + + def runner_new(world): + link_entrances_new(world, 1) + + def runner_old(world): + if main_mode == 'inverted': + link_inverted_entrances(world, 1) + else: + link_entrances(world, 1) + compare_tests(tests, shuffle_mode, main_mode, ls, runner_old, runner_new) + + +def run_test_stats(): + # logging.basicConfig(format='%(message)s', level=logging.DEBUG) + random.seed(None) + tests = 10000 + + for main_mode in ['open']: + for shuffle_mode in [#'dungeonssimple', 'dungeonsfull', + 'simple']:#, 'restricted', 'full', 'crossed']: + for ls in [True, False]: + if ls and (main_mode == 'standard' or shuffle_mode in ['dungeonssimple', 'dungeonsfull']): + continue + + def runner_new(world): + link_entrances_new(world, 1) + + run_tests(tests, shuffle_mode, main_mode, ls, runner_new) + + +def run_old_stats(): + # logging.basicConfig(format='%(message)s', level=logging.DEBUG) + random.seed(None) + tests = 10000 + + for main_mode in ['open']: + for shuffle_mode in [#'dungeonssimple', 'dungeonsfull', + 'simple']:#, 'restricted', 'full', 'crossed']: + for ls in [True, False]: + if ls and (main_mode == 'standard' or shuffle_mode in ['dungeonssimple', 'dungeonsfull']): + continue + + def runner(world): + if main_mode == 'inverted': + link_inverted_entrances(world, 1) + else: + link_entrances(world, 1) + run_tests(tests, shuffle_mode, main_mode, ls, runner) + + +def compare_tests(tests, shuffle_mode, main_mode, links=False, runner_old=None, runner_new=None): + print(f'Testing {shuffle_mode} {main_mode}') + entrance_set = set() + exit_set = set() + ctr = defaultdict(Counter) + start = time.time() + test_loop(tests, entrance_set, exit_set, ctr, shuffle_mode, main_mode, links, runner_old) + print(f'Old test took {time.time() - start}s') + start = time.time() + test_loop(tests, entrance_set, exit_set, ctr, shuffle_mode, main_mode, links, runner_new, -1) + # test_loop(tests, entrance_set, exit_set, ctr, shuffle_mode, main_mode, links, runner_new) + print(f'New test took {time.time() - start}s') + dump_file(tests, entrance_set, exit_set, ctr, shuffle_mode, main_mode, links) + + +def run_tests(tests, shuffle_mode, main_mode, links=False, runner=None): + print(f'Testing {shuffle_mode} {main_mode}') + entrance_set = set() + exit_set = set() + ctr = defaultdict(Counter) + test_loop(tests, entrance_set, exit_set, ctr, shuffle_mode, main_mode, links, runner) + + +def test_loop(tests, entrance_set, exit_set, ctr, shuffle_mode, main_mode, links, runner, value=1): + for i in range(0, tests): + if i % 1000 == 0: + print(f'Test {i}') + seed = random.randint(0, 999999999) + # seed = 635441530 + random.seed(seed) + world = World(1, {1: shuffle_mode}, {1: 'vanilla'}, {1: 'noglitches'}, {1: main_mode}, {}, {}, {}, + {}, {}, {}, {}, {}, True, {}, {}, [], {}) + world.customizer = False + world.shufflelinks = {1: links} + if world.mode[1] != 'inverted': + create_regions(world, 1) + else: + create_inverted_regions(world, 1) + create_dungeon_regions(world, 1) + # print(f'Linking seed {seed}') + # try: + runner(world) + # except Exception as e: + # print(f'Failure during seed {seed} with {e}') + for data in world.spoiler.entrances.values(): + ent = data['entrance'] + ext = data['exit'] + # drc = data['direction'] + entrance_set.add(ent) + exit_set.add(ext) + ctr[ent][ext] += value + + +def dump_file(tests, entrance_set, exit_set, ctr, shuffle_mode, main_mode, links): + columns = sorted(list(exit_set)) + rows = sorted(list(entrance_set)) + filename = f'er_stats_{shuffle_mode}_{main_mode}_{"ls" if links else "v"}.csv' + with open(filename, 'w') as stat_file: + stat_file.write(',') + stat_file.write(','.join(columns)) + stat_file.write('\n') + for r in rows: + stat_file.write(f'{r},') + formatted = [] + for c in columns: + occurance = ctr[r][c] / tests + formatted.append(f'{occurance:.5%}') + stat_file.write(','.join(formatted)) + stat_file.write('\n') + + +if __name__ == "__main__": + # logging.basicConfig(format='%(message)s', level=logging.DEBUG) + # run_tests(1, 'restricted', 'inverted', True, lambda world: link_entrances_new(world, 1)) + # run_test_stats() + # run_old_stats() + run_stats() diff --git a/test/stats/__init__.py b/test/stats/__init__.py new file mode 100644 index 00000000..e69de29b From 5e07e49798bd67ac9e8fdd132844109a4c677001 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 25 Mar 2022 08:43:11 -0600 Subject: [PATCH 02/12] Fix documentation and example Fix msu_resume issue Fix empty locations Throw error on unknown items --- Fill.py | 2 + Items.py | 3 +- Main.py | 2 +- Rom.py | 2 +- docs/Customizer.md | 4 +- docs/SuperTrueIceRodHunt.yaml | 125 +++++++++++++++++++++++++++ docs/customizer_example.yaml | 2 +- source/gui/adjust/overview.py | 1 + source/overworld/EntranceShuffle2.py | 6 +- 9 files changed, 139 insertions(+), 8 deletions(-) create mode 100644 docs/SuperTrueIceRodHunt.yaml diff --git a/Fill.py b/Fill.py index 43a3abc0..03fb6fed 100644 --- a/Fill.py +++ b/Fill.py @@ -451,6 +451,8 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None def ensure_good_pots(world, write_skips=False): for loc in world.get_locations(): + if loc.item is None: + loc.item = ItemFactory('Nothing', loc.player) # convert Arrows 5 and Nothing when necessary if (loc.item.name in {'Arrows (5)', 'Nothing'} and (loc.type != LocationType.Pot or loc.item.player != loc.player)): diff --git a/Items.py b/Items.py index 757fc8e5..7f6c5054 100644 --- a/Items.py +++ b/Items.py @@ -14,8 +14,7 @@ def ItemFactory(items, player): advancement, priority, type, code, price, pedestal_hint, pedestal_credit, sickkid_credit, zora_credit, witch_credit, fluteboy_credit, hint_text = item_table[item] ret.append(Item(item, advancement, priority, type, code, price, pedestal_hint, pedestal_credit, sickkid_credit, zora_credit, witch_credit, fluteboy_credit, hint_text, player)) else: - logging.getLogger('').warning('Unknown Item: %s', item) - return None + raise RuntimeError(f'Unknown Item: {item}') if singleton: return ret[0] diff --git a/Main.py b/Main.py index 147f4ca6..07adc036 100644 --- a/Main.py +++ b/Main.py @@ -33,7 +33,7 @@ from source.overworld.EntranceShuffle2 import link_entrances_new from source.tools.BPS import create_bps_from_data from source.classes.CustomSettings import CustomSettings -__version__ = '1.0.1.12v' +__version__ = '1.0.1.12w' from source.classes.BabelFish import BabelFish diff --git a/Rom.py b/Rom.py index 398a7c1b..4a228d79 100644 --- a/Rom.py +++ b/Rom.py @@ -2268,7 +2268,7 @@ def write_strings(rom, world, player, team): random.shuffle(items_to_hint) hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull'] else 8 hint_count += 2 if world.doorShuffle[player] == 'crossed' else 0 - while hint_count > 0: + while hint_count > 0 and len(items_to_hint) > 0: this_item = items_to_hint.pop(0) this_location = world.find_items_not_key_only(this_item, player) random.shuffle(this_location) diff --git a/docs/Customizer.md b/docs/Customizer.md index af9ae7ed..047deefe 100644 --- a/docs/Customizer.md +++ b/docs/Customizer.md @@ -160,14 +160,14 @@ This is done as `: ` E.g. `Skull Woods: Helmasaur King` for helmacopter. Be sure to turn on at least one enemizer setting for the bosses to actually be randomized. -### startinventory +### start_inventory This must be defined by player. Each player number should be listed with a list of items to start with. This is a yaml list (note the hyphens): ``` -startinventory: +start_inventory: 1: - Pegasus Boots - Progressive Sword diff --git a/docs/SuperTrueIceRodHunt.yaml b/docs/SuperTrueIceRodHunt.yaml new file mode 100644 index 00000000..3b644bf9 --- /dev/null +++ b/docs/SuperTrueIceRodHunt.yaml @@ -0,0 +1,125 @@ +meta: + algorithm: balanced + players: 1 +settings: + 1: + door_shuffle: vanilla + dropshuffle: true + experimental: true + goal: ganon + hints: false + intensity: 1 + pseudoboots: false + pottery: lottery + shopsanity: false + shuffle: vanilla + shufflelinks: false + shufflebosses: false +item_pool: + 1: + Ice Rod: 1 + Progressive Sword: 1 +placements: + 1: + Turtle Rock - Boss: Triforce +start_inventory: + 1: + - Progressive Bow + - Progressive Bow + - Blue Boomerang + - Red Boomerang + - Hookshot + - Mushroom + - Magic Powder + - Fire Rod + - Bombos + - Ether + - Quake + - Lamp + - Hammer + - Ocarina + - Bug Catching Net + - Book of Mudora + - Shovel + - Cane of Somaria + - Cane of Byrna + - Cape + - Magic Mirror + - Moon Pearl + - Titans Mitts + - Tempered Sword + - Pegasus Boots + - Flippers + - Red Mail + - Progressive Shield + - Progressive Shield + - Bottle (Red Potion) + - Bottle (Green Potion) + - Bottle (Blue Potion) + - Bottle (Fairy) + - Magic Upgrade (1/2) + - Magic Upgrade (1/2) + - Boss Heart Container + - Boss Heart Container + - Boss Heart Container + - Boss Heart Container + - Boss Heart Container + - Boss Heart Container + - Boss Heart Container + - Boss Heart Container + - Boss Heart Container + - Boss Heart Container + - Boss Heart Container + - Boss Heart Container + - Boss Heart Container + - Boss Heart Container + - Boss Heart Container + - Boss Heart Container + - Boss Heart Container + - Arrows (10) + - Arrows (10) + - Arrows (10) + - Bombs (10) + - Rupees (300) + - Rupees (300) + - Rupees (300) + - Rupees (300) + - Rupees (300) + - Rupees (300) + - Rupees (300) + - Rupees (300) + - Rupees (300) + - Rupees (300) + - Rupees (300) + - Rupees (300) + - Rupees (300) + - Rupees (300) + - Rupees (300) + - Rupees (300) + - Rupees (300) + - Rupees (300) + - Rupees (300) + - Rupees (300) + - Rupees (300) + - Rupees (300) + - Rupees (300) + - Rupees (300) + - Rupees (300) + - Rupees (300) + - Rupees (300) + - Rupees (300) + - Rupees (300) + - Rupees (300) + - Rupees (300) + - Rupees (300) + - Rupees (300) + - Rupees (50) + - Rupees (20) + - Rupees (20) + - Rupees (5) + - Rupee (1) + - Rupee (1) + - Rupee (1) + - Rupee (1) + + \ No newline at end of file diff --git a/docs/customizer_example.yaml b/docs/customizer_example.yaml index 3100e6c2..3d0c7624 100644 --- a/docs/customizer_example.yaml +++ b/docs/customizer_example.yaml @@ -102,7 +102,7 @@ bosses: Palace of Darkness: Arrghus Thieves Town: Blind Ganons Tower (top): Vitreous -startinventory: +start_inventory: 1: - Pegasus Boots diff --git a/source/gui/adjust/overview.py b/source/gui/adjust/overview.py index 7ab7f474..607c16b1 100644 --- a/source/gui/adjust/overview.py +++ b/source/gui/adjust/overview.py @@ -158,6 +158,7 @@ def adjust_page(top, parent, settings): "nobgm": "disablemusic", "reduce_flashing": "reduce_flashing", "shuffle_sfx": "shuffle_sfx", + "msu_resume": "msu_resume", } guiargs = Namespace() for option in options: diff --git a/source/overworld/EntranceShuffle2.py b/source/overworld/EntranceShuffle2.py index b46e05e0..f69a475f 100644 --- a/source/overworld/EntranceShuffle2.py +++ b/source/overworld/EntranceShuffle2.py @@ -17,6 +17,8 @@ class EntrancePool(object): self.links_on_mountain = False self.decoupled_entrances = [] self.decoupled_exits = [] + self.original_entrances = set() + self.original_exits = set() self.world = world self.player = player @@ -48,6 +50,8 @@ def link_entrances_new(world, player): avail_pool.exits.add('Inverted Dark Sanctuary Exit') inverted_substitution(avail_pool, avail_pool.entrances, True, True) inverted_substitution(avail_pool, avail_pool.exits, False, True) + avail_pool.original_entrances.update(avail_pool.entrances) + avail_pool.original_exits.update(avail_pool.exits) default_map = {} default_map.update(entrance_map) one_way_map = {} @@ -378,7 +382,7 @@ def do_holes_and_linked_drops(entrances, exits, avail, cross_world, keep_togethe hole_entrances, hole_targets = [], [] for hole in drop_map: - if hole in entrances and hole in linked_drop_map: + if hole in avail.original_entrances and hole in linked_drop_map: linked_entrance = linked_drop_map[hole] if hole in entrances and linked_entrance in entrances: hole_entrances.append((linked_entrance, hole)) From b5684cbf7dddcf09c7e4794a3af935ce6359a323 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 29 Mar 2022 12:13:19 -0600 Subject: [PATCH 03/12] Minor bug with ER customization --- source/overworld/EntranceShuffle2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/overworld/EntranceShuffle2.py b/source/overworld/EntranceShuffle2.py index f69a475f..20578cc5 100644 --- a/source/overworld/EntranceShuffle2.py +++ b/source/overworld/EntranceShuffle2.py @@ -946,9 +946,9 @@ def find_entrances_and_exits(avail_pool, entrance_pool): for item in entrance_pool: if item in avail_pool.entrances: entrances.append(item) - if 'Links House' in item: - targets.append('Chris Houlihan Room Exit') if item in entrance_map and entrance_map[item] in avail_pool.exits: + if item in ['Links House Exit', 'Inverted Links House Exit']: + targets.append('Chris Houlihan Room Exit') targets.append(entrance_map[item]) elif item in single_entrance_map and single_entrance_map[item] in avail_pool.exits: targets.append(single_entrance_map[item]) From cf227aafc03be29b650dd5d3ea7041748409c87c Mon Sep 17 00:00:00 2001 From: aerinon Date: Mon, 4 Apr 2022 13:37:07 -0600 Subject: [PATCH 04/12] Trinity goal --- BaseClasses.py | 7 ++- CLI.py | 4 +- Fill.py | 5 +- ItemList.py | 57 ++++++++++++------- Mystery.py | 3 + Rom.py | 12 ++-- Rules.py | 6 +- mystery_example.yml | 7 +++ resources/app/cli/args.json | 1 + resources/app/cli/lang/en.json | 4 +- resources/app/gui/lang/en.json | 1 + resources/app/gui/randomize/item/widgets.json | 1 + source/tools/MysteryUtils.py | 7 ++- 13 files changed, 78 insertions(+), 37 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index b1b58cd9..1c057b1c 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -2532,7 +2532,7 @@ class Spoiler(object): outfile.write('Retro: %s\n' % ('Yes' if self.metadata['retro'][player] else 'No')) outfile.write('Swords: %s\n' % self.metadata['weapons'][player]) outfile.write('Goal: %s\n' % self.metadata['goal'][player]) - if self.metadata['goal'][player] == 'triforcehunt': + if self.metadata['goal'][player] in ['triforcehunt', 'trinity']: outfile.write('Triforce Pieces Required: %s\n' % self.metadata['triforcegoal'][player]) outfile.write('Triforce Pieces Total: %s\n' % self.metadata['triforcepool'][player]) outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][player]) @@ -2548,7 +2548,8 @@ class Spoiler(object): outfile.write('Crystals required for GT: %s\n' % (str(self.metadata['gt_crystals'][player]) + addition)) addition = ' (Random)' if self.world.crystals_ganon_orig[player] == 'random' else '' outfile.write('Crystals required for Ganon: %s\n' % (str(self.metadata['ganon_crystals'][player]) + addition)) - outfile.write('Pyramid hole pre-opened: %s\n' % ('Yes' if self.metadata['open_pyramid'][player] else 'No')) + if self.metadata['goal'][player] != 'trinity': + outfile.write('Pyramid hole pre-opened: %s\n' % ('Yes' if self.metadata['open_pyramid'][player] else 'No')) outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player]) outfile.write(f"Restricted Boss Items: {self.metadata['restricted_boss_items'][player]}\n") outfile.write('Map shuffle: %s\n' % ('Yes' if self.metadata['mapshuffle'][player] else 'No')) @@ -2742,7 +2743,7 @@ world_mode = {"open": 0, "standard": 1, "inverted": 2} sword_mode = {"random": 0, "assured": 1, "swordless": 2, "vanilla": 3} # byte 2: GGGD DFFH (goal, diff, item_func, hints) -goal_mode = {"ganon": 0, "pedestal": 1, "dungeons": 2, "triforcehunt": 3, "crystals": 4} +goal_mode = {'ganon': 0, 'pedestal': 1, 'dungeons': 2, 'triforcehunt': 3, 'crystals': 4, 'trinity': 5} diff_mode = {"normal": 0, "hard": 1, "expert": 2} func_mode = {"normal": 0, "hard": 1, "expert": 2} diff --git a/CLI.py b/CLI.py index 9a2914de..6a04e4c9 100644 --- a/CLI.py +++ b/CLI.py @@ -195,8 +195,8 @@ def parse_settings(): "mixed_travel": "prevent", "standardize_palettes": "standardize", - "triforce_pool": 30, - "triforce_goal": 20, + "triforce_pool": 0, + "triforce_goal": 0, "triforce_pool_min": 0, "triforce_pool_max": 0, "triforce_goal_min": 0, diff --git a/Fill.py b/Fill.py index 663ce09f..7199c1af 100644 --- a/Fill.py +++ b/Fill.py @@ -392,7 +392,10 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None else: max_trash = gt_count scaled_trash = math.floor(max_trash * scale_factor) - gftower_trash_count = (random.randint(scaled_trash, max_trash) if world.goal[player] == 'triforcehunt' else random.randint(0, scaled_trash)) + if world.goal[player] in ['triforcehunt', 'trinity']: + gftower_trash_count = random.randint(scaled_trash, max_trash) + else: + gftower_trash_count = random.randint(0, scaled_trash) gtower_locations = [location for location in fill_locations if location.parent_region.dungeon and location.parent_region.dungeon.name == 'Ganons Tower' and location.player == player] diff --git a/ItemList.py b/ItemList.py index a489f9fb..3884efc0 100644 --- a/ItemList.py +++ b/ItemList.py @@ -180,7 +180,7 @@ def get_custom_array_key(item): def generate_itempool(world, player): - if (world.difficulty[player] not in ['normal', 'hard', 'expert'] or world.goal[player] not in ['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'crystals'] + if (world.difficulty[player] not in ['normal', 'hard', 'expert'] or world.goal[player] not in ['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'trinity', 'crystals'] or world.mode[player] not in ['open', 'standard', 'inverted'] or world.timer not in ['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'] or world.progressive not in ['on', 'off', 'random']): raise NotImplementedError('Not supported yet') @@ -192,8 +192,8 @@ def generate_itempool(world, player): else: world.push_item(world.get_location('Ganon', player), ItemFactory('Triforce', player), False) - if world.goal[player] in ['triforcehunt']: - region = world.get_region('Light World',player) + if world.goal[player] in ['triforcehunt', 'trinity']: + region = world.get_region('Light World', player) loc = Location(player, "Murahdahla", parent=region) region.locations.append(loc) world.dynamic_locations.append(loc) @@ -338,14 +338,11 @@ def generate_itempool(world, player): if clock_mode is not None: world.clock_mode = clock_mode - if world.goal[player] == 'triforcehunt': - if world.treasure_hunt_count[player] == 0: - world.treasure_hunt_count[player] = 20 - if world.treasure_hunt_total[player] == 0: - world.treasure_hunt_total[player] = 30 + goal = world.goal[player] + if goal in ['triforcehunt', 'trinity']: + g, t = set_default_triforce(goal, world.treasure_hunt_count[player], world.treasure_hunt_total[player]) + world.treasure_hunt_count[player], world.treasure_hunt_total[player] = g, t world.treasure_hunt_icon[player] = 'Triforce Piece' - if world.custom: - world.treasure_hunt_count[player] = treasure_hunt_count world.itempool.extend([item for item in get_dungeon_item_pool(world) if item.player == player and ((item.smallkey and world.keyshuffle[player]) @@ -412,7 +409,6 @@ def generate_itempool(world, player): if world.pottery[player] not in ['none', 'cave']: world.itempool += [ItemFactory('Small Key (Universal)', player)] * 19 - create_dynamic_shop_locations(world, player) if world.pottery[player] not in ['none', 'keys']: @@ -788,9 +784,9 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, placed_items = {} precollected_items = [] clock_mode = None - if goal == 'triforcehunt': + if goal in ['triforcehunt', 'trinity']: if treasure_hunt_total == 0: - treasure_hunt_total = 30 + treasure_hunt_total = 30 if goal == 'triforcehunt' else 10 triforcepool = ['Triforce Piece'] * int(treasure_hunt_total) pool.extend(alwaysitems) @@ -866,7 +862,7 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, place_item('Link\'s Uncle', swords_to_use.pop()) place_item('Blacksmith', swords_to_use.pop()) place_item('Pyramid Fairy - Left', swords_to_use.pop()) - if goal != 'pedestal': + if goal not in ['pedestal', 'trinity']: place_item('Master Sword Pedestal', swords_to_use.pop()) else: place_item('Master Sword Pedestal', 'Triforce') @@ -888,7 +884,7 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, elif timer == 'timed-ohko': pool.extend(diff.timedohko) clock_mode = 'countdown-ohko' - if goal == 'triforcehunt': + if goal in ['triforcehunt', 'trinity']: pool.extend(triforcepool) for extra in diff.extras: @@ -896,7 +892,7 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, # note: massage item pool now handles shrinking the pool appropriately - if goal == 'pedestal' and swords != 'vanilla': + if goal in ['pedestal', 'trinity'] and swords != 'vanilla': place_item('Master Sword Pedestal', 'Triforce') if retro: pool = [item.replace('Single Arrow','Rupees (5)') for item in pool] @@ -945,6 +941,11 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s if customitemarray["triforce"] > total_items_to_place: customitemarray["triforce"] = total_items_to_place + # Triforce Pieces + if goal in ['triforcehunt', 'trinity']: + g, t = set_default_triforce(goal, customitemarray["triforcepiecesgoal"], customitemarray["triforcepieces"]) + customitemarray["triforcepiecesgoal"], customitemarray["triforcepieces"] = g, t + itemtotal = 0 # Bow to Silver Arrows Upgrade, including Generic Keys & Rupoors for x in [*range(0, 66 + 1), 68, 69]: @@ -979,7 +980,8 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s treasure_hunt_count = max(min(customitemarray["triforcepiecesgoal"], 254), 1) treasure_hunt_icon = 'Triforce Piece' # Ensure game is always possible to complete here, force sufficient pieces if the player is unwilling. - if (customitemarray["triforcepieces"] < treasure_hunt_count) and (goal == 'triforcehunt') and (customitemarray["triforce"] == 0): + if ((customitemarray["triforcepieces"] < treasure_hunt_count) and (goal in ['triforcehunt', 'trinity']) + and (customitemarray["triforce"] == 0)): extrapieces = treasure_hunt_count - customitemarray["triforcepieces"] pool.extend(['Triforce Piece'] * extrapieces) itemtotal = itemtotal + extrapieces @@ -991,7 +993,7 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s elif timer == 'ohko': clock_mode = 'ohko' - if goal == 'pedestal': + if goal in ['pedestal', 'trinity']: place_item('Master Sword Pedestal', 'Triforce') itemtotal = itemtotal + 1 @@ -1052,10 +1054,25 @@ def make_customizer_pool(world, player): return pool, placed_items, precollected_items, clock_mode, 1 + +# To display, count must be between 1 and 254 - larger values are not yet supported +def set_default_triforce(goal, custom_goal, custom_total): + triforce_goal, triforce_total = 0, 0 + if goal == 'triforcehunt': + triforce_goal, triforce_total = 20, 30 + elif goal == 'trinity': + triforce_goal, triforce_total = 8, 10 + if custom_goal > 0: + triforce_goal = max(min(custom_goal, 254), 1) + if custom_total > 0: + triforce_total = max(min(custom_total, 254), triforce_goal) + return triforce_goal, triforce_total + + # A quick test to ensure all combinations generate the correct amount of items. def test(): for difficulty in ['normal', 'hard', 'expert']: - for goal in ['ganon', 'triforcehunt', 'pedestal']: + for goal in ['ganon', 'triforcehunt', 'pedestal', 'trinity']: for timer in ['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown']: for mode in ['open', 'standard', 'inverted', 'retro']: for swords in ['random', 'assured', 'swordless', 'vanilla']: @@ -1069,7 +1086,7 @@ def test(): count = len(out[0]) + len(out[1]) correct_count = total_items_to_place - if goal == 'pedestal' and swords != 'vanilla': + if goal in ['pedestal', 'trinity'] and swords != 'vanilla': # pedestal goals generate one extra item correct_count += 1 if retro: diff --git a/Mystery.py b/Mystery.py index 593e9516..bc6351af 100644 --- a/Mystery.py +++ b/Mystery.py @@ -1,6 +1,9 @@ import argparse import logging import RaceRandom as random +import urllib.request +import urllib.parse +import yaml from DungeonRandomizer import parse_cli from Main import main as DRMain diff --git a/Rom.py b/Rom.py index 2cf72cfb..87ae5f46 100644 --- a/Rom.py +++ b/Rom.py @@ -719,7 +719,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): dr_flags = DROptions.Eternal_Mini_Bosses if world.doorShuffle[player] == 'vanilla' else DROptions.Town_Portal if world.doorShuffle[player] == 'crossed': dr_flags |= DROptions.Map_Info - if world.collection_rate[player] and world.goal[player] != 'triforcehunt': + if world.collection_rate[player] and world.goal[player] not in ['triforcehunt', 'trinity']: dr_flags |= DROptions.Debug if world.doorShuffle[player] == 'crossed' and world.logic[player] != 'nologic'\ and world.mixed_travel[player] == 'prevent': @@ -1234,7 +1234,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): # set up goals for treasure hunt rom.write_bytes(0x180165, [0x0E, 0x28] if world.treasure_hunt_icon[player] == 'Triforce Piece' else [0x0D, 0x28]) - if world.goal[player] == 'triforcehunt': + if world.goal[player] in ['triforcehunt', 'trinity']: rom.write_byte(0x180167, int(world.treasure_hunt_count[player]) % 256) rom.write_byte(0x180194, 1) # Must turn in triforced pieces (instant win not enabled) @@ -1261,7 +1261,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_bytes(0x50563, [0x3F, 0x14]) # disable below ganon chest rom.write_byte(0x50599, 0x00) # disable below ganon chest rom.write_bytes(0xE9A5, [0x7E, 0x00, 0x24]) # disable below ganon chest - rom.write_byte(0x18008B, 0x01 if world.open_pyramid[player] else 0x00) # pre-open Pyramid Hole + rom.write_byte(0x18008B, 0x01 if world.open_pyramid[player] or world.goal[player] == 'trinity' else 0x00) # pre-open Pyramid Hole rom.write_byte(0x18008C, 0x01 if world.crystals_needed_for_gt[player] == 0 else 0x00) # GT pre-opened if crystal requirement is 0 rom.write_byte(0xF5D73, 0xF0) # bees are catchable rom.write_byte(0xF5F10, 0xF0) # bees are catchable @@ -1447,7 +1447,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_byte(0x18003E, 0x01) # make ganon invincible elif world.goal[player] in ['dungeons']: rom.write_byte(0x18003E, 0x02) # make ganon invincible until all dungeons are beat - elif world.goal[player] in ['crystals']: + elif world.goal[player] in ['crystals', 'trinity']: rom.write_byte(0x18003E, 0x04) # make ganon invincible until all crystals else: rom.write_byte(0x18003E, 0x03) # make ganon invincible until all crystals and aga 2 are collected @@ -2414,6 +2414,10 @@ def write_strings(rom, world, player, team): tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.' tt['sign_ganon'] = 'You need to get to the pedestal... Ganon is invincible!' else: + if world.goal[player] == 'trinity': + trinity_crystal_text = ('%d crystal to beat Ganon.' if world.crystals_needed_for_ganon[player] == 1 else '%d crystals to beat Ganon.') % world.crystals_needed_for_ganon[player] + tt['sign_ganon'] = 'Three ways to victory! %s Get to it!' % trinity_crystal_text + tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\ninvisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\nhidden in a hollow tree. If you bring\n%d triforce pieces, I can reassemble it." % int(world.treasure_hunt_count[player]) tt['ganon_fall_in'] = Ganon1_texts[random.randint(0, len(Ganon1_texts) - 1)] tt['ganon_fall_in_alt'] = 'You cannot defeat me until you finish your goal!' tt['ganon_phase_3_alt'] = 'Got wax in\nyour ears?\nI can not die!' diff --git a/Rules.py b/Rules.py index 5268b409..0ab4fd9b 100644 --- a/Rules.py +++ b/Rules.py @@ -58,7 +58,7 @@ def set_rules(world, player): elif world.goal[player] == 'ganon': # require aga2 to beat ganon add_rule(world.get_location('Ganon', player), lambda state: state.has('Beat Agahnim 2', player)) - elif world.goal[player] == 'triforcehunt': + elif world.goal[player] in ['triforcehunt', 'trinity']: add_rule(world.get_location('Murahdahla', player), lambda state: state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) >= int(state.world.treasure_hunt_count[player])) if world.mode[player] != 'inverted': @@ -873,7 +873,7 @@ def default_rules(world, player): set_rule(world.get_entrance('Floating Island Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has_Pearl(player) and state.has_sword(player) and state.has_turtle_rock_medallion(player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword required to cast magic (!) - set_rule(world.get_entrance('Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or world.open_pyramid[player]) + set_rule(world.get_entrance('Pyramid Hole', player), lambda state: world.open_pyramid[player] or world.goal[player] == 'trinity' or state.has('Beat Agahnim 2', player)) if world.swords[player] == 'swordless': swordless_rules(world, player) @@ -1025,7 +1025,7 @@ def inverted_rules(world, player): set_rule(world.get_entrance('Dark Grassy Lawn Flute', player), lambda state: state.can_flute(player)) set_rule(world.get_entrance('Hammer Peg Area Flute', player), lambda state: state.can_flute(player)) - set_rule(world.get_entrance('Inverted Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or world.open_pyramid[player]) + set_rule(world.get_entrance('Inverted Pyramid Hole', player), lambda state: world.open_pyramid[player] or world.goal[player] == 'trinity' or state.has('Beat Agahnim 2', player)) if world.swords[player] == 'swordless': swordless_rules(world, player) diff --git a/mystery_example.yml b/mystery_example.yml index abd46ee5..a7f26ddb 100644 --- a/mystery_example.yml +++ b/mystery_example.yml @@ -1,4 +1,10 @@ description: Example door rando weights + algorithm: + balanced: 12 + vanilla_fill: 1 + major_only: 1 + dungeon_only: 1 + district: 1 door_shuffle: vanilla: 0 basic: 2 @@ -52,6 +58,7 @@ dungeons: 1 pedestal: 2 triforce-hunt: 2 + trinity: 2 triforce_goal_min: 10 triforce_goal_max: 30 triforce_pool_min: 20 diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index b2769bbf..ad971548 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -43,6 +43,7 @@ "pedestal", "dungeons", "triforcehunt", + "trinity", "crystals" ] }, diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index d815cb6d..40bf1a70 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -104,7 +104,9 @@ "All Dungeons: Collect all crystals, pendants, beat both", " Agahnim fights and then defeat Ganon.", "Triforce Hunt: Places 30 Triforce Pieces in the world, collect", - " 20 of them to beat the game." + " 20 of them to beat the game.", + "Trinity: Can beat the game by defeating Ganon, pulling", + " Pedestal, or delivering Triforce Pieces." ], "difficulty": [ "Select game difficulty. Affects available itempool. (default: %(default)s)", diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index c81dcb61..dc31d613 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -226,6 +226,7 @@ "randomizer.item.goal.pedestal": "Master Sword Pedestal", "randomizer.item.goal.dungeons": "All Dungeons", "randomizer.item.goal.triforcehunt": "Triforce Hunt", + "randomizer.item.goal.trinity": "Trinity", "randomizer.item.goal.crystals": "Crystals", "randomizer.item.crystals_gt": "Crystals to open GT", diff --git a/resources/app/gui/randomize/item/widgets.json b/resources/app/gui/randomize/item/widgets.json index 2177335b..76537817 100644 --- a/resources/app/gui/randomize/item/widgets.json +++ b/resources/app/gui/randomize/item/widgets.json @@ -35,6 +35,7 @@ "pedestal", "dungeons", "triforcehunt", + "trinity", "crystals" ] }, diff --git a/source/tools/MysteryUtils.py b/source/tools/MysteryUtils.py index 6cc1d4ff..6e456ff8 100644 --- a/source/tools/MysteryUtils.py +++ b/source/tools/MysteryUtils.py @@ -49,7 +49,7 @@ def roll_settings(weights): ret.mystery = get_choice_default('mystery', default=True) glitch_map = {'none': 'noglitches', 'no_logic': 'nologic', 'owglitches': 'owglitches', - 'minorglitches': 'minorglitches'} + 'owg': 'owglitches', 'minorglitches': 'minorglitches'} glitches_required = get_choice('glitches_required') if glitches_required is not None: if glitches_required not in glitch_map.keys(): @@ -103,9 +103,10 @@ def roll_settings(weights): 'fast_ganon': 'crystals', 'dungeons': 'dungeons', 'pedestal': 'pedestal', - 'triforce-hunt': 'triforcehunt' + 'triforce-hunt': 'triforcehunt', + 'trinity': 'trinity' }[goal] - ret.openpyramid = goal == 'fast_ganon' if ret.shuffle in ['vanilla', 'dungeonsfull', 'dungeonssimple'] else False + ret.openpyramid = goal in ['fast_ganon', 'trinity'] if ret.shuffle in ['vanilla', 'dungeonsfull', 'dungeonssimple'] else False ret.crystals_gt = get_choice('tower_open') From 52a0e88d12d274a4c222a28b81ec247a6493f84a Mon Sep 17 00:00:00 2001 From: aerinon Date: Mon, 4 Apr 2022 14:29:40 -0600 Subject: [PATCH 05/12] Minor ohko fix --- DoorShuffle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index df97441f..676546b4 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1967,7 +1967,7 @@ ohko_forbidden = { def filter_dashable_candidates(candidates, world): - forbidden_set = dashable_forbidden if world.can_take_damage else ohko_forbidden + forbidden_set = dashable_forbidden if world.timer in ['ohko', 'timed-ohko'] else ohko_forbidden return [x for x in candidates if x not in forbidden_set and x.dest not in forbidden_set] From 833b570af6f9d939ec6cd27e9cd006a86c286a06 Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 13 Apr 2022 13:22:15 -0600 Subject: [PATCH 06/12] Customizer item_pool updates --- ItemList.py | 149 +++++++++++++++++++++++++++---- RELEASENOTES.md | 4 + docs/Customizer.md | 12 +-- source/classes/CustomSettings.py | 13 +++ 4 files changed, 154 insertions(+), 24 deletions(-) diff --git a/ItemList.py b/ItemList.py index 3884efc0..f43a935d 100644 --- a/ItemList.py +++ b/ItemList.py @@ -1,4 +1,4 @@ -from collections import namedtuple +from collections import namedtuple, defaultdict import logging import math import RaceRandom as random @@ -257,15 +257,17 @@ def generate_itempool(world, player): world.get_location('Zelda Drop Off', player).locked = True # set up item pool + skip_pool_adjustments = False if world.customizer and world.customizer.get_item_pool(): (pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms) = make_customizer_pool(world, player) + skip_pool_adjustments = True elif world.custom: (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) = make_custom_item_pool(world.progressive, world.shuffle[player], world.difficulty[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.bombbag[player], world.customitemarray) world.rupoor_cost = min(world.customitemarray[player]["rupoorcost"], 9999) else: (pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms) = get_pool_core(world.progressive, world.shuffle[player], world.difficulty[player], world.treasure_hunt_total[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.bombbag[player], world.doorShuffle[player], world.logic[player]) - if player in world.pool_adjustment.keys(): + if player in world.pool_adjustment.keys() and not skip_pool_adjustments: amt = world.pool_adjustment[player] if amt < 0: trash_options = [x for x in pool if x in trash_items] @@ -311,7 +313,7 @@ def generate_itempool(world, player): world.get_location(location, player).event = True world.get_location(location, player).locked = True - if world.shopsanity[player]: + if world.shopsanity[player] and not skip_pool_adjustments: for shop in world.shops[player]: if shop.region.name in shop_to_location_table: for index, slot in enumerate(shop.inventory): @@ -373,7 +375,10 @@ def generate_itempool(world, player): return item if not choice else ItemFactory("Bee Trap", player) if choice == 'trap' else ItemFactory("Bee", player) return item - world.itempool += [beemizer(item) for item in items] + if not skip_pool_adjustments: + world.itempool += [beemizer(item) for item in items] + else: + world.itempool += items # shuffle medallions mm_medallion, tr_medallion = None, None @@ -403,15 +408,15 @@ def generate_itempool(world, player): set_up_shops(world, player) if world.retro[player]: - set_up_take_anys(world, player) - if world.dropshuffle[player]: + set_up_take_anys(world, player, skip_pool_adjustments) + if world.dropshuffle[player] and not skip_pool_adjustments: world.itempool += [ItemFactory('Small Key (Universal)', player)] * 13 - if world.pottery[player] not in ['none', 'cave']: + if world.pottery[player] not in ['none', 'cave'] and not skip_pool_adjustments: world.itempool += [ItemFactory('Small Key (Universal)', player)] * 19 create_dynamic_shop_locations(world, player) - if world.pottery[player] not in ['none', 'keys']: + if world.pottery[player] not in ['none', 'keys'] and not skip_pool_adjustments: add_pot_contents(world, player) @@ -426,7 +431,7 @@ take_any_locations = [ 'Dark Lake Hylia Ledge Spike Cave', 'Fortune Teller (Dark)', 'Dark Sanctuary Hint', 'Dark Desert Hint'] -def set_up_take_anys(world, player): +def set_up_take_anys(world, player, skip_adjustments=False): if world.mode[player] == 'inverted': if 'Dark Sanctuary Hint' in take_any_locations: take_any_locations.remove('Dark Sanctuary Hint') @@ -450,12 +455,13 @@ def set_up_take_anys(world, player): sword = next((item for item in world.itempool if item.type == 'Sword' and item.player == player), None) if sword: - world.itempool.append(ItemFactory('Rupees (20)', player)) - if not world.shopsanity[player]: - world.itempool.remove(sword) + if not skip_adjustments: + world.itempool.append(ItemFactory('Rupees (20)', player)) + if not world.shopsanity[player]: + world.itempool.remove(sword) old_man_take_any.shop.add_inventory(0, sword.name, 0, 0, create_location=True) else: - if world.shopsanity[player]: + if world.shopsanity[player] and not skip_adjustments: world.itempool.append(ItemFactory('Rupees (300)', player)) old_man_take_any.shop.add_inventory(0, 'Rupees (300)', 0, 0, create_location=world.shopsanity[player]) @@ -473,7 +479,7 @@ def set_up_take_anys(world, player): world.shops[player].append(take_any.shop) take_any.shop.add_inventory(0, 'Blue Potion', 0, 0, create_location=world.shopsanity[player]) take_any.shop.add_inventory(1, 'Boss Heart Container', 0, 0, create_location=world.shopsanity[player]) - if world.shopsanity[player]: + if world.shopsanity[player] and not skip_adjustments: world.itempool.append(ItemFactory('Blue Potion', player)) world.itempool.append(ItemFactory('Boss Heart Container', player)) @@ -1032,12 +1038,62 @@ def make_customizer_pool(world, player): assert loc not in placed_items placed_items[loc] = item + dungeon_locations, dungeon_count = defaultdict(set), defaultdict(int) + for l in world.get_unfilled_locations(player): + if l.parent_region.dungeon: + dungeon = l.parent_region.dungeon + dungeon_locations[dungeon.name].add(l) + if dungeon.name not in dungeon_count: + d_count = 1 if dungeon.big_key else 0 + d_count += len(dungeon.small_keys) + len(dungeon.dungeon_items) + dungeon_count[dungeon.name] = d_count + diff = difficulties[world.difficulty[player]] for item_name, amount in world.customizer.get_item_pool()[player].items(): if isinstance(amount, int): if item_name == 'Bottle (Random)': for _ in range(amount): pool.append(random.choice(diff.bottles)) + elif item_name.startswith('Small Key') and item_name != 'Small Key (Universal)': + d_item = ItemFactory(item_name, player) + if not world.keyshuffle[player]: + d_name = d_item.dungeon + dungeon = world.get_dungeon(d_name, player) + target_amount = max(amount, len(dungeon.small_keys)) + additional_amount = target_amount - len(dungeon.small_keys) + possible_fit = min(additional_amount, len(dungeon_locations[d_name])-dungeon_count[d_name]) + if possible_fit > 0: + dungeon_count[d_name] += possible_fit + dungeon.small_keys.extend([d_item] * amount) + additional_amount -= possible_fit + if additional_amount > 0: + pool.extend([item_name] * amount) + else: + dungeon = world.get_dungeon(d_item.dungeon, player) + target_amount = max(amount, len(dungeon.small_keys)) + additional_amount = target_amount - len(dungeon.small_keys) + dungeon.small_keys.extend([d_item] * additional_amount) + elif item_name.startswith('Big Key') or item_name.startswith('Map') or item_name.startswith('Compass'): + d_item = ItemFactory(item_name, player) + if ((d_item.bigkey and not world.bigkeyshuffle[player]) + or (d_item.compass and not world.compassshuffle[player]) + or (d_item.map and not world.mapshuffle[player])): + d_name = d_item.dungeon + dungeon = world.get_dungeon(d_name, player) + current_amount = 1 if d_item == dungeon.big_key or d_item in dungeon.dungeon_items else 0 + additional_amount = amount - current_amount + possible_fit = min(additional_amount, len(dungeon_locations[d_name])-dungeon_count[d_name]) + if possible_fit > 0: + dungeon_count[d_name] += possible_fit + dungeon.dungeon_items.extend([d_item] * amount) + additional_amount -= possible_fit + if additional_amount > 0: + pool.extend([item_name] * amount) + else: + dungeon = world.get_dungeon(d_item.dungeon, player) + current_amount = 1 if d_item == dungeon.big_key or d_item in dungeon.dungeon_items else 0 + additional_amount = amount - current_amount + dungeon.dungeon_items.extend([d_item] * additional_amount) else: pool.extend([item_name] * amount) @@ -1049,12 +1105,75 @@ def make_customizer_pool(world, player): elif timer == 'ohko': clock_mode = 'ohko' - if world.goal[player] == 'pedestal': + if world.goal[player] in ['pedestal', 'trinity']: place_item('Master Sword Pedestal', 'Triforce') + guaranteed_items = alwaysitems + ['Magic Mirror', 'Moon Pearl'] + if world.shopsanity[player]: + guaranteed_items.extend(['Blue Potion', 'Green Potion', 'Red Potion']) + if world.retro[player]: + guaranteed_items.append('Small Key (Universal)') + for item in guaranteed_items: + if item not in pool: + pool.append(item) + + glove_count = sum(1 for i in pool if i == 'Progressive Glove') + glove_count = 2 if next((i for i in pool if i == 'Titans Glove'), None) is not None else glove_count + for i in range(glove_count, 2): + pool.append('Progressive Glove') + + if world.bombbag[player]: + if 'Bomb Upgrade (+10)' not in pool: + pool.append('Bomb Upgrade (+10)') + + if world.swords[player] != 'swordless': + beam_swords = {'Master Sword', 'Tempered Sword', 'Golden Sword'} + sword_count = sum(1 for i in pool if i in 'Progressive Sword') + sword_count = 2 if next((i for i in pool if i in beam_swords), None) is not None else sword_count + for i in range(sword_count, 2): + pool.append('Progressive Sword') + + bow_found = next((i for i in pool if i in {'Bow', 'Progressive Bow'}), None) + if not bow_found: + pool.append('Progressive Bow') + + heart_found = next((i for i in pool if i in {'Boss Heart Container', 'Sanctuary Heart Container'}), None) + if heart_found is None: + pool.append('Boss Heart Container') + + g, t = set_default_triforce(world.goal[player], world.treasure_hunt_count[player], + world.treasure_hunt_total[player]) + if t != 0: + pieces = sum(1 for i in pool if i == 'Triforce Piece') + if pieces < t: + pool.extend(['Triforce Piece'] * (t - pieces)) + + ttl_locations = sum(1 for x in world.get_unfilled_locations(player) if '- Prize' not in x.name) + pool_size = len(get_player_dungeon_item_pool(world, player)) + len(pool) + + if pool_size < ttl_locations: + amount_to_add = ttl_locations - pool_size + pool.extend(random.choices(list(filler_items.keys()), filler_items.values(), k=amount_to_add)) + return pool, placed_items, precollected_items, clock_mode, 1 +filler_items = { + 'Arrows (10)': 12, + 'Bombs (3)': 16, + 'Rupees (300)': 5, + 'Rupees (100)': 1, + 'Rupees (50)': 7, + 'Rupees (20)': 28, + 'Rupees (5)': 4, +} + + +def get_player_dungeon_item_pool(world, player): + return [item for dungeon in world.dungeons for item in dungeon.all_items + if dungeon.player == player and item.location is None] + + # To display, count must be between 1 and 254 - larger values are not yet supported def set_default_triforce(goal, custom_goal, custom_total): triforce_goal, triforce_total = 0, 0 diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 206232df..f3acda80 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -154,6 +154,10 @@ Same as above but both small keys and bigs keys of the dungeon are not allowed o ## Notes and Bug Fixes +#### Customizer + +* Fixed up the item_pool section to skip a lot of pool manipulations. Key item will be added to the bow if not detected. Extra dungeon items can be added to the pool and will be confined to the dungeon if possible (and not shuffled). If the pool isn't full, junk items are added to the pool to fill it out. + #### Volatile * 1.0.1.12 diff --git a/docs/Customizer.md b/docs/Customizer.md index 047deefe..cb951fed 100644 --- a/docs/Customizer.md +++ b/docs/Customizer.md @@ -50,20 +50,14 @@ Rom/Adjust flags like sprite, quickswap are not outputing with the print_custom_ This must be defined by player. Each player number should be listed with the appropriate pool. -Then each player can have the entire item pool defined. The name of item should be followed by the number of that item in the pool. All key items need to be listed here for now. +Then each player can have the entire item pool defined. The name of item should be followed by the number of that item in the pool. Many key items will be added to the pool if not detected. `Bottle (Random)` is supported to randomize bottle contents according to those allowed by difficulty. Pendants and crystals are supported here. -##### Known Issues +##### Caveat -1. Dungeon items amount can be increased but not eliminated (as the amount of each dungeon item is either pre-determined or calculated by door rando) and these extra items may not be confined to the dungeon -2. Door rando removes Red Rupees from the pool to make room for extra dungeon items as needed. -3. Shopsanity appends extra shop items to the pool. -4. Beemizer runs after pool creation changing junk items into bees -5. Retro + Shopsanity adds more items to the pool -6. Retro + either of dropshuffle or pottery adds keys to the pool -7. Most pottery settings add a large amount of junk items to the pool +Dungeon items amount can be increased (but not decreased as the minimum of each dungeon item is either pre-determined or calculated by door rando) if the type of dungeon item is not shuffled then it is attempted to be placed in the dungeon. Extra item beyond dungeon capacity not be confined to the dungeon. ### placements diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py index 3b05f7bf..7aef1796 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -75,6 +75,12 @@ class CustomSettings(object): args.shopsanity[p] = get_setting(settings['shopsanity'], args.shopsanity[p]) args.dropshuffle[p] = get_setting(settings['dropshuffle'], args.dropshuffle[p]) args.pottery[p] = get_setting(settings['pottery'], args.pottery[p]) + + if get_setting(settings['keydropshuffle'], args.keydropshuffle[p]): + args.dropshuffle[p] = True + if args.pottery[p] == 'none': + args.pottery[p] = 'keys' + args.mixed_travel[p] = get_setting(settings['mixed_travel'], args.mixed_travel[p]) args.standardize_palettes[p] = get_setting(settings['standardize_palettes'], args.standardize_palettes[p]) @@ -88,6 +94,13 @@ class CustomSettings(object): args.keyshuffle[p] = get_setting(settings['keyshuffle'], args.keyshuffle[p]) args.mapshuffle[p] = get_setting(settings['mapshuffle'], args.mapshuffle[p]) args.compassshuffle[p] = get_setting(settings['compassshuffle'], args.compassshuffle[p]) + + if get_setting(settings['keysanity'], args.keysanity): + args.bigkeyshuffle[p] = True + args.keyshuffle[p] = True + args.mapshuffle[p] = True + args.compassshuffle[p] = True + args.shufflebosses[p] = get_setting(settings['shufflebosses'], args.shufflebosses[p]) args.shuffleenemies[p] = get_setting(settings['shuffleenemies'], args.shuffleenemies[p]) args.enemy_health[p] = get_setting(settings['enemy_health'], args.enemy_health[p]) From b3c9f1cf03d4ae7c79940b4d84177157607dde59 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 14 Apr 2022 10:16:51 -0600 Subject: [PATCH 07/12] List nearly everything in custom yaml (spoiler skip and shuffled not the same right now) --- source/classes/CustomSettings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py index 7aef1796..0145c319 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -259,7 +259,7 @@ class CustomSettings(object): for p in self.player_range: placements[p] = {} for location in world.get_locations(): - if location.type != LocationType.Logical and not location.skip: + if location.type != LocationType.Logical: if location.player != location.item.player: placements[location.player][location.name] = f'{location.item.name}#{location.item.player}' else: From 8d5fe3088917ca9fff460f1be8c0c0f193856912 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 21 Apr 2022 11:26:24 -0600 Subject: [PATCH 08/12] Fix wording --- RELEASENOTES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 35e9c5d4..424e4c45 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -12,7 +12,7 @@ New pottery option that control which pots (and large blocks) are in the locatio not include key pots. * CaveKeys: Both non-dungeon pots and pots that used to have keys are in the pool. * Reduced: Same as CaveKeys but also roughly a quarter of dungeon pots are added to the location pool picked at random. This is a dynamic mode so pots in the pool will be colored. Pots out of the pool will have vanilla contents. -* Clustered: LIke reduced but pot are grouped by logical sets and roughly 50% of pots are chosen from those group. This is a dynamic mode like the above. +* Clustered: Like reduced but pots are grouped by logical sets and roughly 50% of pots are chosen from those group. This is a dynamic mode like the above. * Nonempty: All pots that had some sort of objects under them are chosen to be in the location pool. This excludes most large blocks and some pots out of dungeons. * Dungeon Pots: The pots that are in dungeons are in the pool. (Includes serveral large blocks) * Lottery: All pots and large blocks are in the pool @@ -166,7 +166,7 @@ Same as above but both small keys and bigs keys of the dungeon are not allowed o #### Customizer -* Fixed up the item_pool section to skip a lot of pool manipulations. Key item will be added to the bow if not detected. Extra dungeon items can be added to the pool and will be confined to the dungeon if possible (and not shuffled). If the pool isn't full, junk items are added to the pool to fill it out. +* Fixed up the item_pool section to skip a lot of pool manipulations. Key items will be added (like the bow) if not detected. Extra dungeon items can be added to the pool and will be confined to the dungeon if possible (and not shuffled). If the pool isn't full, junk items are added to the pool to fill it out. #### Volatile From dbb69359aa64b44e62f589998a0ba6adb348c9a2 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 12 May 2022 14:38:53 -0600 Subject: [PATCH 09/12] Fix customizer GUI yaml selection --- RELEASENOTES.md | 1 + source/gui/bottom.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 424e4c45..d91029d8 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -166,6 +166,7 @@ Same as above but both small keys and bigs keys of the dungeon are not allowed o #### Customizer +* Fixed up the GUI selection of the customizer file. * Fixed up the item_pool section to skip a lot of pool manipulations. Key items will be added (like the bow) if not detected. Extra dungeon items can be added to the pool and will be confined to the dungeon if possible (and not shuffled). If the pool isn't full, junk items are added to the pool to fill it out. #### Volatile diff --git a/source/gui/bottom.py b/source/gui/bottom.py index 5efeeac3..b887e1ac 100644 --- a/source/gui/bottom.py +++ b/source/gui/bottom.py @@ -220,6 +220,11 @@ def create_guiargs(parent): # Get baserom path guiargs.rom = parent.pages["randomizer"].pages["generation"].widgets["rom"].storageVar.get() + # Get customizer path + customizer_value = parent.pages["randomizer"].pages["generation"].widgets["customizer"].storageVar.get() + if customizer_value and customizer_value != 'None': + guiargs.customizer = customizer_value + # Get if we're using the Custom Item Pool guiargs.custom = bool(parent.pages["randomizer"].pages["generation"].widgets["usecustompool"].storageVar.get()) From 9a6b7c624d8d45f8f2b2cae75eb1d6ac6491d13f Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 18 May 2022 15:16:09 -0600 Subject: [PATCH 10/12] Minor fix for mystery --- source/classes/CustomSettings.py | 1 + source/tools/MysteryUtils.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py index 0145c319..2a8a39f6 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -59,6 +59,7 @@ class CustomSettings(object): if isinstance(player_setting, str): weights = get_weights(os.path.join(self.relative_dir, player_setting)) settings = defaultdict(lambda: None, vars(roll_settings(weights))) + args.mystery = True else: settings = defaultdict(lambda: None, player_setting) args.shuffle[p] = get_setting(settings['shuffle'], args.shuffle[p]) diff --git a/source/tools/MysteryUtils.py b/source/tools/MysteryUtils.py index 51c6c153..3fbbbf8d 100644 --- a/source/tools/MysteryUtils.py +++ b/source/tools/MysteryUtils.py @@ -46,7 +46,6 @@ def roll_settings(weights): ret = argparse.Namespace() ret.algorithm = get_choice('algorithm') - ret.mystery = get_choice_default('mystery', default=True) glitch_map = {'none': 'noglitches', 'no_logic': 'nologic', 'owglitches': 'owglitches', 'owg': 'owglitches', 'minorglitches': 'minorglitches'} From 041443be3d72bcf9e95673f0a3c3674499d3b47c Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 19 May 2022 10:28:19 -0600 Subject: [PATCH 11/12] Fix windows absolute path parsing --- source/classes/CustomSettings.py | 12 +++++++----- source/tools/MysteryUtils.py | 13 ++++++++----- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py index 2a8a39f6..c31e628f 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -4,6 +4,7 @@ import urllib.parse import yaml from yaml.representer import Representer from collections import defaultdict +from pathlib import Path import RaceRandom as random from BaseClasses import LocationType, DoorType @@ -334,10 +335,11 @@ class CustomSettings(object): def load_yaml(path): try: - if urllib.parse.urlparse(path).scheme: + return yaml.load(path, Loader=yaml.SafeLoader) + except yaml.YAMLError as exc: + if os.path.exists(Path(path)): + with open(path, "r", encoding="utf-8") as f: + return yaml.load(f, Loader=yaml.SafeLoader) + elif urllib.parse.urlparse(path).scheme in ['http', 'https']: return yaml.load(urllib.request.urlopen(path), Loader=yaml.FullLoader) - with open(path, 'r', encoding='utf-8') as f: - return yaml.load(f, Loader=yaml.SafeLoader) - except Exception as e: - raise Exception(f'Failed to read customizer file: {e}') diff --git a/source/tools/MysteryUtils.py b/source/tools/MysteryUtils.py index 3fbbbf8d..9df4eca4 100644 --- a/source/tools/MysteryUtils.py +++ b/source/tools/MysteryUtils.py @@ -1,5 +1,7 @@ import argparse import RaceRandom as random +import os +from pathlib import Path import urllib.request import urllib.parse @@ -8,12 +10,13 @@ import yaml def get_weights(path): try: - if urllib.parse.urlparse(path).scheme: + return yaml.load(path, Loader=yaml.SafeLoader) + except yaml.YAMLError as exc: + if os.path.exists(Path(path)): + with open(path, "r", encoding="utf-8") as f: + return yaml.load(f, Loader=yaml.SafeLoader) + elif urllib.parse.urlparse(path).scheme in ['http', 'https']: return yaml.load(urllib.request.urlopen(path), Loader=yaml.FullLoader) - with open(path, 'r', encoding='utf-8') as f: - return yaml.load(f, Loader=yaml.SafeLoader) - except Exception as e: - raise Exception(f'Failed to read weights file: {e}') def roll_settings(weights): From e40c4290c9f4d14cad6283a761dda2585ff85f1c Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 19 May 2022 12:20:30 -0600 Subject: [PATCH 12/12] Fix yaml parsing --- source/classes/CustomSettings.py | 13 +++++-------- source/tools/MysteryUtils.py | 13 +++++-------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py index c31e628f..016b3b64 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -334,12 +334,9 @@ class CustomSettings(object): def load_yaml(path): - try: - return yaml.load(path, Loader=yaml.SafeLoader) - except yaml.YAMLError as exc: - if os.path.exists(Path(path)): - with open(path, "r", encoding="utf-8") as f: - return yaml.load(f, Loader=yaml.SafeLoader) - elif urllib.parse.urlparse(path).scheme in ['http', 'https']: - return yaml.load(urllib.request.urlopen(path), Loader=yaml.FullLoader) + if os.path.exists(Path(path)): + with open(path, "r", encoding="utf-8") as f: + return yaml.load(f, Loader=yaml.SafeLoader) + elif urllib.parse.urlparse(path).scheme in ['http', 'https']: + return yaml.load(urllib.request.urlopen(path), Loader=yaml.FullLoader) diff --git a/source/tools/MysteryUtils.py b/source/tools/MysteryUtils.py index 9df4eca4..415433c0 100644 --- a/source/tools/MysteryUtils.py +++ b/source/tools/MysteryUtils.py @@ -9,14 +9,11 @@ import yaml def get_weights(path): - try: - return yaml.load(path, Loader=yaml.SafeLoader) - except yaml.YAMLError as exc: - if os.path.exists(Path(path)): - with open(path, "r", encoding="utf-8") as f: - return yaml.load(f, Loader=yaml.SafeLoader) - elif urllib.parse.urlparse(path).scheme in ['http', 'https']: - return yaml.load(urllib.request.urlopen(path), Loader=yaml.FullLoader) + if os.path.exists(Path(path)): + with open(path, "r", encoding="utf-8") as f: + return yaml.load(f, Loader=yaml.SafeLoader) + elif urllib.parse.urlparse(path).scheme in ['http', 'https']: + return yaml.load(urllib.request.urlopen(path), Loader=yaml.FullLoader) def roll_settings(weights):