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/Doors.py b/Doors.py index 751b2d4c..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 ['lean', '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 b3602680..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 ['lean', '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 4a1f38a4..d95b2cb8 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -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 712fb0d4..a19ff73c 100644 --- a/Rom.py +++ b/Rom.py @@ -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 16ba3eb8..f1383ee6 100644 --- a/Rules.py +++ b/Rules.py @@ -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/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 6dd111d7..b5de938d 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 @@ -91,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 {} @@ -98,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 @@ -131,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']) @@ -139,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 @@ -259,6 +267,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) @@ -273,9 +283,15 @@ 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) @@ -283,6 +299,7 @@ def do_main_shuffle(entrances, exits, avail, mode_def): 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: + #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 @@ -333,12 +350,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: @@ -354,6 +376,8 @@ 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 @@ -368,6 +392,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)') @@ -392,10 +420,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') @@ -452,11 +487,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') @@ -465,14 +509,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): @@ -482,6 +545,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: @@ -493,6 +561,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)) @@ -501,8 +573,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 @@ -514,6 +587,9 @@ 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 avail.world.owShuffle[avail.player] == 'vanilla': # simple shuffle - @@ -564,6 +640,10 @@ 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) @@ -598,14 +678,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): @@ -966,21 +1048,41 @@ 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: + for c in caves: + if swap_ext == c: + caves.remove(swap_ext) + break + if swap_ext in c: + c.remove(swap_ext) + if len(c) == 0: + caves.remove(c) + break def do_fixed_shuffle(avail, entrance_list): @@ -1008,7 +1110,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: @@ -1194,6 +1296,10 @@ def do_mandatory_connections(avail, entrances, cave_options, must_exit): if entrance in must_exit: must_exit.remove(entrance) 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) @@ -1206,6 +1312,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: @@ -1213,9 +1332,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!') @@ -1225,11 +1345,17 @@ def do_mandatory_connections(avail, entrances, cave_options, must_exit): 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 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) @@ -1243,6 +1369,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): @@ -1267,7 +1396,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): @@ -1378,6 +1512,46 @@ 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 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) @@ -1841,6 +2015,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', @@ -2003,6 +2194,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 = [] @@ -2083,6 +2276,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