From 0507134301b3436149e2cdd7716ee9422c0a483d Mon Sep 17 00:00:00 2001 From: qwint Date: Mon, 20 Nov 2023 02:28:16 -0600 Subject: [PATCH 1/2] updates based on testing (read: flailing) --- BaseClasses.py | 23 ++++++++++++----------- EntranceRando.py | 19 ++++++++++++------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 71d728fd8c7b..f6b29e366080 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -654,17 +654,18 @@ def update_reachable_regions(self, player: int): if new_region in rrp: bc.remove(connection) elif connection.can_reach(self): - 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 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) def copy(self) -> CollectionState: ret = CollectionState(self.multiworld) diff --git a/EntranceRando.py b/EntranceRando.py index a3386f9b63b8..fcd89c99c533 100644 --- a/EntranceRando.py +++ b/EntranceRando.py @@ -40,17 +40,21 @@ 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): + def _is_dead_end(entrance: Entrance, visited_regions=""): """ Checks whether a entrance is an unconditional dead end, that is, no matter what you have, it will never lead to new randomizable exits. """ - # 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) + return not entrance.connected_region.exits or all(EntranceLookup._is_dead_end(exit, visited_regions) for exit in entrance.connected_region.exits if exit.name != entrance.name) @@ -120,11 +124,11 @@ def place(self, start: Union[Region, Entrance]) -> None: starting_entrance_name = None if isinstance(start, Entrance): starting_entrance_name = start.name - q.put(start.parent_region) + q.put(start.connected_region) else: q.put(start) - while q: + while not q.empty(): region = q.get() if region in self.placed_regions: continue @@ -146,7 +150,7 @@ def place(self, start: Union[Region, Entrance]) -> None: elif exit.connected_region not in self.placed_regions: # traverse unseen static connections if exit.can_reach(self.collection_state): - q.put(exit) + q.put(exit.connected_region) else: self._pending_exits.add(exit) @@ -255,7 +259,7 @@ def randomize_entrances( # todo - this doesn't prioritize placing new rooms like the original did; # that's problematic because early loops would lead to failures # this is needed to reduce bias; otherwise newer exits are prioritized - rng.shuffle(state._placeable_exits) + # rng.shuffle(state._placeable_exits) source_exit = state._placeable_exits.pop() target_groups = get_target_groups(source_exit.er_group) @@ -286,6 +290,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 raise Exception("Unable to place all non-dead-end entrances with available source exits") # anything we couldn't place before might be placeable now From 907cea6864e448db7b6e7e100adc51b88e2de139 Mon Sep 17 00:00:00 2001 From: qwint Date: Mon, 20 Nov 2023 21:06:05 -0600 Subject: [PATCH 2/2] Updates from feedback --- BaseClasses.py | 37 ++++++++++++++++++++----------------- EntranceRando.py | 15 +++++---------- 2 files changed, 25 insertions(+), 27 deletions(-) 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