mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-04-01 10:13:24 -07:00
Merge remote-tracking branch 'remotes/upstream/main'
# Conflicts: # docs/CODEOWNERS
This commit is contained in:
@@ -81,6 +81,9 @@
|
||||
# Hylics 2
|
||||
/worlds/hylics2/ @TRPG0
|
||||
|
||||
# Inscryption
|
||||
/worlds/inscryption/ @DrBibop @Glowbuzz
|
||||
|
||||
# Jak and Daxter: The Precursor Legacy
|
||||
/worlds/jakanddaxter/ @massimilianodelliubaldini
|
||||
|
||||
@@ -152,7 +155,7 @@
|
||||
/worlds/saving_princess/ @LeonarthCG
|
||||
|
||||
# Shivers
|
||||
/worlds/shivers/ @GodlFire
|
||||
/worlds/shivers/ @GodlFire @korydondzila
|
||||
|
||||
# A Short Hike
|
||||
/worlds/shorthike/ @chandler05 @BrandenEK
|
||||
|
||||
430
docs/entrance randomization.md
Normal file
430
docs/entrance randomization.md
Normal file
@@ -0,0 +1,430 @@
|
||||
# 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.
|
||||
@@ -540,7 +540,7 @@ In JSON this may look like:
|
||||
| ----- | ----- |
|
||||
| 0 | Nothing special about this item |
|
||||
| 0b001 | If set, indicates the item can unlock logical advancement |
|
||||
| 0b010 | If set, indicates the item is important but not in a way that unlocks advancement |
|
||||
| 0b010 | If set, indicates the item is especially useful |
|
||||
| 0b100 | If set, indicates the item is a trap |
|
||||
|
||||
### JSONMessagePart
|
||||
@@ -554,6 +554,7 @@ class JSONMessagePart(TypedDict):
|
||||
color: Optional[str] # only available if type is a color
|
||||
flags: Optional[int] # only available if type is an item_id or item_name
|
||||
player: Optional[int] # only available if type is either item or location
|
||||
hint_status: Optional[HintStatus] # only available if type is hint_status
|
||||
```
|
||||
|
||||
`type` is used to denote the intent of the message part. This can be used to indicate special information which may be rendered differently depending on client. How these types are displayed in Archipelago's ALttP client is not the end-all be-all. Other clients may choose to interpret and display these messages differently.
|
||||
@@ -569,6 +570,7 @@ Possible values for `type` include:
|
||||
| location_id | Location ID, should be resolved to Location Name |
|
||||
| location_name | Location Name, not currently used over network, but supported by reference Clients. |
|
||||
| entrance_name | Entrance Name. No ID mapping exists. |
|
||||
| hint_status | The [HintStatus](#HintStatus) of the hint. Both `text` and `hint_status` are given. |
|
||||
| color | Regular text that should be colored. Only `type` that will contain `color` data. |
|
||||
|
||||
|
||||
|
||||
@@ -248,7 +248,8 @@ will all have the same ID. Name must not be numeric (must contain at least 1 let
|
||||
Other classifications include:
|
||||
|
||||
* `filler`: a regular item or trash item
|
||||
* `useful`: generally quite useful, but not required for anything logical. Cannot be placed on excluded locations
|
||||
* `useful`: item that is especially useful. Cannot be placed on excluded or unreachable locations. When combined with
|
||||
another flag like "progression", it means "an especially useful progression item".
|
||||
* `trap`: negative impact on the player
|
||||
* `skip_balancing`: denotes that an item should not be moved to an earlier sphere for the purpose of balancing (to be
|
||||
combined with `progression`; see below)
|
||||
@@ -699,9 +700,92 @@ When importing a file that defines a class that inherits from `worlds.AutoWorld.
|
||||
is automatically extended by the mixin's members. These members should be prefixed with the name of the implementing
|
||||
world since the namespace is shared with all other logic mixins.
|
||||
|
||||
Some uses could be to add additional variables to the state object, or to have a custom state machine that gets modified
|
||||
with the state.
|
||||
Please do this with caution and only when necessary.
|
||||
LogicMixin is handy when your logic is more complex than one-to-one location-item relationships.
|
||||
A game in which "The red key opens the red door" can just express this relationship through a one-line access rule.
|
||||
But now, consider a game with a heavy focus on combat, where the main logical consideration is which enemies you can
|
||||
defeat with your current items.
|
||||
There could be dozens of weapons, armor pieces, or consumables that each improve your ability to defeat
|
||||
specific enemies to varying degrees. It would be useful to be able to keep track of "defeatable enemies" as a state variable,
|
||||
and have this variable be recalculated as necessary based on newly collected/removed items.
|
||||
This is the capability of LogicMixin: Adding custom variables to state that get recalculated as necessary.
|
||||
|
||||
In general, a LogicMixin class should have at least one mutable variable that is tracking some custom state per player,
|
||||
as well as `init_mixin` and `copy_mixin` functions so that this variable gets initialized and copied correctly when
|
||||
`CollectionState()` and `CollectionState.copy()` are called respectively.
|
||||
|
||||
```python
|
||||
from BaseClasses import CollectionState, MultiWorld
|
||||
from worlds.AutoWorld import LogicMixin
|
||||
|
||||
class MyGameState(LogicMixin):
|
||||
mygame_defeatable_enemies: Dict[int, Set[str]] # per player
|
||||
|
||||
def init_mixin(self, multiworld: MultiWorld) -> None:
|
||||
# Initialize per player with the corresponding "nothing" value, such as 0 or an empty set.
|
||||
# You can also use something like Collections.defaultdict
|
||||
self.mygame_defeatable_enemies = {
|
||||
player: set() for player in multiworld.get_game_players("My Game")
|
||||
}
|
||||
|
||||
def copy_mixin(self, new_state: CollectionState) -> CollectionState:
|
||||
# Be careful to make a "deep enough" copy here!
|
||||
new_state.mygame_defeatable_enemies = {
|
||||
player: enemies.copy() for player, enemies in self.mygame_defeatable_enemies.items()
|
||||
}
|
||||
```
|
||||
|
||||
After doing this, you can now access `state.mygame_defeatable_enemies[player]` from your access rules.
|
||||
|
||||
Usually, doing this coincides with an override of `World.collect` and `World.remove`, where the custom state variable
|
||||
gets recalculated when a relevant item is collected or removed.
|
||||
|
||||
```python
|
||||
# __init__.py
|
||||
|
||||
def collect(self, state: CollectionState, item: Item) -> bool:
|
||||
change = super().collect(state, item)
|
||||
if change and item in COMBAT_ITEMS:
|
||||
state.mygame_defeatable_enemies[self.player] |= get_newly_unlocked_enemies(state)
|
||||
return change
|
||||
|
||||
def remove(self, state: CollectionState, item: Item) -> bool:
|
||||
change = super().remove(state, item)
|
||||
if change and item in COMBAT_ITEMS:
|
||||
state.mygame_defeatable_enemies[self.player] -= get_newly_locked_enemies(state)
|
||||
return change
|
||||
```
|
||||
|
||||
Using LogicMixin can greatly slow down your code if you don't use it intelligently. This is because `collect`
|
||||
and `remove` are called very frequently during fill. If your `collect` & `remove` cause a heavy calculation
|
||||
every time, your code might end up being *slower* than just doing calculations in your access rules.
|
||||
|
||||
One way to optimise recalculations is to make use of the fact that `collect` should only unlock things,
|
||||
and `remove` should only lock things.
|
||||
In our example, we have two different functions: `get_newly_unlocked_enemies` and `get_newly_locked_enemies`.
|
||||
`get_newly_unlocked_enemies` should only consider enemies that are *not already in the set*
|
||||
and check whether they were **unlocked**.
|
||||
`get_newly_locked_enemies` should only consider enemies that are *already in the set*
|
||||
and check whether they **became locked**.
|
||||
|
||||
Another impactful way to optimise LogicMixin is to use caching.
|
||||
Your custom state variables don't actually need to be recalculated on every `collect` / `remove`, because there are
|
||||
often multiple calls to `collect` / `remove` between access rule calls. Thus, it would be much more efficient to hold
|
||||
off on recaculating until the an actual access rule call happens.
|
||||
A common way to realize this is to define a `mygame_state_is_stale` variable that is set to True in `collect`, `remove`,
|
||||
and `init_mixin`. The calls to the actual recalculating functions are then moved to the start of the relevant
|
||||
access rules like this:
|
||||
|
||||
```python
|
||||
def can_defeat_enemy(state: CollectionState, player: int, enemy: str) -> bool:
|
||||
if state.mygame_state_is_stale[player]:
|
||||
state.mygame_defeatable_enemies[player] = recalculate_defeatable_enemies(state)
|
||||
state.mygame_state_is_stale[player] = False
|
||||
|
||||
return enemy in state.mygame_defeatable_enemies[player]
|
||||
```
|
||||
|
||||
Only use LogicMixin if necessary. There are often other ways to achieve what it does, like making clever use of
|
||||
`state.prog_items`, using event items, pseudo-regions, etc.
|
||||
|
||||
#### pre_fill
|
||||
|
||||
|
||||
Reference in New Issue
Block a user