 218f28912e
			
		
	
	218f28912e
	
	
	
		
			
			* Initial implementation of Generic ER * Move ERType to Entrance.Type, fix typing imports * updates based on testing (read: flailing) * Updates from feedback * Various bug fixes in ERCollectionState * Use deque instead of queue.Queue * Allow partial entrances in collection state earlier, doc improvements * Prevent early loops in region graph, improve reusability of ER stage code * Typos, grammar, PEP8, and style "fixes" * use RuntimeError instead of bare Exceptions * return tuples from connect since it's slightly faster for our purposes * move the shuffle to the beginning of find_pairing * do er_state placements within pairing lookups to remove code duplication * requested adjustments * Add some temporary performance logging * Use CollectionState to track available exits and placed regions * Add a method to automatically disconnect entrances in a coupled-compliant way Update docs and cleanup todos * Make find_placeable_exits deterministic by sorting blocked_connections set * Move EntranceType out of Entrance * Handle minimal accessibility, autodetect regions, and improvements to disconnect * Add on_connect callback to react to succeeded entrance placements * Relax island-prevention constraints after a successful run on minimal accessibility; better error message on failure * First set of unit tests for generic ER * Change on_connect to send lists, add unit tests for EntranceLookup * Fix duplicated location names in tests * Update tests after merge * Address review feedback, start docs with diagrams * Fix rendering of hidden nodes in ER doc * Move most docstring content into a docs article * Clarify when randomize_entrances can be called safely * Address review feedback * Apply suggestions from code review Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com> * Docs on ERPlacementState, add coupled/uncoupled handling to deadend detection * Documentation clarifications * Update groups to allow any hashable * Restrict groups from hashable to int * Implement speculative sweeping in stage 1, address misc review comments * Clean unused imports in BaseClasses.py * Restrictive region/speculative sweep test * sweep_for_events->advancement * Remove redundant __str__ Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com> * Allow partial entrances in auto indirect condition sweep * Treat regions needed for logic as non-dead-end regardless of if they have exits, flip order of stage 3 and 4 to ensure there are enough exits for the dead ends * Typing fixes suggested by mypy * Remove erroneous newline Not sure why the merge conflict editor is different and worse than the normal editor. Crazy * Use modern typing for ER * Enforce the use of explicit indirect conditions * Improve doc on required indirect conditions --------- Co-authored-by: qwint <qwint.42@gmail.com> Co-authored-by: alwaysintreble <mmmcheese158@gmail.com> Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
		
			
				
	
	
		
			430 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			430 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
| # Entrance Randomization
 | |
| 
 | |
| This document discusses the API and underlying implementation of the generic entrance randomization algorithm
 | |
| exposed in [entrance_rando.py](/entrance_rando.py). Throughout the doc, entrance randomization is frequently abbreviated
 | |
| as "ER."
 | |
| 
 | |
| This doc assumes familiarity with Archipelago's graph logic model. If you don't have a solid understanding of how
 | |
| regions work, you should start there.
 | |
| 
 | |
| ## Entrance randomization concepts
 | |
| 
 | |
| ### Terminology
 | |
| 
 | |
| Some important terminology to understand when reading this doc and working with ER is listed below.
 | |
| 
 | |
| * Entrance rando - sometimes called "room rando," "transition rando," "door rando," or similar,
 | |
|   this is a game mode in which the game map itself is randomized.
 | |
|   In Archipelago, these things are often represented as `Entrance`s in the region graph, so we call it Entrance rando.
 | |
| * Entrances and exits - entrances are ways into your region, exits are ways out of the region. In code, they are both
 | |
|   represented as `Entrance` objects. In this doc, the terms "entrances" and "exits" will be used in this sense; the
 | |
|   `Entrance` class will always be referenced in a code block with an uppercase E.
 | |
| * Dead end - a connected group of regions which can never help ER progress. This means that it:
 | |
|   * Is not in any indirect conditions/access rules.
 | |
|   * Has no plando'd or otherwise preplaced progression items, including events.
 | |
|   * Has no randomized exits.
 | |
| * One way transition - a transition that, in the game, is not safe to reverse through (for example, in Hollow Knight,
 | |
|   some transitions are inaccessible backwards in vanilla and would put you out of bounds). One way transitions are 
 | |
|   paired together during randomization to prevent such unsafe game states. Most transitions are not one way.
 | |
| 
 | |
| ### Basic randomization strategy
 | |
| 
 | |
| The Generic ER algorithm works by using the logic structures you are already familiar with. To give a basic example,
 | |
| let's assume a toy world is defined with the vanilla region graph modeled below. In this diagram, the smaller boxes
 | |
| represent regions while the larger boxes represent scenes. Scenes are not an Archipelago concept, the grouping is
 | |
| purely illustrative.
 | |
| 
 | |
| ```mermaid
 | |
| %%{init: {"graph": {"defaultRenderer": "elk"}} }%%
 | |
| graph LR
 | |
|     subgraph startingRoom [Starting Room]
 | |
|         S[Starting Room Right Door]
 | |
|     end
 | |
|     subgraph sceneB [Scene B]
 | |
|         BR1[Scene B Right Door]
 | |
|     end
 | |
|     subgraph sceneA [Scene A]
 | |
|         AL1[Scene A Lower Left Door] <--> AR1[Scene A Right Door]
 | |
|         AL2[Scene A Upper Left Door] <--> AR1
 | |
|     end
 | |
|     subgraph sceneC [Scene C]
 | |
|         CL1[Scene C Left Door] <--> CR1[Scene C Upper Right Door]
 | |
|         CL1 <--> CR2[Scene C Lower Right Door]
 | |
|     end
 | |
|     subgraph sceneD [Scene D]
 | |
|         DL1[Scene D Left Door] <--> DR1[Scene D Right Door]
 | |
|     end
 | |
|     subgraph endingRoom [Ending Room]
 | |
|         EL1[Ending Room Upper Left Door] <--> Victory
 | |
|         EL2[Ending Room Lower Left Door] <--> Victory
 | |
|     end
 | |
|     Menu --> S
 | |
|     S <--> AL2
 | |
|     BR1 <--> AL1
 | |
|     AR1 <--> CL1
 | |
|     CR1 <--> DL1
 | |
|     DR1 <--> EL1
 | |
|     CR2 <--> EL2
 | |
|     
 | |
|     classDef hidden display:none;
 | |
| ```
 | |
| 
 | |
| First, the world begins by splitting the `Entrance`s which should be randomized. This is essentially all that has to be
 | |
| done on the world side; calling the `randomize_entrances` function will do the rest, using your region definitions and
 | |
| logic to generate a valid world layout by connecting the partially connected edges you've defined. After you have done
 | |
| that, your region graph might look something like the following diagram. Note how each randomizable entrance/exit pair
 | |
| (represented as a bidirectional arrow) is disconnected on one end.
 | |
| 
 | |
| > [!NOTE]
 | |
| > It is required to use explicit indirect conditions when using Generic ER. Without this restriction,
 | |
| > Generic ER would have no way to correctly determine that a region may be required in logic,
 | |
| > leading to significantly higher failure rates due to mis-categorized regions.
 | |
| 
 | |
| ```mermaid
 | |
| %%{init: {"graph": {"defaultRenderer": "elk"}} }%%
 | |
| graph LR
 | |
|     subgraph startingRoom [Starting Room]
 | |
|         S[Starting Room Right Door]
 | |
|     end
 | |
|     subgraph sceneA [Scene A]
 | |
|         AL1[Scene A Upper Left Door] <--> AR1[Scene A Right Door]
 | |
|         AL2[Scene A Lower Left Door] <--> AR1
 | |
|     end
 | |
|     subgraph sceneB [Scene B]
 | |
|         BR1[Scene B Right Door]
 | |
|     end
 | |
|     subgraph sceneC [Scene C]
 | |
|         CL1[Scene C Left Door] <--> CR1[Scene C Upper Right Door]
 | |
|         CL1 <--> CR2[Scene C Lower Right Door]
 | |
|     end
 | |
|     subgraph sceneD [Scene D]
 | |
|         DL1[Scene D Left Door] <--> DR1[Scene D Right Door]
 | |
|     end
 | |
|     subgraph endingRoom [Ending Room]
 | |
|         EL1[Ending Room Upper Left Door] <--> Victory
 | |
|         EL2[Ending Room Lower Left Door] <--> Victory
 | |
|     end
 | |
|     Menu --> S
 | |
|     S <--> T1:::hidden
 | |
|     T2:::hidden <--> AL1
 | |
|     T3:::hidden <--> AL2
 | |
|     AR1 <--> T5:::hidden
 | |
|     BR1 <--> T4:::hidden
 | |
|     T6:::hidden <--> CL1
 | |
|     CR1 <--> T7:::hidden
 | |
|     CR2 <--> T11:::hidden
 | |
|     T8:::hidden <--> DL1
 | |
|     DR1 <--> T9:::hidden
 | |
|     T10:::hidden <--> EL1
 | |
|     T12:::hidden <--> EL2
 | |
|     
 | |
|     classDef hidden display:none;
 | |
| ```
 | |
| 
 | |
| From here, you can call the `randomize_entrances` function and Archipelago takes over. Starting from the Menu region,
 | |
| the algorithm will sweep out to find eligible region exits to randomize. It will then select an eligible target entrance
 | |
| and connect them, prioritizing giving access to unvisited regions first until all regions are placed. Once the exit has
 | |
| been connected to the new region, placeholder entrances are deleted. This process is visualized in the diagram below
 | |
| with the newly connected edge highlighted in red.
 | |
| 
 | |
| ```mermaid
 | |
| %%{init: {"graph": {"defaultRenderer": "elk"}} }%%
 | |
| graph LR
 | |
|     subgraph startingRoom [Starting Room]
 | |
|         S[Starting Room Right Door]
 | |
|     end
 | |
|     subgraph sceneA [Scene A]
 | |
|         AL1[Scene A Upper Left Door] <--> AR1[Scene A Right Door]
 | |
|         AL2[Scene A Lower Left Door] <--> AR1
 | |
|     end
 | |
|     subgraph sceneB [Scene B]
 | |
|         BR1[Scene B Right Door]
 | |
|     end
 | |
|     subgraph sceneC [Scene C]
 | |
|         CL1[Scene C Left Door] <--> CR1[Scene C Upper Right Door]
 | |
|         CL1 <--> CR2[Scene C Lower Right Door]
 | |
|     end
 | |
|     subgraph sceneD [Scene D]
 | |
|         DL1[Scene D Left Door] <--> DR1[Scene D Right Door]
 | |
|     end
 | |
|     subgraph endingRoom [Ending Room]
 | |
|         EL1[Ending Room Upper Left Door] <--> Victory
 | |
|         EL2[Ending Room Lower Left Door] <--> Victory
 | |
|     end
 | |
|     Menu --> S
 | |
|     S <--> CL1
 | |
|     T2:::hidden <--> AL1
 | |
|     T3:::hidden <--> AL2
 | |
|     AR1 <--> T5:::hidden
 | |
|     BR1 <--> T4:::hidden
 | |
|     CR1 <--> T7:::hidden
 | |
|     CR2 <--> T11:::hidden
 | |
|     T8:::hidden <--> DL1
 | |
|     DR1 <--> T9:::hidden
 | |
|     T10:::hidden <--> EL1
 | |
|     T12:::hidden <--> EL2
 | |
|     
 | |
|     classDef hidden display:none;
 | |
|     linkStyle 8 stroke:red,stroke-width:5px;
 | |
| ```
 | |
| 
 | |
| This process is then repeated until all disconnected `Entrance`s have been connected or deleted, eventually resulting
 | |
| in a randomized region layout.
 | |
| 
 | |
| ```mermaid
 | |
| %%{init: {"graph": {"defaultRenderer": "elk"}} }%%
 | |
| graph LR
 | |
|     subgraph startingRoom [Starting Room]
 | |
|         S[Starting Room Right Door]
 | |
|     end
 | |
|     subgraph sceneA [Scene A]
 | |
|         AL1[Scene A Upper Left Door] <--> AR1[Scene A Right Door]
 | |
|         AL2[Scene A Lower Left Door] <--> AR1
 | |
|     end
 | |
|     subgraph sceneB [Scene B]
 | |
|         BR1[Scene B Right Door]
 | |
|     end
 | |
|     subgraph sceneC [Scene C]
 | |
|         CL1[Scene C Left Door] <--> CR1[Scene C Upper Right Door]
 | |
|         CL1 <--> CR2[Scene C Lower Right Door]
 | |
|     end
 | |
|     subgraph sceneD [Scene D]
 | |
|         DL1[Scene D Left Door] <--> DR1[Scene D Right Door]
 | |
|     end
 | |
|     subgraph endingRoom [Ending Room]
 | |
|         EL1[Ending Room Upper Left Door] <--> Victory
 | |
|         EL2[Ending Room Lower Left Door] <--> Victory
 | |
|     end
 | |
|     Menu --> S
 | |
|     S <--> CL1
 | |
|     AR1 <--> DL1
 | |
|     BR1 <--> EL2
 | |
|     CR1 <--> EL1
 | |
|     CR2 <--> AL1
 | |
|     DR1 <--> AL2
 | |
|     
 | |
|     classDef hidden display:none;
 | |
| ```
 | |
| 
 | |
| #### ER and minimal accessibility
 | |
| 
 | |
| In general, even on minimal accessibility, ER will prefer to provide access to as many regions as possible. This is for
 | |
| 2 reasons:
 | |
| 1. Generally, having items spread across the world is going to be a more fun/engaging experience for players than
 | |
|    severely restricting their map. Imagine an ER arrangement with just the start region, the goal region, and exactly
 | |
|    enough locations in between them to get the goal - this may be the intuitive behavior of minimal, or even the desired
 | |
|    behavior in some cases, but it is not a particularly interesting randomizer.
 | |
| 2. Giving access to more of the world will give item fill a higher chance to succeed.
 | |
| 
 | |
| However, ER will cull unreachable regions and exits if necessary to save the generation of a beaten minimal.
 | |
| 
 | |
| ## Usage
 | |
| 
 | |
| ### Defining entrances to be randomized
 | |
| 
 | |
| The first step to using generic ER is defining entrances to be randomized. In order to do this, you will need to
 | |
| leave partially disconnected exits without a `target_region` and partially disconnected entrances without a
 | |
| `parent_region`. You can do this either by hand using `region.create_exit` and `region.create_er_target`, or you can
 | |
| create your vanilla region graph and then use `disconnect_entrance_for_randomization` to split the desired edges.
 | |
| If you're not sure which to use, prefer the latter approach as it will automatically satisfy the requirements for 
 | |
| coupled randomization (discussed in more depth later).
 | |
| 
 | |
| > [!TIP]
 | |
| > It's recommended to give your `Entrance`s non-default names when creating them. The default naming scheme is 
 | |
| > `f"{parent_region} -> {target_region}"` which is generally not helpful in an entrance rando context - after all,
 | |
| > the target region will not be the same as vanilla and regions are often not user-facing anyway. Instead consider names
 | |
| > that describe the location of the exit, such as "Starting Room Right Door."
 | |
| 
 | |
| When creating your `Entrance`s you should also set the randomization type and group. One-way `Entrance`s represent
 | |
| transitions which are impossible to traverse in reverse. All other transitions are two-ways. To ensure that all
 | |
| transitions can be accessed in the game, one-ways are only randomized with other one-ways and two-ways are only
 | |
| randomized with other two-ways. You can set whether an `Entrance` is one-way or two-way using the `randomization_type`
 | |
| attribute.
 | |
| 
 | |
| `Entrance`s can also set the `randomization_group` attribute to allow for grouping during randomization. This can be
 | |
| any integer you define and may be based on player options. Some possible use cases for grouping include:
 | |
| * Directional matching - only match leftward-facing transitions to rightward-facing ones
 | |
| * Terrain matching - only match water transitions to water transitions and land transitions to land transitions
 | |
| * Dungeon shuffle - only shuffle entrances within a dungeon/area with each other
 | |
| * Combinations of the above
 | |
| 
 | |
| By default, all `Entrance`s are placed in the group 0. An entrance can only be a member of one group, but a given group
 | |
| may connect to many other groups.
 | |
| 
 | |
| ### Calling generic ER
 | |
| 
 | |
| Once you have defined all your entrances and exits and connected the Menu region to your region graph, you can call 
 | |
| `randomize_entrances` to perform randomization.
 | |
| 
 | |
| #### Coupled and uncoupled modes
 | |
| 
 | |
| In coupled randomization, an entrance placed from A to B guarantees that the reverse placement B to A also exists
 | |
| (assuming that A and B are both two-way doors). Uncoupled randomization does not make this guarantee.
 | |
| 
 | |
| When using coupled mode, there are some requirements for how placeholder ER targets for two-ways are named. 
 | |
| `disconnect_entrance_for_randomization` will handle this for you. However, if you opt to create your ER targets and
 | |
| exits by hand, you will need to ensure that ER targets into a region are named the same as the exit they correspond to.
 | |
| This allows the randomizer to find and connect the reverse pairing after the first pairing is completed. See the diagram
 | |
| below for an example of incorrect and correct naming.
 | |
| 
 | |
| Incorrect target naming:
 | |
| 
 | |
| ```mermaid
 | |
| %%{init: {"graph": {"defaultRenderer": "elk"}} }%%
 | |
| graph LR
 | |
|     subgraph a [" "]
 | |
|         direction TB
 | |
|         target1
 | |
|         target2
 | |
|     end
 | |
|     subgraph b [" "]
 | |
|         direction TB
 | |
|         Region
 | |
|     end
 | |
|     Region["Room1"] -->|Room1 Right Door| target1:::hidden
 | |
|     Region --- target2:::hidden -->|Room2 Left Door| Region
 | |
| 
 | |
|     linkStyle 1 stroke:none;
 | |
|     classDef hidden display:none;
 | |
|     style a display:none;
 | |
|     style b display:none;
 | |
| ```
 | |
| 
 | |
| Correct target naming:
 | |
| 
 | |
| ```mermaid
 | |
| %%{init: {"graph": {"defaultRenderer": "elk"}} }%%
 | |
| graph LR
 | |
|     subgraph a [" "]
 | |
|         direction TB
 | |
|         target1
 | |
|         target2
 | |
|     end
 | |
|     subgraph b [" "]
 | |
|         direction TB
 | |
|         Region
 | |
|     end
 | |
|     Region["Room1"] -->|Room1 Right Door| target1:::hidden
 | |
|     Region --- target2:::hidden -->|Room1 Right Door| Region
 | |
| 
 | |
|     linkStyle 1 stroke:none;
 | |
|     classDef hidden display:none;
 | |
|     style a display:none;
 | |
|     style b display:none;
 | |
| ```
 | |
| 
 | |
| #### Implementing grouping
 | |
| 
 | |
| When you created your entrances, you defined the group each entrance belongs to. Now you will have to define how groups
 | |
| should connect with each other. This is done with the `target_group_lookup` and `preserve_group_order` parameters.
 | |
| There is also a convenience function `bake_target_group_lookup` which can help to prepare group lookups when more
 | |
| complex group mapping logic is needed. Some recipes for `target_group_lookup` are presented here.
 | |
| 
 | |
| For the recipes below, assume the following groups (if the syntax used here is unfamiliar to you, "bit masking" and 
 | |
| "bitwise operators" would be the terms to search for):
 | |
| ```python
 | |
| class Groups(IntEnum):
 | |
|     # Directions
 | |
|     LEFT = 1
 | |
|     RIGHT = 2
 | |
|     TOP = 3
 | |
|     BOTTOM = 4
 | |
|     DOOR = 5
 | |
|     # Areas
 | |
|     FIELD = 1 << 3
 | |
|     CAVE = 2 << 3
 | |
|     MOUNTAIN = 3 << 3
 | |
|     # Bitmasks
 | |
|     DIRECTION_MASK = FIELD - 1
 | |
|     AREA_MASK = ~0 << 3
 | |
| ```
 | |
| 
 | |
| Directional matching:
 | |
| ```python
 | |
| direction_matching_group_lookup = {
 | |
|     # with preserve_group_order = False, pair a left transition to either a right transition or door randomly
 | |
|     # with preserve_group_order = True, pair a left transition to a right transition, or else a door if no
 | |
|     #   viable right transitions remain
 | |
|     Groups.LEFT: [Groups.RIGHT, Groups.DOOR],
 | |
|     # ...
 | |
| }
 | |
| ```
 | |
| 
 | |
| Terrain matching or dungeon shuffle:
 | |
| ```python
 | |
| def randomize_within_same_group(group: int) -> List[int]:
 | |
|     return [group]
 | |
| identity_group_lookup = bake_target_group_lookup(world, randomize_within_same_group)
 | |
| ```
 | |
| 
 | |
| Directional + area shuffle:
 | |
| ```python
 | |
| def get_target_groups(group: int) -> List[int]:
 | |
|     # example group: LEFT | CAVE
 | |
|     # example result: [RIGHT | CAVE, DOOR | CAVE]
 | |
|     direction = group & Groups.DIRECTION_MASK
 | |
|     area = group & Groups.AREA_MASK
 | |
|     return [pair_direction | area for pair_direction in direction_matching_group_lookup[direction]]
 | |
| target_group_lookup = bake_target_group_lookup(world, get_target_groups)
 | |
| ```
 | |
| 
 | |
| #### When to call `randomize_entrances`
 | |
| 
 | |
| The short answer is that you will almost always want to do ER in `pre_fill`. For more information why, continue reading.
 | |
| 
 | |
| ER begins by collecting the entire item pool and then uses your access rules to try and prevent some kinds of failures. 
 | |
| This means 2 things about when you can call ER:
 | |
| 1. You must supply your item pool before calling ER, or call ER before setting any rules which require items.
 | |
| 2. If you have rules dependent on anything other than items (e.g. `Entrance`s or events), you must set your rules
 | |
|    and create your events before you call ER if you want to guarantee a correct output.
 | |
| 
 | |
| If the conditions above are met, you could theoretically do ER as early as `create_regions`. However, plando is also 
 | |
| a consideration. Since item plando happens between `set_rules` and `pre_fill` and modifies the item pool, doing ER 
 | |
| in `pre_fill` is the only way to account for placements made by item plando, otherwise you risk impossible seeds or 
 | |
| generation failures. Obviously, if your world implements entrance plando, you will likely want to do that before ER as
 | |
| well.
 | |
| 
 | |
| #### Informing your client about randomized entrances
 | |
| 
 | |
| `randomize_entrances` returns the completed `ERPlacementState`. The `pairings` attribute contains a list of the
 | |
| created placements by name which can be used to populate slot data.
 | |
| 
 | |
| ### Imposing custom constraints on randomization
 | |
| 
 | |
| Generic ER is, as the name implies, generic! That means that your world may have some use case which is not covered by
 | |
| the ER implementation. To solve this, you can create a custom `Entrance` class which provides custom implementations
 | |
| for `is_valid_source_transition` and `can_connect_to`. These allow arbitrary constraints to be implemented on
 | |
| randomization, for instance helping to prevent restrictive sphere 1s or ensuring a maximum distance from a "hub" region.
 | |
| 
 | |
| > [!IMPORTANT]
 | |
| > When implementing these functions, make sure to use `super().is_valid_source_transition` and `super().can_connect_to`
 | |
| > as part of your implementation. Otherwise ER may behave unexpectedly.
 | |
| 
 | |
| ## Implementation details
 | |
| 
 | |
| This section is a medium-level explainer of the implementation of ER for those who don't want to decipher the code.
 | |
| However, a basic understanding of the mechanics of `fill_restrictive` will be helpful as many of the underlying
 | |
| algorithms are shared
 | |
| 
 | |
| ER uses a forward fill approach to create the region layout. First, ER collects `all_state` and performs a region sweep
 | |
| from Menu, similar to fill. ER then proceeds in stages to complete the randomization:
 | |
| 1. Attempt to connect all non-dead-end regions, prioritizing access to unseen regions so there will always be new exits
 | |
|    to pair off.
 | |
| 2. Attempt to connect all dead-end regions, so that all regions will be placed
 | |
| 3. Connect all remaining dangling edges now that all regions are placed.
 | |
|     1. Connect any other dead end entrances (e.g. second entrances to the same dead end regions).
 | |
|     2. Connect all remaining non-dead-ends amongst each other.
 | |
| 
 | |
| The process for each connection will do the following:
 | |
| 1. Select a randomizable exit of a reachable region which is a valid source transition.
 | |
| 2. Get its group and check `target_group_lookup` to determine which groups are valid targets.
 | |
| 3. Look up ER targets from those groups and find one which is valid according to `can_connect_to`
 | |
| 4. Connect the source exit to the target's target_region and delete the target.
 | |
|    * In stage 1, before placing the last valid source transition, an additional speculative sweep is performed to ensure
 | |
|      that there will be an available exit after the placement so randomization can continue.
 | |
| 5. If it's coupled mode, find the reverse exit and target by name and connect them as well.
 | |
| 6. Sweep to update reachable regions.
 | |
| 7. Call the `on_connect` callback.
 | |
| 
 | |
| This process repeats until the stage is complete, no valid source transition is found, or no valid target transition is
 | |
| found for any source transition. Unlike fill, there is no attempt made to save a failed randomization. |