From d532a579e8272b3d923985ec1fc339b9aaf0f785 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sun, 30 Oct 2022 15:52:40 -0500 Subject: [PATCH 01/21] Changed copy_world_limited to premature and made it only for one player --- EntranceShuffle.py | 12 +++---- Main.py | 80 ++++++++++++++++++++++----------------------- OverworldShuffle.py | 12 +++---- 3 files changed, 52 insertions(+), 52 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 951c55f6..5ac4a5ac 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -1440,12 +1440,12 @@ def place_old_man(world, pool, player, ignore_list=[]): def junk_fill_inaccessible(world, player): - from Main import copy_world_limited + from Main import copy_world_premature find_inaccessible_regions(world, player) for p in range(1, world.players + 1): world.key_logic[p] = {} - base_world = copy_world_limited(world) + base_world = copy_world_premature(world, player) base_world.override_bomb_check = True # remove regions that have a dungeon entrance @@ -1602,12 +1602,12 @@ def unbias_dungeons(Dungeon_Exits): def build_accessible_entrance_list(world, start_region, player, assumed_inventory=[], cross_world=False, region_rules=True, exit_rules=True, include_one_ways=False): - from Main import copy_world_limited + from Main import copy_world_premature from Items import ItemFactory for p in range(1, world.players + 1): world.key_logic[p] = {} - base_world = copy_world_limited(world) + base_world = copy_world_premature(world, player) base_world.override_bomb_check = True connect_simple(base_world, 'Links House S&Q', start_region, player) @@ -1710,12 +1710,12 @@ def get_distant_entrances(world, start_entrance, player): def can_reach(world, entrance_name, region_name, player): - from Main import copy_world_limited + from Main import copy_world_premature from Items import ItemFactory for p in range(1, world.players + 1): world.key_logic[p] = {} - base_world = copy_world_limited(world) + base_world = copy_world_premature(world, player) base_world.override_bomb_check = True entrance = world.get_entrance(entrance_name, player) diff --git a/Main.py b/Main.py index 0efc38ff..d7646f83 100644 --- a/Main.py +++ b/Main.py @@ -558,7 +558,7 @@ def copy_world(world): return ret -def copy_world_limited(world): +def copy_world_premature(world, player): # ToDo: Not good yet ret = World(world.players, world.owShuffle, world.owCrossed, world.owMixed, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, @@ -621,63 +621,63 @@ def copy_world_limited(world): ret.is_copied_world = True - for player in range(1, world.players + 1): - create_regions(ret, player) - update_world_regions(ret, player) - if world.logic[player] in ('owglitches', 'nologic'): - create_owg_connections(ret, player) - create_flute_exits(ret, player) - create_dungeon_regions(ret, player) - create_owedges(ret, player) - create_shops(ret, player) - create_doors(ret, player) - create_rooms(ret, player) - create_dungeons(ret, player) + create_regions(ret, player) + update_world_regions(ret, player) + if world.logic[player] in ('owglitches', 'nologic'): + create_owg_connections(ret, player) + create_flute_exits(ret, player) + create_dungeon_regions(ret, player) + create_owedges(ret, player) + create_shops(ret, player) + create_doors(ret, player) + create_rooms(ret, player) + create_dungeons(ret, player) - for player in range(1, world.players + 1): - if world.mode[player] == 'standard': - parent = ret.get_region('Menu', player) - target = ret.get_region('Hyrule Castle Secret Entrance', player) - connection = Entrance(player, 'Uncle S&Q', parent) - parent.exits.append(connection) - connection.connect(target) + if world.mode[player] == 'standard': + parent = ret.get_region('Menu', player) + target = ret.get_region('Hyrule Castle Secret Entrance', player) + connection = Entrance(player, 'Uncle S&Q', parent) + parent.exits.append(connection) + connection.connect(target) # connect copied world copied_locations = {(loc.name, loc.player): loc for loc in ret.get_locations()} # caches all locations for region in world.regions: - copied_region = ret.get_region(region.name, region.player) - copied_region.is_light_world = region.is_light_world - copied_region.is_dark_world = region.is_dark_world - copied_region.dungeon = region.dungeon - copied_region.locations = [copied_locations[(location.name, location.player)] for location in region.locations if (location.name, location.player) in copied_locations] - for location in copied_region.locations: - location.parent_region = copied_region - for entrance in region.entrances: - ret.get_entrance(entrance.name, entrance.player).connect(copied_region) + if region.player == player: + copied_region = ret.get_region(region.name, region.player) + copied_region.is_light_world = region.is_light_world + copied_region.is_dark_world = region.is_dark_world + copied_region.dungeon = region.dungeon + copied_region.locations = [copied_locations[(location.name, location.player)] for location in region.locations if (location.name, location.player) in copied_locations] + for location in copied_region.locations: + location.parent_region = copied_region + for entrance in region.entrances: + ret.get_entrance(entrance.name, entrance.player).connect(copied_region) for item in world.precollected_items: - ret.push_precollected(ItemFactory(item.name, item.player)) + if item.player == player: + ret.push_precollected(ItemFactory(item.name, item.player)) for edge in world.owedges: - if edge.dest is not None: + if edge.player == player and edge.dest: copiededge = ret.check_for_owedge(edge.name, edge.player) if copiededge is not None: copiededge.dest = ret.check_for_owedge(edge.dest.name, edge.dest.player) for door in world.doors: - entrance = ret.check_for_entrance(door.name, door.player) - if entrance is not None: - destdoor = ret.check_for_door(entrance.door.name, entrance.door.player) - entrance.door = destdoor - if destdoor is not None: - destdoor.entrance = entrance + if door.player == player: + entrance = ret.check_for_entrance(door.name, door.player) + if entrance is not None: + destdoor = ret.check_for_door(entrance.door.name, entrance.door.player) + entrance.door = destdoor + if destdoor is not None: + destdoor.entrance = entrance ret.key_logic = world.key_logic.copy() from OverworldShuffle import categorize_world_regions - for player in range(1, world.players + 1): - categorize_world_regions(ret, player) - set_rules(ret, player) + categorize_world_regions(ret, player) + set_rules(ret, player) return ret diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 340152a6..d88c8d58 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -904,13 +904,13 @@ def can_reach_smith(world, player): return found def build_sectors(world, player): - from Main import copy_world_limited + from Main import copy_world_premature from OWEdges import OWTileRegions # perform accessibility check on duplicate world for p in range(1, world.players + 1): world.key_logic[p] = {} - base_world = copy_world_limited(world) + base_world = copy_world_premature(world, player) # build lists of contiguous regions accessible with full inventory (excl portals/mirror/flute/entrances) regions = list(OWTileRegions.copy().keys()) @@ -970,7 +970,7 @@ def build_sectors(world, player): def build_accessible_region_list(world, start_region, player, build_copy_world=False, cross_world=False, region_rules=True, ignore_ledges = False): from BaseClasses import CollectionState - from Main import copy_world_limited + from Main import copy_world_premature from Items import ItemFactory from Utils import stack_size3a @@ -997,7 +997,7 @@ def build_accessible_region_list(world, start_region, player, build_copy_world=F if build_copy_world: for p in range(1, world.players + 1): world.key_logic[p] = {} - base_world = copy_world_limited(world) + base_world = copy_world_premature(world, player) base_world.override_bomb_check = True else: base_world = world @@ -1040,7 +1040,7 @@ def validate_layout(world, player): 'Pyramid Area': ['Pyramid Exit Ledge'] } - from Main import copy_world_limited + from Main import copy_world_premature from Utils import stack_size3a from EntranceShuffle import default_dungeon_connections, default_connector_connections, default_item_connections, default_shop_connections, default_drop_connections, default_dropexit_connections @@ -1073,7 +1073,7 @@ def validate_layout(world, player): for p in range(1, world.players + 1): world.key_logic[p] = {} - base_world = copy_world_limited(world) + base_world = copy_world_premature(world, player) explored_regions = list() if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull'] or not world.shufflelinks[player]: From e83d0fa8e48fcf2bbf3c230200c276ae8edcd546 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sun, 30 Oct 2022 16:04:53 -0500 Subject: [PATCH 02/21] Bumper cave fix --- EntranceShuffle.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 5ac4a5ac..86121e74 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -2138,8 +2138,8 @@ default_connector_connections = [('Old Man Cave (West)', 'Old Man Cave Exit (Wes ('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)'), - ('Bumper Cave (Top)', 'Bumper Cave (top)'), - ('Bumper Cave (Bottom)', 'Bumper Cave (bottom)'), + ('Bumper Cave (Top)', 'Bumper Cave Exit (Top)'), + ('Bumper Cave (Bottom)', 'Bumper Cave Exit (Bottom)'), ('Superbunny Cave (Top)', 'Superbunny Cave Exit (Top)'), ('Superbunny Cave (Bottom)', 'Superbunny Cave Exit (Bottom)'), ('Hookshot Cave', 'Hookshot Cave Front Exit'), From 8919729262bc42c394ad9fab24add56daec7fbe3 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Mon, 31 Oct 2022 13:43:18 -0500 Subject: [PATCH 03/21] Merged light/dark world marking --- Main.py | 7 ++-- OverworldShuffle.py | 5 ++- Regions.py | 83 +++++++++++++++++---------------------------- 3 files changed, 35 insertions(+), 60 deletions(-) diff --git a/Main.py b/Main.py index d7646f83..d375b24b 100644 --- a/Main.py +++ b/Main.py @@ -14,7 +14,7 @@ from Items import ItemFactory from KeyDoorShuffle import validate_key_placement from OverworldGlitchRules import create_owg_connections from PotShuffle import shuffle_pots, shuffle_pot_switches -from Regions import create_regions, create_shops, mark_light_world_regions, mark_dark_world_regions, create_dungeon_regions, adjust_locations +from Regions import create_regions, create_shops, mark_light_dark_world_regions, create_dungeon_regions, adjust_locations from OWEdges import create_owedges from OverworldShuffle import link_overworld, update_world_regions, create_flute_exits from EntranceShuffle import link_entrances @@ -204,10 +204,7 @@ def main(args, seed=None, fish=None): for player in range(1, world.players + 1): link_doors(world, player) - if world.mode[player] != 'inverted': - mark_light_world_regions(world, player) - else: - mark_dark_world_regions(world, player) + mark_light_dark_world_regions(world, player) logger.info(world.fish.translate("cli", "cli", "generating.itempool")) for player in range(1, world.players + 1): diff --git a/OverworldShuffle.py b/OverworldShuffle.py index d88c8d58..aa877f5c 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -2,7 +2,7 @@ import RaceRandom as random, logging, copy from collections import OrderedDict, defaultdict from DungeonGenerator import GenerationException from BaseClasses import OWEdge, WorldType, RegionType, Direction, Terrain, PolSlot, Entrance -from Regions import mark_dark_world_regions, mark_light_world_regions +from Regions import mark_light_dark_world_regions from OWEdges import OWTileRegions, OWEdgeGroups, OWEdgeGroupsTerrain, OWExitTypes, OpenStd, parallel_links, IsParallel from OverworldGlitchRules import create_owg_connections from Utils import bidict @@ -843,8 +843,7 @@ def categorize_world_regions(world, player): for exitname in OWExitTypes[type]: world.get_entrance(exitname, player).spot_type = type - mark_light_world_regions(world, player) - mark_dark_world_regions(world, player) + mark_light_dark_world_regions(world, player) def update_world_regions(world, player): if world.owMixed[player]: diff --git a/Regions.py b/Regions.py index 027d4e1b..e9755a8c 100644 --- a/Regions.py +++ b/Regions.py @@ -1076,67 +1076,46 @@ def _create_region(player, name, type, hint='Hyrule', locations=None, exits=None ret.locations.append(Location(player, location, address, crystal, hint_text, ret, None, player_address)) return ret -def mark_light_world_regions(world, player): +def mark_light_dark_world_regions(world, player): # cross world caves may have some sections marked as both in_light_world, and in_dark_work. # That is ok. the bunny logic will check for this case and incorporate special rules. - queue = collections.deque(region for region in world.get_regions(player) if region.type == RegionType.LightWorld) - seen = set(queue) - while queue: - current = queue.popleft() - current.is_light_world = True - for exit in current.exits: - if exit.connected_region is None or exit.connected_region.type == RegionType.DarkWorld: # todo: remove none check - # Don't venture into the dark world - continue - if exit.connected_region not in seen: - seen.add(exit.connected_region) - queue.append(exit.connected_region) + # Note: I don't see why the order would matter, but the original Inverted code reversed the order + if world.mode[player] != 'inverted': + mark_light() + mark_dark() + else: + mark_dark() + mark_light() - queue = collections.deque(region for region in world.get_regions(player) if region.type == RegionType.DarkWorld) - seen = set(queue) - while queue: - current = queue.popleft() - current.is_dark_world = True - for exit in current.exits: - if exit.connected_region is not None: - if exit.connected_region.type == RegionType.LightWorld: - # Don't venture into the light world - continue - if exit.connected_region not in seen: - seen.add(exit.connected_region) - queue.append(exit.connected_region) - - -def mark_dark_world_regions(world, player): - # cross world caves may have some sections marked as both in_light_world, and in_dark_work. - # That is ok. the bunny logic will check for this case and incorporate special rules. - queue = collections.deque(region for region in world.get_regions(player) if region.type == RegionType.DarkWorld) - seen = set(queue) - while queue: - current = queue.popleft() - current.is_dark_world = True - for exit in current.exits: - if exit.connected_region is None or exit.connected_region.type == RegionType.LightWorld: # todo: remove none check - # Don't venture into the light world - continue - if exit.connected_region not in seen: - seen.add(exit.connected_region) - queue.append(exit.connected_region) - - queue = collections.deque(region for region in world.get_regions(player) if region.type == RegionType.LightWorld) - seen = set(queue) - while queue: - current = queue.popleft() - current.is_light_world = True - for exit in current.exits: - if exit.connected_region is not None: - if exit.connected_region.type == RegionType.DarkWorld: + def mark_light(): + queue = collections.deque(region for region in world.get_regions(player) if region.type == RegionType.LightWorld) + seen = set(queue) + while queue: + current = queue.popleft() + current.is_light_world = True + for exit in current.exits: + if exit.connected_region is None or exit.connected_region.type == RegionType.DarkWorld: # todo: remove none check # Don't venture into the dark world continue if exit.connected_region not in seen: seen.add(exit.connected_region) queue.append(exit.connected_region) + def mark_dark(): + queue = collections.deque(region for region in world.get_regions(player) if region.type == RegionType.DarkWorld) + seen = set(queue) + while queue: + current = queue.popleft() + current.is_dark_world = True + for exit in current.exits: + if exit.connected_region is not None: + if exit.connected_region.type == RegionType.LightWorld: + # Don't venture into the light world + continue + if exit.connected_region not in seen: + seen.add(exit.connected_region) + queue.append(exit.connected_region) + def create_shops(world, player): world.shops[player] = [] From 246006d3949a69f96689793889da825cdf031155 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Mon, 31 Oct 2022 14:10:03 -0500 Subject: [PATCH 04/21] Merged light/dark world marking --- Regions.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Regions.py b/Regions.py index e9755a8c..d568497c 100644 --- a/Regions.py +++ b/Regions.py @@ -1079,14 +1079,6 @@ def _create_region(player, name, type, hint='Hyrule', locations=None, exits=None def mark_light_dark_world_regions(world, player): # cross world caves may have some sections marked as both in_light_world, and in_dark_work. # That is ok. the bunny logic will check for this case and incorporate special rules. - # Note: I don't see why the order would matter, but the original Inverted code reversed the order - if world.mode[player] != 'inverted': - mark_light() - mark_dark() - else: - mark_dark() - mark_light() - def mark_light(): queue = collections.deque(region for region in world.get_regions(player) if region.type == RegionType.LightWorld) seen = set(queue) @@ -1116,6 +1108,14 @@ def mark_light_dark_world_regions(world, player): seen.add(exit.connected_region) queue.append(exit.connected_region) + # Note: I don't see why the order would matter, but the original Inverted code reversed the order + if world.mode[player] != 'inverted': + mark_light() + mark_dark() + else: + mark_dark() + mark_light() + def create_shops(world, player): world.shops[player] = [] From 4639db8b2a20e6519e5ffb772b7e9240b0531d52 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Mon, 31 Oct 2022 16:11:56 -0500 Subject: [PATCH 05/21] Marking light/dark regions after OWR --- Main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Main.py b/Main.py index d375b24b..64c83655 100644 --- a/Main.py +++ b/Main.py @@ -187,6 +187,7 @@ def main(args, seed=None, fish=None): link_overworld(world, player) create_shops(world, player) update_world_regions(world, player) + mark_light_dark_world_regions(world, player) create_flute_exits(world, player) logger.info(world.fish.translate("cli","cli","shuffling.world")) From e4d760a00c9ccf37641f443c773d6bb87a636ce7 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Mon, 31 Oct 2022 16:13:52 -0500 Subject: [PATCH 06/21] Improvement to copy_world_premature --- Main.py | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/Main.py b/Main.py index 64c83655..c96f3dcd 100644 --- a/Main.py +++ b/Main.py @@ -639,18 +639,24 @@ def copy_world_premature(world, player): connection.connect(target) # connect copied world - copied_locations = {(loc.name, loc.player): loc for loc in ret.get_locations()} # caches all locations + copied_locations = {(loc.name, loc.player): loc for loc in ret.get_locations() if loc.player == player} # caches all locations for region in world.regions: if region.player == player: copied_region = ret.get_region(region.name, region.player) - copied_region.is_light_world = region.is_light_world - copied_region.is_dark_world = region.is_dark_world - copied_region.dungeon = region.dungeon + if region.dungeon: + copied_region.dungeon = ret.get_dungeon(region.dungeon.name, region.player) copied_region.locations = [copied_locations[(location.name, location.player)] for location in region.locations if (location.name, location.player) in copied_locations] for location in copied_region.locations: location.parent_region = copied_region for entrance in region.entrances: - ret.get_entrance(entrance.name, entrance.player).connect(copied_region) + copied_region.entrances.append(ret.get_entrance(entrance.name, entrance.player)) + for exit in region.exits: + if exit.connected_region: + dest_region = ret.get_region(exit.connected_region.name, region.player) + ret.get_entrance(exit.name, exit.player).connect(dest_region) + + from OverworldShuffle import categorize_world_regions + categorize_world_regions(ret, player) for item in world.precollected_items: if item.player == player: @@ -659,22 +665,19 @@ def copy_world_premature(world, player): for edge in world.owedges: if edge.player == player and edge.dest: copiededge = ret.check_for_owedge(edge.name, edge.player) - if copiededge is not None: - copiededge.dest = ret.check_for_owedge(edge.dest.name, edge.dest.player) + copiededge.dest = ret.check_for_owedge(edge.dest.name, edge.dest.player) for door in world.doors: if door.player == player: - entrance = ret.check_for_entrance(door.name, door.player) - if entrance is not None: - destdoor = ret.check_for_door(entrance.door.name, entrance.door.player) - entrance.door = destdoor - if destdoor is not None: - destdoor.entrance = entrance + copied_door = ret.check_for_door(door.name, door.player) + copied_entrance = ret.check_for_entrance(door.name, door.player) + copied_door.entrance = copied_entrance + copied_entrance.door = copied_door - ret.key_logic = world.key_logic.copy() + for player, portals in world.dungeon_portals.items(): + for portal in portals: + connect_portal(portal, ret, player) - from OverworldShuffle import categorize_world_regions - categorize_world_regions(ret, player) set_rules(ret, player) return ret From 734740a0a7efa1e6e53adea29a5e699ea6b2fbb3 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 1 Nov 2022 11:45:43 -0500 Subject: [PATCH 07/21] Improvement to copy_world_premature --- Main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Main.py b/Main.py index c96f3dcd..aa9572c9 100644 --- a/Main.py +++ b/Main.py @@ -616,6 +616,7 @@ def copy_world_premature(world, player): ret.owflutespots = world.owflutespots.copy() ret.prizes = world.prizes.copy() ret.restrict_boss_items = world.restrict_boss_items.copy() + ret.key_logic = world.key_logic.copy() ret.is_copied_world = True From 3e17b61428603dd4d9478ea33196f66326e156dc Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 1 Nov 2022 12:36:00 -0500 Subject: [PATCH 08/21] Improvement to copy_world --- Main.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Main.py b/Main.py index aa9572c9..b5635fde 100644 --- a/Main.py +++ b/Main.py @@ -437,6 +437,8 @@ def copy_world(world): ret.owFluteShuffle = world.owFluteShuffle.copy() ret.shuffle_bonk_drops = world.shuffle_bonk_drops.copy() ret.open_pyramid = world.open_pyramid.copy() + ret.shufflelinks = world.shufflelinks.copy() + ret.shuffle_ganon = world.shuffle_ganon.copy() ret.boss_shuffle = world.boss_shuffle.copy() ret.enemy_shuffle = world.enemy_shuffle.copy() ret.enemy_health = world.enemy_health.copy() @@ -513,6 +515,8 @@ def copy_world(world): new_location.event = True if location.locked: new_location.locked = True + if location.skip: + new_location.skip = True # these need to be modified properly by set_rules new_location.access_rule = lambda state: True new_location.item_rule = lambda state: True @@ -599,6 +603,8 @@ def copy_world_premature(world, player): ret.owFluteShuffle = world.owFluteShuffle.copy() ret.shuffle_bonk_drops = world.shuffle_bonk_drops.copy() ret.open_pyramid = world.open_pyramid.copy() + ret.shufflelinks = world.shufflelinks.copy() + ret.shuffle_ganon = world.shuffle_ganon.copy() ret.boss_shuffle = world.boss_shuffle.copy() ret.enemy_shuffle = world.enemy_shuffle.copy() ret.enemy_health = world.enemy_health.copy() @@ -671,7 +677,7 @@ def copy_world_premature(world, player): for door in world.doors: if door.player == player: copied_door = ret.check_for_door(door.name, door.player) - copied_entrance = ret.check_for_entrance(door.name, door.player) + copied_entrance = ret.check_for_entrance(door.entrance.name, door.player) copied_door.entrance = copied_entrance copied_entrance.door = copied_door From dadf88bcda2a283fe11c39357e58e139e3c1c9b0 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 1 Nov 2022 15:26:11 -0500 Subject: [PATCH 09/21] Fixed sweep_for_events to better detect new locations --- BaseClasses.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index d9da6466..0cc91899 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1006,18 +1006,17 @@ class CollectionState(object): 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 (not self.world.keyshuffle[location.item.player] 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 7b25e1931f0b3b60b7913eb9dbeeb89217978df8 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 1 Nov 2022 15:30:23 -0500 Subject: [PATCH 10/21] Aerinon's keylogic fix for Vanilla Mire case --- BaseClasses.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 0cc91899..c508b8d0 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -757,7 +757,7 @@ class CollectionState(object): child_states.append(child_state) else: terminal_states.append(next_child) - common_regions, common_bc, common_doors, first = {}, {}, set(), True + common_regions, common_bc, common_doors, common_r_doors, first = {}, {}, set(), set(), True bc = self.blocked_connections[player] for term_state in terminal_states: t_rrp = term_state.reachable_regions[player] @@ -768,6 +768,8 @@ class CollectionState(object): common_bc = {x: y for x, y in t_bc.items() if x not in bc} common_doors = {x for x in term_state.opened_doors[player] - self.opened_doors[player] if valid_d_door(x)} + common_r_doors = {x for x in term_state.reached_doors[player] - self.reached_doors[player] + if valid_d_door(x)} else: cm_rrp = {x: y for x, y in t_rrp.items() if x not in rrp or y != rrp[x]} common_regions = {k: self.comb_crys(v, cm_rrp[k]) for k, v in common_regions.items() @@ -775,15 +777,19 @@ class CollectionState(object): common_bc.update({x: y for x, y in t_bc.items() if x not in bc and x not in common_bc}) common_doors &= {x for x in term_state.opened_doors[player] - self.opened_doors[player] if valid_d_door(x)} + common_r_doors &= {x for x in term_state.reached_doors[player] - self.reached_doors[player] + if valid_d_door(x)} terminal_queue = deque() - for door in common_doors: + for door in common_r_doors: pair = self.find_door_pair(player, dungeon_name, door) if door not in self.reached_doors[player]: self.door_counter[player][0][dungeon_name] += 1 self.reached_doors[player].add(door) if pair not in self.reached_doors[player]: self.reached_doors[player].add(pair) + for door in common_doors: + pair = self.find_door_pair(player, dungeon_name, door) self.opened_doors[player].add(door) if door in checklist: terminal_queue.append(checklist[door]) @@ -813,8 +819,8 @@ class CollectionState(object): missing_bc[blocked] = crystal for k in missing_bc: bc[k] = missing_bc[k] - self.record_dungeon_exploration(player, dungeon_name, checklist, - common_doors, missing_regions, missing_bc, paths) + self.record_dungeon_exploration(player, dungeon_name, checklist, common_doors, common_r_doors, + missing_regions, missing_bc, paths) checklist.clear() @staticmethod @@ -895,15 +901,17 @@ class CollectionState(object): exp_key = (prog_set, frozenset(checklist)) if dungeon_name in ec and exp_key in ec[dungeon_name]: # apply - common_doors, missing_regions, missing_bc, paths = ec[dungeon_name][exp_key] + common_doors, common_r_doors, missing_regions, missing_bc, paths = ec[dungeon_name][exp_key] terminal_queue = deque() - for door in common_doors: + for door in common_r_doors: pair = self.find_door_pair(player, dungeon_name, door) if door not in self.reached_doors[player]: self.door_counter[player][0][dungeon_name] += 1 self.reached_doors[player].add(door) if pair not in self.reached_doors[player]: self.reached_doors[player].add(pair) + for door in common_doors: + pair = self.find_door_pair(player, dungeon_name, door) self.opened_doors[player].add(door) if door in checklist: terminal_queue.append(checklist[door]) @@ -930,11 +938,11 @@ class CollectionState(object): return False def record_dungeon_exploration(self, player, dungeon_name, checklist, - common_doors, missing_regions, missing_bc, paths): + common_doors, common_r_doors, missing_regions, missing_bc, paths): ec = self.world.exp_cache[player] prog_set = self.reduce_prog_items(player, dungeon_name) exp_key = (prog_set, frozenset(checklist)) - ec[dungeon_name][exp_key] = (common_doors, missing_regions, missing_bc, paths) + ec[dungeon_name][exp_key] = (common_doors, common_r_doors, missing_regions, missing_bc, paths) def reduce_prog_items(self, player, dungeon_name): # todo: possibly could include an analysis of dungeon items req. like Hammer, Hookshot, etc From 45065134cde8c6c9fc8cac80c854eeb571a091de Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 1 Nov 2022 15:41:39 -0500 Subject: [PATCH 11/21] Improved OWEdges in copy_world --- Main.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Main.py b/Main.py index b5635fde..e99b4000 100644 --- a/Main.py +++ b/Main.py @@ -464,6 +464,7 @@ def copy_world(world): create_owg_connections(ret, player) create_flute_exits(ret, player) create_dungeon_regions(ret, player) + create_owedges(ret, player) create_shops(ret, player) create_rooms(ret, player) create_dungeons(ret, player) @@ -534,7 +535,11 @@ def copy_world(world): ret.state.prog_items = world.state.prog_items.copy() ret.state.stale = {player: True for player in range(1, world.players + 1)} - ret.owedges = world.owedges + for edge in world.owedges: + if edge.dest: + copiededge = ret.check_for_owedge(edge.name, edge.player) + copiededge.dest = ret.check_for_owedge(edge.dest.name, edge.dest.player) + ret.doors = world.doors for door in ret.doors: entrance = ret.check_for_entrance(door.name, door.player) From 9d46aaec58dfa2c1a6e24c8cfd3f6053433d78fe Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 1 Nov 2022 15:42:10 -0500 Subject: [PATCH 12/21] Adding pre-aga Farmable locations to optional location list --- Main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Main.py b/Main.py index e99b4000..2cc834d3 100644 --- a/Main.py +++ b/Main.py @@ -726,6 +726,8 @@ def create_playthrough(world): # get locations containing progress items prog_locations = [location for location in world.get_filled_locations() if location.item.advancement] optional_locations = ['Trench 1 Switch', 'Trench 2 Switch', 'Ice Block Drop', 'Skull Star Tile'] + optional_locations.extend(['Hyrule Castle Courtyard Tree Pull', 'Mountain Entry Area Tree Pull']) # adding pre-aga tree pulls + optional_locations.extend(['Lumberjack Area Bush Crab', 'South Pass Area Bush Crab']) # adding pre-aga bush crabs state_cache = [None] collection_spheres = [] state = CollectionState(world) From 34d77653d340ef0bdfbd3e72ad8b65cd02ec1f61 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 1 Nov 2022 15:05:28 -0600 Subject: [PATCH 13/21] More robust sweep_for_events (supports events that later become unreachable) Minor fix for AllowSmall key logic --- BaseClasses.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index c508b8d0..c2f28d16 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -862,7 +862,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): @@ -1003,11 +1017,13 @@ 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 From 29a1e1fc120493d1938eaab80a18feb409341f45 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 1 Nov 2022 16:09:24 -0500 Subject: [PATCH 14/21] Revert "Aerinon's keylogic fix for Vanilla Mire case" This reverts commit 7b25e1931f0b3b60b7913eb9dbeeb89217978df8. --- BaseClasses.py | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index c2f28d16..9195b60e 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -757,7 +757,7 @@ class CollectionState(object): child_states.append(child_state) else: terminal_states.append(next_child) - common_regions, common_bc, common_doors, common_r_doors, first = {}, {}, set(), set(), True + common_regions, common_bc, common_doors, first = {}, {}, set(), True bc = self.blocked_connections[player] for term_state in terminal_states: t_rrp = term_state.reachable_regions[player] @@ -768,8 +768,6 @@ class CollectionState(object): common_bc = {x: y for x, y in t_bc.items() if x not in bc} common_doors = {x for x in term_state.opened_doors[player] - self.opened_doors[player] if valid_d_door(x)} - common_r_doors = {x for x in term_state.reached_doors[player] - self.reached_doors[player] - if valid_d_door(x)} else: cm_rrp = {x: y for x, y in t_rrp.items() if x not in rrp or y != rrp[x]} common_regions = {k: self.comb_crys(v, cm_rrp[k]) for k, v in common_regions.items() @@ -777,19 +775,15 @@ class CollectionState(object): common_bc.update({x: y for x, y in t_bc.items() if x not in bc and x not in common_bc}) common_doors &= {x for x in term_state.opened_doors[player] - self.opened_doors[player] if valid_d_door(x)} - common_r_doors &= {x for x in term_state.reached_doors[player] - self.reached_doors[player] - if valid_d_door(x)} terminal_queue = deque() - for door in common_r_doors: + for door in common_doors: pair = self.find_door_pair(player, dungeon_name, door) if door not in self.reached_doors[player]: self.door_counter[player][0][dungeon_name] += 1 self.reached_doors[player].add(door) if pair not in self.reached_doors[player]: self.reached_doors[player].add(pair) - for door in common_doors: - pair = self.find_door_pair(player, dungeon_name, door) self.opened_doors[player].add(door) if door in checklist: terminal_queue.append(checklist[door]) @@ -819,8 +813,8 @@ class CollectionState(object): missing_bc[blocked] = crystal for k in missing_bc: bc[k] = missing_bc[k] - self.record_dungeon_exploration(player, dungeon_name, checklist, common_doors, common_r_doors, - missing_regions, missing_bc, paths) + self.record_dungeon_exploration(player, dungeon_name, checklist, + common_doors, missing_regions, missing_bc, paths) checklist.clear() @staticmethod @@ -915,17 +909,15 @@ class CollectionState(object): exp_key = (prog_set, frozenset(checklist)) if dungeon_name in ec and exp_key in ec[dungeon_name]: # apply - common_doors, common_r_doors, missing_regions, missing_bc, paths = ec[dungeon_name][exp_key] + common_doors, missing_regions, missing_bc, paths = ec[dungeon_name][exp_key] terminal_queue = deque() - for door in common_r_doors: + for door in common_doors: pair = self.find_door_pair(player, dungeon_name, door) if door not in self.reached_doors[player]: self.door_counter[player][0][dungeon_name] += 1 self.reached_doors[player].add(door) if pair not in self.reached_doors[player]: self.reached_doors[player].add(pair) - for door in common_doors: - pair = self.find_door_pair(player, dungeon_name, door) self.opened_doors[player].add(door) if door in checklist: terminal_queue.append(checklist[door]) @@ -952,11 +944,11 @@ class CollectionState(object): return False def record_dungeon_exploration(self, player, dungeon_name, checklist, - common_doors, common_r_doors, missing_regions, missing_bc, paths): + common_doors, missing_regions, missing_bc, paths): ec = self.world.exp_cache[player] prog_set = self.reduce_prog_items(player, dungeon_name) exp_key = (prog_set, frozenset(checklist)) - ec[dungeon_name][exp_key] = (common_doors, common_r_doors, missing_regions, missing_bc, paths) + ec[dungeon_name][exp_key] = (common_doors, missing_regions, missing_bc, paths) def reduce_prog_items(self, player, dungeon_name): # todo: possibly could include an analysis of dungeon items req. like Hammer, Hookshot, etc From 06216d977ba54dfd45d9cce9236a606381e73b7b Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 1 Nov 2022 16:14:01 -0500 Subject: [PATCH 15/21] Potential key lock logic fix --- Rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rules.py b/Rules.py index 07c7e6af..a7941814 100644 --- a/Rules.py +++ b/Rules.py @@ -2111,7 +2111,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 35d9b48cad91775bd2a51c6fd8398b805845dfd5 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 1 Nov 2022 17:06:11 -0500 Subject: [PATCH 16/21] "Should" be a better copied world Prior to this, it was as close to DR as could be, but I'm pretty sure this is more correct --- Main.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Main.py b/Main.py index 2cc834d3..6deb977e 100644 --- a/Main.py +++ b/Main.py @@ -456,6 +456,7 @@ def copy_world(world): ret.owflutespots = world.owflutespots.copy() ret.prizes = world.prizes.copy() ret.restrict_boss_items = world.restrict_boss_items.copy() + ret.inaccessible_regions = world.inaccessible_regions.copy() for player in range(1, world.players + 1): create_regions(ret, player) @@ -503,6 +504,10 @@ def copy_world(world): location.parent_region = copied_region for entrance in region.entrances: ret.get_entrance(entrance.name, entrance.player).connect(copied_region) + for exit in region.exits: + if exit.connected_region: + dest_region = ret.get_region(exit.connected_region.name, region.player) + ret.get_entrance(exit.name, exit.player).connect(dest_region) # fill locations for location in world.get_locations(): @@ -540,14 +545,16 @@ def copy_world(world): copiededge = ret.check_for_owedge(edge.name, edge.player) copiededge.dest = ret.check_for_owedge(edge.dest.name, edge.dest.player) + # everything below this line is changing the original object, seems to be complicated to replicate similar objects organically ret.doors = world.doors for door in ret.doors: - entrance = ret.check_for_entrance(door.name, door.player) - if entrance is not None: - entrance.door = door + copied_entrance = ret.check_for_entrance(door.entrance.name, door.player) + door.entrance = copied_entrance + if copied_entrance: + copied_entrance.door = door + ret.paired_doors = world.paired_doors ret.rooms = world.rooms - ret.inaccessible_regions = world.inaccessible_regions ret.dungeon_layouts = world.dungeon_layouts ret.key_logic = world.key_logic ret.dungeon_portals = world.dungeon_portals From 46786ca4891c6950da1df26df144031e5af6e4b7 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 1 Nov 2022 20:16:01 -0500 Subject: [PATCH 17/21] Renamed Tile Swap to Tile Flip --- BaseClasses.py | 6 +++--- EntranceShuffle.py | 2 +- OverworldGlitchRules.py | 4 ++-- OverworldShuffle.py | 22 +++++++++++----------- README.md | 24 ++++++++++++------------ resources/app/cli/lang/en.json | 4 ++-- resources/app/gui/lang/en.json | 2 +- 7 files changed, 32 insertions(+), 32 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 9195b60e..f59086dd 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -3020,7 +3020,7 @@ class Spoiler(object): outfile.write('Crossed OW:'.ljust(line_width) + '%s\n' % self.metadata['ow_crossed'][player]) if self.metadata['ow_shuffle'][player] != 'vanilla' or self.metadata['ow_crossed'][player] != 'none': outfile.write('Keep Similar OW Edges Together:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_keepsimilar'][player])) - outfile.write('Swapped OW (Mixed):'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_mixed'][player])) + outfile.write('OW Tile Flip (Mixed):'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_mixed'][player])) outfile.write('Whirlpool Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_whirlpool'][player])) outfile.write('Flute Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_fluteshuffle'][player]) outfile.write('Bonk Drops:'.ljust(line_width) + '%s\n' % yn(self.metadata['bonk_drops'][player])) @@ -3112,10 +3112,10 @@ class Spoiler(object): outfile.write(str('(Player ' + str(player) + ')\n')) # player name outfile.write(self.maps[('flute', player)]['text'] + '\n\n') - # overworld tile swaps + # overworld tile flips for player in range(1, self.world.players + 1): if ('swaps', player) in self.maps: - outfile.write('OW Tile Swaps:\n') + outfile.write('OW Tile Flips:\n') break for player in range(1, self.world.players + 1): if ('swaps', player) in self.maps: diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 86121e74..d2e493d6 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -182,7 +182,7 @@ def link_entrances(world, player): if 0x03 in world.owswaps[player][0] == 0x05 in world.owswaps[player][0]: # if WDM and EDM are in same world connect_caves(world, lw_wdm_entrances + lw_edm_entrances, [], caves, player) else: - # place Old Man House in WDM if not swapped + # place Old Man House in WDM if not flipped if not world.is_tile_swapped(0x03, player): connect_caves(world, lw_wdm_entrances, [], list(Old_Man_House), player) else: diff --git a/OverworldGlitchRules.py b/OverworldGlitchRules.py index 58f49b8f..29986c80 100644 --- a/OverworldGlitchRules.py +++ b/OverworldGlitchRules.py @@ -334,7 +334,7 @@ def set_owg_rules(player, world, connections, default_rule): glitch_regions = (['Central Cliffs', 'Eastern Cliff', 'Desert Northeast Cliffs'], ['Dark Central Cliffs', 'Darkness Cliff', 'Mire Northeast Cliffs']) -# same screen clips, no OWR tile swap implications +# same screen clips, no Tile Flip OWR implications boots_clips_local = [ # (name, from_region, to_region) ('Hera Ascent Clip', 'West Death Mountain (Bottom)', 'West Death Mountain (Top)'), #cannot guarantee camera correction, but a bomb clip exists ('WDDM Bomb Clip', 'West Dark Death Mountain (Bottom)', 'West Dark Death Mountain (Top)'), #cannot guarantee camera correction, but a bomb clip exists @@ -386,7 +386,7 @@ boots_clips_local = [ # (name, from_region, to_region) # Common structure for cross-screen connections: # (name, from_region, to_region) <- each three consists of [LW, DW] -# This is so OWR Tile Swap can properly connect both connections, and simultaneously be aware of which one requires pearl +# This is so Tile Flip OWR can properly connect both connections, and simultaneously be aware of which one requires pearl # Note: Some clips have no way to reach the OOB area, and others have no way to get from the OOB area # to a proper destination, these are marked with 'None'; these connections will not be made boots_clips = [ diff --git a/OverworldShuffle.py b/OverworldShuffle.py index aa877f5c..a6fd48d3 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -73,7 +73,7 @@ def link_overworld(world, player): raise Exception('Cannot move a parallel edge without the other') new_mode = OpenStd.Open if tuple((OpenStd.Open, WorldType((int(wrld) + 1) % 2), dir, terrain, parallel, count)) not in new_groups.keys(): - # when Links House tile is swapped, the DW edges need to get put into existing Standard group + # when Links House tile is flipped, the DW edges need to get put into existing Standard group new_mode = OpenStd.Standard new_groups[(new_mode, WorldType((int(wrld) + 1) % 2), dir, terrain, parallel, count)][0].append(forward_set) new_groups[(new_mode, WorldType((int(wrld) + 1) % 2), dir, terrain, parallel, count)][1].append(back_set) @@ -103,7 +103,7 @@ def link_overworld(world, player): else: raise NotImplementedError('Cannot move one side of a non-parallel connection') else: - raise NotImplementedError('Invalid OW Edge swap scenario') + raise NotImplementedError('Invalid OW Edge flip scenario') return new_groups tile_groups = define_tile_groups(world, player, False) @@ -143,7 +143,7 @@ def link_overworld(world, player): trimmed_groups = reorganize_groups(world, trimmed_groups, player) # tile shuffle - logging.getLogger('').debug('Swapping overworld tiles') + logging.getLogger('').debug('Flipping overworld tiles') if world.owMixed[player]: swapped_edges = shuffle_tiles(world, tile_groups, world.owswaps[player], False, player) @@ -199,8 +199,8 @@ def link_overworld(world, player): if world.owCrossed[player] in ['grouped', 'limited'] or (world.owShuffle[player] == 'vanilla' and world.owCrossed[player] == 'chaos'): if world.owCrossed[player] == 'grouped': - # the idea is to XOR the new swaps with the ones from Mixed so that non-parallel edges still work - # Polar corresponds to Grouped with no swaps in ow_crossed_tiles_mask + # the idea is to XOR the new flips with the ones from Mixed so that non-parallel edges still work + # Polar corresponds to Grouped with no flips in ow_crossed_tiles_mask ow_crossed_tiles_mask = [[],[],[]] crossed_edges = shuffle_tiles(world, define_tile_groups(world, player, True), ow_crossed_tiles_mask, True, player) ow_crossed_tiles = [i for i in range(0x82) if (i in world.owswaps[player][0]) != (i in ow_crossed_tiles_mask[0])] @@ -252,7 +252,7 @@ def link_overworld(world, player): elif edge in parallel_links_new.inverse: crossed_edges.append(parallel_links_new.inverse[edge][0]) - # after tile swap and crossed, determine edges that need to swap + # after tile flip and crossed, determine edges that need to flip edges_to_swap = [e for e in swapped_edges+crossed_edges if (e not in swapped_edges) or (e not in crossed_edges)] # whirlpool shuffle @@ -307,9 +307,9 @@ def link_overworld(world, player): logging.getLogger('').debug('Shuffling overworld layout') if world.owShuffle[player] == 'vanilla': - # apply outstanding swaps + # apply outstanding flips trimmed_groups = performSwap(trimmed_groups, edges_to_swap) - assert len(edges_to_swap) == 0, 'Not all edges were swapped successfully: ' + ', '.join(edges_to_swap) + assert len(edges_to_swap) == 0, 'Not all edges were flipped successfully: ' + ', '.join(edges_to_swap) # vanilla transitions groups = list(trimmed_groups.values()) @@ -619,8 +619,8 @@ def shuffle_tiles(world, groups, result_list, do_grouped, player): attempts = 1000 while True: - if attempts == 0: # expected to only occur with custom swaps - raise GenerationException('Could not find valid tile swaps') + if attempts == 0: # expected to only occur with custom flips + raise GenerationException('Could not find valid tile flips') # tile shuffle happens here removed = list() @@ -702,7 +702,7 @@ def define_tile_groups(world, player, do_grouped): if world.mode[player] == 'standard' and (0x1b in group or 0x2b in group or 0x2c in group): return False - # sanctuary/chapel should not be swapped if S+Q guaranteed to output on that screen + # sanctuary/chapel should not be flipped if S+Q guaranteed to output on that screen if 0x13 in group and ((world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull'] \ and (world.mode[player] in ['standard', 'inverted'] or world.doorShuffle[player] != 'crossed' or world.intensity[player] < 3)) \ or (world.shuffle[player] in ['lite', 'lean'] and world.mode[player] == 'inverted')): diff --git a/README.md b/README.md index 36abf9da..bf39ece6 100644 --- a/README.md +++ b/README.md @@ -55,18 +55,18 @@ This is a common sentiment among those who are unfamiliar with OWR's offerings. OWR definitely has a lot of options, and all of them by themselves are pretty simple to grasp, but combining multiple OWR options together increases the complexity and confusion in exponential fashion. Now, of course, some OWR options like Flute Shuffle can safely be combined at any level and isn't gonna make anything more complicated. But specifically, avoid combining these 3 options, at least when going for your first seed: - OW Layout Shuffle -- OW Tile Swap (Mixed) +- OW Tile Flip (Mixed) - Crossed OW ## "Any recommendations for a first-timer?" For a first (and second) seed... *and I say "second" because I feel like both of these recommendations I'm about to make have VERY different vibes, have different levels of challenge, but are both, of their own right, worthy of being tried at least once.* Your first OWR experience can be combined with any mode combination that you are already familiar with and have a lot of experience in playing. If you like Crosskeys and feel very comfortable running that, feel free to turn on all those settings in addition to ONE of these two options: -1. `OW Tile Swap (Mixed)` - Overly, a pretty easy-breezy mode, it doesn't require too much big brain, and is pretty managable even without proper logic tracking, as long as you at least have a standard map tracker. This is actually my favorite way to run OWR today +1. `OW Tile Flip (Mixed)` - Overly, a pretty easy-breezy mode, it doesn't require too much big brain, and is pretty managable even without proper logic tracking, as long as you at least have a standard map tracker. This is actually my favorite way to run OWR today - DO NOT turn on Layout or Whirlpool Shuffle, leave this on `Vanilla` - DO NOT turn on Crossed OWR - `Flute Shuffle` or `Bonk Drops` could be enabled if desired, altho I'd recommend against it, at least for a fresh viewpoint of Mixed OWR -2. `OW Layout Shuffle` - Set to `Parallel`. This is the original spirit and vision of OWR from the time of its own founding. It's definitely much more complicated to run than OW Tile Swap, so keep that in mind. +2. `OW Layout Shuffle` - Set to `Parallel`. This is the original spirit and vision of OWR from the time of its own founding. It's definitely much more complicated to run than OW Tile Flip, so keep that in mind. - `Starting Boots` - Either actual boots or pseudoboots, you will be spending a lot of time navigating the OW, so it's best to do it with the ability to run fast. - - DO NOT turn on OW Tile Swap (Mixed) + - DO NOT turn on OW Tile Flip (Mixed) - DO NOT turn on Crossed OWR - Enable `Whirlpool Shuffle` - Recommended to always be enabled with Layout Shuffle - Enable `Keep Similar Edges Together` - This just helps keep some of your sanity for a first experience @@ -134,7 +134,7 @@ Note: These changes do impact the logic. If you use `CodeTracker`, these Inverte Only settings specifically added by this Overworld Shuffle fork are found here. All door and entrance randomizer settings are supported. See their [readme](https://github.com/Aerinon/ALttPDoorRandomizer/blob/master/README.md) ## Overworld Layout Shuffle (--ow_shuffle) -OW Edge Transitions are shuffled to create new world layouts. A brief visual representation of this can be viewed [here](https://media.discordapp.net/attachments/783989090017738753/857299555183362078/ow-modes.gif). (This graphic also includes combinations of Crossed and Tile Swap) +OW Edge Transitions are shuffled to create new world layouts. A brief visual representation of this can be viewed [here](https://media.discordapp.net/attachments/783989090017738753/857299555183362078/ow-modes.gif). (This graphic also includes combinations of Crossed and Tile Flip) ### Vanilla @@ -168,11 +168,11 @@ Transitions will remain same-world. ### Grouped -This option shuffles connections cross-world in the same manner as Tile Swap/Mixed, the connections coming in and going out of a Tile Group (see `Terminology` section above) are crossed (ie. meaning it is impossible to take a different path to a tile and end up in the opposite world, unlike Limited and Chaos). This is considered the simplest way to play Crossed OWR. +This option shuffles connections cross-world in the same manner as Tile Flip (Mixed), the connections coming in and going out of a Tile Group (see `Terminology` section above) are crossed (ie. meaning it is impossible to take a different path to a tile and end up in the opposite world, unlike Limited and Chaos). This is considered the simplest way to play Crossed OWR. ### Polar -Only effective if Mixed/Tile Swap is enabled. Polar follows the same principle as Grouped, except that it preserves the original/vanilla connections even when tiles are swapped/mixed. This results in a completely vanilla overworld, except that some tiles will transform Link to a Bunny. Even though these tiles give the appearance of your normal LW tile, due to how Mixed/Tile Swap works, those LW tiles give DW properties (such as bunnying, ability to mirror, and prevents flute usage). This offers an interesting twist on Mixed where you have a pre-conditioned knowledge of the terrain you will encounter, but not necessarily be able to do what you need to do there, due to bunny state. (see `Tile Swap/Mixed` section for more details) +Only effective if Tile Flip (Mixed) is enabled. Polar follows the same principle as Grouped, except that it preserves the original/vanilla connections even when tiles are flipped/mixed. This results in a completely vanilla overworld, except that some tiles will transform Link to a Bunny. Even though these tiles give the appearance of your normal LW tile, due to how Tile Flip works, those LW tiles give DW properties (such as bunnying, ability to mirror, and prevents flute usage). This offers an interesting twist on Mixed where you have a pre-conditioned knowledge of the terrain you will encounter, but not necessarily be able to do what you need to do there, due to bunny state. (see `Tile Flip / Mixed` section for more details) ### Limited @@ -194,17 +194,17 @@ This keeps similar edge transitions together. ie. The 2 west land edges of Potio Note: This affects OW Layout Shuffle mostly, but also affects Limited and Chaos modes in Crossed OW. -## Tile Swap / Mixed Overworld (--ow_mixed) +## Tile Flip / Mixed Overworld (--ow_mixed) -Tile Swap (often referred to as Mixed OWR) can be thought of as a hybrid of Open and Inverted, where OW tiles are randomly chosen to be swapped to become a part of the opposite world. When this occurs, that tile will use the Inverted version of that tile. For instance, if the Cave 45 tile becomes swapped, that means while walking around in the LW, you will find the screen that's south of Stumpy instead, and Cave 45 will instead be found in the DW; but like Inverted, the Cave 45 tile is modified to not have a ledge, this ensures that it will be possible to access it. +Tile Flip (often referred to as Mixed OWR) can be thought of as a hybrid of Open and Inverted, where OW tiles are randomly chosen to be flipped to become a part of the opposite world. When this occurs, that tile will use the Inverted version of that tile. For instance, if the Cave 45 tile becomes flipped, that means while walking around in the LW, you will find the screen that's south of Stumpy instead, and Cave 45 will instead be found in the DW; but like Inverted, the Cave 45 tile is modified to not have a ledge, this ensures that it will be possible to access it. Being that this uses concepts from Inverted, it will be important to review the OWR-exclusive changes that have been made to Inverted (often referred to as Inverted 2.0). See `Inverted Changes` for more details. During gameplay: - When on the OW, there will be an L or D in the upper left corner, indicating which world you are currently in. Mirroring still works the same, you must be in the DW to mirror to the LW. - - When doing a map check (pressing X while on the OW), the tiles shown will reflect the swapped tiles. This means that dungeon prizes will show the prizes for the dungeons that are now part of that world, beware of Desert/Mire and Eastern/PoD. Here is an image showing the difference of appearance when tiles are swapped on the [map check](https://media.discordapp.net/attachments/783989090017738753/970646558049714196/lttp-lw-mapcheck.gif) screen. + - When doing a map check (pressing X while on the OW), the tiles shown will reflect the flipped tiles. This means that dungeon prizes will show the prizes for the dungeons that are now part of that world, beware of Desert/Mire and Eastern/PoD. Here is an image showing the difference of appearance when tiles are flipped on the [map check](https://media.discordapp.net/attachments/783989090017738753/970646558049714196/lttp-lw-mapcheck.gif) screen. -Note: Tiles are put into Tile Groups (see `Terminology`) that must be shuffled together when certain settings are enabled. For instance, if ER is disabled, then any tiles that have a connector cave that leads to a different tile, then those tiles must swap together. +Note: Tiles are put into Tile Groups (see `Terminology`) that must be shuffled together when certain settings are enabled. For instance, if ER is disabled, then any tiles that have a connector cave that leads to a different tile, then those tiles must flip together. ## Whirlpool Shuffle (--ow_whirlpool) @@ -350,7 +350,7 @@ This keeps similar edge transitions paired together with other pairs of transiti --ow_mixed ``` -This gives each OW tile a random chance to be swapped to the opposite world +This gives each OW tile a random chance to be flipped to the opposite world ``` --ow_fluteshuffle diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 5869f144..afc77cba 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -221,8 +221,8 @@ "None: No transitions are cross-world connections.", "Grouped: This ensures a two-plane separation so that you cannot", " walk around and access the other plane version by walking.", - "Polar: Only used when Mixed is enabled. This retains original", - " connections even when overworld tiles are swapped.", + "Polar: Only used when Tile Flip is enabled. This retains original", + " connections even when overworld tiles are flipped.", "Limited: Exactly nine transitions are randomly chosen as", " cross-world connections (to emulate the nine portals).", "Chaos: Every transition has a 50/50 chance to become a", diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index 9a92eb81..bcfba383 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -142,7 +142,7 @@ "randomizer.overworld.keepsimilar": "Keep Similar Edges Together", - "randomizer.overworld.mixed": "Tile Swap (Mixed)", + "randomizer.overworld.mixed": "Tile Flip (Mixed)", "randomizer.overworld.whirlpool": "Whirlpool Shuffle", From 2e72a306e733e770984e41c517d211c306b0656d Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 1 Nov 2022 20:39:45 -0500 Subject: [PATCH 18/21] Updated Readme --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index bf39ece6..34d195ce 100644 --- a/README.md +++ b/README.md @@ -206,6 +206,21 @@ During gameplay: Note: Tiles are put into Tile Groups (see `Terminology`) that must be shuffled together when certain settings are enabled. For instance, if ER is disabled, then any tiles that have a connector cave that leads to a different tile, then those tiles must flip together. +## Tile Flip vs Crossed Explained +The above OWR options are very difficult to describe. The above descriptions are written in a way that is most correct even when these options are combined with more complicated modes. But, this section aims to simplify the explanation by assuming the user chooses a normal 'Open 7/7 Defeat Ganon' seed but with just one OWR setting enabled. + +Both of these options are very similar and often confused from each other. +- Tile Flip is a mode where some DW tiles are moved and BECOME part of the LW (and the LW counterparts become part of the DW). What does it mean to "become" part of a world? It means that it will inherit (NOT bring over) the properties of that world it is moving to (such as being able to flute, ability to use the mirror, or being susceptible to bunnying). +- Crossed on the other hand doesn't change the properties of tiles, instead it transports Link *physically?* across worlds upon transitioning. This also means that Link can be transformed into a bunny moving from tile to tile. +tldr: Tile Flip moves the tiles, Crossed moves Link + +So, let's run an example of 2 tiles, Link's House and the screen to the right of it. Transitioning right from Link's House: In vanilla, you get the Stone Bridge screen and Link stays his normal self and is just normal LW behavior. Now, let's assume Links House screen stays vanilla, but the tile to the right is getting Flipped or Crossed. +- In Tile Flip, you'd get the Hammer Bridge screen and Link would stay as Link and you'd be able to flute away from this screen if you had a flute. +- In Crossed, you'd get the same Hammer Bridge Screen, but this time Link would be transformed into a bunny, just like he'd normally be when on that tile. +- In Polar Crossed (when both Mixed and Crossed effects are applied together), you get the normal Stone Bridge screen, but Link is transformed to a bunny (because the Stone Bridge screen has moved to the DW AND Link is also moving across worlds). + +As you can see, things get pretty complicated when mixing modes together. Doing this can definitely create a very unique and interesting experience, but one that is very hard to grasp. And then beyond that there is OW Layout Shuffle, which is where transition destinations are shuffled, so Link will get transported to a different tile entirely, but the same rules apply when you eventually find the Stone/Hammer Bridge screen, you just likely won't find that screen thru a transition on Link's House screen. + ## Whirlpool Shuffle (--ow_whirlpool) When enabled, the whirlpool connections are shuffled. If Crossed OW is enabled, the whirlpools can also be cross-world as well. For Limited Crossed OW, this doesn't count towards the limited number of crossed edge transitions. From df897c53ff55d23d3e58f897165d990c64a953e4 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 3 Nov 2022 12:35:06 -0500 Subject: [PATCH 19/21] Bandaid fix for universal key error --- BaseClasses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BaseClasses.py b/BaseClasses.py index f59086dd..61cede10 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -857,7 +857,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 in state.world.key_logic[player]: 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: From 254df9703bf7ff0f59da28b017e4cd826fa6cb70 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 3 Nov 2022 13:24:19 -0500 Subject: [PATCH 20/21] Improved copy_world_premature --- Main.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Main.py b/Main.py index 6deb977e..386ddf6e 100644 --- a/Main.py +++ b/Main.py @@ -688,11 +688,10 @@ def copy_world_premature(world, player): for door in world.doors: if door.player == player: - copied_door = ret.check_for_door(door.name, door.player) copied_entrance = ret.check_for_entrance(door.entrance.name, door.player) - copied_door.entrance = copied_entrance - copied_entrance.door = copied_door - + door.entrance = copied_entrance + if copied_entrance: + copied_entrance.door = door for player, portals in world.dungeon_portals.items(): for portal in portals: connect_portal(portal, ret, player) From a901234d9892b7cf2d9aea0d525a090a538b7414 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 3 Nov 2022 13:47:32 -0500 Subject: [PATCH 21/21] Version bump 0.2.11.1 --- CHANGELOG.md | 16 ++++++++++++++++ OverworldShuffle.py | 2 +- README.md | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0a059de..211f24a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## 0.2.11.1 +- Renamed mode: Tile Swap (Mixed) is now called Tile Flip (Mixed) +- Fixed generation errors due to issue with new Farmable item locations + +## 0.2.11.0 +- New OWR mode option: Free Terrain + - When used with OW Layout Shuffle, land and water transitions are combined into one pool and shuffled, this means land transitions can lead to water and vice versa. There is already tracker support for this change on DunkaTracker. Thanks @Catobat for the work on all of this. +- Glitched modes now have correct fake world behavior in all modes, including Inverted and even Mixed OWR +- Glitched + Mixed OWR now has correct logic (previously it was completely unimplemented) +- Lite ER is back and working! +- There was an issue with ER resulting in regions being inaccessible, this has been fixed. +- Changed Crossworld ER modes so that DW inaccessible areas are resolved before considering LW inaccessible areas, to give the algo a chance to make some of the LW areas accessible thru the DW +- Added new Bomb/Rupee farm logic, which uses pseudo-items, simplifies the graph searching and they even show up in the Playthru Calc (shows a logical path to Farmable Bombs if you ever question how you are able to get early bombs when the opening area is limited) +- Fixed issue with grabbing an item near Murahduhla and freezing the game +- Various logic corrections, including the DR Bumper Cave fix for pottery logic + ## 0.2.10.1 - Merged DR v1.0.1.3 - Fixed Zelda despawn in TT Prison diff --git a/OverworldShuffle.py b/OverworldShuffle.py index a6fd48d3..a2254434 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.2.11.0' +version_number = '0.2.11.1' # branch indicator is intentionally different across branches version_branch = '-u' diff --git a/README.md b/README.md index 34d195ce..93ad893e 100644 --- a/README.md +++ b/README.md @@ -217,7 +217,7 @@ tldr: Tile Flip moves the tiles, Crossed moves Link So, let's run an example of 2 tiles, Link's House and the screen to the right of it. Transitioning right from Link's House: In vanilla, you get the Stone Bridge screen and Link stays his normal self and is just normal LW behavior. Now, let's assume Links House screen stays vanilla, but the tile to the right is getting Flipped or Crossed. - In Tile Flip, you'd get the Hammer Bridge screen and Link would stay as Link and you'd be able to flute away from this screen if you had a flute. - In Crossed, you'd get the same Hammer Bridge Screen, but this time Link would be transformed into a bunny, just like he'd normally be when on that tile. -- In Polar Crossed (when both Mixed and Crossed effects are applied together), you get the normal Stone Bridge screen, but Link is transformed to a bunny (because the Stone Bridge screen has moved to the DW AND Link is also moving across worlds). +- In Polar Crossed (when both Tile Flip and Crossed effects are applied together), you get the normal Stone Bridge screen, but Link is transformed to a bunny (because the Stone Bridge screen has moved to the DW AND Link is also moving across worlds). As you can see, things get pretty complicated when mixing modes together. Doing this can definitely create a very unique and interesting experience, but one that is very hard to grasp. And then beyond that there is OW Layout Shuffle, which is where transition destinations are shuffled, so Link will get transported to a different tile entirely, but the same rules apply when you eventually find the Stone/Hammer Bridge screen, you just likely won't find that screen thru a transition on Link's House screen.