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. |