From 195f6c86d2fac620d121c0704b2063ae6846ad95 Mon Sep 17 00:00:00 2001 From: compiling <8335770+compiling@users.noreply.github.com> Date: Sun, 10 May 2020 19:27:13 +1000 Subject: [PATCH 1/2] Replace world exploration with a faster algorithm - use BFS and keep track of all entrances that are currently blocked by progression items --- BaseClasses.py | 51 +++++++++++++++++++++++++--------- EntranceShuffle.py | 40 ++++++++++++++++++++++----- InvertedRegions.py | 3 +- Regions.py | 1 + Rom.py | 3 +- Rules.py | 69 +++++++++++++++++++--------------------------- 6 files changed, 104 insertions(+), 63 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index af34ef1a..421fbf88 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -4,9 +4,9 @@ import copy from enum import Enum, unique import logging import json -from collections import OrderedDict, Counter +from collections import OrderedDict, Counter, deque -from EntranceShuffle import door_addresses +from EntranceShuffle import door_addresses, indirect_connections from Utils import int16_as_bytes from typing import Union @@ -396,6 +396,7 @@ class CollectionState(object): self.prog_items = Counter() self.world = parent self.reachable_regions = {player: set() for player in range(1, parent.players + 1)} + self.blocked_connections = {player: set() for player in range(1, parent.players + 1)} self.events = [] self.path = {} self.locations_checked = set() @@ -404,24 +405,46 @@ class CollectionState(object): self.collect(item, True) def update_reachable_regions(self, player: int): - player_regions = self.world.get_regions(player) self.stale[player] = False rrp = self.reachable_regions[player] - new_regions = True - reachable_regions_count = len(rrp) - while new_regions: - player_regions = [region for region in player_regions if region not in rrp] - for candidate in player_regions: - if candidate.can_reach_private(self): - rrp.add(candidate) - new_regions = len(rrp) > reachable_regions_count - reachable_regions_count = len(rrp) + bc = self.blocked_connections[player] + queue = deque(self.blocked_connections[player]) + start = self.world.get_region('Menu', player) + + # init on first call - this can't be done on construction since the regions don't exist yet + if not start in rrp: + rrp.add(start) + bc.update(start.exits) + queue.extend(start.exits) + + # run BFS on all connections, and keep track of those blocked by missing items + while True: + try: + connection = queue.popleft() + new_region = connection.connected_region + if new_region in rrp: + bc.remove(connection) + elif connection.can_reach(self): + rrp.add(new_region) + bc.remove(connection) + bc.update(new_region.exits) + queue.extend(new_region.exits) + self.path[new_region] = (new_region.name, self.path.get(connection, None)) + + # Retry connections if the new region can unblock them + if new_region.name in indirect_connections: + new_entrance = self.world.get_entrance(indirect_connections[new_region.name], player) + if new_entrance in bc and new_entrance not in queue: + queue.append(new_entrance) + except IndexError: + break def copy(self) -> CollectionState: ret = CollectionState(self.world) ret.prog_items = self.prog_items.copy() ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in range(1, self.world.players + 1)} + ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in range(1, self.world.players + 1)} ret.events = copy.copy(self.events) ret.path = copy.copy(self.path) ret.locations_checked = copy.copy(self.locations_checked) @@ -737,6 +760,7 @@ class CollectionState(object): del (self.prog_items[to_remove, item.player]) # invalidate caches, nothing can be trusted anymore now self.reachable_regions[item.player] = set() + self.blocked_connections[item.player] = set() self.stale[item.player] = True @unique @@ -814,10 +838,11 @@ class Entrance(object): self.vanilla = None self.access_rule = lambda state: True self.player = player + self.hide_path = False def can_reach(self, state): if self.parent_region.can_reach(state) and self.access_rule(state): - if not self in state.path: + if not self.hide_path and not self in state.path: state.path[self] = (self.name, state.path.get(self.parent_region, (self.parent_region.name, None))) return True diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 6d452d2a..0b5d3b56 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -1198,7 +1198,9 @@ def link_inverted_entrances(world, player): sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in bomb_shop_doors] sanc_door = random.choice(sanc_doors) bomb_shop_doors.remove(sanc_door) - connect_doors(world, [sanc_door], ['Inverted Dark Sanctuary'], player) + + connect_entrance(world, sanc_door, 'Inverted Dark Sanctuary', player) + world.get_entrance('Inverted Dark Sanctuary Exit', player).connect(world.get_entrance(sanc_door, player).parent_region) lw_dm_entrances = ['Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'Paradox Cave (Top)', 'Old Man House (Bottom)', 'Fairy Ascension Cave (Bottom)', 'Fairy Ascension Cave (Top)', 'Spiral Cave (Bottom)', 'Old Man Cave (East)', @@ -1273,7 +1275,8 @@ def link_inverted_entrances(world, player): sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in dw_entrances] sanc_door = random.choice(sanc_doors) dw_entrances.remove(sanc_door) - connect_doors(world, [sanc_door], ['Inverted Dark Sanctuary'], player) + connect_entrance(world, sanc_door, 'Inverted Dark Sanctuary', player) + world.get_entrance('Inverted Dark Sanctuary Exit', player).connect(world.get_entrance(sanc_door, player).parent_region) # tavern back door cannot be shuffled yet connect_doors(world, ['Tavern North'], ['Tavern'], player) @@ -1404,7 +1407,8 @@ def link_inverted_entrances(world, player): sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in dw_entrances] sanc_door = random.choice(sanc_doors) dw_entrances.remove(sanc_door) - connect_doors(world, [sanc_door], ['Inverted Dark Sanctuary'], player) + connect_entrance(world, sanc_door, 'Inverted Dark Sanctuary', player) + world.get_entrance('Inverted Dark Sanctuary Exit', player).connect(world.get_entrance(sanc_door, player).parent_region) # place old man house # no dw must exits in inverted, but we randomize whether cave is in light or dark world @@ -1541,7 +1545,8 @@ def link_inverted_entrances(world, player): sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in entrances] sanc_door = random.choice(sanc_doors) entrances.remove(sanc_door) - connect_doors(world, [sanc_door], ['Inverted Dark Sanctuary'], player) + connect_entrance(world, sanc_door, 'Inverted Dark Sanctuary', player) + world.get_entrance('Inverted Dark Sanctuary Exit', player).connect(world.get_entrance(sanc_door, player).parent_region) # tavern back door cannot be shuffled yet connect_doors(world, ['Tavern North'], ['Tavern'], player) @@ -1674,7 +1679,8 @@ def link_inverted_entrances(world, player): sanc_door = random.choice(sanc_doors) entrances.remove(sanc_door) doors.remove(sanc_door) - connect_doors(world, [sanc_door], ['Inverted Dark Sanctuary'], player) + connect_entrance(world, sanc_door, 'Inverted Dark Sanctuary', player) + world.get_entrance('Inverted Dark Sanctuary Exit', player).connect(world.get_entrance(sanc_door, player).parent_region) # now let's deal with mandatory reachable stuff def extract_reachable_exit(cavelist): @@ -2810,7 +2816,10 @@ Isolated_LH_Doors = ['Kings Grave', 'Turtle Rock Isolated Ledge Entrance'] # these are connections that cannot be shuffled and always exist. They link together separate parts of the world we need to divide into regions -mandatory_connections = [('Lake Hylia Central Island Pier', 'Lake Hylia Central Island'), +mandatory_connections = [('Links House S&Q', 'Links House'), + ('Sanctuary S&Q', 'Sanctuary'), + ('Old Man S&Q', 'Old Man House'), + ('Lake Hylia Central Island Pier', 'Lake Hylia Central Island'), ('Lake Hylia Central Island Teleporter', 'Dark Lake Hylia Central Island'), ('Zoras River', 'Zoras River'), ('Kings Grave Outer Rocks', 'Kings Grave Area'), @@ -2991,7 +3000,11 @@ mandatory_connections = [('Lake Hylia Central Island Pier', 'Lake Hylia Central ('Pyramid Drop', 'East Dark World') ] -inverted_mandatory_connections = [('Lake Hylia Central Island Pier', 'Lake Hylia Central Island'), +inverted_mandatory_connections = [('Links House S&Q', 'Inverted Links House'), + ('Dark Sanctuary S&Q', 'Inverted Dark Sanctuary'), + ('Old Man S&Q', 'Old Man House'), + ('Castle Ledge S&Q', 'Hyrule Castle Ledge'), + ('Lake Hylia Central Island Pier', 'Lake Hylia Central Island'), ('Lake Hylia Island Pier', 'Lake Hylia Island'), ('Lake Hylia Warp', 'Northeast Light World'), ('Northeast Light World Warp', 'Light World'), @@ -3495,6 +3508,7 @@ inverted_default_connections = [('Waterfall of Wishing', 'Waterfall of Wishing' ('Inverted Links House Exit', 'South Dark World'), ('Inverted Big Bomb Shop', 'Inverted Big Bomb Shop'), ('Inverted Dark Sanctuary', 'Inverted Dark Sanctuary'), + ('Inverted Dark Sanctuary Exit', 'West Dark World'), ('Old Man Cave (West)', 'Bumper Cave'), ('Old Man Cave (East)', 'Death Mountain Return Cave'), ('Old Man Cave Exit (West)', 'West Dark World'), @@ -3626,6 +3640,18 @@ inverted_default_dungeon_connections = [('Desert Palace Entrance (South)', 'Dese ('Turtle Rock Exit (Front)', 'Dark Death Mountain'), ('Ice Palace Exit', 'Dark Lake Hylia')] +# Regions that can be required to access entrances through rules, not paths +indirect_connections = { + 'Turtle Rock (Top)': 'Turtle Rock', + 'East Dark World': 'Pyramid Fairy', + 'Big Bomb Shop': 'Pyramid Fairy', + 'Dark Desert': 'Pyramid Fairy', + 'West Dark World': 'Pyramid Fairy', + 'South Dark World': 'Pyramid Fairy', + 'Light World': 'Pyramid Fairy', + 'Old Man Cave': 'Old Man S&Q' +} + # format: # Key=Name # addr = (door_index, exitdata) # multiexit diff --git a/InvertedRegions.py b/InvertedRegions.py index e0f61fe0..0879b9e7 100644 --- a/InvertedRegions.py +++ b/InvertedRegions.py @@ -6,6 +6,7 @@ from Regions import create_lw_region, create_dw_region, create_cave_region, crea def create_inverted_regions(world, player): world.regions += [ + create_dw_region(player, 'Menu', None, ['Links House S&Q', 'Dark Sanctuary S&Q', 'Old Man S&Q', 'Castle Ledge S&Q']), create_lw_region(player, 'Light World', ['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure', 'Purple Chest', 'Bombos Tablet'], ["Blinds Hideout", "Hyrule Castle Secret Entrance Drop", 'Kings Grave Outer Rocks', 'Dam', 'Inverted Big Bomb Shop', 'Tavern North', 'Chicken House', 'Aginahs Cave', 'Sahasrahlas Hut', 'Kakariko Well Drop', 'Kakariko Well Cave', @@ -197,7 +198,7 @@ def create_inverted_regions(world, player): create_cave_region(player, 'C-Shaped House', 'a house with a chest', ['C-Shaped House']), create_cave_region(player, 'Chest Game', 'a game of 16 chests', ['Chest Game']), create_cave_region(player, 'Red Shield Shop', 'the rare shop'), - create_cave_region(player, 'Inverted Dark Sanctuary', 'a storyteller'), + create_cave_region(player, 'Inverted Dark Sanctuary', 'a storyteller', None, ['Inverted Dark Sanctuary Exit']), create_cave_region(player, 'Bumper Cave', 'a connector', None, ['Bumper Cave Exit (Bottom)', 'Bumper Cave Exit (Top)']), create_dw_region(player, 'Skull Woods Forest', None, ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', 'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)']), diff --git a/Regions.py b/Regions.py index d79d6ec8..d98d84b5 100644 --- a/Regions.py +++ b/Regions.py @@ -5,6 +5,7 @@ from BaseClasses import Region, Location, Entrance, RegionType, Shop, ShopType def create_regions(world, player): world.regions += [ + create_lw_region(player, 'Menu', None, ['Links House S&Q', 'Sanctuary S&Q', 'Old Man S&Q']), create_lw_region(player, 'Light World', ['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure', 'Purple Chest'], ["Blinds Hideout", "Hyrule Castle Secret Entrance Drop", 'Zoras River', 'Kings Grave Outer Rocks', 'Dam', 'Links House', 'Tavern North', 'Chicken House', 'Aginahs Cave', 'Sahasrahlas Hut', 'Kakariko Well Drop', 'Kakariko Well Cave', diff --git a/Rom.py b/Rom.py index 9339325d..cb8505c2 100644 --- a/Rom.py +++ b/Rom.py @@ -2054,7 +2054,8 @@ def set_inverted_mode(world, player, rom): rom.write_bytes(snes_to_pc(0x06B2AB), [0xF0, 0xE1, 0x05]) def patch_shuffled_dark_sanc(world, rom, player): - dark_sanc_entrance = str(world.get_region('Inverted Dark Sanctuary', player).entrances[0].name) + dark_sanc = world.get_region('Inverted Dark Sanctuary', player) + dark_sanc_entrance = str([i for i in dark_sanc.entrances if i.parent_region.name != 'Menu'][0].name) room_id, ow_area, vram_loc, scroll_y, scroll_x, link_y, link_x, camera_y, camera_x, unknown_1, unknown_2, door_1, door_2 = door_addresses[dark_sanc_entrance][1] door_index = door_addresses[str(dark_sanc_entrance)][0] diff --git a/Rules.py b/Rules.py index eb5869d7..6e28807c 100644 --- a/Rules.py +++ b/Rules.py @@ -1,7 +1,7 @@ import collections import logging import OverworldGlitchRules -from BaseClasses import RegionType, World +from BaseClasses import RegionType, World, Entrance from Items import ItemFactory from OverworldGlitchRules import overworld_glitches_rules @@ -10,20 +10,10 @@ def set_rules(world, player): if world.logic[player] == 'nologic': logging.getLogger('').info('WARNING! Seeds generated under this logic often require major glitches and may be impossible!') - if world.mode[player] != 'inverted': - world.get_region('Links House', player).can_reach_private = lambda state: True - world.get_region('Sanctuary', player).can_reach_private = lambda state: True - old_rule = world.get_region('Old Man House', player).can_reach - world.get_region('Old Man House', player).can_reach_private = lambda state: state.can_reach('Old Man', 'Location', player) or old_rule(state) - return - else: - world.get_region('Inverted Links House', player).can_reach_private = lambda state: True - world.get_region('Inverted Dark Sanctuary', player).entrances[0].parent_region.can_reach_private = lambda state: True - if world.shuffle[player] != 'vanilla': - old_rule = world.get_region('Old Man House', player).can_reach - world.get_region('Old Man House', player).can_reach_private = lambda state: state.can_reach('Old Man', 'Location', player) or old_rule(state) - world.get_region('Hyrule Castle Ledge', player).can_reach_private = lambda state: True - return + world.get_region('Menu', player).can_reach_private = lambda state: True + for exit in world.get_region('Menu', player).exits: + exit.hide_path = True + return global_rules(world, player) if world.mode[player] != 'inverted': @@ -148,9 +138,12 @@ def global_rules(world, player): # ganon can only carry triforce add_item_rule(world.get_location('Ganon', player), lambda item: item.name == 'Triforce' and item.player == player) - # we can s&q to the old man house after we rescue him. This may be somewhere completely different if caves are shuffled! - old_rule = world.get_region('Old Man House', player).can_reach_private - world.get_region('Old Man House', player).can_reach_private = lambda state: state.can_reach('Old Man', 'Location', player) or old_rule(state) + # determines which S&Q locations are available - hide from paths since it isn't an in-game location + world.get_region('Menu', player).can_reach_private = lambda state: True + for exit in world.get_region('Menu', player).exits: + exit.hide_path = True + + set_rule(world.get_entrance('Old Man S&Q', player), lambda state: state.can_reach('Old Man', 'Location', player)) set_rule(world.get_location('Sunken Treasure', player), lambda state: state.has('Open Floodgate', player)) set_rule(world.get_location('Dark Blacksmith Ruins', player), lambda state: state.has('Return Smith', player)) @@ -206,7 +199,7 @@ def global_rules(world, player): # logic patch to prevent placing a crystal in Desert that's required to reach the required keys if not (world.keyshuffle[player] and world.bigkeyshuffle[player]): - add_rule(world.get_location('Desert Palace - Prize', player), lambda state: state.world.get_region('Desert Palace Main (Outer)', 1).can_reach(state)) + add_rule(world.get_location('Desert Palace - Prize', player), lambda state: state.world.get_region('Desert Palace Main (Outer)', player).can_reach(state)) set_rule(world.get_entrance('Tower of Hera Small Key Door', player), lambda state: state.has_key('Small Key (Tower of Hera)', player) or item_name(state, 'Tower of Hera - Big Key Chest', player) == ('Small Key (Tower of Hera)', player)) set_rule(world.get_entrance('Tower of Hera Big Key Door', player), lambda state: state.has('Big Key (Tower of Hera)', player)) @@ -382,15 +375,6 @@ def global_rules(world, player): def default_rules(world, player): - if world.mode[player] == 'standard': - world.get_region('Hyrule Castle Secret Entrance', player).can_reach_private = lambda state: True - old_rule = world.get_region('Links House', player).can_reach_private - world.get_region('Links House', player).can_reach_private = lambda state: state.can_reach('Sanctuary', 'Region', player) or old_rule(state) - else: - # these are default save&quit points and always accessible - world.get_region('Links House', player).can_reach_private = lambda state: True - world.get_region('Sanctuary', player).can_reach_private = lambda state: True - # overworld requirements set_rule(world.get_entrance('Kings Grave', player), lambda state: state.has_Boots(player)) set_rule(world.get_entrance('Kings Grave Outer Rocks', player), lambda state: state.can_lift_heavy_rocks(player)) @@ -501,16 +485,8 @@ def default_rules(world, player): def inverted_rules(world, player): - # s&q regions. link's house entrance is set to true so the filler knows the chest inside can always be reached - world.get_region('Inverted Links House', player).can_reach_private = lambda state: True - world.get_region('Inverted Links House', player).entrances[0].can_reach = lambda state: True - world.get_region('Inverted Dark Sanctuary', player).entrances[0].parent_region.can_reach_private = lambda state: True - - old_rule_old_man = world.get_region('Old Man House', player).can_reach_private - world.get_region('Old Man House', player).can_reach_private = lambda state: state.can_reach('Old Man', 'Location', player) or old_rule_old_man(state) - - old_rule_castle_ledge = world.get_region('Hyrule Castle Ledge', player).can_reach_private - world.get_region('Hyrule Castle Ledge', player).can_reach_private = lambda state: (state.has_Mirror(player) and state.has('Beat Agahnim 1', player) and state.can_reach_light_world(player)) or old_rule_castle_ledge(state) + # s&q regions. + set_rule(world.get_entrance('Castle Ledge S&Q', player), lambda state: state.has_Mirror(player) and state.has('Beat Agahnim 1', player)) # overworld requirements set_rule(world.get_location('Maze Race', player), lambda state: state.has_Pearl(player)) @@ -805,9 +781,20 @@ def swordless_rules(world, player): set_rule(world.get_location('Bombos Tablet', player), lambda state: state.has('Book of Mudora', player) and state.has('Hammer', player)) +def add_connection(parent_name, target_name, entrance_name, world, player): + parent = world.get_region(parent_name, player) + target = world.get_region(target_name, player) + connection = Entrance(player, entrance_name, parent) + parent.exits.append(connection) + connection.connect(target) + def standard_rules(world, player): + add_connection('Menu', 'Hyrule Castle Secret Entrance', 'Uncle S&Q', world, player) + world.get_entrance('Uncle S&Q', player).hide_path = True set_rule(world.get_entrance('Hyrule Castle Exit (East)', player), lambda state: state.can_reach('Sanctuary', 'Region', player)) set_rule(world.get_entrance('Hyrule Castle Exit (West)', player), lambda state: state.can_reach('Sanctuary', 'Region', player)) + set_rule(world.get_entrance('Links House S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player)) + set_rule(world.get_entrance('Sanctuary S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player)) def set_trock_key_rules(world, player): @@ -1033,7 +1020,7 @@ def set_big_bomb_rules(world, player): # the basic routes assume you can reach eastern light world with the bomb. # you can then use the southern teleporter, or (if you have beaten Aga1) the hyrule castle gate warp def basic_routes(state): - return southern_teleporter(state) or state.can_reach('Top of Pyramid', 'Entrance', player) + return southern_teleporter(state) or state.has('Beat Agahnim 1', player) # Key for below abbreviations: # P = pearl @@ -1066,7 +1053,7 @@ def set_big_bomb_rules(world, player): #1. Mirror and enter via gate: Need mirror and Aga1 #2. cross peg bridge: Need hammer and moon pearl # -> CPB or (M and A) - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: cross_peg_bridge(state) or (state.has_Mirror(player) and state.can_reach('Top of Pyramid', 'Entrance', player))) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: cross_peg_bridge(state) or (state.has_Mirror(player) and state.has('Beat Agahnim 1', player))) elif bombshop_entrance.name in Isolated_DW_entrances: # 1. mirror then flute then basic routes # -> M and Flute and BR @@ -1335,7 +1322,7 @@ def set_bunny_rules(world: World, player: int, inverted: bool): def path_to_access_rule(path, entrance): - return lambda state: state.can_reach(entrance) and all(rule(state) for rule in path) + return lambda state: state.can_reach(entrance.name, 'Entrance', entrance.player) and all(rule(state) for rule in path) def options_to_access_rule(options): return lambda state: any(rule(state) for rule in options) From d6dc559ed6c79f79ce6bd11c6998ccf6a8e3109a Mon Sep 17 00:00:00 2001 From: compiling <8335770+compiling@users.noreply.github.com> Date: Sun, 10 May 2020 19:54:40 +1000 Subject: [PATCH 2/2] Update dungeon tests to work with the new exploration algorithm --- test/dungeons/TestDungeon.py | 16 +++++++++++++--- test/dungeons/TestSkullWoods.py | 3 +++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/test/dungeons/TestDungeon.py b/test/dungeons/TestDungeon.py index 9caf64e4..1457abce 100644 --- a/test/dungeons/TestDungeon.py +++ b/test/dungeons/TestDungeon.py @@ -13,7 +13,8 @@ class TestDungeon(unittest.TestCase): def setUp(self): self.world = World(1, {1:'vanilla'}, {1:'noglitches'}, {1:'open'}, {1:'random'}, {1:'normal'}, {1:'normal'}, {1:False}, {1:'on'}, {1:'ganon'}, 'balanced', {1:'items'}, True, {1:False}, False, None, {1:False}) - self.starting_regions = [] + self.starting_regions = [] # Where to start exploring + self.remove_exits = [] # Block dungeon exits self.world.difficulty_requirements[1] = difficulties['normal'] create_regions(self.world, 1) create_dungeons(self.world, 1) @@ -21,6 +22,7 @@ class TestDungeon(unittest.TestCase): for exitname, regionname in mandatory_connections: connect_simple(self.world, exitname, regionname, 1) connect_simple(self.world, 'Big Bomb Shop', 'Big Bomb Shop', 1) + self.world.get_region('Menu', 1).exits = [] self.world.swamp_patch_required[1] = True set_rules(self.world, 1) generate_itempool(self.world, 1) @@ -28,8 +30,8 @@ class TestDungeon(unittest.TestCase): self.world.itempool.extend(ItemFactory(['Green Pendant', 'Red Pendant', 'Blue Pendant', 'Beat Agahnim 1', 'Beat Agahnim 2', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'], 1)) def run_tests(self, access_pool): - for region in self.starting_regions: - self.world.get_region(region, 1).can_reach_private = lambda _: True + for exit in self.remove_exits: + self.world.get_entrance(exit, 1).connected_region = self.world.get_region('Menu', 1) for location, access, *item_pool in access_pool: items = item_pool[0] @@ -42,6 +44,14 @@ class TestDungeon(unittest.TestCase): else: items = ItemFactory(items, 1) state = CollectionState(self.world) + state.reachable_regions[1].add(self.world.get_region('Menu', 1)) + for region_name in self.starting_regions: + region = self.world.get_region(region_name, 1) + state.reachable_regions[1].add(region) + for exit in region.exits: + if exit.connected_region is not None: + state.blocked_connections[1].add(exit) + for item in items: item.advancement = True state.collect(item) diff --git a/test/dungeons/TestSkullWoods.py b/test/dungeons/TestSkullWoods.py index 01ab4b58..f7103cf2 100644 --- a/test/dungeons/TestSkullWoods.py +++ b/test/dungeons/TestSkullWoods.py @@ -42,6 +42,7 @@ class TestSkullWoods(TestDungeon): def testSkullWoodsLeftOnly(self): self.starting_regions = ['Skull Woods First Section (Left)'] + self.remove_exits = ['Skull Woods First Section Exit'] self.run_tests([ ["Skull Woods - Big Chest", False, []], ["Skull Woods - Big Chest", False, [], ['Never in logic']], @@ -59,6 +60,7 @@ class TestSkullWoods(TestDungeon): def testSkullWoodsBackOnly(self): self.starting_regions = ['Skull Woods First Section (Top)'] + self.remove_exits = ['Skull Woods First Section Exit'] self.run_tests([ ["Skull Woods - Big Chest", False, []], ["Skull Woods - Big Chest", False, [], ['Big Key (Skull Woods)']], @@ -81,6 +83,7 @@ class TestSkullWoods(TestDungeon): def testSkullWoodsMiddle(self): self.starting_regions = ['Skull Woods Second Section'] + self.remove_exits = ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)'] self.run_tests([["Skull Woods - Big Key Chest", True, []]]) def testSkullWoodsBack(self):