From dfb9ebfbdb64015a58326d6dc2827ba6b3e6b956 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 22 Mar 2022 16:13:31 -0600 Subject: [PATCH 01/63] 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/63] 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/63] 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/63] 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/63] 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/63] 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/63] 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 6076441fbfcbc9c143547856bfe181a8139fe81a Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 4 Mar 2022 16:45:51 -0700 Subject: [PATCH 08/63] Fix some interior doors during key door identification as missing one reachable side --- DoorShuffle.py | 15 ++++++++++----- Main.py | 2 +- RELEASENOTES.md | 2 ++ 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index 3d3a6eda..8c0b8e25 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -14,6 +14,7 @@ from RoomData import DoorKind, PairedDoor, reset_rooms from DungeonGenerator import ExplorationState, convert_regions, generate_dungeon, pre_validate, determine_required_paths, drop_entrances from DungeonGenerator import create_dungeon_builders, split_dungeon_builder, simple_dungeon_builder, default_dungeon_entrances from DungeonGenerator import dungeon_portals, dungeon_drops, GenerationException +from DungeonGenerator import valid_region_to_explore as valid_region_to_explore_lim from KeyDoorShuffle import analyze_dungeon, build_key_layout, validate_key_layout, determine_prize_lock from Utils import ncr, kth_combination @@ -1368,6 +1369,8 @@ def combine_layouts(recombinant_builders, dungeon_builders, entrances_map): dungeon_builders[recombine.name] = recombine +# todo: this allows cross-dungeon exploring via HC Ledge or Inaccessible Regions +# todo: @deprecated def valid_region_to_explore(region, world, player): return region and (region.type == RegionType.Dungeon or region.name in world.inaccessible_regions[player] @@ -1559,7 +1562,7 @@ okay_normals = [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind. def find_key_door_candidates(region, checked, world, player): - dungeon = region.dungeon + dungeon_name = region.dungeon.name candidates = [] checked_doors = list(checked) queue = deque([(region, None, None)]) @@ -1569,14 +1572,16 @@ def find_key_door_candidates(region, checked, world, player): d = ext.door if d and d.controller: d = d.controller - if d and not d.blocked and not d.entranceFlag and d.dest is not last_door and d.dest is not last_region and d not in checked_doors: + if d and not d.blocked and d.dest is not last_door and d.dest is not last_region and d not in checked_doors: valid = False - if 0 <= d.doorListPos < 4 and d.type in [DoorType.Interior, DoorType.Normal, DoorType.SpiralStairs]: + if (0 <= d.doorListPos < 4 and d.type in [DoorType.Interior, DoorType.Normal, DoorType.SpiralStairs] + and not d.entranceFlag): room = world.get_room(d.roomIndex, player) position, kind = room.doorList[d.doorListPos] - if d.type == DoorType.Interior: valid = kind in [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable] + if valid and d.dest not in candidates: # interior doors are not separable yet + candidates.append(d.dest) elif d.type == DoorType.SpiralStairs: valid = kind in [DoorKind.StairKey, DoorKind.StairKey2, DoorKind.StairKeyLow] elif d.type == DoorType.Normal: @@ -1595,7 +1600,7 @@ def find_key_door_candidates(region, checked, world, player): if valid and d not in candidates: candidates.append(d) connected = ext.connected_region - if connected and (connected.type != RegionType.Dungeon or connected.dungeon == dungeon): + if valid_region_to_explore_lim(connected, dungeon_name, world, player): queue.append((ext.connected_region, d, current)) if d is not None: checked_doors.append(d) diff --git a/Main.py b/Main.py index 573f935f..7dc012e7 100644 --- a/Main.py +++ b/Main.py @@ -29,7 +29,7 @@ from Fill import sell_potions, sell_keys, balance_multiworld_progression, balanc from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops from Utils import output_path, parse_player_names -__version__ = '1.0.0-dev' +__version__ = '1.0.1-dev' from source.classes.BabelFish import BabelFish diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 7a224c7c..b59df407 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -15,6 +15,8 @@ CLI: ```--bombbag``` # Bug Fixes and Notes. +* 1.0.1 + * Fixed a bug with key doors not detecting one side of an interior door * 0.5.1.7 * Baserom update * Fix for Inverted Mode: Dark Lake Hylia shop defaults to selling a blue potion From 481516cc4c2af6644560d8a8cef6f7bee568ef80 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 24 Feb 2022 14:39:45 -0700 Subject: [PATCH 09/63] SSL not verified for alttpr.com/sprites --- source/classes/SpriteSelector.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/source/classes/SpriteSelector.py b/source/classes/SpriteSelector.py index cb76e64c..9a18cacd 100644 --- a/source/classes/SpriteSelector.py +++ b/source/classes/SpriteSelector.py @@ -4,6 +4,7 @@ import json import os import random import shutil +import ssl from urllib.parse import urlparse from urllib.request import urlopen import webbrowser @@ -149,7 +150,7 @@ class SpriteSelector(object): try: task.update_status("Downloading official sprites list") - with urlopen('https://alttpr.com/sprites') as response: + with urlopen('https://alttpr.com/sprites', context=ssl._create_unverified_context()) as response: sprites_arr = json.loads(response.read().decode("utf-8")) except Exception as e: resultmessage = "Error getting list of official sprites. Sprites not updated.\n\n%s: %s" % (type(e).__name__, e) From f9fdd6ede92c22b3b672b474f545c392331f0c3b Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 4 Mar 2022 16:46:46 -0700 Subject: [PATCH 10/63] Note for fix --- RELEASENOTES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b59df407..4c3ec552 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -17,6 +17,7 @@ CLI: ```--bombbag``` * 1.0.1 * Fixed a bug with key doors not detecting one side of an interior door + * Sprite selector fix for systems with SSL issues * 0.5.1.7 * Baserom update * Fix for Inverted Mode: Dark Lake Hylia shop defaults to selling a blue potion From 3df1072f0aa8cb8d70b5580d097a2bc83367f8ff Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 21 Jan 2022 16:15:51 -0700 Subject: [PATCH 11/63] Fix bombbag bug --- RELEASENOTES.md | 2 ++ Rules.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4c3ec552..28e4f5d7 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -15,6 +15,8 @@ CLI: ```--bombbag``` # Bug Fixes and Notes. +* 1.0.0.1 + * Add Light Hype Fairy to bombbag mode as needing bombs * 1.0.1 * Fixed a bug with key doors not detecting one side of an interior door * Sprite selector fix for systems with SSL issues diff --git a/Rules.py b/Rules.py index 83a2175b..c2bb27ca 100644 --- a/Rules.py +++ b/Rules.py @@ -568,7 +568,7 @@ def bomb_rules(world, player): bonkable_doors = ['Two Brothers House Exit (West)', 'Two Brothers House Exit (East)'] # Technically this is incorrectly defined, but functionally the same as what is intended. bombable_doors = ['Ice Rod Cave', 'Light World Bomb Hut', 'Light World Death Mountain Shop', 'Mini Moldorm Cave', 'Hookshot Cave Back to Middle', 'Hookshot Cave Front to Middle', 'Hookshot Cave Middle to Front','Hookshot Cave Middle to Back', - 'Dark Lake Hylia Ledge Fairy', 'Hype Cave', 'Brewery'] + 'Dark Lake Hylia Ledge Fairy', 'Hype Cave', 'Brewery', 'Light Hype Fairy'] for entrance in bonkable_doors: add_rule(world.get_entrance(entrance, player), lambda state: state.can_use_bombs(player) or state.has_Boots(player)) for entrance in bombable_doors: From 42c6e18655e1076ea1f50a647e459aa1f1713aa0 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 24 May 2022 09:36:52 -0600 Subject: [PATCH 12/63] Mailmap --- .mailmap | 1 + 1 file changed, 1 insertion(+) create mode 100644 .mailmap diff --git a/.mailmap b/.mailmap new file mode 100644 index 00000000..d8857508 --- /dev/null +++ b/.mailmap @@ -0,0 +1 @@ + \ No newline at end of file From 752c063a9abba5aaf3b20d52d266b8160fb5fe45 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 24 May 2022 10:00:40 -0600 Subject: [PATCH 13/63] Mailmap fix --- .mailmap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mailmap b/.mailmap index d8857508..9f3cc0db 100644 --- a/.mailmap +++ b/.mailmap @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file From 8d5fe3088917ca9fff460f1be8c0c0f193856912 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 21 Apr 2022 11:26:24 -0600 Subject: [PATCH 14/63] 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 15/63] 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 16/63] 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 17/63] 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 18/63] 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): From ac651253fe22a6b4242b46b4ebb6ba8aae7b4690 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 12 May 2022 14:21:05 -0600 Subject: [PATCH 19/63] Fix a couple bugs on generation --- DoorShuffle.py | 6 ++++-- TestSuite.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index 88b489f3..7b47c1ee 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -207,8 +207,9 @@ def vanilla_key_logic(world, player): world.dungeon_layouts[player][builder.name] = builder add_inaccessible_doors(world, player) + entrances_map, potentials, connections = determine_entrance_list(world, player) for builder in builders: - origin_list = find_accessible_entrances(world, player, builder) + origin_list = entrances_map[builder.name] start_regions = convert_regions(origin_list, world, player) doors = convert_key_doors(default_small_key_doors[builder.name], world, player) key_layout = build_key_layout(builder, start_regions, doors, world, player) @@ -1723,6 +1724,7 @@ def find_key_door_candidates(region, checked, world, player): current, last_door, last_region = queue.pop() for ext in current.exits: d = ext.door + controlled = d if d and d.controller: d = d.controller if d and not d.blocked and d.dest is not last_door and d.dest is not last_region and d not in checked_doors: @@ -1754,7 +1756,7 @@ def find_key_door_candidates(region, checked, world, player): candidates.append(d) connected = ext.connected_region if valid_region_to_explore_lim(connected, dungeon_name, world, player): - queue.append((ext.connected_region, d, current)) + queue.append((ext.connected_region, controlled, current)) if d is not None: checked_doors.append(d) return candidates, checked_doors diff --git a/TestSuite.py b/TestSuite.py index 062cb2eb..ede5ae53 100644 --- a/TestSuite.py +++ b/TestSuite.py @@ -45,7 +45,7 @@ def main(args=None): test("Vanilla ", "--shuffle vanilla") test("Retro ", "--retro --shuffle vanilla") - test("Keysanity ", "--shuffle vanilla --keydropshuffle drops_only --keysanity") + test("Keysanity ", "--shuffle vanilla --keydropshuffle --keysanity") test("Shopsanity", "--shuffle vanilla --shopsanity") test("Simple ", "--shuffle simple") test("Full ", "--shuffle full") From 3052bd75858428884edc8920e9b1e72daf7498f1 Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 25 May 2022 10:28:15 -0600 Subject: [PATCH 20/63] Spelling --- .mailmap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mailmap b/.mailmap index 9f3cc0db..1aecc0db 100644 --- a/.mailmap +++ b/.mailmap @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file From 14571508ae8fabc0318080e77067261988552e5b Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 24 May 2022 11:29:41 -0600 Subject: [PATCH 21/63] Init work on decoupling doors --- BaseClasses.py | 10 +- CLI.py | 3 +- DoorShuffle.py | 75 +- Main.py | 1 + mystery_example.yml | 1 + resources/app/cli/args.json | 4 + resources/app/cli/lang/en.json | 1 + resources/app/gui/lang/en.json | 1 + .../app/gui/randomize/dungeon/widgets.json | 1 + source/classes/CustomSettings.py | 2 + source/classes/constants.py | 1 + source/dungeon/DungeonStitcher.py | 848 ++++++++++++++++++ source/tools/MysteryUtils.py | 1 + 13 files changed, 916 insertions(+), 33 deletions(-) create mode 100644 source/dungeon/DungeonStitcher.py diff --git a/BaseClasses.py b/BaseClasses.py index 2b56d39f..7c040640 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -142,6 +142,7 @@ class World(object): set_player_attr('collection_rate', False) set_player_attr('colorizepots', False) set_player_attr('pot_pool', {}) + set_player_attr('decoupledoors', False) set_player_attr('shopsanity', False) set_player_attr('mixed_travel', 'prevent') @@ -2462,6 +2463,7 @@ class Spoiler(object): 'shufflelinks': self.world.shufflelinks, 'door_shuffle': self.world.doorShuffle, 'intensity': self.world.intensity, + 'decoupledoors': self.world.decoupledoors, 'item_pool': self.world.difficulty, 'item_functionality': self.world.difficulty_adjustments, 'gt_crystals': self.world.crystals_needed_for_gt, @@ -2544,6 +2546,7 @@ class Spoiler(object): outfile.write(f"Link's House Shuffled: {yn(self.metadata['shufflelinks'])}\n") outfile.write('Door Shuffle: %s\n' % self.metadata['door_shuffle'][player]) outfile.write('Intensity: %s\n' % self.metadata['intensity'][player]) + outfile.write(f"Decouple Doors: {yn(self.metadata['decoupledoors'][player])}\n") outfile.write(f"Drop Shuffle: {yn(self.metadata['dropshuffle'][player])}\n") outfile.write(f"Pottery Mode: {self.metadata['pottery'][player]}\n") outfile.write(f"Pot Shuffle (Legacy): {yn(self.metadata['potshuffle'][player])}\n") @@ -2759,7 +2762,7 @@ goal_mode = {'ganon': 0, 'pedestal': 1, 'dungeons': 2, 'triforcehunt': 3, 'cryst diff_mode = {"normal": 0, "hard": 1, "expert": 2} func_mode = {"normal": 0, "hard": 1, "expert": 2} -# byte 3: S?MM PIII (shop, unused, mixed, palettes, intensity) +# byte 3: SDMM PIII (shop, decouple doors, mixed, palettes, intensity) # keydrop now has it's own byte mixed_travel_mode = {"prevent": 0, "allow": 1, "force": 2} # intensity is 3 bits (reserves 4-7 levels) @@ -2813,7 +2816,8 @@ class Settings(object): (goal_mode[w.goal[p]] << 5) | (diff_mode[w.difficulty[p]] << 3) | (func_mode[w.difficulty_adjustments[p]] << 1) | (1 if w.hints[p] else 0), - (0x80 if w.shopsanity[p] else 0) | (mixed_travel_mode[w.mixed_travel[p]] << 4) + (0x80 if w.shopsanity[p] else 0) | (0x40 if w.decoupledoors[p] else 0) + | (mixed_travel_mode[w.mixed_travel[p]] << 4) | (0x8 if w.standardize_palettes[p] == "original" else 0) | (0 if w.intensity[p] == "random" else w.intensity[p]), @@ -2861,7 +2865,7 @@ class Settings(object): args.retro[p] = True if settings[1] & 0x01 else False args.hints[p] = True if settings[2] & 0x01 else False args.shopsanity[p] = True if settings[3] & 0x80 else False - # args.keydropshuffle[p] = True if settings[3] & 0x40 else False + args.decoupledoors[p] = True if settings[3] & 0x40 else False args.mixed_travel[p] = r(mixed_travel_mode)[(settings[3] & 0x30) >> 4] args.standardize_palettes[p] = "original" if settings[3] & 0x8 else "standardize" intensity = settings[3] & 0x7 diff --git a/CLI.py b/CLI.py index c659d3b5..77b020b4 100644 --- a/CLI.py +++ b/CLI.py @@ -125,7 +125,7 @@ def parse_cli(argv, no_defaults=False): 'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep', 'remote_items', 'shopsanity', 'dropshuffle', 'pottery', 'keydropshuffle', 'mixed_travel', 'standardize_palettes', 'code', 'reduce_flashing', 'shuffle_sfx', - 'msu_resume', 'collection_rate', 'colorizepots']: + 'msu_resume', 'collection_rate', 'colorizepots', 'decoupledoors']: value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name) if player == 1: setattr(ret, name, {1: value}) @@ -192,6 +192,7 @@ def parse_settings(): "keysanity": False, "door_shuffle": "basic", "intensity": 2, + 'decoupledoors': False, "experimental": False, "dungeon_counters": "default", "mixed_travel": "prevent", diff --git a/DoorShuffle.py b/DoorShuffle.py index 7b47c1ee..56a4594b 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -12,9 +12,11 @@ from Dungeons import dungeon_regions, region_starts, standard_starts, split_regi from Dungeons import dungeon_bigs, dungeon_hints from Items import ItemFactory from RoomData import DoorKind, PairedDoor, reset_rooms -from DungeonGenerator import ExplorationState, convert_regions, generate_dungeon, pre_validate, determine_required_paths, drop_entrances +from source.dungeon.DungeonStitcher import GenerationException, generate_dungeon +# from DungeonGenerator import generate_dungeon +from DungeonGenerator import ExplorationState, convert_regions, pre_validate, determine_required_paths, drop_entrances from DungeonGenerator import create_dungeon_builders, split_dungeon_builder, simple_dungeon_builder, default_dungeon_entrances -from DungeonGenerator import dungeon_portals, dungeon_drops, GenerationException +from DungeonGenerator import dungeon_portals, dungeon_drops from DungeonGenerator import valid_region_to_explore as valid_region_to_explore_lim from KeyDoorShuffle import analyze_dungeon, build_key_layout, validate_key_layout, determine_prize_lock from Utils import ncr, kth_combination @@ -780,7 +782,8 @@ def within_dungeon(world, player): for builder in world.dungeon_layouts[player].values(): shuffle_key_doors(builder, world, player) logging.getLogger('').info('%s: %s', world.fish.translate("cli", "cli", "keydoor.shuffle.time"), time.process_time()-start) - smooth_door_pairs(world, player) + if not world.decoupledoors[player]: + smooth_door_pairs(world, player) if world.intensity[player] >= 3: portal = world.get_portal('Sanctuary', player) @@ -1059,7 +1062,8 @@ def cross_dungeon(world, player): target_items += 19 # 19 pot keys d_items = target_items - all_dungeon_items_cnt world.pool_adjustment[player] = d_items - smooth_door_pairs(world, player) + if not world.decoupledoors[player]: + smooth_door_pairs(world, player) # Re-assign dungeon bosses gt = world.get_dungeon('Ganons Tower', player) @@ -1567,8 +1571,9 @@ def find_small_key_door_candidates(builder, start_regions, world, player): checked_doors.update(checked) flat_candidates = [] for candidate in candidates: - # not valid if: Normal and Pair in is Checked and Pair is not in Candidates - if candidate.type != DoorType.Normal or candidate.dest not in checked_doors or candidate.dest in candidates: + # not valid if: Normal Coupled and Pair in is Checked and Pair is not in Candidates + if (world.decoupledoors[player] or candidate.type != DoorType.Normal + or candidate.dest not in checked_doors or candidate.dest in candidates): flat_candidates.append(candidate) paired_candidates = build_pair_list(flat_candidates) @@ -1609,8 +1614,7 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True 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) + sample_list = build_sample_list(combinations) 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 @@ -1625,12 +1629,7 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True determine_prize_lock(key_layout, world, player) while not validate_key_layout(key_layout, world, player): itr += 1 - stop_early = False - if itr % 1000 == 0: - mark = time.process_time()-start - if (mark > 10 and itr*100/combinations > 50) or (mark > 20 and itr*100/combinations > 25) or mark > 30: - stop_early = True - if itr >= combinations or stop_early: + if itr >= len(sample_list): if not drop_keys: logger.info('No valid layouts for %s with %s doors', builder.name, builder.key_doors_num) return False @@ -1640,8 +1639,7 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True if builder.key_doors_num < 0: 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) + sample_list = build_sample_list(combinations) itr = 0 start = time.process_time() # reset time since itr reset proposal = kth_combination(sample_list[itr], key_door_pool, key_doors_needed) @@ -1660,6 +1658,20 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True return True +def build_sample_list(combinations): + if combinations <= 1000000: + sample_list = list(range(0, int(combinations))) + + else: + num_set = set() + while len(num_set) < 1000000: + num_set.add(random.randint(0, combinations)) + sample_list = list(num_set) + sample_list.sort() + random.shuffle(sample_list) + return sample_list + + def log_key_logic(d_name, key_logic): logger = logging.getLogger('') if logger.isEnabledFor(logging.DEBUG): @@ -1693,7 +1705,8 @@ def build_pair_list(flat_list): queue = deque(flat_list) while len(queue) > 0: d = queue.pop() - if d.dest in queue and d.type != DoorType.SpiralStairs: + paired = d.dest.dest == d + if d.dest in queue and d.type != DoorType.SpiralStairs and paired: paired_list.append((d, d.dest)) queue.remove(d.dest) else: @@ -1716,6 +1729,7 @@ okay_normals = [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind. def find_key_door_candidates(region, checked, world, player): + decoupled = world.decoupledoors[player] dungeon_name = region.dungeon.name candidates = [] checked_doors = list(checked) @@ -1730,7 +1744,7 @@ def find_key_door_candidates(region, checked, world, player): if d and not d.blocked and d.dest is not last_door and d.dest is not last_region and d not in checked_doors: valid = False if (0 <= d.doorListPos < 4 and d.type in [DoorType.Interior, DoorType.Normal, DoorType.SpiralStairs] - and not d.entranceFlag): + and not d.entranceFlag): room = world.get_room(d.roomIndex, player) position, kind = room.doorList[d.doorListPos] if d.type == DoorType.Interior: @@ -1740,18 +1754,21 @@ def find_key_door_candidates(region, checked, world, player): elif d.type == DoorType.SpiralStairs: valid = kind in [DoorKind.StairKey, DoorKind.StairKey2, DoorKind.StairKeyLow] elif d.type == DoorType.Normal: - d2 = d.dest - if d2 not in candidates: - if d2.type == DoorType.Normal: - room_b = world.get_room(d2.roomIndex, player) - pos_b, kind_b = room_b.doorList[d2.doorListPos] - valid = kind in okay_normals and kind_b in okay_normals and valid_key_door_pair(d, d2) - else: - valid = kind in okay_normals - if valid and 0 <= d2.doorListPos < 4: - candidates.append(d2) + if decoupled: + valid = kind in okay_normals else: - valid = True + d2 = d.dest + if d2 not in candidates: + if d2.type == DoorType.Normal: + room_b = world.get_room(d2.roomIndex, player) + pos_b, kind_b = room_b.doorList[d2.doorListPos] + valid = kind in okay_normals and kind_b in okay_normals and valid_key_door_pair(d, d2) + else: + valid = kind in okay_normals + if valid and 0 <= d2.doorListPos < 4: + candidates.append(d2) + else: + valid = True if valid and d not in candidates: candidates.append(d) connected = ext.connected_region diff --git a/Main.py b/Main.py index 1c2357bb..87289631 100644 --- a/Main.py +++ b/Main.py @@ -443,6 +443,7 @@ def copy_world(world): ret.enemy_damage = world.enemy_damage.copy() ret.beemizer = world.beemizer.copy() ret.intensity = world.intensity.copy() + ret.decoupledoors = world.decoupledoors.copy() ret.experimental = world.experimental.copy() ret.shopsanity = world.shopsanity.copy() ret.dropshuffle = world.dropshuffle.copy() diff --git a/mystery_example.yml b/mystery_example.yml index b51c58c5..8363d05d 100644 --- a/mystery_example.yml +++ b/mystery_example.yml @@ -17,6 +17,7 @@ 1: 2 2: 2 3: 4 + decoupledoors: off dropshuffle: on: 1 off: 1 diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 7c438cbf..8b971856 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -164,6 +164,10 @@ "3", "2", "1", "random" ] }, + "deoupledoors": { + "action": "store_true", + "type": "bool" + }, "experimental": { "action": "store_true", "type": "bool" diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 4e805b12..599915b4 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -215,6 +215,7 @@ "3: And shuffles dungeon lobbies", "random: Picks one of those at random" ], + "decoupledoors" : [ "Door entrances and exits are decoupled" ], "experimental": [ "Enable experimental features. (default: %(default)s)" ], "dungeon_counters": [ "Enable dungeon chest counters. (default: %(default)s)" ], "crystals_ganon": [ diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index c0ab9367..8e3f0f8d 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -55,6 +55,7 @@ "randomizer.dungeon.smallkeyshuffle": "Small Keys", "randomizer.dungeon.bigkeyshuffle": "Big Keys", "randomizer.dungeon.keydropshuffle": "Key Drop Shuffle (Legacy)", + "randomizer.dungeon.decoupledoors": "Decouple Doors", "randomizer.dungeon.dropshuffle": "Shuffle Enemy Key Drops", "randomizer.dungeon.potshuffle": "Pot Shuffle (Legacy)", "randomizer.dungeon.pottery": "Pottery", diff --git a/resources/app/gui/randomize/dungeon/widgets.json b/resources/app/gui/randomize/dungeon/widgets.json index 2a99ea1a..b45df12f 100644 --- a/resources/app/gui/randomize/dungeon/widgets.json +++ b/resources/app/gui/randomize/dungeon/widgets.json @@ -22,6 +22,7 @@ "width": 45 } }, + "decoupledoors": { "type": "checkbox" }, "keydropshuffle": { "type": "checkbox" }, "pottery": { "type": "selectbox", diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py index 016b3b64..57f9cf7e 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -87,6 +87,7 @@ class CustomSettings(object): args.standardize_palettes[p] = get_setting(settings['standardize_palettes'], args.standardize_palettes[p]) args.intensity[p] = get_setting(settings['intensity'], args.intensity[p]) + args.decoupledoors[p] = get_setting(settings['decoupledoors'], args.decoupledoors[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]) @@ -181,6 +182,7 @@ class CustomSettings(object): 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]['decoupledoors'] = world.decoupledoors[p] settings_dict[p]['logic'] = world.logic[p] settings_dict[p]['mode'] = world.mode[p] settings_dict[p]['swords'] = world.swords[p] diff --git a/source/classes/constants.py b/source/classes/constants.py index 4de4ac13..677e163b 100644 --- a/source/classes/constants.py +++ b/source/classes/constants.py @@ -95,6 +95,7 @@ SETTINGSTOPROCESS = { "bigkeyshuffle": "bigkeyshuffle", "dungeondoorshuffle": "door_shuffle", "dungeonintensity": "intensity", + "decoupledoors": "decoupledoors", "keydropshuffle": "keydropshuffle", "dropshuffle": "dropshuffle", "pottery": "pottery", diff --git a/source/dungeon/DungeonStitcher.py b/source/dungeon/DungeonStitcher.py new file mode 100644 index 00000000..e16b449d --- /dev/null +++ b/source/dungeon/DungeonStitcher.py @@ -0,0 +1,848 @@ +import RaceRandom as random +import collections +import logging +import time + +from BaseClasses import CrystalBarrier, DoorType, Hook, RegionType, Sector +from BaseClasses import hook_from_door, flooded_keys +from Regions import dungeon_events, flooded_keys_reverse + + +def pre_validate(builder, entrance_region_names, split_dungeon, world, player): + pass + # todo: determine the part of check_valid that are necessary here + + +def generate_dungeon(builder, entrance_region_names, split_dungeon, world, player): + if builder.valid_proposal: # we made this earlier in gen, just use it + proposed_map = builder.valid_proposal + else: + proposed_map = generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon, world, player) + builder.valid_proposal = proposed_map + queue = collections.deque(proposed_map.items()) + while len(queue) > 0: + a, b = queue.popleft() + if world.decoupledoors[player]: + connect_doors_one_way(a, b) + else: + connect_doors(a, b) + queue.remove((b, a)) + if len(builder.sectors) == 0: + return Sector() + available_sectors = list(builder.sectors) + master_sector = available_sectors.pop() + for sub_sector in available_sectors: + master_sector.regions.extend(sub_sector.regions) + master_sector.outstanding_doors.clear() + master_sector.r_name_set = None + return master_sector + + +def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon, world, player): + logger = logging.getLogger('') + name = builder.name + logger.debug(f'Generating Dungeon: {name}') + entrance_regions = convert_regions(entrance_region_names, world, player) + excluded = {} + for region in entrance_regions: + portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region == region), None) + if portal: + if portal.destination: + excluded[region] = None + elif len(entrance_regions) > 1: + p_region = portal.door.entrance.connected_region + access_region = next(x.parent_region for x in p_region.entrances + if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]) + if (access_region.name in world.inaccessible_regions[player] and + region.name not in world.enabled_entrances[player]): + excluded[region] = None + elif len(region.entrances) == 1: # for holes + access_region = next(x.parent_region for x in region.entrances + if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld] + or x.parent_region.name == 'Sewer Drop') + if access_region.name == 'Sewer Drop': + access_region = next(x.parent_region for x in access_region.entrances) + if (access_region.name in world.inaccessible_regions[player] and + region.name not in world.enabled_entrances[player]): + excluded[region] = None + entrance_regions = [x for x in entrance_regions if x not in excluded.keys()] + doors_to_connect, idx = {}, 0 + all_regions = set() + bk_special = False + for sector in builder.sectors: + for door in sector.outstanding_doors: + doors_to_connect[door.name] = door, idx + idx += 1 + all_regions.update(sector.regions) + bk_special |= check_for_special(sector.regions) + bk_needed = False + for sector in builder.sectors: + bk_needed |= determine_if_bk_needed(sector, split_dungeon, bk_special, world, player) + finished = False + # flag if standard and this is hyrule castle + paths = determine_paths_for_dungeon(world, player, all_regions, name) + proposed_map = create_random_proposal(doors_to_connect, world, player) + itr = 0 + hash_code = proposal_hash(doors_to_connect, proposed_map) + hash_code_set = set() + start = time.time() + while not finished: + if itr > 1000: + elasped = time.time() - start + raise GenerationException(f'Generation taking too long. {elasped}. Ref {name}') + if hash_code in hash_code_set: + proposed_map = create_random_proposal(doors_to_connect, world, player) + hash_code = proposal_hash(doors_to_connect, proposed_map) + if hash_code not in hash_code_set: + hash_code_set.add(hash_code) + explored_state = explore_proposal(name, entrance_regions, all_regions, proposed_map, doors_to_connect, + bk_needed, bk_special, world, player) + if check_valid(name, explored_state, proposed_map, doors_to_connect, all_regions, + bk_needed, bk_special, paths, entrance_regions, world, player): + finished = True + else: + proposed_map, hash_code = modify_proposal(proposed_map, explored_state, doors_to_connect, + hash_code_set, world, player) + itr += 1 + return proposed_map + + +def create_random_proposal(doors_to_connect, world, player): + logger = logging.getLogger('') + hooks = [Hook.North, Hook.South, Hook.East, Hook.West, Hook.Stairs] + primary_bucket = collections.defaultdict(list) + secondary_bucket = collections.defaultdict(list) + for name, door in doors_to_connect.items(): + door, idx = door + primary_bucket[hook_from_door(door)].append(door) + secondary_bucket[hook_from_door(door)].append(door) + proposal = {} + while True: + hooks_left, left = [], 0 + for hook in hooks: + hook_len = len(primary_bucket[hook]) + if hook_len > 0: + hooks_left.append(hook) + left += hook_len + if left == 0: + return proposal + next_hook = random.choice(hooks_left) + primary_door = random.choice(primary_bucket[next_hook]) + opp_hook, secondary_door = type_map[next_hook], None + while (secondary_door is None or secondary_door == primary_door + or decouple_check(primary_bucket[next_hook], secondary_bucket[opp_hook], + primary_door, secondary_door, world, player)): + secondary_door = random.choice(secondary_bucket[opp_hook]) + proposal[primary_door] = secondary_door + primary_bucket[next_hook].remove(primary_door) + secondary_bucket[opp_hook].remove(secondary_door) + if not world.decoupledoors[player]: + proposal[secondary_door] = primary_door + primary_bucket[opp_hook].remove(secondary_door) + secondary_bucket[next_hook].remove(primary_door) + logger.debug(f'Linking {primary_door.name} <-> {secondary_door.name}') + else: + logger.debug(f'Linking {primary_door.name} -> {secondary_door.name}') + + +def decouple_check(primary_list, secondary_list, primary_door, secondary_door, world, player): + if world.decoupledoors[player] and len(primary_list) == 2 and len(secondary_list) == 2: + primary_alone = next(d for d in primary_list if d != primary_door) + secondary_alone = next(d for d in secondary_list if d != secondary_door) + return primary_alone == secondary_alone + return False + + +def proposal_hash(doors_to_connect, proposed_map): + hash_code = '' + for name, door_pair in doors_to_connect.items(): + door, idx = door_pair + hash_code += str(idx) + str(doors_to_connect[proposed_map[door].name][1]) + return hash_code + + +def modify_proposal(proposed_map, explored_state, doors_to_connect, hash_code_set, world, player): + logger = logging.getLogger('') + hash_code, itr = None, 0 + while hash_code is None or hash_code in hash_code_set: + if itr > 10: + proposed_map = create_random_proposal(doors_to_connect, world, player) + hash_code = proposal_hash(doors_to_connect, proposed_map) + return proposed_map, hash_code + visited_bucket = collections.defaultdict(list) + unvisted_bucket = collections.defaultdict(list) + visited_choices = [] + unvisted_count = 0 + for door_one, door_two in proposed_map.items(): + if door_one in explored_state.visited_doors: + visited_bucket[hook_from_door(door_one)].append(door_one) + visited_choices.append(door_one) + else: + unvisted_bucket[hook_from_door(door_one)].append(door_one) + unvisted_count += 1 + if unvisted_count == 0: + # something is wrong beyond connectedness, crystal switch puzzle or bk layout - reshuffle + proposed_map = create_random_proposal(doors_to_connect, world, player) + hash_code = proposal_hash(doors_to_connect, proposed_map) + return proposed_map, hash_code + + attempt, opp_hook = None, None + opp_hook_len = 0 + while opp_hook_len == 0: + attempt = random.choice(visited_choices) + opp_hook = type_map[hook_from_door(attempt)] + opp_hook_len = len(unvisted_bucket[opp_hook]) + unvisted_bucket[opp_hook].sort(key=lambda d: d.name) + new_door = random.choice(unvisted_bucket[opp_hook]) + old_target = proposed_map[attempt] + proposed_map[attempt] = new_door + if not world.decoupledoors[player]: + old_attempt = proposed_map[new_door] + else: + old_attempt = next(x for x in proposed_map if proposed_map[x] == new_door) + proposed_map[old_attempt] = old_target + if not world.decoupledoors[player]: + proposed_map[old_target] = old_attempt + proposed_map[new_door] = attempt + hash_code = proposal_hash(doors_to_connect, proposed_map) + itr += 1 + + if not world.decoupledoors[player]: + logger.debug(f'Re-linking {attempt.name} <-> {new_door.name}') + logger.debug(f'Re-linking {old_attempt.name} <-> {old_target.name}') + else: + logger.debug(f'Re-Linking {attempt.name} -> {new_door.name}') + logger.debug(f'Re-Linking {old_attempt.name} -> {old_target.name}') + hash_code_set.add(hash_code) + return proposed_map, hash_code + + +def explore_proposal(name, entrance_regions, all_regions, proposed_map, valid_doors, + bk_needed, bk_special, world, player): + start = ExplorationState(dungeon=name) + start.big_key_special = bk_special + + bk_flag = False if world.bigkeyshuffle[player] and not bk_special else bk_needed + + def exception(d): + return name == 'Skull Woods 2' and d.name == 'Skull Pinball WS' + original_state = extend_reachable_state_improved(entrance_regions, start, proposed_map, all_regions, + valid_doors, bk_flag, world, player, exception) + return original_state + + +def check_valid(name, exploration_state, proposed_map, doors_to_connect, all_regions, + bk_needed, bk_special, paths, entrance_regions, world, player): + all_visited = set() + all_visited.update(exploration_state.visited_blue) + all_visited.update(exploration_state.visited_orange) + if len(all_regions.difference(all_visited)) > 0: + return False + if not valid_paths(name, paths, entrance_regions, doors_to_connect, all_regions, proposed_map, + bk_needed, bk_special, world, player): + return False + return True + + +def determine_if_bk_needed(sector, split_dungeon, bk_special, world, player): + if not split_dungeon or bk_special: + for region in sector.regions: + for ext in region.exits: + door = world.check_for_door(ext.name, player) + if door is not None and door.bigKey: + return True + return False + + +def check_for_special(regions): + for region in regions: + for loc in region.locations: + if loc.forced_big_key(): + return True + return False + + +def valid_paths(name, paths, entrance_regions, valid_doors, all_regions, proposed_map, + bk_needed, bk_special, world, player): + for path in paths: + if type(path) is tuple: + target = path[1] + start_regions = [] + for region in all_regions: + if path[0] == region.name: + start_regions.append(region) + break + else: + target = path + start_regions = entrance_regions + if not valid_path(name, start_regions, target, valid_doors, proposed_map, all_regions, + bk_needed, bk_special, world, player): + return False + return True + + +def valid_path(name, starting_regions, target, valid_doors, proposed_map, all_regions, + bk_needed, bk_special, world, player): + target_regions = set() + if type(target) is not list: + for region in all_regions: + if target == region.name: + target_regions.add(region) + break + else: + for region in all_regions: + if region.name in target: + target_regions.add(region) + + start = ExplorationState(dungeon=name) + start.big_key_special = bk_special + bk_flag = False if world.bigkeyshuffle[player] and not bk_special else bk_needed + + def exception(d): + return name == 'Skull Woods 2' and d.name == 'Skull Pinball WS' + original_state = extend_reachable_state_improved(starting_regions, start, proposed_map, all_regions, + valid_doors, bk_flag, world, player, exception) + + for exp_door in original_state.unattached_doors: + if not exp_door.door.blocked: + return True # outstanding connection possible + for target in target_regions: + if original_state.visited_at_all(target): + return True + return False # couldn't find an outstanding door or the target + + +boss_path_checks = ['Eastern Boss', 'Desert Boss', 'Hera Boss', 'Tower Agahnim 1', 'PoD Boss', 'Swamp Boss', + 'Skull Boss', 'Ice Boss', 'Mire Boss', 'TR Boss', 'GT Agahnim 2'] + +# pinball is allowed to orphan you +drop_path_checks = ['Skull Pot Circle', 'Skull Left Drop', 'Skull Back Drop', 'Sewers Rat Path'] + + +def determine_paths_for_dungeon(world, player, all_regions, name): + all_r_names = set(x.name for x in all_regions) + paths = [] + non_hole_portals = [] + for portal in world.dungeon_portals[player]: + if portal.door.entrance.parent_region in all_regions: + non_hole_portals.append(portal.door.entrance.parent_region.name) + if portal.destination: + paths.append(portal.door.entrance.parent_region.name) + if world.mode[player] == 'standard' and name == 'Hyrule Castle': + paths.append('Hyrule Dungeon Cellblock') + paths.append(('Hyrule Dungeon Cellblock', 'Sanctuary')) + if world.doorShuffle[player] in ['basic'] and name == 'Thieves Town': + paths.append('Thieves Attic Window') + elif 'Thieves Attic Window' in all_r_names: + paths.append('Thieves Attic Window') + for boss in boss_path_checks: + if boss in all_r_names: + paths.append(boss) + if 'Thieves Boss' in all_r_names: + paths.append('Thieves Boss') + if world.get_dungeon("Thieves Town", player).boss.enemizer_name == 'Blind': + paths.append(('Thieves Blind\'s Cell', 'Thieves Boss')) + for drop_check in drop_path_checks: + if drop_check in all_r_names: + paths.append((drop_check, non_hole_portals)) + return paths + + +def convert_regions(region_names, world, player): + region_list = [] + for name in region_names: + region_list.append(world.get_region(name, player)) + return region_list + + +type_map = { + Hook.Stairs: Hook.Stairs, + Hook.North: Hook.South, + Hook.South: Hook.North, + Hook.West: Hook.East, + Hook.East: Hook.West +} + + +def connect_doors(a, b): + # Return on unsupported types. + if a.type in [DoorType.Hole, DoorType.Warp, DoorType.Interior, DoorType.Logical]: + return + # Connect supported types + if a.type in [DoorType.Normal, DoorType.SpiralStairs, DoorType.Open, DoorType.StraightStairs, DoorType.Ladder]: + if a.blocked: + connect_one_way(b.entrance, a.entrance) + elif b.blocked: + connect_one_way(a.entrance, b.entrance) + else: + connect_two_way(a.entrance, b.entrance) + dep_doors, target = [], None + if len(a.dependents) > 0: + dep_doors, target = a.dependents, b + elif len(b.dependents) > 0: + dep_doors, target = b.dependents, a + if target is not None: + target_region = target.entrance.parent_region + for dep in dep_doors: + connect_simple_door(dep, target_region) + return + # If we failed to account for a type, panic + raise RuntimeError('Unknown door type ' + a.type.name) + + +def connect_doors_one_way(a, b): + # Return on unsupported types. + if a.type in [DoorType.Hole, DoorType.Warp, DoorType.Interior, DoorType.Logical]: + return + # Connect supported types + if a.type in [DoorType.Normal, DoorType.SpiralStairs, DoorType.Open, DoorType.StraightStairs, DoorType.Ladder]: + if not a.blocked: + connect_one_way(a.entrance, b.entrance) + dep_doors, target = [], None + if len(a.dependents) > 0: + dep_doors, target = a.dependents, b + if target is not None: + target_region = target.entrance.parent_region + for dep in dep_doors: + connect_simple_door(dep, target_region) + return + # If we failed to account for a type, panic + raise RuntimeError('Unknown door type ' + a.type.name) + + +def connect_two_way(entrance, ext): + + # if these were already connected somewhere, remove the backreference + if entrance.connected_region is not None: + entrance.connected_region.entrances.remove(entrance) + if ext.connected_region is not None: + ext.connected_region.entrances.remove(ext) + + entrance.connect(ext.parent_region) + ext.connect(entrance.parent_region) + if entrance.parent_region.dungeon: + ext.parent_region.dungeon = entrance.parent_region.dungeon + x = entrance.door + y = ext.door + if x is not None: + x.dest = y + if y is not None: + y.dest = x + + +def connect_one_way(entrance, ext): + + # if these were already connected somewhere, remove the backreference + if entrance.connected_region is not None: + entrance.connected_region.entrances.remove(entrance) + + entrance.connect(ext.parent_region) + if entrance.parent_region.dungeon: + ext.parent_region.dungeon = entrance.parent_region.dungeon + x = entrance.door + if x is not None: + x.dest = ext.door + + +def connect_simple_door(exit_door, region): + exit_door.entrance.connect(region) + exit_door.dest = region + + +special_big_key_doors = ['Hyrule Dungeon Cellblock Door', "Thieves Blind's Cell Door"] + + +class ExplorationState(object): + + def __init__(self, init_crystal=CrystalBarrier.Orange, dungeon=None): + + self.unattached_doors = [] + self.avail_doors = [] + self.event_doors = [] + + self.visited_orange = [] + self.visited_blue = [] + self.visited_doors = set() + self.events = set() + self.crystal = init_crystal + + # key region stuff + self.door_krs = {} + + # key validation stuff + self.small_doors = [] + self.big_doors = [] + self.opened_doors = [] + self.big_key_opened = False + self.big_key_special = False + + self.found_locations = [] + self.ttl_locations = 0 + self.used_locations = 0 + self.key_locations = 0 + self.used_smalls = 0 + self.bk_found = set() + + self.non_door_entrances = [] + self.dungeon = dungeon + self.pinball_used = False + + self.prize_door_set = {} + self.prize_doors = [] + self.prize_doors_opened = False + + def copy(self): + ret = ExplorationState(dungeon=self.dungeon) + ret.unattached_doors = list(self.unattached_doors) + ret.avail_doors = list(self.avail_doors) + ret.event_doors = list(self.event_doors) + ret.visited_orange = list(self.visited_orange) + ret.visited_blue = list(self.visited_blue) + ret.events = set(self.events) + ret.crystal = self.crystal + ret.door_krs = self.door_krs.copy() + + ret.small_doors = list(self.small_doors) + ret.big_doors = list(self.big_doors) + ret.opened_doors = list(self.opened_doors) + ret.big_key_opened = self.big_key_opened + ret.big_key_special = self.big_key_special + ret.ttl_locations = self.ttl_locations + ret.key_locations = self.key_locations + ret.used_locations = self.used_locations + ret.used_smalls = self.used_smalls + ret.found_locations = list(self.found_locations) + ret.bk_found = set(self.bk_found) + + ret.non_door_entrances = list(self.non_door_entrances) + ret.dungeon = self.dungeon + ret.pinball_used = self.pinball_used + + ret.prize_door_set = dict(self.prize_door_set) + ret.prize_doors = list(self.prize_doors) + ret.prize_doors_opened = self.prize_doors_opened + return ret + + def next_avail_door(self): + self.avail_doors.sort(key=lambda x: 0 if x.flag else 1 if x.door.bigKey else 2) + exp_door = self.avail_doors.pop() + self.crystal = exp_door.crystal + return exp_door + + def visit_region(self, region, key_region=None, key_checks=False, bk_flag=False): + if region.type != RegionType.Dungeon: + self.crystal = CrystalBarrier.Orange + if self.crystal == CrystalBarrier.Either: + if region not in self.visited_blue: + self.visited_blue.append(region) + if region not in self.visited_orange: + self.visited_orange.append(region) + elif self.crystal == CrystalBarrier.Orange: + self.visited_orange.append(region) + elif self.crystal == CrystalBarrier.Blue: + self.visited_blue.append(region) + if region.type == RegionType.Dungeon: + for location in region.locations: + if key_checks and location not in self.found_locations: + if location.forced_item and 'Small Key' in location.item.name: + self.key_locations += 1 + if location.name not in dungeon_events and '- Prize' not in location.name and location.name not in ['Agahnim 1', 'Agahnim 2']: + self.ttl_locations += 1 + if location not in self.found_locations: # todo: special logic for TT Boss? + self.found_locations.append(location) + if not bk_flag: + self.bk_found.add(location) + if location.name in dungeon_events and location.name not in self.events: + if self.flooded_key_check(location): + self.perform_event(location.name, key_region) + if location.name in flooded_keys_reverse.keys() and self.location_found( + flooded_keys_reverse[location.name]): + self.perform_event(flooded_keys_reverse[location.name], key_region) + if '- Prize' in location.name: + self.prize_received = True + + def flooded_key_check(self, location): + if location.name not in flooded_keys.keys(): + return True + return flooded_keys[location.name] in [x.name for x in self.found_locations] + + def location_found(self, location_name): + for l in self.found_locations: + if l.name == location_name: + return True + return False + + def perform_event(self, location_name, key_region): + self.events.add(location_name) + queue = collections.deque(self.event_doors) + while len(queue) > 0: + exp_door = queue.popleft() + if exp_door.door.req_event == location_name: + self.avail_doors.append(exp_door) + self.event_doors.remove(exp_door) + if key_region is not None: + d_name = exp_door.door.name + if d_name not in self.door_krs.keys(): + self.door_krs[d_name] = key_region + + def add_all_entrance_doors_check_unattached(self, region, world, player): + door_list = [x for x in get_doors(world, region, player) if x.type in [DoorType.Normal, DoorType.SpiralStairs]] + door_list.extend(get_entrance_doors(world, region, player)) + for door in door_list: + if self.can_traverse(door): + if door.dest is None and not self.in_door_list_ic(door, self.unattached_doors): + self.append_door_to_list(door, self.unattached_doors) + elif door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door, + self.event_doors): + self.append_door_to_list(door, self.event_doors) + elif not self.in_door_list(door, self.avail_doors): + self.append_door_to_list(door, self.avail_doors) + for entrance in region.entrances: + door = world.check_for_door(entrance.name, player) + if door is None: + self.non_door_entrances.append(entrance) + + def add_all_doors_check_unattached(self, region, world, player): + for door in get_doors(world, region, player): + if self.can_traverse(door): + if door.controller is not None: + door = door.controller + if door.dest is None and not self.in_door_list_ic(door, self.unattached_doors): + self.append_door_to_list(door, self.unattached_doors) + elif door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door, + self.event_doors): + self.append_door_to_list(door, self.event_doors) + elif not self.in_door_list(door, self.avail_doors): + self.append_door_to_list(door, self.avail_doors) + + def add_all_doors_check_proposed(self, region, proposed_map, valid_doors, flag, world, player, exception): + for door in get_doors(world, region, player): + if door in proposed_map and door.name in valid_doors: + self.visited_doors.add(door) + if door.blocked and exception(door): + self.pinball_used = True + if self.can_traverse(door, exception): + if door.controller is not None: + door = door.controller + if door.dest is None and door not in proposed_map.keys() and door.name in valid_doors: + if not self.in_door_list_ic(door, self.unattached_doors): + self.append_door_to_list(door, self.unattached_doors, flag) + else: + other = self.find_door_in_list(door, self.unattached_doors) + if self.crystal != other.crystal: + other.crystal = CrystalBarrier.Either + elif door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door, + self.event_doors): + self.append_door_to_list(door, self.event_doors, flag) + elif not self.in_door_list(door, self.avail_doors): + self.append_door_to_list(door, self.avail_doors, flag) + + def add_all_doors_check_key_region(self, region, key_region, world, player): + for door in get_doors(world, region, player): + if self.can_traverse(door): + if door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door, + self.event_doors): + self.append_door_to_list(door, self.event_doors) + elif not self.in_door_list(door, self.avail_doors): + self.append_door_to_list(door, self.avail_doors) + if door.name not in self.door_krs.keys(): + self.door_krs[door.name] = key_region + else: + if door.name not in self.door_krs.keys(): + self.door_krs[door.name] = key_region + + def add_all_doors_check_keys(self, region, key_door_proposal, world, player): + for door in get_doors(world, region, player): + if self.can_traverse(door): + if door.controller: + door = door.controller + if door in key_door_proposal and door not in self.opened_doors: + if not self.in_door_list(door, self.small_doors): + self.append_door_to_list(door, self.small_doors) + elif (door.bigKey or door.name in special_big_key_doors) and not self.big_key_opened: + if not self.in_door_list(door, self.big_doors): + self.append_door_to_list(door, self.big_doors) + elif door.req_event is not None and door.req_event not in self.events: + if not self.in_door_list(door, self.event_doors): + self.append_door_to_list(door, self.event_doors) + elif not self.in_door_list(door, self.avail_doors): + self.append_door_to_list(door, self.avail_doors) + + def visited(self, region): + if self.crystal == CrystalBarrier.Either: + return region in self.visited_blue and region in self.visited_orange + elif self.crystal == CrystalBarrier.Orange: + return region in self.visited_orange + elif self.crystal == CrystalBarrier.Blue: + return region in self.visited_blue + return False + + def visited_at_all(self, region): + return region in self.visited_blue or region in self.visited_orange + + def found_forced_bk(self): + for location in self.found_locations: + if location.forced_big_key(): + return True + return False + + def can_traverse(self, door, exception=None): + if door.blocked: + return exception(door) if exception else False + if door.crystal not in [CrystalBarrier.Null, CrystalBarrier.Either]: + return self.crystal == CrystalBarrier.Either or door.crystal == self.crystal + return True + + def count_locations_exclude_specials(self, world, player): + return count_locations_exclude_big_chest(self.found_locations, world, player) + + def validate(self, door, region, world, player): + return self.can_traverse(door) and not self.visited(region) and valid_region_to_explore(region, self.dungeon, + world, player) + + def in_door_list(self, door, door_list): + for d in door_list: + if d.door == door and d.crystal == self.crystal: + return True + return False + + @staticmethod + def in_door_list_ic(door, door_list): + for d in door_list: + if d.door == door: + return True + return False + + @staticmethod + def find_door_in_list(door, door_list): + for d in door_list: + if d.door == door: + return d + return None + + def append_door_to_list(self, door, door_list, flag=False): + if door.crystal == CrystalBarrier.Null: + door_list.append(ExplorableDoor(door, self.crystal, flag)) + else: + door_list.append(ExplorableDoor(door, door.crystal, flag)) + + def key_door_sort(self, d): + if d.door.smallKey: + if d.door in self.opened_doors: + return 1 + else: + return 0 + return 2 + + +def count_locations_exclude_big_chest(locations, world, player): + cnt = 0 + for loc in locations: + if ('- Big Chest' not in loc.name and not loc.forced_item and not reserved_location(loc, world, player) + and not prize_or_event(loc) and not blind_boss_unavail(loc, locations, world, player)): + cnt += 1 + return cnt + + +def prize_or_event(loc): + return loc.name in dungeon_events or '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2'] + + +def reserved_location(loc, world, player): + return hasattr(world, 'item_pool_config') and loc.name in world.item_pool_config.reserved_locations[player] + + +def blind_boss_unavail(loc, locations, world, player): + if loc.name == "Thieves' Town - Boss": + return (loc.parent_region.dungeon.boss.name == 'Blind' and + (not any(x for x in locations if x.name == 'Suspicious Maiden') or + (world.get_region('Thieves Attic Window', player).dungeon.name == 'Thieves Town' and + not any(x for x in locations if x.name == 'Attic Cracked Floor')))) + return False + + +class ExplorableDoor(object): + + def __init__(self, door, crystal, flag): + self.door = door + self.crystal = crystal + self.flag = flag + + def __str__(self): + return str(self.__unicode__()) + + def __unicode__(self): + return '%s (%s)' % (self.door.name, self.crystal.name) + + +def extend_reachable_state_improved(search_regions, state, proposed_map, all_regions, valid_doors, bk_flag, world, player, exception): + local_state = state.copy() + for region in search_regions: + local_state.visit_region(region) + local_state.add_all_doors_check_proposed(region, proposed_map, valid_doors, False, world, player, exception) + while len(local_state.avail_doors) > 0: + explorable_door = local_state.next_avail_door() + if explorable_door.door.bigKey: + if bk_flag: + big_not_found = (not special_big_key_found(local_state) if local_state.big_key_special + else local_state.count_locations_exclude_specials(world, player) == 0) + if big_not_found: + continue # we can't open this door + if explorable_door.door in proposed_map: + connect_region = world.get_entrance(proposed_map[explorable_door.door].name, player).parent_region + else: + connect_region = world.get_entrance(explorable_door.door.name, player).connected_region + if connect_region is not None: + if valid_region_to_explore_in_regions(connect_region, all_regions, world, player) and not local_state.visited( + connect_region): + flag = explorable_door.flag or explorable_door.door.bigKey + local_state.visit_region(connect_region, bk_flag=flag) + local_state.add_all_doors_check_proposed(connect_region, proposed_map, valid_doors, flag, world, player, exception) + return local_state + + +def special_big_key_found(state): + for location in state.found_locations: + if location.forced_item and location.forced_item.bigkey: + return True + return False + + +def valid_region_to_explore_in_regions(region, all_regions, world, player): + if region is None: + return False + return ((region.type == RegionType.Dungeon and region in all_regions) + or region.name in world.inaccessible_regions[player] + or (region.name == 'Hyrule Castle Ledge' and world.mode[player] == 'standard')) + + +def valid_region_to_explore(region, name, world, player): + if region is None: + return False + return ((region.type == RegionType.Dungeon and region.dungeon and region.dungeon.name in name) + or region.name in world.inaccessible_regions[player] + or (region.name == 'Hyrule Castle Ledge' and world.mode[player] == 'standard')) + + +def get_doors(world, region, player): + res = [] + for ext in region.exits: + door = world.check_for_door(ext.name, player) + if door is not None: + res.append(door) + return res + + +def get_entrance_doors(world, region, player): + res = [] + for ext in region.entrances: + door = world.check_for_door(ext.name, player) + if door is not None: + res.append(door) + return res + + +class GenerationException(Exception): + pass + + diff --git a/source/tools/MysteryUtils.py b/source/tools/MysteryUtils.py index 415433c0..8325719b 100644 --- a/source/tools/MysteryUtils.py +++ b/source/tools/MysteryUtils.py @@ -77,6 +77,7 @@ def roll_settings(weights): door_shuffle = get_choice('door_shuffle') ret.door_shuffle = door_shuffle if door_shuffle != 'none' else 'vanilla' ret.intensity = get_choice('intensity') + ret.decoupledoors = get_choice('decoupledoors') == 'on' ret.experimental = get_choice('experimental') == 'on' ret.collection_rate = get_choice('collection_rate') == 'on' From 6e9d949439c5df5933d19052764d85cb774365c7 Mon Sep 17 00:00:00 2001 From: Catobat <69204835+Catobat@users.noreply.github.com> Date: Mon, 13 Jun 2022 20:38:24 +0200 Subject: [PATCH 22/63] Shuffle Back of Tavern --- BaseClasses.py | 7 ++- CLI.py | 5 +- ER_hint_reference.txt | 2 + EntranceShuffle.py | 4 +- Main.py | 3 +- README.md | 5 ++ Rom.py | 6 ++- data/base2current.bps | Bin 93021 -> 93118 bytes mystery_testsuite.yml | 3 ++ resources/app/cli/args.json | 4 ++ resources/app/cli/lang/en.json | 3 ++ resources/app/gui/lang/en.json | 1 + .../app/gui/randomize/entrando/widgets.json | 1 + source/classes/CustomSettings.py | 2 + source/classes/constants.py | 1 + source/overworld/EntranceShuffle2.py | 44 ++++++++++-------- source/tools/MysteryUtils.py | 1 + 17 files changed, 64 insertions(+), 28 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 2b56d39f..fd9e0c9c 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -2460,6 +2460,7 @@ class Spoiler(object): 'goal': self.world.goal, 'shuffle': self.world.shuffle, 'shufflelinks': self.world.shufflelinks, + 'shuffletavern': self.world.shuffletavern, 'door_shuffle': self.world.doorShuffle, 'intensity': self.world.intensity, 'item_pool': self.world.difficulty, @@ -2541,7 +2542,8 @@ class Spoiler(object): outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][player]) outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'][player]) outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player]) - outfile.write(f"Link's House Shuffled: {yn(self.metadata['shufflelinks'])}\n") + outfile.write(f"Link's House Shuffled: {yn(self.metadata['shufflelinks'][player])}\n") + outfile.write(f"Back of Tavern Shuffled: {yn(self.metadata['shuffletavern'][player])}\n") outfile.write('Door Shuffle: %s\n' % self.metadata['door_shuffle'][player]) outfile.write('Intensity: %s\n' % self.metadata['intensity'][player]) outfile.write(f"Drop Shuffle: {yn(self.metadata['dropshuffle'][player])}\n") @@ -2827,7 +2829,7 @@ class Settings(object): (0x80 if w.bigkeyshuffle[p] else 0) | (0x40 if w.keyshuffle[p] else 0) | (0x20 if w.mapshuffle[p] else 0) | (0x10 if w.compassshuffle[p] else 0) - | (enemy_mode[w.enemy_shuffle[p]]), + | (0x8 if w.shuffletavern[p] else 0) | (enemy_mode[w.enemy_shuffle[p]]), (e_health[w.enemy_health[p]] << 5) | (e_dmg[w.enemy_damage[p]] << 3) | (0x4 if w.potshuffle[p] else 0) | (0x2 if w.bombbag[p] else 0) | (1 if w.shufflelinks[p] else 0), @@ -2885,6 +2887,7 @@ class Settings(object): args.mapshuffle[p] = True if settings[7] & 0x20 else False args.compassshuffle[p] = True if settings[7] & 0x10 else False # args.shufflebosses[p] = r(boss_mode)[(settings[7] & 0xc) >> 2] + args.shuffletavern[p] = True if settings[7] & 0x8 else False args.shuffleenemies[p] = r(enemy_mode)[settings[7] & 0x3] args.enemy_health[p] = r(e_health)[(settings[8] & 0xE0) >> 5] diff --git a/CLI.py b/CLI.py index c659d3b5..62fee1b3 100644 --- a/CLI.py +++ b/CLI.py @@ -119,8 +119,8 @@ def parse_cli(argv, no_defaults=False): 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory', 'usestartinventory', 'bombbag', 'overworld_map', 'restrict_boss_items', 'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max', - 'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'pseudoboots', - 'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters', + 'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'shuffletavern', + 'pseudoboots', 'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters', 'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots', 'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep', 'remote_items', 'shopsanity', 'dropshuffle', 'pottery', 'keydropshuffle', @@ -170,6 +170,7 @@ def parse_settings(): "shuffleganon": True, "shuffle": "vanilla", "shufflelinks": False, + "shuffletavern": False, "overworld_map": "default", "pseudoboots": False, diff --git a/ER_hint_reference.txt b/ER_hint_reference.txt index 999fb436..018dfb3a 100644 --- a/ER_hint_reference.txt +++ b/ER_hint_reference.txt @@ -91,6 +91,7 @@ While the exact verbage of location names and item names can be found in the sou Overworld Entrance naming: +Links House: The hero's old residence Turtle Rock: Turtle Rock Main Misery Mire: Misery Mire Ice Palace: Ice Palace @@ -141,6 +142,7 @@ 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 +Tavern North: A backdoor Aginahs Cave: The open desert cave Sahasrahlas Hut: The house near armos Cave Shop (Lake Hylia): The cave NW Lake Hylia diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 0d3f502b..2899b28b 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -3364,7 +3364,7 @@ door_addresses = {'Links House': (0x00, (0x0104, 0x2c, 0x0506, 0x0a9a, 0x0832, 0 '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 + 'Tavern North': (0x42, (0x0103, 0x18, 0x1440, 0x08a7, 0x0206, 0x091b, 0x0288, 0x0914, 0x0293, 0xf7, 0x09, 0xFFFF, 0x0000)), '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)), @@ -3653,7 +3653,7 @@ ow_prize_table = {'Links House': (0x8b1, 0xb2d), 'Inverted Big Bomb Shop': (0x8b 'Light Hype Fairy': (0x940, 0xc80), 'Desert Fairy': (0x420, 0xe00), 'Kings Grave': (0x920, 0x520), - 'Tavern North': None, # can't mark this one technically + 'Tavern North': (0x270, 0x900), 'Chicken House': (0x120, 0x880), 'Aginahs Cave': (0x2e0, 0xd00), 'Sahasrahlas Hut': (0xcf0, 0x6c0), diff --git a/Main.py b/Main.py index 1c2357bb..19515022 100644 --- a/Main.py +++ b/Main.py @@ -118,6 +118,7 @@ def main(args, seed=None, fish=None): world.treasure_hunt_count = {k: int(v) for k, v in args.triforce_goal.items()} world.treasure_hunt_total = {k: int(v) for k, v in args.triforce_pool.items()} world.shufflelinks = args.shufflelinks.copy() + world.shuffletavern = args.shuffletavern.copy() world.pseudoboots = args.pseudoboots.copy() world.overworld_map = args.overworld_map.copy() world.restrict_boss_items = args.restrict_boss_items.copy() @@ -192,7 +193,7 @@ 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.experimental[player] or (world.customizer and world.customizer.get_entrances()): + if world.experimental[player] or world.shuffle[player] in ['lite', 'lean'] or world.shuffletavern[player] or (world.customizer and world.customizer.get_entrances()): link_entrances_new(world, player) else: if world.mode[player] != 'inverted': diff --git a/README.md b/README.md index f419292f..f2980840 100644 --- a/README.md +++ b/README.md @@ -333,6 +333,11 @@ Start with dash ability, but no way to use boots to accomplish checks Whether to shuffle links house in most ER modes. +``` +--shuffletavern +``` + +Whether to shuffle the back of the tavern in most ER modes. ``` --bombbag diff --git a/Rom.py b/Rom.py index 616b7978..6289874a 100644 --- a/Rom.py +++ b/Rom.py @@ -37,7 +37,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '01166fb16b38b49ef79acc9993dc4f02' +RANDOMIZERBASEHASH = 'e5556450b16f74709c666e419f2a42ad' class JsonRom(object): @@ -730,6 +730,8 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): else: # patch door table rom.write_byte(0xDBB73 + exit.addresses, exit.target) + if exit.name == 'Tavern North': + rom.write_byte(0x157D0, exit.target) if world.mode[player] == 'inverted': patch_shuffled_dark_sanc(world, rom, player) @@ -2048,6 +2050,8 @@ def write_strings(rom, world, player, team): 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.shuffletavern[player] and world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull']: + entrances_to_hint.update({'Tavern North': 'A backdoor'}) 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'}) diff --git a/data/base2current.bps b/data/base2current.bps index bd6417a47ef665eabc70d7f6055e50cdcb4562a4..d44d2e2d89935611c1bbf1e5eee7fb9a50bb251f 100644 GIT binary patch delta 7654 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 K{mEBG82=AhtZ_&H delta 7235 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 Date: Mon, 13 Jun 2022 20:51:11 +0200 Subject: [PATCH 23/63] Fix Insanity --- source/overworld/EntranceShuffle2.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/source/overworld/EntranceShuffle2.py b/source/overworld/EntranceShuffle2.py index 79687b38..66c5c131 100644 --- a/source/overworld/EntranceShuffle2.py +++ b/source/overworld/EntranceShuffle2.py @@ -542,8 +542,8 @@ def figure_out_must_exits_same_world(entrances, exits, avail): 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 + must_exit_lw = (Inverted_LW_Must_Exit if avail.inverted else LW_Must_Exit).copy() + must_exit_dw = (Inverted_DW_Must_Exit if avail.inverted else DW_Must_Exit).copy() 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) @@ -554,8 +554,8 @@ def must_exits_helper(avail, lw_entrances, dw_entrances): 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 + must_exit_lw = (Inverted_LW_Must_Exit if avail.inverted else LW_Must_Exit).copy() + must_exit_dw = (Inverted_DW_Must_Exit if avail.inverted else DW_Must_Exit).copy() 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) From 5dce2daa0b02debf83cb7abf0c20157fbece7c4c Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 14 Jun 2022 12:29:57 -0600 Subject: [PATCH 24/63] Standard throne room changes --- DoorShuffle.py | 32 +++++++- DungeonGenerator.py | 167 +++++++++++++++++++++++++++++++----------- Main.py | 2 +- RELEASENOTES.md | 8 ++ Rom.py | 2 +- data/base2current.bps | Bin 93021 -> 93062 bytes 6 files changed, 164 insertions(+), 47 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index 0d36a2b0..23df9b58 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -14,7 +14,7 @@ from Items import ItemFactory from RoomData import DoorKind, PairedDoor, reset_rooms from DungeonGenerator import ExplorationState, convert_regions, generate_dungeon, pre_validate, determine_required_paths, drop_entrances from DungeonGenerator import create_dungeon_builders, split_dungeon_builder, simple_dungeon_builder, default_dungeon_entrances -from DungeonGenerator import dungeon_portals, dungeon_drops, GenerationException +from DungeonGenerator import dungeon_portals, dungeon_drops, GenerationException, connect_doors from DungeonGenerator import valid_region_to_explore as valid_region_to_explore_lim from KeyDoorShuffle import analyze_dungeon, build_key_layout, validate_key_layout, determine_prize_lock from Utils import ncr, kth_combination @@ -685,6 +685,13 @@ def create_dungeon_entrances(world, player): choice = random.choice(filtered_choices) r_name = portal.door.entrance.parent_region.name split_map[key][choice].append(r_name) + elif key == 'Hyrule Castle' and world.mode[player] == 'standard': + for portal_name in portal_list: + portal = world.get_portal(portal_name, player) + choice = 'Sewers' if portal_name == 'Sanctuary' else 'Dungeon' + r_name = portal.door.entrance.parent_region.name + split_map[key][choice].append(r_name) + entrance_map[key].append(r_name) else: for portal_name in portal_list: portal = world.get_portal(portal_name, player) @@ -774,7 +781,8 @@ def main_dungeon_generation(dungeon_builders, recombinant_builders, connections_ logging.getLogger('').info(world.fish.translate("cli", "cli", "generating.dungeon")) while len(sector_queue) > 0: builder = sector_queue.popleft() - split_dungeon = builder.name.startswith('Desert Palace') or builder.name.startswith('Skull Woods') + split_dungeon = (builder.name.startswith('Desert Palace') or builder.name.startswith('Skull Woods') + or (builder.name.startswith('Hyrule Castle') and world.mode[player] == 'standard')) name = builder.name if split_dungeon: name = ' '.join(builder.name.split(' ')[:-1]) @@ -782,6 +790,7 @@ def main_dungeon_generation(dungeon_builders, recombinant_builders, connections_ del dungeon_builders[builder.name] continue origin_list = list(builder.entrance_list) + find_standard_origins(builder, recombinant_builders, origin_list) find_enabled_origins(builder.sectors, enabled_entrances, origin_list, entrances_map, name) split_dungeon = treat_split_as_whole_dungeon(split_dungeon, name, origin_list, world, player) if len(origin_list) <= 0 or not pre_validate(builder, origin_list, split_dungeon, world, player): @@ -798,7 +807,7 @@ def main_dungeon_generation(dungeon_builders, recombinant_builders, connections_ builder.master_sector = ds builder.layout_starts = origin_list if len(builder.entrance_list) <= 0 else builder.entrance_list last_key = None - combine_layouts(recombinant_builders, dungeon_builders, entrances_map) + combine_layouts(recombinant_builders, dungeon_builders, entrances_map, world, player) world.dungeon_layouts[player] = {} for builder in dungeon_builders.values(): builder.entrance_list = builder.layout_starts = builder.path_entrances = find_accessible_entrances(world, player, builder) @@ -872,6 +881,14 @@ def add_shuffled_entrances(sectors, region_list, entrance_list): entrance_list.append(region.name) +def find_standard_origins(builder, recomb_builders, origin_list): + if builder.name == 'Hyrule Castle Sewers': + throne_door = recomb_builders['Hyrule Castle'].throne_door + sewer_entrance = throne_door.entrance.parent_region.name + if sewer_entrance not in origin_list: + origin_list.append(sewer_entrance) + + def find_enabled_origins(sectors, enabled, entrance_list, entrance_map, key): for sector in sectors: for region in sector.regions: @@ -1378,7 +1395,7 @@ def merge_sectors(all_sectors, world, player): # those with split region starts like Desert/Skull combine for key layouts -def combine_layouts(recombinant_builders, dungeon_builders, entrances_map): +def combine_layouts(recombinant_builders, dungeon_builders, entrances_map, world, player): for recombine in recombinant_builders.values(): queue = deque(dungeon_builders.values()) while len(queue) > 0: @@ -1390,6 +1407,10 @@ def combine_layouts(recombinant_builders, dungeon_builders, entrances_map): recombine.master_sector.name = recombine.name else: recombine.master_sector.regions.extend(builder.master_sector.regions) + if recombine.name == 'Hyrule Castle': + recombine.master_sector.regions.extend(recombine.throne_sector.regions) + throne_n = world.get_door('Hyrule Castle Throne Room N', player) + connect_doors(throne_n, recombine.throne_door) recombine.layout_starts = list(entrances_map[recombine.name]) dungeon_builders[recombine.name] = recombine @@ -1487,11 +1508,14 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True proposal = kth_combination(sample_list[itr], builder.candidates, builder.key_doors_num) # eliminate start region if portal marked as destination + std_flag = world.mode[player] == 'standard' and builder.name == 'Hyrule Castle' excluded = {} for region in start_regions: portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region == region), None) if portal and portal.destination: excluded[region] = None + if std_flag and (not portal or portal.find_portal_entrance().parent_region.name != 'Hyrule Castle Courtyard'): + excluded[region] = None start_regions = [x for x in start_regions if x not in excluded.keys()] key_layout = build_key_layout(builder, start_regions, proposal, world, player) diff --git a/DungeonGenerator.py b/DungeonGenerator.py index f0038f45..68b82732 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -43,7 +43,10 @@ def pre_validate(builder, entrance_region_names, split_dungeon, world, player): for sector in builder.sectors: for door in sector.outstanding_doors: doors_to_connect[door.name] = door - all_regions.update(sector.regions) + if world.mode[player] == 'standard' and builder.name == 'Hyrule Castle Dungeon': + all_regions.update([x for x in sector.regions if x.name != 'Hyrule Castle Behind Tapestry']) + else: + all_regions.update(sector.regions) bk_special |= check_for_special(sector.regions) bk_needed = False for sector in builder.sectors: @@ -91,18 +94,27 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon p_region = portal.door.entrance.connected_region access_region = next(x.parent_region for x in p_region.entrances if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]) + if ((access_region.name in world.inaccessible_regions[player] and + region.name not in world.enabled_entrances[player]) + or (world.mode[player] == 'standard' and access_region.name != 'Hyrule Castle Courtyard' + and 'Hyrule Castle' in builder.name)): + excluded[region] = None + else: # for non-portals, holes and sewers in std + access_region = next((x.parent_region for x in region.entrances + if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld] + or x.parent_region.name == 'Sewer Drop'), None) + if access_region is None: + if builder.sewers_access is None: + excluded[region] = None + else: + if access_region.name == 'Sewer Drop': + if world.mode[player] == 'standard' and (builder.sewers_access is None + or builder.sewers_access.entrance.parent_region != region): + excluded[region] = None + access_region = next(x.parent_region for x in access_region.entrances) if (access_region.name in world.inaccessible_regions[player] and region.name not in world.enabled_entrances[player]): excluded[region] = None - elif len(region.entrances) == 1: # for holes - access_region = next(x.parent_region for x in region.entrances - if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld] - or x.parent_region.name == 'Sewer Drop') - if access_region.name == 'Sewer Drop': - access_region = next(x.parent_region for x in access_region.entrances) - if (access_region.name in world.inaccessible_regions[player] and - region.name not in world.enabled_entrances[player]): - excluded[region] = None entrance_regions = [x for x in entrance_regions if x not in excluded.keys()] doors_to_connect = {} all_regions = set() @@ -143,7 +155,11 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon dungeon, hangers, hooks = gen_dungeon_info(name, builder.sectors, entrance_regions, all_regions, proposed_map, doors_to_connect, bk_needed, bk_special, world, player) dungeon_cache[depth] = dungeon, hangers, hooks - valid = check_valid(name, dungeon, hangers, hooks, proposed_map, doors_to_connect, all_regions, + if len(proposed_map) != len(doors_to_connect) and builder.name == 'Hyrule Castle Dungeon': + check_regions = all_regions.difference({world.get_region('Hyrule Castle Behind Tapestry', player)}) + else: + check_regions = all_regions + valid = check_valid(name, dungeon, hangers, hooks, proposed_map, doors_to_connect, check_regions, bk_needed, bk_special, paths, entrance_regions, world, player) else: dungeon, hangers, hooks = dungeon_cache[depth] @@ -565,9 +581,15 @@ def determine_paths_for_dungeon(world, player, all_regions, name): non_hole_portals.append(portal.door.entrance.parent_region.name) if portal.destination: paths.append(portal.door.entrance.parent_region.name) - if world.mode[player] == 'standard' and name == 'Hyrule Castle': - paths.append('Hyrule Dungeon Cellblock') - paths.append(('Hyrule Dungeon Cellblock', 'Sanctuary')) + if world.mode[player] == 'standard': + if name == 'Hyrule Castle': + paths.append('Hyrule Dungeon Cellblock') + paths.append(('Hyrule Dungeon Cellblock', 'Sanctuary')) + if name == 'Hyrule Castle Sewers': + paths.append('Sanctuary') + if name == 'Hyrule Castle Dungeon': + paths.append('Hyrule Dungeon Cellblock') + paths.append(('Hyrule Dungeon Cellblock', 'Hyrule Castle Throne Room')) if world.doorShuffle[player] in ['basic'] and name == 'Thieves Town': paths.append('Thieves Attic Window') elif 'Thieves Attic Window' in all_r_names: @@ -1206,6 +1228,11 @@ class DungeonBuilder(object): self.split_dungeon_map = None self.exception_list = [] + self.throne_door = None + self.throne_sector = None + self.chosen_lobby = None + self.sewers_access = None + def polarity_complement(self): pol = Polarity() for sector in self.sectors: @@ -1258,7 +1285,7 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, for r_name in dungeon_boss_sectors[key]: assign_sector(find_sector(r_name, candidate_sectors), current_dungeon, candidate_sectors, global_pole) if key == 'Hyrule Castle' and world.mode[player] == 'standard': - for r_name in ['Hyrule Dungeon Cellblock', 'Sanctuary']: # need to deliver zelda + for r_name in ['Hyrule Dungeon Cellblock', 'Sanctuary', 'Hyrule Castle Throne Room']: # need to deliver zelda assign_sector(find_sector(r_name, candidate_sectors), current_dungeon, candidate_sectors, global_pole) if key == 'Thieves Town' and world.get_dungeon("Thieves Town", player).boss.enemizer_name == 'Blind': @@ -2077,16 +2104,18 @@ def assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, builde while len(problem_builders) > 0: for name, builder in problem_builders.items(): candidates = find_branching_candidates(builder, neutral_choices, builder_info) - valid, choice = False, None + valid, choice, package = False, None, None while not valid: if len(candidates) <= 0: raise GenerationException('Cross Dungeon Builder: Complex branch problems: %s' % name) - choice = random.choice(candidates) - candidates.remove(choice) + choice, package = random.choice(candidates) + candidates.remove((choice, package)) valid = global_pole.is_valid_choice(dungeon_map, builder, choice) and valid_polarized_assignment(builder, choice) neutral_choices.remove(choice) for sector in choice: assign_sector(sector, builder, polarized_sectors, global_pole) + if package: + builder.throne_door, builder.throne_sector, builder.chosen_lobby = package builder.unfulfilled.clear() problem_builders = identify_branching_issues(problem_builders, builder_info) @@ -2107,16 +2136,21 @@ def assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, builde chosen_sectors = defaultdict(list) for i, choice in enumerate(choices): chosen_sectors[choice].extend(neutral_choices[i]) - all_valid = True + all_valid, package_map = True, {} for name, sector_list in chosen_sectors.items(): - if not valid_assignment(dungeon_map[name], sector_list, builder_info): + flag, package = valid_assignment(dungeon_map[name], sector_list, builder_info) + if not flag: all_valid = False break + if package: + package_map[dungeon_map[name]] = package if all_valid: for i, choice in enumerate(choices): builder = dungeon_map[choice] for sector in neutral_choices[i]: assign_sector(sector, builder, polarized_sectors, global_pole) + if builder in package_map: + builder.throne_door, builder.throne_sector, builder.chosen_lobby = package_map[builder] tries += 1 @@ -2629,9 +2663,9 @@ def weed_candidates(builder, candidates, best_charge): def find_branching_candidates(builder, neutral_choices, builder_info): candidates = [] for choice in neutral_choices: - resolved, problem_list = check_for_valid_layout(builder, choice, builder_info) + resolved, problem_list, package = check_for_valid_layout(builder, choice, builder_info) if resolved: - candidates.append(choice) + candidates.append((choice, package)) return candidates @@ -2786,13 +2820,13 @@ def categorize_groupings(sectors): def valid_assignment(builder, sector_list, builder_info): if not valid_entrance(builder, sector_list, builder_info): - return False + return False, None if not valid_c_switch(builder, sector_list): - return False + return False, None if not valid_polarized_assignment(builder, sector_list): - return False - resolved, problems = check_for_valid_layout(builder, sector_list, builder_info) - return resolved + return False, None + resolved, problems, package = check_for_valid_layout(builder, sector_list, builder_info) + return resolved, package def valid_entrance(builder, sector_list, builder_info): @@ -2898,31 +2932,56 @@ def assign_the_rest(dungeon_map, neutral_sectors, global_pole, builder_info): chosen_sectors = defaultdict(list) for i, choice in enumerate(choices): chosen_sectors[choice].append(neutral_sector_list[i]) - all_valid = True + all_valid, package_map = True, {} for name, sector_list in chosen_sectors.items(): - if not valid_assignment(dungeon_map[name], sector_list, builder_info): + flag, package = valid_assignment(dungeon_map[name], sector_list, builder_info) + if not flag: all_valid = False break + if package: + package_map[dungeon_map[name]] = package if all_valid: for name, sector_list in chosen_sectors.items(): builder = dungeon_map[name] for sector in sector_list: assign_sector(sector, builder, neutral_sectors, global_pole) + if builder in package_map: + builder.throne_door, builder.throne_sector, builder.chosen_lobby = package_map[builder] tries += 1 def split_dungeon_builder(builder, split_list, builder_info): + ents, splits, c_tuple, world, player = builder_info if builder.split_dungeon_map and len(builder.exception_list) == 0: for name, proposal in builder.valid_proposal.items(): builder.split_dungeon_map[name].valid_proposal = proposal + if builder.name == 'Hyrule Castle': + builder.chosen_lobby.outstanding_doors.remove(builder.throne_door) + builder.throne_sector.outstanding_doors.remove(world.get_door('Hyrule Castle Throne Room N', player)) return builder.split_dungeon_map # we made this earlier in gen, just use it attempts, comb_w_replace, merge_attempt, merge_limit = 0, None, 0, len(split_list) - 1 while attempts < 5: # does not solve coin flips 3% of the time try: candidate_sectors = dict.fromkeys(builder.sectors) - global_pole = GlobalPolarity(candidate_sectors) + if builder.name == 'Hyrule Castle': + throne_sector = find_sector('Hyrule Castle Throne Room', candidate_sectors) + chosen_lobbies = {r_name for x in split_list.values() for r_name in x} + choices = {} + for sector in candidate_sectors: + if sector.adj_outflow() > 1 and sector != throne_sector: + for door in sector.outstanding_doors: + if door.direction == Direction.South and door.entrance.parent_region not in chosen_lobbies: + choices[door] = sector + chosen_door = random.choice(list(choices.keys())) + split_list['Sewers'].append(chosen_door.entrance.parent_region.name) + choices[chosen_door].outstanding_doors.remove(chosen_door) + builder.throne_door = chosen_door + builder.throne_sector = throne_sector + builder.chosen_lobby = choices[chosen_door] + throne_sector.outstanding_doors.remove(world.get_door('Hyrule Castle Throne Room N', player)) + global_pole = GlobalPolarity(candidate_sectors) dungeon_map, sub_builder, merge_keys = {}, None, [] if merge_attempt > 0: candidates = [] @@ -2932,7 +2991,6 @@ def split_dungeon_builder(builder, split_list, builder_info): continue elif len(split_entrances) <= 0: continue - ents, splits, c_tuple, world, player = builder_info r_name = split_entrances[0] p = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region.name == r_name), None) if p and not p.deadEnd: @@ -2953,6 +3011,13 @@ 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 builder.name == 'Hyrule Castle': + assign_sector(find_sector('Hyrule Castle Throne Room', candidate_sectors), + dungeon_map['Hyrule Castle Dungeon'], candidate_sectors, global_pole) + assign_sector(find_sector('Hyrule Dungeon Cellblock', candidate_sectors), + dungeon_map['Hyrule Castle Dungeon'], candidate_sectors, global_pole) + dungeon_map['Hyrule Castle Dungeon'].throne_door = world.get_door('Hyrule Castle Throne Room N', player) + dungeon_map['Hyrule Castle Sewers'].sewers_access = builder.throne_door comb_w_replace = len(dungeon_map) ** len(candidate_sectors) return balance_split(candidate_sectors, dungeon_map, global_pole, builder_info) except (GenerationException, NeutralizingException): @@ -2960,7 +3025,13 @@ def split_dungeon_builder(builder, split_list, builder_info): attempts += 5 # all the combinations were tried already, no use repeating else: attempts += 1 - if attempts >= 5 and merge_attempt < merge_limit: + if builder.throne_door: + previous = find_sector(builder.throne_door.entrance.parent_region.name, builder.sectors) + previous.outstanding_doors.append(builder.throne_door) + builder.throne_sector.outstanding_doors.append(world.get_door('Hyrule Castle Throne Room N', player)) + split_list['Sewers'].remove(builder.throne_door.entrance.parent_region.name) + builder.throne_door = None + if attempts >= 5 and merge_attempt < merge_limit and builder.name != 'Hyrule Castle': merge_attempt, attempts = merge_attempt + 1, 0 raise GenerationException('Unable to resolve in 5 attempts') @@ -2981,16 +3052,21 @@ def balance_split(candidate_sectors, dungeon_map, global_pole, builder_info): chosen_sectors = defaultdict(list) for i, choice in enumerate(choices): chosen_sectors[choice].append(main_sector_list[i]) - all_valid = True + all_valid, package_map = True, {} for name, builder in dungeon_map.items(): - if not valid_assignment(builder, chosen_sectors[name], builder_info): + flag, package = valid_assignment(builder, chosen_sectors[name], builder_info) + if not flag: all_valid = False break + if package: + package_map[builder] = package if all_valid: for name, sector_list in chosen_sectors.items(): builder = dungeon_map[name] for sector in sector_list: assign_sector(sector, builder, candidate_sectors, global_pole) + if builder in package_map: + builder.throne_door, builder.throne_sector, builder.chosen_lobby = package_map[builder] return dungeon_map tries += 1 raise GenerationException('Split Dungeon Builder: Impossible dungeon. Ref %s' % next(iter(dungeon_map.keys()))) @@ -3380,7 +3456,7 @@ class DungeonAccess: def identify_branching_issues(dungeon_map, builder_info): unconnected_builders = {} for name, builder in dungeon_map.items(): - resolved, unreached_doors = check_for_valid_layout(builder, [], builder_info) + resolved, unreached_doors, package = check_for_valid_layout(builder, [], builder_info) if not resolved: unconnected_builders[name] = builder for hook, door_list in unreached_doors.items(): @@ -3421,16 +3497,27 @@ def check_for_valid_layout(builder, sector_list, builder_info): proposal = generate_dungeon_find_proposal(split_build, entrance_regions, split, world, player) # record split proposals builder.valid_proposal[name] = proposal + package = None + if temp_builder.name == 'Hyrule Castle': + temp_builder.chosen_lobby.outstanding_doors.append(temp_builder.throne_door) + temp_builder.throne_sector.outstanding_doors.append(world.get_door('Hyrule Castle Throne Room N', player)) + package = temp_builder.throne_door, temp_builder.throne_sector, temp_builder.chosen_lobby + split_list['Sewers'].remove(temp_builder.throne_door.entrance.parent_region.name) builder.exception_list = list(sector_list) - return True, {} + return True, {}, package except (GenerationException, NeutralizingException): builder.split_dungeon_map = None builder.valid_proposal = None + if temp_builder.name == 'Hyrule Castle' and temp_builder.throne_door: + temp_builder.chosen_lobby.outstanding_doors.append(temp_builder.throne_door) + temp_builder.throne_sector.outstanding_doors.append(world.get_door('Hyrule Castle Throne Room N', player)) + old_entrance = temp_builder.throne_door.entrance.parent_region.name + split_dungeon_entrances[builder.name]['Sewers'].remove(old_entrance) unreached_doors = resolve_equations(builder, sector_list) - return False, unreached_doors + return False, unreached_doors, None else: unreached_doors = resolve_equations(builder, sector_list) - return len(unreached_doors) == 0, unreached_doors + return len(unreached_doors) == 0, unreached_doors, None def find_independent_entrances(entrance_regions, world, player): @@ -3831,9 +3918,7 @@ def find_free_equation(equations): def copy_door_equations(builder, sector_list): equations = {} for sector in builder.sectors + sector_list: - if sector.equations is None: - # todo: sort equations? - sector.equations = calc_sector_equations(sector) + sector.equations = calc_sector_equations(sector) curr_list = equations[sector] = [] for equation in sector.equations: curr_list.append(equation.copy()) diff --git a/Main.py b/Main.py index f3dcb0ee..32b7dd7f 100644 --- a/Main.py +++ b/Main.py @@ -31,7 +31,7 @@ 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.tools.BPS import create_bps_from_data -__version__ = '1.0.2.4-v' +__version__ = '1.0.2.4-x' from source.classes.BabelFish import BabelFish diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 31d1e10e..286704e6 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -155,8 +155,16 @@ Same as above but both small keys and bigs keys of the dungeon are not allowed o ## Notes and Bug Fixes +#### StandardThrone + + * Changed standard dungeon generation to always have Throne Room in hyrule castle and always have sanctuary behind it + * S&Q/death in standard after moving the tapestry but before delivering Zelda will result in spawning at the tapestry + * Mirror scroll will return you to Zelda's cell instead of last entrance. This reverts to normal behavior once the tapestry open trigger is reach + #### Volatile +* 1.0.2.5 + * Some textual changes for hints * 1.0.2.4 * Updated tourney winners (included Doors Async League winners) * Fixed a couple issues with dungeon counters and the DungeonCompletion field for autotracking diff --git a/Rom.py b/Rom.py index 1c9d82a7..f062e4d3 100644 --- a/Rom.py +++ b/Rom.py @@ -37,7 +37,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '01166fb16b38b49ef79acc9993dc4f02' +RANDOMIZERBASEHASH = '4941faf4496875a373a51119f2bedf4c' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index bd6417a47ef665eabc70d7f6055e50cdcb4562a4..4005551c18297514faad1e8e8ed9872d957d261b 100644 GIT binary patch delta 1773 zcmW;MeNaZ(aH``v8a5+tx5*aL2>CW3#?m~Zs55cWLQ8T z;k`)2Ktha%BtoEhBcvLThUu%TZDpd-7PGURjjeWDSpJY56et$(gLXUB-Dm$fzqucC z=bSV5-s!JHPW>xntP}|+OOxDO1=@}faDcBts0rMH3F5kq?3!d37WOhm@|}~8$PW`c z&18agsMf#;ubIiup|$-7nf&(ycFtY| zVrhnC4}V?}Z6zCFzcfJ{y3B54Y~%qrE=`Hx&n9h)?3v!u6_!j|f>CJ_8iuc=yJfG= z@)cilTFm4zh>^WOt(-C}U4WXof`~5#(XJI|c2nw14kl%i_|b0c|HEl94mAgNyS6Q`nXfZ1-^K_wJdI)T}Wo` zmtBIcsLiN?Qh;68VXTCbY>%AJS9QVoQGmrvd>^^+oDB#S!)v*@ zXd0;8AS#6{RX#eyb*O66Q6BfW?g@URxN6QL_TGM?3kZV|9;v;pyuCLy+;e6iD7TYK zkEj{qmqk1n;VtGjpc|lLCYtao?qY6S?X6)_gMvcEEYvoUF>!XA z*Iv}NYnR1r^XLtjN57l?(O8Jh!bnpVuDlQPO^0!QZ}y2TI6tX5!Q#;?AZ=F5`l?kQ z80#Le&XN_VXeOgx_mB4=I^0NO%OM^>w7~Q3$PPc)o8vcWk1*rQ!T>%d(RjjGR2DN6 zqIrYHv76k@8?BJlT8Zj`ZH>>nY$?2*M84)5Lj`zrg>BTz20Go` zhf$t7Lmzb>^R_Xmej+Hkj1eP?lp7=F9#KBkO&vM%0(Z3~K7-nfRp*I}RcPWxJ5~YP zvFeoYih0-YDtKdfmAspH3EnNd$-HU2sk}RRGk2^ylB&Wd2NcVK!jVPqvLn3`n6_Rt z2eEXDygFmme2%VhpT%gGYA2M?rPA0#Gv=~OKZQCQ;1fCvPs@cL>G*wHSBBs7_Jg`$ z_56@GqhQsrb*1k;!wlgpRIeIdxqSBI$Zx6`+fg@m0rPQT)r`TP>fC)`)zG@)eE(&* z$h10|;rGl&T=EoJ8B*E&jA?Y~nMRA=_0}^>+_R-Wl)d=#_lL{jZ%lvGW-0ygUjF{7 zX9&u@){Jc`BMKM_H2Uq5;-&nx|9SI^g!ehOUKJrvdBD=^{DilaleZglxF^=zTO!IU zy9SgV9JA<(+5p{!zkk{zu9BLVc9%v9?_7%)x5!M)X_rpMnXWwwiARy9K9N#Cuq+x+ zl?QO@LS9yfO-Wfkd!lnDW`V=c;D-je^*8fSVtA|xM6o7D4WHVC8V{m0j*Ycl?m$r+ z|2oiljB*^%a)w(KNYq+L57vsf`Oh+uj5=ttDhzRydg(9Rwux&KBcD$X;)vO$j rL&qHB;V1_W)n0iXj$R2B{i>RA>{)XqG7_mu#5u9ozy0}P|Jwfp4>1UU delta 1774 zcmW;Ndr(tX9tZH8n-G!!fkhtS6~h%#T-gDEE%HnOONywGQMaYRMMXhVcc@w(0?ECB zh{41VeufC)aYv09(GA6IQV}{x1NIAqwt|g4C6Z`#_KF(o30i?pBVEMVcjg=F+UxLkyW;j!;&UbBmOpETeW zXy8Q&Y!4{6wAF;)cLBxI`}TP#f`cbP3cnOZz-fLK_QC~U@Fj7*d9#$L{M6>bDS?jD zY{H)nU2=~Y@Yh3^s=E#NN3+yi=Q;=!r18U<1?7YZ-vV0%t9TFRsr958Uj=o7#6V_o zT=|r0?@C&r@c(`X*95B(0WLwN-%pPyxBH6KXu$J8;`bVr&@25nu#ucD3B1D&|8T*Y zUDtJ9ysmXbi<_q+Ii1G{EnZG%uHa+g!iUsyJp$JScLB32rYk2H;JwgykQlCoej*;2 zqd5IKe0J6n8GSZh*9AD_g#!6-+g_k7O$W zYha{iBZ`E@nq64Rw@_3YgM?63n~m6Tu{PbW>~k{Cm_^twkNLrqT0H#7BpK4ZE9eC2 z4;_#&Wg6lge>n*20Ixe3nrx0GBoje@W!W&9;}DrN%`TpO1XLX!Av2pDopNft!CA%2zJpF+!1s@@>hHc&lLpx(M|R?+PmaqjX}d z={F^n@b`vntgaBE8uNrHsXv%xm&c?UYs62wN>DcLLlXF^Q5+UQ8jHtOM(Mb!=Pb9U zi#sE6++I?3G>KMD^OjqR?iOr}Y{3MV$L`$6wA9#LCI{Y)dx>xOJq zuJ~w~n`B$oya}YtdO-ZMse&!qkI`X7FIA4ebVIi)Iyy1r{^VKF;jp|b;d$-o_O}xf zrOZ`&HzVl2HWluuicvVMR7a}Gr%D@3a_gCLO!}z`AAF7VtK*(Sbeh+?I?W4ljERiv z8Pgb38M7I)7&kE%Fm7QiX57iRgRzWJ!MLBXBF$^rUg7Eu^014^rw)&0_gQm?z6cCBR&@`{%EOE%z!6N$KPX{$oDkzlC7!+4t`dV@LhRDLoav+uz zt62+|`y4%RtjK%rf@4pSSIb)H?$gdltaXK6ZSeJu=0UWdG?&|01FSJRy^7RMBv~tq zz1orm>&4}`M3z|MH+%c4wO8lnP6jQNJ5q=w55KSEh`mc`|Jm>-j&dMv>En=$ukx2v z?EwxZo%@D5_Yr?kIzCuZ!7Y+6ihoYl_G`#mqo$wz+$j5g-cRv5(4$hoKgpi31_AML z0doyxGZLk3g@F~TkZdNy!K6v$H+X7-zlr){n~nanMj9w&&r(Flgv8NE3tMy{bacn< zguvvAQ@tWP#xknpYDl~bOE=E&Hu~wwmi{b1Fy0jNw)yMHll>+B^!1z1e8f9L_1#>N zrq{zAO%ggW<-pO*R?WtHV< z^(mWEu4e8SI>%C4>Kp0bf3_QO*A^4!3w-I|&(o1#?l!$CNE@w`%}3BXr*0x{SjQ%U zc~8Tg$;`v_w(~lh` Date: Thu, 16 Jun 2022 12:35:16 -0600 Subject: [PATCH 25/63] Mirror scroll behavior during escape altered. Pray I do not alter it further --- RELEASENOTES.md | 6 +++++- Rom.py | 2 +- data/base2current.bps | Bin 93062 -> 93061 bytes 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 286704e6..6ebb761e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -157,9 +157,13 @@ Same as above but both small keys and bigs keys of the dungeon are not allowed o #### StandardThrone +* Bug fixes + * Fix HC Ledge entrances after rescuing Zelda, also includes the throne spawn point now. +* Original Release * Changed standard dungeon generation to always have Throne Room in hyrule castle and always have sanctuary behind it * S&Q/death in standard after moving the tapestry but before delivering Zelda will result in spawning at the tapestry - * Mirror scroll will return you to Zelda's cell instead of last entrance. This reverts to normal behavior once the tapestry open trigger is reach + * Mirror scroll will return you to Zelda's cell instead of last entrance. This reverts to normal behavior once the tapestry open trigger is reached + #### Volatile diff --git a/Rom.py b/Rom.py index f062e4d3..cc28f3bd 100644 --- a/Rom.py +++ b/Rom.py @@ -37,7 +37,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '4941faf4496875a373a51119f2bedf4c' +RANDOMIZERBASEHASH = 'add982e935888df04ddfa570bc07bede' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index 4005551c18297514faad1e8e8ed9872d957d261b..060733f6e6579357fc34f52d728345c5cfe9ea01 100644 GIT binary patch delta 1735 zcmW;Mdr(tX8UXN|JTM88L=7RluNQ>kW2+)oc}PI1QP9Ck8LLs*MIG>>3k>oQNbZeB z3?#r#^ z+s$~A^+L6Q;t!hf+hF4Vj;f$t@Vl({Aw6lFdPFO!8ygKD8*hOl>}^!Vd>x8h^Pgk+IZ$Yy#R_uF72UKlV{FNVQp=s7gkS6(f38ag*qZ~LP%0}_sc0MXp~9XbIrNAwQ??tQ z`Dl5YaVPA|@krR?s(Ovrd^E?z7L1XviAQJ?EYHnF51=-843)rI)kf6LoKWpwj`EoQ z)Xw28JD?}LTc@=?G$!@lW90_Z**US7pbN0xIAxz>sJ|@C`@cZ~DQ6_=?0IK@U zy!?0Dz*>(BDmIhwR~V>Yjs619>r1)Kzre5Es zNdq30`3WWOE|ndlW`hlCMy>tA!!P@w7VySOd6RuUiH*qq!Etld-u=|lF}^(grgo4e zdN|kAiNay^;mv}o3EH_4Oox+-Qb&ePAu_^9a)JU6Ad=@f*tc-xj`}K>>yrir6`JUA zLGr(fe}njolo0er2^s0ADWQhg(s27l6`t8Nn%PqE7FpK<|2@0`ErWvQbo41SHNPkB z_?hr=Ib(TP2YlO{gH#aHvR2Z(;y0`M@~}$pjDBV80HUQFRlw&h35q48rRa{A)tZZ9AX}@7&)OBBc#hOOpw!MkCH`UE!&C0%G77SX zF2fU`OB)||NdC>;)5-&i{3}uUC(*rMMleF{-~J_c&Nqf_@P)Pr?S@2MjLtntxH;JP zQ({>4krwZJ&2{OLU%;M>kn=!B2&z~QvDUG+vbL}aR)&DTGUSY4oy{7_Drb#njbmNF zn#{U{b@9rOBYLmDM;7E2xhK6rM@qThZVBGidY?Nbl=b;t7|?Z~aacz#iP)JDGM^^* zyKNlOwF0!{4zYC0xLI@Ms&;3jm`Mb;(ng&w9^5c46q;1#enm$dEkMebz9J zIaTVAVeWO?k-iOkDSMfl)5{rVIF*!PWU2GbO(8@5l=I?DxJ1=E4i<*aRvWH9etcXu zUFJ=->LO<5uQKHpYn;`*6Ew_%+J?OYfnaa+2||p?<^2HlU9~@^e8=CjzwFngF|1!^sQ6Ebe@Si z>RQPI%V2_V1K&g)cNOuO>w`}P@ez@x?l7gkCm1%gRN~_jy;<39b|qoCJFRspVR%H3 z3Yh$l)F>%B)&z57O_UmL+4&V7L{vByRd=|3#X9!KpmQ9;k;h#9csCMM7m?>%m^q)U zM&S*cO;(v9j!-}QGxP4~APU=MDFCPmBxP6!LL#l)fP}jE*a(D!s|R=T4(AJc-x+92@mF+UuU{n;1{x! z-QAw13clr#G7(>sv4WZfCmaVE+tk)LROXn7Kx??oEjLmmXznZlUP(DJZsbIwyi(x@ Mn{KaL>vq@u4@G?T)Bpeg delta 1727 zcmW;Me^3)=8VB%w^8-SF5DS4|_^~b^%GDc0;ZQ(A(V7A}C|B@`C{{7dImNrSw(*#oaz&BC8K9KNO#QC>}0EB6nfj0CyN|MF;={6N1zhC=qHI_#pHx zeCOLPx$I_-(I(;+XeBD?=a1NVbqhlX9)VqA^u6a{DG^Z!Nq&VW8%%!bCXmnJL=Y)4Pw5=+DZCtZ&&fm@p9zkZgVY{+Id5yt@cUq$s?bU>02X}SGD ze-nn;muwleJ?EnmT1NE5*u+{wQzk&X?Xb3t(Uh7vfG;gIp|sxKI>xLC}{?~4oB5s6R%BxPozX{gMcLWPj6&OseqySh9H?dJYd zxki|>NY3ESdn-DZU@FL2z>B7Q$MLcsM!q4(aqE_SZ4ERzipyN5ru^_(w@cK4wv+fQ zq*d<~mc@60v6>K9?WLd&xaw`_Jy@(hfcq|3X5>_Y^N6+lL&ljvC0;y-!6zSdGC1Mr zXW?xqsmVZ_;7>KF{+lQ0SaUixaA_)JLvL*iihxgRLj~9@d{vu{ln{Kp0LkH<i#A5FWqh;?QGo^u^N1aQD52xM!4w$NMxzG! zr&b}r8o;Z417(1^ZVQ@%s=7Z&nJ1)6fK6rL3_PsMKzX33&zAWn|Hx=Ajj5Y#;XfG| zI8tANs$jA{D)_27@0QN2zNI^J+UHDkq>NT^=GKs&Q@O-~u^-;d6`RnEJMk7+e+D z8)KhCMY7j+JlPAiyeP#BSc=!?$1CAo!z}qJB4oX$~ zW=usxFMMs)FhixI%@9pTHzq7yI_o?Gnq2R>erIy7SD&!ddsaV#+wwGC{VP{Hn+M-4 zr7gt+*ad8i3ph&a#}jR7d%XJkCEJHD<04&e)#iH7R_HI!&7G336g#(2i5|bR`zswi zq~jOUq=>xW_ru2_+dt1<(RBnm`E->Gcb8Bfk)$^WMgwLnUo}N{0Y5>+%}`eF4kYyCkJjBYnbNCWZDJQIa}; zBYn!Q^XIPISQJMhg|XL1+0^Ip8BLVA1muO?=`9u|X}+_fv&F)v<4tHEmvvi%Vggl0 z@KG6Q4P3W~${dI+vqo364Y=|f`RhREFw(k*8~W@mB(Be+&R1~qyW5aIx!1_Z^%11z z*}L3tCvG60^3Gip|2KxZHlz1Bjq9pVEX*_7_j7{#4w1h=O)+wpXv+Wm2i#vCxP>SO zm@hZ-XL Date: Tue, 28 Jun 2022 13:05:38 -0600 Subject: [PATCH 26/63] Large refactor underway. Wrote new main method Implemented trap door shuffle to some degree Still needs the other types --- DoorShuffle.py | 585 ++++++++++++++++++++++++++++-- Doors.py | 26 +- DungeonGenerator.py | 4 +- RoomData.py | 8 +- source/dungeon/DungeonStitcher.py | 104 ++++-- 5 files changed, 648 insertions(+), 79 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index 3d20be76..b8c8abbe 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -6,13 +6,14 @@ from enum import unique, Flag from typing import DefaultDict, Dict, List from BaseClasses import RegionType, Region, Door, DoorType, Direction, Sector, CrystalBarrier, DungeonInfo, dungeon_keys -from BaseClasses import PotFlags, LocationType +from BaseClasses import PotFlags, LocationType, Direction from Doors import reset_portals from Dungeons import dungeon_regions, region_starts, standard_starts, split_region_starts from Dungeons import dungeon_bigs, dungeon_hints from Items import ItemFactory from RoomData import DoorKind, PairedDoor, reset_rooms from source.dungeon.DungeonStitcher import GenerationException, generate_dungeon +from source.dungeon.DungeonStitcher import ExplorationState as ExplorationState2 # from DungeonGenerator import generate_dungeon from DungeonGenerator import ExplorationState, convert_regions, pre_validate, determine_required_paths, drop_entrances from DungeonGenerator import create_dungeon_builders, split_dungeon_builder, simple_dungeon_builder, default_dungeon_entrances @@ -759,6 +760,131 @@ def find_entrance_region(portal): return None +# each dungeon_pool members is a pair of lists: dungeon names and regions in those dungeons +def main_dungeon_pool(dungeon_pool, world, player): + add_inaccessible_doors(world, player) + entrances_map, potentials, connections = determine_entrance_list(world, player) + connections_tuple = (entrances_map, potentials, connections) + entrances, splits = create_dungeon_entrances(world, player) + + dungeon_builders = {} + door_type_pools = [] + for pool, region_list in dungeon_pool: + if len(pool) == 1: + dungeon_key = next(pool) + sector_pool = convert_to_sectors(region_list, world, player) + merge_sectors(sector_pool, world, player) + dungeon_builders[dungeon_key] = simple_dungeon_builder(dungeon_key, sector_pool) + dungeon_builders[dungeon_key].entrance_list = list(entrances_map[dungeon_key]) + else: + if 'Hyrule Castle' in pool: + hc = world.get_dungeon('Hyrule Castle', player) + hc.dungeon_items.append(ItemFactory('Compass (Escape)', player)) + if 'Agahnims Tower' in pool: + at = world.get_dungeon('Agahnims Tower', player) + at.dungeon_items.append(ItemFactory('Compass (Agahnims Tower)', player)) + at.dungeon_items.append(ItemFactory('Map (Agahnims Tower)', player)) + sector_pool = convert_to_sectors(region_list, world, player) + merge_sectors(sector_pool, world, player) + # todo: which dungeon to create + dungeon_builders.update(create_dungeon_builders(sector_pool, connections_tuple, + world, player, pool, entrances, splits)) + door_type_pools.append((pool, DoorTypePool(sector_pool, world, player))) + + update_forced_keys(dungeon_builders, entrances_map, world, player) + recombinant_builders = {} + builder_info = entrances, splits, connections_tuple, world, player + handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map, builder_info) + + main_dungeon_generation(dungeon_builders, recombinant_builders, connections_tuple, world, player) + + setup_custom_door_types(world, player) + paths = determine_required_paths(world, player) + shuffle_door_types(door_type_pools, paths, world, player) + + check_required_paths(paths, 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 + if world.retro[player]: + target_items += 1 if world.dropshuffle[player] else 0 # the hc big key + else: + target_items += 29 # small keys in chests + if world.dropshuffle[player]: + target_items += 14 # 13 dropped smalls + 1 big + if world.pottery[player] not in ['none', 'cave']: + target_items += 19 # 19 pot keys + d_items = target_items - all_dungeon_items_cnt + world.pool_adjustment[player] = d_items + if not world.decoupledoors[player]: + smooth_door_pairs(world, player) + + +def update_forced_keys(dungeon_builders, entrances_map, world, player): + for builder in dungeon_builders.values(): + builder.entrance_list = list(entrances_map[builder.name]) + dungeon_obj = world.get_dungeon(builder.name, player) + for sector in builder.sectors: + for region in sector.regions: + region.dungeon = dungeon_obj + for loc in region.locations: + if loc.forced_item: + key_name = (dungeon_keys[builder.name] if loc.name != 'Hyrule Castle - Big Key Drop' + else dungeon_bigs[builder.name]) + loc.forced_item = loc.item = ItemFactory(key_name, player) + + +def finish_up_work(world, player): + dungeon_builders = world.dungeon_layouts[player] + # Re-assign dungeon bosses + gt = world.get_dungeon('Ganons Tower', player) + for name, builder in dungeon_builders.items(): + reassign_boss('GT Ice Armos', 'bottom', builder, gt, world, player) + reassign_boss('GT Lanmolas 2', 'middle', builder, gt, world, player) + reassign_boss('GT Moldorm', 'top', builder, gt, world, player) + + sanctuary = world.get_region('Sanctuary', player) + d_name = sanctuary.dungeon.name + if d_name != 'Hyrule Castle': + possible_portals = [] + for portal_name in dungeon_portals[d_name]: + portal = world.get_portal(portal_name, player) + if portal.door.name == 'Sanctuary S': + possible_portals.clear() + possible_portals.append(portal) + break + if not portal.destination and not portal.deadEnd: + possible_portals.append(portal) + if len(possible_portals) == 1: + world.sanc_portal[player] = possible_portals[0] + else: + reachable_portals = [] + for portal in possible_portals: + start_area = portal.door.entrance.parent_region + state = ExplorationState(dungeon=d_name) + state.visit_region(start_area) + state.add_all_doors_check_unattached(start_area, world, player) + explore_state(state, world, player) + if state.visited_at_all(sanctuary): + reachable_portals.append(portal) + world.sanc_portal[player] = random.choice(reachable_portals) + if world.intensity[player] >= 3: + if player in world.sanc_portal: + portal = world.sanc_portal[player] + else: + portal = world.get_portal('Sanctuary', player) + target = portal.door.entrance.parent_region + connect_simple_door(world, 'Sanctuary Mirror Route', target, player) + + check_entrance_fixes(world, player) + + if world.standardize_palettes[player] == 'standardize' and world.doorShuffle[player] not in ['basic']: + palette_assignment(world, player) + + refine_hints(dungeon_builders) + refine_boss_exits(world, player) + + # def unpair_all_doors(world, player): # for paired_door in world.paired_doors[player]: # paired_door.pair = False @@ -1522,7 +1648,9 @@ def setup_custom_door_types(world, player): 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': []} + # todo: dash/bomb door pool specific + customizeable_types = ['Key Door', 'Dash Door', 'Bomb Door', 'Trap Door', 'Big Key Door'] + world.custom_door_types[player] = type_map = {x: defaultdict(list) for x in customizeable_types} for door, dest in custom_doors['doors'].items(): if isinstance(dest, dict): if 'type' in dest: @@ -1531,17 +1659,206 @@ def setup_custom_door_types(world, 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': + else: # 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) + + +class DoorTypePool: + def __init__(self, sectors, world, player): + self.smalls = 0 + self.bombable = 0 + self.dashable = 0 + self.bigs = 0 + self.traps = 0 + # self.tricky = 0 + # self.hidden = 0 + # todo: custom pools? + self.count_via_sectors(sectors, world, player) + + def count_via_sectors(self, sectors, world, player): + skips = set() + for sector in sectors: + for region in sector.regions: + for ext in region.exits: + if ext.door: + d = ext.door + if d.name not in skips and d.type in [DoorType.Normal, DoorType.Interior]: + if d.smallKey: + self.smalls += 1 + elif d.bigKey: + self.bigs += 1 + elif d.blocked and d.trapFlag and 'Boss' not in d.name and 'Agahnim' not in d.name: + self.traps += 1 + elif d.name == 'TR Compass Room NW': + self.tricky += 1 + elif d.name in ['Skull Vines NW', 'Tower Altar NW']: + self.hidden += 1 + else: + kind = world.get_room(d.roomIndex, player).kind(d) + if kind == DoorKind.Bombable: + self.bombable += 1 + elif kind == DoorKind.Dashable: + self.dashable += 1 + if d.type == DoorType.Interior: + skips.add(d.dest.name) ## lookup a different way for interior door shuffle + elif d.type == DoorType.Normal: + for dp in world.paired_doors[player]: + if d.name == dp.door_a or d.name == dp.door_b: + skips.add(dp.door_b if d.name == dp.door_a else dp.door_a) + break + + def chaos_shuffle(self): + weights = [1, 2, 4, 3, 2, 1] + self.smalls = random.choices(self.get_choices(self.smalls), weights=weights) + self.bombable = random.choices(self.get_choices(self.bombable), weights=weights) + self.dashable = random.choices(self.get_choices(self.dashable), weights=weights) + self.bigs = random.choices(self.get_choices(self.bigs), weights=weights) + self.traps = random.choices(self.get_choices(self.traps), weights=weights) + # self.tricky = random.choices(self.get_choices(self.tricky), weights=weights) + # self.hidden = random.choices(self.get_choices(self.hidden), weights=weights) + + @staticmethod + def get_choices(number): + return [max(number-i, 0) for i in range(-1, 5)] + + +class BuilderDoorCandidates: + def __init__(self): + self.small = set() + self.big = set() + self.trap = set() + self.bombable = set() + self.dashable = set() + + self.checked = set() + + +def shuffle_door_types(door_type_pools, paths, world, player): + start_regions_map = {} + for name, builder in world.dungeon_layouts[player]: + start_regions = convert_regions(builder.path_entrances, world, player) + start_regions_map[name] = start_regions + used_doors = shuffle_door_traps(door_type_pools, paths, start_regions_map, world, player) + # big keys + # small keys + + # bombable / dashable + + # tricky / hidden + + +def shuffle_door_traps(door_type_pools, paths, start_regions_map, world, player): + used_doors = set() + for pool, door_type_pool in door_type_pools: + ttl = 0 + suggestion_map, trap_map, flex_map = {}, {}, {} + remaining = door_type_pool.traps + if player in world.custom_door_types: + custom_trap_doors = world.custom_door_types[player]['Trap Door'] + else: + custom_trap_doors = defaultdict(list) + + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + find_trappable_candidates(builder, world, player) + if custom_trap_doors[dungeon]: + builder.candidates.trap = filter_key_door_pool(builder.candidates.trap, custom_trap_doors[dungeon]) + remaining -= len(custom_trap_doors[dungeon]) + ttl += len(builder.candidates.trap) + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + proportion = len(builder.candidates.trap) + calc = int(round(proportion * door_type_pool.traps/ttl)) + suggested = min(proportion, calc) + remaining -= suggested + suggestion_map[dungeon] = suggested + flex_map[dungeon] = (proportion - suggested) if suggested < proportion else 0 + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + valid_traps, trap_number = find_valid_trap_combination(builder, suggestion_map[dungeon], + start_regions_map[dungeon], paths, world, player) + trap_map[dungeon] = valid_traps + if trap_number < suggestion_map[dungeon]: + flex_map[dungeon] = 0 + remaining += suggestion_map[dungeon] - trap_number + suggestion_map[dungeon] = trap_number + builder_order = [x for x in pool if flex_map[x] > 0] + queue = deque(builder_order) + while len(queue) > 0 and remaining > 0: + dungeon = queue.popleft() + builder = world.dungeon_layouts[player][dungeon] + increased = suggestion_map[dungeon] + 1 + valid_traps, trap_number = find_valid_trap_combination(builder, increased, start_regions_map[dungeon], + paths, world, player) + if valid_traps: + trap_map[dungeon] = valid_traps + remaining -= 1 + suggestion_map[dungeon] = increased + flex_map[dungeon] -= 1 + if flex_map[dungeon] > 0: + queue.append(dungeon) + # time to re-assign + reassign_trap_doors(trap_map, world, player) + for name, traps in trap_map.items(): + used_doors.update(traps) + return used_doors + + +def shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world, player): + for pool, door_type_pool in door_type_pools: + ttl = 0 + suggestion_map, bk_map, flex_map = {}, {}, {} + remaining = door_type_pool.bigs + if player in world.custom_door_types: + custom_bk_doors = world.custom_door_types[player]['Big Key Door'] + else: + custom_bk_doors = defaultdict(list) + + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + find_big_key_candidates(builder, used_doors, world, player) + if custom_bk_doors[dungeon]: + builder.candidates.trap = filter_key_door_pool(builder.candidates.big, custom_bk_doors[dungeon]) + remaining -= len(custom_bk_doors[dungeon]) + ttl += len(builder.candidates.big) + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + proportion = len(builder.candidates.big) + calc = int(round(proportion * door_type_pool.big/ttl)) + suggested = min(proportion, calc) + remaining -= suggested + suggestion_map[dungeon] = suggested + flex_map[dungeon] = (proportion - suggested) if suggested < proportion else 0 + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + valid_doors, bk_number = find_valid_bk_combination(builder, suggestion_map[dungeon], + start_regions_map[dungeon], world, player) + bk_map[dungeon] = valid_doors + if bk_number < suggestion_map[dungeon]: + flex_map[dungeon] = 0 + remaining += suggestion_map[dungeon] - bk_number + suggestion_map[dungeon] = bk_number + builder_order = [x for x in pool if flex_map[x] > 0] + queue = deque(builder_order) + while len(queue) > 0 and remaining > 0: + dungeon = queue.popleft() + builder = world.dungeon_layouts[player][dungeon] + increased = suggestion_map[dungeon] + 1 + valid_doors, bk_number = find_valid_bk_combination(builder, increased, start_regions_map[dungeon], + paths, world, player) + if valid_doors: + bk_map[dungeon] = valid_doors + remaining -= 1 + suggestion_map[dungeon] = increased + flex_map[dungeon] -= 1 + if flex_map[dungeon] > 0: + queue.append(dungeon) + # time to re-assign + reassign_big_key_doors(bk_map, world, player) def shuffle_key_doors(builder, world, player): @@ -1582,6 +1899,223 @@ def find_current_key_doors(builder): return current_doors +def find_trappable_candidates(builder, world, player): + if world.door_type_mode[player] != 'original': # all, chaos + r_set = builder.master_sector.region_set() + filtered_doors = [ext.door for r in r_set for ext in r.exits + if ext.door and ext.door.type in [DoorType.Interior, DoorType.Normal]] + for d in filtered_doors: + # I only support the first 3 due to the trapFlag right now + if 0 <= d.doorListPos < 3 and not d.entranceFlag: + room = world.get_room(d.roomIndex, player) + kind = room.kind(d) + if d.type == DoorType.Interior: + if (kind in [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable, + DoorKind.BigKey] + or (d.blocked and d.trapFlag != 0 and 'Boss' not in d.name and 'Agahnim' not in d.name) + or (kind == DoorKind.TrapTriggerable and d.direction in [Direction.North, Direction.East]) + or (kind == DoorKind.Trap2 and d.direction in [Direction.South, Direction.West])): + builder.candidates.trap.add(d) + elif d.type == DoorType.Normal: + if (kind in [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable, + DoorKind.BigKey] + or (d.blocked and d.trapFlag != 0 and 'Boss' not in d.name and 'Agahnim' not in d.name)): + builder.candidates.trap.add(d) + else: + r_set = builder.master_sector.region_set() + for r in r_set: + for ext in r.exits: + if ext.door: + d = ext.door + if d.blocked and d.trapFlag != 0 and 'Boss' not in d.name and 'Agahnim' not in d.name: + builder.candidates.trap.add(d) + + +def find_valid_trap_combination(builder, suggested, start_regions, paths, world, player, drop=True): + trap_door_pool = builder.candidates.trap + trap_doors_needed = suggested + if player in world.custom_door_types: + custom_trap_doors = world.custom_door_types[player]['Trap Door'][builder.name] + else: + custom_trap_doors = [] + if custom_trap_doors: + trap_door_pool = filter_key_door_pool(trap_door_pool, custom_trap_doors) + trap_doors_needed -= len(custom_trap_doors) + if len(trap_door_pool) < trap_doors_needed: + if not drop: + return None, 0 + trap_doors_needed = len(trap_door_pool) + combinations = ncr(len(trap_door_pool), trap_doors_needed) + itr = 0 + sample_list = build_sample_list(combinations, 1000) + proposal = kth_combination(sample_list[itr], trap_door_pool, trap_doors_needed) + proposal.extend(custom_trap_doors) + + start_regions = filter_start_regions(builder, start_regions, world, player) + while not validate_trap_layout(proposal, builder, start_regions, paths, world, player): + itr += 1 + if itr >= len(sample_list): + if not drop: + return None, 0 + trap_doors_needed -= 1 + if trap_doors_needed < 0: + raise Exception(f'Bad dungeon {builder.name} - maybe custom trap doors are bad') + combinations = ncr(len(trap_door_pool), trap_doors_needed) + sample_list = build_sample_list(combinations, 1000) + itr = 0 + proposal = kth_combination(sample_list[itr], trap_door_pool, trap_doors_needed) + proposal.extend(custom_trap_doors) + return proposal, trap_doors_needed + + +# eliminate start region if portal marked as destination +def filter_start_regions(builder, start_regions, world, player): + std_flag = world.mode[player] == 'standard' and builder.name == 'Hyrule Castle' + excluded = {} + for region in start_regions: + portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region == region), None) + if portal and portal.destination: + excluded[region] = None + if std_flag and (not portal or portal.find_portal_entrance().parent_region.name != 'Hyrule Castle Courtyard'): + excluded[region] = None + return [x for x in start_regions if x not in excluded.keys()] + + +def validate_trap_layout(proposal, builder, start_regions, paths, world, player): + return check_required_paths_with_traps(paths, proposal, builder.name, start_regions, world, player) + + +def check_required_paths_with_traps(paths, proposal, dungeon_name, start_regions, world, player): + if len(paths[dungeon_name]) > 0: + states_to_explore = {} + common_starts = tuple(start_regions) + for path in paths[dungeon_name]: + if type(path) is tuple: + states_to_explore[tuple([path[0]])] = (path[1], 'any') + else: + if common_starts not in states_to_explore: + states_to_explore[common_starts] = ([], 'all') + states_to_explore[common_starts][0].append(path) + cached_initial_state = None + for start_regs, info in states_to_explore.items(): + dest_regs, path_type = info + if type(dest_regs) is not list: + dest_regs = [dest_regs] + check_paths = convert_regions(dest_regs, world, player) + start_regions = convert_regions(start_regs, world, player) + initial = start_regs == common_starts + if not initial or cached_initial_state is None: + init = determine_init_crystal(initial, cached_initial_state, start_regions) + state = ExplorationState2(init, dungeon_name) + for region in start_regions: + state.visit_region(region) + state.add_all_doors_check_proposed_traps(region, proposal, world, player) + explore_state_proposed_traps(state, world, player) + if initial and cached_initial_state is None: + cached_initial_state = state + else: + state = cached_initial_state + if path_type == 'any': + valid, bad_region = check_if_any_regions_visited(state, check_paths) + else: + valid, bad_region = check_if_all_regions_visited(state, check_paths) + if not valid: + return False + return True + + +def reassign_trap_doors(trap_map, world, player): + logger = logging.getLogger('') + for name, traps in trap_map.items(): + builder = world.dungeon_layouts[player][name] + queue = deque(find_current_trap_doors(builder)) + while len(queue) > 0: + d = queue.pop() + if d.type is DoorType.Interior and d not in traps: + room = world.get_room(d.roomIndex, player) + kind = room.kind(d) + if kind == DoorKind.Trap: + new_type = (DoorKind.TrapTriggerable if d.direction in [Direction.South, Direction.East] else + DoorKind.Trap2) + room.change(d.doorListPos, new_type) + elif kind in [DoorKind.Trap2, DoorKind.TrapTriggerable]: + room.change(d.doorListPos, DoorKind.Normal) + d.blocked = False + elif d.type is DoorType.Normal and d not in traps: + world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) + d.blocked = False + for d in traps: + change_door_to_trap(d, world, player) + world.spoiler.set_door_type(d.name, 'Trap Door', player) + logger.debug('Key Door: %s', d.name) + + +def find_current_trap_doors(builder): + current_doors = [] + for region in builder.master_sector.regions: + for ext in region.exits: + d = ext.door + if d and d.blocked and d.trapFlag != 0: + current_doors.append(d) + return current_doors + + +def change_door_to_trap(d, world, player): + room = world.get_room(d.roomIndex, player) + if d.type is DoorType.Interior: + kind = room.kind(d) + new_kind = None + if kind == DoorKind.TrapTriggerable and d.direction in [Direction.North, Direction.West]: + new_kind = DoorKind.Trap + elif kind == DoorKind.Trap2 and d.direction in [Direction.South, Direction.East]: + new_kind = DoorKind.Trap + elif d.direction in [Direction.North, Direction.West]: + new_kind = DoorKind.Trap2 + elif d.direction in [Direction.South, Direction.East]: + new_kind = DoorKind.TrapTriggerable + if new_kind: + d.blocked = True + verify_door_list_pos(d, room, world, player, pos=3) + d.trapFlag = {0: 0x4, 1: 0x2, 2: 0x1}[d.doorListPos] + room.change(d.doorListPos, new_kind) + elif d.type is DoorType.Normal: + d.blocked = True + verify_door_list_pos(d, room, world, player, pos=3) + d.trapFlag = {0: 0x4, 1: 0x2, 2: 0x1}[d.doorListPos] + room.change(d.doorListPos, DoorKind.Trap) + + +def find_big_key_candidates(builder, used, world, player): + if world.door_type_mode[player] != 'original': # all, chaos + r_set = builder.master_sector.region_set() + filtered_doors = [ext.door for r in r_set for ext in r.exits + if ext.door and ext.door.type in [DoorType.Interior, DoorType.Normal]] + for d in filtered_doors: + if 0 <= d.doorListPos < 4 and not d.entranceFlag: + room = world.get_room(d.roomIndex, player) + kind = room.kind(d) + if d.type in [DoorType.Interior, DoorType.Normal]: + if (kind in [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable, + DoorKind.BigKey] and d not in used) + or (d.blocked and d.trapFlag != 0 and 'Boss' not in d.name and 'Agahnim' not in d.name) + or (kind == DoorKind.TrapTriggerable and d.direction in [Direction.North, Direction.East]) + or (kind == DoorKind.Trap2 and d.direction in [Direction.South, Direction.West])): + builder.candidates.big.add(d) + elif d.type == DoorType.Normal: + if (kind in [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable, + DoorKind.BigKey] + or (d.blocked and d.trapFlag != 0 and 'Boss' not in d.name and 'Agahnim' not in d.name)): + builder.candidates.trap.add(d) + else: + r_set = builder.master_sector.region_set() + for r in r_set: + for ext in r.exits: + if ext.door: + d = ext.door + if d.bigKey and d.type in [DoorType.Normal, DoorType.Interior]: + builder.candidates.big.add(d) + + def find_small_key_door_candidates(builder, start_regions, world, player): # traverse dungeon and find candidates candidates = [] @@ -1638,16 +2172,7 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True sample_list = build_sample_list(combinations) 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 - std_flag = world.mode[player] == 'standard' and builder.name == 'Hyrule Castle' - excluded = {} - for region in start_regions: - portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region == region), None) - if portal and portal.destination: - excluded[region] = None - if std_flag and (not portal or portal.find_portal_entrance().parent_region.name != 'Hyrule Castle Courtyard'): - excluded[region] = None - start_regions = [x for x in start_regions if x not in excluded.keys()] + start_regions = filter_start_regions(builder, start_regions, world, player) key_layout = build_key_layout(builder, start_regions, proposal, world, player) determine_prize_lock(key_layout, world, player) @@ -1682,13 +2207,12 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True return True -def build_sample_list(combinations): - if combinations <= 1000000: +def build_sample_list(combinations, max_combinations=1000000): + if combinations <= max_combinations: sample_list = list(range(0, int(combinations))) - else: num_set = set() - while len(num_set) < 1000000: + while len(num_set) < max_combinations: num_set.add(random.randint(0, combinations)) sample_list = list(num_set) sample_list.sort() @@ -1885,9 +2409,9 @@ def change_door_to_small_key(d, 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() +def verify_door_list_pos(d, room, world, player, pos=4): + if d.doorListPos >= pos: + new_index = room.next_free(pos) 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 @@ -1895,7 +2419,7 @@ def verify_door_list_pos(d, room, world, player): other.doorListPos = d.doorListPos d.doorListPos = new_index else: - raise Exception(f'Invalid stateful door: {d.name}. Only 4 stateful doors per supertile') + raise Exception(f'Invalid stateful door: {d.name}. Only {pos} stateful doors per supertile') def smooth_door_pairs(world, player): @@ -2261,6 +2785,15 @@ def explore_state(state, world, player): state.add_all_doors_check_unattached(connect_region, world, player) +def explore_state_proposed_traps(state, proposed_traps, world, player): + while len(state.avail_doors) > 0: + door = state.next_avail_door().door + connect_region = world.get_entrance(door.name, player).connected_region + if not state.visited(connect_region) and valid_region_to_explore(connect_region, world, player): + state.visit_region(connect_region) + state.add_all_doors_check_proposed_traps(connect_region, proposed_traps, world, player) + + def explore_state_not_inaccessible(state, world, player): while len(state.avail_doors) > 0: door = state.next_avail_door().door diff --git a/Doors.py b/Doors.py index 26d474cc..99b4fab3 100644 --- a/Doors.py +++ b/Doors.py @@ -348,7 +348,7 @@ def create_doors(world, player): create_door(player, 'Tower Catwalk North Stairs', StrS).dir(No, 0x40, Left, High), create_door(player, 'Tower Antechamber South Stairs', StrS).dir(So, 0x30, Left, High), create_door(player, 'Tower Antechamber NW', Intr).dir(No, 0x30, Left, High).pos(1), - create_door(player, 'Tower Altar SW', Intr).dir(So, 0x30, Left, High).no_exit().pos(1), + create_door(player, 'Tower Altar SW', Intr).dir(So, 0x30, Left, High).no_exit().trap(0x2).pos(1), create_door(player, 'Tower Altar NW', Nrml).dir(No, 0x30, Left, High).pos(0), create_door(player, 'Tower Agahnim 1 SW', Nrml).dir(So, 0x20, Left, High).no_exit().trap(0x4).pos(0), @@ -667,7 +667,7 @@ def create_doors(world, player): create_door(player, 'Thieves Conveyor Maze SW', Intr).dir(So, 0xbc, Left, High).pos(6), create_door(player, 'Thieves Pot Alcove Top NW', Intr).dir(No, 0xbc, Left, High).pos(6), create_door(player, 'Thieves Conveyor Maze EN', Intr).dir(Ea, 0xbc, Top, High).pos(2), - create_door(player, 'Thieves Hallway WN', Intr).dir(We, 0xbc, Top, High).no_exit().pos(2), + create_door(player, 'Thieves Hallway WN', Intr).dir(We, 0xbc, Top, High).no_exit().trap(0x1).pos(2), create_door(player, 'Thieves Conveyor Maze Down Stairs', Sprl).dir(Dn, 0xbc, 0, HTH).ss(A, 0x11, 0x80, True, True), create_door(player, 'Thieves Boss SE', Nrml).dir(So, 0xac, Right, High).no_exit().trap(0x4).pos(0), create_door(player, 'Thieves Spike Track ES', Nrml).dir(Ea, 0xbb, Bot, High).pos(5), @@ -742,7 +742,7 @@ def create_doors(world, player): create_door(player, 'Ice Big Key Push Block', Lgcl), create_door(player, 'Ice Big Key Down Ladder', Lddr).dir(So, 0x1f, 3, High), create_door(player, 'Ice Stalfos Hint SE', Intr).dir(So, 0x3e, Right, High).pos(0), - create_door(player, 'Ice Conveyor NE', Intr).dir(No, 0x3e, Right, High).no_exit().pos(0), + create_door(player, 'Ice Conveyor NE', Intr).dir(No, 0x3e, Right, High).no_exit().trap(0x4).pos(0), create_door(player, 'Ice Conveyor to Crystal', Lgcl), create_door(player, 'Ice Conveyor Crystal Exit', Lgcl), create_door(player, 'Ice Conveyor SW', Nrml).dir(So, 0x3e, Left, High).small_key().pos(1).portal(Z, 0x20), @@ -760,7 +760,7 @@ def create_doors(world, player): create_door(player, 'Ice Firebar ES', Intr).dir(Ea, 0x5e, Bot, High).pos(3), create_door(player, 'Ice Firebar Down Ladder', Lddr).dir(So, 0x5e, 5, High), create_door(player, 'Ice Spike Cross NE', Intr).dir(No, 0x5e, Right, High).pos(1), - create_door(player, 'Ice Falling Square SE', Intr).dir(So, 0x5e, Right, High).no_exit().pos(1), + create_door(player, 'Ice Falling Square SE', Intr).dir(So, 0x5e, Right, High).no_exit().trap(0x1).pos(1), create_door(player, 'Ice Falling Square Hole', Hole), create_door(player, 'Ice Spike Room WS', Nrml).dir(We, 0x5f, Bot, High).small_key().pos(0), create_door(player, 'Ice Spike Room Down Stairs', Sprl).dir(Dn, 0x5f, 3, HTH).ss(Z, 0x11, 0x48, True, True), @@ -840,12 +840,12 @@ def create_doors(world, player): create_door(player, 'Mire Hub Top NW', Nrml).dir(No, 0xc2, Left, High).pos(2), create_door(player, 'Mire Lone Shooter WS', Nrml).dir(We, 0xc3, Bot, High).pos(6), create_door(player, 'Mire Lone Shooter ES', Intr).dir(Ea, 0xc3, Bot, High).pos(3), - create_door(player, 'Mire Falling Bridge WS', Intr).dir(We, 0xc3, Bot, High).no_exit().pos(3), + create_door(player, 'Mire Falling Bridge WS', Intr).dir(We, 0xc3, Bot, High).no_exit().trap(0x8).pos(3), create_door(player, 'Mire Falling Bridge W', Intr).dir(We, 0xc3, Mid, High).pos(2), - create_door(player, 'Mire Failure Bridge E', Intr).dir(Ea, 0xc3, Mid, High).no_exit().pos(2), + create_door(player, 'Mire Failure Bridge E', Intr).dir(Ea, 0xc3, Mid, High).no_exit().trap(0x1).pos(2), create_door(player, 'Mire Failure Bridge W', Nrml).dir(We, 0xc3, Mid, High).pos(5), create_door(player, 'Mire Falling Bridge WN', Intr).dir(We, 0xc3, Top, High).pos(1), - create_door(player, 'Mire Map Spike Side EN', Intr).dir(Ea, 0xc3, Top, High).no_exit().pos(1), + create_door(player, 'Mire Map Spike Side EN', Intr).dir(Ea, 0xc3, Top, High).no_exit().trap(0x2).pos(1), create_door(player, 'Mire Map Spot WN', Nrml).dir(We, 0xc3, Top, High).small_key().pos(0), create_door(player, 'Mire Crystal Dead End NW', Nrml).dir(No, 0xc3, Left, High).pos(4), create_door(player, 'Mire Map Spike Side Drop Down', Lgcl), @@ -903,7 +903,7 @@ def create_doors(world, player): create_door(player, 'Mire Tile Room NW', Intr).dir(No, 0xc1, Left, High).pos(3), create_door(player, 'Mire Compass Room SW', Intr).dir(So, 0xc1, Left, High).pos(3), create_door(player, 'Mire Compass Room EN', Intr).dir(Ea, 0xc1, Top, High).pos(2), - create_door(player, 'Mire Wizzrobe Bypass WN', Intr).dir(We, 0xc1, Top, High).no_exit().pos(2), + create_door(player, 'Mire Wizzrobe Bypass WN', Intr).dir(We, 0xc1, Top, High).no_exit().trap(0x1).pos(2), create_door(player, 'Mire Compass Blue Barrier', Lgcl), create_door(player, 'Mire Compass Chest Exit', Lgcl), create_door(player, 'Mire Neglected Room NE', Nrml).dir(No, 0xd1, Right, High).pos(2), @@ -912,7 +912,7 @@ def create_doors(world, player): create_door(player, 'Mire Neglected Room SE', Intr).dir(So, 0xd1, Right, High).pos(3), create_door(player, 'Mire Chest View NE', Intr).dir(No, 0xd1, Right, High).pos(3), create_door(player, 'Mire BK Chest Ledge WS', Intr).dir(We, 0xd1, Bot, High).pos(0), - create_door(player, 'Mire Warping Pool ES', Intr).dir(Ea, 0xd1, Bot, High).no_exit().pos(0), + create_door(player, 'Mire Warping Pool ES', Intr).dir(Ea, 0xd1, Bot, High).no_exit().trap(0x4).pos(0), create_door(player, 'Mire Warping Pool Warp', Warp), create_door(player, 'Mire Torches Top Down Stairs', Sprl).dir(Dn, 0x97, 0, HTH).ss(A, 0x11, 0xb0, True).kill(), create_door(player, 'Mire Torches Top SW', Intr).dir(So, 0x97, Left, High).pos(1), @@ -1011,7 +1011,7 @@ def create_doors(world, player): create_door(player, 'TR Big Chest Entrance SE', Nrml).dir(So, 0x24, Right, High).pos(4).kill().portal(X, 0x00), create_door(player, 'TR Big Chest Entrance Gap', Lgcl), create_door(player, 'TR Big Chest NE', Intr).dir(No, 0x24, Right, High).pos(3), - create_door(player, 'TR Dodgers SE', Intr).dir(So, 0x24, Right, High).no_exit().pos(3), + create_door(player, 'TR Dodgers SE', Intr).dir(So, 0x24, Right, High).no_exit().trap(0x8).pos(3), create_door(player, 'TR Dodgers NE', Nrml).dir(No, 0x24, Right, High).big_key().pos(0), create_door(player, 'TR Lazy Eyes SE', Nrml).dir(So, 0x23, Right, High).pos(0).portal(X, 0x00), create_door(player, 'TR Lazy Eyes ES', Nrml).dir(Ea, 0x23, Bot, High).pos(1), @@ -1073,7 +1073,7 @@ def create_doors(world, player): create_door(player, 'GT Hope Room EN', Nrml).dir(Ea, 0x8c, Top, High).trap(0x4).pos(0), create_door(player, 'GT Torch EN', Intr).dir(Ea, 0x8c, Top, High).small_key().pos(2), create_door(player, 'GT Hope Room WN', Intr).dir(We, 0x8c, Top, High).small_key().pos(2), - create_door(player, 'GT Torch SW', Intr).dir(So, 0x8c, Left, High).no_exit().pos(1), + create_door(player, 'GT Torch SW', Intr).dir(So, 0x8c, Left, High).no_exit().trap(0x2).pos(1), create_door(player, 'GT Big Chest NW', Intr).dir(No, 0x8c, Left, High).pos(1), create_door(player, 'GT Blocked Stairs Down Stairs', Sprl).dir(Dn, 0x8c, 3, HTH).ss(Z, 0x12, 0x40, True, True).kill(), create_door(player, 'GT Blocked Stairs Block Path', Lgcl), @@ -1179,7 +1179,7 @@ def create_doors(world, player): create_door(player, 'GT Ice Armos NE', Intr).dir(No, 0x1c, Right, High).pos(0), create_door(player, 'GT Big Key Room SE', Intr).dir(So, 0x1c, Right, High).pos(0), create_door(player, 'GT Ice Armos WS', Intr).dir(We, 0x1c, Bot, High).pos(1), - create_door(player, 'GT Four Torches ES', Intr).dir(Ea, 0x1c, Bot, High).no_exit().pos(1), + create_door(player, 'GT Four Torches ES', Intr).dir(Ea, 0x1c, Bot, High).no_exit().trap(0x2).pos(1), create_door(player, 'GT Four Torches NW', Intr).dir(No, 0x1c, Left, High).pos(2), create_door(player, 'GT Fairy Abyss SW', Intr).dir(So, 0x1c, Left, High).pos(2), create_door(player, 'GT Four Torches Up Stairs', Sprl).dir(Up, 0x1c, 0, HTH).ss(Z, 0x1b, 0x2c, True, True), @@ -1211,7 +1211,7 @@ def create_doors(world, player): create_door(player, 'GT Beam Dash WS', Intr).dir(We, 0x6c, Bot, High).pos(0), create_door(player, 'GT Lanmolas 2 ES', Intr).dir(Ea, 0x6c, Bot, High).pos(0), create_door(player, 'GT Lanmolas 2 NW', Intr).dir(No, 0x6c, Left, High).pos(1), - create_door(player, 'GT Quad Pot SW', Intr).dir(So, 0x6c, Left, High).no_exit().pos(1), + create_door(player, 'GT Quad Pot SW', Intr).dir(So, 0x6c, Left, High).no_exit().trap(0x2).pos(1), create_door(player, 'GT Quad Pot Up Stairs', Sprl).dir(Up, 0x6c, 0, HTH).ss(A, 0x1b, 0x6c, True, True), create_door(player, 'GT Wizzrobes 1 Down Stairs', Sprl).dir(Dn, 0xa5, 0, HTH).ss(A, 0x12, 0x80, True, True), create_door(player, 'GT Wizzrobes 1 SW', Intr).dir(So, 0xa5, Left, High).pos(2), diff --git a/DungeonGenerator.py b/DungeonGenerator.py index eb5ef97b..7e3b2ef7 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -1262,7 +1262,7 @@ def simple_dungeon_builder(name, sector_list): return builder -def create_dungeon_builders(all_sectors, connections_tuple, world, player, +def create_dungeon_builders(all_sectors, connections_tuple, world, player, dungeon_pool, dungeon_entrances=None, split_dungeon_entrances=None): logger = logging.getLogger('') logger.info('Shuffling Dungeon Sectors') @@ -1278,7 +1278,7 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, global_pole = GlobalPolarity(candidate_sectors) dungeon_map = {} - for key in dungeon_regions.keys(): + for key in dungeon_pool: dungeon_map[key] = DungeonBuilder(key) for key in dungeon_boss_sectors.keys(): current_dungeon = dungeon_map[key] diff --git a/RoomData.py b/RoomData.py index e3f432d0..cfdd183f 100644 --- a/RoomData.py +++ b/RoomData.py @@ -316,9 +316,9 @@ class Room(object): byte_array.append(kind.value) return byte_array - def next_free(self): + def next_free(self, pos=4): for i, door in enumerate(self.doorList): - if i >= 4: + if i >= pos: return None pos, kind = door if kind not in [DoorKind.SmallKey, DoorKind.Dashable, DoorKind.Bombable, DoorKind.TrapTriggerable, @@ -395,8 +395,8 @@ class DoorKind(Enum): Bombable = 0x2E BlastWall = 0x30 Hidden = 0x32 - TrapTriggerable = 0x36 # right side trap or south side trap - Trap2 = 0x38 # left side trap or north side trap + TrapTriggerable = 0x36 # right side trap or south side trap (West, South) + Trap2 = 0x38 # left side trap or north side trap (East, North) NormalLow2 = 0x40 TrapTriggerableLow = 0x44 Warp = 0x46 diff --git a/source/dungeon/DungeonStitcher.py b/source/dungeon/DungeonStitcher.py index e16b449d..05721c9b 100644 --- a/source/dungeon/DungeonStitcher.py +++ b/source/dungeon/DungeonStitcher.py @@ -68,16 +68,11 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon entrance_regions = [x for x in entrance_regions if x not in excluded.keys()] doors_to_connect, idx = {}, 0 all_regions = set() - bk_special = False for sector in builder.sectors: for door in sector.outstanding_doors: doors_to_connect[door.name] = door, idx idx += 1 all_regions.update(sector.regions) - bk_special |= check_for_special(sector.regions) - bk_needed = False - for sector in builder.sectors: - bk_needed |= determine_if_bk_needed(sector, split_dungeon, bk_special, world, player) finished = False # flag if standard and this is hyrule castle paths = determine_paths_for_dungeon(world, player, all_regions, name) @@ -96,9 +91,9 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon if hash_code not in hash_code_set: hash_code_set.add(hash_code) explored_state = explore_proposal(name, entrance_regions, all_regions, proposed_map, doors_to_connect, - bk_needed, bk_special, world, player) + world, player) if check_valid(name, explored_state, proposed_map, doors_to_connect, all_regions, - bk_needed, bk_special, paths, entrance_regions, world, player): + paths, entrance_regions, world, player): finished = True else: proposed_map, hash_code = modify_proposal(proposed_map, explored_state, doors_to_connect, @@ -217,29 +212,21 @@ def modify_proposal(proposed_map, explored_state, doors_to_connect, hash_code_se return proposed_map, hash_code -def explore_proposal(name, entrance_regions, all_regions, proposed_map, valid_doors, - bk_needed, bk_special, world, player): +def explore_proposal(name, entrance_regions, all_regions, proposed_map, valid_doors, world, player): start = ExplorationState(dungeon=name) - start.big_key_special = bk_special - - bk_flag = False if world.bigkeyshuffle[player] and not bk_special else bk_needed - - def exception(d): - return name == 'Skull Woods 2' and d.name == 'Skull Pinball WS' - original_state = extend_reachable_state_improved(entrance_regions, start, proposed_map, all_regions, - valid_doors, bk_flag, world, player, exception) + original_state = extend_reachable_state_lenient(entrance_regions, start, proposed_map, + all_regions, valid_doors, world, player) return original_state def check_valid(name, exploration_state, proposed_map, doors_to_connect, all_regions, - bk_needed, bk_special, paths, entrance_regions, world, player): + paths, entrance_regions, world, player): all_visited = set() all_visited.update(exploration_state.visited_blue) all_visited.update(exploration_state.visited_orange) if len(all_regions.difference(all_visited)) > 0: return False - if not valid_paths(name, paths, entrance_regions, doors_to_connect, all_regions, proposed_map, - bk_needed, bk_special, world, player): + if not valid_paths(name, paths, entrance_regions, doors_to_connect, all_regions, proposed_map, world, player): return False return True @@ -262,8 +249,7 @@ def check_for_special(regions): return False -def valid_paths(name, paths, entrance_regions, valid_doors, all_regions, proposed_map, - bk_needed, bk_special, world, player): +def valid_paths(name, paths, entrance_regions, valid_doors, all_regions, proposed_map, world, player): for path in paths: if type(path) is tuple: target = path[1] @@ -275,14 +261,12 @@ def valid_paths(name, paths, entrance_regions, valid_doors, all_regions, propose else: target = path start_regions = entrance_regions - if not valid_path(name, start_regions, target, valid_doors, proposed_map, all_regions, - bk_needed, bk_special, world, player): + if not valid_path(name, start_regions, target, valid_doors, proposed_map, all_regions, world, player): return False return True -def valid_path(name, starting_regions, target, valid_doors, proposed_map, all_regions, - bk_needed, bk_special, world, player): +def valid_path(name, starting_regions, target, valid_doors, proposed_map, all_regions, world, player): target_regions = set() if type(target) is not list: for region in all_regions: @@ -295,16 +279,11 @@ def valid_path(name, starting_regions, target, valid_doors, proposed_map, all_re target_regions.add(region) start = ExplorationState(dungeon=name) - start.big_key_special = bk_special - bk_flag = False if world.bigkeyshuffle[player] and not bk_special else bk_needed - - def exception(d): - return name == 'Skull Woods 2' and d.name == 'Skull Pinball WS' - original_state = extend_reachable_state_improved(starting_regions, start, proposed_map, all_regions, - valid_doors, bk_flag, world, player, exception) + original_state = extend_reachable_state_lenient(starting_regions, start, proposed_map, all_regions, + valid_doors, world, player) for exp_door in original_state.unattached_doors: - if not exp_door.door.blocked: + if not exp_door.door.blocked or exp_door.door.trapFlag != 0: return True # outstanding connection possible for target in target_regions: if original_state.visited_at_all(target): @@ -637,6 +616,37 @@ class ExplorationState(object): elif not self.in_door_list(door, self.avail_doors): self.append_door_to_list(door, self.avail_doors, flag) + def add_all_doors_check_proposed_2(self, region, proposed_map, valid_doors, flag, world, player): + for door in get_doors(world, region, player): + if door in proposed_map and door.name in valid_doors: + self.visited_doors.add(door) + if self.can_traverse_ignore_traps(door): + if door.controller is not None: + door = door.controller + if door.dest is None and door not in proposed_map.keys() and door.name in valid_doors: + if not self.in_door_list_ic(door, self.unattached_doors): + self.append_door_to_list(door, self.unattached_doors, flag) + else: + other = self.find_door_in_list(door, self.unattached_doors) + if self.crystal != other.crystal: + other.crystal = CrystalBarrier.Either + elif door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door, + self.event_doors): + self.append_door_to_list(door, self.event_doors, flag) + elif not self.in_door_list(door, self.avail_doors): + self.append_door_to_list(door, self.avail_doors, flag) + + def add_all_doors_check_proposed_traps(self, region, proposed_traps, world, player): + for door in get_doors(world, region, player): + if self.can_traverse_ignore_traps(door) and door not in proposed_traps: + if door.controller is not None: + door = door.controller + if door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door, + self.event_doors): + self.append_door_to_list(door, self.event_doors, False) + elif not self.in_door_list(door, self.avail_doors): + self.append_door_to_list(door, self.avail_doors, False) + def add_all_doors_check_key_region(self, region, key_region, world, player): for door in get_doors(world, region, player): if self.can_traverse(door): @@ -693,6 +703,13 @@ class ExplorationState(object): return self.crystal == CrystalBarrier.Either or door.crystal == self.crystal return True + def can_traverse_ignore_traps(self, door): + if door.blocked and door.trapFlag == 0: + return False + if door.crystal not in [CrystalBarrier.Null, CrystalBarrier.Either]: + return self.crystal == CrystalBarrier.Either or door.crystal == self.crystal + return True + def count_locations_exclude_specials(self, world, player): return count_locations_exclude_big_chest(self.found_locations, world, player) @@ -801,6 +818,25 @@ def extend_reachable_state_improved(search_regions, state, proposed_map, all_reg return local_state +def extend_reachable_state_lenient(search_regions, state, proposed_map, all_regions, valid_doors, world, player): + local_state = state.copy() + for region in search_regions: + local_state.visit_region(region) + local_state.add_all_doors_check_proposed_2(region, proposed_map, valid_doors, world, player) + while len(local_state.avail_doors) > 0: + explorable_door = local_state.next_avail_door() + if explorable_door.door in proposed_map: + connect_region = world.get_entrance(proposed_map[explorable_door.door].name, player).parent_region + else: + connect_region = world.get_entrance(explorable_door.door.name, player).connected_region + if connect_region is not None: + if (valid_region_to_explore_in_regions(connect_region, all_regions, world, player) + and not local_state.visited(connect_region)): + local_state.visit_region(connect_region) + local_state.add_all_doors_check_proposed_2(connect_region, proposed_map, valid_doors, world, player) + return local_state + + def special_big_key_found(state): for location in state.found_locations: if location.forced_item and location.forced_item.bigkey: From d9f0e2a7b6e9226f2c2517fc0d51210eed2c6bc0 Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 6 Jul 2022 10:06:29 -0600 Subject: [PATCH 27/63] Options added for door_type_mode and new partitioned mode --- BaseClasses.py | 17 +- CLI.py | 3 +- DoorShuffle.py | 770 +++++++++++++----- DungeonGenerator.py | 26 +- KeyDoorShuffle.py | 50 +- Main.py | 1 + Rom.py | 31 +- RoomData.py | 4 +- data/base2current.bps | Bin 93061 -> 93211 bytes mystery_example.yml | 5 + resources/app/cli/args.json | 11 +- resources/app/cli/lang/en.json | 8 + resources/app/gui/lang/en.json | 7 + .../app/gui/randomize/dungeon/widgets.json | 14 + source/classes/CustomSettings.py | 2 + source/classes/constants.py | 1 + source/dungeon/DungeonStitcher.py | 29 +- source/tools/MysteryUtils.py | 1 + 18 files changed, 722 insertions(+), 258 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 8c7957d8..39286e37 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -28,6 +28,7 @@ class World(object): self.shuffle = shuffle.copy() self.doorShuffle = doorShuffle.copy() self.intensity = {} + self.door_type_mode = {} self.logic = logic.copy() self.mode = mode.copy() self.swords = swords.copy() @@ -143,6 +144,7 @@ class World(object): set_player_attr('colorizepots', False) set_player_attr('pot_pool', {}) set_player_attr('decoupledoors', False) + set_player_attr('door_type_mode', 'original') set_player_attr('shopsanity', False) set_player_attr('mixed_travel', 'prevent') @@ -1872,7 +1874,6 @@ class Sector(object): self.item_logic = set() self.chest_location_set = set() - def region_set(self): if self.r_name_set is None: self.r_name_set = dict.fromkeys(map(lambda r: r.name, self.regions)) @@ -2154,7 +2155,7 @@ class Location(object): def gen_name(self): name = self.name world = self.parent_region.world if self.parent_region and self.parent_region.world else None - if self.parent_region.dungeon and world and world.doorShuffle[self.player] == 'crossed': + if self.parent_region.dungeon and world and world.doorShuffle[self.player] not in ['basic', 'vanilla']: name += f' @ {self.parent_region.dungeon.name}' if world and world.players > 1: name += f' ({world.get_player_names(self.player)})' @@ -2377,6 +2378,8 @@ class Spoiler(object): 'overworld_map': self.world.overworld_map, 'door_shuffle': self.world.doorShuffle, 'intensity': self.world.intensity, + 'door_type_mode': self.world.door_type_mode, + 'decoupledoors': self.world.decoupledoors, 'dungeon_counters': self.world.dungeon_counters, 'item_pool': self.world.difficulty, 'item_functionality': self.world.difficulty_adjustments, @@ -2578,6 +2581,7 @@ class Spoiler(object): outfile.write('Door Shuffle: %s\n' % self.metadata['door_shuffle'][player]) if self.metadata['door_shuffle'][player] != 'vanilla': outfile.write(f"Intensity: {self.metadata['intensity'][player]}\n") + outfile.write(f"Door Type Mode: {self.metadata['door_type_mode'][player]}\n") outfile.write(f"Decouple Doors: {yn(self.metadata['decoupledoors'][player])}\n") outfile.write(f"Experimental: {yn(self.metadata['experimental'][player])}\n") outfile.write(f"Dungeon Counters: {self.metadata['dungeon_counters'][player]}\n") @@ -2815,7 +2819,7 @@ class Pot(object): # byte 0: DDDE EEEE (DR, ER) -dr_mode = {"basic": 1, "crossed": 2, "vanilla": 0} +dr_mode = {"basic": 1, "crossed": 2, "vanilla": 0, "partitioned": 3} er_mode = {"vanilla": 0, "simple": 1, "restricted": 2, "full": 3, "crossed": 4, "insanity": 5, 'lite': 8, 'lean': 9, "dungeonsfull": 7, "dungeonssimple": 6} @@ -2845,7 +2849,8 @@ counter_mode = {"default": 0, "off": 1, "on": 2, "pickup": 3} # byte 6: CCCC CPAA (crystals ganon, pyramid, access access_mode = {"items": 0, "locations": 1, "none": 2} -# byte 7: BSMC ??EE (big, small, maps, compass, bosses, enemies) +# byte 7: BSMC DDEE (big, small, maps, compass, door_type, enemies) +door_type_mode = {'original': 0, 'big': 1, 'all': 2, 'chaos': 3} enemy_mode = {"none": 0, "shuffled": 1, "chaos": 2, "random": 2, "legacy": 3} # byte 8: HHHD DPBS (enemy_health, enemy_dmg, potshuffle, bomb logic, shuffle links) @@ -2898,7 +2903,7 @@ class Settings(object): (0x80 if w.bigkeyshuffle[p] else 0) | (0x40 if w.keyshuffle[p] else 0) | (0x20 if w.mapshuffle[p] else 0) | (0x10 if w.compassshuffle[p] else 0) - | (enemy_mode[w.enemy_shuffle[p]]), + | (door_type_mode[w.door_type_mode[p]] << 2) | (enemy_mode[w.enemy_shuffle[p]]), (e_health[w.enemy_health[p]] << 5) | (e_dmg[w.enemy_damage[p]] << 3) | (0x4 if w.potshuffle[p] else 0) | (0x2 if w.bombbag[p] else 0) | (1 if w.shufflelinks[p] else 0), @@ -2955,7 +2960,7 @@ class Settings(object): args.keyshuffle[p] = True if settings[7] & 0x40 else False args.mapshuffle[p] = True if settings[7] & 0x20 else False args.compassshuffle[p] = True if settings[7] & 0x10 else False - # args.shufflebosses[p] = r(boss_mode)[(settings[7] & 0xc) >> 2] + args.door_type_mode[p] = r(door_type_mode)[(settings[7] & 0xc) >> 2] args.shuffleenemies[p] = r(enemy_mode)[settings[7] & 0x3] args.enemy_health[p] = r(e_health)[(settings[8] & 0xE0) >> 5] diff --git a/CLI.py b/CLI.py index 77b020b4..2e01e4bc 100644 --- a/CLI.py +++ b/CLI.py @@ -125,7 +125,7 @@ def parse_cli(argv, no_defaults=False): 'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep', 'remote_items', 'shopsanity', 'dropshuffle', 'pottery', 'keydropshuffle', 'mixed_travel', 'standardize_palettes', 'code', 'reduce_flashing', 'shuffle_sfx', - 'msu_resume', 'collection_rate', 'colorizepots', 'decoupledoors']: + 'msu_resume', 'collection_rate', 'colorizepots', 'decoupledoors', 'door_type_mode']: value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name) if player == 1: setattr(ret, name, {1: value}) @@ -192,6 +192,7 @@ def parse_settings(): "keysanity": False, "door_shuffle": "basic", "intensity": 2, + 'door_type_mode': 'original', 'decoupledoors': False, "experimental": False, "dungeon_counters": "default", diff --git a/DoorShuffle.py b/DoorShuffle.py index b8c8abbe..42d32c24 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -4,6 +4,7 @@ import logging import time from enum import unique, Flag from typing import DefaultDict, Dict, List +from itertools import chain from BaseClasses import RegionType, Region, Door, DoorType, Direction, Sector, CrystalBarrier, DungeonInfo, dungeon_keys from BaseClasses import PotFlags, LocationType, Direction @@ -18,8 +19,9 @@ from source.dungeon.DungeonStitcher import ExplorationState as ExplorationState2 from DungeonGenerator import ExplorationState, convert_regions, pre_validate, determine_required_paths, drop_entrances from DungeonGenerator import create_dungeon_builders, split_dungeon_builder, simple_dungeon_builder, default_dungeon_entrances from DungeonGenerator import dungeon_portals, dungeon_drops, connect_doors -from DungeonGenerator import valid_region_to_explore as valid_region_to_explore_lim +from DungeonGenerator import valid_region_to_explore from KeyDoorShuffle import analyze_dungeon, build_key_layout, validate_key_layout, determine_prize_lock +from KeyDoorShuffle import validate_bk_layout, check_bk_special from Utils import ncr, kth_combination @@ -87,7 +89,7 @@ def link_doors_prep(world, player): find_inaccessible_regions(world, player) - if world.intensity[player] >= 3 and world.doorShuffle[player] in ['basic', 'crossed']: + if world.intensity[player] >= 3 and world.doorShuffle[player] != 'vanilla': choose_portals(world, player) else: if world.shuffle[player] == 'vanilla': @@ -128,14 +130,21 @@ def link_doors_prep(world, player): def link_doors_main(world, player): + pool = None if world.doorShuffle[player] == 'basic': - within_dungeon(world, player) + pool = [([name], regions) for name, regions in dungeon_regions.items()] + elif world.doorShuffle[player] == 'partitioned': + groups = [['Hyrule Castle', 'Eastern Palace', 'Desert Palace', 'Tower of Hera', 'Agahnims Tower'], + ['Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town'], + ['Ice Palace', 'Misery Mire', 'Turtle Rock', 'Ganons Tower']] + pool = [(group, list(chain.from_iterable([dungeon_regions[d] for d in group]))) for group in groups] elif world.doorShuffle[player] == 'crossed': - cross_dungeon(world, player) + pool = [list(dungeon_regions.keys()), sum(r for r in dungeon_regions.values())] elif world.doorShuffle[player] != 'vanilla': logging.getLogger('').error('Invalid door shuffle setting: %s' % world.doorShuffle[player]) raise Exception('Invalid door shuffle setting: %s' % world.doorShuffle[player]) - + if pool: + main_dungeon_pool(pool, world, player) if world.doorShuffle[player] != 'vanilla': create_door_spoiler(world, player) @@ -395,8 +404,20 @@ def pair_existing_key_doors(world, player, door_a, door_b): def choose_portals(world, player): - if world.doorShuffle[player] in ['basic', 'crossed']: - cross_flag = world.doorShuffle[player] == 'crossed' + if world.doorShuffle[player] != ['vanilla']: + shuffle_flag = world.doorShuffle[player] != 'basic' + allowed = {} + if world.doorShuffle[player] == 'basic': + allowed = {name: {name} for name in dungeon_regions} + elif world.doorShuffle[player] == 'partitioned': + groups = [['Hyrule Castle', 'Eastern Palace', 'Desert Palace', 'Tower of Hera', 'Agahnims Tower'], + ['Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town'], + ['Ice Palace', 'Misery Mire', 'Turtle Rock', 'Ganons Tower']] + allowed = {name: set(group) for group in groups for name in group} + elif world.doorShuffle[player] == 'crossed': + all_dungeons = set(dungeon_regions.keys()) + allowed = {name: all_dungeons for name in dungeon_regions} + # key drops allow the big key in the right place in Desert Tiles 2 bk_shuffle = world.bigkeyshuffle[player] or world.dropshuffle[player] std_flag = world.mode[player] == 'standard' @@ -443,7 +464,7 @@ def choose_portals(world, player): custom = customizer_portals(master_door_list, world, player) - if cross_flag: + if shuffle_flag: random.shuffle(shuffled_info) for dungeon, info in shuffled_info: outstanding_portals = list(dungeon_portals[dungeon]) @@ -457,15 +478,15 @@ 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, custom, need_passage=True, - crossed=cross_flag, bk_shuffle=bk_shuffle, rupee_bow=rupee_bow_flag) + candidates = find_portal_candidates(master_door_list, dungeon, custom, allowed, need_passage=True, + 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, custom, dead_end_allowed=True, - crossed=cross_flag, bk_shuffle=bk_shuffle, rupee_bow=rupee_bow_flag) + candidates = find_portal_candidates(master_door_list, dungeon, custom, allowed, dead_end_allowed=True, + 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, custom, world, player) if choice.deadEnd: @@ -476,7 +497,7 @@ 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, custom, crossed=cross_flag, + candidates = find_portal_candidates(master_door_list, dungeon, custom, allowed, bk_shuffle=bk_shuffle, standard=hc_flag, rupee_bow=rupee_bow_flag) choice, portal = assign_portal(candidates, outstanding_portals, custom, world, player) clean_up_portal_assignment(portal_assignment, dungeon, portal, master_door_list, outstanding_portals) @@ -620,7 +641,7 @@ def disconnect_portal(portal, world, player): chosen_door.entranceFlag = False -def find_portal_candidates(door_list, dungeon, custom, need_passage=False, dead_end_allowed=False, crossed=False, +def find_portal_candidates(door_list, dungeon, custom, allowed, need_passage=False, dead_end_allowed=False, bk_shuffle=False, standard=False, rupee_bow=False): custom_portals, assigned_doors = custom if assigned_doors: @@ -628,10 +649,8 @@ def find_portal_candidates(door_list, dungeon, custom, need_passage=False, dead_ 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: - ret = [x for x in ret if x.entrance.parent_region.dungeon.name == dungeon] + ret = [x for x in ret if not x.dungeonLink or x.dungeonLink == dungeon or x.dungeonLink.startswith('link')] + ret = [x for x in ret if x.entrance.parent_region.dungeon.name in allowed[dungeon]] if need_passage: ret = [x for x in ret if x.passage] if not dead_end_allowed: @@ -789,7 +808,7 @@ def main_dungeon_pool(dungeon_pool, world, player): # todo: which dungeon to create dungeon_builders.update(create_dungeon_builders(sector_pool, connections_tuple, world, player, pool, entrances, splits)) - door_type_pools.append((pool, DoorTypePool(sector_pool, world, player))) + door_type_pools.append((pool, DoorTypePool(pool, world, player))) update_forced_keys(dungeon_builders, entrances_map, world, player) recombinant_builders = {} @@ -816,8 +835,60 @@ def main_dungeon_pool(dungeon_pool, world, player): target_items += 19 # 19 pot keys d_items = target_items - all_dungeon_items_cnt world.pool_adjustment[player] = d_items + # todo: remove unused pairs if not world.decoupledoors[player]: smooth_door_pairs(world, player) + cross_dungeon_clean_up(world, player) + + +def cross_dungeon_clean_up(world, player): + # Re-assign dungeon bosses + gt = world.get_dungeon('Ganons Tower', player) + for name, builder in world.dungeon_layouts[player].items(): + reassign_boss('GT Ice Armos', 'bottom', builder, gt, world, player) + reassign_boss('GT Lanmolas 2', 'middle', builder, gt, world, player) + reassign_boss('GT Moldorm', 'top', builder, gt, world, player) + + sanctuary = world.get_region('Sanctuary', player) + d_name = sanctuary.dungeon.name + if d_name != 'Hyrule Castle': + possible_portals = [] + for portal_name in dungeon_portals[d_name]: + portal = world.get_portal(portal_name, player) + if portal.door.name == 'Sanctuary S': + possible_portals.clear() + possible_portals.append(portal) + break + if not portal.destination and not portal.deadEnd: + possible_portals.append(portal) + if len(possible_portals) == 1: + world.sanc_portal[player] = possible_portals[0] + else: + reachable_portals = [] + for portal in possible_portals: + start_area = portal.door.entrance.parent_region + state = ExplorationState(dungeon=d_name) + state.visit_region(start_area) + state.add_all_doors_check_unattached(start_area, world, player) + explore_state(state, world, player) + if state.visited_at_all(sanctuary): + reachable_portals.append(portal) + world.sanc_portal[player] = random.choice(reachable_portals) + if world.intensity[player] >= 3: + if player in world.sanc_portal: + portal = world.sanc_portal[player] + else: + portal = world.get_portal('Sanctuary', player) + target = portal.door.entrance.parent_region + connect_simple_door(world, 'Sanctuary Mirror Route', target, player) + + check_entrance_fixes(world, player) + + if world.standardize_palettes[player] == 'standardize' and world.doorShuffle[player] != 'basic': + palette_assignment(world, player) + + refine_hints(world.dungeon_layouts[player]) + refine_boss_exits(world, player) def update_forced_keys(dungeon_builders, entrances_map, world, player): @@ -1629,14 +1700,6 @@ def combine_layouts(recombinant_builders, dungeon_builders, entrances_map, world dungeon_builders[recombine.name] = recombine -# todo: this allows cross-dungeon exploring via HC Ledge or Inaccessible Regions -# todo: @deprecated -def valid_region_to_explore(region, world, player): - return region and (region.type == RegionType.Dungeon - or region.name in world.inaccessible_regions[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) @@ -1668,58 +1731,30 @@ def setup_custom_door_types(world, player): class DoorTypePool: - def __init__(self, sectors, world, player): + def __init__(self, pool, world, player): self.smalls = 0 self.bombable = 0 self.dashable = 0 self.bigs = 0 self.traps = 0 - # self.tricky = 0 - # self.hidden = 0 + self.tricky = 0 + self.hidden = 0 # todo: custom pools? - self.count_via_sectors(sectors, world, player) + for dungeon in pool: + counts = door_type_counts[dungeon] + if world.door_type_mode[player] == 'chaos': + counts = self.chaos_shuffle(counts) + self.smalls += counts[0] + self.bigs += counts[1] + self.traps += counts[2] + self.bombable += counts[3] + self.dashable += counts[4] + self.hidden += counts[5] + self.tricky += counts[6] - def count_via_sectors(self, sectors, world, player): - skips = set() - for sector in sectors: - for region in sector.regions: - for ext in region.exits: - if ext.door: - d = ext.door - if d.name not in skips and d.type in [DoorType.Normal, DoorType.Interior]: - if d.smallKey: - self.smalls += 1 - elif d.bigKey: - self.bigs += 1 - elif d.blocked and d.trapFlag and 'Boss' not in d.name and 'Agahnim' not in d.name: - self.traps += 1 - elif d.name == 'TR Compass Room NW': - self.tricky += 1 - elif d.name in ['Skull Vines NW', 'Tower Altar NW']: - self.hidden += 1 - else: - kind = world.get_room(d.roomIndex, player).kind(d) - if kind == DoorKind.Bombable: - self.bombable += 1 - elif kind == DoorKind.Dashable: - self.dashable += 1 - if d.type == DoorType.Interior: - skips.add(d.dest.name) ## lookup a different way for interior door shuffle - elif d.type == DoorType.Normal: - for dp in world.paired_doors[player]: - if d.name == dp.door_a or d.name == dp.door_b: - skips.add(dp.door_b if d.name == dp.door_a else dp.door_a) - break - - def chaos_shuffle(self): + def chaos_shuffle(self, counts): weights = [1, 2, 4, 3, 2, 1] - self.smalls = random.choices(self.get_choices(self.smalls), weights=weights) - self.bombable = random.choices(self.get_choices(self.bombable), weights=weights) - self.dashable = random.choices(self.get_choices(self.dashable), weights=weights) - self.bigs = random.choices(self.get_choices(self.bigs), weights=weights) - self.traps = random.choices(self.get_choices(self.traps), weights=weights) - # self.tricky = random.choices(self.get_choices(self.tricky), weights=weights) - # self.hidden = random.choices(self.get_choices(self.hidden), weights=weights) + return [random.choices(self.get_choices(counts[i]), weights=weights) for i, c in enumerate(counts)] @staticmethod def get_choices(number): @@ -1728,24 +1763,24 @@ class DoorTypePool: class BuilderDoorCandidates: def __init__(self): - self.small = set() - self.big = set() - self.trap = set() - self.bombable = set() - self.dashable = set() - - self.checked = set() + self.small = [] + self.big = [] + self.trap = [] + self.bombable = [] + self.dashable = [] def shuffle_door_types(door_type_pools, paths, world, player): start_regions_map = {} - for name, builder in world.dungeon_layouts[player]: + for name, builder in world.dungeon_layouts[player].items(): start_regions = convert_regions(builder.path_entrances, world, player) start_regions_map[name] = start_regions + builder.candidates = BuilderDoorCandidates() used_doors = shuffle_door_traps(door_type_pools, paths, start_regions_map, world, player) # big keys + used_doors = shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world, player) # small keys - + used_doors = shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, world, player) # bombable / dashable # tricky / hidden @@ -1780,13 +1815,15 @@ def shuffle_door_traps(door_type_pools, paths, start_regions_map, world, player) for dungeon in pool: builder = world.dungeon_layouts[player][dungeon] valid_traps, trap_number = find_valid_trap_combination(builder, suggestion_map[dungeon], - start_regions_map[dungeon], paths, world, player) + start_regions_map[dungeon], paths, world, player, + drop=True) trap_map[dungeon] = valid_traps if trap_number < suggestion_map[dungeon]: flex_map[dungeon] = 0 remaining += suggestion_map[dungeon] - trap_number - suggestion_map[dungeon] = trap_number + suggestion_map[dungeon] = trap_number builder_order = [x for x in pool if flex_map[x] > 0] + random.shuffle(builder_order) queue = deque(builder_order) while len(queue) > 0 and remaining > 0: dungeon = queue.popleft() @@ -1820,15 +1857,15 @@ def shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world, for dungeon in pool: builder = world.dungeon_layouts[player][dungeon] - find_big_key_candidates(builder, used_doors, world, player) + find_big_key_candidates(builder, start_regions_map[dungeon], used_doors, world, player) if custom_bk_doors[dungeon]: - builder.candidates.trap = filter_key_door_pool(builder.candidates.big, custom_bk_doors[dungeon]) + builder.candidates.big = filter_key_door_pool(builder.candidates.big, custom_bk_doors[dungeon]) remaining -= len(custom_bk_doors[dungeon]) ttl += len(builder.candidates.big) for dungeon in pool: builder = world.dungeon_layouts[player][dungeon] proportion = len(builder.candidates.big) - calc = int(round(proportion * door_type_pool.big/ttl)) + calc = int(round(proportion * door_type_pool.bigs/ttl)) suggested = min(proportion, calc) remaining -= suggested suggestion_map[dungeon] = suggested @@ -1836,20 +1873,21 @@ def shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world, for dungeon in pool: builder = world.dungeon_layouts[player][dungeon] valid_doors, bk_number = find_valid_bk_combination(builder, suggestion_map[dungeon], - start_regions_map[dungeon], world, player) + start_regions_map[dungeon], world, player, True) bk_map[dungeon] = valid_doors if bk_number < suggestion_map[dungeon]: flex_map[dungeon] = 0 remaining += suggestion_map[dungeon] - bk_number - suggestion_map[dungeon] = bk_number + suggestion_map[dungeon] = bk_number builder_order = [x for x in pool if flex_map[x] > 0] + random.shuffle(builder_order) queue = deque(builder_order) while len(queue) > 0 and remaining > 0: dungeon = queue.popleft() builder = world.dungeon_layouts[player][dungeon] increased = suggestion_map[dungeon] + 1 valid_doors, bk_number = find_valid_bk_combination(builder, increased, start_regions_map[dungeon], - paths, world, player) + world, player) if valid_doors: bk_map[dungeon] = valid_doors remaining -= 1 @@ -1859,6 +1897,93 @@ def shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world, queue.append(dungeon) # time to re-assign reassign_big_key_doors(bk_map, world, player) + for name, big_list in bk_map.items(): + used_doors.update(flatten_pair_list(big_list)) + return used_doors + + +def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, world, player): + for pool, door_type_pool in door_type_pools: + ttl = 0 + suggestion_map, small_map, flex_map = {}, {}, {} + remaining = door_type_pool.smalls + total_keys = remaining + if player in world.custom_door_types: + custom_key_doors = world.custom_door_types[player]['Key Door'] + else: + custom_key_doors = defaultdict(list) + + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + find_small_key_door_candidates(builder, start_regions_map[dungeon], used_doors, world, player) + if custom_key_doors[dungeon]: + builder.candidates.small = filter_key_door_pool(builder.candidates.small, custom_key_doors[dungeon]) + remaining -= len(custom_key_doors[dungeon]) + builder.key_doors_num = max(0, len(builder.candidates.small) - builder.key_drop_cnt) + total_keys -= builder.key_drop_cnt + ttl += builder.key_doors_num + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + calculated = int(round(builder.key_doors_num*total_keys/ttl)) + max_keys = max(0, builder.location_cnt - calc_used_dungeon_items(builder, world, player)) + cand_len = max(0, len(builder.candidates.small) - builder.key_drop_cnt) + limit = min(max_keys, cand_len) + suggested = min(calculated, limit) + combo_size = ncr(len(builder.candidates.small), suggested + builder.key_drop_cnt) + while combo_size > 500000 and suggested > 0: + suggested -= 1 + combo_size = ncr(len(builder.candidates.small), suggested + builder.key_drop_cnt) + suggestion_map[dungeon] = builder.key_doors_num = suggested + builder.key_drop_cnt + builder.combo_size = combo_size + flex_map[dungeon] = (limit - suggested) if suggested < limit else 0 + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + valid_doors, small_number = find_valid_combination(builder, suggestion_map[dungeon], + start_regions_map[dungeon], world, player) + small_map[dungeon] = valid_doors + actual_chest_keys = small_number - builder.key_drop_cnt + if actual_chest_keys < suggestion_map[dungeon]: + flex_map[dungeon] = 0 + remaining += suggestion_map[dungeon] - actual_chest_keys + suggestion_map[dungeon] = small_number + builder_order = [world.dungeon_layouts[player][x] for x in pool if flex_map[x] > 0] + builder_order.sort(key=lambda b: b.combo_size) + queue = deque(builder_order) + while len(queue) > 0 and remaining > 0: + builder = queue.popleft() + dungeon = builder.name + increased = suggestion_map[dungeon] + 1 + builder.key_doors_num = increased + valid_doors, small_number = find_valid_combination(builder, increased, start_regions_map[dungeon], + world, player) + if valid_doors: + small_map[dungeon] = valid_doors + remaining -= 1 + suggestion_map[dungeon] = increased + flex_map[dungeon] -= 1 + if flex_map[dungeon] > 0: + builder.combo_size = ncr(len(builder.candidates.small), builder.key_doors_num) + queue.append(builder) + queue = deque(sorted(queue, key=lambda b: b.combo_size)) + else: + builder.key_doors_num -= 1 + # time to re-assign + reassign_key_doors(small_map, world, player) + for dungeon_name in pool: + if not world.retro[player]: + builder = world.dungeon_layouts[player][dungeon_name] + log_key_logic(builder.name, world.key_logic[player][builder.name]) + if world.doorShuffle[player] != 'basic': + actual_chest_keys = max(builder.key_doors_num - builder.key_drop_cnt, 0) + dungeon = world.get_dungeon(dungeon_name, player) + if actual_chest_keys == 0: + dungeon.small_keys = [] + else: + dungeon.small_keys = [ItemFactory(dungeon_keys[dungeon_name], player)] * actual_chest_keys + + for name, small_list in small_map.items(): + used_doors.update(flatten_pair_list(small_list)) + return used_doors def shuffle_key_doors(builder, world, player): @@ -1900,9 +2025,9 @@ def find_current_key_doors(builder): def find_trappable_candidates(builder, world, player): - if world.door_type_mode[player] != 'original': # all, chaos + if world.door_type_mode[player] not in ['original', 'big']: # all, chaos r_set = builder.master_sector.region_set() - filtered_doors = [ext.door for r in r_set for ext in r.exits + filtered_doors = [ext.door for r in r_set for ext in world.get_region(r, player).exits if ext.door and ext.door.type in [DoorType.Interior, DoorType.Normal]] for d in filtered_doors: # I only support the first 3 due to the trapFlag right now @@ -1913,14 +2038,14 @@ def find_trappable_candidates(builder, world, player): if (kind in [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable, DoorKind.BigKey] or (d.blocked and d.trapFlag != 0 and 'Boss' not in d.name and 'Agahnim' not in d.name) - or (kind == DoorKind.TrapTriggerable and d.direction in [Direction.North, Direction.East]) - or (kind == DoorKind.Trap2 and d.direction in [Direction.South, Direction.West])): - builder.candidates.trap.add(d) + or (kind == DoorKind.TrapTriggerable and d.direction in [Direction.South, Direction.East]) + or (kind == DoorKind.Trap2 and d.direction in [Direction.North, Direction.West])): + builder.candidates.trap.append(d) elif d.type == DoorType.Normal: if (kind in [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable, DoorKind.BigKey] or (d.blocked and d.trapFlag != 0 and 'Boss' not in d.name and 'Agahnim' not in d.name)): - builder.candidates.trap.add(d) + builder.candidates.trap.append(d) else: r_set = builder.master_sector.region_set() for r in r_set: @@ -1928,7 +2053,7 @@ def find_trappable_candidates(builder, world, player): if ext.door: d = ext.door if d.blocked and d.trapFlag != 0 and 'Boss' not in d.name and 'Agahnim' not in d.name: - builder.candidates.trap.add(d) + builder.candidates.trap.append(d) def find_valid_trap_combination(builder, suggested, start_regions, paths, world, player, drop=True): @@ -1965,6 +2090,7 @@ def find_valid_trap_combination(builder, suggested, start_regions, paths, world, itr = 0 proposal = kth_combination(sample_list[itr], trap_door_pool, trap_doors_needed) proposal.extend(custom_trap_doors) + builder.trap_door_proposal = proposal return proposal, trap_doors_needed @@ -1982,21 +2108,40 @@ def filter_start_regions(builder, start_regions, world, player): def validate_trap_layout(proposal, builder, start_regions, paths, world, player): - return check_required_paths_with_traps(paths, proposal, builder.name, start_regions, world, player) + flag, state = check_required_paths_with_traps(paths, proposal, builder.name, start_regions, world, player) + if not flag: + return False + bk_special_loc = find_bk_special_location(builder, world, player) + if bk_special_loc: + if not state.found_forced_bk(): + return False + if world.accessibility[player] != 'beatable': + all_locations = [l for r in builder.master_sector.region_set() for l in world.get_region(r, player).locations] + if any(l not in state.found_locations for l in all_locations): + return False + return True +def find_bk_special_location(builder, world, player): + for r_name in builder.master_sector.region_set(): + region = world.get_region(r_name, player) + for loc in region.locations: + if loc.forced_big_key(): + return loc + return None + def check_required_paths_with_traps(paths, proposal, dungeon_name, start_regions, world, player): + cached_initial_state = None if len(paths[dungeon_name]) > 0: - states_to_explore = {} common_starts = tuple(start_regions) + states_to_explore = {common_starts: ([], 'all')} for path in paths[dungeon_name]: if type(path) is tuple: states_to_explore[tuple([path[0]])] = (path[1], 'any') else: - if common_starts not in states_to_explore: - states_to_explore[common_starts] = ([], 'all') + # if common_starts not in states_to_explore: + # states_to_explore[common_starts] = ([], 'all') states_to_explore[common_starts][0].append(path) - cached_initial_state = None for start_regs, info in states_to_explore.items(): dest_regs, path_type = info if type(dest_regs) is not list: @@ -2005,12 +2150,14 @@ def check_required_paths_with_traps(paths, proposal, dungeon_name, start_regions start_regions = convert_regions(start_regs, world, player) initial = start_regs == common_starts if not initial or cached_initial_state is None: + if cached_initial_state and any(not cached_initial_state.visited_at_all(r) for r in start_regions): + return False, None # can't start processing the initial state because start regs aren't reachable init = determine_init_crystal(initial, cached_initial_state, start_regions) state = ExplorationState2(init, dungeon_name) for region in start_regions: state.visit_region(region) state.add_all_doors_check_proposed_traps(region, proposal, world, player) - explore_state_proposed_traps(state, world, player) + explore_state_proposed_traps(state, proposal, world, player) if initial and cached_initial_state is None: cached_initial_state = state else: @@ -2020,8 +2167,8 @@ def check_required_paths_with_traps(paths, proposal, dungeon_name, start_regions else: valid, bad_region = check_if_all_regions_visited(state, check_paths) if not valid: - return False - return True + return False, None + return True, cached_initial_state def reassign_trap_doors(trap_map, world, player): @@ -2041,13 +2188,14 @@ def reassign_trap_doors(trap_map, world, player): elif kind in [DoorKind.Trap2, DoorKind.TrapTriggerable]: room.change(d.doorListPos, DoorKind.Normal) d.blocked = False + connect_one_way(world, d.name, d.dest.name, player) elif d.type is DoorType.Normal and d not in traps: world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) d.blocked = False for d in traps: change_door_to_trap(d, world, player) world.spoiler.set_door_type(d.name, 'Trap Door', player) - logger.debug('Key Door: %s', d.name) + logger.debug('Trap Door: %s', d.name) def find_current_trap_doors(builder): @@ -2078,34 +2226,37 @@ def change_door_to_trap(d, world, player): verify_door_list_pos(d, room, world, player, pos=3) d.trapFlag = {0: 0x4, 1: 0x2, 2: 0x1}[d.doorListPos] room.change(d.doorListPos, new_kind) + if d.entrance.connected_region is not None: + d.entrance.connected_region.entrances.remove(d.entrance) + d.entrance.connected_region = None elif d.type is DoorType.Normal: d.blocked = True verify_door_list_pos(d, room, world, player, pos=3) d.trapFlag = {0: 0x4, 1: 0x2, 2: 0x1}[d.doorListPos] room.change(d.doorListPos, DoorKind.Trap) + if d.entrance.connected_region is not None: + d.entrance.connected_region.entrances.remove(d.entrance) + d.entrance.connected_region = None -def find_big_key_candidates(builder, used, world, player): - if world.door_type_mode[player] != 'original': # all, chaos - r_set = builder.master_sector.region_set() - filtered_doors = [ext.door for r in r_set for ext in r.exits - if ext.door and ext.door.type in [DoorType.Interior, DoorType.Normal]] - for d in filtered_doors: - if 0 <= d.doorListPos < 4 and not d.entranceFlag: - room = world.get_room(d.roomIndex, player) - kind = room.kind(d) - if d.type in [DoorType.Interior, DoorType.Normal]: - if (kind in [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable, - DoorKind.BigKey] and d not in used) - or (d.blocked and d.trapFlag != 0 and 'Boss' not in d.name and 'Agahnim' not in d.name) - or (kind == DoorKind.TrapTriggerable and d.direction in [Direction.North, Direction.East]) - or (kind == DoorKind.Trap2 and d.direction in [Direction.South, Direction.West])): - builder.candidates.big.add(d) - elif d.type == DoorType.Normal: - if (kind in [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable, - DoorKind.BigKey] - or (d.blocked and d.trapFlag != 0 and 'Boss' not in d.name and 'Agahnim' not in d.name)): - builder.candidates.trap.add(d) +def find_big_key_candidates(builder, start_regions, used, world, player): + if world.door_type_mode[player] != 'original': # big, all, chaos + # traverse dungeon and find candidates + candidates = [] + checked_doors = set() + for region in start_regions: + possible, checked = find_big_key_door_candidates(region, checked_doors, used, world, player) + candidates.extend([x for x in possible if x not in candidates]) + checked_doors.update(checked) + flat_candidates = [] + for candidate in candidates: + # not valid if: Normal Coupled and Pair in is Checked and Pair is not in Candidates + if (world.decoupledoors[player] or candidate.type != DoorType.Normal + or candidate.dest not in checked_doors or candidate.dest in candidates): + flat_candidates.append(candidate) + + paired_candidates = build_pair_list(flat_candidates) + builder.candidates.big = paired_candidates else: r_set = builder.master_sector.region_set() for r in r_set: @@ -2113,15 +2264,177 @@ def find_big_key_candidates(builder, used, world, player): if ext.door: d = ext.door if d.bigKey and d.type in [DoorType.Normal, DoorType.Interior]: - builder.candidates.big.add(d) + builder.candidates.big.append(d) -def find_small_key_door_candidates(builder, start_regions, world, player): +def find_big_key_door_candidates(region, checked, used, world, player): + decoupled = world.decoupledoors[player] + dungeon_name = region.dungeon.name + candidates = [] + checked_doors = list(checked) + queue = deque([(region, None, None)]) + while len(queue) > 0: + current, last_door, last_region = queue.pop() + for ext in current.exits: + d = ext.door + controlled = d + if d and d.controller: + d = d.controller + if (d and not d.blocked and d.dest is not last_door and d.dest is not last_region + and d not in checked_doors and d not in used): + valid = False + if (0 <= d.doorListPos < 4 and d.type in [DoorType.Interior, DoorType.Normal] + and not d.entranceFlag and d.direction in [Direction.North, Direction.South]): + room = world.get_room(d.roomIndex, player) + position, kind = room.doorList[d.doorListPos] + if d.type == DoorType.Interior: + valid = kind in okay_interiors + if valid and d.dest not in candidates: # interior doors are not separable yet + candidates.append(d.dest) + elif d.type == DoorType.Normal: + if decoupled: + valid = kind in okay_normals + else: + d2 = d.dest + if d2 not in candidates and d2 not in used: + if d2.type == DoorType.Normal: + room_b = world.get_room(d2.roomIndex, player) + pos_b, kind_b = room_b.doorList[d2.doorListPos] + valid = kind in okay_normals and kind_b in okay_normals and valid_key_door_pair(d, d2) + else: + valid = kind in okay_normals + if valid and 0 <= d2.doorListPos < 4: + candidates.append(d2) + else: + valid = True + if valid and d not in candidates: + candidates.append(d) + connected = ext.connected_region + if valid_region_to_explore(connected, dungeon_name, world, player): + queue.append((ext.connected_region, controlled, current)) + if d is not None: + checked_doors.append(d) + return candidates, checked_doors + + +def find_valid_bk_combination(builder, suggested, start_regions, world, player, drop=True): + bk_door_pool = builder.candidates.big + bk_doors_needed = suggested + if player in world.custom_door_types: + custom_bk_doors = world.custom_door_types[player]['Big Key Door'][builder.name] + else: + custom_bk_doors = [] + if custom_bk_doors: + bk_door_pool = filter_key_door_pool(bk_door_pool, custom_bk_doors) + bk_doors_needed -= len(custom_bk_doors) + if len(bk_door_pool) < bk_doors_needed: + if not drop: + return None, 0 + bk_doors_needed = len(bk_door_pool) + combinations = ncr(len(bk_door_pool), bk_doors_needed) + itr = 0 + sample_list = build_sample_list(combinations, 10000) + proposal = kth_combination(sample_list[itr], bk_door_pool, bk_doors_needed) + proposal.extend(custom_bk_doors) + + start_regions = filter_start_regions(builder, start_regions, world, player) + while not validate_bk_layout(proposal, builder, start_regions, world, player): + itr += 1 + if itr >= len(sample_list): + if not drop: + return None, 0 + bk_doors_needed -= 1 + if bk_doors_needed < 0: + raise Exception(f'Bad dungeon {builder.name} - maybe custom bk doors are bad') + combinations = ncr(len(bk_door_pool), bk_doors_needed) + sample_list = build_sample_list(combinations, 10000) + itr = 0 + proposal = kth_combination(sample_list[itr], bk_door_pool, bk_doors_needed) + proposal.extend(custom_bk_doors) + builder.bk_door_proposal = proposal + return proposal, bk_doors_needed + + +def find_current_bk_doors(builder): + current_doors = [] + for region in builder.master_sector.regions: + for ext in region.exits: + d = ext.door + if d and d.type != DoorType.Logical and d.bigKey: + current_doors.append(d) + return current_doors + + +def reassign_big_key_doors(bk_map, world, player): + for name, big_doors in bk_map.items(): + flat_proposal = flatten_pair_list(big_doors) + builder = world.dungeon_layouts[player][name] + queue = deque(find_current_bk_doors(builder)) + while len(queue) > 0: + d = queue.pop() + if d.type is DoorType.Interior and d not in flat_proposal and d.dest not in flat_proposal: + if not d.entranceFlag: + world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) + d.bigKey = False + elif d.type is DoorType.Normal and d not in flat_proposal: + if not d.entranceFlag: + world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) + d.bigKey = False + for obj in big_doors: + if type(obj) is tuple: + d1 = obj[0] + d2 = obj[1] + if d1.type is DoorType.Interior: + change_door_to_big_key(d1, world, player) + d2.bigKey = True # ensure flag is set + else: + names = [d1.name, d2.name] + found = False + for dp in world.paired_doors[player]: + if dp.door_a in names and dp.door_b in names: + dp.pair = True + found = True + elif dp.door_a in names: + dp.pair = False + elif dp.door_b in names: + dp.pair = False + if not found: + world.paired_doors[player].append(PairedDoor(d1.name, d2.name)) + change_door_to_big_key(d1, world, player) + change_door_to_big_key(d2, world, player) + world.spoiler.set_door_type(d1.name+' <-> '+d2.name, 'Big Key Door', player) + else: + d = obj + if d.type is DoorType.Interior: + change_door_to_big_key(d, world, player) + d.dest.bigKey = True # ensure flag is set + elif d.type is DoorType.SpiralStairs: + pass # we don't have spiral stairs candidates yet that aren't already key doors + elif d.type is DoorType.Normal: + change_door_to_big_key(d, world, player) + if not world.decoupledoors[player] and d.dest: + if d.dest.type in [DoorType.Normal]: + dest_room = world.get_room(d.dest.roomIndex, player) + if stateful_door(d.dest, dest_room.kind(d.dest)): + change_door_to_big_key(d.dest, world, player) + add_pair(d, d.dest, world, player) + world.spoiler.set_door_type(d.name, 'Big Key Door', player) + + +def change_door_to_big_key(d, world, player): + d.bigKey = True + room = world.get_room(d.roomIndex, player) + if room.doorList[d.doorListPos][1] != DoorKind.BigKey: + verify_door_list_pos(d, room, world, player) + room.change(d.doorListPos, DoorKind.BigKey) + + +def find_small_key_door_candidates(builder, start_regions, used, world, player): # traverse dungeon and find candidates candidates = [] checked_doors = set() for region in start_regions: - possible, checked = find_key_door_candidates(region, checked_doors, world, player) + possible, checked = find_key_door_candidates(region, checked_doors, used, world, player) candidates.extend([x for x in possible if x not in candidates]) checked_doors.update(checked) flat_candidates = [] @@ -2132,25 +2445,26 @@ def find_small_key_door_candidates(builder, start_regions, world, player): flat_candidates.append(candidate) paired_candidates = build_pair_list(flat_candidates) - builder.candidates = paired_candidates + builder.candidates.small = paired_candidates def calc_used_dungeon_items(builder, world, player): base = 2 + basic_flag = world.doorShuffle[player] == 'basic' if not world.bigkeyshuffle[player]: if builder.bk_required and not builder.bk_provided: base += 1 - if not world.compassshuffle[player]: + if not world.compassshuffle[player] and (builder.name not in ['Hyrule Castle', 'Agahnims Tower'] or not basic_flag): base += 1 - if not world.mapshuffle[player]: + if not world.mapshuffle[player] and (builder.name != 'Agahnims Tower' or not basic_flag): base += 1 return base -def find_valid_combination(builder, start_regions, world, player, drop_keys=True): +def find_valid_combination(builder, target, 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 + key_door_pool = list(builder.candidates.small) + key_doors_needed = target if player in world.custom_door_types: custom_key_doors = world.custom_door_types[player]['Key Door'][builder.name] else: @@ -2163,8 +2477,9 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True 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 + return None, 0 builder.key_doors_num -= key_doors_needed - len(key_door_pool) # reduce number of key doors + key_doors_needed = len(key_door_pool) 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 @@ -2181,12 +2496,12 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True if itr >= len(sample_list): if not drop_keys: logger.info('No valid layouts for %s with %s doors', builder.name, builder.key_doors_num) - return False + return None, 0 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 - less than 0 key doors not valid' % builder.name) + if key_doors_needed < 0: + raise Exception(f'Bad dungeon {builder.name} - less than 0 key doors or invalid custom key door') combinations = ncr(len(key_door_pool), max(0, key_doors_needed)) sample_list = build_sample_list(combinations) itr = 0 @@ -2200,14 +2515,15 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True # make changes if player not in world.key_logic.keys(): world.key_logic[player] = {} + builder.total_keys = builder.key_doors_num analyze_dungeon(key_layout, world, player) builder.key_door_proposal = proposal world.key_logic[player][builder.name] = key_layout.key_logic world.key_layout[player][builder.name] = key_layout - return True + return builder.key_door_proposal, key_doors_needed -def build_sample_list(combinations, max_combinations=1000000): +def build_sample_list(combinations, max_combinations=100000): if combinations <= max_combinations: sample_list = list(range(0, int(combinations))) else: @@ -2273,10 +2589,13 @@ def flatten_pair_list(paired_list): return flat_list -okay_normals = [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable, DoorKind.DungeonChanger] +okay_normals = [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable, + DoorKind.DungeonChanger, DoorKind.BigKey] + +okay_interiors = [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable, DoorKind.BigKey] -def find_key_door_candidates(region, checked, world, player): +def find_key_door_candidates(region, checked, used, world, player): decoupled = world.decoupledoors[player] dungeon_name = region.dungeon.name candidates = [] @@ -2289,14 +2608,15 @@ def find_key_door_candidates(region, checked, world, player): controlled = d if d and d.controller: d = d.controller - if d and not d.blocked and d.dest is not last_door and d.dest is not last_region and d not in checked_doors: + if (d and not d.blocked and d.dest is not last_door and d.dest is not last_region + and d not in checked_doors and d not in used): valid = False if (0 <= d.doorListPos < 4 and d.type in [DoorType.Interior, DoorType.Normal, DoorType.SpiralStairs] and not d.entranceFlag): room = world.get_room(d.roomIndex, player) position, kind = room.doorList[d.doorListPos] if d.type == DoorType.Interior: - valid = kind in [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable] + valid = kind in okay_interiors if valid and d.dest not in candidates: # interior doors are not separable yet candidates.append(d.dest) elif d.type == DoorType.SpiralStairs: @@ -2306,7 +2626,7 @@ def find_key_door_candidates(region, checked, world, player): valid = kind in okay_normals else: d2 = d.dest - if d2 not in candidates: + if d2 not in candidates and d2 not in used: if d2.type == DoorType.Normal: room_b = world.get_room(d2.roomIndex, player) pos_b, kind_b = room_b.doorList[d2.doorListPos] @@ -2320,7 +2640,7 @@ def find_key_door_candidates(region, checked, world, player): if valid and d not in candidates: candidates.append(d) connected = ext.connected_region - if valid_region_to_explore_lim(connected, dungeon_name, world, player): + if valid_region_to_explore(connected, dungeon_name, world, player): queue.append((ext.connected_region, controlled, current)) if d is not None: checked_doors.append(d) @@ -2333,72 +2653,80 @@ def valid_key_door_pair(door1, door2): return len(door1.entrance.parent_region.exits) <= 1 or len(door2.entrance.parent_region.exits) <= 1 -def reassign_key_doors(builder, world, player): +def reassign_key_doors(small_map, world, player): logger = logging.getLogger('') - logger.debug('Key doors for %s', builder.name) - proposal = builder.key_door_proposal - flat_proposal = flatten_pair_list(proposal) - queue = deque(find_current_key_doors(builder)) - while len(queue) > 0: - d = queue.pop() - if d.type is DoorType.SpiralStairs and d not in proposal: - room = world.get_room(d.roomIndex, player) - if room.doorList[d.doorListPos][1] == DoorKind.StairKeyLow: - room.delete(d.doorListPos) - else: - if len(room.doorList) > 1: - room.mirror(d.doorListPos) # I think this works for crossed now - else: + for name, small_doors in small_map.items(): + logger.debug(f'Key doors for {name}') + builder = world.dungeon_layouts[player][name] + proposal = builder.key_door_proposal + flat_proposal = flatten_pair_list(proposal) + queue = deque(find_current_key_doors(builder)) + while len(queue) > 0: + d = queue.pop() + if d.type is DoorType.SpiralStairs and d not in proposal: + room = world.get_room(d.roomIndex, player) + if room.doorList[d.doorListPos][1] == DoorKind.StairKeyLow: room.delete(d.doorListPos) - d.smallKey = False - elif d.type is DoorType.Interior and d not in flat_proposal and d.dest not in flat_proposal: - if not d.entranceFlag: - world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) - d.smallKey = False - d.dest.smallKey = False - queue.remove(d.dest) - elif d.type is DoorType.Normal and d not in flat_proposal: - if not d.entranceFlag: - world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) - d.smallKey = False - for dp in world.paired_doors[player]: - if dp.door_a == d.name or dp.door_b == d.name: - dp.pair = False - for obj in proposal: - if type(obj) is tuple: - d1 = obj[0] - d2 = obj[1] - if d1.type is DoorType.Interior: - change_door_to_small_key(d1, world, player) - d2.smallKey = True # ensure flag is set - else: - names = [d1.name, d2.name] - found = False + else: + if len(room.doorList) > 1: + room.mirror(d.doorListPos) # I think this works for crossed now + else: + room.delete(d.doorListPos) + d.smallKey = False + elif d.type is DoorType.Interior and d not in flat_proposal and d.dest not in flat_proposal: + if not d.entranceFlag: + world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) + d.smallKey = False + d.dest.smallKey = False + queue.remove(d.dest) + elif d.type is DoorType.Normal and d not in flat_proposal: + if not d.entranceFlag: + world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) + d.smallKey = False for dp in world.paired_doors[player]: - if dp.door_a in names and dp.door_b in names: - dp.pair = True - found = True - elif dp.door_a in names: + if dp.door_a == d.name or dp.door_b == d.name: dp.pair = False - elif dp.door_b in names: - dp.pair = False - if not found: - world.paired_doors[player].append(PairedDoor(d1.name, d2.name)) - change_door_to_small_key(d1, world, player) - change_door_to_small_key(d2, world, player) - world.spoiler.set_door_type(d1.name+' <-> '+d2.name, 'Key Door', player) - logger.debug('Key Door: %s', d1.name+' <-> '+d2.name) - else: - d = obj - if d.type is DoorType.Interior: - change_door_to_small_key(d, world, player) - d.dest.smallKey = True # ensure flag is set - elif d.type is DoorType.SpiralStairs: - pass # we don't have spiral stairs candidates yet that aren't already key doors - elif d.type is DoorType.Normal: - change_door_to_small_key(d, world, player) - world.spoiler.set_door_type(d.name, 'Key Door', player) - logger.debug('Key Door: %s', d.name) + for obj in proposal: + if type(obj) is tuple: + d1 = obj[0] + d2 = obj[1] + if d1.type is DoorType.Interior: + change_door_to_small_key(d1, world, player) + d2.smallKey = True # ensure flag is set + else: + names = [d1.name, d2.name] + found = False + for dp in world.paired_doors[player]: + if dp.door_a in names and dp.door_b in names: + dp.pair = True + found = True + elif dp.door_a in names: + dp.pair = False + elif dp.door_b in names: + dp.pair = False + if not found: + world.paired_doors[player].append(PairedDoor(d1.name, d2.name)) + change_door_to_small_key(d1, world, player) + change_door_to_small_key(d2, world, player) + world.spoiler.set_door_type(d1.name+' <-> '+d2.name, 'Key Door', player) + logger.debug('Key Door: %s', d1.name+' <-> '+d2.name) + else: + d = obj + if d.type is DoorType.Interior: + change_door_to_small_key(d, world, player) + d.dest.smallKey = True # ensure flag is set + elif d.type is DoorType.SpiralStairs: + pass # we don't have spiral stairs candidates yet that aren't already key doors + elif d.type is DoorType.Normal: + change_door_to_small_key(d, world, player) + if not world.decoupledoors[player] and d.dest: + if d.dest.type in [DoorType.Normal]: + dest_room = world.get_room(d.dest.roomIndex, player) + if stateful_door(d.dest, dest_room.kind(d.dest)): + change_door_to_small_key(d.dest, world, player) + add_pair(d, d.dest, world, player) + world.spoiler.set_door_type(d.name, 'Key Door', player) + logger.debug('Key Door: %s', d.name) def change_door_to_small_key(d, world, player): @@ -2499,7 +2827,7 @@ def remove_pair(door, world, player): def stateful_door(door, kind): if 0 <= door.doorListPos < 4: - return kind in [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable] #, DoorKind.BigKey] + return kind in [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable, DoorKind.BigKey] return False @@ -2780,7 +3108,8 @@ def explore_state(state, world, player): while len(state.avail_doors) > 0: door = state.next_avail_door().door connect_region = world.get_entrance(door.name, player).connected_region - if state.can_traverse(door) and not state.visited(connect_region) and valid_region_to_explore(connect_region, world, player): + if (state.can_traverse(door) and not state.visited(connect_region) + and valid_region_to_explore(connect_region, state.dungeon, world, player)): state.visit_region(connect_region) state.add_all_doors_check_unattached(connect_region, world, player) @@ -2789,7 +3118,8 @@ def explore_state_proposed_traps(state, proposed_traps, world, player): while len(state.avail_doors) > 0: door = state.next_avail_door().door connect_region = world.get_entrance(door.name, player).connected_region - if not state.visited(connect_region) and valid_region_to_explore(connect_region, world, player): + if (not state.visited(connect_region) + and valid_region_to_explore(connect_region, state.dungeon, world, player)): state.visit_region(connect_region) state.add_all_doors_check_proposed_traps(connect_region, proposed_traps, world, player) @@ -2851,6 +3181,7 @@ class DROptions(Flag): # Open_Desert_Wall = 0x80 # No longer pre-opening desert wall - unused Hide_Total = 0x100 DarkWorld_Spawns = 0x200 + BigKeyDoor_Shuffle = 0x400 # DATA GOES DOWN HERE @@ -3913,4 +4244,21 @@ bomb_dash_counts = { 'Ganons Tower': (2, 1) } +# small, big, trap, bomb, dash, hidden, tricky +door_type_counts = { + 'Hyrule Castle': (4, 0, 1, 0, 2, 0, 0), + 'Eastern Palace': (2, 2, 0, 0, 0, 0, 0), + 'Desert Palace': (4, 1, 0, 0, 0, 0, 0), + 'Agahnims Tower': (4, 0, 1, 0, 0, 1, 0), + 'Swamp Palace': (6, 0, 0, 2, 0, 0, 0), + 'Palace of Darkness': (6, 1, 1, 3, 2, 0, 0), + 'Misery Mire': (6, 3, 5, 2, 0, 0, 0), + 'Skull Woods': (5, 0, 2, 2, 0, 1, 0), + 'Ice Palace': (6, 1, 3, 0, 0, 0, 0), + 'Tower of Hera': (1, 1, 0, 0, 0, 0, 0), + 'Thieves Town': (3, 1, 2, 1, 1, 0, 0), + 'Turtle Rock': (6, 2, 2, 0, 2, 0, 1), # 2 bombs kind of for entrances, but I put 0 here + 'Ganons Tower': (8, 2, 5, 2, 1, 0, 0) +} + diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 7e3b2ef7..f06c3535 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -808,6 +808,7 @@ class ExplorationState(object): self.prize_door_set = {} self.prize_doors = [] self.prize_doors_opened = False + self.prize_received = False def copy(self): ret = ExplorationState(dungeon=self.dungeon) @@ -839,6 +840,7 @@ class ExplorationState(object): ret.prize_door_set = dict(self.prize_door_set) ret.prize_doors = list(self.prize_doors) ret.prize_doors_opened = self.prize_doors_opened + ret.prize_received = self.prize_received return ret def next_avail_door(self): @@ -984,6 +986,20 @@ class ExplorationState(object): elif not self.in_door_list(door, self.avail_doors): self.append_door_to_list(door, self.avail_doors) + def add_all_doors_check_big_keys(self, region, big_key_door_proposal, world, player): + for door in get_doors(world, region, player): + if self.can_traverse(door): + if door.controller: + door = door.controller + if (door in big_key_door_proposal or door.name in special_big_key_doors) and not self.big_key_opened: + if not self.in_door_list(door, self.big_doors): + self.append_door_to_list(door, self.big_doors) + elif door.req_event is not None and door.req_event not in self.events: + if not self.in_door_list(door, self.event_doors): + self.append_door_to_list(door, self.event_doors) + elif not self.in_door_list(door, self.avail_doors): + self.append_door_to_list(door, self.avail_doors) + def visited(self, region): if self.crystal == CrystalBarrier.Either: return region in self.visited_blue and region in self.visited_orange @@ -1213,6 +1229,8 @@ class DungeonBuilder(object): self.combo_size = None self.flex = 0 self.key_door_proposal = None + self.bk_door_proposal = None + self.trap_door_proposal = None self.allowance = None if 'Stonewall' in name: @@ -1279,9 +1297,7 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, dunge dungeon_map = {} for key in dungeon_pool: - dungeon_map[key] = DungeonBuilder(key) - for key in dungeon_boss_sectors.keys(): - current_dungeon = dungeon_map[key] + current_dungeon = dungeon_map[key] = DungeonBuilder(key) for r_name in dungeon_boss_sectors[key]: assign_sector(find_sector(r_name, candidate_sectors), current_dungeon, candidate_sectors, global_pole) if key == 'Hyrule Castle' and world.mode[player] == 'standard': @@ -1293,7 +1309,7 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, dunge candidate_sectors, global_pole) entrances_map, potentials, connections = connections_tuple accessible_sectors, reverse_d_map = set(), {} - for key in dungeon_entrances.keys(): + for key in dungeon_pool: current_dungeon = dungeon_map[key] current_dungeon.all_entrances = dungeon_entrances[key] for r_name in current_dungeon.all_entrances: @@ -1419,6 +1435,8 @@ def identify_destination_sectors(accessible_sectors, reverse_d_map, dungeon_map, if ent_name in found_connections: continue sector = find_sector(ent_name, reverse_d_map.keys()) + if sector is None: + continue if sector in accessible_sectors: found_connections.add(ent_name) accessible_overworld.add(region) # todo: drops don't give ow access diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index bb4f6835..8a70abcf 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -248,7 +248,7 @@ def find_all_locations(sector): def calc_max_chests(builder, key_layout, world, player): - if world.doorShuffle[player] != 'crossed': + if world.doorShuffle[player] in ['basic', 'vanilla']: return len(world.get_dungeon(key_layout.sector.name, player).small_keys) return max(0, builder.key_doors_num - key_layout.max_drops) @@ -1169,6 +1169,16 @@ def expand_key_state(state, flat_proposal, world, player): state.add_all_doors_check_keys(connect_region, flat_proposal, world, player) +def expand_big_key_state(state, flat_proposal, world, player): + while len(state.avail_doors) > 0: + exp_door = state.next_avail_door() + door = exp_door.door + connect_region = world.get_entrance(door.name, player).connected_region + if state.validate(door, connect_region, world, player): + state.visit_region(connect_region, key_checks=True) + state.add_all_doors_check_big_keys(connect_region, flat_proposal, world, player) + + def flatten_pair_list(paired_list): flat_list = [] for d in paired_list: @@ -1398,6 +1408,42 @@ def prize_relevance(key_layout, dungeon_entrance): return None +def prize_relevance_sig2(start_regions, d_name, dungeon_entrance): + if len(start_regions) > 1 and dungeon_entrance and dungeon_table[d_name].prize: + if dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower']: + return 'GT' + elif dungeon_entrance.name == 'Pyramid Fairy': + return 'BigBomb' + return None + + +def validate_bk_layout(proposal, builder, start_regions, world, player): + bk_special = check_bk_special(builder.master_sector.regions, world, player) + if world.bigkeyshuffle[player] and (world.dropshuffle[player] or not bk_special): + return True + flat_proposal = flatten_pair_list(proposal) + state = ExplorationState(dungeon=builder.name) + state.big_key_special = bk_special + for region in start_regions: + dungeon_entrance, portal_door = find_outside_connection(region) + prize_relevant_flag = prize_relevance_sig2(start_regions, builder.name, dungeon_entrance) + if prize_relevant_flag: + state.append_door_to_list(portal_door, state.prize_doors) + state.prize_door_set[portal_door] = dungeon_entrance + # key_layout.prize_relevant = prize_relevant_flag + else: + state.visit_region(region, key_checks=True) + state.add_all_doors_check_big_keys(region, flat_proposal, world, player) + expand_big_key_state(state, flat_proposal, world, player) + if bk_special: + for loc in state.found_locations: + if loc.forced_big_key(): + return True + else: + return len(state.bk_found) > 0 + return False + + # Soft lock stuff def validate_key_layout(key_layout, world, player): # retro is all good - except for hyrule castle in standard mode @@ -1601,7 +1647,7 @@ def create_key_counters(key_layout, world, player): state.key_locations = len(builder.key_door_proposal) - builder.key_drop_cnt else: builder = world.dungeon_layouts[player][key_layout.sector.name] - state.key_locations = builder.total_keys - builder.key_drop_cnt + state.key_locations = max(0, builder.total_keys - builder.key_drop_cnt) state.big_key_special = False for region in key_layout.sector.regions: for location in region.locations: diff --git a/Main.py b/Main.py index 80c79887..4e6b9c4b 100644 --- a/Main.py +++ b/Main.py @@ -106,6 +106,7 @@ def main(args, seed=None, fish=None): world.enemy_damage = args.enemy_damage.copy() world.beemizer = args.beemizer.copy() world.intensity = {player: random.randint(1, 3) if args.intensity[player] == 'random' else int(args.intensity[player]) for player in range(1, world.players + 1)} + world.door_type_mode = args.door_type_mode.copy() world.experimental = args.experimental.copy() world.dungeon_counters = args.dungeon_counters.copy() world.fish = fish diff --git a/Rom.py b/Rom.py index 4cb0354c..d6717617 100644 --- a/Rom.py +++ b/Rom.py @@ -37,7 +37,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'add982e935888df04ddfa570bc07bede' +RANDOMIZERBASEHASH = 'afcd895b87559cd29b04aa3714cbc929' class JsonRom(object): @@ -738,11 +738,11 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): # setup dr option flags based on experimental, etc. dr_flags = DROptions.Eternal_Mini_Bosses if world.doorShuffle[player] == 'vanilla' else DROptions.Town_Portal - if world.doorShuffle[player] == 'crossed': + if world.doorShuffle[player] not in ['vanilla', 'basic']: dr_flags |= DROptions.Map_Info 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'\ + if world.doorShuffle[player] not in ['vanilla', 'basic'] and world.logic[player] != 'nologic'\ and world.mixed_travel[player] == 'prevent': # PoD Falling Bridge or Hammjump # 1FA607: db $2D, $79, $69 ; 0x0069: Vertical Rail ↕ | { 0B, 1E } | Size: 05 @@ -763,6 +763,8 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): dr_flags |= DROptions.DarkWorld_Spawns if world.logic[player] != 'nologic': dr_flags |= DROptions.Fix_EG + if world.door_type_mode in ['big', 'chaos']: + dr_flags |= DROptions.BigKeyDoor_Shuffle my_locations = world.get_filled_locations(player) valid_locations = [l for l in my_locations if ((l.type == LocationType.Pot and not l.forced_item) @@ -772,7 +774,8 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): valid_loc_by_dungeon = valid_dungeon_locations(valid_locations) # fix hc big key problems (map and compass too) - if world.doorShuffle[player] == 'crossed' or world.dropshuffle[player] or world.pottery[player] not in ['none', 'cave']: + if (world.doorShuffle[player] not in ['vanilla', 'basic'] or world.dropshuffle[player] + or world.pottery[player] not in ['none', 'cave']): rom.write_byte(0x151f1, 2) rom.write_byte(0x15270, 2) sanctuary = world.get_region('Sanctuary', player) @@ -800,7 +803,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_bytes(0x13fff4, [0xe4, 0x00]) # patch doors - if world.doorShuffle[player] == 'crossed': + if world.doorShuffle[player] not in ['vanilla', 'basic']: rom.write_byte(0x138002, 2) for name, layout in world.key_layout[player].items(): offset = compass_data[name][4]//2 @@ -1377,7 +1380,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): if len(portal_list) == 1: portal_idx = 0 else: - if world.doorShuffle[player] == 'crossed': + if world.doorShuffle[player] not in ['vanilla', 'basic']: # the random choice excludes sanctuary portal_idx = next((i for i, elem in enumerate(portal_list) if world.get_portal(elem, player).chosen), random.choice([1, 2, 3])) @@ -1392,7 +1395,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_bytes(0x53E56+ow_map_index*2, int16_as_bytes(coords[1])) rom.write_byte(0x53EA6+ow_map_index, world_indicator) # in crossed doors - flip the compass exists flags - if world.doorShuffle[player] == 'crossed': + if world.doorShuffle[player] not in ['vanilla', 'basic']: for dungeon, portal_list in dungeon_portals.items(): ow_map_index = dungeon_table[dungeon].map_index exists_flag = any(x for x in world.get_dungeon(dungeon, player).dungeon_items if x.type == 'Compass') @@ -1495,7 +1498,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_bytes(0x180188, [0x20, 0, 0]) # Zelda respawn refills (magic, bombs, arrows) rom.write_bytes(0x18018B, [0x20, 0, 0]) # Mantle respawn refills (magic, bombs, arrows) magic_max, magic_small = 0x80, 0x20 - if world.doorShuffle[player] == 'crossed': + if world.doorShuffle[player] not in ['vanilla', 'basic']: # Uncle respawn refills (magic, bombs, arrows) rom.write_bytes(0x180185, [max(0x20, magic_max), max(3, bomb_max), max(10, bow_max)]) rom.write_bytes(0x180188, [0x20, 3, 10]) # Zelda respawn refills (magic, bombs, arrows) @@ -2081,13 +2084,13 @@ def write_strings(rom, world, player, team): # Next we write a few hints for specific inconvenient locations. We don't make many because in entrance this is highly unpredictable. locations_to_hint = InconvenientLocations.copy() - if world.doorShuffle[player] != 'crossed': + if world.doorShuffle[player] == 'vanilla': locations_to_hint.extend(InconvenientDungeonLocations) if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']: locations_to_hint.extend(InconvenientVanillaLocations) random.shuffle(locations_to_hint) hint_count = 3 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull'] else 5 - hint_count -= 2 if world.doorShuffle[player] == 'crossed' else 0 + hint_count -= 2 if world.doorShuffle[player] not in ['vanilla', 'basic'] else 0 del locations_to_hint[hint_count:] for location in locations_to_hint: if location == 'Swamp Left': @@ -2150,7 +2153,7 @@ def write_strings(rom, world, player, team): items_to_hint.extend(BigKeys) 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 + hint_count += 2 if world.doorShuffle[player] not in ['vanilla', 'basic'] else 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) @@ -2163,8 +2166,8 @@ def write_strings(rom, world, player, team): tt[hint_locations.pop(0)] = this_hint hint_count -= 1 - # Adding a hint for the Thieves' Town Attic location in Crossed door shuffle. - if world.doorShuffle[player] in ['crossed']: + # Adding a hint for the Thieves' Town Attic location in mixed door shuffles. + if world.doorShuffle[player] not in ['vanilla', 'basic']: attic_hint = world.get_location("Thieves' Town - Attic", player).parent_region.dungeon.name this_hint = 'A cracked floor can be found in ' + attic_hint + '.' if world.intensity[player] < 2 and hint_locations[0] == 'telepathic_tile_thieves_town_upstairs': @@ -2324,7 +2327,7 @@ def write_strings(rom, world, player, team): tt['tablet_bombos_book'] = bombos_text # attic hint - if world.doorShuffle[player] in ['crossed']: + if world.doorShuffle[player] not in ['vanilla', 'basic']: attic_hint = world.get_location("Thieves' Town - Attic", player).parent_region.dungeon.name tt['blind_not_that_way'] = f'{attic_hint} is too bright for my eyes' # see tagalog.asm tables at 957,967 or Follower_HandleTrigger in JPDASM diff --git a/RoomData.py b/RoomData.py index cfdd183f..03700e32 100644 --- a/RoomData.py +++ b/RoomData.py @@ -395,8 +395,8 @@ class DoorKind(Enum): Bombable = 0x2E BlastWall = 0x30 Hidden = 0x32 - TrapTriggerable = 0x36 # right side trap or south side trap (West, South) - Trap2 = 0x38 # left side trap or north side trap (East, North) + TrapTriggerable = 0x36 # right side trap or bottom side trap (West, North) + Trap2 = 0x38 # left side trap or top side trap (East, South) NormalLow2 = 0x40 TrapTriggerableLow = 0x44 Warp = 0x46 diff --git a/data/base2current.bps b/data/base2current.bps index 060733f6e6579357fc34f52d728345c5cfe9ea01..d657ad40ba6c5ce7ceabd188463dc93b16f6db49 100644 GIT binary patch delta 3819 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 {secondary_door.name}') + logger.debug(f' Linking {primary_door.name} <-> {secondary_door.name}') else: - logger.debug(f'Linking {primary_door.name} -> {secondary_door.name}') + logger.debug(f' Linking {primary_door.name} -> {secondary_door.name}') def decouple_check(primary_list, secondary_list, primary_door, secondary_door, world, player): @@ -203,11 +203,11 @@ def modify_proposal(proposed_map, explored_state, doors_to_connect, hash_code_se itr += 1 if not world.decoupledoors[player]: - logger.debug(f'Re-linking {attempt.name} <-> {new_door.name}') - logger.debug(f'Re-linking {old_attempt.name} <-> {old_target.name}') + logger.debug(f' Re-linking {attempt.name} <-> {new_door.name}') + logger.debug(f' Re-linking {old_attempt.name} <-> {old_target.name}') else: - logger.debug(f'Re-Linking {attempt.name} -> {new_door.name}') - logger.debug(f'Re-Linking {old_attempt.name} -> {old_target.name}') + logger.debug(f' Re-Linking {attempt.name} -> {new_door.name}') + logger.debug(f' Re-Linking {old_attempt.name} -> {old_target.name}') hash_code_set.add(hash_code) return proposed_map, hash_code @@ -349,12 +349,7 @@ def connect_doors(a, b): return # Connect supported types if a.type in [DoorType.Normal, DoorType.SpiralStairs, DoorType.Open, DoorType.StraightStairs, DoorType.Ladder]: - if a.blocked: - connect_one_way(b.entrance, a.entrance) - elif b.blocked: - connect_one_way(a.entrance, b.entrance) - else: - connect_two_way(a.entrance, b.entrance) + connect_two_way(a.entrance, b.entrance) dep_doors, target = [], None if len(a.dependents) > 0: dep_doors, target = a.dependents, b @@ -527,7 +522,7 @@ class ExplorationState(object): self.key_locations += 1 if location.name not in dungeon_events and '- Prize' not in location.name and location.name not in ['Agahnim 1', 'Agahnim 2']: self.ttl_locations += 1 - if location not in self.found_locations: # todo: special logic for TT Boss? + if location not in self.found_locations: self.found_locations.append(location) if not bk_flag: self.bk_found.add(location) @@ -616,7 +611,7 @@ class ExplorationState(object): elif not self.in_door_list(door, self.avail_doors): self.append_door_to_list(door, self.avail_doors, flag) - def add_all_doors_check_proposed_2(self, region, proposed_map, valid_doors, flag, world, player): + def add_all_doors_check_proposed_2(self, region, proposed_map, valid_doors, world, player): for door in get_doors(world, region, player): if door in proposed_map and door.name in valid_doors: self.visited_doors.add(door) @@ -625,16 +620,16 @@ class ExplorationState(object): door = door.controller if door.dest is None and door not in proposed_map.keys() and door.name in valid_doors: if not self.in_door_list_ic(door, self.unattached_doors): - self.append_door_to_list(door, self.unattached_doors, flag) + self.append_door_to_list(door, self.unattached_doors) else: other = self.find_door_in_list(door, self.unattached_doors) if self.crystal != other.crystal: other.crystal = CrystalBarrier.Either elif door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door, self.event_doors): - self.append_door_to_list(door, self.event_doors, flag) + self.append_door_to_list(door, self.event_doors) elif not self.in_door_list(door, self.avail_doors): - self.append_door_to_list(door, self.avail_doors, flag) + self.append_door_to_list(door, self.avail_doors) def add_all_doors_check_proposed_traps(self, region, proposed_traps, world, player): for door in get_doors(world, region, player): diff --git a/source/tools/MysteryUtils.py b/source/tools/MysteryUtils.py index 8325719b..3816ef7c 100644 --- a/source/tools/MysteryUtils.py +++ b/source/tools/MysteryUtils.py @@ -77,6 +77,7 @@ def roll_settings(weights): door_shuffle = get_choice('door_shuffle') ret.door_shuffle = door_shuffle if door_shuffle != 'none' else 'vanilla' ret.intensity = get_choice('intensity') + ret.door_type_mode = get_choice('door_type_mode') ret.decoupledoors = get_choice('decoupledoors') == 'on' ret.experimental = get_choice('experimental') == 'on' ret.collection_rate = get_choice('collection_rate') == 'on' From 09fbdc46cadb018f2731a67d8345aa1ad2f55953 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 5 Aug 2022 14:01:19 -0600 Subject: [PATCH 28/63] Finish bomb/dash doors Lots of minor fixes Fixed a few existing bugs --- DoorShuffle.py | 417 +++++++++++++++++++++++------- DungeonGenerator.py | 12 +- Main.py | 1 + Rom.py | 5 +- RoomData.py | 6 +- data/base2current.bps | Bin 93211 -> 93229 bytes source/dungeon/DungeonStitcher.py | 3 +- 7 files changed, 338 insertions(+), 106 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index 42d32c24..69baf30f 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -18,7 +18,7 @@ from source.dungeon.DungeonStitcher import ExplorationState as ExplorationState2 # from DungeonGenerator import generate_dungeon from DungeonGenerator import ExplorationState, convert_regions, pre_validate, determine_required_paths, drop_entrances from DungeonGenerator import create_dungeon_builders, split_dungeon_builder, simple_dungeon_builder, default_dungeon_entrances -from DungeonGenerator import dungeon_portals, dungeon_drops, connect_doors +from DungeonGenerator import dungeon_portals, dungeon_drops, connect_doors, count_reserved_locations from DungeonGenerator import valid_region_to_explore from KeyDoorShuffle import analyze_dungeon, build_key_layout, validate_key_layout, determine_prize_lock from KeyDoorShuffle import validate_bk_layout, check_bk_special @@ -139,7 +139,7 @@ def link_doors_main(world, player): ['Ice Palace', 'Misery Mire', 'Turtle Rock', 'Ganons Tower']] pool = [(group, list(chain.from_iterable([dungeon_regions[d] for d in group]))) for group in groups] elif world.doorShuffle[player] == 'crossed': - pool = [list(dungeon_regions.keys()), sum(r for r in dungeon_regions.values())] + pool = [(list(dungeon_regions.keys()), sum((r for r in dungeon_regions.values()), []))] elif world.doorShuffle[player] != 'vanilla': logging.getLogger('').error('Invalid door shuffle setting: %s' % world.doorShuffle[player]) raise Exception('Invalid door shuffle setting: %s' % world.doorShuffle[player]) @@ -172,6 +172,9 @@ def mark_regions(world, player): def create_door_spoiler(world, player): logger = logging.getLogger('') + shuffled_door_types = [DoorType.Normal, DoorType.SpiralStairs] + if world.intensity[player] > 1: + shuffled_door_types += [DoorType.Open, DoorType.StraightStairs, DoorType.Ladder] queue = deque(world.dungeon_layouts[player].values()) while len(queue) > 0: @@ -185,20 +188,23 @@ def create_door_spoiler(world, player): for ext in next.exits: door_a = ext.door connect = ext.connected_region - if door_a and door_a.type in [DoorType.Normal, DoorType.SpiralStairs, DoorType.Open, - DoorType.StraightStairs, DoorType.Ladder] and door_a not in done: + if door_a and door_a.type in shuffled_door_types and door_a not in done: done.add(door_a) + door_b = door_a.dest if door_b and not isinstance(door_b, Region): - done.add(door_b) - if not door_a.blocked and not door_b.blocked: - world.spoiler.set_door(door_a.name, door_b.name, 'both', player, builder.name) - elif door_a.blocked: - world.spoiler.set_door(door_b.name, door_a.name, 'entrance', player, builder.name) - elif door_b.blocked: + if world.decoupledoors[player]: world.spoiler.set_door(door_a.name, door_b.name, 'entrance', player, builder.name) else: - logger.warning('This is a bug during door spoiler') + done.add(door_b) + if not door_a.blocked and not door_b.blocked: + world.spoiler.set_door(door_a.name, door_b.name, 'both', player, builder.name) + elif door_a.blocked: + world.spoiler.set_door(door_b.name, door_a.name, 'entrance', player, builder.name) + elif door_b.blocked: + world.spoiler.set_door(door_a.name, door_b.name, 'entrance', player, builder.name) + else: + logger.warning('This is a bug during door spoiler') elif not isinstance(door_b, Region): logger.warning('Door not connected: %s', door_a.name) if connect and connect.type == RegionType.Dungeon and connect not in visited: @@ -305,12 +311,7 @@ def connect_door_only(world, exit_name, region, player): def connect_interior_doors(a, b, world, player): door_a = world.get_door(a, player) door_b = world.get_door(b, player) - if door_a.blocked: - connect_one_way(world, b, a, player) - elif door_b.blocked: - connect_one_way(world, a, b, player) - else: - connect_two_way(world, a, b, player) + connect_two_way(world, a, b, player) def connect_two_way(world, entrancename, exitname, player): @@ -790,7 +791,7 @@ def main_dungeon_pool(dungeon_pool, world, player): door_type_pools = [] for pool, region_list in dungeon_pool: if len(pool) == 1: - dungeon_key = next(pool) + dungeon_key = next(iter(pool)) sector_pool = convert_to_sectors(region_list, world, player) merge_sectors(sector_pool, world, player) dungeon_builders[dungeon_key] = simple_dungeon_builder(dungeon_key, sector_pool) @@ -823,6 +824,17 @@ def main_dungeon_pool(dungeon_pool, world, player): check_required_paths(paths, world, player) + for pool, door_type_pool in door_type_pools: + for name in pool: + builder = world.dungeon_layouts[player][name] + region_set = builder.master_sector.region_set() + builder.bk_required = len(builder.bk_door_proposal) > 0 or any(x in region_set for x in special_bk_regions) + dungeon = world.get_dungeon(name, player) + if not builder.bk_required or builder.bk_provided: + dungeon.big_key = None + elif builder.bk_required and not builder.bk_provided: + dungeon.big_key = ItemFactory(dungeon_bigs[name], 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 if world.retro[player]: @@ -835,12 +847,12 @@ def main_dungeon_pool(dungeon_pool, world, player): target_items += 19 # 19 pot keys d_items = target_items - all_dungeon_items_cnt world.pool_adjustment[player] = d_items - # todo: remove unused pairs - if not world.decoupledoors[player]: - smooth_door_pairs(world, player) cross_dungeon_clean_up(world, player) +special_bk_regions = ['Hyrule Dungeon Cellblock', "Thieves Blind's Cell"] + + def cross_dungeon_clean_up(world, player): # Re-assign dungeon bosses gt = world.get_dungeon('Ganons Tower', player) @@ -956,47 +968,6 @@ def finish_up_work(world, player): refine_boss_exits(world, player) -# def unpair_all_doors(world, player): -# for paired_door in world.paired_doors[player]: -# paired_door.pair = False - -def within_dungeon(world, player): - add_inaccessible_doors(world, player) - entrances_map, potentials, connections = determine_entrance_list(world, player) - connections_tuple = (entrances_map, potentials, connections) - - dungeon_builders = {} - for key in dungeon_regions.keys(): - sector_list = convert_to_sectors(dungeon_regions[key], world, player) - dungeon_builders[key] = simple_dungeon_builder(key, sector_list) - dungeon_builders[key].entrance_list = list(entrances_map[key]) - recombinant_builders = {} - entrances, splits = create_dungeon_entrances(world, player) - builder_info = entrances, splits, connections_tuple, world, player - handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map, builder_info) - main_dungeon_generation(dungeon_builders, recombinant_builders, connections_tuple, 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() - for builder in world.dungeon_layouts[player].values(): - shuffle_key_doors(builder, world, player) - logging.getLogger('').info('%s: %s', world.fish.translate("cli", "cli", "keydoor.shuffle.time"), time.process_time()-start) - if not world.decoupledoors[player]: - smooth_door_pairs(world, player) - - if world.intensity[player] >= 3: - portal = world.get_portal('Sanctuary', player) - target = portal.door.entrance.parent_region - connect_simple_door(world, 'Sanctuary Mirror Route', target, player) - - refine_boss_exits(world, player) - - def handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map, builder_info): dungeon_entrances, split_dungeon_entrances, c_tuple, world, player = builder_info if dungeon_entrances is None: @@ -1601,7 +1572,12 @@ def refine_boss_exits(world, player): if len(reachable_portals) == 0: reachable_portals = possible_portals unreachable = world.inaccessible_regions[player] - filtered = [x for x in reachable_portals if x.door.entrance.connected_region.name not in unreachable] + filtered = [] + for reachable in reachable_portals: + for entrance in reachable.door.entrance.connected_region.entrances: + parent = entrance.parent_region + if parent.type != RegionType.Dungeon and parent.name not in unreachable: + filtered.append(reachable) if 0 < len(filtered) < len(reachable_portals): reachable_portals = filtered chosen_one = random.choice(reachable_portals) if len(reachable_portals) > 1 else reachable_portals[0] @@ -1766,8 +1742,7 @@ class BuilderDoorCandidates: self.small = [] self.big = [] self.trap = [] - self.bombable = [] - self.dashable = [] + self.bomb_dash = [] def shuffle_door_types(door_type_pools, paths, world, player): @@ -1776,17 +1751,19 @@ def shuffle_door_types(door_type_pools, paths, world, player): start_regions = convert_regions(builder.path_entrances, world, player) start_regions_map[name] = start_regions builder.candidates = BuilderDoorCandidates() - used_doors = shuffle_door_traps(door_type_pools, paths, start_regions_map, world, player) + + world.paired_doors[player].clear() + used_doors = shuffle_trap_doors(door_type_pools, paths, start_regions_map, world, player) # big keys used_doors = shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world, player) # small keys used_doors = shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, world, player) # bombable / dashable - - # tricky / hidden + used_doors = shuffle_bomb_dash_doors(door_type_pools, used_doors, start_regions_map, world, player) + # handle paired list -def shuffle_door_traps(door_type_pools, paths, start_regions_map, world, player): +def shuffle_trap_doors(door_type_pools, paths, start_regions_map, world, player): used_doors = set() for pool, door_type_pool in door_type_pools: ttl = 0 @@ -1804,6 +1781,8 @@ def shuffle_door_traps(door_type_pools, paths, start_regions_map, world, player) builder.candidates.trap = filter_key_door_pool(builder.candidates.trap, custom_trap_doors[dungeon]) remaining -= len(custom_trap_doors[dungeon]) ttl += len(builder.candidates.trap) + if ttl == 0: + return used_doors for dungeon in pool: builder = world.dungeon_layouts[player][dungeon] proportion = len(builder.candidates.trap) @@ -1862,6 +1841,8 @@ def shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world, builder.candidates.big = filter_key_door_pool(builder.candidates.big, custom_bk_doors[dungeon]) remaining -= len(custom_bk_doors[dungeon]) ttl += len(builder.candidates.big) + if ttl == 0: + return used_doors for dungeon in pool: builder = world.dungeon_layouts[player][dungeon] proportion = len(builder.candidates.big) @@ -1934,6 +1915,7 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, worl suggested -= 1 combo_size = ncr(len(builder.candidates.small), suggested + builder.key_drop_cnt) suggestion_map[dungeon] = builder.key_doors_num = suggested + builder.key_drop_cnt + remaining -= suggested + builder.key_drop_cnt builder.combo_size = combo_size flex_map[dungeon] = (limit - suggested) if suggested < limit else 0 for dungeon in pool: @@ -1986,6 +1968,79 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, worl return used_doors +def shuffle_bomb_dash_doors(door_type_pools, used_doors, start_regions_map, world, player): + for pool, door_type_pool in door_type_pools: + ttl = 0 + suggestion_map, bd_map = {}, {} + remaining_bomb = door_type_pool.bombable + remaining_dash = door_type_pool.dashable + + if player in world.custom_door_types: + custom_bomb_doors = world.custom_door_types[player]['Bomb Door'] + custom_dash_doors = world.custom_door_types[player]['Dash Door'] + else: + custom_bomb_doors = defaultdict(list) + custom_dash_doors = defaultdict(list) + + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + find_bd_candidates(builder, start_regions_map[dungeon], used_doors, world, player) + if custom_bomb_doors[dungeon]: + builder.candidates.bomb_dash = filter_key_door_pool(builder.candidates.bomb_dash, custom_bomb_doors[dungeon]) + remaining_bomb -= len(custom_bomb_doors[dungeon]) + if custom_dash_doors[dungeon]: + builder.candidates.bomb_dash = filter_key_door_pool(builder.candidates.bomb_dash, custom_dash_doors[dungeon]) + remaining_dash -= len(custom_dash_doors[dungeon]) + ttl += len(builder.candidates.bomb_dash) + if ttl == 0: + return used_doors + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + proportion = len(builder.candidates.bomb_dash) + calc = int(round(proportion * door_type_pool.bombable/ttl)) + suggested_bomb = min(proportion, calc) + remaining_bomb -= suggested_bomb + calc = int(round(proportion * door_type_pool.dashable/ttl)) + suggested_dash = min(proportion, calc) + remaining_dash -= suggested_dash + suggestion_map[dungeon] = suggested_bomb, suggested_dash + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + bomb_doors, dash_doors, bd_number = find_valid_bd_combination(builder, suggestion_map[dungeon], world, player) + bd_map[dungeon] = (bomb_doors, dash_doors) + if bd_number < suggestion_map[dungeon][0] + suggestion_map[dungeon][1]: + remaining_bomb += suggestion_map[dungeon][0] - len(bomb_doors) + remaining_dash += suggestion_map[dungeon][1] - len(dash_doors) + suggestion_map[dungeon] = len(bomb_doors), len(dash_doors) + builder_order = [x for x in pool] + random.shuffle(builder_order) + queue = deque(builder_order) + while len(queue) > 0 and (remaining_bomb > 0 or remaining_dash > 0): + dungeon = queue.popleft() + builder = world.dungeon_layouts[player][dungeon] + type_pool = [] + if remaining_bomb > 0: + type_pool.append('bomb') + if remaining_dash > 0: + type_pool.append('dash') + type_choice = random.choice(type_pool) + pair = suggestion_map[dungeon] + pair = pair[0] + (1 if type_choice == 'bomb' else 0), pair[1] + (1 if type_choice == 'dash' else 0) + bomb_doors, dash_doors, bd_number = find_valid_bd_combination(builder, pair, world, player) + if bomb_doors and dash_doors: + bd_map[dungeon] = (bomb_doors, dash_doors) + remaining_bomb -= (1 if type_choice == 'bomb' else 0) + remaining_dash -= (1 if type_choice == 'dash' else 0) + suggestion_map[dungeon] = pair + queue.append(dungeon) + # time to re-assign + reassign_bd_doors(bd_map, world, player) + for name, pair in bd_map.items(): + used_doors.update(flatten_pair_list(pair[0])) + used_doors.update(flatten_pair_list(pair[1])) + return used_doors + + 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? @@ -2049,7 +2104,7 @@ def find_trappable_candidates(builder, world, player): else: r_set = builder.master_sector.region_set() for r in r_set: - for ext in r.exits: + for ext in world.get_region(r, player).exits: if ext.door: d = ext.door if d.blocked and d.trapFlag != 0 and 'Boss' not in d.name and 'Agahnim' not in d.name: @@ -2188,7 +2243,7 @@ def reassign_trap_doors(trap_map, world, player): elif kind in [DoorKind.Trap2, DoorKind.TrapTriggerable]: room.change(d.doorListPos, DoorKind.Normal) d.blocked = False - connect_one_way(world, d.name, d.dest.name, player) + # connect_one_way(world, d.name, d.dest.name, player) elif d.type is DoorType.Normal and d not in traps: world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) d.blocked = False @@ -2213,18 +2268,19 @@ def change_door_to_trap(d, world, player): if d.type is DoorType.Interior: kind = room.kind(d) new_kind = None - if kind == DoorKind.TrapTriggerable and d.direction in [Direction.North, Direction.West]: + if kind == DoorKind.TrapTriggerable and d.direction in [Direction.South, Direction.East]: new_kind = DoorKind.Trap - elif kind == DoorKind.Trap2 and d.direction in [Direction.South, Direction.East]: + elif kind == DoorKind.Trap2 and d.direction in [Direction.North, Direction.West]: new_kind = DoorKind.Trap - elif d.direction in [Direction.North, Direction.West]: - new_kind = DoorKind.Trap2 elif d.direction in [Direction.South, Direction.East]: + new_kind = DoorKind.Trap2 + elif d.direction in [Direction.North, Direction.West]: new_kind = DoorKind.TrapTriggerable if new_kind: d.blocked = True - verify_door_list_pos(d, room, world, player, pos=3) - d.trapFlag = {0: 0x4, 1: 0x2, 2: 0x1}[d.doorListPos] + pos = 3 if d.type == DoorType.Normal else 4 + verify_door_list_pos(d, room, world, player, pos) + d.trapFlag = {0: 0x4, 1: 0x2, 2: 0x1, 3: 0x8}[d.doorListPos] room.change(d.doorListPos, new_kind) if d.entrance.connected_region is not None: d.entrance.connected_region.entrances.remove(d.entrance) @@ -2281,10 +2337,10 @@ def find_big_key_door_candidates(region, checked, used, world, player): if d and d.controller: d = d.controller if (d and not d.blocked and d.dest is not last_door and d.dest is not last_region - and d not in checked_doors and d not in used): + and d not in checked_doors): valid = False if (0 <= d.doorListPos < 4 and d.type in [DoorType.Interior, DoorType.Normal] - and not d.entranceFlag and d.direction in [Direction.North, Direction.South]): + and not d.entranceFlag and d.direction in [Direction.North, Direction.South] and d not in used): room = world.get_room(d.roomIndex, player) position, kind = room.doorList[d.doorListPos] if d.type == DoorType.Interior: @@ -2366,6 +2422,7 @@ def find_current_bk_doors(builder): def reassign_big_key_doors(bk_map, world, player): + logger = logging.getLogger('') for name, big_doors in bk_map.items(): flat_proposal = flatten_pair_list(big_doors) builder = world.dungeon_layouts[player][name] @@ -2388,21 +2445,11 @@ def reassign_big_key_doors(bk_map, world, player): change_door_to_big_key(d1, world, player) d2.bigKey = True # ensure flag is set else: - names = [d1.name, d2.name] - found = False - for dp in world.paired_doors[player]: - if dp.door_a in names and dp.door_b in names: - dp.pair = True - found = True - elif dp.door_a in names: - dp.pair = False - elif dp.door_b in names: - dp.pair = False - if not found: - world.paired_doors[player].append(PairedDoor(d1.name, d2.name)) + world.paired_doors[player].append(PairedDoor(d1.name, d2.name)) change_door_to_big_key(d1, world, player) change_door_to_big_key(d2, world, player) world.spoiler.set_door_type(d1.name+' <-> '+d2.name, 'Big Key Door', player) + logger.debug(f'Big Key Door: {d1.name} <-> {d2.name}') else: d = obj if d.type is DoorType.Interior: @@ -2419,6 +2466,7 @@ def reassign_big_key_doors(bk_map, world, player): change_door_to_big_key(d.dest, world, player) add_pair(d, d.dest, world, player) world.spoiler.set_door_type(d.name, 'Big Key Door', player) + logger.debug(f'Big Key Door: {d.name}') def change_door_to_big_key(d, world, player): @@ -2449,7 +2497,7 @@ def find_small_key_door_candidates(builder, start_regions, used, world, player): def calc_used_dungeon_items(builder, world, player): - base = 2 + base = max(count_reserved_locations(world, player, builder.location_set), 2) basic_flag = world.doorShuffle[player] == 'basic' if not world.bigkeyshuffle[player]: if builder.bk_required and not builder.bk_provided: @@ -2523,7 +2571,186 @@ def find_valid_combination(builder, target, start_regions, world, player, drop_k return builder.key_door_proposal, key_doors_needed -def build_sample_list(combinations, max_combinations=100000): +def find_bd_candidates(builder, start_regions, used, world, player): + # traverse dungeon and find candidates + candidates = [] + checked_doors = set() + for region in start_regions: + possible, checked = find_bd_door_candidates(region, checked_doors, used, world, player) + candidates.extend([x for x in possible if x not in candidates]) + checked_doors.update(checked) + flat_candidates = [] + for candidate in candidates: + # not valid if: Normal Coupled and Pair in is Checked and Pair is not in Candidates + if (world.decoupledoors[player] or candidate.type != DoorType.Normal + or candidate.dest not in checked_doors or candidate.dest in candidates): + flat_candidates.append(candidate) + builder.candidates.bomb_dash = build_pair_list(flat_candidates) + + +def find_bd_door_candidates(region, checked, used, world, player): + decoupled = world.decoupledoors[player] + dungeon_name = region.dungeon.name + candidates = [] + checked_doors = list(checked) + queue = deque([(region, None, None)]) + while len(queue) > 0: + current, last_door, last_region = queue.pop() + for ext in current.exits: + d = ext.door + controlled = d + if d and d.controller: + d = d.controller + if (d and not d.blocked and d.dest is not last_door and d.dest is not last_region + and d not in checked_doors): + valid = False + if (0 <= d.doorListPos < 4 and d.type in [DoorType.Interior, DoorType.Normal] and not d.entranceFlag + and d not in used): + room = world.get_room(d.roomIndex, player) + position, kind = room.doorList[d.doorListPos] + if d.type == DoorType.Interior: + valid = kind in okay_interiors + if valid and d.dest not in candidates: # interior doors are not separable yet + candidates.append(d.dest) + elif d.type == DoorType.Normal: + if decoupled: + valid = kind in okay_normals + else: + d2 = d.dest + if d2 not in candidates and d2 not in used: + if d2.type == DoorType.Normal: + room_b = world.get_room(d2.roomIndex, player) + pos_b, kind_b = room_b.doorList[d2.doorListPos] + valid = kind in okay_normals and kind_b in okay_normals and valid_key_door_pair(d, d2) + else: + valid = kind in okay_normals + if valid and 0 <= d2.doorListPos < 4: + candidates.append(d2) + else: + valid = True + if valid and d not in candidates: + candidates.append(d) + connected = ext.connected_region + if valid_region_to_explore(connected, dungeon_name, world, player): + queue.append((ext.connected_region, controlled, current)) + if d is not None: + checked_doors.append(d) + return candidates, checked_doors + + +def find_valid_bd_combination(builder, suggested, world, player): + # bombable/dashable doors could be excluded in escape in standard until we can guarantee bomb access + # if world.mode[player] == 'standard' and builder.name == 'Hyrule Castle': + # return None, None, 0 + bd_door_pool = builder.candidates.bomb_dash + bomb_doors_needed, dash_doors_needed = suggested + ttl_needed = bomb_doors_needed + dash_doors_needed + if player in world.custom_door_types: + custom_bomb_doors = world.custom_door_types[player]['Bomb Door'][builder.name] + custom_dash_doors = world.custom_door_types[player]['Dash Door'][builder.name] + else: + custom_bomb_doors = [] + custom_dash_doors = [] + if custom_bomb_doors: + bd_door_pool = filter_key_door_pool(bd_door_pool, custom_bomb_doors) + bomb_doors_needed -= len(custom_bomb_doors) + if custom_dash_doors: + bd_door_pool = filter_key_door_pool(bd_door_pool, custom_dash_doors) + dash_doors_needed -= len(custom_dash_doors) + while len(bd_door_pool) < bomb_doors_needed + dash_doors_needed: + bomb_doors_needed = round(len(bd_door_pool) * bomb_doors_needed/ttl_needed) + dash_doors_needed = round(len(bd_door_pool) * dash_doors_needed/ttl_needed) + bomb_proposal = random.sample(bd_door_pool, k=bomb_doors_needed) + bomb_proposal.extend(custom_bomb_doors) + dash_pool = [x for x in bd_door_pool if x not in bomb_proposal] + dash_proposal = random.sample(dash_pool, k=dash_doors_needed) + dash_proposal.extend(custom_dash_doors) + return bomb_proposal, dash_proposal, ttl_needed + + +def reassign_bd_doors(bd_map, world, player): + for name, pair in bd_map.items(): + flat_bomb_proposal = flatten_pair_list(pair[0]) + flat_dash_proposal = flatten_pair_list(pair[1]) + + def not_in_proposal(door): + return (door not in flat_bomb_proposal and door.dest not in flat_bomb_proposal and + door not in flat_dash_proposal and door.dest not in flat_bomb_proposal) + + builder = world.dungeon_layouts[player][name] + queue = deque(find_current_bd_doors(builder, world)) + while len(queue) > 0: + d = queue.pop() + if d.type is DoorType.Interior and not_in_proposal(d): + if not d.entranceFlag: + world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) + elif d.type is DoorType.Normal and not_in_proposal(d): + if not d.entranceFlag: + world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) + do_bombable_dashable(flat_bomb_proposal, DoorKind.Bombable, world, player) + do_bombable_dashable(flat_dash_proposal, DoorKind.Dashable, world, player) + + +def do_bombable_dashable(proposal, kind, world, player): + for obj in proposal: + if type(obj) is tuple: + d1 = obj[0] + d2 = obj[1] + if d1.type is DoorType.Interior: + change_door_to_kind(d1, kind, world, player) + else: + names = [d1.name, d2.name] + found = False + for dp in world.paired_doors[player]: + if dp.door_a in names and dp.door_b in names: + dp.pair = True + found = True + elif dp.door_a in names: + dp.pair = False + elif dp.door_b in names: + dp.pair = False + if not found: + world.paired_doors[player].append(PairedDoor(d1.name, d2.name)) + change_door_to_kind(d1, kind, world, player) + change_door_to_kind(d2, kind, world, player) + spoiler_type = 'Bomb Door' if kind == DoorKind.Bombable else 'Dash Door' + world.spoiler.set_door_type(d1.name+' <-> '+d2.name, spoiler_type, player) + else: + d = obj + if d.type is DoorType.Interior: + change_door_to_kind(d, kind, world, player) + elif d.type is DoorType.Normal: + change_door_to_kind(d, kind, world, player) + if not world.decoupledoors[player] and d.dest: + if d.dest.type in okay_normals and not std_forbidden(d.dest, world, player): + dest_room = world.get_room(d.dest.roomIndex, player) + if stateful_door(d.dest, dest_room.kind(d.dest)): + change_door_to_kind(d.dest, kind, world, player) + add_pair(d, d.dest, world, player) + spoiler_type = 'Bomb Door' if kind == DoorKind.Bombable else 'Dash Door' + world.spoiler.set_door_type(d.name, spoiler_type, player) + + +def find_current_bd_doors(builder, world): + current_doors = [] + for region in builder.master_sector.regions: + for ext in region.exits: + d = ext.door + if d and d.type in [DoorType.Interior, DoorType.Normal]: + kind = d.kind(world) + if kind in [DoorKind.Dashable, DoorKind.Bombable]: + current_doors.append(d) + return current_doors + + +def change_door_to_kind(d, kind, world, player): + room = world.get_room(d.roomIndex, player) + if room.doorList[d.doorListPos][1] != kind: + verify_door_list_pos(d, room, world, player) + room.change(d.doorListPos, kind) + + +def build_sample_list(combinations, max_combinations=10000): if combinations <= max_combinations: sample_list = list(range(0, int(combinations))) else: @@ -2609,10 +2836,10 @@ def find_key_door_candidates(region, checked, used, world, player): if d and d.controller: d = d.controller if (d and not d.blocked and d.dest is not last_door and d.dest is not last_region - and d not in checked_doors and d not in used): + and d not in checked_doors): valid = False if (0 <= d.doorListPos < 4 and d.type in [DoorType.Interior, DoorType.Normal, DoorType.SpiralStairs] - and not d.entranceFlag): + and not d.entranceFlag and d not in used): room = world.get_room(d.roomIndex, player) position, kind = room.doorList[d.doorListPos] if d.type == DoorType.Interior: diff --git a/DungeonGenerator.py b/DungeonGenerator.py index f06c3535..055a7252 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -15,6 +15,8 @@ from Regions import dungeon_events, flooded_keys_reverse from Dungeons import dungeon_regions, split_region_starts from RoomData import DoorKind +from source.dungeon.DungeonStitcher import generate_dungeon_find_proposal + class GraphPiece: @@ -62,7 +64,7 @@ def generate_dungeon(builder, entrance_region_names, split_dungeon, world, playe if builder.valid_proposal: # we made this earlier in gen, just use it proposed_map = builder.valid_proposal else: - proposed_map = generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon, world, player) + proposed_map = generate_dungeon_find_proposal_old(builder, entrance_region_names, split_dungeon, world, player) builder.valid_proposal = proposed_map queue = collections.deque(proposed_map.items()) while len(queue) > 0: @@ -80,7 +82,7 @@ def generate_dungeon(builder, entrance_region_names, split_dungeon, world, playe return master_sector -def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon, world, player): +def generate_dungeon_find_proposal_old(builder, entrance_region_names, split_dungeon, world, player): logger = logging.getLogger('') name = builder.name entrance_regions = convert_regions(entrance_region_names, world, player) @@ -868,9 +870,9 @@ class ExplorationState(object): self.key_locations += 1 if location.name not in dungeon_events and '- Prize' not in location.name and location.name not in ['Agahnim 1', 'Agahnim 2']: self.ttl_locations += 1 - if location not in self.found_locations: # todo: special logic for TT Boss? + if location not in self.found_locations: self.found_locations.append(location) - if not bk_Flag: + if not bk_Flag and (not location.forced_item or 'Big Key' in location.item.name): self.bk_found.add(location) if location.name in dungeon_events and location.name not in self.events: if self.flooded_key_check(location): @@ -1199,6 +1201,7 @@ class DungeonBuilder(object): self.name = name self.sectors = [] self.location_cnt = 0 + self.location_set = set() self.key_drop_cnt = 0 self.dungeon_items = None # during fill how many dungeon items are left self.free_items = None # during fill how many dungeon items are left @@ -1569,6 +1572,7 @@ def assign_sector_helper(sector, builder): builder.sectors.append(sector) builder.location_cnt += sector.chest_locations builder.key_drop_cnt += sector.key_only_locations + builder.location_set.update(sector.chest_location_set) if sector.c_switch: builder.c_switch_present = True if sector.blue_barrier: diff --git a/Main.py b/Main.py index 4e6b9c4b..5efc990b 100644 --- a/Main.py +++ b/Main.py @@ -107,6 +107,7 @@ def main(args, seed=None, fish=None): world.beemizer = args.beemizer.copy() world.intensity = {player: random.randint(1, 3) if args.intensity[player] == 'random' else int(args.intensity[player]) for player in range(1, world.players + 1)} world.door_type_mode = args.door_type_mode.copy() + world.decoupledoors = args.decoupledoors.copy() world.experimental = args.experimental.copy() world.dungeon_counters = args.dungeon_counters.copy() world.fish = fish diff --git a/Rom.py b/Rom.py index d6717617..18f0c70b 100644 --- a/Rom.py +++ b/Rom.py @@ -37,7 +37,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'afcd895b87559cd29b04aa3714cbc929' +RANDOMIZERBASEHASH = 'b4daf51397414464562a772f72f0b551' class JsonRom(object): @@ -843,7 +843,8 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): if world.doorShuffle[player] != 'vanilla': for name, pair in boss_indicator.items(): dungeon_id, boss_door = pair - opposite_door = world.get_door(boss_door, player).dest + boss_region = world.get_door(boss_door, player).entrance.parent_region + opposite_door = next(iter(x for x in boss_region.entrances if x.name != 'Skull Final Drop WS')).door if opposite_door and isinstance(opposite_door, Door) and opposite_door.roomIndex > -1: dungeon_name = opposite_door.entrance.parent_region.dungeon.name dungeon_id = boss_indicator[dungeon_name][0] diff --git a/RoomData.py b/RoomData.py index 03700e32..85c3b1fd 100644 --- a/RoomData.py +++ b/RoomData.py @@ -320,7 +320,7 @@ class Room(object): for i, door in enumerate(self.doorList): if i >= pos: return None - pos, kind = door + position, 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, @@ -395,8 +395,8 @@ class DoorKind(Enum): Bombable = 0x2E BlastWall = 0x30 Hidden = 0x32 - TrapTriggerable = 0x36 # right side trap or bottom side trap (West, North) - Trap2 = 0x38 # left side trap or top side trap (East, South) + TrapTriggerable = 0x36 # right side trap or bottom side trap (door directions: West, North) + Trap2 = 0x38 # left side trap or top side trap (door directions: East, South) NormalLow2 = 0x40 TrapTriggerableLow = 0x44 Warp = 0x46 diff --git a/data/base2current.bps b/data/base2current.bps index d657ad40ba6c5ce7ceabd188463dc93b16f6db49..f20e51505f4403e3ec1f477e0fcba47d5b000d0f 100644 GIT binary patch delta 9728 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 delta 9675 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 diff --git a/source/dungeon/DungeonStitcher.py b/source/dungeon/DungeonStitcher.py index b179cb1c..aa74dee0 100644 --- a/source/dungeon/DungeonStitcher.py +++ b/source/dungeon/DungeonStitcher.py @@ -370,8 +370,7 @@ def connect_doors_one_way(a, b): return # Connect supported types if a.type in [DoorType.Normal, DoorType.SpiralStairs, DoorType.Open, DoorType.StraightStairs, DoorType.Ladder]: - if not a.blocked: - connect_one_way(a.entrance, b.entrance) + connect_one_way(a.entrance, b.entrance) dep_doors, target = [], None if len(a.dependents) > 0: dep_doors, target = a.dependents, b From 5dd9403afd988e1b0d395296f435cd96f3d12f61 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 11 Aug 2022 15:38:15 -0600 Subject: [PATCH 29/63] Correct rom for tavern work --- Rom.py | 2 +- data/base2current.bps | Bin 93041 -> 93253 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index d63ac585..2b76e5e4 100644 --- a/Rom.py +++ b/Rom.py @@ -37,7 +37,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'e33204b6023f07025eba16874308f57d' +RANDOMIZERBASEHASH = 'a8b35a6396c104e9419ff1e46342e4db' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index fc22d9c3956780075f6752188bd52fca2ae6c189..a5f9cc86e0647cb500a73cc56d94863bd395834d 100644 GIT binary patch delta 16205 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 Date: Thu, 11 Aug 2022 16:07:11 -0600 Subject: [PATCH 30/63] Rom update --- data/base2current.bps | Bin 93229 -> 93357 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/data/base2current.bps b/data/base2current.bps index f20e51505f4403e3ec1f477e0fcba47d5b000d0f..7811e083f0ec3e68051cbd97a49c301b23b61b7b 100644 GIT binary patch delta 8035 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 delta 8030 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 From c3dcd569924d38e97282e63fe6cafb54fcde8908 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 11 Aug 2022 16:37:15 -0600 Subject: [PATCH 31/63] Some fixes sent by Catobat --- DoorShuffle.py | 4 ++-- Rom.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index 165dabf6..9d4a8475 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1730,7 +1730,7 @@ class DoorTypePool: def chaos_shuffle(self, counts): weights = [1, 2, 4, 3, 2, 1] - return [random.choices(self.get_choices(counts[i]), weights=weights) for i, c in enumerate(counts)] + return [random.choices(self.get_choices(counts[i]), weights=weights)[0] for i, c in enumerate(counts)] @staticmethod def get_choices(number): @@ -2316,7 +2316,7 @@ def find_big_key_candidates(builder, start_regions, used, world, player): else: r_set = builder.master_sector.region_set() for r in r_set: - for ext in r.exits: + for ext in world.get_region(r, player).exits: if ext.door: d = ext.door if d.bigKey and d.type in [DoorType.Normal, DoorType.Interior]: diff --git a/Rom.py b/Rom.py index 453e486a..3bc64b82 100644 --- a/Rom.py +++ b/Rom.py @@ -753,7 +753,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): dr_flags |= DROptions.DarkWorld_Spawns if world.logic[player] != 'nologic': dr_flags |= DROptions.Fix_EG - if world.door_type_mode in ['big', 'chaos']: + if world.door_type_mode[player] in ['big', 'all', 'chaos']: dr_flags |= DROptions.BigKeyDoor_Shuffle my_locations = world.get_filled_locations(player) From a5354f1f30b21bb644f2361041639268c2d4c66c Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 12 Aug 2022 13:50:04 -0600 Subject: [PATCH 32/63] Spoiler bug --- BaseClasses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BaseClasses.py b/BaseClasses.py index 118ff9de..2dc61574 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -2574,7 +2574,7 @@ class Spoiler(object): outfile.write(f"Pseudoboots: {yn(self.metadata['pseudoboots'][player])}\n") outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player]) if self.metadata['shuffle'][player] != 'vanilla': - outfile.write(f"Link's House Shuffled: {yn(self.metadata['shufflelinks'])}\n") + outfile.write(f"Link's House Shuffled: {yn(self.metadata['shufflelinks'][player])}\n") outfile.write(f"Back of Tavern Shuffled: {yn(self.metadata['shuffletavern'][player])}\n") outfile.write(f"GT/Ganon Shuffled: {yn(self.metadata['shuffleganon'])}\n") outfile.write(f"Overworld Map: {self.metadata['overworld_map'][player]}\n") From 3e7dfaf8567952ba2ecca140b1b7377ab6df5002 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 23 Aug 2022 15:06:36 -0600 Subject: [PATCH 33/63] Fix Key Rat region --- DoorShuffle.py | 9 +++++---- Doors.py | 8 +++++--- Dungeons.py | 8 ++++---- KeyDoorShuffle.py | 2 +- PotShuffle.py | 12 ++++++------ Regions.py | 5 ++++- Rules.py | 3 ++- 7 files changed, 27 insertions(+), 20 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index 9d4a8475..586a2c64 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -3888,6 +3888,7 @@ interior_doors = [ ('Hyrule Dungeon Armory Interior Key Door S', 'Hyrule Dungeon Armory Interior Key Door N'), ('Hyrule Dungeon Armory ES', 'Hyrule Dungeon Armory Boomerang WS'), ('Hyrule Dungeon Map Room Key Door S', 'Hyrule Dungeon North Abyss Key Door N'), + ('Sewers Dark Aquabats N', 'Sewers Key Rat S'), ('Sewers Rat Path WS', 'Sewers Secret Room ES'), ('Sewers Rat Path WN', 'Sewers Secret Room EN'), ('Sewers Yet More Rats S', 'Sewers Pull Switch N'), @@ -4058,7 +4059,7 @@ interior_doors = [ ] key_doors = [ - ('Sewers Key Rat Key Door N', 'Sewers Secret Room Key Door S'), + ('Sewers Key Rat NE', 'Sewers Secret Room Key Door S'), ('Sewers Dark Cross Key Door N', 'Sewers Water S'), ('Eastern Dark Square Key Door WN', 'Eastern Cannonball Ledge Key Door EN'), ('Eastern Darkness Up Stairs', 'Eastern Attic Start Down Stairs'), @@ -4078,7 +4079,7 @@ key_doors = [ default_small_key_doors = { 'Hyrule Castle': [ - ('Sewers Key Rat Key Door N', 'Sewers Secret Room Key Door S'), + ('Sewers Key Rat NE', 'Sewers Secret Room Key Door S'), ('Sewers Dark Cross Key Door N', 'Sewers Water S'), ('Hyrule Dungeon Map Room Key Door S', 'Hyrule Dungeon North Abyss Key Door N'), ('Hyrule Dungeon Armory Interior Key Door N', 'Hyrule Dungeon Armory Interior Key Door S') @@ -4178,8 +4179,8 @@ default_door_connections = [ ('Hyrule Castle Throne Room N', 'Sewers Behind Tapestry S'), ('Hyrule Dungeon Guardroom N', 'Hyrule Dungeon Armory S'), ('Sewers Dark Cross Key Door N', 'Sewers Water S'), - ('Sewers Water W', 'Sewers Key Rat E'), - ('Sewers Key Rat Key Door N', 'Sewers Secret Room Key Door S'), + ('Sewers Water W', 'Sewers Dark Aquabats ES'), + ('Sewers Key Rat NE', 'Sewers Secret Room Key Door S'), ('Eastern Lobby Bridge N', 'Eastern Cannonball S'), ('Eastern Cannonball N', 'Eastern Courtyard Ledge S'), ('Eastern Cannonball Ledge WN', 'Eastern Big Key EN'), diff --git a/Doors.py b/Doors.py index 99b4fab3..38daaa6a 100644 --- a/Doors.py +++ b/Doors.py @@ -104,8 +104,10 @@ def create_doors(world, player): create_door(player, 'Sewers Dark Cross Key Door N', Nrml).dir(No, 0x32, Mid, High).small_key().pos(0), create_door(player, 'Sewers Water S', Nrml).dir(So, 0x22, Mid, High).small_key().pos(0).portal(Z, 0x22), create_door(player, 'Sewers Water W', Nrml).dir(We, 0x22, Bot, High).pos(1), - create_door(player, 'Sewers Key Rat E', Nrml).dir(Ea, 0x21, Bot, High).pos(1), - create_door(player, 'Sewers Key Rat Key Door N', Nrml).dir(No, 0x21, Right, High).small_key().pos(0), + create_door(player, 'Sewers Dark Aquabats ES', Nrml).dir(Ea, 0x21, Bot, High).pos(2), + create_door(player, 'Sewers Dark Aquabats N', Intr).dir(No, 0x21, Mid, High).pos(1), + create_door(player, 'Sewers Key Rat S', Intr).dir(So, 0x21, Mid, High).pos(1), + create_door(player, 'Sewers Key Rat NE', Nrml).dir(No, 0x21, Right, High).small_key().pos(0), create_door(player, 'Sewers Secret Room Key Door S', Nrml).dir(So, 0x11, Right, High).small_key().pos(2).portal(X, 0x02), create_door(player, 'Sewers Rat Path WS', Intr).dir(We, 0x11, Bot, High).pos(1), create_door(player, 'Sewers Rat Path WN', Intr).dir(We, 0x11, Top, High).pos(0), @@ -1567,7 +1569,7 @@ def reset_portals(world, player): def create_paired_doors(world, player): world.paired_doors[player] = [ - PairedDoor('Sewers Secret Room Key Door S', 'Sewers Key Rat Key Door N', True), + PairedDoor('Sewers Secret Room Key Door S', 'Sewers Key Rat NE', True), PairedDoor('TR Pokey 2 ES', 'TR Lava Island WS', True), # TR Pokey Key PairedDoor('TR Dodgers NE', 'TR Lava Escape SE', True), # TR Big key door by pipes PairedDoor('PoD Falling Bridge WN', 'PoD Dark Maze EN', True), # Pod Dark maze door diff --git a/Dungeons.py b/Dungeons.py index 188cf59f..58ebcc75 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -52,10 +52,10 @@ hyrule_castle_regions = [ 'Hyrule Dungeon South Abyss', 'Hyrule Dungeon South Abyss Catwalk', 'Hyrule Dungeon Guardroom', 'Hyrule Dungeon Armory Main', 'Hyrule Dungeon Armory Boomerang', 'Hyrule Dungeon Armory North Branch', 'Hyrule Dungeon Staircase', 'Hyrule Dungeon Cellblock', 'Hyrule Dungeon Cell', 'Sewers Behind Tapestry', - 'Sewers Rope Room', 'Sewers Dark Cross', 'Sewers Water', 'Sewers Key Rat', 'Sewers Rat Path', - 'Sewers Secret Room Blocked Path', 'Sewers Secret Room', 'Sewers Yet More Rats', 'Sewers Pull Switch', 'Sanctuary', - 'Sanctuary Portal', 'Hyrule Castle West Portal', 'Hyrule Castle South Portal', 'Hyrule Castle East Portal' - + 'Sewers Rope Room', 'Sewers Dark Cross', 'Sewers Water', 'Sewers Dark Aquabats', 'Sewers Key Rat', + 'Sewers Rat Path', 'Sewers Secret Room Blocked Path', 'Sewers Secret Room', 'Sewers Yet More Rats', + 'Sewers Pull Switch', 'Sanctuary', 'Sanctuary Portal', 'Hyrule Castle West Portal', 'Hyrule Castle South Portal', + 'Hyrule Castle East Portal' ] eastern_regions = [ diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 8a70abcf..4d501764 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -1908,7 +1908,7 @@ def val_hyrule(key_logic, world, player): val_rule(key_logic.door_rules['Hyrule Dungeon Map Room Key Door S'], 1) val_rule(key_logic.door_rules['Hyrule Dungeon Armory Interior Key Door N'], 2) val_rule(key_logic.door_rules['Sewers Dark Cross Key Door N'], 3) - val_rule(key_logic.door_rules['Sewers Key Rat Key Door N'], 4) + val_rule(key_logic.door_rules['Sewers Key Rat NE'], 4) else: val_rule(key_logic.door_rules['Sewers Secret Room Key Door S'], 2) val_rule(key_logic.door_rules['Sewers Dark Cross Key Door N'], 2) diff --git a/PotShuffle.py b/PotShuffle.py index 7806f495..eafc7399 100644 --- a/PotShuffle.py +++ b/PotShuffle.py @@ -93,12 +93,12 @@ vanilla_pots = { Pot(28, 23, PotItem.Nothing, 'Ice Pengator Switch', obj=RoomObject(0x1FC388, [0x3B, 0xBB, 0xFA])), Pot(86, 26, PotItem.Nothing, 'Ice Big Key', obj=RoomObject(0x1FC397, [0xAF, 0xD3, 0xFA])), Pot(86, 27, PotItem.Nothing, 'Ice Big Key', obj=RoomObject(0x1FC39A, [0xAF, 0xDB, 0xFA]))], - 0x21: [Pot(160, 20, PotItem.Nothing, 'Sewers Key Rat', obj=RoomObject(0x0A8C71, [0x43, 0xA7, 0xFA])), - Pot(168, 24, PotItem.SmallMagic, 'Sewers Key Rat', obj=RoomObject(0x0A8C7A, [0x53, 0xC7, 0xFA])), - Pot(48, 28, PotItem.Heart, 'Sewers Key Rat', obj=RoomObject(0x0A8C80, [0x63, 0xE3, 0xFA])), - Pot(82, 28, PotItem.SmallMagic, 'Sewers Key Rat', obj=RoomObject(0x0A8C7D, [0xA7, 0xE3, 0xFA])), - Pot(100, 28, PotItem.Nothing, 'Sewers Key Rat', obj=RoomObject(0x0A8C74, [0xCB, 0xE3, 0xFA])), - Pot(104, 28, PotItem.Nothing, 'Sewers Key Rat', obj=RoomObject(0x0A8C77, [0xD3, 0xE3, 0xFA]))], + 0x21: [Pot(160, 20, PotItem.Nothing, 'Sewers Dark Aquabats', obj=RoomObject(0x0A8C71, [0x43, 0xA7, 0xFA])), + Pot(168, 24, PotItem.SmallMagic, 'Sewers Dark Aquabats', obj=RoomObject(0x0A8C7A, [0x53, 0xC7, 0xFA])), + Pot(48, 28, PotItem.Heart, 'Sewers Dark Aquabats', obj=RoomObject(0x0A8C80, [0x63, 0xE3, 0xFA])), + Pot(82, 28, PotItem.SmallMagic, 'Sewers Dark Aquabats', obj=RoomObject(0x0A8C7D, [0xA7, 0xE3, 0xFA])), + Pot(100, 28, PotItem.Nothing, 'Sewers Dark Aquabats', obj=RoomObject(0x0A8C74, [0xCB, 0xE3, 0xFA])), + Pot(104, 28, PotItem.Nothing, 'Sewers Dark Aquabats', obj=RoomObject(0x0A8C77, [0xD3, 0xE3, 0xFA]))], 0x23: [Pot(86, 26, PotItem.OneRupee, 'TR Lazy Eyes', obj=RoomObject(0x1FED09, [0xAF, 0xD3, 0xFA])), Pot(90, 26, PotItem.Heart, 'TR Lazy Eyes', obj=RoomObject(0x1FED0C, [0xB7, 0xD3, 0xFA])), Pot(94, 26, PotItem.OneRupee, 'TR Lazy Eyes', obj=RoomObject(0x1FED0F, [0xBF, 0xD3, 0xFA])), diff --git a/Regions.py b/Regions.py index c0ef0364..c3fe40c5 100644 --- a/Regions.py +++ b/Regions.py @@ -284,7 +284,10 @@ def create_dungeon_regions(world, player): create_dungeon_region(player, 'Sewers Rope Room', 'Hyrule Castle', None, ['Sewers Rope Room Up Stairs', 'Sewers Rope Room North Stairs']), create_dungeon_region(player, 'Sewers Dark Cross', 'Hyrule Castle', ['Sewers - Dark Cross'], ['Sewers Dark Cross Key Door N', 'Sewers Dark Cross South Stairs']), create_dungeon_region(player, 'Sewers Water', 'Hyrule Castle', None, ['Sewers Water S', 'Sewers Water W']), - create_dungeon_region(player, 'Sewers Key Rat', 'Hyrule Castle', ['Hyrule Castle - Key Rat Key Drop'], ['Sewers Key Rat E', 'Sewers Key Rat Key Door N']), + create_dungeon_region(player, 'Sewers Dark Aquabats', 'Hyrule Castle', None, + ['Sewers Dark Aquabats ES', 'Sewers Dark Aquabats N']), + create_dungeon_region(player, 'Sewers Key Rat', 'Hyrule Castle', ['Hyrule Castle - Key Rat Key Drop'], + ['Sewers Key Rat S', 'Sewers Key Rat NE']), create_dungeon_region(player, 'Sewers Secret Room Blocked Path', 'Hyrule Castle', None, ['Sewers Secret Room Up Stairs']), create_dungeon_region(player, 'Sewers Rat Path', 'Hyrule Castle', None, ['Sewers Secret Room Key Door S', 'Sewers Secret Room Push Block', 'Sewers Rat Path WS', 'Sewers Rat Path WN']), create_dungeon_region(player, 'Sewers Secret Room', 'Hyrule Castle', ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle', 'Sewers - Secret Room - Right'], diff --git a/Rules.py b/Rules.py index 18ec1fff..b86a7345 100644 --- a/Rules.py +++ b/Rules.py @@ -1149,7 +1149,8 @@ def add_conditional_lamps(world, player): 'Sewers Behind Tapestry': {'sewer': True, 'entrances': ['Sewers Behind Tapestry S', 'Sewers Behind Tapestry Down Stairs'], 'locations': []}, 'Sewers Rope Room': {'sewer': True, 'entrances': ['Sewers Rope Room Up Stairs', 'Sewers Rope Room North Stairs'], 'locations': []}, 'Sewers Water': {'sewer': True, 'entrances': ['Sewers Water S', 'Sewers Water W'], 'locations': []}, - 'Sewers Key Rat': {'sewer': True, 'entrances': ['Sewers Key Rat E', 'Sewers Key Rat Key Door N'], 'locations': ['Hyrule Castle - Key Rat Key Drop']}, + 'Sewers Dark Aquabats': {'sewer': True, 'entrances': ['Sewers Dark Aquabats N', 'Sewers Dark Aquabats ES'], 'locations': []}, + 'Sewers Key Rat': {'sewer': True, 'entrances': ['Sewers Key Rat S', 'Sewers Key Rat NE'], 'locations': ['Hyrule Castle - Key Rat Key Drop']}, 'Old Man Cave': {'sewer': False, 'entrances': ['Old Man Cave Exit (East)']}, 'Old Man House Back': {'sewer': False, 'entrances': ['Old Man House Back to Front', 'Old Man House Exit (Top)']}, 'Death Mountain Return Cave (left)': {'sewer': False, 'entrances': ['Death Mountain Return Cave E', 'Death Mountain Return Cave Exit (West)']}, From 5b8b0cb7f8c0835c339fd645d268f49a48531619 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 23 Aug 2022 15:27:22 -0600 Subject: [PATCH 34/63] Mire Falling Bridge logic refinement for possible trap door movement --- DoorShuffle.py | 4 ++++ Doors.py | 8 ++++++++ Dungeons.py | 18 ++++++++++-------- Regions.py | 7 ++++++- Rules.py | 9 +++++---- 5 files changed, 33 insertions(+), 13 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index 586a2c64..31744208 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -3593,6 +3593,8 @@ logical_connections = [ ('Mire Hub Top Blue Barrier', 'Mire Hub Switch'), ('Mire Hub Switch Blue Barrier N', 'Mire Hub Top'), ('Mire Hub Switch Blue Barrier S', 'Mire Hub'), + ('Mire Falling Bridge Hook Path', 'Mire Falling Bridge - Chest'), + ('Mire Falling Bridge Hook Only Path', 'Mire Falling Bridge - Chest'), ('Mire Map Spike Side Drop Down', 'Mire Lone Shooter'), ('Mire Map Spike Side Blue Barrier', 'Mire Crystal Dead End'), ('Mire Map Spot Blue Barrier', 'Mire Crystal Dead End'), @@ -3725,6 +3727,8 @@ vanilla_logical_connections = [ ('Ice Cross Right Push Block Bottom', 'Ice Compass Room'), ('Ice Cross Bottom Push Block Right', 'Ice Pengator Switch'), ('Ice Cross Top Push Block Right', 'Ice Pengator Switch'), + ('Mire Falling Bridge Primary Path', 'Mire Lone Shooter'), + ('Mire Falling Bridge Failure Path', 'Mire Failure Bridge'), ] spiral_staircases = [ diff --git a/Doors.py b/Doors.py index 38daaa6a..5a205f1c 100644 --- a/Doors.py +++ b/Doors.py @@ -847,6 +847,10 @@ def create_doors(world, player): create_door(player, 'Mire Failure Bridge E', Intr).dir(Ea, 0xc3, Mid, High).no_exit().trap(0x1).pos(2), create_door(player, 'Mire Failure Bridge W', Nrml).dir(We, 0xc3, Mid, High).pos(5), create_door(player, 'Mire Falling Bridge WN', Intr).dir(We, 0xc3, Top, High).pos(1), + create_door(player, 'Mire Falling Bridge Hook Path', Lgcl), + create_door(player, 'Mire Falling Bridge Hook Only Path', Lgcl), + create_door(player, 'Mire Falling Bridge Primary Path', Lgcl), # dynamic + create_door(player, 'Mire Falling Bridge Failure Path', Lgcl), # dynamic create_door(player, 'Mire Map Spike Side EN', Intr).dir(Ea, 0xc3, Top, High).no_exit().trap(0x2).pos(1), create_door(player, 'Mire Map Spot WN', Nrml).dir(We, 0xc3, Top, High).small_key().pos(0), create_door(player, 'Mire Crystal Dead End NW', Nrml).dir(No, 0xc3, Left, High).pos(4), @@ -1477,6 +1481,10 @@ def create_doors(world, player): controller_door(south_controller, world.get_door('Ice Cross Top Push Block Bottom', player)) controller_door(east_controller, world.get_door('Ice Cross Bottom Push Block Right', player)) controller_door(east_controller, world.get_door('Ice Cross Top Push Block Right', player)) + failure_controller = world.get_door('Mire Falling Bridge W', player) + primary_controller = world.get_door('Mire Falling Bridge WS', player) + controller_door(failure_controller, world.get_door('Mire Falling Bridge Failure Path', player)) + controller_door(primary_controller, world.get_door('Mire Falling Bridge Primary Path', player)) assign_entrances(world, player) diff --git a/Dungeons.py b/Dungeons.py index 58ebcc75..59a6a963 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -155,14 +155,16 @@ ice_regions = [ mire_regions = [ 'Mire Lobby', 'Mire Post-Gap', 'Mire 2', 'Mire Hub', 'Mire Hub Right', 'Mire Hub Top', 'Mire Hub Switch', - 'Mire Lone Shooter', 'Mire Failure Bridge', 'Mire Falling Bridge', 'Mire Map Spike Side', 'Mire Map Spot', - 'Mire Crystal Dead End', 'Mire Hidden Shooters', 'Mire Hidden Shooters Blocked', 'Mire Cross', 'Mire Minibridge', - 'Mire BK Door Room', 'Mire Spikes', 'Mire Ledgehop', 'Mire Bent Bridge', 'Mire Over Bridge', 'Mire Right Bridge', - 'Mire Left Bridge', 'Mire Fishbone', 'Mire South Fish', 'Mire Spike Barrier', 'Mire Square Rail', 'Mire Lone Warp', - 'Mire Wizzrobe Bypass', 'Mire Conveyor Crystal', 'Mire Conveyor - Crystal', 'Mire Tile Room', 'Mire Compass Room', 'Mire Compass Chest', - 'Mire Neglected Room', 'Mire Chest View', 'Mire Conveyor Barrier', 'Mire BK Chest Ledge', 'Mire Warping Pool', - 'Mire Torches Top', 'Mire Torches Bottom', 'Mire Attic Hint', 'Mire Dark Shooters', 'Mire Key Rupees', - 'Mire Block X', 'Mire Tall Dark and Roomy', 'Mire Tall Dark and Roomy - Ranged Crystal', 'Mire Crystal Right', 'Mire Crystal Mid', 'Mire Crystal Left', + 'Mire Lone Shooter', 'Mire Failure Bridge', 'Mire Falling Bridge - Primary', 'Mire Falling Bridge - Failure', + 'Mire Falling Bridge - Chest', 'Mire Map Spike Side', 'Mire Map Spot', 'Mire Crystal Dead End', + 'Mire Hidden Shooters', 'Mire Hidden Shooters Blocked', 'Mire Cross', 'Mire Minibridge', 'Mire BK Door Room', + 'Mire Spikes', 'Mire Ledgehop', 'Mire Bent Bridge', 'Mire Over Bridge', 'Mire Right Bridge', 'Mire Left Bridge', + 'Mire Fishbone', 'Mire South Fish', 'Mire Spike Barrier', 'Mire Square Rail', 'Mire Lone Warp', + 'Mire Wizzrobe Bypass', 'Mire Conveyor Crystal', 'Mire Conveyor - Crystal', 'Mire Tile Room', 'Mire Compass Room', + 'Mire Compass Chest', 'Mire Neglected Room', 'Mire Chest View', 'Mire Conveyor Barrier', 'Mire BK Chest Ledge', + 'Mire Warping Pool', 'Mire Torches Top', 'Mire Torches Bottom', 'Mire Attic Hint', 'Mire Dark Shooters', + 'Mire Key Rupees', 'Mire Block X', 'Mire Tall Dark and Roomy', 'Mire Tall Dark and Roomy - Ranged Crystal', + 'Mire Crystal Right', 'Mire Crystal Mid', 'Mire Crystal Left', 'Mire Crystal Top', 'Mire Shooter Rupees', 'Mire Falling Foes', 'Mire Firesnake Skip', 'Mire Antechamber', 'Mire Boss', 'Mire Portal' ] diff --git a/Regions.py b/Regions.py index c3fe40c5..88c0242b 100644 --- a/Regions.py +++ b/Regions.py @@ -636,7 +636,12 @@ def create_dungeon_regions(world, player): create_dungeon_region(player, 'Mire Hub Switch', 'Misery Mire', ['Misery Mire - Main Lobby'], ['Mire Hub Switch Blue Barrier N', 'Mire Hub Switch Blue Barrier S']), create_dungeon_region(player, 'Mire Lone Shooter', 'Misery Mire', None, ['Mire Lone Shooter WS', 'Mire Lone Shooter ES']), create_dungeon_region(player, 'Mire Failure Bridge', 'Misery Mire', None, ['Mire Failure Bridge W', 'Mire Failure Bridge E']), - create_dungeon_region(player, 'Mire Falling Bridge', 'Misery Mire', ['Misery Mire - Big Chest'], ['Mire Falling Bridge WS', 'Mire Falling Bridge W', 'Mire Falling Bridge WN']), + create_dungeon_region(player, 'Mire Falling Bridge - Failure', 'Misery Mire', None, + ['Mire Falling Bridge W', 'Mire Falling Bridge Hook Only Path', 'Mire Falling Bridge Primary Path']), + create_dungeon_region(player, 'Mire Falling Bridge - Primary', 'Misery Mire', None, + ['Mire Falling Bridge WS', 'Mire Falling Bridge Hook Path', 'Mire Falling Bridge Failure Path']), + create_dungeon_region(player, 'Mire Falling Bridge - Chest', 'Misery Mire', ['Misery Mire - Big Chest'], + ['Mire Falling Bridge WN']), create_dungeon_region(player, 'Mire Map Spike Side', 'Misery Mire', None, ['Mire Map Spike Side EN', 'Mire Map Spike Side Drop Down', 'Mire Map Spike Side Blue Barrier']), create_dungeon_region(player, 'Mire Map Spot', 'Misery Mire', ['Misery Mire - Map Chest'], ['Mire Map Spot WN', 'Mire Map Spot Blue Barrier']), create_dungeon_region(player, 'Mire Crystal Dead End', 'Misery Mire', None, ['Mire Crystal Dead End Left Barrier', 'Mire Crystal Dead End Right Barrier', 'Mire Crystal Dead End NW']), diff --git a/Rules.py b/Rules.py index b86a7345..a0d78863 100644 --- a/Rules.py +++ b/Rules.py @@ -314,7 +314,8 @@ def global_rules(world, player): set_rule(world.get_entrance('Mire Lobby Gap', player), lambda state: state.has_Boots(player) or state.has('Hookshot', player)) set_rule(world.get_entrance('Mire Post-Gap Gap', player), lambda state: state.has_Boots(player) or state.has('Hookshot', player)) - set_rule(world.get_entrance('Mire Falling Bridge WN', player), lambda state: state.has_Boots(player) or state.has('Hookshot', player)) # this is due to the fact the the door opposite is blocked + set_rule(world.get_entrance('Mire Falling Bridge Hook Path', player), lambda state: state.has_Boots(player) or state.has('Hookshot', player)) + set_rule(world.get_entrance('Mire Falling Bridge Hook Only Path', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('Mire 2 NE', player), lambda state: state.has_sword(player) or (state.has('Fire Rod', player) and (state.can_use_bombs(player) or state.can_extend_magic(player, 9))) or # 9 fr shots or 8 with some bombs (state.has('Ice Rod', player) and state.can_use_bombs(player)) or # freeze popo and throw, bomb to finish @@ -2021,9 +2022,9 @@ bunny_impassible_doors = { 'Ice Backwards Room Hole', 'Ice Switch Room SE', 'Ice Antechamber NE', 'Ice Antechamber Hole', 'Mire Lobby Gap', 'Mire Post-Gap Gap', 'Mire 2 NE', 'Mire Hub Upper Blue Barrier', 'Mire Hub Lower Blue Barrier', 'Mire Hub Right Blue Barrier', 'Mire Hub Top Blue Barrier', 'Mire Hub Switch Blue Barrier N', - 'Mire Hub Switch Blue Barrier S', 'Mire Falling Bridge WN', 'Mire Map Spike Side Blue Barrier', - 'Mire Map Spot Blue Barrier', 'Mire Crystal Dead End Left Barrier', 'Mire Crystal Dead End Right Barrier', - 'Mire Cross ES', 'Mire Left Bridge Hook Path', 'Mire Fishbone Blue Barrier', + 'Mire Hub Switch Blue Barrier S', 'Mire Falling Bridge Hook Path', 'Mire Falling Bridge Hook Only Path', + 'Mire Map Spike Side Blue Barrier', 'Mire Map Spot Blue Barrier', 'Mire Crystal Dead End Left Barrier', + 'Mire Crystal Dead End Right Barrier', 'Mire Cross ES', 'Mire Left Bridge Hook Path', 'Mire Fishbone Blue Barrier', 'Mire South Fish Blue Barrier', 'Mire Tile Room NW', 'Mire Compass Blue Barrier', 'Mire Attic Hint Hole', 'Mire Dark Shooters SW', 'Mire Crystal Mid Blue Barrier', 'Mire Crystal Left Blue Barrier', 'TR Main Lobby Gap', 'TR Lobby Ledge Gap', 'TR Hub SW', 'TR Hub SE', 'TR Hub ES', 'TR Hub EN', 'TR Hub NW', 'TR Hub NE', 'TR Hub Path', From 32305a2202c28ca80d9bb3b0d28fc0bbf7c2d4e7 Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 24 Aug 2022 12:53:04 -0600 Subject: [PATCH 35/63] Copy world needs to copy location type --- Main.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Main.py b/Main.py index 1f9d7066..7c93eaf8 100644 --- a/Main.py +++ b/Main.py @@ -584,11 +584,7 @@ def copy_dynamic_regions_and_locations(world, ret): for location in world.dynamic_locations: new_reg = ret.get_region(location.parent_region.name, location.parent_region.player) new_loc = Location(location.player, location.name, location.address, location.crystal, location.hint_text, new_reg) - # todo: this is potentially dangerous. later refactor so we - # can apply dynamic region rules on top of copied world like other rules - new_loc.access_rule = location.access_rule - new_loc.always_allow = location.always_allow - new_loc.item_rule = location.item_rule + new_loc.type = location.type new_reg.locations.append(new_loc) ret.clear_location_cache() From 262b36cac66d3d47f4f098ace26d1663ad0b60fc Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 24 Aug 2022 13:10:32 -0600 Subject: [PATCH 36/63] Custom yaml properly record interior doors that are key doors --- 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 03a203f9..dd99853e 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -306,7 +306,7 @@ class CustomSettings(object): 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: + if door.name in door_kinds: door_value = {'type': door_kinds[door.name]} door_map[door.name] = door_value # intra-tile note skip.add(door.dest) From 6b369b68b7885471b73b4eea04f9488d250b70ff Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 24 Aug 2022 13:29:14 -0600 Subject: [PATCH 37/63] Update lite/lean with pottery options. Houses and caves with pots are shuffled in the ER pool if the pottery setting turns that on. --- source/overworld/EntranceShuffle2.py | 63 +++++++++++++++++++--------- 1 file changed, 43 insertions(+), 20 deletions(-) diff --git a/source/overworld/EntranceShuffle2.py b/source/overworld/EntranceShuffle2.py index cb176aba..da454795 100644 --- a/source/overworld/EntranceShuffle2.py +++ b/source/overworld/EntranceShuffle2.py @@ -770,6 +770,9 @@ def do_vanilla_connect(pool_def, avail): if pool_def['condition'] == 'shopsanity': if avail.world.shopsanity[avail.player]: return + elif pool_def['condition'] == 'pottery': # this condition involves whether caves with pots are shuffled or not + if avail.world.pottery[avail.player] not in ['none', 'keys', 'dungeon']: + return defaults = inverted_default_connections if avail.inverted else default_connections for entrance in pool_def['entrances']: if entrance in avail.entrances: @@ -1188,16 +1191,13 @@ modes = { 'fixed_non_items': { 'special': 'vanilla', 'condition': '', - 'entrances': ['Dark Death Mountain Fairy', 'Dark Desert Fairy', 'Dark Desert Hint', 'Archery Game', + 'entrances': ['Dark Death Mountain Fairy', 'Dark Desert Fairy', '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'], + 'Dark Lake Hylia Ledge Hint', 'Dark Lake Hylia Ledge Fairy', 'Dark Lake Hylia Fairy', + 'Dark Lake Hylia Shop', 'East Dark World Hint', 'Kakariko Gamble Game', 'Good Bee Cave', + 'Long Fairy Cave', 'Bush Covered House', 'Fortune Teller (Light)', 'Lost Woods Gamble', + 'Desert Fairy', 'Light Hype Fairy', 'Lake Hylia Fortune Teller', 'Lake Hylia Fairy', + 'Bonk Fairy (Light)', 'Inverted Dark Sanctuary'], }, 'fixed_shops': { 'special': 'vanilla', @@ -1206,7 +1206,16 @@ modes = { '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 + 'fixed_pottery': { + 'special': 'vanilla', + 'condition': 'pottery', + 'entrances': ['Lumberjack House', 'Snitch Lady (West)', 'Snitch Lady (East)', 'Tavern (Front)', + 'Light World Bomb Hut', '20 Rupee Cave', '50 Rupee Cave', 'Hookshot Fairy', + 'Palace of Darkness Hint', 'Dark Lake Hylia Ledge Spike Cave', + 'Dark Desert Hint'] + + }, + 'item_caves': { # shuffles shops/pottery if they weren't fixed in the last steps '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', @@ -1215,6 +1224,10 @@ modes = { '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)', + 'Lumberjack House', 'Snitch Lady (West)', 'Snitch Lady (East)', 'Tavern (Front)', + 'Light World Bomb Hut', '20 Rupee Cave', '50 Rupee Cave', 'Hookshot Fairy', + 'Palace of Darkness Hint', 'Dark Lake Hylia Ledge Spike Cave', + 'Dark Desert Hint', 'Links House', 'Inverted Links House', 'Tavern North'] }, 'old_man_cave': { # have to do old man cave first so lw dungeon don't use up everything @@ -1254,16 +1267,13 @@ modes = { 'fixed_non_items': { 'special': 'vanilla', 'condition': '', - 'entrances': ['Dark Death Mountain Fairy', 'Dark Desert Fairy', 'Dark Desert Hint', 'Archery Game', + 'entrances': ['Dark Death Mountain Fairy', 'Dark Desert Fairy', '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'], + 'Dark Lake Hylia Ledge Hint', 'Dark Lake Hylia Ledge Fairy', 'Dark Lake Hylia Fairy', + 'Dark Lake Hylia Shop', 'East Dark World Hint', 'Kakariko Gamble Game', 'Good Bee Cave', + 'Long Fairy Cave', 'Bush Covered House', 'Fortune Teller (Light)', 'Lost Woods Gamble', + 'Desert Fairy', 'Light Hype Fairy', 'Lake Hylia Fortune Teller', 'Lake Hylia Fairy', + 'Bonk Fairy (Light)', 'Inverted Dark Sanctuary'], }, 'fixed_shops': { 'special': 'vanilla', @@ -1272,7 +1282,16 @@ modes = { '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 + 'fixed_pottery': { + 'special': 'vanilla', + 'condition': 'pottery', + 'entrances': ['Lumberjack House', 'Snitch Lady (West)', 'Snitch Lady (East)', 'Tavern (Front)', + 'Light World Bomb Hut', '20 Rupee Cave', '50 Rupee Cave', 'Hookshot Fairy', + 'Palace of Darkness Hint', 'Dark Lake Hylia Ledge Spike Cave', + 'Dark Desert Hint'] + + }, + 'item_caves': { # shuffles shops/pottery if they weren't fixed in the last steps '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', @@ -1281,6 +1300,10 @@ modes = { '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)', + 'Lumberjack House', 'Snitch Lady (West)', 'Snitch Lady (East)', 'Tavern (Front)', + 'Light World Bomb Hut', '20 Rupee Cave', '50 Rupee Cave', 'Hookshot Fairy', + 'Palace of Darkness Hint', 'Dark Lake Hylia Ledge Spike Cave', + 'Dark Desert Hint', 'Links House', 'Inverted Links House', 'Tavern North'] } } From 16d7ce5e76c5c55f1e1a029bc41bafc1d85f1125 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 25 Aug 2022 08:59:44 -0600 Subject: [PATCH 38/63] Minor issue with new Mire Falling Bridge and a miss in dungeon gen for dungeon pools. --- DoorShuffle.py | 2 +- Doors.py | 4 +--- DungeonGenerator.py | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index 31744208..b6e264af 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -3595,6 +3595,7 @@ logical_connections = [ ('Mire Hub Switch Blue Barrier S', 'Mire Hub'), ('Mire Falling Bridge Hook Path', 'Mire Falling Bridge - Chest'), ('Mire Falling Bridge Hook Only Path', 'Mire Falling Bridge - Chest'), + ('Mire Falling Bridge Failure Path', 'Mire Falling Bridge - Failure'), ('Mire Map Spike Side Drop Down', 'Mire Lone Shooter'), ('Mire Map Spike Side Blue Barrier', 'Mire Crystal Dead End'), ('Mire Map Spot Blue Barrier', 'Mire Crystal Dead End'), @@ -3728,7 +3729,6 @@ vanilla_logical_connections = [ ('Ice Cross Bottom Push Block Right', 'Ice Pengator Switch'), ('Ice Cross Top Push Block Right', 'Ice Pengator Switch'), ('Mire Falling Bridge Primary Path', 'Mire Lone Shooter'), - ('Mire Falling Bridge Failure Path', 'Mire Failure Bridge'), ] spiral_staircases = [ diff --git a/Doors.py b/Doors.py index 5a205f1c..c80ea51a 100644 --- a/Doors.py +++ b/Doors.py @@ -850,7 +850,7 @@ def create_doors(world, player): create_door(player, 'Mire Falling Bridge Hook Path', Lgcl), create_door(player, 'Mire Falling Bridge Hook Only Path', Lgcl), create_door(player, 'Mire Falling Bridge Primary Path', Lgcl), # dynamic - create_door(player, 'Mire Falling Bridge Failure Path', Lgcl), # dynamic + create_door(player, 'Mire Falling Bridge Failure Path', Lgcl), create_door(player, 'Mire Map Spike Side EN', Intr).dir(Ea, 0xc3, Top, High).no_exit().trap(0x2).pos(1), create_door(player, 'Mire Map Spot WN', Nrml).dir(We, 0xc3, Top, High).small_key().pos(0), create_door(player, 'Mire Crystal Dead End NW', Nrml).dir(No, 0xc3, Left, High).pos(4), @@ -1481,9 +1481,7 @@ def create_doors(world, player): controller_door(south_controller, world.get_door('Ice Cross Top Push Block Bottom', player)) controller_door(east_controller, world.get_door('Ice Cross Bottom Push Block Right', player)) controller_door(east_controller, world.get_door('Ice Cross Top Push Block Right', player)) - failure_controller = world.get_door('Mire Falling Bridge W', player) primary_controller = world.get_door('Mire Falling Bridge WS', player) - controller_door(failure_controller, world.get_door('Mire Falling Bridge Failure Path', player)) controller_door(primary_controller, world.get_door('Mire Falling Bridge Primary Path', player)) assign_entrances(world, player) diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 055a7252..4404d53f 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -1346,8 +1346,8 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, dunge sanc = find_sector('Sanctuary', candidate_sectors) if sanc: # only run if sanc if a candidate lw_builders = [] - for name, portal_list in dungeon_portals.items(): - for portal_name in portal_list: + for name in dungeon_pool: + for portal_name in dungeon_portals[name]: if world.get_portal(portal_name, player).light_world: lw_builders.append(dungeon_map[name]) break From af4f8e5b4b141cac36e175754f315de9340ff5fa Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 1 Sep 2022 13:59:26 -0600 Subject: [PATCH 39/63] Flute mode And some odd fixes --- BaseClasses.py | 17 ++++++++-- CLI.py | 2 ++ DoorShuffle.py | 11 ++++--- InitialSram.py | 1 + ItemList.py | 9 ++++-- Items.py | 1 + Main.py | 6 +++- Rom.py | 5 +-- Rules.py | 30 +++++++++--------- data/base2current.bps | Bin 93382 -> 93432 bytes mystery_example.yml | 3 ++ resources/app/cli/args.json | 6 ++++ resources/app/cli/lang/en.json | 5 +++ resources/app/gui/lang/en.json | 12 ++----- resources/app/gui/randomize/item/widgets.json | 13 ++------ source/classes/CustomSettings.py | 2 ++ source/classes/constants.py | 3 +- source/item/FillUtil.py | 3 +- source/tools/MysteryUtils.py | 1 + 19 files changed, 79 insertions(+), 51 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 25cee608..8ff91295 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -895,7 +895,7 @@ class CollectionState(object): 'Golden Sword', 'Progressive Sword', 'Progressive Glove', 'Silver Arrows', 'Green Pendant', 'Blue Pendant', 'Red Pendant', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7', 'Blue Boomerang', 'Red Boomerang', 'Blue Shield', 'Red Shield', - 'Mirror Shield', 'Progressive Shield', 'Bug Catching Net', 'Cane of Byrna', + 'Mirror Shield', 'Progressive Shield', 'Bug Catching Net', 'Cane of Byrna', 'Ocarina (Activated)', 'Boss Heart Container', 'Sanctuary Heart Container', 'Piece of Heart', 'Magic Upgrade (1/2)', 'Magic Upgrade (1/4)'] or item_name.startswith(('Bottle', 'Small Key', 'Big Key')) @@ -1129,8 +1129,11 @@ class CollectionState(object): return self.has('Fire Rod', player) or self.has('Lamp', player) def can_flute(self, player): + if any(map(lambda i: i.name in ['Ocarina', 'Ocarina (Activated)'], self.world.precollected_items)): + return True lw = self.world.get_region('Light World', player) - return self.has('Ocarina', player) and lw.can_reach(self) and self.is_not_bunny(lw, player) + return self.has('Ocarina (Activated)', player) or (self.has('Ocarina', player) and lw.can_reach(self) + and self.is_not_bunny(lw, player)) def can_melt_things(self, player): return self.has('Fire Rod', player) or (self.has('Bombos', player) and self.has_sword(player)) @@ -2371,6 +2374,7 @@ class Spoiler(object): 'retro': self.world.retro, 'bombbag': self.world.bombbag, 'weapons': self.world.swords, + 'flute_mode': self.world.flute_mode, 'goal': self.world.goal, 'shuffle': self.world.shuffle, 'shuffleganon': self.world.shuffle_ganon, @@ -2569,6 +2573,7 @@ class Spoiler(object): outfile.write(f"Restricted Boss Items: {self.metadata['restricted_boss_items'][player]}\n") outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][player]) outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'][player]) + outfile.write(f"Flute Mode: {self.metadata['flute_mode'][player]}\n") outfile.write(f"Shopsanity: {yn(self.metadata['shopsanity'][player])}\n") outfile.write(f"Bombbag: {yn(self.metadata['bombbag'][player])}\n") outfile.write(f"Pseudoboots: {yn(self.metadata['pseudoboots'][player])}\n") @@ -2873,6 +2878,8 @@ boss_mode = {"none": 0, "simple": 1, "full": 2, "chaos": 3, 'random': 3, 'unique # byte 10: settings_version +# byte 11: F???, ???? (flute_mode) +flute_mode = {'normal': 0, 'active': 1} # additions # psuedoboots does not effect code @@ -2917,7 +2924,9 @@ class Settings(object): (rb_mode[w.restrict_boss_items[p]] << 6) | (algo_mode[w.algorithm] << 3) | (boss_mode[w.boss_shuffle[p]]), - settings_version]) + settings_version, + + flute_mode[w.flute_mode[p]] << 7]) return base64.b64encode(code, "+-".encode()).decode() @staticmethod @@ -2979,6 +2988,8 @@ class Settings(object): args.restrict_boss_items[p] = r(rb_mode)[(settings[9] & 0xC0) >> 6] args.algorithm = r(algo_mode)[(settings[9] & 0x38) >> 3] args.shufflebosses[p] = r(boss_mode)[(settings[9] & 0x07)] + if len(settings) > 11: + args.flute_mode[p] = r(flute_mode)[(settings[11] & 0x80) >> 7] class KeyRuleType(FastEnum): diff --git a/CLI.py b/CLI.py index 987652fd..44c7b76c 100644 --- a/CLI.py +++ b/CLI.py @@ -115,6 +115,7 @@ def parse_cli(argv, no_defaults=False): playerargs = parse_cli(shlex.split(getattr(ret, f"p{player}")), True) for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', + 'flute_mode', 'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'openpyramid', 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory', 'usestartinventory', 'bombbag', 'overworld_map', 'restrict_boss_items', @@ -156,6 +157,7 @@ def parse_settings(): "crystals_gt": "7", "crystals_ganon": "7", "swords": "random", + 'flute_mode': 'normal', "difficulty": "normal", "item_functionality": "normal", "timer": "none", diff --git a/DoorShuffle.py b/DoorShuffle.py index 3ced8a44..89463230 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -834,7 +834,7 @@ def main_dungeon_pool(dungeon_pool, world, player): for name in pool: builder = world.dungeon_layouts[player][name] region_set = builder.master_sector.region_set() - builder.bk_required = len(builder.bk_door_proposal) > 0 or any(x in region_set for x in special_bk_regions) + builder.bk_required = builder.bk_door_proposal or any(x in region_set for x in special_bk_regions) dungeon = world.get_dungeon(name, player) if not builder.bk_required or builder.bk_provided: dungeon.big_key = None @@ -1793,7 +1793,7 @@ def shuffle_trap_doors(door_type_pools, paths, start_regions_map, world, player) remaining -= len(custom_trap_doors[dungeon]) ttl += len(builder.candidates.trap) if ttl == 0: - return used_doors + continue for dungeon in pool: builder = world.dungeon_layouts[player][dungeon] proportion = len(builder.candidates.trap) @@ -1853,7 +1853,7 @@ def shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world, remaining -= len(custom_bk_doors[dungeon]) ttl += len(builder.candidates.big) if ttl == 0: - return used_doors + continue for dungeon in pool: builder = world.dungeon_layouts[player][dungeon] proportion = len(builder.candidates.big) @@ -2004,7 +2004,7 @@ def shuffle_bomb_dash_doors(door_type_pools, used_doors, start_regions_map, worl remaining_dash -= len(custom_dash_doors[dungeon]) ttl += len(builder.candidates.bomb_dash) if ttl == 0: - return used_doors + continue for dungeon in pool: builder = world.dungeon_layouts[player][dungeon] proportion = len(builder.candidates.bomb_dash) @@ -2508,8 +2508,9 @@ def find_small_key_door_candidates(builder, start_regions, used, world, player): def calc_used_dungeon_items(builder, world, player): - base = max(count_reserved_locations(world, player, builder.location_set), 2) basic_flag = world.doorShuffle[player] == 'basic' + base = 0 if basic_flag else 2 # at least 2 items per dungeon, except in basic + base = max(count_reserved_locations(world, player, builder.location_set), base) if not world.bigkeyshuffle[player]: if builder.bk_required and not builder.bk_provided: base += 1 diff --git a/InitialSram.py b/InitialSram.py index 772e1d46..63c822cd 100644 --- a/InitialSram.py +++ b/InitialSram.py @@ -144,6 +144,7 @@ class InitialSram: 'Big Key (Ganons Tower)': (0x366, 0x04), 'Compass (Ganons Tower)': (0x364, 0x04), 'Map (Ganons Tower)': (0x368, 0x04)} set_or_table = {'Flippers': (0x356, 1, 0x379, 0x02),'Pegasus Boots': (0x355, 1, 0x379, 0x04), 'Shovel': (0x34C, 1, 0x38C, 0x04), 'Ocarina': (0x34C, 3, 0x38C, 0x01), + 'Ocarina (Activated)': (0x34C, 3, 0x38C, 0x01), 'Mushroom': (0x344, 1, 0x38C, 0x20 | 0x08), 'Magic Powder': (0x344, 2, 0x38C, 0x10), 'Blue Boomerang': (0x341, 1, 0x38C, 0x80), 'Red Boomerang': (0x341, 2, 0x38C, 0x40)} keys = {'Small Key (Eastern Palace)': [0x37E], 'Small Key (Desert Palace)': [0x37F], diff --git a/ItemList.py b/ItemList.py index f2500e07..1dffb859 100644 --- a/ItemList.py +++ b/ItemList.py @@ -267,7 +267,7 @@ def generate_itempool(world, player): (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]) + (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], world.flute_mode[player] == 'active') if player in world.pool_adjustment.keys() and not skip_pool_adjustments: amt = world.pool_adjustment[player] @@ -789,7 +789,8 @@ def add_pot_contents(world, player): world.itempool.append(ItemFactory(item, player)) -def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, goal, mode, swords, retro, bombbag, door_shuffle, logic): +def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, goal, mode, swords, retro, bombbag, + door_shuffle, logic, flute_activated): pool = [] placed_items = {} precollected_items = [] @@ -802,6 +803,10 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, pool.extend(alwaysitems) + if flute_activated: + pool.remove('Ocarina') + pool.append('Ocarina (Activated)') + def place_item(loc, item): assert loc not in placed_items placed_items[loc] = item diff --git a/Items.py b/Items.py index ef88991f..da9d7385 100644 --- a/Items.py +++ b/Items.py @@ -30,6 +30,7 @@ item_table = {'Bow': (True, False, None, 0x0B, 200, 'You have\nchosen the\narche 'Hookshot': (True, False, None, 0x0A, 250, 'BOING!!!\nBOING!!!\nBOING!!!', 'and the tickle beam', 'tickle-monster kid', 'tickle beam for sale', 'witch and tickle boy', 'beam boy tickles again', 'the Hookshot'), 'Magic Mirror': (True, False, None, 0x1A, 250, 'Isn\'t your\nreflection so\npretty?', 'the face reflector', 'the narcissistic kid', 'your face for sale', 'trades looking-glass', 'narcissistic boy is happy again', 'the mirror'), 'Ocarina': (True, False, None, 0x14, 250, 'Save the duck\nand fly to\nfreedom!', 'and the duck call', 'the duck-call kid', 'duck call for sale', 'duck-calls for trade', 'ocarina boy plays again', 'the flute'), + 'Ocarina (Activated)': (True, False, None, 0x4A, 250, 'Save the duck\nand fly to\nfreedom!', 'and the duck call', 'the duck-call kid', 'duck call for sale', 'duck-calls for trade', 'ocarina boy plays again', 'the flute'), 'Pegasus Boots': (True, False, None, 0x4B, 250, 'Gotta go fast!', 'and the sprint shoes', 'the running-man kid', 'sprint shoe for sale', 'shrooms for speed', 'gotta-go-fast boy runs again', 'the boots'), 'Power Glove': (True, False, None, 0x1B, 100, 'Now you can\nlift weak\nstuff!', 'and the grey mittens', 'body-building kid', 'lift glove for sale', 'fungus for gloves', 'body-building boy lifts again', 'the Glove'), 'Cape': (True, False, None, 0x19, 50, 'Wear this to\nbecome\ninvisible!', 'the camouflage cape', 'red riding-hood kid', 'red hood for sale', 'hood from a hood', 'dapper boy hides again', 'the cape'), diff --git a/Main.py b/Main.py index e63a5ec7..1c6cc442 100644 --- a/Main.py +++ b/Main.py @@ -95,6 +95,7 @@ def main(args, seed=None, fish=None): world.keyshuffle = args.keyshuffle.copy() world.bigkeyshuffle = args.bigkeyshuffle.copy() world.bombbag = args.bombbag.copy() + world.flute_mode = args.flute_mode.copy() world.crystals_needed_for_ganon = {player: random.randint(0, 7) if args.crystals_ganon[player] == 'random' else int(args.crystals_ganon[player]) for player in range(1, world.players + 1)} world.crystals_needed_for_gt = {player: random.randint(0, 7) if args.crystals_gt[player] == 'random' else int(args.crystals_gt[player]) for player in range(1, world.players + 1)} world.crystals_ganon_orig = args.crystals_ganon.copy() @@ -158,7 +159,9 @@ def main(args, seed=None, fish=None): if args.usestartinventory[player]: for tok in filter(None, args.startinventory[player].split(',')): - item = ItemFactory(tok.strip(), player) + name = tok.strip() + name = name if name != 'Ocarina' or world.flute_mode[player] != 'active' else 'Ocarina (Activated)' + item = ItemFactory(name, player) if item: world.push_precollected(item) @@ -451,6 +454,7 @@ def copy_world(world): ret.keyshuffle = world.keyshuffle.copy() ret.bigkeyshuffle = world.bigkeyshuffle.copy() ret.bombbag = world.bombbag.copy() + ret.flute_mode = world.flute_mode.copy() ret.crystals_needed_for_ganon = world.crystals_needed_for_ganon.copy() ret.crystals_needed_for_gt = world.crystals_needed_for_gt.copy() ret.crystals_ganon_orig = world.crystals_ganon_orig.copy() diff --git a/Rom.py b/Rom.py index ef640da5..458e24df 100644 --- a/Rom.py +++ b/Rom.py @@ -37,7 +37,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '7b877dcee4ece38713768b74acb333a6' +RANDOMIZERBASEHASH = '0be31dc5cb338e7e85d1ce65e839c99e' class JsonRom(object): @@ -2161,8 +2161,8 @@ def write_strings(rom, world, player, team): 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) if this_location: + random.shuffle(this_location) item_name = this_location[0].item.hint_text item_name = item_name[0].upper() + item_name[1:] this_hint = f'{item_name} can be found {hint_text(this_location[0])}.' @@ -2847,6 +2847,7 @@ RelevantItems = ['Bow', 'Hookshot', 'Magic Mirror', 'Ocarina', + 'Ocarina (Activated)', 'Pegasus Boots', 'Power Glove', 'Cape', diff --git a/Rules.py b/Rules.py index d12e7341..9d3dad12 100644 --- a/Rules.py +++ b/Rules.py @@ -786,9 +786,9 @@ def default_rules(world, player): set_rule(world.get_entrance('50 Rupee Cave', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Death Mountain Entrance Rock', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Bumper Cave Entrance Mirror Spot', player), lambda state: state.has_Mirror(player)) - set_rule(world.get_entrance('Flute Spot 1', player), lambda state: state.has('Ocarina', player)) + set_rule(world.get_entrance('Flute Spot 1', player), lambda state: state.can_flute(player)) set_rule(world.get_entrance('Lake Hylia Central Island Teleporter', player), lambda state: state.can_lift_heavy_rocks(player)) - set_rule(world.get_entrance('Dark Desert Teleporter', player), lambda state: state.has('Ocarina', player) and state.can_lift_heavy_rocks(player)) + set_rule(world.get_entrance('Dark Desert Teleporter', player), lambda state: state.can_flute(player) and state.can_lift_heavy_rocks(player)) set_rule(world.get_entrance('East Hyrule Teleporter', player), lambda state: state.has('Hammer', player) and state.can_lift_rocks(player) and state.has_Pearl(player)) # bunny cannot use hammer set_rule(world.get_entrance('South Hyrule Teleporter', player), lambda state: state.has('Hammer', player) and state.can_lift_rocks(player) and state.has_Pearl(player)) # bunny cannot use hammer set_rule(world.get_entrance('Kakariko Teleporter', player), lambda state: ((state.has('Hammer', player) and state.can_lift_rocks(player)) or state.can_lift_heavy_rocks(player)) and state.has_Pearl(player)) # bunny cannot lift bushes @@ -1514,7 +1514,7 @@ def set_big_bomb_rules(world, player): #2. Mirror and Flute and basic routes (can make difference if accessed via insanity or w/ mirror from connector, and then via hyrule castle gate, because no gloves are needed in that case) #3. Go to south DW and then cross peg bridge: Need Mitts and hammer and moon pearl # -> (Mitts and CPB) or (((G or Flute) and M) and BR)) - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.can_lift_heavy_rocks(player) and cross_peg_bridge(state)) or (((state.can_lift_rocks(player) or state.has('Ocarina', player)) and state.has_Mirror(player)) and basic_routes(state))) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.can_lift_heavy_rocks(player) and cross_peg_bridge(state)) or (((state.can_lift_rocks(player) or state.can_flute(player)) and state.has_Mirror(player)) and basic_routes(state))) elif bombshop_entrance.name in Southern_DW_entrances: #1. Mirror and enter via gate: Need mirror and Aga1 #2. cross peg bridge: Need hammer and moon pearl @@ -1523,52 +1523,52 @@ def set_big_bomb_rules(world, player): elif bombshop_entrance.name in Isolated_DW_entrances: # 1. mirror then flute then basic routes # -> M and Flute and BR - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has_Mirror(player) and state.has('Ocarina', player) and basic_routes(state)) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has_Mirror(player) and state.can_flute(player) and basic_routes(state)) elif bombshop_entrance.name in Isolated_LW_entrances: # 1. flute then basic routes # Prexisting mirror spot is not permitted, because mirror might have been needed to reach these isolated locations. # -> Flute and BR - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Ocarina', player) and basic_routes(state)) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.can_flute(player) and basic_routes(state)) elif bombshop_entrance.name in West_LW_DM_entrances: # 1. flute then basic routes or mirror # Prexisting mirror spot is permitted, because flute can be used to reach west DM directly. # -> Flute and (M or BR) - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Ocarina', player) and (state.has_Mirror(player) or basic_routes(state))) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.can_flute(player) and (state.has_Mirror(player) or basic_routes(state))) elif bombshop_entrance.name in East_LW_DM_entrances: # 1. flute then basic routes or mirror and hookshot # Prexisting mirror spot is permitted, because flute can be used to reach west DM directly and then east DM via Hookshot # -> Flute and ((M and Hookshot) or BR) - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Ocarina', player) and ((state.has_Mirror(player) and state.has('Hookshot', player)) or basic_routes(state))) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.can_flute(player) and ((state.has_Mirror(player) and state.has('Hookshot', player)) or basic_routes(state))) elif bombshop_entrance.name == 'Fairy Ascension Cave (Bottom)': # Same as East_LW_DM_entrances except navigation without BR requires Mitts # -> Flute and ((M and Hookshot and Mitts) or BR) - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Ocarina', player) and ((state.has_Mirror(player) and state.has('Hookshot', player) and state.can_lift_heavy_rocks(player)) or basic_routes(state))) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.can_flute(player) and ((state.has_Mirror(player) and state.has('Hookshot', player) and state.can_lift_heavy_rocks(player)) or basic_routes(state))) elif bombshop_entrance.name in Castle_ledge_entrances: # 1. mirror on pyramid to castle ledge, grab bomb, return through mirror spot: Needs mirror # 2. flute then basic routes # -> M or (Flute and BR) - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has_Mirror(player) or (state.has('Ocarina', player) and basic_routes(state))) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has_Mirror(player) or (state.can_flute(player) and basic_routes(state))) elif bombshop_entrance.name in Desert_mirrorable_ledge_entrances: # Cases when you have mire access: Mirror to reach locations, return via mirror spot, move to center of desert, mirror anagin and: # 1. Have mire access, Mirror to reach locations, return via mirror spot, move to center of desert, mirror again and then basic routes # 2. flute then basic routes # -> (Mire access and M) or Flute) and BR - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: ((state.can_reach('Dark Desert', 'Region', player) and state.has_Mirror(player)) or state.has('Ocarina', player)) and basic_routes(state)) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: ((state.can_reach('Dark Desert', 'Region', player) and state.has_Mirror(player)) or state.can_flute(player)) and basic_routes(state)) elif bombshop_entrance.name == 'Old Man Cave (West)': # 1. Lift rock then basic_routes # 2. flute then basic_routes # -> (Flute or G) and BR - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Ocarina', player) or state.can_lift_rocks(player)) and basic_routes(state)) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.can_flute(player) or state.can_lift_rocks(player)) and basic_routes(state)) elif bombshop_entrance.name == 'Graveyard Cave': # 1. flute then basic routes # 2. (has west dark world access) use existing mirror spot (required Pearl), mirror again off ledge # -> (Flute or (M and P and West Dark World access) and BR - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Ocarina', player) or (state.can_reach('West Dark World', 'Region', player) and state.has_Pearl(player) and state.has_Mirror(player))) and basic_routes(state)) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.can_flute(player) or (state.can_reach('West Dark World', 'Region', player) and state.has_Pearl(player) and state.has_Mirror(player))) and basic_routes(state)) elif bombshop_entrance.name in Mirror_from_SDW_entrances: # 1. flute then basic routes # 2. (has South dark world access) use existing mirror spot, mirror again off ledge # -> (Flute or (M and South Dark World access) and BR - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Ocarina', player) or (state.can_reach('South Dark World', 'Region', player) and state.has_Mirror(player))) and basic_routes(state)) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.can_flute(player) or (state.can_reach('South Dark World', 'Region', player) and state.has_Mirror(player))) and basic_routes(state)) elif bombshop_entrance.name == 'Dark World Potion Shop': # 1. walk down by lifting rock: needs gloves and pearl` # 2. walk down by hammering peg: needs hammer and pearl @@ -1580,11 +1580,11 @@ def set_big_bomb_rules(world, player): # (because otherwise mirror was used to reach the grave, so would cancel a pre-existing mirror spot) # to account for insanity, must consider a way to escape without a cave for basic_routes # -> (M and Mitts) or ((Mitts or Flute or (M and P and West Dark World access)) and BR) - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.can_lift_heavy_rocks(player) and state.has_Mirror(player)) or ((state.can_lift_heavy_rocks(player) or state.has('Ocarina', player) or (state.can_reach('West Dark World', 'Region', player) and state.has_Pearl(player) and state.has_Mirror(player))) and basic_routes(state))) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.can_lift_heavy_rocks(player) and state.has_Mirror(player)) or ((state.can_lift_heavy_rocks(player) or state.can_flute(player) or (state.can_reach('West Dark World', 'Region', player) and state.has_Pearl(player) and state.has_Mirror(player))) and basic_routes(state))) elif bombshop_entrance.name == 'Waterfall of Wishing': # same as the Normal_LW_entrances case except in insanity it's possible you could be here without Flippers which # means you need an escape route of either Flippers or Flute - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Flippers', player) or state.has('Ocarina', player)) and (basic_routes(state) or state.has_Mirror(player))) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Flippers', player) or state.can_flute(player)) and (basic_routes(state) or state.has_Mirror(player))) def set_inverted_big_bomb_rules(world, player): diff --git a/data/base2current.bps b/data/base2current.bps index 2137836da0324c77a1c4a6f9b2adf4d3e253c105..9740c94d9fb0f54c63443282dd8a82eff36b7240 100644 GIT binary patch delta 2139 zcmW-iYgAKL7RPgLo`m;1C}6l~LBtWO1Bg;1j}|2$RjP~zWYrfEbs?fW4fh5k1`=Yj z4Jky(EstVES8&~Bu_Fvtd}X>iO;sunXQt{X8m$CsGmf*ynd^M{|IYrh&)NUI_c}Kx zxNj!714W4AvBOQl=cpd#z$5f6x(_|rcW4aC*itkE!|dmn@ELuU!$Ih4xWHZGw|AU! zI!jK%;h5%yDkEME|KeWspPiy!s#iayMC9NG)pa%ZDO}-Y27djNN>Fy-i9M7HD|W`K zw0I!|@-vVP3i!?FG5mwS1Y7g~`~}+h*-2_Ysl~Zd)PNTMPg75p$%qfBdz?jN}?}d%pjbH24KE0Tp*sNp0#GCkg!&`!b3GhIr9iT zeg$fU(SmFj6;B#*J$xlx>ciZKPrRf~wRJLweeh8D0eS@j&!muXy=)a@N}1A#mp)<4 zId9f!@d3#791ZP|oxR)R#5azc(wAF$RF0;6!s3%+0kKy+x=QDGtze@+(WLJk_Oi#b z=G3aT?#Q^t%W9lGHyX}qtsqqRUo5=>wH9w^>5-5m@l20jwo%2`2M^L@pbLf8{gvW0 z=<%G%9%lyQ9uwv{HUD)qAB=ae&+d?<6l zNJtFoqNhWQANY(sp^8OKYL}e~YE<_@Q|i8r-!oNt5o*B%HK2YqPL(xj@zx&d9+ZdA z)iK5X?foQ_F^?QJC6i@@;h;!ea7KN@PE`@QDxKhrnsH0gr@myTSQ@6a4F_dDHt4oBK+2IQEG7pEk3{qTJ~1QwR`n1Xj}-9|Bu`*|47@l}=ec;w&N+or#j@3t z3D?99-X)fJ=B2!sC60e3ACZhI@cwt01ugmwsuJC1l;j3hlH2UTP{7cip&!FAh9ZVh z48;r=GmK@poM8gPRSc6Eu45=ka+{X!@t*APbFrl}FKsSU$}af1z730i1+_%H&!I%O z_6wr4*T5nySt)Rd*d~nL^hhf^|CVo7Kp43OTP1<5WaP&OXU}!o&On~j-PL1DmAX|2 zXWKhfkHqHu^=?(bB|}61_C2JrxR=$(8leM9N!7Ikb9$~@b#>PK)q6QhUNvDkaD|M- z{_q;6NL;kRO;%czWTj4NiE-=p|2DI?@ZHvz+lnAqc_w6m2k}h?Gk95##PZ%iEh|bU z&L$7TE#*p&^j|gwJ>LAol0KT#Ao2A%^pqlku8D1Rw9j$1lHO$aZn5uYpEq}itQbqD z$g77>??L>~X+fcfhCFU5_JG?%k%GfQ4SCXXR7k%X;$u;rf=!_6;=sj=4*tsoM*DJ9P2aK)vUy0VvE*gb$G@jIo`Q9R-p|f)cy=G7 zm))6UW1B?a`BUuXh!v!+u9hI<-d5F)8J1K(hTJ#}Dep=tHt+i&L^ZA zBSI%XWw&-`RcJc6w#GSy+$uH)R%k9{P70c{`+J)fOI%5&?fLYHpNhjgcZL%BP=~i| zsU?7(oe9BsI~TQ>ieqW-xehO0avlK^1wHWYf-kBt75Si#Fki)Brw_{Fb61vMx09IC$n delta 2082 zcmWmEeN+=y769U zgb=PF3lTE~B1Y8UnL~A}tP1rb+qEZKYgOFs?$Q-COEut1TRdxb`p^BH_s@Or-TU7C z`2qjN1O9L^;<@VmkHCjWg_go3x`u2pfPIAqp`5Eg?J&xHiv8^|bBf19=qQ}!Cy5j8 z(=J!(Q8*OQ7DgHIO!$R=e&P5-dRnt!ik6TU-l4wK+BB4OghDPAuf)&_L4`HED7d<{fA0T!3y$O0)hySm2Aj4$DFKY%|E^)gK zSF{cM*ML8INW0`M23!Jv7Pbq%chdw9FNa;8@*wnp786Fi^Bzrb_YmFq?FY1*=gK>x z!*{?Jo@@NO>P0;{-K1Df=O{+gA#OUFG~&C#Dq88o?uidg(;eOAGc>*t{wWGaS3%$v?_1Jr#FHMebva|! z>u@#Xd5!xT3Qye`aNVMut$Bo{paoWWN25+=r*|wDbufD0Z@8;QX3WVo-Tl(2mWvdA z_vR8_S0zE=w1pOGba-?7fSe?WCkFhYoi6$ELKM^C=SG3s{!a6n4S3eXfGdry7rV@1 zFSQL!{spKQ-2PXkkDt&yiv|yWY+4fhsm8@tP;Dq&`;>M?lK(0kgRcW3P$e@LU<~&$ zJfce^ts1w3Ueuzgh3@3CYzy05Z$dM1pB~m+y-%07>hP`s`dg?7o^4PY@!{WkU(Oy< zZ`weX6P*Vonu6n+>khhx(AVgN$2Dwq`3`S4k|^!hnHUY>6m+dm`3Lw_MR28iG>}{s z>dX17(c0=Do!+LPW_^sMi^7YKmi1ZzO)!C;}j*HW{sghT82Y9GtaLC;(p7euizDffIFE zeDIqK#)uwlURL7f)|ME6;zM1 zk7HMnF=H~P#|TGuZCYvN$X@@bydocO!iQdC7WC#z2#%XGzZo|NVXP}zqgmIpu47GR zO=3-FO=HbsRj|Itn#WqiTFAPWbx+)!DR=LJiC(drtGG68cbkfLgCEh2z5Wr-5z#*W zadWyq5c{p29Kv!S0iF==i?-%Y>I%=y3(Pnu$Rw;N6t*BEXMp%)*RJ2V?Oy?I7N1v!WHn0 zlwyr9z(d%5GStTd>OF}+rm+t$V^XHH`sp~K zg|bXC5a!kEJnNqw_WL2|>+F+Ee6!rwXUh~#EE<#98yKG>nmmT;{A#)Hn%!-^5*xJIF>d`y@i(vn9kNg{Z4St`u-(vn9l?|LzzqXG=KY-HFp@o`|v!9z(cMu}2?{Y8^GCkZ|t zkHK=qjAh)6!afSpLP?O8%z#TqUa1W!OHH-qZB|E)iv0knkE1kIH!{W7i;+io7SW&P z0b@5P(bgT>_9e-L<^3Iyd(*Z!JzHxKQ^CrN=YemKlzrDwg^Hl;+sp|3Zk1t*d}z8Q z#a92GIdMyZ7KUvmjdly0UTiAvOeffRf77uBg)qN3E^s}H6k1EPg8BS5&GU+u69%z^ z=k)3iWj^?3f{Psvgyiocw{KcY>Sgr=88u&1f46f*?GF&OLQ5+95-UvRz_@nO2FVn; zr9bj5QiBnJ6S>^3{;(=-FW=rW+oZI~%%47bMd>9#a^ir z!Vu_OU|(rD$SiXQU>>RA?WUyVOzLc}w?`p6!h%)InK`B^l;{~S=$`J4i Y%1Belg^&PrCtrAG_|o|;hYrL40YqAt`2YX_ diff --git a/mystery_example.yml b/mystery_example.yml index 34ce9963..78b47a95 100644 --- a/mystery_example.yml +++ b/mystery_example.yml @@ -45,6 +45,9 @@ bombbag: on: 1 off: 4 + flute_mode: + normal: 3 + active: 1 entrance_shuffle: none: 15 dungeonssimple: 3 diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 6e8d0310..d25f542b 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -42,6 +42,12 @@ "vanilla" ] }, + "flute_mode": { + "choices": [ + "normal", + "active" + ] + }, "goal": { "choices": [ "ganon", diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 1bfd124a..eab0e34d 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -121,6 +121,11 @@ "Hard: Reduced functionality.", "Expert: Greatly reduced functionality." ], + "flute_mode": [ + "Determine if you need to wake up the bird or not on flute pickup (default: %(default)s)", + "Normal: Normal functionality.", + "Active: Flute is activated on pickup." + ], "timer": [ "Select game timer setting. Affects available itempool. (default: %(default)s)", "None: No timer.", diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index ef9ffd13..5a6e0a9f 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -285,10 +285,9 @@ "randomizer.item.shopsanity": "Shopsanity", - "randomizer.item.itemfunction": "Item Functionality", - "randomizer.item.itemfunction.normal": "Normal", - "randomizer.item.itemfunction.hard": "Hard", - "randomizer.item.itemfunction.expert": "Expert", + "randomizer.item.flute_mode": "Flute Mode", + "randomizer.item.flute_mode.normal": "Normal", + "randomizer.item.flute_mode.active": "Pre-Activated", "randomizer.item.timer": "Timer Setting", "randomizer.item.timer.none": "No Timer", @@ -298,11 +297,6 @@ "randomizer.item.timer.ohko": "OHKO", "randomizer.item.timer.timed-countdown": "Timed Countdown", - "randomizer.item.progressives": "Progressive Items", - "randomizer.item.progressives.on": "On", - "randomizer.item.progressives.off": "Off", - "randomizer.item.progressives.random": "Random", - "randomizer.item.accessibility": "Accessibility", "randomizer.item.accessibility.items": "100% Inventory", "randomizer.item.accessibility.locations": "100% Locations", diff --git a/resources/app/gui/randomize/item/widgets.json b/resources/app/gui/randomize/item/widgets.json index 76537817..b55d6117 100644 --- a/resources/app/gui/randomize/item/widgets.json +++ b/resources/app/gui/randomize/item/widgets.json @@ -78,12 +78,11 @@ "expert" ] }, - "itemfunction": { + "flute_mode": { "type": "selectbox", "options": [ "normal", - "hard", - "expert" + "active" ] }, "timer": { @@ -97,14 +96,6 @@ "timed-countdown" ] }, - "progressives": { - "type": "selectbox", - "options": [ - "on", - "off", - "random" - ] - }, "accessibility": { "type": "selectbox", "options": [ diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py index 64ef8b3d..82cf0ac9 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -68,6 +68,7 @@ class CustomSettings(object): 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.flute_mode[p] = get_setting(settings['flute_mode'], args.flute_mode[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]) @@ -189,6 +190,7 @@ class CustomSettings(object): settings_dict[p]['logic'] = world.logic[p] settings_dict[p]['mode'] = world.mode[p] settings_dict[p]['swords'] = world.swords[p] + settings_dict[p]['flute_mode'] = world.flute_mode[p] settings_dict[p]['difficulty'] = world.difficulty[p] settings_dict[p]['goal'] = world.goal[p] settings_dict[p]['accessibility'] = world.accessibility[p] diff --git a/source/classes/constants.py b/source/classes/constants.py index 2dccc1fe..3fb8da88 100644 --- a/source/classes/constants.py +++ b/source/classes/constants.py @@ -67,9 +67,8 @@ SETTINGSTOPROCESS = { "crystals_ganon": "crystals_ganon", "weapons": "swords", "itempool": "difficulty", - "itemfunction": "item_functionality", + "flute_mode": "flute_mode", "timer": "timer", - "progressives": "progressive", "accessibility": "accessibility", "sortingalgo": "algorithm", "beemizer": "beemizer", diff --git a/source/item/FillUtil.py b/source/item/FillUtil.py index 9614eeef..6f60b72e 100644 --- a/source/item/FillUtil.py +++ b/source/item/FillUtil.py @@ -454,6 +454,7 @@ vanilla_mapping = { 'Hookshot': ['Swamp Palace - Big Chest'], 'Magic Mirror': ['Old Man'], 'Ocarina': ['Flute Spot'], + 'Ocarina (Activated)': ['Flute Spot'], 'Pegasus Boots': ['Sahasrahla'], 'Power Glove': ['Desert Palace - Big Chest'], 'Cape': ["King's Tomb"], @@ -779,7 +780,7 @@ major_items = {'Bombos', 'Book of Mudora', 'Cane of Somaria', 'Ether', 'Fire Rod 'Bug Catching Net', 'Cane of Byrna', 'Blue Boomerang', 'Red Boomerang', 'Progressive Glove', 'Power Glove', 'Titans Mitts', 'Bottle', 'Bottle (Red Potion)', 'Bottle (Green Potion)', 'Magic Mirror', 'Bottle (Blue Potion)', 'Bottle (Fairy)', 'Bottle (Bee)', 'Bottle (Good Bee)', 'Magic Upgrade (1/2)', - 'Sanctuary Heart Container', 'Boss Heart Container', 'Progressive Shield', + 'Sanctuary Heart Container', 'Boss Heart Container', 'Progressive Shield', 'Ocarina (Activated)', 'Mirror Shield', 'Progressive Armor', 'Blue Mail', 'Red Mail', 'Progressive Sword', 'Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword', 'Bow', 'Silver Arrows', 'Triforce Piece', 'Moon Pearl', 'Progressive Bow', 'Progressive Bow (Alt)'} diff --git a/source/tools/MysteryUtils.py b/source/tools/MysteryUtils.py index 5ca41b6d..ae628b7c 100644 --- a/source/tools/MysteryUtils.py +++ b/source/tools/MysteryUtils.py @@ -144,6 +144,7 @@ def roll_settings(weights): }[swords] ret.difficulty = get_choice('item_pool') + ret.flute_mode = get_choice_default('flute_mode', default='normal') ret.item_functionality = get_choice('item_functionality') From 87834986d4f5767b9e0b2bc6c11a196bce74bc78 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 2 Sep 2022 13:09:39 -0600 Subject: [PATCH 40/63] Retro break up of various options --- BaseClasses.py | 71 ++++--- CLI.py | 25 ++- DoorShuffle.py | 14 +- DungeonGenerator.py | 2 +- Dungeons.py | 2 +- Fill.py | 22 ++- InitialSram.py | 4 +- ItemList.py | 179 ++++++++++-------- KeyDoorShuffle.py | 25 +-- Main.py | 9 +- PotShuffle.py | 7 +- Regions.py | 2 +- Rom.py | 36 ++-- Rules.py | 2 +- mystery_example.yml | 18 +- mystery_testsuite.yml | 1 - resources/app/cli/args.json | 22 ++- resources/app/cli/lang/en.json | 15 +- resources/app/gui/lang/en.json | 16 +- .../app/gui/randomize/dungeon/keysanity.json | 1 - .../app/gui/randomize/dungeon/widgets.json | 8 + .../app/gui/randomize/entrando/widgets.json | 8 + resources/app/gui/randomize/item/widgets.json | 9 + source/classes/CustomSettings.py | 5 +- source/classes/constants.py | 2 + source/gui/bottom.py | 8 + source/item/FillUtil.py | 21 +- source/tools/MysteryUtils.py | 10 +- 28 files changed, 351 insertions(+), 193 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 8ff91295..a601a3c5 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -22,7 +22,7 @@ from source.dungeon.RoomObject import RoomObject class World(object): def __init__(self, players, shuffle, doorShuffle, logic, mode, swords, difficulty, difficulty_adjustments, - timer, progressive, goal, algorithm, accessibility, shuffle_ganon, retro, custom, customitemarray, hints): + timer, progressive, goal, algorithm, accessibility, shuffle_ganon, custom, customitemarray, hints): self.players = players self.teams = 1 self.shuffle = shuffle.copy() @@ -64,7 +64,6 @@ class World(object): self.fix_trock_exit = {} self.shuffle_ganon = shuffle_ganon self.fix_gtower_exit = self.shuffle_ganon - self.retro = retro.copy() self.custom = custom self.customitemarray = customitemarray self.can_take_damage = True @@ -91,10 +90,6 @@ class World(object): self.pot_contents = {} for player in range(1, players + 1): - # If World State is Retro, set to Open and set Retro flag - if self.mode[player] == "retro": - self.mode[player] = "open" - self.retro[player] = True def set_player_attr(attr, val): self.__dict__.setdefault(attr, {})[player] = val set_player_attr('_region_cache', {}) @@ -118,10 +113,12 @@ class World(object): 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) + set_player_attr('keyshuffle', 'standard') set_player_attr('bigkeyshuffle', False) set_player_attr('restrict_boss_items', 'none') set_player_attr('bombbag', False) + set_player_attr('flute_mode', False) + set_player_attr('bow_mode', False) set_player_attr('difficulty_requirements', None) set_player_attr('boss_shuffle', 'none') set_player_attr('enemy_shuffle', 'none') @@ -134,6 +131,7 @@ class World(object): set_player_attr('crystals_ganon_orig', {}) set_player_attr('crystals_gt_orig', {}) set_player_attr('open_pyramid', False) + set_player_attr('take_any', 'none') set_player_attr('treasure_hunt_icon', 'Triforce Piece') set_player_attr('treasure_hunt_count', 0) set_player_attr('treasure_hunt_total', 0) @@ -154,6 +152,11 @@ class World(object): set_player_attr('exp_cache', defaultdict(dict)) set_player_attr('enabled_entrances', {}) + def finish_init(self): + for player in range(1, self.players + 1): + if self.mode[player] == 'retro': + self.mode[player] == 'open' + def get_name_string_for_object(self, obj): return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_names(obj.player)})' @@ -375,7 +378,8 @@ class World(object): def push_precollected(self, item): item.world = self - if (item.smallkey and self.keyshuffle[item.player]) or (item.bigkey and self.bigkeyshuffle[item.player]): + if ((item.smallkey and self.keyshuffle[item.player] != 'none') + or (item.bigkey and self.bigkeyshuffle[item.player])): item.advancement = True self.precollected_items.append(item) self.state.collect(item, True) @@ -587,7 +591,7 @@ class CollectionState(object): if key_logic.sm_doors[door]: self.reached_doors[player].add(key_logic.sm_doors[door].name) if not connection.can_reach(self): - checklist_key = 'Universal' if self.world.retro[player] else dungeon_name + checklist_key = 'Universal' if self.world.keyshuffle[player] == 'universal' else dungeon_name checklist = self.dungeons_to_check[player][checklist_key] checklist[connection.name] = (connection, crystal_state) elif door.name not in self.opened_doors[player]: @@ -759,7 +763,7 @@ class CollectionState(object): return None def set_dungeon_limits(self, player, dungeon_name): - if self.world.retro[player] and self.world.mode[player] == 'standard': + if self.world.keyshuffle[player] == 'universal' and self.world.mode[player] == 'standard': self.dungeon_limits = ['Hyrule Castle', 'Agahnims Tower'] else: self.dungeon_limits = [dungeon_name] @@ -935,7 +939,7 @@ class CollectionState(object): checked_locations = 0 while new_locations: reachable_events = [location for location in locations if location.event and - (not key_only or (not self.world.keyshuffle[location.item.player] and location.item.smallkey) or (not self.world.bigkeyshuffle[location.item.player] and location.item.bigkey)) + (not key_only or (self.world.keyshuffle[location.item.player] == 'none' and location.item.smallkey) or (not self.world.bigkeyshuffle[location.item.player] and location.item.bigkey)) and location.can_reach(self)] reachable_events = self._do_not_flood_the_keys(reachable_events) for event in reachable_events: @@ -995,7 +999,7 @@ class CollectionState(object): return self.prog_items[item, player] >= count def has_sm_key(self, item, player, count=1): - if self.world.retro[player]: + if self.world.keyshuffle[player] == 'universal': if self.world.mode[player] == 'standard' and self.world.doorShuffle[player] == 'vanilla' and item == 'Small Key (Escape)': return True # Cannot access the shop until escape is finished. This is safe because the key is manually placed in make_custom_item_pool return self.can_buy_unlimited('Small Key (Universal)', player) @@ -1091,7 +1095,7 @@ class CollectionState(object): or self.has('Cane of Somaria', player)) def can_shoot_arrows(self, player): - if self.world.retro[player]: + if self.world.bow_mode[player] in ['retro', 'retro_silvers']: #todo: Non-progressive silvers grant wooden arrows, but progressive bows do not. Always require shop arrows to be safe return self.has('Bow', player) and (self.can_buy_unlimited('Single Arrow', player) or self.has('Single Arrow', player)) return self.has('Bow', player) @@ -1373,7 +1377,7 @@ class Region(object): return False def can_fill(self, item): - inside_dungeon_item = ((item.smallkey and not self.world.keyshuffle[item.player]) + inside_dungeon_item = ((item.smallkey and self.world.keyshuffle[item.player] == 'none') or (item.bigkey and not self.world.bigkeyshuffle[item.player]) or (item.map and not self.world.mapshuffle[item.player]) or (item.compass and not self.world.compassshuffle[item.player])) @@ -2239,7 +2243,7 @@ class Item(object): return item_dungeon def is_inside_dungeon_item(self, world): - return ((self.smallkey and not world.keyshuffle[self.player]) + return ((self.smallkey and world.keyshuffle[self.player] == 'none') or (self.bigkey and not world.bigkeyshuffle[self.player]) or (self.compass and not world.compassshuffle[self.player]) or (self.map and not world.mapshuffle[self.player])) @@ -2371,15 +2375,16 @@ class Spoiler(object): self.metadata = {'version': ERVersion, 'logic': self.world.logic, 'mode': self.world.mode, - 'retro': self.world.retro, 'bombbag': self.world.bombbag, 'weapons': self.world.swords, 'flute_mode': self.world.flute_mode, + 'bow_mode': self.world.bow_mode, 'goal': self.world.goal, 'shuffle': self.world.shuffle, 'shuffleganon': self.world.shuffle_ganon, 'shufflelinks': self.world.shufflelinks, 'shuffletavern': self.world.shuffletavern, + 'take_any': self.world.take_any, 'overworld_map': self.world.overworld_map, 'door_shuffle': self.world.doorShuffle, 'intensity': self.world.intensity, @@ -2561,7 +2566,6 @@ class Spoiler(object): outfile.write(f'Settings Code: {self.metadata["code"][player]}\n') outfile.write('Logic: %s\n' % self.metadata['logic'][player]) outfile.write('Mode: %s\n' % self.metadata['mode'][player]) - 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] in ['triforcehunt', 'trinity']: @@ -2574,6 +2578,7 @@ class Spoiler(object): outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][player]) outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'][player]) outfile.write(f"Flute Mode: {self.metadata['flute_mode'][player]}\n") + outfile.write(f"Bow Mode: {self.metadata['bow_mode'][player]}\n") outfile.write(f"Shopsanity: {yn(self.metadata['shopsanity'][player])}\n") outfile.write(f"Bombbag: {yn(self.metadata['bombbag'][player])}\n") outfile.write(f"Pseudoboots: {yn(self.metadata['pseudoboots'][player])}\n") @@ -2583,6 +2588,8 @@ class Spoiler(object): outfile.write(f"Back of Tavern Shuffled: {yn(self.metadata['shuffletavern'][player])}\n") outfile.write(f"GT/Ganon Shuffled: {yn(self.metadata['shuffleganon'])}\n") outfile.write(f"Overworld Map: {self.metadata['overworld_map'][player]}\n") + outfile.write(f"Take Any Caves: {self.metadata['take_any'][player]}\n") + outfile.write(f"Overworld Map: {self.metadata['overworld_map'][player]}\n") 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('Door Shuffle: %s\n' % self.metadata['door_shuffle'][player]) @@ -2597,7 +2604,7 @@ class Spoiler(object): outfile.write(f"Pot Shuffle (Legacy): {yn(self.metadata['potshuffle'][player])}\n") outfile.write('Map shuffle: %s\n' % ('Yes' if self.metadata['mapshuffle'][player] else 'No')) outfile.write('Compass shuffle: %s\n' % ('Yes' if self.metadata['compassshuffle'][player] else 'No')) - outfile.write('Small Key shuffle: %s\n' % ('Yes' if self.metadata['keyshuffle'][player] else 'No')) + outfile.write(f"Small Key shuffle: {self.metadata['keyshuffle'][player]}\n") outfile.write('Big Key shuffle: %s\n' % ('Yes' if self.metadata['bigkeyshuffle'][player] else 'No')) outfile.write('Boss shuffle: %s\n' % self.metadata['boss_shuffle'][player]) outfile.write('Enemy shuffle: %s\n' % self.metadata['enemy_shuffle'][player]) @@ -2835,7 +2842,7 @@ dr_mode = {"basic": 1, "crossed": 2, "vanilla": 0, "partitioned": 3} 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) +# byte 1: LLLW WSS? (logic, mode, sword) logic_mode = {"noglitches": 0, "minorglitches": 1, "nologic": 2, "owglitches": 3, "majorglitches": 4} world_mode = {"open": 0, "standard": 1, "inverted": 2} sword_mode = {"random": 0, "assured": 1, "swordless": 2, "vanilla": 3} @@ -2861,7 +2868,7 @@ counter_mode = {"default": 0, "off": 1, "on": 2, "pickup": 3} # byte 6: CCCC CPAA (crystals ganon, pyramid, access access_mode = {"items": 0, "locations": 1, "none": 2} -# byte 7: BSMC DDEE (big, small, maps, compass, door_type, enemies) +# byte 7: B?MC DDEE (big, ?, maps, compass, door_type, enemies) door_type_mode = {'original': 0, 'big': 1, 'all': 2, 'chaos': 3} enemy_mode = {"none": 0, "shuffled": 1, "chaos": 2, "random": 2, "legacy": 3} @@ -2878,15 +2885,18 @@ boss_mode = {"none": 0, "simple": 1, "full": 2, "chaos": 3, 'random': 3, 'unique # byte 10: settings_version -# byte 11: F???, ???? (flute_mode) +# byte 11: FBBB, TTSS (flute_mode, bow_mode, take_any, small_key_mode) flute_mode = {'normal': 0, 'active': 1} +keyshuffle_mode = {'none': 0, 'wild': 1, 'universal': 2} # reserved 8 modes? +take_any_mode = {'none': 0, 'random': 1, 'fixed': 2} +bow_mode = {'progressive': 0, 'silvers': 1, 'retro': 2, 'retro_silver': 3} # additions # psuedoboots does not effect code # sfx_shuffle and other adjust items does not effect settings code # Bump this when making changes that are not backwards compatible (nearly all of them) -settings_version = 0 +settings_version = 1 class Settings(object): @@ -2897,7 +2907,7 @@ class Settings(object): (dr_mode[w.doorShuffle[p]] << 5) | er_mode[w.shuffle[p]], (logic_mode[w.logic[p]] << 5) | (world_mode[w.mode[p]] << 3) - | (sword_mode[w.swords[p]] << 1) | (1 if w.retro[p] else 0), + | (sword_mode[w.swords[p]] << 1), (goal_mode[w.goal[p]] << 5) | (diff_mode[w.difficulty[p]] << 3) | (func_mode[w.difficulty_adjustments[p]] << 1) | (1 if w.hints[p] else 0), @@ -2915,7 +2925,7 @@ class Settings(object): ((8 if w.crystals_ganon_orig[p] == "random" else int(w.crystals_ganon_orig[p])) << 3) | (0x4 if w.open_pyramid[p] else 0) | access_mode[w.accessibility[p]], - (0x80 if w.bigkeyshuffle[p] else 0) | (0x40 if w.keyshuffle[p] else 0) + (0x80 if w.bigkeyshuffle[p] else 0) | (0x20 if w.mapshuffle[p] else 0) | (0x10 if w.compassshuffle[p] else 0) | (door_type_mode[w.door_type_mode[p]] << 2) | (enemy_mode[w.enemy_shuffle[p]]), @@ -2926,14 +2936,16 @@ class Settings(object): settings_version, - flute_mode[w.flute_mode[p]] << 7]) + (flute_mode[w.flute_mode[p]] << 7 | bow_mode[w.bow_mode[p]] << 4 + | take_any_mode[w.take_any[p]] << 2 | keyshuffle_mode[w.keyshuffle[p]]) + ]) return base64.b64encode(code, "+-".encode()).decode() @staticmethod def adjust_args_from_code(code, player, args): settings, p = base64.b64decode(code.encode(), "+-".encode()), player - if len(settings) < 11: + if len(settings) < 12: raise Exception('Provided code is incompatible with this version') if settings[10] != settings_version: raise Exception('Provided code is incompatible with this version') @@ -2950,7 +2962,7 @@ class Settings(object): args.item_functionality[p] = r(func_mode)[(settings[2] & 0x6) >> 1] args.goal[p] = r(goal_mode)[(settings[2] & 0xE0) >> 5] args.accessibility[p] = r(access_mode)[settings[6] & 0x3] - args.retro[p] = True if settings[1] & 0x01 else False + # args.retro[p] = True if settings[1] & 0x01 else False args.hints[p] = True if settings[2] & 0x01 else False args.shopsanity[p] = True if settings[3] & 0x80 else False args.decoupledoors[p] = True if settings[3] & 0x40 else False @@ -2973,7 +2985,7 @@ class Settings(object): args.openpyramid[p] = True if settings[6] & 0x4 else False args.bigkeyshuffle[p] = True if settings[7] & 0x80 else False - args.keyshuffle[p] = True if settings[7] & 0x40 else False + # args.keyshuffle[p] = True if settings[7] & 0x40 else False args.mapshuffle[p] = True if settings[7] & 0x20 else False args.compassshuffle[p] = True if settings[7] & 0x10 else False args.door_type_mode[p] = r(door_type_mode)[(settings[7] & 0xc) >> 2] @@ -2990,6 +3002,9 @@ class Settings(object): args.shufflebosses[p] = r(boss_mode)[(settings[9] & 0x07)] if len(settings) > 11: args.flute_mode[p] = r(flute_mode)[(settings[11] & 0x80) >> 7] + args.bow_mode[p] = r(bow_mode)[(settings[11] & 0x70) >> 4] + args.take_any[p] = r(take_any_mode)[(settings[11] & 0xC) >> 2] + args.keyshuffle[p] = r(keyshuffle_mode)[settings[11] & 0x3] class KeyRuleType(FastEnum): diff --git a/CLI.py b/CLI.py index 44c7b76c..942bd7f2 100644 --- a/CLI.py +++ b/CLI.py @@ -103,19 +103,28 @@ def parse_cli(argv, no_defaults=False): ret = parser.parse_args(argv) if ret.keysanity: - ret.mapshuffle, ret.compassshuffle, ret.keyshuffle, ret.bigkeyshuffle = [True] * 4 + ret.mapshuffle, ret.compassshuffle, ret.bigkeyshuffle = [True] * 3 + ret.keyshuffle = 'wild' if ret.keydropshuffle: ret.dropshuffle = True ret.pottery = 'keys' if ret.pottery == 'none' else ret.pottery + if ret.retro or ret.mode == 'retro': + if ret.bow_mode == 'progressive': + ret.bow_mode = 'retro' + elif ret.bow_mode == 'silvers': + ret.bow_mode = 'retro_silvers' + ret.take_any = 'random' if ret.take_any == 'none' else ret.take_any + ret.keyshuffle = 'universal' + if player_num: defaults = copy.deepcopy(ret) 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', - 'flute_mode', + 'flute_mode', 'bow_mode', 'take_any', 'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'openpyramid', 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory', 'usestartinventory', 'bombbag', 'overworld_map', 'restrict_boss_items', @@ -158,6 +167,7 @@ def parse_settings(): "crystals_ganon": "7", "swords": "random", 'flute_mode': 'normal', + 'bow_mode': 'progressive', "difficulty": "normal", "item_functionality": "normal", "timer": "none", @@ -175,6 +185,7 @@ def parse_settings(): "shufflelinks": False, "shuffletavern": False, "overworld_map": "default", + 'take_any': 'none', "pseudoboots": False, "shuffleenemies": "none", @@ -189,11 +200,11 @@ def parse_settings(): 'pottery': 'none', 'colorizepots': False, 'shufflepots': False, - "mapshuffle": False, - "compassshuffle": False, - "keyshuffle": False, - "bigkeyshuffle": False, - "keysanity": False, + 'mapshuffle': False, + 'compassshuffle': False, + 'keyshuffle': 'none', + 'bigkeyshuffle': False, + 'keysanity': False, "door_shuffle": "basic", "intensity": 2, 'door_type_mode': 'original', diff --git a/DoorShuffle.py b/DoorShuffle.py index 89463230..0d56882e 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -470,7 +470,7 @@ def choose_portals(world, player): for dungeon, info in shuffled_info: outstanding_portals = list(dungeon_portals[dungeon]) hc_flag = std_flag and dungeon == 'Hyrule Castle' - rupee_bow_flag = hc_flag and world.retro[player] # rupee bow + rupee_bow_flag = hc_flag and world.bow_mode[player].startswith('retro') # rupee bow if hc_flag: sanc = world.get_portal('Sanctuary', player) sanc.destination = True @@ -843,7 +843,7 @@ def main_dungeon_pool(dungeon_pool, 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 - if world.retro[player]: + if world.keyshuffle[player] == 'universal': target_items += 1 if world.dropshuffle[player] else 0 # the hc big key else: target_items += 29 # small keys in chests @@ -1248,7 +1248,7 @@ def cross_dungeon(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 - if world.retro[player]: + if world.keyshuffle[player] == 'universal': target_items += 1 if world.dropshuffle[player] else 0 # the hc big key else: target_items += 29 # small keys in chests @@ -1336,7 +1336,7 @@ def filter_key_door_pool(pool, selected_custom): def assign_cross_keys(dungeon_builders, world, player): logging.getLogger('').info(world.fish.translate("cli", "cli", "shuffling.keydoors")) start = time.process_time() - if world.retro[player]: + if world.keyshuffle[player] == 'universal': remaining = 29 if world.dropshuffle[player]: remaining += 13 @@ -1424,7 +1424,7 @@ def assign_cross_keys(dungeon_builders, world, player): # Last Step: Adjust Small Key Dungeon Pool for name, builder in dungeon_builders.items(): reassign_key_doors(builder, world, player) - if not world.retro[player]: + if world.keyshuffle[player] != 'universal': log_key_logic(builder.name, world.key_logic[player][builder.name]) actual_chest_keys = max(builder.key_doors_num - builder.key_drop_cnt, 0) dungeon = world.get_dungeon(name, player) @@ -1963,7 +1963,7 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, worl # time to re-assign reassign_key_doors(small_map, world, player) for dungeon_name in pool: - if not world.retro[player]: + if world.keyshuffle[player] != 'universal': builder = world.dungeon_layouts[player][dungeon_name] log_key_logic(builder.name, world.key_logic[player][builder.name]) if world.doorShuffle[player] != 'basic': @@ -2470,7 +2470,7 @@ def reassign_big_key_doors(bk_map, world, player): pass # we don't have spiral stairs candidates yet that aren't already key doors elif d.type is DoorType.Normal: change_door_to_big_key(d, world, player) - if not world.decoupledoors[player] and d.dest: + if not world.decoupledoors[player] and d.dest and world.door_type_mode[player] != 'original': if d.dest.type in [DoorType.Normal]: dest_room = world.get_room(d.dest.roomIndex, player) if stateful_door(d.dest, dest_room.kind(d.dest)): diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 4404d53f..445ed9b3 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -1355,7 +1355,7 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, dunge sanc_builder = random.choice(lw_builders) assign_sector(sanc, sanc_builder, candidate_sectors, global_pole) - bow_sectors, retro_std_flag = {}, world.retro[player] and world.mode[player] == 'standard' + bow_sectors, retro_std_flag = {}, world.bow_mode[player].startswith('retro') and world.mode[player] == 'standard' free_location_sectors = {} crystal_switches = {} crystal_barriers = {} diff --git a/Dungeons.py b/Dungeons.py index 59a6a963..9d862b05 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -5,7 +5,7 @@ from Items import ItemFactory def create_dungeons(world, player): def make_dungeon(name, id, default_boss, dungeon_regions, big_key, small_keys, dungeon_items): - dungeon = Dungeon(name, dungeon_regions, big_key, [] if world.retro[player] else small_keys, + dungeon = Dungeon(name, dungeon_regions, big_key, [] if world.keyshuffle[player] == 'universal' else small_keys, dungeon_items, player, id) dungeon.boss = BossFactory(default_boss, player) for region in dungeon.regions: diff --git a/Fill.py b/Fill.py index a8eecc3a..db18996f 100644 --- a/Fill.py +++ b/Fill.py @@ -39,7 +39,8 @@ def fill_dungeons_restrictive(world, shuffled_locations): # with shuffled dungeon items they are distributed as part of the normal item pool for item in world.get_items(): - if (item.smallkey and world.keyshuffle[item.player]) or (item.bigkey and world.bigkeyshuffle[item.player]): + if ((item.smallkey and world.keyshuffle[item.player] != 'none') + or (item.bigkey and world.bigkeyshuffle[item.player])): item.advancement = True elif (item.map and world.mapshuffle[item.player]) or (item.compass and world.compassshuffle[item.player]): item.priority = True @@ -50,7 +51,7 @@ def fill_dungeons_restrictive(world, shuffled_locations): (bigs if i.bigkey else smalls if i.smallkey else others).append(i) unplaced_smalls = list(smalls) for i in world.itempool: - if i.smallkey and world.keyshuffle[i.player]: + if i.smallkey and world.keyshuffle[i.player] != 'none': unplaced_smalls.append(i) def fill(base_state, items, key_pool): @@ -160,7 +161,7 @@ def valid_key_placement(item, location, key_pool, world): if not valid_reserved_placement(item, location, world): return False if ((not item.smallkey and not item.bigkey) or item.player != location.player - or world.retro[item.player] or world.logic[item.player] == 'nologic'): + or world.keyshuffle[item.player] == 'universal' or world.logic[item.player] == 'nologic'): return True dungeon = location.parent_region.dungeon if dungeon: @@ -215,7 +216,7 @@ def track_dungeon_items(item, location, world): def is_dungeon_item(item, world): - return ((item.smallkey and not world.keyshuffle[item.player]) + return ((item.smallkey and world.keyshuffle[item.player] == 'none') or (item.bigkey and not world.bigkeyshuffle[item.player]) or (item.compass and not world.compassshuffle[item.player]) or (item.map and not world.mapshuffle[item.player])) @@ -419,7 +420,8 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None # Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots # todo: crossed - progitempool.sort(key=lambda item: 1 if item.name == 'Small Key (Escape)' and world.keyshuffle[item.player] and world.mode[item.player] == 'standard' else 0) + progitempool.sort(key=lambda item: 1 if item.name == 'Small Key (Escape)' + and world.keyshuffle[item.player] != 'none' and world.mode[item.player] == 'standard' else 0) key_pool = [x for x in progitempool if x.smallkey] # sort maps and compasses to the back -- this may not be viable in equitable & ambrosia @@ -498,7 +500,7 @@ def ensure_good_pots(world, write_skips=False): else: loc.item = ItemFactory(invalid_location_replacement[loc.item.name], loc.player) # do the arrow retro check - if world.retro[loc.item.player] and loc.item.name in {'Arrows (5)', 'Arrows (10)'}: + if world.bow_mode[loc.item.player].startswith('retro') and loc.item.name in {'Arrows (5)', 'Arrows (10)'}: loc.item = ItemFactory('Rupees (5)', loc.item.player) # don't write out all pots to spoiler if write_skips: @@ -663,7 +665,7 @@ def balance_multiworld_progression(world): candidate_items = collections.defaultdict(set) while True: for location in balancing_sphere: - if location.event and (world.keyshuffle[location.item.player] or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey): + if location.event and (world.keyshuffle[location.item.player] != 'none' or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey): balancing_state.collect(location.item, True, location) player = location.item.player if player in balancing_players and not location.locked and location.player != player: @@ -738,7 +740,7 @@ def balance_multiworld_progression(world): sphere_locations.add(location) for location in sphere_locations: - if location.event and (world.keyshuffle[location.item.player] or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey): + if location.event and (world.keyshuffle[location.item.player] != 'none' or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey): state.collect(location.item, True, location) checked_locations |= sphere_locations @@ -812,7 +814,9 @@ def balance_money_progression(world): return True if item.name in ['Progressive Armor', 'Blue Mail', 'Red Mail']: return True - if world.retro[player] and (item.name in ['Single Arrow', 'Small Key (Universal)']): + if world.keyshuffle[player] == 'universal' and item.name == 'Small Key (Universal)': + return True + if world.bow_mode[player].startswith('retro') and item.name == 'Single Arrow': return True if location.name in pay_for_locations: return True diff --git a/InitialSram.py b/InitialSram.py index 63c822cd..8d0ade11 100644 --- a/InitialSram.py +++ b/InitialSram.py @@ -73,7 +73,7 @@ class InitialSram: if startingstate.has('Bow', player): equip[0x340] = 3 if startingstate.has('Silver Arrows', player) else 1 equip[0x38E] |= 0x20 # progressive flag to get the correct hint in all cases - if not world.retro[player]: + if not world.bow_mode[player].startswith('retro'): equip[0x38E] |= 0x80 if startingstate.has('Silver Arrows', player): equip[0x38E] |= 0x40 @@ -188,7 +188,7 @@ class InitialSram: elif item.name in bombs: starting_bombs += bombs[item.name] elif item.name in arrows: - if world.retro[player]: + if world.bow_mode[player].startswith('retro'): equip[0x38E] |= 0x80 starting_arrows = 1 else: diff --git a/ItemList.py b/ItemList.py index 1dffb859..c1981365 100644 --- a/ItemList.py +++ b/ItemList.py @@ -264,10 +264,10 @@ def generate_itempool(world, player): (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) + (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) = make_custom_item_pool(world, player, world.progressive, world.shuffle[player], world.difficulty[player], world.timer, world.goal[player], world.mode[player], world.swords[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], world.flute_mode[player] == 'active') + (pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms) = get_pool_core(world, player, 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.bombbag[player], world.doorShuffle[player], world.logic[player], world.flute_mode[player] == 'active') if player in world.pool_adjustment.keys() and not skip_pool_adjustments: amt = world.pool_adjustment[player] @@ -295,7 +295,8 @@ def generate_itempool(world, player): if not found_sword and world.swords[player] != 'swordless': found_sword = True possible_weapons.append(item) - if item in ['Progressive Bow', 'Bow'] and not found_bow and not world.retro[player]: + if (item in ['Progressive Bow', 'Bow'] and not found_bow + and not world.bow_mode[player].startswith('retro')): found_bow = True possible_weapons.append(item) if item in ['Hammer', 'Fire Rod', 'Cane of Somaria', 'Cane of Byrna']: @@ -349,7 +350,7 @@ def generate_itempool(world, player): world.treasure_hunt_icon[player] = 'Triforce Piece' world.itempool.extend([item for item in get_dungeon_item_pool(world) if item.player == player - and ((item.smallkey and world.keyshuffle[player]) + and ((item.smallkey and world.keyshuffle[player] != 'none') or (item.bigkey and world.bigkeyshuffle[player]) or (item.map and world.mapshuffle[player]) or (item.compass and world.compassshuffle[player]))]) @@ -409,8 +410,9 @@ def generate_itempool(world, player): set_up_shops(world, player) - if world.retro[player]: + if world.take_any[player] != 'none': set_up_take_anys(world, player, skip_pool_adjustments) + if world.keyshuffle[player] == 'universal': 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'] and not skip_pool_adjustments: @@ -432,6 +434,10 @@ take_any_locations = [ 'Palace of Darkness Hint', 'East Dark World Hint', 'Archery Game', 'Dark Lake Hylia Ledge Hint', 'Dark Lake Hylia Ledge Spike Cave', 'Fortune Teller (Dark)', 'Dark Sanctuary Hint', 'Dark Desert Hint'] +fixed_take_anys = [ + 'Desert Healer Fairy', 'Swamp Healer Fairy', 'Dark Death Mountain Healer Fairy', + 'Dark Lake Hylia Ledge Healer Fairy', 'Bonk Fairy (Dark)'] + def set_up_take_anys(world, player, skip_adjustments=False): if world.mode[player] == 'inverted': @@ -440,9 +446,12 @@ def set_up_take_anys(world, player, skip_adjustments=False): if 'Archery Game' in take_any_locations: take_any_locations.remove('Archery Game') - take_any_candidates = [x for x in take_any_locations if len(world.get_region(x, player).locations) == 0] - - regions = random.sample(take_any_candidates, 5) + if world.take_any[player] == 'random': + take_any_candidates = [x for x in take_any_locations if len(world.get_region(x, player).locations) == 0] + regions = random.sample(take_any_candidates, 5) + elif world.take_any[player] == 'fixed': + regions = list(fixed_take_anys) + random.shuffle(regions) old_man_take_any = Region("Old Man Sword Cave", RegionType.Cave, 'the sword cave', player) world.regions.append(old_man_take_any) @@ -539,38 +548,47 @@ def fill_prizes(world, attempts=15): def set_up_shops(world, player): - if world.retro[player]: + retro_bow = world.bow_mode[player].startswith('retro') + universal_keys = world.keyshuffle[player] == 'universal' + if retro_bow or universal_keys: if world.shopsanity[player]: - removals = [next(item for item in world.itempool if item.name == 'Arrows (10)' and item.player == player)] - red_pots = [item for item in world.itempool if item.name == 'Red Potion' and item.player == player][:5] - shields_n_hearts = [item for item in world.itempool if item.name in ['Blue Shield', 'Small Heart'] and item.player == player] - removals.extend([item for item in world.itempool if item.name == 'Arrow Upgrade (+5)' and item.player == player]) - removals.extend(red_pots) - removals.extend(random.sample(shields_n_hearts, 5)) + removals = [] + if retro_bow: + removals = [next(item for item in world.itempool if item.name == 'Arrows (10)' and item.player == player)] + removals.extend([item for item in world.itempool if item.name == 'Arrow Upgrade (+5)' and item.player == player]) + shields_n_hearts = [item for item in world.itempool if item.name in ['Blue Shield', 'Small Heart'] and item.player == player] + removals.extend(random.sample(shields_n_hearts, 5)) + if universal_keys: + red_pots = [item for item in world.itempool if item.name == 'Red Potion' and item.player == player][:5] + removals.extend(red_pots) for remove in removals: world.itempool.remove(remove) - for i in range(6): # replace the Arrows (10) and randomly selected hearts/blue shield - arrow_item = ItemFactory('Single Arrow', player) - arrow_item.advancement = True - world.itempool.append(arrow_item) - for i in range(5): # replace the red potions - world.itempool.append(ItemFactory('Small Key (Universal)', player)) - world.itempool.append(ItemFactory('Rupees (50)', player)) # replaces the arrow upgrade + if retro_bow: + for i in range(6): # replace the Arrows (10) and randomly selected hearts/blue shield + arrow_item = ItemFactory('Single Arrow', player) + arrow_item.advancement = True + world.itempool.append(arrow_item) + world.itempool.append(ItemFactory('Rupees (50)', player)) # replaces the arrow upgrade + if universal_keys: + for i in range(5): # replace the red potions + world.itempool.append(ItemFactory('Small Key (Universal)', player)) # TODO: move hard+ mode changes for shields here, utilizing the new shops else: - rss = world.get_region('Red Shield Shop', player).shop - if not rss.locked: - rss.custom = True - rss.add_inventory(2, 'Single Arrow', 80) + if retro_bow: + rss = world.get_region('Red Shield Shop', player).shop + if not rss.locked: + rss.custom = True + rss.add_inventory(2, 'Single Arrow', 80) + rss.locked = True + cap_shop = world.get_region('Capacity Upgrade', player).shop + cap_shop.inventory[1] = None # remove arrow capacity upgrades in retro for shop in random.sample([s for s in world.shops[player] if not s.locked and s.region.player == player], 5): shop.custom = True shop.locked = True - shop.add_inventory(0, 'Single Arrow', 80) - shop.add_inventory(1, 'Small Key (Universal)', 100) - shop.add_inventory(2, 'Bombs (10)', 50) - rss.locked = True - cap_shop = world.get_region('Capacity Upgrade', player).shop - cap_shop.inventory[1] = None # remove arrow capacity upgrades in retro + if retro_bow: + shop.add_inventory(0, 'Single Arrow', 80) + if universal_keys: + shop.add_inventory(1, 'Small Key (Universal)', 100) if world.bombbag[player]: if world.shopsanity[player]: removals = [item for item in world.itempool if item.name == 'Bomb Upgrade (+5)' and item.player == player] @@ -583,10 +601,11 @@ def set_up_shops(world, player): def customize_shops(world, player): - found_bomb_upgrade, found_arrow_upgrade = False, world.retro[player] + retro_bow = world.bow_mode[player].startswith('retro') + found_bomb_upgrade, found_arrow_upgrade = False, retro_bow possible_replacements = [] shops_to_customize = shop_to_location_table.copy() - if world.retro[player]: + if world.take_any[player] != 'none': shops_to_customize.update(retro_shops) for shop_name, loc_list in shops_to_customize.items(): shop = world.get_region(shop_name, player).shop @@ -609,7 +628,7 @@ def customize_shops(world, player): price = 0 else: price = 120 if shop_name == 'Potion Shop' and item.name == 'Red Potion' else item.price - if world.retro[player] and item.name == 'Single Arrow': + if retro_bow and item.name == 'Single Arrow': price = 80 # randomize price shop.add_inventory(idx, item.name, randomize_price(price), max_repeat, player=item.player) @@ -636,7 +655,7 @@ def customize_shops(world, player): if not found_arrow_upgrade and len(possible_replacements) > 0: choices = [] for shop, idx, loc, item in possible_replacements: - if item.name == 'Arrows (10)' or (item.name == 'Single Arrow' and not world.retro[player]): + if item.name == 'Arrows (10)' or (item.name == 'Single Arrow' and not retro_bow): choices.append((shop, idx, loc, item)) if len(choices) > 0: shop, idx, loc, item = random.choice(choices) @@ -780,16 +799,17 @@ rupee_chart = {'Rupee (1)': 1, 'Rupees (5)': 5, 'Rupees (20)': 20, 'Rupees (50)' def add_pot_contents(world, player): + retro_bow = world.bow_mode[player].startswith('retro') for super_tile, pot_list in vanilla_pots.items(): for pot in pot_list: if pot.item not in [PotItem.Hole, PotItem.Key, PotItem.Switch]: if valid_pot_location(pot, world.pot_pool[player], world, player): - item = ('Rupees (5)' if world.retro[player] and pot_items[pot.item] == 'Arrows (5)' + item = ('Rupees (5)' if retro_bow and pot_items[pot.item] == 'Arrows (5)' else pot_items[pot.item]) world.itempool.append(ItemFactory(item, player)) -def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, goal, mode, swords, retro, bombbag, +def get_pool_core(world, player, progressive, shuffle, difficulty, treasure_hunt_total, timer, goal, mode, swords, bombbag, door_shuffle, logic, flute_activated): pool = [] placed_items = {} @@ -862,7 +882,7 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, else: pool.extend(diff.basicarmor) - if want_progressives(): + if 'silvers' not in world.bow_mode[player]: pool.extend(['Progressive Bow'] * 2) elif swords != 'swordless': pool.extend(diff.basicbow) @@ -910,13 +930,14 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, 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] - pool = [item.replace('Arrows (10)','Rupees (5)') for item in pool] - pool = [item.replace('Arrow Upgrade (+5)','Rupees (5)') for item in pool] - pool = [item.replace('Arrow Upgrade (+10)','Rupees (5)') for item in pool] + if world.bow_mode[player].startswith('retro'): + pool = [item.replace('Single Arrow', 'Rupees (5)') for item in pool] + pool = [item.replace('Arrows (10)', 'Rupees (5)') for item in pool] + pool = [item.replace('Arrow Upgrade (+5)', 'Rupees (5)') for item in pool] + pool = [item.replace('Arrow Upgrade (+10)', 'Rupees (5)') for item in pool] + if world.keyshuffle[player] == 'universal': pool.extend(diff.retro) - if door_shuffle != 'vanilla': # door shuffle needs more keys for retro + if door_shuffle != 'vanilla': # door shuffle needs more keys for universal keys replace = 'Rupees (20)' if difficulty == 'normal' else 'Rupees (5)' indices = [i for i, x in enumerate(pool) if x == replace] for i in range(0, min(10, len(indices))): @@ -932,7 +953,7 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, return (pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms) -def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, swords, retro, bombbag, customitemarray): +def make_custom_item_pool(world, player, progressive, shuffle, difficulty, timer, goal, mode, swords, bombbag, customitemarray): if isinstance(customitemarray,dict) and 1 in customitemarray: customitemarray = customitemarray[1] pool = [] @@ -1014,7 +1035,7 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s itemtotal = itemtotal + 1 if mode == 'standard': - if retro: + if world.keyshuffle[player] == 'universal': key_location = random.choice(['Secret Passage', 'Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest', 'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross']) place_item(key_location, 'Small Key (Universal)') pool.extend(['Small Key (Universal)'] * max((customitemarray["generickeys"] - 1), 0)) @@ -1028,7 +1049,7 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s pool.extend(['Magic Mirror'] * customitemarray["mirror"]) pool.extend(['Moon Pearl'] * customitemarray["pearl"]) - if retro: + if world.keyshuffle[player] == 'universal': itemtotal = itemtotal - 28 # Corrects for small keys not being in item pool in Retro Mode if itemtotal < total_items_to_place: nothings = total_items_to_place - itemtotal @@ -1066,7 +1087,7 @@ def make_customizer_pool(world, player): 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]: + if world.keyshuffle[player] == 'none': d_name = d_item.dungeon dungeon = world.get_dungeon(d_name, player) target_amount = max(amount, len(dungeon.small_keys)) @@ -1121,7 +1142,7 @@ def make_customizer_pool(world, player): guaranteed_items = alwaysitems + ['Magic Mirror', 'Moon Pearl'] if world.shopsanity[player]: guaranteed_items.extend(['Blue Potion', 'Green Potion', 'Red Potion']) - if world.retro[player]: + if world.keyshuffle[player] == 'universal': guaranteed_items.append('Small Key (Universal)') for item in guaranteed_items: if item not in pool: @@ -1199,34 +1220,34 @@ def set_default_triforce(goal, custom_goal, custom_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', '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']: - for progressive in ['on', 'off']: - for shuffle in ['full']: - for logic in ['noglitches', 'minorglitches', 'owglitches', 'nologic']: - for retro in [True, False]: - for bombbag in [True, False]: - for door_shuffle in ['basic', 'crossed', 'vanilla']: - out = get_pool_core(progressive, shuffle, difficulty, 30, timer, goal, mode, swords, retro, bombbag, door_shuffle, logic) - count = len(out[0]) + len(out[1]) - - correct_count = total_items_to_place - if goal in ['pedestal', 'trinity'] and swords != 'vanilla': - # pedestal goals generate one extra item - correct_count += 1 - if retro: - correct_count += 28 - try: - assert count == correct_count, "expected {0} items but found {1} items for {2}".format(correct_count, count, (progressive, shuffle, difficulty, timer, goal, mode, swords, retro, bombbag)) - except AssertionError as e: - print(e) - -if __name__ == '__main__': - test() +# def test(): +# for difficulty in ['normal', 'hard', 'expert']: +# 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']: +# for progressive in ['on', 'off']: +# for shuffle in ['full']: +# for logic in ['noglitches', 'minorglitches', 'owglitches', 'nologic']: +# for retro in [True, False]: +# for bombbag in [True, False]: +# for door_shuffle in ['basic', 'crossed', 'vanilla']: +# out = get_pool_core(progressive, shuffle, difficulty, 30, timer, goal, mode, swords, retro, bombbag, door_shuffle, logic) +# count = len(out[0]) + len(out[1]) +# +# correct_count = total_items_to_place +# if goal in ['pedestal', 'trinity'] and swords != 'vanilla': +# # pedestal goals generate one extra item +# correct_count += 1 +# if retro: +# correct_count += 28 +# try: +# assert count == correct_count, "expected {0} items but found {1} items for {2}".format(correct_count, count, (progressive, shuffle, difficulty, timer, goal, mode, swords, retro, bombbag)) +# except AssertionError as e: +# print(e) +# +# if __name__ == '__main__': +# test() def fill_specific_items(world): @@ -1270,7 +1291,7 @@ def fill_specific_items(world): def is_dungeon_item(item, world, player): - return ((item.startswith('Small Key') and not world.keyshuffle[player]) + return ((item.startswith('Small Key') and world.keyshuffle[player] == 'none') 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/KeyDoorShuffle.py b/KeyDoorShuffle.py index 4d501764..9cca37e3 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -158,7 +158,7 @@ class PlacementRule(object): left -= rule_needed return False - def is_satisfiable(self, outside_keys, unplaced_keys, big_key_loc, prize_location, cr_count): + def is_satisfiable(self, outside_keys, wild_keys, unplaced_keys, big_key_loc, prize_location, cr_count): if self.prize_relevance and prize_location: if self.prize_relevance == 'BigBomb': if prize_location.item.name not in ['Crystal 5', 'Crystal 6']: @@ -272,7 +272,7 @@ def analyze_dungeon(key_layout, world, player): key_logic.bk_chests.update(find_big_chest_locations(key_layout.all_chest_locations)) key_logic.bk_chests.update(find_big_key_locked_locations(key_layout.all_chest_locations)) key_logic.prize_location = dungeon_table[key_layout.sector.name].prize - if world.retro[player] and world.mode[player] != 'standard': + if world.keyshuffle[player] == 'universal' and world.mode[player] != 'standard': return original_key_counter = find_counter({}, False, key_layout, False) @@ -923,7 +923,7 @@ def self_lock_possible(counter): def available_chest_small_keys(key_counter, world, player): - if not world.keyshuffle[player] and not world.retro[player]: + if world.keyshuffle[player] == 'none': cnt = 0 for loc in key_counter.free_locations: if key_counter.big_key_opened or '- Big Chest' not in loc.name: @@ -934,7 +934,7 @@ def available_chest_small_keys(key_counter, world, player): def available_chest_small_keys_logic(key_counter, world, player, sm_restricted): - if not world.keyshuffle[player] and not world.retro[player]: + if world.keyshuffle[player] == 'none': cnt = 0 for loc in key_counter.free_locations: if loc not in sm_restricted and (key_counter.big_key_opened or '- Big Chest' not in loc.name): @@ -1446,8 +1446,9 @@ def validate_bk_layout(proposal, builder, start_regions, world, player): # Soft lock stuff def validate_key_layout(key_layout, world, player): - # retro is all good - except for hyrule castle in standard mode - if (world.retro[player] and (world.mode[player] != 'standard' or key_layout.sector.name != 'Hyrule Castle')) or world.logic[player] == 'nologic': + # universal key is all good - except for hyrule castle in standard mode + if world.logic[player] == 'nologic' or (world.keyshuffle[player] == 'universal' and + (world.mode[player] != 'standard' or key_layout.sector.name != 'Hyrule Castle')): return True flat_proposal = key_layout.flat_prop state = ExplorationState(dungeon=key_layout.sector.name) @@ -1575,8 +1576,8 @@ def enough_small_locations(state, avail_small_loc): def determine_prize_lock(key_layout, world, player): - if ((world.retro[player] and (world.mode[player] != 'standard' or key_layout.sector.name != 'Hyrule Castle')) - or world.logic[player] == 'nologic'): + if world.logic[player] == 'nologic' or (world.keyshuffle[player] == 'universal' and + (world.mode[player] != 'standard' or key_layout.sector.name != 'Hyrule Castle')): return # done, doesn't matter what flat_proposal = key_layout.flat_prop state = ExplorationState(dungeon=key_layout.sector.name) @@ -1608,7 +1609,7 @@ def determine_prize_lock(key_layout, world, player): def cnt_avail_small_locations(free_locations, key_only, state, world, player): - if not world.keyshuffle[player] and not world.retro[player]: + if world.keyshuffle[player] == 'none': bk_adj = 1 if state.big_key_opened and not state.big_key_special else 0 avail_chest_keys = min(free_locations - bk_adj, state.key_locations - key_only) return max(0, avail_chest_keys + key_only - state.used_smalls) @@ -1616,7 +1617,7 @@ def cnt_avail_small_locations(free_locations, key_only, state, world, player): def cnt_avail_small_locations_by_ctr(free_locations, counter, layout, world, player): - if not world.keyshuffle[player] and not world.retro[player]: + if world.keyshuffle[player] == 'none': bk_adj = 1 if counter.big_key_opened and not layout.big_key_special else 0 avail_chest_keys = min(free_locations - bk_adj, layout.max_chests) return max(0, avail_chest_keys + len(counter.key_only_locations) - counter.used_keys) @@ -2059,14 +2060,14 @@ def val_rule(rule, skn, allow=False, loc=None, askn=None, setCheck=None): # Soft lock stuff def validate_key_placement(key_layout, world, player): - if world.retro[player] or world.accessibility[player] == 'none': + if world.keyshuffle[player] == 'universal' or world.accessibility[player] == 'none': return True # Can't keylock in retro. Expected if beatable only. max_counter = find_max_counter(key_layout) keys_outside = 0 big_key_outside = False smallkey_name = dungeon_keys[key_layout.sector.name] bigkey_name = dungeon_bigs[key_layout.sector.name] - if world.keyshuffle[player]: + if world.keyshuffle[player] != 'none': keys_outside = key_layout.max_chests - sum(1 for i in max_counter.free_locations if i.item is not None and i.item.name == smallkey_name and i.item.player == player) if world.bigkeyshuffle[player]: max_counter = find_max_counter(key_layout) diff --git a/Main.py b/Main.py index 1c6cc442..b1f94d6b 100644 --- a/Main.py +++ b/Main.py @@ -75,7 +75,7 @@ def main(args, seed=None, fish=None): 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) + args.accessibility, args.shuffleganon, args.custom, args.customitemarray, args.hints) world.customizer = customized if customized else None logger = logging.getLogger('') if seed is None: @@ -96,6 +96,7 @@ def main(args, seed=None, fish=None): world.bigkeyshuffle = args.bigkeyshuffle.copy() world.bombbag = args.bombbag.copy() world.flute_mode = args.flute_mode.copy() + world.bow_mode = args.bow_mode.copy() world.crystals_needed_for_ganon = {player: random.randint(0, 7) if args.crystals_ganon[player] == 'random' else int(args.crystals_ganon[player]) for player in range(1, world.players + 1)} world.crystals_needed_for_gt = {player: random.randint(0, 7) if args.crystals_gt[player] == 'random' else int(args.crystals_gt[player]) for player in range(1, world.players + 1)} world.crystals_ganon_orig = args.crystals_ganon.copy() @@ -124,11 +125,13 @@ def main(args, seed=None, fish=None): world.shuffletavern = args.shuffletavern.copy() world.pseudoboots = args.pseudoboots.copy() world.overworld_map = args.overworld_map.copy() + world.take_any = args.take_any.copy() world.restrict_boss_items = args.restrict_boss_items.copy() world.collection_rate = args.collection_rate.copy() world.colorizepots = args.colorizepots.copy() world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)} + world.finish_init() logger.info( world.fish.translate("cli","cli","app.title") + "\n", @@ -247,7 +250,7 @@ def main(args, seed=None, fish=None): for player in range(1, world.players + 1): if world.shopsanity[player]: sell_potions(world, player) - if world.retro[player]: + if world.keyshuffle[player] == 'universal': sell_keys(world, player) else: lock_shop_locations(world, player) @@ -425,7 +428,7 @@ def copy_world(world): # ToDo: Not good yet ret = World(world.players, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, - world.accessibility, world.shuffle_ganon, world.retro, world.custom, world.customitemarray, world.hints) + world.accessibility, world.shuffle_ganon, world.custom, world.customitemarray, world.hints) ret.teams = world.teams ret.player_names = copy.deepcopy(world.player_names) ret.remote_items = world.remote_items.copy() diff --git a/PotShuffle.py b/PotShuffle.py index 2848dfe8..fd59f6cc 100644 --- a/PotShuffle.py +++ b/PotShuffle.py @@ -879,7 +879,8 @@ def shuffle_pots(world, player): elif old_pot.item == PotItem.Switch: available_pots = (pot for pot in new_pots if (pot.room == old_pot.room or pot.room in movable_switch_rooms[old_pot.room]) and not (pot.flags & PotFlags.NoSwitch)) elif old_pot.item == PotItem.Key: - if world.doorShuffle[player] == 'vanilla' and not world.retro[player] and world.pottery[player] == 'none' and world.logic[player] != 'nologic': + if (world.doorShuffle[player] == 'vanilla' and world.keyshuffle[player] != 'universal' + and world.pottery[player] == 'none' and world.logic[player] != 'nologic'): available_pots = (pot for pot in new_pots if pot.room not in invalid_key_rooms) else: available_pots = new_pots @@ -890,7 +891,7 @@ def shuffle_pots(world, player): new_pot = random.choice(available_pots) new_pot.item = old_pot.item - if world.retro[player] and new_pot.item == PotItem.FiveArrows: + if world.bow_mode[player].startswith('retro') and new_pot.item == PotItem.FiveArrows: new_pot.item = PotItem.FiveRupees if new_pot.item == PotItem.Key: @@ -938,7 +939,7 @@ def shuffle_pot_switches(world, player): new_pot = random.choice(available_pots) new_pot.item, old_pot.item = old_pot.item, new_pot.item - if world.retro[player] and new_pot.item == PotItem.FiveArrows: + if world.bow_mode[player].startswith('retro') and new_pot.item == PotItem.FiveArrows: new_pot.item = PotItem.FiveRupees if new_pot.item == PotItem.Switch and (new_pot.flags & PotFlags.SwitchLogicChange): diff --git a/Regions.py b/Regions.py index d0ff68ab..3e288727 100644 --- a/Regions.py +++ b/Regions.py @@ -1029,7 +1029,7 @@ def adjust_locations(world, player): loc.event = False item_dungeon = key_item.dungeon dungeon = world.get_dungeon(item_dungeon, player) - if key_item.smallkey and not world.retro[player]: + if key_item.smallkey and world.keyshuffle[player] != 'universal': dungeon.small_keys.append(key_item) elif key_item.bigkey: dungeon.big_key = key_item diff --git a/Rom.py b/Rom.py index 458e24df..544f2c5b 100644 --- a/Rom.py +++ b/Rom.py @@ -797,7 +797,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_byte(0x138002, 2) for name, layout in world.key_layout[player].items(): offset = compass_data[name][4]//2 - if world.retro[player]: + if world.keyshuffle[player] == 'universal': rom.write_byte(0x13f030+offset, layout.max_chests + layout.max_drops) else: rom.write_byte(0x13f020+offset, layout.max_chests + layout.max_drops) # not currently used @@ -898,7 +898,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_byte(0x142A50, 1) # StandingItemsOn multiClientFlags = ((0x1 if world.dropshuffle[player] else 0) | (0x2 if world.shopsanity[player] else 0) - | (0x4 if world.retro[player] else 0) + | (0x4 if world.take_any[player] != 'none' else 0) | (0x8 if world.pottery[player] != 'none' else 0) | (0x10 if is_mystery else 0)) rom.write_byte(0x142A51, multiClientFlags) @@ -1112,7 +1112,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): prizes = [prize_replacements.get(prize, prize) for prize in prizes] dig_prizes = [prize_replacements.get(prize, prize) for prize in dig_prizes] - if world.retro[player]: + if world.bow_mode[player].startswith('retro'): prize_replacements = {0xE1: 0xDA, #5 Arrows -> Blue Rupee 0xE2: 0xDB} #10 Arrows -> Red Rupee prizes = [prize_replacements.get(prize, prize) for prize in prizes] @@ -1155,7 +1155,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): 0x12, 0x01, 0x35, 0xFF, # lamp -> 5 rupees 0x51, 0x00 if world.bombbag[player] else 0x06, 0x31 if world.bombbag[player] else 0x52, 0xFF, # 6 +5 bomb upgrades -> +10 bomb upgrade. If bombbag -> turns into Bombs (10) 0x53, 0x06, 0x54, 0xFF, # 6 +5 arrow upgrades -> +10 arrow upgrade - 0x58, 0x01, 0x36 if world.retro[player] else 0x43, 0xFF, # silver arrows -> single arrow (red 20 in retro mode) + 0x58, 0x01, 0x36 if world.bow_mode[player].startswith('retro') else 0x43, 0xFF, # silver arrows -> single arrow (red 20 in retro mode) 0x3E, difficulty.boss_heart_container_limit, 0x47, 0xff, # boss heart -> green 20 0x17, difficulty.heart_piece_limit, 0x47, 0xff, # piece of heart -> green 20 0xFF, 0xFF, 0xFF, 0xFF, # end of table sentinel @@ -1204,7 +1204,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): ERtimeincrease = 10 else: ERtimeincrease = 20 - if world.keyshuffle[player] or world.bigkeyshuffle[player] or world.mapshuffle[player]: + if world.keyshuffle[player] != 'none' or world.bigkeyshuffle[player] or world.mapshuffle[player]: ERtimeincrease = ERtimeincrease + 15 if world.clock_mode == 'none': rom.write_bytes(0x180190, [0x00, 0x00, 0x00]) # turn off clock mode @@ -1337,7 +1337,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): # m - enabled for inside maps # c - enabled for inside compasses # s - enabled for inside small keys - rom.write_byte(0x18016A, 0x10 | ((0x01 if world.keyshuffle[player] else 0x00) + rom.write_byte(0x18016A, 0x10 | ((0x01 if world.keyshuffle[player] == 'wild' else 0x00) | (0x02 if world.compassshuffle[player] else 0x00) | (0x04 if world.mapshuffle[player] else 0x00) | (0x08 if world.bigkeyshuffle[player] else 0x00))) # free roaming item text boxes @@ -1403,7 +1403,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): # a - Small Key # enable_menu_map_check = world.overworld_map[player] != 'default' and world.shuffle[player] != 'none' - rom.write_byte(0x180045, ((0x01 if world.keyshuffle[player] else 0x00) + rom.write_byte(0x180045, ((0x01 if world.keyshuffle[player] == 'wild' else 0x00) | (0x02 if world.bigkeyshuffle[player] else 0x00) | (0x04 if world.mapshuffle[player] or enable_menu_map_check else 0x00) | (0x08 if world.compassshuffle[player] else 0x00))) # free roaming items in menu @@ -1434,17 +1434,17 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): write_int16(rom, 0x18017A, get_reveal_bytes('Green Pendant') if world.mapshuffle[player] else 0x0000) # Sahasrahla reveal write_int16(rom, 0x18017C, get_reveal_bytes('Crystal 5')|get_reveal_bytes('Crystal 6') if world.mapshuffle[player] else 0x0000) # Bomb Shop Reveal - rom.write_byte(0x180172, 0x01 if world.retro[player] else 0x00) # universal keys - rom.write_byte(0x180175, 0x01 if world.retro[player] else 0x00) # rupee bow - rom.write_byte(0x180176, 0x0A if world.retro[player] else 0x00) # wood arrow cost - rom.write_byte(0x180178, 0x32 if world.retro[player] else 0x00) # silver arrow cost - rom.write_byte(0x301FC, 0xDA if world.retro[player] else 0xE1) # rupees replace arrows under pots + rom.write_byte(0x180172, 0x01 if world.keyshuffle[player] == 'universal' else 0x00) # universal keys + rom.write_byte(0x180175, 0x01 if world.bow_mode[player].startswith('retro') else 0x00) # rupee bow + rom.write_byte(0x180176, 0x0A if world.bow_mode[player].startswith('retro') else 0x00) # wood arrow cost + rom.write_byte(0x180178, 0x32 if world.bow_mode[player].startswith('retro') else 0x00) # silver arrow cost + rom.write_byte(0x301FC, 0xDA if world.bow_mode[player].startswith('retro') else 0xE1) # rupees replace arrows under pots if enemized: - rom.write_byte(0x1B152e, 0xDA if world.retro[player] else 0xE1) - rom.write_byte(0x30052, 0xDB if world.retro[player] else 0xE2) # replace arrows in fish prize from bottle merchant - rom.write_bytes(0xECB4E, [0xA9, 0x00, 0xEA, 0xEA] if world.retro[player] else [0xAF, 0x77, 0xF3, 0x7E]) # Thief steals rupees instead of arrows - rom.write_bytes(0xF0D96, [0xA9, 0x00, 0xEA, 0xEA] if world.retro[player] else [0xAF, 0x77, 0xF3, 0x7E]) # Pikit steals rupees instead of arrows - rom.write_bytes(0xEDA5, [0x35, 0x41] if world.retro[player] else [0x43, 0x44]) # Chest game gives rupees instead of arrows + rom.write_byte(0x1B152e, 0xDA if world.bow_mode[player].startswith('retro') else 0xE1) + rom.write_byte(0x30052, 0xDB if world.bow_mode[player].startswith('retro') else 0xE2) # replace arrows in fish prize from bottle merchant + rom.write_bytes(0xECB4E, [0xA9, 0x00, 0xEA, 0xEA] if world.bow_mode[player].startswith('retro') else [0xAF, 0x77, 0xF3, 0x7E]) # Thief steals rupees instead of arrows + rom.write_bytes(0xF0D96, [0xA9, 0x00, 0xEA, 0xEA] if world.bow_mode[player].startswith('retro') else [0xAF, 0x77, 0xF3, 0x7E]) # Pikit steals rupees instead of arrows + rom.write_bytes(0xEDA5, [0x35, 0x41] if world.bow_mode[player].startswith('retro') else [0x43, 0x44]) # Chest game gives rupees instead of arrows digging_game_rng = random.randint(1, 30) # set rng for digging game rom.write_byte(0x180020, digging_game_rng) rom.write_byte(0xEFD95, digging_game_rng) @@ -2151,7 +2151,7 @@ def write_strings(rom, world, player, team): # It is done the way it is to re-use the silver code and also to give one hint per each type of item regardless # of how many exist. This supports many settings well. items_to_hint = RelevantItems.copy() - if world.keyshuffle[player]: + if world.keyshuffle[player] == 'wild': items_to_hint.extend(SmallKeys) if world.bigkeyshuffle[player]: items_to_hint.extend(BigKeys) diff --git a/Rules.py b/Rules.py index 9d3dad12..5bd27023 100644 --- a/Rules.py +++ b/Rules.py @@ -2072,7 +2072,7 @@ def add_key_logic_rules(world, player): add_rule(big_chest, create_rule(d_logic.bk_name, player)) if len(d_logic.bk_doors) == 0 and len(d_logic.bk_chests) <= 1: set_always_allow(big_chest, allow_big_key_in_big_chest(d_logic.bk_name, player)) - if world.retro[player]: + if world.keyshuffle[player] == 'universal': for d_name, layout in world.key_layout[player].items(): for door in layout.flat_prop: if world.mode[player] != 'standard' or not retro_in_hc(door.entrance): diff --git a/mystery_example.yml b/mystery_example.yml index 78b47a95..26fb559f 100644 --- a/mystery_example.yml +++ b/mystery_example.yml @@ -48,6 +48,11 @@ flute_mode: normal: 3 active: 1 + bow_mode: + progressive: 5 + silvers: 5 + retro: 1 + retro_silvers: 1 entrance_shuffle: none: 15 dungeonssimple: 3 @@ -66,9 +71,10 @@ open: 1 inverted: 1 retro: 0 - retro: - on: 1 - off: 4 + take_any: + random: 1 + fixed: 3 + none: 16 goals: ganon: 2 fast_ganon: 2 @@ -86,6 +92,12 @@ mc: 3 mcs: 2 full: 5 + mcu: 1 # map, compass, universal smalls +# for use when you aren't using the dungeon_items above +# smallkey_shuffle: +# standard: 5 +# wild: 1 +# universal: 1 dungeon_counters: on: 5 off: 0 diff --git a/mystery_testsuite.yml b/mystery_testsuite.yml index 364806e6..aca535c3 100644 --- a/mystery_testsuite.yml +++ b/mystery_testsuite.yml @@ -50,7 +50,6 @@ world_state: standard: 1 open: 1 inverted: 1 - retro: 0 retro: on: 1 off: 1 diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index d25f542b..0074143d 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -48,6 +48,14 @@ "active" ] }, + "bow_mode": { + "choices": [ + "progressive", + "silvers", + "retro", + "retro_silvers" + ] + }, "goal": { "choices": [ "ganon", @@ -266,8 +274,11 @@ "type": "bool" }, "keyshuffle": { - "action": "store_true", - "type": "bool" + "choices": [ + "none", + "wild", + "universal" + ] }, "bigkeyshuffle": { "action": "store_true", @@ -286,6 +297,13 @@ "action": "store_true", "type": "bool" }, + "take_any": { + "choices": [ + "none", + "random", + "fixed" + ] + }, "startinventory": {}, "usestartinventory": { "type": "bool" diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index eab0e34d..a6dcc3ee 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -122,10 +122,17 @@ "Expert: Greatly reduced functionality." ], "flute_mode": [ - "Determine if you need to wake up the bird or not on flute pickup (default: %(default)s)", + "Determines if you need to wake up the bird or not on flute pickup (default: %(default)s)", "Normal: Normal functionality.", "Active: Flute is activated on pickup." ], + "bow_mode": [ + "Determines how the bow acts in the pool (default: %(default)s)", + "Progressive: Two progressive bows placed. First picked up is the bow. Second is silvers.", + "Silvers Separate: Bow and silvers are completely separate items.", + "Retro: Z1 Bow where arrows cost money and the Single Arrow must be bought or found to shoot", + "Retro + Silvers: Bow and silvers are completely separate items." + ], "timer": [ "Select game timer setting. Affects available itempool. (default: %(default)s)", "None: No timer.", @@ -300,6 +307,12 @@ "Keys are universal, shooting arrows costs rupees,", "and a few other little things make this more like Zelda-1. (default: %(default)s)" ], + "take_any": [ + "Take Any caves from Zelda 1 (default: %(default)s)", + "None: No take any caves", + "Random: Take any caves can replace a random set of un-interesting caves. See documentation for full list", + "Fixed: Take any caves will replace certain location. See documentation for full list" + ], "pseudoboots": [ " Players starts with pseudo boots that allow dashing but no item checks (default: %(default)s"], "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)" ], diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index 5a6e0a9f..9c7f9dfa 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -53,6 +53,9 @@ "randomizer.dungeon.mapshuffle": "Maps", "randomizer.dungeon.compassshuffle": "Compasses", "randomizer.dungeon.smallkeyshuffle": "Small Keys", + "randomizer.dungeon.smallkeyshuffle.standard": "In Dungeon", + "randomizer.dungeon.smallkeyshuffle.wild": "Randomized", + "randomizer.dungeon.smallkeyshuffle.universal": "Universal", "randomizer.dungeon.bigkeyshuffle": "Big Keys", "randomizer.dungeon.keydropshuffle": "Key Drop Shuffle (Legacy)", "randomizer.dungeon.decoupledoors": "Decouple Doors", @@ -155,7 +158,10 @@ "randomizer.entrance.entranceshuffle.dungeonsfull": "Dungeons + Full", "randomizer.entrance.entranceshuffle.dungeonssimple": "Dungeons + Simple", - + "randomizer.entrance.take_any": "Take Any Caves", + "randomizer.entrance.take_any.none": "None", + "randomizer.entrance.take_any.random": "Random", + "randomizer.entrance.take_any.fixed": "Fixed", "randomizer.gameoptions.nobgm": "Disable Music & MSU-1", "randomizer.gameoptions.quickswap": "L/R Quickswapping", @@ -219,7 +225,7 @@ "randomizer.generation.rom.dialog.allfiles": "All Files", "randomizer.item.hints": "Include Helpful Hints", - "randomizer.item.retro": "Retro mode (universal keys)", + "randomizer.item.retro": "Retro mode", "randomizer.item.pseudoboots": "Start with Pseudo Boots", "randomizer.item.bombbag": "Bombbag", @@ -289,6 +295,12 @@ "randomizer.item.flute_mode.normal": "Normal", "randomizer.item.flute_mode.active": "Pre-Activated", + "randomizer.item.bow_mode": "Bow Mode", + "randomizer.item.bow_mode.progressive": "Progressive", + "randomizer.item.bow_mode.silvers": "Silvers Separate", + "randomizer.item.bow_mode.retro": "Retro (Progressive)", + "randomizer.item.bow_mode.retro_silvers": "Retro + Silvers", + "randomizer.item.timer": "Timer Setting", "randomizer.item.timer.none": "No Timer", "randomizer.item.timer.display": "Stopwatch", diff --git a/resources/app/gui/randomize/dungeon/keysanity.json b/resources/app/gui/randomize/dungeon/keysanity.json index 49a17237..ffd9bc92 100644 --- a/resources/app/gui/randomize/dungeon/keysanity.json +++ b/resources/app/gui/randomize/dungeon/keysanity.json @@ -2,7 +2,6 @@ "keysanity": { "mapshuffle": { "type": "checkbox" }, "compassshuffle": { "type": "checkbox" }, - "smallkeyshuffle": { "type": "checkbox" }, "bigkeyshuffle": { "type": "checkbox" } } } diff --git a/resources/app/gui/randomize/dungeon/widgets.json b/resources/app/gui/randomize/dungeon/widgets.json index df7d3474..02a7f262 100644 --- a/resources/app/gui/randomize/dungeon/widgets.json +++ b/resources/app/gui/randomize/dungeon/widgets.json @@ -1,5 +1,13 @@ { "widgets": { + "keyshuffle": { + "type": "selectbox", + "options": [ + "standard", + "wild", + "universal" + ] + }, "dungeondoorshuffle": { "type": "selectbox", "default": "basic", diff --git a/resources/app/gui/randomize/entrando/widgets.json b/resources/app/gui/randomize/entrando/widgets.json index b4c6698c..a136de91 100644 --- a/resources/app/gui/randomize/entrando/widgets.json +++ b/resources/app/gui/randomize/entrando/widgets.json @@ -29,6 +29,14 @@ "config": { "width": 45 } + }, + "take_any": { + "type": "selectbox", + "options": [ + "none", + "random", + "fixed" + ] } } } diff --git a/resources/app/gui/randomize/item/widgets.json b/resources/app/gui/randomize/item/widgets.json index b55d6117..88f85858 100644 --- a/resources/app/gui/randomize/item/widgets.json +++ b/resources/app/gui/randomize/item/widgets.json @@ -85,6 +85,15 @@ "active" ] }, + "bow_mode": { + "type": "selectbox", + "options": [ + "progressive", + "silvers", + "retro", + "retro_silvers" + ] + }, "timer": { "type": "selectbox", "options": [ diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py index 82cf0ac9..08150a77 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -69,11 +69,13 @@ class CustomSettings(object): args.mode[p] = get_setting(settings['mode'], args.mode[p]) args.swords[p] = get_setting(settings['swords'], args.swords[p]) args.flute_mode[p] = get_setting(settings['flute_mode'], args.flute_mode[p]) + args.bow_mode[p] = get_setting(settings['bow_mode'], args.bow_mode[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.take_any[p] = get_setting(settings['take_any'], args.take_any[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]) @@ -191,11 +193,12 @@ class CustomSettings(object): settings_dict[p]['mode'] = world.mode[p] settings_dict[p]['swords'] = world.swords[p] settings_dict[p]['flute_mode'] = world.flute_mode[p] + settings_dict[p]['bow_mode'] = world.bow_mode[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]['take_any'] = world.take_any[p] settings_dict[p]['hints'] = world.hints[p] settings_dict[p]['shopsanity'] = world.shopsanity[p] settings_dict[p]['dropshuffle'] = world.dropshuffle[p] diff --git a/source/classes/constants.py b/source/classes/constants.py index 3fb8da88..1f801e33 100644 --- a/source/classes/constants.py +++ b/source/classes/constants.py @@ -68,6 +68,7 @@ SETTINGSTOPROCESS = { "weapons": "swords", "itempool": "difficulty", "flute_mode": "flute_mode", + "bow_mode": "bow_mode", "timer": "timer", "accessibility": "accessibility", "sortingalgo": "algorithm", @@ -81,6 +82,7 @@ SETTINGSTOPROCESS = { "shuffletavern": "shuffletavern", "entranceshuffle": "shuffle", "overworld_map": "overworld_map", + "take_any": "take_any", }, "enemizer": { "enemyshuffle": "shuffleenemies", diff --git a/source/gui/bottom.py b/source/gui/bottom.py index b887e1ac..27a22217 100644 --- a/source/gui/bottom.py +++ b/source/gui/bottom.py @@ -285,4 +285,12 @@ def create_guiargs(parent): guiargs.dropshuffle = 1 guiargs.pottery = 'keys' if guiargs.pottery == 'none' else guiargs.pottery + if guiargs.retro or guiargs.mode == 'retro': + if guiargs.bow_mode == 'progressive': + guiargs.bow_mode = 'retro' + elif guiargs.bow_mode == 'silvers': + guiargs.bow_mode = 'retro_silvers' + guiargs.take_any = 'random' if guiargs.take_any == 'none' else guiargs.take_any + guiargs.keyshuffle = 'universal' + return guiargs diff --git a/source/item/FillUtil.py b/source/item/FillUtil.py index 6f60b72e..8cb74805 100644 --- a/source/item/FillUtil.py +++ b/source/item/FillUtil.py @@ -82,10 +82,11 @@ def create_item_pool_config(world): if world.shopsanity[player]: for item, locs in shop_vanilla_mapping.items(): config.static_placement[player][item].extend(locs) - if world.retro[player]: + if world.take_any[player] != 'none': for item, locs in retro_vanilla_mapping.items(): config.static_placement[player][item].extend(locs) # universal keys + if world.keyshuffle[player] == 'universal': universal_key_locations = [] for item, locs in vanilla_mapping.items(): if 'Small Key' in item: @@ -98,12 +99,13 @@ def create_item_pool_config(world): for item, locs in potkeys_vanilla_mapping.items(): universal_key_locations.extend(locs) if world.shopsanity[player]: - single_arrow_placement = list(shop_vanilla_mapping['Red Potion']) - single_arrow_placement.append('Red Shield Shop - Right') - config.static_placement[player]['Single Arrow'] = single_arrow_placement universal_key_locations.extend(shop_vanilla_mapping['Small Heart']) universal_key_locations.extend(shop_vanilla_mapping['Blue Shield']) config.static_placement[player]['Small Key (Universal)'] = universal_key_locations + if world.bow_mode[player].startswith('retro') and world.shopsanity[player]: + single_arrow_placement = list(shop_vanilla_mapping['Red Potion']) + single_arrow_placement.append('Red Shield Shop - Right') + config.static_placement[player]['Single Arrow'] = single_arrow_placement config.location_groups[player] = [ LocationGroup('Major').locs(mode_grouping['Overworld Major'] + mode_grouping['Big Chests'] + mode_grouping['Heart Containers']), LocationGroup('bkhp').locs(mode_grouping['Heart Pieces']), @@ -124,7 +126,7 @@ def create_item_pool_config(world): groups.locations.extend(mode_grouping['Big Keys']) if world.dropshuffle[player] != 'none': groups.locations.extend(mode_grouping['Big Key Drops']) - if world.keyshuffle[player]: + if world.keyshuffle[player] != 'none': groups.locations.extend(mode_grouping['Small Keys']) if world.dropshuffle[player] != 'none': groups.locations.extend(mode_grouping['Key Drops']) @@ -137,7 +139,7 @@ def create_item_pool_config(world): if world.shopsanity[player]: groups.locations.append('Capacity Upgrade - Left') groups.locations.append('Capacity Upgrade - Right') - if world.retro[player]: + if world.take_any[player] != 'none': if world.shopsanity[player]: groups.locations.extend(retro_vanilla_mapping['Heart Container']) groups.locations.append('Old Man Sword Cave Item 1') @@ -249,7 +251,7 @@ def previously_reserved(location, world, player): if world.restrict_boss_items[player] == 'dungeon' and (not world.compassshuffle[player] or not world.mapshuffle[player] or not world.bigkeyshuffle[player] - or not (world.keyshuffle[player] or world.retro[player])): + or world.keyshuffle[player] == 'standard'): return True return False @@ -335,7 +337,7 @@ def determine_major_items(world, player): pass # now what? if world.bigkeyshuffle[player]: major_item_set.update({x for x, y in item_table.items() if y[2] == 'BigKey'}) - if world.keyshuffle[player]: + if world.keyshuffle[player] != 'none': major_item_set.update({x for x, y in item_table.items() if y[2] == 'SmallKey'}) if world.compassshuffle[player]: major_item_set.update({x for x, y in item_table.items() if y[2] == 'Compass'}) @@ -344,8 +346,9 @@ def determine_major_items(world, player): if world.shopsanity[player]: major_item_set.add('Bomb Upgrade (+5)') major_item_set.add('Arrow Upgrade (+5)') - if world.retro[player]: + if world.bow_mode[player].startswith('retro'): major_item_set.add('Single Arrow') + if world.keyshuffle[player] == 'universal': major_item_set.add('Small Key (Universal)') if world.goal == 'triforcehunt': major_item_set.add('Triforce Piece') diff --git a/source/tools/MysteryUtils.py b/source/tools/MysteryUtils.py index ae628b7c..38ee8c1a 100644 --- a/source/tools/MysteryUtils.py +++ b/source/tools/MysteryUtils.py @@ -64,7 +64,13 @@ def roll_settings(weights): 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 + if 'smallkey_shuffle' in weights: + ret.keyshuffle = get_choice('smallkey_shuffle') + else: + if 's' in dungeon_items: + ret.keyshuffle = 'wild' + if 'u' in dungeon_items: + ret.keyshuffle = 'universal' ret.bigkeyshuffle = get_choice('bigkey_shuffle') == 'on' if 'bigkey_shuffle' in weights else 'b' in dungeon_items ret.accessibility = get_choice('accessibility') @@ -130,6 +136,7 @@ def roll_settings(weights): ret.mode = 'open' ret.retro = True ret.retro = get_choice('retro') == 'on' # this overrides world_state if used + ret.take_any = get_choice_default('take_any', default='none') ret.bombbag = get_choice('bombbag') == 'on' @@ -145,6 +152,7 @@ def roll_settings(weights): ret.difficulty = get_choice('item_pool') ret.flute_mode = get_choice_default('flute_mode', default='normal') + ret.bow_mode = get_choice_default('bow_mode', default='progressive') ret.item_functionality = get_choice('item_functionality') From 4e418f4218bf9d30ea7771d765c8a16faeec4d16 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 2 Sep 2022 13:10:03 -0600 Subject: [PATCH 41/63] Important key fix for logic placment --- Fill.py | 3 ++- KeyDoorShuffle.py | 23 +++++++++++++---------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/Fill.py b/Fill.py index db18996f..f695bd9a 100644 --- a/Fill.py +++ b/Fill.py @@ -173,7 +173,8 @@ def valid_key_placement(item, location, key_pool, world): if key_logic.prize_location: prize_loc = world.get_location(key_logic.prize_location, location.player) cr_count = world.crystals_needed_for_gt[location.player] - return key_logic.check_placement(unplaced_keys, location if item.bigkey else None, prize_loc, cr_count) + wild_keys = world.keyshuffle[item.player] != 'none' + return key_logic.check_placement(unplaced_keys, wild_keys, location if item.bigkey else None, prize_loc, cr_count) else: return not item.is_inside_dungeon_item(world) diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 9cca37e3..6d495aed 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -62,9 +62,9 @@ class KeyLogic(object): self.sm_doors = {} self.prize_location = None - def check_placement(self, unplaced_keys, big_key_loc=None, prize_loc=None, cr_count=7): + def check_placement(self, unplaced_keys, wild_keys, big_key_loc=None, prize_loc=None, cr_count=7): for rule in self.placement_rules: - if not rule.is_satisfiable(self.outside_keys, unplaced_keys, big_key_loc, prize_loc, cr_count): + if not rule.is_satisfiable(self.outside_keys, wild_keys, unplaced_keys, big_key_loc, prize_loc, cr_count): return False if big_key_loc: for rule_a, rule_b in itertools.combinations(self.placement_rules, 2): @@ -186,17 +186,20 @@ class PlacementRule(object): if not bk_blocked and check_locations is None: return True available_keys = outside_keys - empty_chests = 0 # todo: sometimes we need an extra empty chest to accomodate the big key too # dungeon bias seed 563518200 for example threshold = self.needed_keys_wo_bk if bk_blocked else self.needed_keys_w_bk - for loc in check_locations: - if not loc.item: - empty_chests += 1 - elif loc.item and loc.item.name == self.small_key: - available_keys += 1 - place_able_keys = min(empty_chests, unplaced_keys) - available_keys += place_able_keys + if not wild_keys: + empty_chests = 0 + for loc in check_locations: + if not loc.item: + empty_chests += 1 + elif loc.item and loc.item.name == self.small_key: + available_keys += 1 + place_able_keys = min(empty_chests, unplaced_keys) + available_keys += place_able_keys + else: + available_keys += unplaced_keys return available_keys >= threshold From 5850edfbe6f7d99a3e62461c36dd9baaded90834 Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 7 Sep 2022 16:49:47 -0600 Subject: [PATCH 42/63] Couple minor issues - let a door not be dead in higher intensity, slightly better reporting --- Doors.py | 7 ++++++- ItemList.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Doors.py b/Doors.py index c80ea51a..32ffd9ae 100644 --- a/Doors.py +++ b/Doors.py @@ -437,7 +437,7 @@ def create_doors(world, player): create_door(player, 'PoD Dark Basement W Up Stairs', Sprl).dir(Up, 0x6a, 0, HTH).ss(S, 0x1b, 0x3c, True), create_door(player, 'PoD Dark Basement E Up Stairs', Sprl).dir(Up, 0x6a, 1, HTH).ss(S, 0x1b, 0x9c, True), create_door(player, 'PoD Dark Alley NE', Nrml).dir(No, 0x6a, Right, High).big_key().pos(0), - create_door(player, 'PoD Mimics 2 SW', Nrml).dir(So, 0x1b, Left, High).pos(1).kill().portal(Z, 0x00), + create_door(player, 'PoD Mimics 2 SW', Nrml).dir(So, 0x1b, Left, High).pos(1).portal(Z, 0x00), create_door(player, 'PoD Mimics 2 NW', Intr).dir(No, 0x1b, Left, High).pos(0), create_door(player, 'PoD Bow Statue SW', Intr).dir(So, 0x1b, Left, High).pos(0), create_door(player, 'PoD Bow Statue Left to Right Barrier - Orange', Lgcl), @@ -1473,6 +1473,11 @@ def create_doors(world, player): world.get_door('GT Spike Crystal Right to Left Barrier - Orange', player).barrier(CrystalBarrier.Orange) world.get_door('GT Spike Crystal Left to Right Bypass', player).barrier(CrystalBarrier.Blue) + # kill certain doors + if world.intensity[player] == 1: # due to ladder & warp being fixed + world.get_door('PoD Mimics 2 SW', player).kill() + + # nifty dynamic logical doors: south_controller = world.get_door('Ice Cross Bottom SE', player) east_controller = world.get_door('Ice Cross Right ES', player) diff --git a/ItemList.py b/ItemList.py index c1981365..2e7f0040 100644 --- a/ItemList.py +++ b/ItemList.py @@ -544,7 +544,7 @@ def fill_prizes(world, attempts=15): continue break else: - raise FillError('Unable to place dungeon prizes') + raise FillError(f'Unable to place dungeon prizes {", ".join(list(map(lambda d: d.hint_text, prize_locs)))}') def set_up_shops(world, player): From e2b9735600b34fa6d6ccf7c7e1616c4dcfee07fb Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 7 Sep 2022 16:50:44 -0600 Subject: [PATCH 43/63] Rain prevention fix for keydoors (plus test case) Couple fixes for standard throne --- DungeonGenerator.py | 19 +++++++++++-------- Rom.py | 2 +- data/base2current.bps | Bin 93432 -> 93438 bytes source/dungeon/DungeonStitcher.py | 12 ++++++++++-- test/customizer/rainprevent_keydoor.yaml | 13 +++++++++++++ 5 files changed, 35 insertions(+), 11 deletions(-) create mode 100644 test/customizer/rainprevent_keydoor.yaml diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 445ed9b3..f982cb6f 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -16,6 +16,7 @@ from Dungeons import dungeon_regions, split_region_starts from RoomData import DoorKind from source.dungeon.DungeonStitcher import generate_dungeon_find_proposal +from source.dungeon.DungeonStitcher import GenerationException as OtherGenException class GraphPiece: @@ -1505,6 +1506,8 @@ def calc_allowance_and_dead_ends(builder, connections_tuple, world, player): builder.branches -= 1 if entrance not in drop_entrances_allowance: needed_connections.append(entrance) + if builder.sewers_access: + starting_allowance += 1 builder.allowance = starting_allowance for entrance in needed_connections: sector = find_sector(entrance, builder.sectors) @@ -3051,7 +3054,7 @@ def split_dungeon_builder(builder, split_list, builder_info): comb_w_replace = len(dungeon_map) ** len(candidate_sectors) return balance_split(candidate_sectors, dungeon_map, global_pole, builder_info) except (GenerationException, NeutralizingException): - if comb_w_replace and comb_w_replace <= 10000: + if comb_w_replace and comb_w_replace <= 10000 and not builder.throne_door: attempts += 5 # all the combinations were tried already, no use repeating else: attempts += 1 @@ -3535,7 +3538,7 @@ def check_for_valid_layout(builder, sector_list, builder_info): split_list['Sewers'].remove(temp_builder.throne_door.entrance.parent_region.name) builder.exception_list = list(sector_list) return True, {}, package - except (GenerationException, NeutralizingException): + except (GenerationException, NeutralizingException, OtherGenException): builder.split_dungeon_map = None builder.valid_proposal = None if temp_builder.name == 'Hyrule Castle' and temp_builder.throne_door: @@ -3948,20 +3951,20 @@ def find_free_equation(equations): def copy_door_equations(builder, sector_list): equations = {} for sector in builder.sectors + sector_list: - sector.equations = calc_sector_equations(sector) + sector.equations = calc_sector_equations(sector, builder.sewers_access) curr_list = equations[sector] = [] for equation in sector.equations: curr_list.append(equation.copy()) return equations -def calc_sector_equations(sector): +def calc_sector_equations(sector, sewers_flag=False): equations = [] - is_entrance = sector.is_entrance_sector() and not sector.destination_entrance + is_entrance = (sector.is_entrance_sector() and not sector.destination_entrance) or sewers_flag if is_entrance: flagged_equations = [] for door in sector.outstanding_doors: - equation, flag = calc_door_equation(door, sector, True) + equation, flag = calc_door_equation(door, sector, True, sewers_flag) if flag: flagged_equations.append(equation) equations.append(equation) @@ -3977,9 +3980,9 @@ def calc_sector_equations(sector): return equations -def calc_door_equation(door, sector, look_for_entrance): +def calc_door_equation(door, sector, look_for_entrance, sewers_flag=None): if look_for_entrance and not door.blocked: - flag = sector.is_entrance_sector() + flag = sector.is_entrance_sector() or sewers_flag if flag: eq = DoorEquation(door) eq.benefit[hook_from_door(door)].append(door) diff --git a/Rom.py b/Rom.py index 544f2c5b..18677375 100644 --- a/Rom.py +++ b/Rom.py @@ -37,7 +37,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '0be31dc5cb338e7e85d1ce65e839c99e' +RANDOMIZERBASEHASH = '61c296effe6180274721d570d2471e1c' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index 9740c94d9fb0f54c63443282dd8a82eff36b7240..03d6471ed59fbc3a0aa2fcb13383c31d53c06f20 100644 GIT binary patch delta 950 zcmW-dZ%i9?7{~AFwX~y!7HC;JVEiZ5+|)Re`KM}NXu(BYvZ=s+ zS3E*-3wJ%r16&qL<(xoB7n-Gk1=(?X5jcC@~6{$f1vW^xOcESU!V4=iNhe570d6EU6 z(NZV!?um73)}+qlL?&Ek-)gfSXuSAaII5#8sgzdIs&nL|HN1HprnV`19!O<3%~JHE9X$~e7RU~Mw04sb^;Ij0p-$KIrk2Yl1?^oZzixh+f1gD-<0n zn&S-espjynlk@75_JS`IhIC83PBChOmL?TLSPxV_R3cr`k*gBVC>C_#mKAmtzIcN( z^#`X7ik{SoYxBcPyjhA}w)nkP`5+#(L1~7ibUWocvDF4{n8ix^d`F`8 zn}+@}8u|fObHJ6gDNz^p4rdQgEBOWg$bsXwih;m=o76ZcS!*10KQ&`hE;Ll@UJgw| z>U6y3&#sj+yT&z#9(mz+?Ec19oSbjWWNu?>XtfI~=wri}Jy@jcqzIgKfo7#jGl60K>a#tooBslPHFN6# delta 973 zcmW+xe@q)?7{0gHqb)03+qFO)KR&3?R-6GhCqjm}O`H`Ii9d)gagECyShi%ifHMkT z%MLA*wbvVbz`0Num(UW@>1n#nU>S<6e_2g18OfFmGlTyuXSNZXk$Us5_j#Z9$@9F= z)(vL!26O3M&>9YJM1}WYCrry>D5k+B&uM?uKuGS=kdtILWvw-e!2mYn$lnWDg+T+nN-pv!FI?v>^8Qc#`-nOjNxe~8fc^QQ znIR94_6rlk63s==b%yCGX_b^0eZF{=SkX$PX>n37h&`mH?ikyx%$e5M}i_% zAQ&e(C3oVP=r9SfV6fL+SrNk$jXoUkqIg(B=$o*+i9%ap@)Zjud(2n;jM#QGawk5o zEUCq3Bno{ha2iRcE-H#kOhgMqph1%YM@dGKN3@6i3XlnJf-1P1yles%YJV26b%6<0;oxrqj_7aC zdDLip)4M=XiYr9?4iXr;Cty4Id`y2Hv*;GtG{Y;V%JaT8ldyM0U>j|?7Bl2^3p5vY zw*}`Qb12bxuQ$C;ho71)=KZ^gfn2hH-B9lhiqIJpJISvW^^Y?0zyin2=SD+Q#B~B+ zn_ljnuTjuBa(p{PEX_nP1tcAnp<9|s%x8UAW(ijKpk%QhR{7K7Qz1O}R&%arE}63I{#HlL4WS z9JGNO_LD9fG@1{Tj0X!`-cXva82D*=RV{tZ22Lm?f7)PYUSE#LE!G?pc7Na}r<0BX zXfe>$r~64)8C21p-u|Rm2KfL@B#vMo%ThtUz1|m%ChWVyVpsAX1R8)hlUwDm>CtbV L-S-)h+rIn{?#hFv diff --git a/source/dungeon/DungeonStitcher.py b/source/dungeon/DungeonStitcher.py index aa74dee0..b5e7515e 100644 --- a/source/dungeon/DungeonStitcher.py +++ b/source/dungeon/DungeonStitcher.py @@ -56,6 +56,8 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon if (access_region.name in world.inaccessible_regions[player] and region.name not in world.enabled_entrances[player]): excluded[region] = None + elif split_dungeon and builder.sewers_access and builder.sewers_access.entrance.parent_region == region: + continue elif len(region.entrances) == 1: # for holes access_region = next(x.parent_region for x in region.entrances if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld] @@ -182,11 +184,17 @@ def modify_proposal(proposed_map, explored_state, doors_to_connect, hash_code_se return proposed_map, hash_code attempt, opp_hook = None, None - opp_hook_len = 0 + opp_hook_len, possible_swaps = 0, list(visited_choices) while opp_hook_len == 0: - attempt = random.choice(visited_choices) + if len(possible_swaps) == 0: + break + attempt = random.choice(possible_swaps) + possible_swaps.remove(attempt) opp_hook = type_map[hook_from_door(attempt)] opp_hook_len = len(unvisted_bucket[opp_hook]) + if opp_hook_len == 0: + itr += 1 + continue unvisted_bucket[opp_hook].sort(key=lambda d: d.name) new_door = random.choice(unvisted_bucket[opp_hook]) old_target = proposed_map[attempt] diff --git a/test/customizer/rainprevent_keydoor.yaml b/test/customizer/rainprevent_keydoor.yaml new file mode 100644 index 00000000..31930dce --- /dev/null +++ b/test/customizer/rainprevent_keydoor.yaml @@ -0,0 +1,13 @@ +meta: + players: 1 +settings: + 1: + door_shuffle: crossed + mode: standard + shuffle: crossed +doors: + 1: + doors: + Hyrule Castle East Lobby N: + dest: Sewers Secret Room Key Door S + type: Key Door From db43ce8d15fd88503a8fc9f1a674644b7578b16b Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 8 Sep 2022 16:24:48 -0600 Subject: [PATCH 44/63] Fix keyshuffle --- BaseClasses.py | 2 +- resources/app/gui/lang/en.json | 2 +- resources/app/gui/randomize/dungeon/widgets.json | 2 +- source/item/FillUtil.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index a601a3c5..60d04d82 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -113,7 +113,7 @@ class World(object): 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', 'standard') + set_player_attr('keyshuffle', 'none') set_player_attr('bigkeyshuffle', False) set_player_attr('restrict_boss_items', 'none') set_player_attr('bombbag', False) diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index 9c7f9dfa..9a55893a 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -53,7 +53,7 @@ "randomizer.dungeon.mapshuffle": "Maps", "randomizer.dungeon.compassshuffle": "Compasses", "randomizer.dungeon.smallkeyshuffle": "Small Keys", - "randomizer.dungeon.smallkeyshuffle.standard": "In Dungeon", + "randomizer.dungeon.smallkeyshuffle.none": "In Dungeon", "randomizer.dungeon.smallkeyshuffle.wild": "Randomized", "randomizer.dungeon.smallkeyshuffle.universal": "Universal", "randomizer.dungeon.bigkeyshuffle": "Big Keys", diff --git a/resources/app/gui/randomize/dungeon/widgets.json b/resources/app/gui/randomize/dungeon/widgets.json index 02a7f262..19606b23 100644 --- a/resources/app/gui/randomize/dungeon/widgets.json +++ b/resources/app/gui/randomize/dungeon/widgets.json @@ -3,7 +3,7 @@ "keyshuffle": { "type": "selectbox", "options": [ - "standard", + "none", "wild", "universal" ] diff --git a/source/item/FillUtil.py b/source/item/FillUtil.py index 8cb74805..040497a2 100644 --- a/source/item/FillUtil.py +++ b/source/item/FillUtil.py @@ -251,7 +251,7 @@ def previously_reserved(location, world, player): if world.restrict_boss_items[player] == 'dungeon' and (not world.compassshuffle[player] or not world.mapshuffle[player] or not world.bigkeyshuffle[player] - or world.keyshuffle[player] == 'standard'): + or world.keyshuffle[player] == 'none'): return True return False From 30833152f63de6e25f68915352c1c2269862921a Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 13 Sep 2022 15:54:06 -0600 Subject: [PATCH 45/63] Minor decoupled adjustment --- DoorShuffle.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index 0d56882e..368aa931 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -353,9 +353,6 @@ def connect_one_way(world, entrancename, exitname, player): y = world.check_for_door(exitname, player) if x is not None: x.dest = y - if y is not None: - y.dest = x - def unmark_ugly_smalls(world, player): for d in ['Eastern Hint Tile Blocked Path SE', 'Eastern Darkness S', 'Thieves Hallway SE', 'Mire Left Bridge S', From 4f5a41d0406e8a4d7ba3e17d35485a76c71335e7 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 16 Sep 2022 10:41:18 -0600 Subject: [PATCH 46/63] Change customizer item pool to warn of missing items --- ItemList.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/ItemList.py b/ItemList.py index f2500e07..c1deb8db 100644 --- a/ItemList.py +++ b/ItemList.py @@ -1114,37 +1114,35 @@ def make_customizer_pool(world, player): place_item('Master Sword Pedestal', 'Triforce') guaranteed_items = alwaysitems + ['Magic Mirror', 'Moon Pearl'] + missing_items = [] 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) + missing_items.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') + missing_items.append('Progressive Glove') if world.bombbag[player]: if 'Bomb Upgrade (+10)' not in pool: - pool.append('Bomb Upgrade (+10)') + missing_items.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') + missing_items.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') + missing_items.append('Progressive Bow') + logging.getLogger('').warning(f'The following items are not in the custom item pool {", ".join(missing_items)}') g, t = set_default_triforce(world.goal[player], world.treasure_hunt_count[player], world.treasure_hunt_total[player]) From 513782683742db05b492f320d665f8c7ec40a839 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 16 Sep 2022 13:00:15 -0600 Subject: [PATCH 47/63] Fix github build? --- resources/ci/common/install.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/ci/common/install.py b/resources/ci/common/install.py index 6830905e..3d0b5965 100644 --- a/resources/ci/common/install.py +++ b/resources/ci/common/install.py @@ -6,7 +6,7 @@ import subprocess # do stuff at the shell level env = common.prepare_env() -pip_requirements = os.path.join("..","resources","app","meta","manifests","pip_requirements.txt") +pip_requirements = os.path.join(".","resources","app","meta","manifests","pip_requirements.txt") if not os.path.isfile(pip_requirements): pip_requirements = os.path.join("..","..","..","resources","app","meta","manifests","pip_requirements.txt") From a9806ec40f52fce69edb631b9cd82471e0311809 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 16 Sep 2022 13:31:26 -0600 Subject: [PATCH 48/63] Minor doc update --- RELEASENOTES.md | 72 +------------------------------------------------ 1 file changed, 1 insertion(+), 71 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index fab0c559..ba7ac9e0 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -179,10 +179,6 @@ Currently bugged, not recommended for use. Same as above but both small keys and bigs keys of the dungeon are not allowed on a boss. (Note: this does not affect universal keys as they are not dungeon-specific) -## Notes and Bug Fixes - -#### Unstable - # Bug Fixes and Notes * 1.1.0 @@ -244,70 +240,4 @@ Same as above but both small keys and bigs keys of the dungeon are not allowed o * Add Light Hype Fairy to bombbag mode as needing bombs * 1.0.1 * Fixed a bug with key doors not detecting one side of an interior door - * Sprite selector fix for systems with SSL issues -* 0.5.1.7 - * Baserom update - * Fix for Inverted Mode: Dark Lake Hylia shop defaults to selling a blue potion - * Fix for Ijwu's enemizer: Boss door in Thieves' Town no longer closes after the maiden hint if Blind is shuffled to Theives' Town in boss shuffle + crossed mode - * No logic now sets the AllowAccidentalMajorGlitches flag in the rom appropriately - * Houlihan room now exits wherever Link's House is shuffled to - * Rom fixes from Catobat and Codemann8. Thanks! -* 0.5.1.6 - * Rules fixes for TT (Boss and Cell) can now have TT Big Key if not otherwise required (boss shuffle + crossed dungeon) - * BUg fix for money balancing - * Add some bomb assumptions for bosses in bombbag mode -* 0.5.1.5 - * Fix for hard pool capacity upgrades missing - * Bonk Fairy (Light) is no longer in logic for ER Standard and is forbidden to be a connector, so rain state isn't exitable - * Bug fix for retro + enemizer and arrows appearing under pots - * Added bombbag and shufflelinks to settings code - * Catobat fixes: - * Fairy refills in spoiler - * Subweights support in mystery - * More defaults for mystery weights - * Less camera jank for straight stair transitions - * Bug with Straight stairs with vanilla doors where Link's walking animation stopped early is fixed -* 0.5.1.4 - * Revert quadrant glitch fix for baserom - * Fix for inverted -* 0.5.1.3 - * Certain lobbies forbidden in standard when rupee bow is enabled - * PoD EG disarmed when mirroring (except in nologic) - * Fixed issue with key logic - * Updated baserom -* 0.5.1.2 - * Allowed Blind's Cell to be shuffled anywhere if Blind is not the boss of Thieves Town - * Remove unique annotation from a FastEnum that was causing problems - * Updated prevent mixed_travel setting to prevent more mixed travel - * Prevent key door loops on the same supertile where you could have spent 2 keys on one logical door - * Promoted dynamic soft-lock prevention on "stonewalls" from experimental to be the primary prevention (Stonewalls are now never pre-opened) - * Fix to money balancing algorithm with small item_pool, thanks Catobat - * Many fixes and refinements to key logic and generation -* 0.5.1.1 - * Shop hints in ER are now more generic instead of using "near X" because they aren't near that anymore - * Added memory location for mutliworld scripts to read what item was just obtain (longer than one frame) - * Fix for bias in boss shuffle "full" - * Fix for certain lone big chests in keysanity (allowed you to get contents without big key) - * Fix for pinball checking - * Fix for multi-entrance dungeons - * 2 fixes for big key placement logic - * ensure big key is placed early if the validator assumes it) - * Open big key doors appropriately when generating rules and big key is forced somewhere - * Updated cutoff entrances for intensity 3 -* 0.5.1.0 - * Large logic refactor introducing a new method of key logic - * Some performance optimization - * Some outstanding bug fixes (boss shuffle "full" picks three unique bosses to be duplicated, e.g.) -* 0.5.0.3 - * Fixed a bug in retro+vanilla and big key placement - * Fixed a problem with shops not registering in the Multiclient until you visit one - * Fixed a bug in the Mystery code with sfx -* 0.5.0.2 - * --shuffle_sfx option added -* 0.5.0.1 - * --bombbag option added -* 0.5.0.0 - * Handles headered roms for enemizer (Thanks compiling) - * Warning added for earlier version of python (Thanks compiling) - * Minor logic issue for defeating Aga in standard (Thanks compiling) - * Fix for boss music in non-DR modes (Thanks codemann8) \ No newline at end of file + * Sprite selector fix for systems with SSL issues \ No newline at end of file From 4f2fed48d7be409b9105104ad9886d35704c677b Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 26 Oct 2022 16:46:32 -0600 Subject: [PATCH 49/63] Big key door fix Some generation fixes (need to look at pre-validate some more) Bumped up escape assist values in non-basic shuffles --- DoorShuffle.py | 20 +++++++++++++------- DungeonGenerator.py | 27 +++++++++++++++++---------- Fill.py | 2 +- Rom.py | 8 +++++--- data/base2current.bps | Bin 93438 -> 93445 bytes source/dungeon/DungeonStitcher.py | 22 ++++++++++++---------- 6 files changed, 48 insertions(+), 31 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index 368aa931..1dea2f99 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1018,7 +1018,8 @@ def main_dungeon_generation(dungeon_builders, recombinant_builders, connections_ find_standard_origins(builder, recombinant_builders, origin_list) find_enabled_origins(builder.sectors, enabled_entrances, origin_list, entrances_map, name) split_dungeon = treat_split_as_whole_dungeon(split_dungeon, name, origin_list, world, player) - if len(origin_list) <= 0 or not pre_validate(builder, origin_list, split_dungeon, world, player): + # todo: figure out pre-validate, ensure all needed origins are enabled? + if len(origin_list) <= 0: # or not pre_validate(builder, origin_list, split_dungeon, world, player): if last_key == builder.name or loops > 1000: origin_name = world.get_region(origin_list[0], player).entrances[0].parent_region.name if len(origin_list) > 0 else 'no origin' raise GenerationException(f'Infinite loop detected for "{builder.name}" located at {origin_name}') @@ -1068,14 +1069,14 @@ def determine_entrance_list(world, player): connections = {} for key, portal_list in dungeon_portals.items(): entrance_map[key] = [] - r_names = {} + r_names = [] if key in dungeon_drops.keys(): for drop in dungeon_drops[key]: - r_names[drop] = None + r_names.append((drop, None)) for portal_name in portal_list: portal = world.get_portal(portal_name, player) - r_names[portal.door.entrance.parent_region.name] = portal - for region_name, portal in r_names.items(): + r_names.append((portal.door.entrance.parent_region.name, portal)) + for region_name, portal in r_names: if portal: region = world.get_region(portal.name + ' Portal', player) else: @@ -2160,11 +2161,16 @@ def find_valid_trap_combination(builder, suggested, start_regions, paths, world, # eliminate start region if portal marked as destination def filter_start_regions(builder, start_regions, world, player): std_flag = world.mode[player] == 'standard' and builder.name == 'Hyrule Castle' - excluded = {} + excluded = {} # todo: drop lobbies, might be better to white list instead (two entrances per region) for region in start_regions: portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region == region), None) if portal and portal.destination: - excluded[region] = None + # make sure that a drop is not accessible for this "destination" + drop_region = next((x.parent_region for x in region.entrances + if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld] + or x.parent_region.name == 'Sewer Drop'), None) + if not drop_region: + excluded[region] = None if std_flag and (not portal or portal.find_portal_entrance().parent_region.name != 'Hyrule Castle Courtyard'): excluded[region] = None return [x for x in start_regions if x not in excluded.keys()] diff --git a/DungeonGenerator.py b/DungeonGenerator.py index f982cb6f..d33db874 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -1356,7 +1356,8 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, dunge sanc_builder = random.choice(lw_builders) assign_sector(sanc, sanc_builder, candidate_sectors, global_pole) - bow_sectors, retro_std_flag = {}, world.bow_mode[player].startswith('retro') and world.mode[player] == 'standard' + retro_std_flag = world.bow_mode[player].startswith('retro') and world.mode[player] == 'standard' + non_hc_sectors = {} free_location_sectors = {} crystal_switches = {} crystal_barriers = {} @@ -1364,7 +1365,9 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, dunge neutral_sectors = {} for sector in candidate_sectors: if retro_std_flag and 'Bow' in sector.item_logic: # these need to be distributed outside of HC - bow_sectors[sector] = None + non_hc_sectors[sector] = None + elif world.mode[player] == 'standard' and 'Open Floodgate' in sector.item_logic: + non_hc_sectors[sector] = None elif sector.chest_locations > 0: free_location_sectors[sector] = None elif sector.c_switch: @@ -1375,8 +1378,8 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, dunge neutral_sectors[sector] = None else: polarized_sectors[sector] = None - if bow_sectors: - assign_bow_sectors(dungeon_map, bow_sectors, global_pole) + if non_hc_sectors: + assign_non_hc_sectors(dungeon_map, non_hc_sectors, global_pole) leftover = assign_location_sectors_minimal(dungeon_map, free_location_sectors, global_pole, world, player) free_location_sectors = scatter_extra_location_sectors(dungeon_map, leftover, global_pole) for sector in free_location_sectors: @@ -1420,7 +1423,8 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, dunge def standard_stair_check(dungeon_map, dungeon, candidate_sectors, global_pole): # this is because there must be at least one non-dead stairway in hc to get out # this check may not be necessary - filtered_sectors = [x for x in candidate_sectors if any(y for y in x.outstanding_doors if not y.dead and y.type == DoorType.SpiralStairs)] + filtered_sectors = [x for x in candidate_sectors if 'Open Floodgate' not in x.item_logic and + any(y for y in x.outstanding_doors if not y.dead and y.type == DoorType.SpiralStairs)] valid = False while not valid: chosen_sector = random.choice(filtered_sectors) @@ -1550,7 +1554,7 @@ def define_sector_features(sectors): sector.bk_required = True for ext in region.exits: door = ext.door - if door is not None: + if door is not None and not door.blocked: if door.crystal == CrystalBarrier.Either: sector.c_switch = True elif door.crystal == CrystalBarrier.Orange: @@ -1562,6 +1566,8 @@ def define_sector_features(sectors): if region.name in ['PoD Mimics 2', 'PoD Bow Statue Right', 'PoD Mimics 1', 'GT Mimics 1', 'GT Mimics 2', 'Eastern Single Eyegore', 'Eastern Duo Eyegores']: sector.item_logic.add('Bow') + if region.name in ['Swamp Lobby', 'Swamp Entrance']: + sector.item_logic.add('Open Floodgate') def assign_sector(sector, dungeon, candidate_sectors, global_pole): @@ -1613,8 +1619,8 @@ def find_sector(r_name, sectors): return None -def assign_bow_sectors(dungeon_map, bow_sectors, global_pole): - sector_list = list(bow_sectors) +def assign_non_hc_sectors(dungeon_map, non_hc_sectors, global_pole): + sector_list = list(non_hc_sectors) random.shuffle(sector_list) population = [] for name in dungeon_map: @@ -1623,7 +1629,7 @@ def assign_bow_sectors(dungeon_map, bow_sectors, global_pole): choices = random.choices(population, k=len(sector_list)) for i, choice in enumerate(choices): builder = dungeon_map[choice] - assign_sector(sector_list[i], builder, bow_sectors, global_pole) + assign_sector(sector_list[i], builder, non_hc_sectors, global_pole) def scatter_extra_location_sectors(dungeon_map, free_location_sectors, global_pole): @@ -3511,7 +3517,8 @@ def check_for_valid_layout(builder, sector_list, builder_info): for portal in world.dungeon_portals[player]: if not portal.destination and portal.name in dungeon_portals[builder.name]: possible_regions.add(portal.door.entrance.parent_region.name) - if builder.name in dungeon_drops.keys(): + if builder.name in dungeon_drops.keys() and (builder.name != 'Hyrule Castle' + or world.mode[player] != 'standard'): possible_regions.update(dungeon_drops[builder.name]) independents = find_independent_entrances(possible_regions, world, player) for name, split_build in builder.split_dungeon_map.items(): diff --git a/Fill.py b/Fill.py index f695bd9a..8487c750 100644 --- a/Fill.py +++ b/Fill.py @@ -289,7 +289,7 @@ def last_ditch_placement(item_to_place, locations, world, state, base_state, ite possible_swaps = [x for x in state.locations_checked if x.item.type == 'Crystal'] else: possible_swaps = [x for x in state.locations_checked - if x.item.type not in ['Event', 'Crystal'] and not x.forced_item] + if x.item.type not in ['Event', 'Crystal'] and not x.forced_item and not x.locked] swap_locations = sorted(possible_swaps, key=location_preference) return try_possible_swaps(swap_locations, item_to_place, locations, world, base_state, itempool, key_pool, single_player_placement) diff --git a/Rom.py b/Rom.py index 18677375..fff0456a 100644 --- a/Rom.py +++ b/Rom.py @@ -37,7 +37,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '61c296effe6180274721d570d2471e1c' +RANDOMIZERBASEHASH = '5639de3bfd500ba238d3f27ea78c19e1' class JsonRom(object): @@ -1492,8 +1492,10 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): if world.doorShuffle[player] not in ['vanilla', 'basic']: # Uncle respawn refills (magic, bombs, arrows) rom.write_bytes(0x180185, [max(0x20, magic_max), max(3, bomb_max), max(10, bow_max)]) - rom.write_bytes(0x180188, [0x20, 3, 10]) # Zelda respawn refills (magic, bombs, arrows) - rom.write_bytes(0x18018B, [0x20, 3, 10]) # Mantle respawn refills (magic, bombs, arrows) + # Zelda respawn refills (magic, bombs, arrows) + rom.write_bytes(0x180188, [max(0x20, magic_max), max(3, bomb_max), max(10, bow_max)]) + # Mantle respawn refills (magic, bombs, arrows) + rom.write_bytes(0x18018B, [max(0x20, magic_max), max(3, bomb_max), max(10, bow_max)]) elif world.doorShuffle[player] == 'basic': # just in case a bomb is needed to get to a chest rom.write_bytes(0x180185, [max(0x00, magic_max), max(3, bomb_max), max(0, bow_max)]) rom.write_bytes(0x180188, [magic_small, 3, bow_small]) # Zelda respawn refills (magic, bombs, arrows) diff --git a/data/base2current.bps b/data/base2current.bps index 03d6471ed59fbc3a0aa2fcb13383c31d53c06f20..3fd78189a58e8bcb70b83f770932c6ebbe1745dc 100644 GIT binary patch delta 867 zcmW-cUu;u#6vyxBy|?YQ(tB_JtwpTAwQg<2kZn$!pk!z@q?9a!MB?8xBsyit!_*gI z#;?WOP{`8WUdq{aL#S*wp(V=}`cOccb(%SdFAK>MTwIhHqYs4iVFWaUdiA`V@A>4L z?>Fb^&%C_G&wm7*^Mm`-l_L;ip{Fv--C<#|Lap^SXjb;Rmfi+`#q$1F+k=x}a|+BB z!2MB%*1*F%UEtywyU@;nozspUU}!~4+xJ3OrM%p`8|t`jf{^v5LU|p?Beb#Lhx@pl zg$_Q7sU)oqu@JY|qKX_SZPGTwM60D%*ujAh@7kgFWQk!$>dl6QJjfSKlqI{=+s%e| zYDmccZD5{c6$AdH5Kpc@;Z6LH%y|Qy$S-{2_t0c}HZ^`oA#wF|L1*RSH|8HYZ`8O5 zU1`xd8OSTA=oX#3_;tEK3OTY>zPRsd<@M_q{xf(DgNkVZ9NyUV)tQ{mk}uK;QpxFr z)N+vlhFEZ30RPV5jU=xf{y6_fxz<>UG?~jfL(-~j(k;eesIt!IIiT8W9qI)WDXTK0 z+weK$MCjc1-gilBHZ{xZ9EmD_)h3tZHuciDEfEp4-T0OWL9-*+PGyS6MF_x6R7B{4 zN7ZX02+(s^rLH=k?V_1qRhqZnn~fRObUjgLm_|5@$snf2pQzM_rzfRzR3LxiV+r<) zJI;-4i0a^ZRoLUEJ=Bhe9B|kiP?CN449+{i*Ah=;H0QC5HimZ`Mr{v1bilBL6WL1` zK28^Bmd9${4TAW&6Y`EhRK=8TVOH4qAeN-?+%c3~K*(|-391Z-&)Bi4!NMyZH>67cHmS}&|Vg%l!lp+3x?zv>&a+b)PdJgitpw6P&a4?vFS=nM@PeMP&TVGXr4?zTyVNd!_~17a zN)}E2zWYtJ?`RT-t3eN>Tj3y<3A`b8Fd603T_eTfobRQa#`RU75HM!A`m_Tc$E@q4 LJ5ygCZ29G1-!Dm} delta 854 zcmW-bZ%i9?7{~AF^=Ly2cm3b)5Bt$>v=uYhUvQd9mxTz{45Ma7GrgHOjkx(jm#Go_ zUD-A5B5Cgq_7pA)h3(wRf=v6OVFtlJ2uplHO!i^~lWoz&#l-YQO+=@7@p@Lq(e`MjSGPN%{;9)u8UFwFdW!uYVTX!sh)59|j z;NTcwJmBX%-rV2WMn0gOIghcS@`&%X=S(dAw(VsC`bMoB^bU0t-#>0NYq- z<`S5a1}ZPGkhD1yiX1N0sKYc3RA%a7w-p+2y%F~I^)GH#ZR9t#XrV0W&~9y3gX7Qg z>V}Cq@`+-?6@_^6onlb@AF-;5&Z!Gxlb=f7Txz&aAw61V)L`Y}`MPg+;7J|=PpWyc zMV>hFMY&_{k~L0OgN9YsoRf03e&RrJlm^KYDN%cH{pj57$-Kdmche%dkv9mrnU8ld z#EKOjnhtKiDRKJo*VSL9=c`NcP3D@xkReUB7&h}T)wIE>R-pR9X6=%NT-9X8aAF7@ z0_@uM{FjnFmzv=WD@iE7&W|n04sB}KDaLvI044>9)Hx#oDi7dU0m3kc1p!*1R#^~$ zhdsA7>Rse=Ei}8XQPqBLrq`^dKZ%PBQ;p?NiePGZL!%+gj5Ur>p8SlPE_hmK9qGR> zXgwD-{;-ekp$>e>4aZ~K6lp*DPG_Y&o|5pRtkiiqHF;=xkamPanJK+n%<2tqXZ82- zj@$gC6(73c4cAB^H-)h?^y@3jgY)fGGJ@}UKy@ck6H Date: Thu, 27 Oct 2022 13:56:37 -0600 Subject: [PATCH 50/63] Bumper cave fix Pottery coloring updated --- EntranceShuffle.py | 12 ++++++++---- InvertedRegions.py | 3 ++- Main.py | 2 +- PotShuffle.py | 18 +++++++++--------- RELEASENOTES.md | 3 +++ Regions.py | 3 ++- Rules.py | 8 ++++---- 7 files changed, 29 insertions(+), 20 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index f67c089b..8ff4fcc8 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -3141,6 +3141,8 @@ mandatory_connections = [('Links House S&Q', 'Links House'), ('Bumper Cave Entrance Mirror Spot', 'Death Mountain Entrance'), ('Bumper Cave Ledge Drop', 'West Dark World'), ('Bumper Cave Ledge Mirror Spot', 'Death Mountain Return Ledge'), + ('Bumper Cave Bottom to Top', 'Bumper Cave (top)'), + ('Bumper Cave Top To Bottom', 'Bumper Cave (bottom)'), ('Skull Woods Forest', 'Skull Woods Forest'), ('Desert Ledge Mirror Spot', 'Desert Ledge'), ('Desert Ledge (Northeast) Mirror Spot', 'Desert Ledge (Northeast)'), @@ -3269,6 +3271,8 @@ inverted_mandatory_connections = [('Links House S&Q', 'Inverted Links House'), ('Bumper Cave Entrance Rock', 'Bumper Cave Entrance'), ('Bumper Cave Entrance Drop', 'West Dark World'), ('Bumper Cave Ledge Drop', 'West Dark World'), + ('Bumper Cave Bottom to Top', 'Bumper Cave (top)'), + ('Bumper Cave Top To Bottom', 'Bumper Cave (bottom)'), ('Skull Woods Forest', 'Skull Woods Forest'), ('Paradox Cave Push Block Reverse', 'Paradox Cave Chest Area'), ('Paradox Cave Push Block', 'Paradox Cave Front'), @@ -3492,8 +3496,8 @@ default_connections = [('Links House', 'Links House'), ('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'), + ('Bumper Cave (Bottom)', 'Bumper Cave (bottom)'), + ('Bumper Cave (Top)', 'Bumper Cave (top)'), ('Red Shield Shop', 'Red Shield Shop'), ('Dark Sanctuary Hint', 'Dark Sanctuary Hint'), ('Fortune Teller (Dark)', 'Fortune Teller (Dark)'), @@ -3655,7 +3659,7 @@ inverted_default_connections = [('Waterfall of Wishing', 'Waterfall of Wishing' ('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 (West)', 'Bumper Cave (bottom)'), ('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'), @@ -3664,7 +3668,7 @@ inverted_default_connections = [('Waterfall of Wishing', 'Waterfall of Wishing' ('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 (West)', 'Bumper Cave (top)'), ('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'), diff --git a/InvertedRegions.py b/InvertedRegions.py index 175b2b76..a02bfbea 100644 --- a/InvertedRegions.py +++ b/InvertedRegions.py @@ -184,7 +184,8 @@ def create_inverted_regions(world, player): create_cave_region(player, 'Chest Game', 'a game of 16 chests', ['Chest Game']), create_cave_region(player, 'Red Shield Shop', 'the rare shop', ['Red Shield Shop - Left', 'Red Shield Shop - Middle', 'Red Shield Shop - Right']), create_cave_region(player, 'Inverted Dark Sanctuary', 'a storyteller', None, ['Inverted Dark Sanctuary Exit']), - create_cave_region(player, 'Bumper Cave', 'a connector', None, ['Bumper Cave Exit (Bottom)', 'Bumper Cave Exit (Top)']), + create_cave_region(player, 'Bumper Cave (bottom)', 'a connector', None, ['Bumper Cave Exit (Bottom)', 'Bumper Cave Bottom to Top']), + create_cave_region(player, 'Bumper Cave (top)', 'a connector', None, ['Bumper Cave Exit (Top)', 'Bumper Cave Top To Bottom']), create_dw_region(player, 'Skull Woods Forest', None, ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', 'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)']), create_dw_region(player, 'Skull Woods Forest (West)', None, ['Skull Woods Second Section Hole', 'Skull Woods Second Section Door (West)', 'Skull Woods Final Section']), diff --git a/Main.py b/Main.py index d1f4542d..d0174f27 100644 --- a/Main.py +++ b/Main.py @@ -31,7 +31,7 @@ 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.tools.BPS import create_bps_from_data -__version__ = '1.1.0-dev' +__version__ = '1.1.1-dev' from source.classes.BabelFish import BabelFish diff --git a/PotShuffle.py b/PotShuffle.py index a2a10c0f..88963ffe 100644 --- a/PotShuffle.py +++ b/PotShuffle.py @@ -745,11 +745,11 @@ vanilla_pots = { 0xE7: [Pot(68, 5, PotItem.OneRupee, 'Death Mountain Return Cave (right)', obj=RoomObject(0x0AB389, [0x8B, 0x2B, 0xFA])), Pot(72, 5, PotItem.OneRupee, 'Death Mountain Return Cave (right)', obj=RoomObject(0x0AB38C, [0x93, 0x2B, 0xFA]))], 0xE8: [Pot(96, 4, PotItem.Heart, 'Superbunny Cave (Bottom)', obj=RoomObject(0x0AA98E, [0xC3, 0x23, 0xFA]))], - 0xEB: [Pot(206, 8, PotItem.FiveRupees, 'Bumper Cave', obj=RoomObject(0x0AADE7, [0x9F, 0x47, 0xFA])), - Pot(210, 8, PotItem.FiveRupees, 'Bumper Cave', obj=RoomObject(0x0AADEA, [0xA7, 0x47, 0xFA])), - Pot(88, 14, PotItem.SmallMagic, 'Bumper Cave', obj=RoomObject(0x0AADED, [0xB3, 0x73, 0xFA])), - Pot(92, 14, PotItem.Heart, 'Bumper Cave', obj=RoomObject(0x0AADF0, [0xBB, 0x73, 0xFA])), - Pot(96, 14, PotItem.SmallMagic, 'Bumper Cave', obj=RoomObject(0x0AADF3, [0xC3, 0x73, 0xFA]))], + 0xEB: [Pot(206, 8, PotItem.FiveRupees, 'Bumper Cave (bottom)', obj=RoomObject(0x0AADE7, [0x9F, 0x47, 0xFA])), + Pot(210, 8, PotItem.FiveRupees, 'Bumper Cave (bottom)', obj=RoomObject(0x0AADEA, [0xA7, 0x47, 0xFA])), + Pot(88, 14, PotItem.SmallMagic, 'Bumper Cave (bottom)', obj=RoomObject(0x0AADED, [0xB3, 0x73, 0xFA])), + Pot(92, 14, PotItem.Heart, 'Bumper Cave (bottom)', obj=RoomObject(0x0AADF0, [0xBB, 0x73, 0xFA])), + Pot(96, 14, PotItem.SmallMagic, 'Bumper Cave (bottom)', obj=RoomObject(0x0AADF3, [0xC3, 0x73, 0xFA]))], 0xF1: [Pot(64, 5, PotItem.Heart, 'Old Man Cave', obj=RoomObject(0x0AA6B2, [0x83, 0x2B, 0xFA]))], 0xF3: [Pot(0x28, 0x14, PotItem.Nothing, 'Elder House', obj=RoomObject(0x0AA76F, [0x53, 0xA3, 0xFA])), Pot(0x2C, 0x14, PotItem.Nothing, 'Elder House', obj=RoomObject(0x0AA772, [0x5B, 0xA3, 0xFA])), @@ -851,10 +851,10 @@ vanilla_pots = { Pot(100, 22, PotItem.Heart, 'Dark Lake Hylia Ledge Spike Cave', obj=RoomObject(0x0AB62A, [0xCB, 0xB3, 0xFA])), Pot(88, 28, PotItem.Heart, 'Dark Lake Hylia Ledge Spike Cave', obj=RoomObject(0x0AB633, [0xB3, 0xE3, 0xFA])), Pot(100, 28, PotItem.Heart, 'Dark Lake Hylia Ledge Spike Cave', obj=RoomObject(0x0AB636, [0xCB, 0xE3, 0xFA]))], - 0x127: [Pot(24, 25, PotItem.Nothing, 'Dark World Hammer Peg Cave', obj=RoomObject(0x2A801A, [0x33, 0xCB, 0xFA])), - Pot(28, 25, PotItem.Nothing, 'Dark World Hammer Peg Cave', obj=RoomObject(0x2A801D, [0x3B, 0xCB, 0xFA])), - Pot(32, 25, PotItem.Nothing, 'Dark World Hammer Peg Cave', obj=RoomObject(0x2A8020, [0x43, 0xCB, 0xFA])), - Pot(36, 25, PotItem.Nothing, 'Dark World Hammer Peg Cave', obj=RoomObject(0x2A8023, [0x4B, 0xCB, 0xFA]))], + 0x127: [Pot(24, 25, PotItem.Nothing, 'Dark World Hammer Peg Cave', obj=RoomObject(0x2B801A, [0x33, 0xCB, 0xFA])), + Pot(28, 25, PotItem.Nothing, 'Dark World Hammer Peg Cave', obj=RoomObject(0x2B801D, [0x3B, 0xCB, 0xFA])), + Pot(32, 25, PotItem.Nothing, 'Dark World Hammer Peg Cave', obj=RoomObject(0x2B8020, [0x43, 0xCB, 0xFA])), + Pot(36, 25, PotItem.Nothing, 'Dark World Hammer Peg Cave', obj=RoomObject(0x2B8023, [0x4B, 0xCB, 0xFA]))], } diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ba7ac9e0..eff11535 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -181,6 +181,9 @@ Same as above but both small keys and bigs keys of the dungeon are not allowed o # Bug Fixes and Notes +* 1.1.1 + * Fixed a logic bug with Bumper Cave where the pots were accessible without Cape or Hookshot from the top entrance + * Fixed a pot coloring issue with hammer peg cave * 1.1.0 * Large features * New pottery modes - see notes above diff --git a/Regions.py b/Regions.py index 31a5ab92..506a8116 100644 --- a/Regions.py +++ b/Regions.py @@ -181,7 +181,8 @@ def create_regions(world, player): create_cave_region(player, 'Chest Game', 'a game of 16 chests', ['Chest Game']), create_cave_region(player, 'Red Shield Shop', 'the rare shop', ['Red Shield Shop - Left', 'Red Shield Shop - Middle', 'Red Shield Shop - Right']), create_cave_region(player, 'Dark Sanctuary Hint', 'a storyteller'), - create_cave_region(player, 'Bumper Cave', 'a connector', None, ['Bumper Cave Exit (Bottom)', 'Bumper Cave Exit (Top)']), + create_cave_region(player, 'Bumper Cave (bottom)', 'a connector', None, ['Bumper Cave Exit (Bottom)', 'Bumper Cave Bottom to Top']), + create_cave_region(player, 'Bumper Cave (top)', 'a connector', None, ['Bumper Cave Exit (Top)', 'Bumper Cave Top To Bottom']), create_dw_region(player, 'Bumper Cave Ledge', ['Bumper Cave Ledge'], ['Bumper Cave Ledge Drop', 'Bumper Cave (Top)', 'Bumper Cave Ledge Mirror Spot'], 'a ledge with an item'), create_dw_region(player, 'Skull Woods Forest', None, ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', 'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)']), diff --git a/Rules.py b/Rules.py index a553fbfe..68f9e616 100644 --- a/Rules.py +++ b/Rules.py @@ -855,8 +855,8 @@ def default_rules(world, player): set_rule(world.get_entrance('Peg Area Rocks', player), lambda state: state.has_Pearl(player) and state.can_lift_heavy_rocks(player)) set_rule(world.get_entrance('Village of Outcasts Pegs', player), lambda state: state.has_Pearl(player) and state.has('Hammer', player)) set_rule(world.get_entrance('Grassy Lawn Pegs', player), lambda state: state.has_Pearl(player) and state.has('Hammer', player)) - set_rule(world.get_entrance('Bumper Cave Exit (Top)', player), lambda state: state.has('Cape', player)) - set_rule(world.get_entrance('Bumper Cave Exit (Bottom)', player), lambda state: state.has('Cape', player) or state.has('Hookshot', player)) + set_rule(world.get_entrance('Bumper Cave Bottom to Top', player), lambda state: state.has('Cape', player)) + set_rule(world.get_entrance('Bumper Cave Top To Bottom', player), lambda state: state.has('Cape', player) or state.has('Hookshot', player)) set_rule(world.get_entrance('Skull Woods Final Section', player), lambda state: state.has('Fire Rod', player) and state.has_Pearl(player)) # bunny cannot use fire rod set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has_Pearl(player) and state.has_sword(player) and state.has_misery_mire_medallion(player)) # sword required to cast magic (!) @@ -1782,8 +1782,8 @@ def set_bunny_rules(world, player, inverted): # regions for the exits of multi-entrace caves/drops that bunny cannot pass # Note spiral cave may be technically passible, but it would be too absurd to require since OHKO mode is a thing. - bunny_impassable_caves = ['Bumper Cave', 'Two Brothers House', 'Hookshot Cave (Middle)', - 'Pyramid', 'Spiral Cave (Top)', 'Fairy Ascension Cave (Drop)'] + bunny_impassable_caves = ['Bumper Cave (top)', 'Bumper Cave (bottom)', 'Two Brothers House', + 'Hookshot Cave (Middle)', 'Pyramid', 'Spiral Cave (Top)', 'Fairy Ascension Cave (Drop)'] bunny_accessible_locations = ['Link\'s Uncle', 'Sahasrahla', 'Sick Kid', 'Lost Woods Hideout', 'Lumberjack Tree', 'Checkerboard Cave', 'Potion Shop', 'Spectacle Rock Cave', 'Pyramid', 'Hype Cave - Generous Guy', 'Peg Cave', 'Bumper Cave Ledge', 'Dark Blacksmith Ruins', From 1167f2b1ba3feabf2c0c99fac617c00b6ac30dbe Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 27 Oct 2022 15:23:58 -0600 Subject: [PATCH 51/63] Bumper cave fix for new entrance shuffle --- source/overworld/EntranceShuffle2.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/source/overworld/EntranceShuffle2.py b/source/overworld/EntranceShuffle2.py index da454795..708e26ac 100644 --- a/source/overworld/EntranceShuffle2.py +++ b/source/overworld/EntranceShuffle2.py @@ -2290,8 +2290,8 @@ default_connections = {'Links House': 'Links House', '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', + 'Bumper Cave (Bottom)': 'Bumper Cave (bottom)', + 'Bumper Cave (Top)': 'Bumper Cave (top)', 'Red Shield Shop': 'Red Shield Shop', 'Dark Sanctuary Hint': 'Dark Sanctuary Hint', 'Fortune Teller (Dark)': 'Fortune Teller (Dark)', @@ -2452,7 +2452,7 @@ inverted_default_connections = {'Waterfall of Wishing': 'Waterfall of Wishing', '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 (West)': 'Bumper Cave (bottom)', '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', @@ -2461,7 +2461,7 @@ inverted_default_connections = {'Waterfall of Wishing': 'Waterfall of Wishing', '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 (West)': 'Bumper Cave (top)', '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', From bd619ff2ef6847956be40e302cca2682614954a2 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 27 Oct 2022 15:48:04 -0600 Subject: [PATCH 52/63] Bumper cave fixes for ES2 --- source/overworld/EntranceShuffle2.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/source/overworld/EntranceShuffle2.py b/source/overworld/EntranceShuffle2.py index 708e26ac..ad073162 100644 --- a/source/overworld/EntranceShuffle2.py +++ b/source/overworld/EntranceShuffle2.py @@ -2292,6 +2292,8 @@ default_connections = {'Links House': 'Links House', 'Dark World Hammer Peg Cave': 'Dark World Hammer Peg Cave', 'Bumper Cave (Bottom)': 'Bumper Cave (bottom)', 'Bumper Cave (Top)': 'Bumper Cave (top)', + 'Bumper Cave Top To Bottom': 'Bumper Cave (bottom)', + 'Bumper Cave Bottom to Top': 'Bumper Cave (top)', 'Red Shield Shop': 'Red Shield Shop', 'Dark Sanctuary Hint': 'Dark Sanctuary Hint', 'Fortune Teller (Dark)': 'Fortune Teller (Dark)', @@ -2453,6 +2455,8 @@ inverted_default_connections = {'Waterfall of Wishing': 'Waterfall of Wishing', 'Inverted Dark Sanctuary': 'Inverted Dark Sanctuary', 'Inverted Dark Sanctuary Exit': 'West Dark World', 'Old Man Cave (West)': 'Bumper Cave (bottom)', + 'Bumper Cave Top To Bottom': 'Bumper Cave (bottom)', + 'Bumper Cave Bottom to Top': 'Bumper Cave (top)', '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', From 82965ac734a7a5f7cebbe442eef60f99f1573043 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 28 Oct 2022 08:56:06 -0600 Subject: [PATCH 53/63] Big Key fix for dungeons with only big chests Door pairing issues Minor gui error --- DoorShuffle.py | 40 ++++++------------- .../app/gui/randomize/dungeon/widgets.json | 2 +- 2 files changed, 14 insertions(+), 28 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index 1dea2f99..5737a5b8 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -831,7 +831,8 @@ def main_dungeon_pool(dungeon_pool, world, player): for name in pool: builder = world.dungeon_layouts[player][name] region_set = builder.master_sector.region_set() - builder.bk_required = builder.bk_door_proposal or any(x in region_set for x in special_bk_regions) + builder.bk_required = (builder.bk_door_proposal or any(x in region_set for x in special_bk_regions) + or len(world.key_logic[player][name].bk_chests) > 0) dungeon = world.get_dungeon(name, player) if not builder.bk_required or builder.bk_provided: dungeon.big_key = None @@ -2362,21 +2363,16 @@ def find_big_key_door_candidates(region, checked, used, world, player): if valid and d.dest not in candidates: # interior doors are not separable yet candidates.append(d.dest) elif d.type == DoorType.Normal: - if decoupled: - valid = kind in okay_normals - else: + valid = kind in okay_normals + if valid and not decoupled: d2 = d.dest if d2 not in candidates and d2 not in used: if d2.type == DoorType.Normal: room_b = world.get_room(d2.roomIndex, player) pos_b, kind_b = room_b.doorList[d2.doorListPos] - valid = kind in okay_normals and kind_b in okay_normals and valid_key_door_pair(d, d2) - else: - valid = kind in okay_normals + valid &= kind_b in okay_normals and valid_key_door_pair(d, d2) if valid and 0 <= d2.doorListPos < 4: candidates.append(d2) - else: - valid = True if valid and d not in candidates: candidates.append(d) connected = ext.connected_region @@ -2628,21 +2624,16 @@ def find_bd_door_candidates(region, checked, used, world, player): if valid and d.dest not in candidates: # interior doors are not separable yet candidates.append(d.dest) elif d.type == DoorType.Normal: - if decoupled: - valid = kind in okay_normals - else: + valid = kind in okay_normals + if valid and not decoupled: d2 = d.dest if d2 not in candidates and d2 not in used: if d2.type == DoorType.Normal: room_b = world.get_room(d2.roomIndex, player) pos_b, kind_b = room_b.doorList[d2.doorListPos] - valid = kind in okay_normals and kind_b in okay_normals and valid_key_door_pair(d, d2) - else: - valid = kind in okay_normals + valid &= kind_b in okay_normals and valid_key_door_pair(d, d2) if valid and 0 <= d2.doorListPos < 4: candidates.append(d2) - else: - valid = True if valid and d not in candidates: candidates.append(d) connected = ext.connected_region @@ -2702,8 +2693,8 @@ def reassign_bd_doors(bd_map, world, player): elif d.type is DoorType.Normal and not_in_proposal(d): if not d.entranceFlag: world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) - do_bombable_dashable(flat_bomb_proposal, DoorKind.Bombable, world, player) - do_bombable_dashable(flat_dash_proposal, DoorKind.Dashable, world, player) + do_bombable_dashable(pair[0], DoorKind.Bombable, world, player) + do_bombable_dashable(pair[1], DoorKind.Dashable, world, player) def do_bombable_dashable(proposal, kind, world, player): @@ -2864,21 +2855,16 @@ def find_key_door_candidates(region, checked, used, world, player): elif d.type == DoorType.SpiralStairs: valid = kind in [DoorKind.StairKey, DoorKind.StairKey2, DoorKind.StairKeyLow] elif d.type == DoorType.Normal: - if decoupled: - valid = kind in okay_normals - else: + valid = kind in okay_normals + if valid and not decoupled: d2 = d.dest if d2 not in candidates and d2 not in used: if d2.type == DoorType.Normal: room_b = world.get_room(d2.roomIndex, player) pos_b, kind_b = room_b.doorList[d2.doorListPos] - valid = kind in okay_normals and kind_b in okay_normals and valid_key_door_pair(d, d2) - else: - valid = kind in okay_normals + valid &= kind_b in okay_normals and valid_key_door_pair(d, d2) if valid and 0 <= d2.doorListPos < 4: candidates.append(d2) - else: - valid = True if valid and d not in candidates: candidates.append(d) connected = ext.connected_region diff --git a/resources/app/gui/randomize/dungeon/widgets.json b/resources/app/gui/randomize/dungeon/widgets.json index 19606b23..bdedbfba 100644 --- a/resources/app/gui/randomize/dungeon/widgets.json +++ b/resources/app/gui/randomize/dungeon/widgets.json @@ -1,6 +1,6 @@ { "widgets": { - "keyshuffle": { + "smallkeyshuffle": { "type": "selectbox", "options": [ "none", From 9fafa8981e5aadf6ed474ddff9940dba327f3b62 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 28 Oct 2022 12:47:43 -0600 Subject: [PATCH 54/63] Big Key Door fix for when big key double sided is off Fix for basic issues - key total doesn't change Potential key lock logic fix --- DoorShuffle.py | 24 +++++++++++++++++------- Rules.py | 2 +- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index 5737a5b8..5458c020 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1903,9 +1903,11 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, worl custom_key_doors = world.custom_door_types[player]['Key Door'] else: custom_key_doors = defaultdict(list) - + total_adjustable = len(pool) > 1 for dungeon in pool: builder = world.dungeon_layouts[player][dungeon] + if not total_adjustable: + builder.total_keys = total_keys find_small_key_door_candidates(builder, start_regions_map[dungeon], used_doors, world, player) if custom_key_doors[dungeon]: builder.candidates.small = filter_key_door_pool(builder.candidates.small, custom_key_doors[dungeon]) @@ -1930,11 +1932,15 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, worl flex_map[dungeon] = (limit - suggested) if suggested < limit else 0 for dungeon in pool: builder = world.dungeon_layouts[player][dungeon] + if total_adjustable: + builder.total_keys = suggestion_map[dungeon] valid_doors, small_number = find_valid_combination(builder, suggestion_map[dungeon], start_regions_map[dungeon], world, player) small_map[dungeon] = valid_doors actual_chest_keys = small_number - builder.key_drop_cnt if actual_chest_keys < suggestion_map[dungeon]: + if total_adjustable: + builder.total_keys = actual_chest_keys flex_map[dungeon] = 0 remaining += suggestion_map[dungeon] - actual_chest_keys suggestion_map[dungeon] = small_number @@ -1953,6 +1959,8 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, worl remaining -= 1 suggestion_map[dungeon] = increased flex_map[dungeon] -= 1 + if total_adjustable: + builder.total_keys = actual_chest_keys if flex_map[dungeon] > 0: builder.combo_size = ncr(len(builder.candidates.small), builder.key_doors_num) queue.append(builder) @@ -2464,7 +2472,8 @@ def reassign_big_key_doors(bk_map, world, player): d = obj if d.type is DoorType.Interior: change_door_to_big_key(d, world, player) - d.dest.bigKey = True # ensure flag is set + if world.door_type_mode[player] != 'original': + d.dest.bigKey = True # ensure flag is set when bk doors are double sided elif d.type is DoorType.SpiralStairs: pass # we don't have spiral stairs candidates yet that aren't already key doors elif d.type is DoorType.Normal: @@ -2574,7 +2583,6 @@ def find_valid_combination(builder, target, start_regions, world, player, drop_k # make changes if player not in world.key_logic.keys(): world.key_logic[player] = {} - builder.total_keys = builder.key_doors_num analyze_dungeon(key_layout, world, player) builder.key_door_proposal = proposal world.key_logic[player][builder.name] = key_layout.key_logic @@ -2620,8 +2628,9 @@ def find_bd_door_candidates(region, checked, used, world, player): room = world.get_room(d.roomIndex, player) position, kind = room.doorList[d.doorListPos] if d.type == DoorType.Interior: - valid = kind in okay_interiors - if valid and d.dest not in candidates: # interior doors are not separable yet + # interior doors are not separable yet + valid = kind in okay_interiors and d.dest not in used + if valid and d.dest not in candidates: candidates.append(d.dest) elif d.type == DoorType.Normal: valid = kind in okay_normals @@ -2849,8 +2858,9 @@ def find_key_door_candidates(region, checked, used, world, player): room = world.get_room(d.roomIndex, player) position, kind = room.doorList[d.doorListPos] if d.type == DoorType.Interior: - valid = kind in okay_interiors - if valid and d.dest not in candidates: # interior doors are not separable yet + valid = kind in okay_interiors and d.dest not in used + # interior doors are not separable yet + if valid and d.dest not in candidates: candidates.append(d.dest) elif d.type == DoorType.SpiralStairs: valid = kind in [DoorKind.StairKey, DoorKind.StairKey2, DoorKind.StairKeyLow] diff --git a/Rules.py b/Rules.py index 3867d681..fa07bd5a 100644 --- a/Rules.py +++ b/Rules.py @@ -2093,7 +2093,7 @@ def eval_small_key_door_main(state, door_name, dungeon, player): elif ruleType == KeyRuleType.AllowSmall: if (door_rule.small_location.item and door_rule.small_location.item.name == key_logic.small_key_name and door_rule.small_location.item.player == player): - return True # always okay if allow small is on + door_openable |= state.has_sm_key(key_logic.small_key_name, player, number) elif isinstance(ruleType, tuple): lock, lock_item = ruleType # this doesn't track logical locks yet, i.e. hammer locks the item and hammer is there, but the item isn't From cb4cf65c9c9a0be03a1a754a3b3e585d89470fd0 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 1 Nov 2022 15:05:28 -0600 Subject: [PATCH 55/63] More robust sweep_for_events (supports events that later become unreachable) Minor fix for AllowSmall key logic --- BaseClasses.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 60d04d82..06c1855d 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -786,7 +786,21 @@ class CollectionState(object): else: door_candidates.append(door.name) return door_candidates - return None + door_candidates, skip = [], set() + if state.world.accessibility[player] != 'locations' and remaining_keys == 0: + key_logic = state.world.key_logic[player][dungeon_name] + for door, paired in key_logic.sm_doors.items(): + if door.name in key_logic.door_rules: + rule = key_logic.door_rules[door.name] + key = KeyRuleType.AllowSmall + if (key in rule.new_rules and key_total >= rule.new_rules[key] and door.name not in skip + and door.name in state.reached_doors[player] and door.name not in state.opened_doors[player]): + if paired: + door_candidates.append((door.name, paired.name)) + skip.add(paired.name) + else: + door_candidates.append(door.name) + return door_candidates if door_candidates else None @staticmethod def print_rrp(rrp): @@ -925,29 +939,30 @@ class CollectionState(object): checked_locations = set([l for l in locations if l in self.locations_checked]) reachable_events = [location for location in locations if location.event and location.can_reach(self)] reachable_events = self._do_not_flood_the_keys(reachable_events) + found_new = False for event in reachable_events: if event not in checked_locations: self.events.append((event.name, event.player)) self.collect(event.item, True, event) - return len(reachable_events) > len(checked_locations) + found_new = True + return found_new def sweep_for_events(self, key_only=False, locations=None): # this may need improvement if locations is None: locations = self.world.get_filled_locations() new_locations = True - checked_locations = 0 while new_locations: reachable_events = [location for location in locations if location.event and (not key_only or (self.world.keyshuffle[location.item.player] == 'none' and location.item.smallkey) or (not self.world.bigkeyshuffle[location.item.player] and location.item.bigkey)) and location.can_reach(self)] reachable_events = self._do_not_flood_the_keys(reachable_events) + new_locations = False for event in reachable_events: if (event.name, event.player) not in self.events: self.events.append((event.name, event.player)) self.collect(event.item, True, event) - new_locations = len(reachable_events) > checked_locations - checked_locations = len(reachable_events) + new_locations = True def can_reach_blue(self, region, player): From fa75d2b4e978c6653262de5dd81129d9cd07907d Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 10 Nov 2022 14:14:54 -0700 Subject: [PATCH 56/63] Possible fix for standing items weirdness Generation fix for Bumper Cave Chaos door type fix Minor fix for universal keys Relaase notes update --- BaseClasses.py | 2 +- DoorShuffle.py | 10 +- RELEASENOTES.md | 239 ++++++++------------------- Rom.py | 2 +- data/base2current.bps | Bin 93445 -> 93456 bytes source/overworld/EntranceShuffle2.py | 8 +- 6 files changed, 79 insertions(+), 182 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 06c1855d..d89147c9 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -787,7 +787,7 @@ class CollectionState(object): door_candidates.append(door.name) return door_candidates door_candidates, skip = [], set() - if state.world.accessibility[player] != 'locations' and remaining_keys == 0: + if state.world.accessibility[player] != 'locations' and remaining_keys == 0 and dungeon_name != 'Universal': key_logic = state.world.key_logic[player][dungeon_name] for door, paired in key_logic.sm_doors.items(): if door.name in key_logic.door_rules: diff --git a/DoorShuffle.py b/DoorShuffle.py index 5458c020..ad22b1c0 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1744,7 +1744,7 @@ class DoorTypePool: @staticmethod def get_choices(number): - return [max(number-i, 0) for i in range(-1, 5)] + return [max(number+i, 0) for i in range(-1, 5)] class BuilderDoorCandidates: @@ -2208,6 +2208,7 @@ def find_bk_special_location(builder, world, player): return loc return None + def check_required_paths_with_traps(paths, proposal, dungeon_name, start_regions, world, player): cached_initial_state = None if len(paths[dungeon_name]) > 0: @@ -2673,8 +2674,11 @@ def find_valid_bd_combination(builder, suggested, world, player): bd_door_pool = filter_key_door_pool(bd_door_pool, custom_dash_doors) dash_doors_needed -= len(custom_dash_doors) while len(bd_door_pool) < bomb_doors_needed + dash_doors_needed: - bomb_doors_needed = round(len(bd_door_pool) * bomb_doors_needed/ttl_needed) - dash_doors_needed = round(len(bd_door_pool) * dash_doors_needed/ttl_needed) + test = random.choice([True, False]) + if test: + bomb_doors_needed -= 1 + else: + dash_doors_needed -= 1 bomb_proposal = random.sample(bd_door_pool, k=bomb_doors_needed) bomb_proposal.extend(custom_bomb_doors) dash_pool = [x for x in bd_door_pool if x not in bomb_proposal] diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ce89548a..8fc802a6 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,215 +1,108 @@ -## New Features +# New Features -## Pottery Lottery and Key Drop Shuffle Changes +One major change with this update is that big key doors and certain trap doors are no longer guaranteed to be vanilla in Dungeon Door Shuffle modes even if you choose not to shuffle those types. A newer algorithm for putting dungeons together has been written and it will remove big key doors and trap doors when necessary to ensure progress can be made. -### Pottery +Please note that retro features are now independently customizable as referenced below. Selecting Retro mode or World State: Retro will change Bow Mode to Retro (Progressive). Take Anys to Random, and Small Keys to Universal. -New pottery option that control which pots (and large blocks) are in the locations pool: +## Flute Mode -* None: No pots are in the pool, like normal randomizer -* Key Pots: The pots that have keys are in the pool. This is about half of the old keydropshuffle option -* Cave Pots: The pots that are not found in dungeons are in the pool. (Includes the large block in Spike Cave). Does -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 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 +Normal mode for flute means you need to activate it at the village statue after finding it like usual. +Activated flute mode mean you can use it immediately upon finding it. the flute SFX plays to let you know this is the case. -By default, switches remain in their vanilla location (unless you turn on the legacy option below) +## Bow Mode -CLI `--pottery