diff --git a/BaseClasses.py b/BaseClasses.py index 57472ad9..099d4d53 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -132,7 +132,7 @@ class World(object): 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 ['lean', 'crossed', 'insanity']) + or shuffle[player] in ['lean', 'swapped', 'crossed', 'insanity']) set_player_attr('mapshuffle', False) set_player_attr('compassshuffle', False) set_player_attr('keyshuffle', 'none') @@ -3395,7 +3395,7 @@ class Pot(object): # byte 0: DDDE EEEE (DR, ER) 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} + 'lean': 9, "dungeonsfull": 7, "dungeonssimple": 6, "swapped": 10} # byte 1: LLLW WSS? (logic, mode, sword) logic_mode = {"noglitches": 0, "minorglitches": 1, "nologic": 2, "owglitches": 3, "majorglitches": 4} diff --git a/CHANGELOG.md b/CHANGELOG.md index 25fa22be..b156a0dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.3.2.0 +- New Swapped ER mode option +- Fixed issue with flipper rules not properly getting pearl requirement added +- Fixed minor issues in generating non-crossed ER on subsequent attempts + ## 0.3.1.2 - Retro now gives a universal key from bottle vendor fish prize - Fixed issue with Aga Door preventing Murahduhla Cutscene diff --git a/Doors.py b/Doors.py index 4020caff..8ba20e40 100644 --- a/Doors.py +++ b/Doors.py @@ -1499,7 +1499,7 @@ def create_doors(world, player): # static portal flags world.get_door('Sanctuary S', player).dead_end(allowPassage=True) - if world.mode[player] == 'open' and world.shuffle[player] not in ['crossed', 'insanity']: + if world.mode[player] == 'open' and world.shuffle[player] not in ['lean', 'swapped', 'crossed', 'insanity']: world.get_door('Sanctuary S', player).lw_restricted = True world.get_door('Eastern Hint Tile Blocked Path SE', player).passage = False world.get_door('TR Big Chest Entrance SE', player).passage = False diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 2056fe4c..3e67fa67 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -1360,7 +1360,7 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, dunge for name, builder in dungeon_map.items(): calc_allowance_and_dead_ends(builder, connections_tuple, world, player) - if world.mode[player] == 'open' and world.shuffle[player] not in ['crossed', 'insanity']: + if world.mode[player] == 'open' and world.shuffle[player] not in ['lean', 'swapped', 'crossed', 'insanity']: sanc = find_sector('Sanctuary', candidate_sectors) if sanc: # only run if sanc if a candidate lw_builders = [] diff --git a/ItemList.py b/ItemList.py index 01b95d68..bb23ecec 100644 --- a/ItemList.py +++ b/ItemList.py @@ -964,7 +964,7 @@ def balance_prices(world, player): def check_hints(world, player): - if world.shuffle[player] in ['simple', 'restricted', 'full', 'lite', 'lean', 'crossed', 'insanity']: + if world.shuffle[player] in ['simple', 'restricted', 'full', 'lite', 'lean', 'swapped', 'crossed', 'insanity']: for shop, location_list in shop_to_location_table.items(): if shop in ['Capacity Upgrade', 'Paradox Shop', 'Potion Shop']: continue # near the queen, near potions, and near 7 chests are fine diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 3f08fcbf..a76e4b33 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -7,7 +7,7 @@ from OWEdges import OWTileRegions, OWEdgeGroups, OWEdgeGroupsTerrain, OWExitType from OverworldGlitchRules import create_owg_connections from Utils import bidict -version_number = '0.3.1.2' +version_number = '0.3.2.0' # branch indicator is intentionally different across branches version_branch = '' @@ -705,7 +705,7 @@ def shuffle_tiles(world, groups, result_list, do_grouped, player): attempts -= 1 continue # ensure sanc can be placed in LW in certain modes - if not do_grouped and world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'lean', 'crossed', 'insanity'] and world.mode[player] != 'inverted' and (world.doorShuffle[player] != 'crossed' or world.intensity[player] < 3 or world.mode[player] == 'standard'): + if not do_grouped and world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'lean', 'swapped', 'crossed', 'insanity'] and world.mode[player] != 'inverted' and (world.doorShuffle[player] != 'crossed' or world.intensity[player] < 3 or world.mode[player] == 'standard'): free_dw_drops = parity[5] + (1 if world.shuffle_ganon else 0) free_drops = 6 + (1 if world.mode[player] != 'standard' else 0) + (1 if world.shuffle_ganon else 0) if free_dw_drops == free_drops: diff --git a/Rom.py b/Rom.py index c21832e0..a19ff73c 100644 --- a/Rom.py +++ b/Rom.py @@ -38,7 +38,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '614fa2fcbb4644beddadcf356e121d5a' +RANDOMIZERBASEHASH = '32f6a9f479f6ccb7e66e9906ff0d0e4c' class JsonRom(object): @@ -1661,7 +1661,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): # rom.write_byte(snes_to_pc(0x0DB730), 0x08) # allows chickens to travel across water # allow smith into multi-entrance caves in appropriate shuffles - if world.shuffle[player] in ['restricted', 'full', 'lite', 'lean', 'crossed', 'insanity'] or (world.shuffle[player] == 'simple' and world.mode[player] == 'inverted'): + if world.shuffle[player] in ['restricted', 'full', 'lite', 'lean', 'swapped', 'crossed', 'insanity'] or (world.shuffle[player] == 'simple' and world.mode[player] == 'inverted'): rom.write_byte(0x18004C, 0x01) # set correct flag for hera basement item diff --git a/Rules.py b/Rules.py index 2cd92599..f1383ee6 100644 --- a/Rules.py +++ b/Rules.py @@ -26,18 +26,6 @@ def set_rules(world, player): if world.swords[player] == 'swordless': swordless_rules(world, player) - ow_bunny_rules(world, player) - ow_terrain_rules(world, player) - - if world.mode[player] == 'standard': - if not world.is_copied_world: - standard_rules(world, player) - else: - misc_key_rules(world, player) - - bomb_rules(world, player) - pot_rules(world, player) - if world.logic[player] == 'noglitches': no_glitches_rules(world, player) elif world.logic[player] == 'minorglitches': @@ -54,6 +42,18 @@ def set_rules(world, player): else: raise NotImplementedError('Not implemented yet') + ow_bunny_rules(world, player) + ow_terrain_rules(world, player) + + if world.mode[player] == 'standard': + if not world.is_copied_world: + standard_rules(world, player) + else: + misc_key_rules(world, player) + + bomb_rules(world, player) + pot_rules(world, player) + if world.goal[player] == 'dungeons': # require all dungeons to beat ganon add_rule(world.get_location('Ganon', player), lambda state: state.can_reach('Master Sword Pedestal', 'Location', player) and state.has_beaten_aga(player) and state.has('Beat Agahnim 2', player) and state.has_crystals(7, player)) @@ -989,7 +989,7 @@ def ow_inverted_rules(world, player): else: set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has_beam_sword(player)) # barrier gets removed after killing agahnim, rule for that added later set_rule(world.get_entrance('GT Approach', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player)) - set_rule(world.get_entrance('GT Leave', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player) or state.world.shuffle[player] in ('restricted', 'full', 'lite', 'lean', 'crossed', 'insanity')) + set_rule(world.get_entrance('GT Leave', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player) or state.world.shuffle[player] in ('restricted', 'full', 'lite', 'lean', 'swapped', 'crossed', 'insanity')) if world.is_tile_swapped(0x03, player): set_rule(world.get_entrance('Spectacle Rock Approach', player), lambda state: world.logic[player] in ['noglitches', 'minorglitches'] and state.has_Pearl(player)) diff --git a/TestSuite.py b/TestSuite.py index 355c1883..9c2f29d0 100644 --- a/TestSuite.py +++ b/TestSuite.py @@ -49,6 +49,9 @@ def main(args=None): test("Shopsanity", "--shuffle vanilla --shopsanity") test("Simple ", "--shuffle simple") test("Full ", "--shuffle full") + test("Lite ", "--shuffle lite") + test("Lean ", "--shuffle lean") + test("Swapped ", "--shuffle swapped") test("Crossed ", "--shuffle crossed") test("Insanity ", "--shuffle insanity") test("OWG ", "--logic owglitches") diff --git a/TestSuiteStat.py b/TestSuiteStat.py index 92d066c6..ecde323d 100644 --- a/TestSuiteStat.py +++ b/TestSuiteStat.py @@ -14,7 +14,7 @@ ALL_SETTINGS = { 'mode': ['open', 'standard', 'inverted'], 'goal': ['ganon', 'pedestal', 'triforcehunt', 'trinity', 'crystals', 'dungeons'], 'swords': ['random', 'swordless', 'assured'], - 'shuffle': ['vanilla','simple','restricted','full','dungeonssimple','dungeonsfull','lite','lean','crossed','insanity'], + 'shuffle': ['vanilla','simple','restricted','full','dungeonssimple','dungeonsfull','lite','lean','swapped','crossed','insanity'], 'shufflelinks': [True, False], 'shuffleganon': [True, False], 'door_shuffle': ['vanilla', 'basic', 'crossed'], @@ -39,7 +39,7 @@ SETTINGS = { 'goal': ['ganon'], 'swords': ['random'], 'shuffle': ['vanilla', - 'dungeonssimple', 'dungeonsfull', 'simple', 'restricted', 'full', 'lite', 'lean', 'crossed', 'insanity' + 'dungeonssimple', 'dungeonsfull', 'simple', 'restricted', 'full', 'lite', 'lean', 'swapped', 'crossed', 'insanity' ], 'shufflelinks': [True, False], 'shuffleganon': [True, False], diff --git a/data/base2current.bps b/data/base2current.bps index 99b8b750..e6c9ca6b 100644 Binary files a/data/base2current.bps and b/data/base2current.bps differ diff --git a/mystery_example.yml b/mystery_example.yml index a74a8914..50ced9ab 100644 --- a/mystery_example.yml +++ b/mystery_example.yml @@ -91,6 +91,7 @@ full: 2 lite: 2 lean: 2 + swapped: 2 crossed: 3 insanity: 1 open_pyramid: diff --git a/mystery_testsuite.yml b/mystery_testsuite.yml index f919b7cc..7210fca6 100644 --- a/mystery_testsuite.yml +++ b/mystery_testsuite.yml @@ -56,6 +56,9 @@ entrance_shuffle: simple: 1 restricted: 1 full: 1 + lite: 1 + lean: 1 + swapped: 1 crossed: 1 insanity: 1 shufflelinks: diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 9f0d2d77..8dca326f 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -209,6 +209,7 @@ "full", "lite", "lean", + "swapped", "crossed", "insanity", "dungeonsfull", diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index f76bf61b..26190a46 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -211,6 +211,7 @@ "Lean: Same as Lite, except connectors can travel cross worlds.", "Crossed: Mix cave and dungeon entrances freely while allowing", " caves to cross between worlds.", + "Swapped: Same as Crossed, but entrances switch places in pairs.", "Insanity: Decouple entrances and exits from each other and", " shuffle them freely. Caves that used to be single", " entrance will still exit to the same location from", diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index 284da304..8dd28803 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -179,6 +179,7 @@ "randomizer.entrance.entranceshuffle.restricted": "Restricted", "randomizer.entrance.entranceshuffle.full": "Full", "randomizer.entrance.entranceshuffle.lean": "Lean", + "randomizer.entrance.entranceshuffle.swapped": "Swapped", "randomizer.entrance.entranceshuffle.crossed": "Crossed", "randomizer.entrance.entranceshuffle.insanity": "Insanity", "randomizer.entrance.entranceshuffle.dungeonsfull": "Dungeons + Full", diff --git a/resources/app/gui/randomize/entrando/widgets.json b/resources/app/gui/randomize/entrando/widgets.json index a3104bf1..c325ea26 100644 --- a/resources/app/gui/randomize/entrando/widgets.json +++ b/resources/app/gui/randomize/entrando/widgets.json @@ -9,6 +9,7 @@ "full", "lite", "lean", + "swapped", "crossed", "insanity", "dungeonsfull", diff --git a/source/overworld/EntranceShuffle2.py b/source/overworld/EntranceShuffle2.py index 89c6b0c0..f11d0081 100644 --- a/source/overworld/EntranceShuffle2.py +++ b/source/overworld/EntranceShuffle2.py @@ -12,6 +12,7 @@ class EntrancePool(object): self.exits = set() self.inverted = False self.coupled = True + self.swapped = False self.default_map = {} self.one_way_map = {} self.skull_handled = False @@ -69,6 +70,8 @@ def link_entrances_new(world, player): avail_pool.one_way_map = one_way_map global LW_Entrances, DW_Entrances + LW_Entrances = [] + DW_Entrances = [] for e in [e for e in avail_pool.entrances if e not in drop_map]: region = world.get_entrance(e, player).parent_region if region.type == RegionType.LightWorld: @@ -89,6 +92,7 @@ def link_entrances_new(world, player): if mode not in modes: raise RuntimeError(f'Shuffle mode {mode} is not yet supported') mode_cfg = copy.deepcopy(modes[mode]) + avail_pool.swapped = mode_cfg['undefined'] == 'swap' if avail_pool.is_standard(): do_standard_connections(avail_pool) pool_list = mode_cfg['pools'] if 'pools' in mode_cfg else {} @@ -96,7 +100,10 @@ def link_entrances_new(world, player): 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) + if avail_pool.swapped: + connect_swapped(holes, targets, avail_pool) + else: + connect_random(holes, targets, avail_pool) elif special_shuffle == 'normal_drops': cross_world = mode_cfg['cross_world'] == 'on' if 'cross_world' in mode_cfg else False keep_together = mode_cfg['keep_drops_together'] == 'on' if 'keep_drops_together' in mode_cfg else True @@ -129,7 +136,10 @@ def link_entrances_new(world, player): exits.remove('Skull Woods First Section Exit') connect_random(entrances, exits, avail_pool, True) entrances, exits = [rem_ent], ['Skull Woods First Section Exit'] - connect_random(entrances, exits, avail_pool, True) + if avail_pool.swapped: + connect_swapped(entrances, exits, avail_pool, True) + else: + connect_random(entrances, exits, avail_pool, True) avail_pool.skull_handled = True else: entrances, exits = find_entrances_and_exits(avail_pool, pool['entrances']) @@ -137,7 +147,7 @@ def link_entrances_new(world, player): undefined_behavior = mode_cfg['undefined'] if undefined_behavior == 'vanilla': do_vanilla_connections(avail_pool) - elif undefined_behavior == 'shuffle': + elif undefined_behavior in ['shuffle', 'swap']: do_main_shuffle(set(avail_pool.entrances), set(avail_pool.exits), avail_pool, mode_cfg) # afterward @@ -180,10 +190,10 @@ def do_vanilla_connections(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 + # drops and holes + keep_together = mode_def['keep_drops_together'] == 'on' if 'keep_drops_together' in mode_def else True do_holes_and_linked_drops(entrances, exits, avail, cross_world, keep_together) if not avail.coupled: @@ -199,6 +209,10 @@ def do_main_shuffle(entrances, exits, avail, mode_def): if not avail.coupled: avail.decoupled_entrances.remove('Agahnims Tower') avail.decoupled_exits.remove('Ganons Tower Exit') + if avail.swapped: + connect_swap('Agahnims Tower', 'Ganons Tower Exit', avail) + entrances.remove('Ganons Tower') + exits.remove('Agahnims Tower Exit') elif 'Ganons Tower' in entrances: connect_two_way('Ganons Tower', 'Ganons Tower Exit', avail) entrances.remove('Ganons Tower') @@ -257,6 +271,8 @@ def do_main_shuffle(entrances, exits, avail, mode_def): rem_exits.update([x for item in multi_exit_caves for x in item if x in avail.exits]) rem_exits.update(exits) + if avail.swapped: + rem_exits = [x for x in rem_exits if x in avail.exits] # old man cave do_old_man_cave_exit(rem_entrances, rem_exits, avail, cross_world) @@ -271,16 +287,25 @@ def do_main_shuffle(entrances, exits, avail, mode_def): bomb_shop_options = [x for x in rem_entrances] if avail.world.is_tile_swapped(0x03, avail.player): bomb_shop_options = [x for x in bomb_shop_options if x not in ['Spectacle Rock Cave', 'Spectacle Rock Cave (Bottom)']] + if avail.swapped and len(bomb_shop_options) > 1: + bomb_shop_options = [x for x in bomb_shop_options if x != 'Big Bomb Shop'] bomb_shop_choice = random.choice(bomb_shop_options) connect_entrance(bomb_shop_choice, bomb_shop, avail) rem_entrances.remove(bomb_shop_choice) + if avail.swapped and bomb_shop_choice != 'Big Bomb Shop': + swap_ent, swap_ext = connect_swap(bomb_shop_choice, bomb_shop, avail) + rem_exits.remove(swap_ext) + rem_entrances.remove(swap_ent) 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)' + + # old man S&Q cave if not cross_world: + #TODO: Add Swapped ER support for this # 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 @@ -331,12 +356,17 @@ def do_main_shuffle(entrances, exits, avail, mode_def): rem_entrances = list(unused_entrances) rem_entrances.sort() rem_exits = list(rem_exits if avail.coupled else avail.decoupled_exits) + if avail.swapped: + rem_exits = [x for x in rem_exits if x in avail.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) + if avail.swapped: + connect_swapped(rem_entrances, rem_exits, avail) + else: + 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: @@ -352,11 +382,16 @@ def do_old_man_cave_exit(entrances, exits, avail, cross_world): region_name = 'West Dark Death Mountain (Top)' om_cave_options = list(get_accessible_entrances(region_name, avail, [], cross_world, True, True, True)) om_cave_options = [e for e in om_cave_options if e in entrances and e != 'Old Man House (Bottom)'] + if avail.swapped: + om_cave_options = [e for e in om_cave_options if e not in Forbidden_Swap_Entrances] assert len(om_cave_options), 'No available entrances left to place Old Man Cave' random.shuffle(om_cave_options) om_cave_choice = None while not om_cave_choice: - om_cave_choice = om_cave_options.pop() + if not len(om_cave_options) and 'Old Man Cave (East)' in entrances: + om_cave_choice = 'Old Man Cave (East)' + else: + om_cave_choice = om_cave_options.pop() choice_region = avail.world.get_entrance(om_cave_choice, avail.player).parent_region.name if 'West Death Mountain (Bottom)' not in build_accessible_region_list(avail.world, choice_region, avail.player, True, True): om_cave_choice = None @@ -366,6 +401,10 @@ def do_old_man_cave_exit(entrances, exits, avail, cross_world): else: connect_two_way(om_cave_choice, 'Old Man Cave Exit (East)', avail) entrances.remove(om_cave_choice) + if avail.swapped and om_cave_choice != 'Old Man Cave (East)': + swap_ent, swap_ext = connect_swap(om_cave_choice, 'Old Man Cave Exit (East)', avail) + entrances.remove(swap_ent) + exits.remove(swap_ext) exits.remove('Old Man Cave Exit (East)') @@ -390,10 +429,17 @@ def do_blacksmith(entrances, exits, avail): blacksmith_options = list(OrderedDict.fromkeys(blacksmith_options + list(get_accessible_entrances(sanc_region.name, avail, assumed_inventory, False, True, True)))) else: logging.getLogger('').warning('Blacksmith is unable to use Sanctuary S&Q as initial accessibility because Sanctuary Exit has not been placed yet') + + if avail.swapped: + blacksmith_options = [e for e in blacksmith_options if e not in Forbidden_Swap_Entrances] blacksmith_options = [x for x in blacksmith_options if x in entrances] blacksmith_choice = random.choice(blacksmith_options) connect_entrance(blacksmith_choice, 'Blacksmiths Hut', avail) entrances.remove(blacksmith_choice) + if avail.swapped and blacksmith_choice != 'Blacksmiths Hut': + swap_ent, swap_ext = connect_swap(blacksmith_choice, 'Blacksmiths Hut', avail) + entrances.remove(swap_ent) + exits.remove(swap_ext) if not avail.coupled: avail.decoupled_exits.remove('Blacksmiths Hut') exits.remove('Blacksmiths Hut') @@ -450,11 +496,20 @@ def do_holes_and_linked_drops(entrances, exits, avail, cross_world, keep_togethe random.shuffle(hole_entrances) if not cross_world and 'Sanctuary Grave' in holes_to_shuffle: hc = avail.world.get_entrance('Hyrule Castle Exit (South)', avail.player) + is_hc_in_dw = avail.world.mode[avail.player] == 'inverted' + if hc.connected_region: + is_hc_in_dw = hc.connected_region.type == RegionType.DarkWorld chosen_entrance = None - if hc.connected_region and hc.connected_region.type == RegionType.DarkWorld: - chosen_entrance = next(entrance for entrance in hole_entrances if entrance[0] in DW_Entrances) + if is_hc_in_dw: + if avail.swapped: + chosen_entrance = next(e for e in hole_entrances if e[0] in DW_Entrances and e[0] != 'Sanctuary') + if not chosen_entrance: + chosen_entrance = next(e for e in hole_entrances if e[0] in DW_Entrances) if not chosen_entrance: - chosen_entrance = next(entrance for entrance in hole_entrances if entrance[0] in LW_Entrances) + if avail.swapped: + chosen_entrance = next(e for e in hole_entrances if e[0] in LW_Entrances and e[0] != 'Sanctuary') + if not chosen_entrance: + chosen_entrance = next(e for e in hole_entrances if e[0] in LW_Entrances) if chosen_entrance: hole_entrances.remove(chosen_entrance) sanc_interior = next(target for target in hole_targets if target[0] == 'Sanctuary Exit') @@ -463,14 +518,33 @@ def do_holes_and_linked_drops(entrances, exits, avail, cross_world, keep_togethe connect_entrance(chosen_entrance[1], sanc_interior[1], avail) # hole remove_from_list(entrances, [chosen_entrance[0], chosen_entrance[1]]) remove_from_list(exits, [sanc_interior[0], sanc_interior[1]]) + if avail.swapped and drop_map[chosen_entrance[1]] != sanc_interior[1]: + swap_ent, swap_ext = connect_swap(chosen_entrance[0], sanc_interior[0], avail) + swap_drop, swap_tgt = connect_swap(chosen_entrance[1], sanc_interior[1], avail) + hole_entrances.remove((swap_ent, swap_drop)) + hole_targets.remove((swap_ext, swap_tgt)) + remove_from_list(entrances, [swap_ent, swap_drop]) + remove_from_list(exits, [swap_ext, swap_tgt]) random.shuffle(hole_targets) - for entrance, drop in hole_entrances: - ext, target = hole_targets.pop() + while len(hole_entrances): + entrance, drop = hole_entrances.pop() + if avail.swapped and len(hole_targets) > 1: + ext, target = next((x, t) for x, t in hole_targets if x != entrance_map[entrance]) + hole_targets.remove((ext, target)) + else: + 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]) + if avail.swapped and drop_map[drop] != target: + swap_ent, swap_ext = connect_swap(entrance, ext, avail) + swap_drop, swap_tgt = connect_swap(drop, target, avail) + hole_entrances.remove((swap_ent, swap_drop)) + hole_targets.remove((swap_ext, swap_tgt)) + remove_from_list(entrances, [swap_ent, swap_drop]) + remove_from_list(exits, [swap_ext, swap_tgt]) def do_dark_sanc(entrances, exits, avail): @@ -480,6 +554,11 @@ def do_dark_sanc(entrances, exits, avail): forbidden = list(Isolated_LH_Doors) if not avail.world.is_tile_swapped(0x05, avail.player): forbidden.append('Mimic Cave') + if avail.swapped: + forbidden.append('Dark Sanctuary Hint') + forbidden.extend(Forbidden_Swap_Entrances) + if not avail.world.is_bombshop_start(avail.player): + forbidden.append('Links House') if avail.world.owShuffle[avail.player] == 'vanilla': choices = [e for e in avail.world.districts[avail.player]['Northwest Dark World'].entrances if e not in forbidden and e in entrances] else: @@ -491,6 +570,10 @@ def do_dark_sanc(entrances, exits, avail): ext.connect(avail.world.get_entrance(choice, avail.player).parent_region) if not avail.coupled: avail.decoupled_entrances.remove(choice) + if avail.swapped and choice != 'Dark Sanctuary Hint': + swap_ent, swap_ext = connect_swap(choice, 'Dark Sanctuary Hint', avail) + entrances.remove(swap_ent) + exits.remove(swap_ext) elif not ext.connected_region: # default to output to vanilla area, assume vanilla connection ext.connect(avail.world.get_region('Dark Chapel Area', avail.player)) @@ -499,8 +582,9 @@ def do_dark_sanc(entrances, exits, avail): def do_links_house(entrances, exits, avail, cross_world): lh_exit = 'Big Bomb Shop' if avail.world.is_bombshop_start(avail.player) else 'Links House Exit' if lh_exit in exits: + links_house_vanilla = 'Big Bomb Shop' if avail.world.is_bombshop_start(avail.player) else 'Links House' if not avail.world.shufflelinks[avail.player]: - links_house = 'Big Bomb Shop' if avail.world.is_bombshop_start(avail.player) else 'Links House' + links_house = links_house_vanilla else: entrance_pool = entrances if avail.coupled else avail.decoupled_entrances @@ -512,8 +596,11 @@ def do_links_house(entrances, exits, avail, cross_world): if avail.inverted: dark_sanc_region = avail.world.get_entrance('Dark Sanctuary Hint Exit', avail.player).connected_region.name forbidden.extend(get_nearby_entrances(avail, dark_sanc_region)) + if avail.swapped: + forbidden.append(links_house_vanilla) + forbidden.extend(Forbidden_Swap_Entrances) shuffle_mode = avail.world.shuffle[avail.player] - if shuffle_mode == 'vanilla': + if avail.world.owShuffle[avail.player] == 'vanilla': # simple shuffle - if shuffle_mode == 'simple': avail.links_on_mountain = True # taken care of by the logic below @@ -562,12 +649,18 @@ def do_links_house(entrances, exits, avail, cross_world): if not avail.coupled: avail.decoupled_entrances.remove(links_house) avail.decoupled_exits.remove(lh_exit) + if avail.swapped and links_house != links_house_vanilla: + swap_ent, swap_ext = connect_swap(links_house, lh_exit, avail) + entrances.remove(swap_ent) + exits.remove(swap_ext) # links on dm dm_spots = LH_DM_Connector_List.union(LH_DM_Exit_Forbidden) if links_house in dm_spots and avail.world.owShuffle[avail.player] == 'vanilla': if avail.links_on_mountain: return # connector is fine + logging.getLogger('').warning(f'Links House is placed in tight area and is now unhandled. Report any errors that occur from here.') + return if avail.world.shuffle[avail.player] in ['lite', 'lean']: rem_exits = [e for e in avail.exits if e in Connector_Exit_Set and e not in Dungeon_Exit_Set] multi_exit_caves = figure_out_connectors(rem_exits) @@ -594,14 +687,16 @@ def do_links_house(entrances, exits, avail, cross_world): possible_exits.sort() chosen_dm_escape = random.choice(possible_dm_exits) chosen_landing = random.choice(possible_exits) + chosen_exit_start = chosen_cave.pop(0) + chosen_exit_end = chosen_cave.pop() if avail.coupled: - connect_two_way(chosen_dm_escape, chosen_cave.pop(0), avail) - connect_two_way(chosen_landing, chosen_cave.pop(), avail) + connect_two_way(chosen_dm_escape, chosen_exit_start, avail) + connect_two_way(chosen_landing, chosen_exit_end, 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) + connect_entrance(chosen_dm_escape, chosen_exit_start, avail) + connect_exit(chosen_exit_end, chosen_landing, avail) entrances.remove(chosen_dm_escape) avail.decoupled_entrances.remove(chosen_landing) if len(chosen_cave): @@ -962,21 +1057,44 @@ def do_cross_world_connectors(entrances, caves, avail): cave_candidate = (None, 0) for i, cave in enumerate(caves): if isinstance(cave, str): - cave = (cave,) + 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,) + cave = [cave] - for ext in cave: + while len(cave): + ext = cave.pop() 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) + if avail.swapped and len(entrances) > 1: + chosen_entrance = next(e for e in entrances if combine_map[e] != ext) + entrances.remove(chosen_entrance) + else: + chosen_entrance = entrances.pop() + connect_two_way(chosen_entrance, ext, avail) + if avail.swapped: + swap_ent, swap_ext = connect_swap(chosen_entrance, ext, avail) + if swap_ent: + entrances.remove(swap_ent) + if chosen_entrance not in single_entrance_map: + if swap_ext in cave: + cave.remove(swap_ext) + else: + for c in caves: + if swap_ext == c: + caves.remove(swap_ext) + break + if not isinstance(c, str) and swap_ext in c: + c.remove(swap_ext) + if len(c) == 0: + caves.remove(c) + break def do_fixed_shuffle(avail, entrance_list): @@ -1004,7 +1122,7 @@ def do_fixed_shuffle(avail, entrance_list): choice = choices[i] elif rules.must_exit_to_lw: lw_exits = set() - for e, x in {**entrance_map, **single_entrance_map, **drop_map}.items(): + for e, x in combine_map.items(): if x in avail.exits: region = avail.world.get_entrance(e, avail.player).parent_region if region.type == RegionType.LightWorld: @@ -1189,7 +1307,12 @@ def do_mandatory_connections(avail, entrances, cave_options, must_exit): invalid_connections[entrance] = set() if entrance in must_exit: must_exit.remove(entrance) - entrances.append(entrance) + if entrance not in entrances: + entrances.append(entrance) + if avail.swapped: + swap_forbidden = [e for e in entrances if combine_map[e] in must_exit] + for e in swap_forbidden: + entrances.remove(e) entrances.sort() # sort these for consistency random.shuffle(entrances) random.shuffle(cave_options) @@ -1202,6 +1325,19 @@ def do_mandatory_connections(avail, entrances, cave_options, must_exit): invalid_connections[ext] = invalid_connections[ext].union({'Agahnims Tower', 'Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)'}) break + def connect_cave_swap(entrance, exit, current_cave): + swap_entrance, swap_exit = connect_swap(entrance, exit, avail) + if swap_entrance and entrance not in single_entrance_map: + for option in cave_options: + if swap_exit in option and option == current_cave: + x=0 + if swap_exit in option and option != current_cave: + option.remove(swap_exit) + if len(option) == 0: + cave_options.remove(option) + break + return swap_entrance, swap_exit + used_caves = [] required_entrances = 0 # Number of entrances reserved for used_caves while must_exit: @@ -1209,9 +1345,10 @@ def do_mandatory_connections(avail, entrances, cave_options, must_exit): # find multi exit cave candidates = [] for candidate in cave_options: - if not isinstance(candidate, str) and (candidate in used_caves + if not isinstance(candidate, str) and len(candidate) > 1 and (candidate in used_caves or len(candidate) < len(entrances) - required_entrances): - candidates.append(candidate) + if not avail.swapped or (combine_map[exit] not in candidate and not any(e for e in must_exit if combine_map[e] in candidate)): #maybe someday allow these, but we need to disallow mutual locks in Swapped + candidates.append(candidate) cave = random.choice(candidates) if cave is None: raise RuntimeError('No more caves left. Should not happen!') @@ -1219,13 +1356,22 @@ def do_mandatory_connections(avail, entrances, cave_options, must_exit): # 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) + if avail.swapped and exit in swap_forbidden: + swap_forbidden.remove(exit) + else: + entrances.remove(exit) connect_two_way(exit, rnd_cave[-1], avail) + if avail.swapped: + swap_ent, _ = connect_cave_swap(exit, rnd_cave[-1], cave) + entrances.remove(swap_ent) 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 avail.swapped and combine_map[entrance] != rnd_cave[0]: + swap_ent, _ = connect_cave_swap(entrance, rnd_cave[0], cave) + entrances.remove(swap_ent) if cave in used_caves: required_entrances -= 2 used_caves.remove(cave) @@ -1239,6 +1385,9 @@ def do_mandatory_connections(avail, entrances, cave_options, must_exit): cave_entrances.append(entrance) entrances.remove(entrance) connect_two_way(entrance, cave_exit, avail) + if avail.swapped and combine_map[entrance] != cave_exit: + swap_ent, _ = connect_cave_swap(entrance, cave_exit, cave) + entrances.remove(swap_ent) if entrance not in invalid_connections: invalid_connections[exit] = set() if all(entrance in invalid_connections for entrance in cave_entrances): @@ -1263,7 +1412,12 @@ def do_mandatory_connections(avail, entrances, cave_options, must_exit): invalid_cave_connections[tuple(cave)] = set() entrances.remove(entrance) connect_two_way(entrance, cave_exit, avail) + if avail.swapped and combine_map[entrance] != cave_exit: + swap_ent, _ = connect_cave_swap(entrance, cave_exit, cave) + entrances.remove(swap_ent) cave_options.remove(cave) + if avail.swapped: + entrances.extend(swap_forbidden) def do_mandatory_connections_decoupled(avail, cave_options, must_exit): @@ -1374,6 +1528,48 @@ def inverted_substitution(avail_pool, collection, is_entrance, is_set=False): pass +def connect_swapped(entrancelist, targetlist, avail, two_way=False): + random.shuffle(entrancelist) + sorted_targets = list() + for ent in entrancelist: + if ent in combine_map: + if combine_map[ent] not in targetlist: + logging.getLogger('').error(f'{combine_map[ent]} not in target list, cannot swap entrance') + raise Exception(f'{combine_map[ent]} not in target list, cannot swap entrance') + sorted_targets.append(combine_map[ent]) + if len(sorted_targets): + targetlist = list(sorted_targets) + else: + targetlist = list(targetlist) + indexlist = list(range(len(targetlist))) + random.shuffle(indexlist) + + while len(indexlist) > 1: + index1 = indexlist.pop() + index2 = indexlist.pop() + targetlist[index1], targetlist[index2] = targetlist[index2], targetlist[index1] + + for exit, target in zip(entrancelist, targetlist): + if two_way: + connect_two_way(exit, target, avail) + else: + connect_entrance(exit, target, avail) + + +def connect_swap(entrance, exit, avail): + swap_exit = combine_map[entrance] + if swap_exit != exit: + swap_entrance = next(e for e, x in combine_map.items() if x == exit) + if swap_entrance in ['Pyramid Entrance', 'Pyramid Hole'] and avail.world.is_tile_swapped(0x1b, avail.player): + swap_entrance = 'Inverted ' + swap_entrance + if entrance in entrance_map: + connect_two_way(swap_entrance, swap_exit, avail) + else: + connect_entrance(swap_entrance, swap_exit, avail) + return swap_entrance, swap_exit + return None, None + + def connect_random(exitlist, targetlist, avail, two_way=False): targetlist = list(targetlist) random.shuffle(targetlist) @@ -1837,6 +2033,23 @@ modes = { }, } }, + 'swapped': { + 'undefined': 'swap', + '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)'] + }, + } + }, 'crossed': { 'undefined': 'shuffle', 'keep_drops_together': 'on', @@ -1999,6 +2212,8 @@ single_entrance_map = { 'Blinds Hideout': 'Blinds Hideout', 'Waterfall of Wishing': 'Waterfall of Wishing' } +combine_map = {**entrance_map, **single_entrance_map, **drop_map} + LW_Entrances = [] DW_Entrances = [] @@ -2079,6 +2294,8 @@ Must_Exit_Invalid_Connections = defaultdict(set) Simple_DM_Non_Connectors = {'Old Man Cave Ledge', 'Spiral Cave (Top)', 'Superbunny Cave (Bottom)', 'Spectacle Rock Cave (Peak)', 'Spectacle Rock Cave (Top)'} +Forbidden_Swap_Entrances = {'Old Man Cave (East)', 'Blacksmiths Hut', 'Big Bomb Shop'} + # these are connections that cannot be shuffled and always exist. # They link together underworld regions