diff --git a/BaseClasses.py b/BaseClasses.py index f6b29e366080..5d6e9ade371f 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -634,7 +634,7 @@ def __init__(self, parent: MultiWorld): for item in items: self.collect(item, True) - def update_reachable_regions(self, player: int): + def update_reachable_regions(self, player: int, allow_partial_entrances: bool = False): self.stale[player] = False rrp = self.reachable_regions[player] bc = self.blocked_connections[player] @@ -654,18 +654,21 @@ def update_reachable_regions(self, player: int): if new_region in rrp: bc.remove(connection) elif connection.can_reach(self): - if new_region: - # assert new_region, f"tried to search through an Entrance \"{connection}\" with no Region" - 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 - for new_entrance in self.multiworld.indirect_connections.get(new_region, set()): - if new_entrance in bc and new_entrance not in queue: - queue.append(new_entrance) + if not allow_partial_entrances: + assert new_region, f"tried to search through an Entrance \"{connection}\" with no Region" + else: + if not new_region: + break + 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 + for new_entrance in self.multiworld.indirect_connections.get(new_region, set()): + if new_entrance in bc and new_entrance not in queue: + queue.append(new_entrance) def copy(self) -> CollectionState: ret = CollectionState(self.multiworld) @@ -794,8 +797,8 @@ def __init__(self, player: int, name: str = '', parent: Region = None, self.er_group = er_group self.er_type = er_type - def can_reach(self, state: CollectionState) -> bool: - if self.parent_region.can_reach(state) and self.access_rule(state): + def can_reach(self, state: CollectionState, allow_partial_entrances: bool = False) -> bool: + if self.parent_region.can_reach(state, allow_partial_entrances) and self.access_rule(state): 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 @@ -925,9 +928,9 @@ def set_exits(self, new): exits = property(get_exits, set_exits) - def can_reach(self, state: CollectionState) -> bool: + def can_reach(self, state: CollectionState, allow_partial_entrances: bool = False) -> bool: if state.stale[self.player]: - state.update_reachable_regions(self.player) + state.update_reachable_regions(self.player, allow_partial_entrances) return self in state.reachable_regions[self.player] @property diff --git a/EntranceRando.py b/EntranceRando.py index fcd89c99c533..611851af4b05 100644 --- a/EntranceRando.py +++ b/EntranceRando.py @@ -40,7 +40,7 @@ def __init__(self, rng: random.Random): # todo - investigate whether this might leak memory (holds references to Entrances)? @staticmethod @functools.cache - def _is_dead_end(entrance: Entrance, visited_regions=""): + def _is_dead_end(entrance: Entrance): """ Checks whether a entrance is an unconditional dead end, that is, no matter what you have, it will never lead to new randomizable exits. @@ -48,13 +48,8 @@ def _is_dead_end(entrance: Entrance, visited_regions=""): # obviously if this is an unpaired exit, then leads to unpaired exits! if not entrance.connected_region: return False - for region in visited_regions.split("::"): - if region == entrance.connected_region.name: - return True - else: - visited_regions += "::" + entrance.connected_region.name # if the connected region has no exits, it's a dead end. otherwise its exits must all be dead ends. - return not entrance.connected_region.exits or all(EntranceLookup._is_dead_end(exit, visited_regions) + return not entrance.connected_region.exits or all(EntranceLookup._is_dead_end(exit) for exit in entrance.connected_region.exits if exit.name != entrance.name) @@ -149,7 +144,7 @@ def place(self, start: Union[Region, Entrance]) -> None: self._pending_exits.add(exit) elif exit.connected_region not in self.placed_regions: # traverse unseen static connections - if exit.can_reach(self.collection_state): + if exit.can_reach(self.collection_state, True): q.put(exit.connected_region) else: self._pending_exits.add(exit) @@ -161,7 +156,7 @@ def sweep_pending_exits(self) -> None: """ no_longer_pending_exits = [] for exit in self._pending_exits: - if exit.connected_region and exit.can_reach(self.collection_state): + if exit.connected_region and exit.can_reach(self.collection_state, True): # this is an unrandomized entrance, so place it and propagate self.place(exit.connected_region) no_longer_pending_exits.append(exit) @@ -290,7 +285,7 @@ def randomize_entrances( # none of the existing targets can pair to the existing sources. Since dead ends will never add new sources # this means the current targets can never be paired (in most cases) # todo - investigate ways to prevent this case - return state + return state # this short circuts the exception for testing purposes in order to see how far ER got. raise Exception("Unable to place all non-dead-end entrances with available source exits") # anything we couldn't place before might be placeable now