mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-24 02:13:22 -07:00
Compare commits
8 Commits
NewSoupVi-
...
NewSoupVi-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d729b00b22 | ||
|
|
f9fc6944d3 | ||
|
|
e984583e5e | ||
|
|
7e03a87608 | ||
|
|
456bc481a3 | ||
|
|
b4752cd32d | ||
|
|
ceec51b9e1 | ||
|
|
d3312287a8 |
@@ -692,17 +692,25 @@ class CollectionState():
|
|||||||
|
|
||||||
def update_reachable_regions(self, player: int):
|
def update_reachable_regions(self, player: int):
|
||||||
self.stale[player] = False
|
self.stale[player] = False
|
||||||
|
world: AutoWorld.World = self.multiworld.worlds[player]
|
||||||
reachable_regions = self.reachable_regions[player]
|
reachable_regions = self.reachable_regions[player]
|
||||||
blocked_connections = self.blocked_connections[player]
|
|
||||||
queue = deque(self.blocked_connections[player])
|
queue = deque(self.blocked_connections[player])
|
||||||
start = self.multiworld.get_region("Menu", player)
|
start: Region = world.get_region(world.origin_region_name)
|
||||||
|
|
||||||
# init on first call - this can't be done on construction since the regions don't exist yet
|
# init on first call - this can't be done on construction since the regions don't exist yet
|
||||||
if start not in reachable_regions:
|
if start not in reachable_regions:
|
||||||
reachable_regions.add(start)
|
reachable_regions.add(start)
|
||||||
blocked_connections.update(start.exits)
|
self.blocked_connections[player].update(start.exits)
|
||||||
queue.extend(start.exits)
|
queue.extend(start.exits)
|
||||||
|
|
||||||
|
if world.explicit_indirect_conditions:
|
||||||
|
self._update_reachable_regions_explicit_indirect_conditions(player, queue)
|
||||||
|
else:
|
||||||
|
self._update_reachable_regions_auto_indirect_conditions(player, queue)
|
||||||
|
|
||||||
|
def _update_reachable_regions_explicit_indirect_conditions(self, player: int, queue: deque):
|
||||||
|
reachable_regions = self.reachable_regions[player]
|
||||||
|
blocked_connections = self.blocked_connections[player]
|
||||||
# run BFS on all connections, and keep track of those blocked by missing items
|
# run BFS on all connections, and keep track of those blocked by missing items
|
||||||
while queue:
|
while queue:
|
||||||
connection = queue.popleft()
|
connection = queue.popleft()
|
||||||
@@ -722,6 +730,29 @@ class CollectionState():
|
|||||||
if new_entrance in blocked_connections and new_entrance not in queue:
|
if new_entrance in blocked_connections and new_entrance not in queue:
|
||||||
queue.append(new_entrance)
|
queue.append(new_entrance)
|
||||||
|
|
||||||
|
def _update_reachable_regions_auto_indirect_conditions(self, player: int, queue: deque):
|
||||||
|
reachable_regions = self.reachable_regions[player]
|
||||||
|
blocked_connections = self.blocked_connections[player]
|
||||||
|
new_connection: bool = True
|
||||||
|
# run BFS on all connections, and keep track of those blocked by missing items
|
||||||
|
while new_connection:
|
||||||
|
new_connection = False
|
||||||
|
while queue:
|
||||||
|
connection = queue.popleft()
|
||||||
|
new_region = connection.connected_region
|
||||||
|
if new_region in reachable_regions:
|
||||||
|
blocked_connections.remove(connection)
|
||||||
|
elif connection.can_reach(self):
|
||||||
|
assert new_region, f"tried to search through an Entrance \"{connection}\" with no Region"
|
||||||
|
reachable_regions.add(new_region)
|
||||||
|
blocked_connections.remove(connection)
|
||||||
|
blocked_connections.update(new_region.exits)
|
||||||
|
queue.extend(new_region.exits)
|
||||||
|
self.path[new_region] = (new_region.name, self.path.get(connection, None))
|
||||||
|
new_connection = True
|
||||||
|
# sweep for indirect connections, mostly Entrance.can_reach(unrelated_Region)
|
||||||
|
queue.extend(blocked_connections)
|
||||||
|
|
||||||
def copy(self) -> CollectionState:
|
def copy(self) -> CollectionState:
|
||||||
ret = CollectionState(self.multiworld)
|
ret = CollectionState(self.multiworld)
|
||||||
ret.prog_items = {player: counter.copy() for player, counter in self.prog_items.items()}
|
ret.prog_items = {player: counter.copy() for player, counter in self.prog_items.items()}
|
||||||
|
|||||||
@@ -77,4 +77,4 @@ There, you will find examples of games in the `worlds` folder:
|
|||||||
You may also find developer documentation in the `docs` folder:
|
You may also find developer documentation in the `docs` folder:
|
||||||
[/docs Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/docs).
|
[/docs Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/docs).
|
||||||
|
|
||||||
If you have more questions, feel free to ask in the **#archipelago-dev** channel on our Discord.
|
If you have more questions, feel free to ask in the **#ap-world-dev** channel on our Discord.
|
||||||
|
|||||||
@@ -118,9 +118,6 @@
|
|||||||
# Noita
|
# Noita
|
||||||
/worlds/noita/ @ScipioWright @heinermann
|
/worlds/noita/ @ScipioWright @heinermann
|
||||||
|
|
||||||
# Ocarina of Time
|
|
||||||
/worlds/oot/ @espeon65536
|
|
||||||
|
|
||||||
# Old School Runescape
|
# Old School Runescape
|
||||||
/worlds/osrs @digiholic
|
/worlds/osrs @digiholic
|
||||||
|
|
||||||
@@ -230,6 +227,9 @@
|
|||||||
# Links Awakening DX
|
# Links Awakening DX
|
||||||
# /worlds/ladx/
|
# /worlds/ladx/
|
||||||
|
|
||||||
|
# Ocarina of Time
|
||||||
|
# /worlds/oot/
|
||||||
|
|
||||||
## Disabled Unmaintained Worlds
|
## Disabled Unmaintained Worlds
|
||||||
|
|
||||||
# The following worlds in this repo are currently unmaintained and disabled as they do not work in core. If you are
|
# The following worlds in this repo are currently unmaintained and disabled as they do not work in core. If you are
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ display as `Value1` on the webhost.
|
|||||||
files, and both will resolve as `value1`. This should be used when changing options around, i.e. changing a Toggle to a
|
files, and both will resolve as `value1`. This should be used when changing options around, i.e. changing a Toggle to a
|
||||||
Choice, and defining `alias_true = option_full`.
|
Choice, and defining `alias_true = option_full`.
|
||||||
- All options with a fixed set of possible values (i.e. those which inherit from `Toggle`, `(Text)Choice` or
|
- All options with a fixed set of possible values (i.e. those which inherit from `Toggle`, `(Text)Choice` or
|
||||||
`(Named/Special)Range`) support `random` as a generic option. `random` chooses from any of the available values for that
|
`(Named)Range`) support `random` as a generic option. `random` chooses from any of the available values for that
|
||||||
option, and is reserved by AP. You can set this as your default value, but you cannot define your own `option_random`.
|
option, and is reserved by AP. You can set this as your default value, but you cannot define your own `option_random`.
|
||||||
However, you can override `from_text` and handle `text == "random"` to customize its behavior or
|
However, you can override `from_text` and handle `text == "random"` to customize its behavior or
|
||||||
implement it for additional option types.
|
implement it for additional option types.
|
||||||
@@ -129,6 +129,23 @@ class Difficulty(Choice):
|
|||||||
default = 1
|
default = 1
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Option Visibility
|
||||||
|
Every option has a Visibility IntFlag, defaulting to `all` (`0b1111`). This lets you choose where the option will be
|
||||||
|
displayed. This only impacts where options are displayed, not how they can be used. Hidden options are still valid
|
||||||
|
options in a yaml. The flags are as follows:
|
||||||
|
* `none` (`0b0000`): This option is not shown anywhere
|
||||||
|
* `template` (`0b0001`): This option shows up in template yamls
|
||||||
|
* `simple_ui` (`0b0010`): This option shows up on the options page
|
||||||
|
* `complex_ui` (`0b0100`): This option shows up on the advanced/weighted options page
|
||||||
|
* `spoiler` (`0b1000`): This option shows up in spoiler logs
|
||||||
|
|
||||||
|
```python
|
||||||
|
from Options import Choice, Visibility
|
||||||
|
|
||||||
|
class HiddenChoiceOption(Choice):
|
||||||
|
visibility = Visibility.none
|
||||||
|
```
|
||||||
|
|
||||||
### Option Groups
|
### Option Groups
|
||||||
Options may be categorized into groups for display on the WebHost. Option groups are displayed in the order specified
|
Options may be categorized into groups for display on the WebHost. Option groups are displayed in the order specified
|
||||||
by your world on the player-options and weighted-options pages. In the generated template files, there will be a comment
|
by your world on the player-options and weighted-options pages. In the generated template files, there will be a comment
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ Recommended steps
|
|||||||
* Refer to [Windows Compilers on the python wiki](https://wiki.python.org/moin/WindowsCompilers) for details.
|
* Refer to [Windows Compilers on the python wiki](https://wiki.python.org/moin/WindowsCompilers) for details.
|
||||||
Generally, selecting the box for "Desktop Development with C++" will provide what you need.
|
Generally, selecting the box for "Desktop Development with C++" will provide what you need.
|
||||||
* Build tools are not required if all modules are installed pre-compiled. Pre-compiled modules are pinned on
|
* Build tools are not required if all modules are installed pre-compiled. Pre-compiled modules are pinned on
|
||||||
[Discord in #archipelago-dev](https://discord.com/channels/731205301247803413/731214280439103580/905154456377757808)
|
[Discord in #ap-core-dev](https://discord.com/channels/731205301247803413/731214280439103580/905154456377757808)
|
||||||
|
|
||||||
* It is recommended to use [PyCharm IDE](https://www.jetbrains.com/pycharm/)
|
* It is recommended to use [PyCharm IDE](https://www.jetbrains.com/pycharm/)
|
||||||
* Run Generate.py which will prompt installation of missing modules, press enter to confirm
|
* Run Generate.py which will prompt installation of missing modules, press enter to confirm
|
||||||
|
|||||||
@@ -303,6 +303,31 @@ generation (entrance randomization).
|
|||||||
An access rule is a function that returns `True` or `False` for a `Location` or `Entrance` based on the current `state`
|
An access rule is a function that returns `True` or `False` for a `Location` or `Entrance` based on the current `state`
|
||||||
(items that have been collected).
|
(items that have been collected).
|
||||||
|
|
||||||
|
The two possible ways to make a [CollectionRule](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/generic/Rules.py#L10) are:
|
||||||
|
- `def rule(state: CollectionState) -> bool:`
|
||||||
|
- `lambda state: ... boolean expression ...`
|
||||||
|
|
||||||
|
An access rule can be assigned through `set_rule(location, rule)`.
|
||||||
|
|
||||||
|
Access rules usually check for one of two things.
|
||||||
|
- Items that have been collected (e.g. `state.has("Sword", player)`)
|
||||||
|
- Locations, Regions or Entrances that have been reached (e.g. `state.can_reach_region("Boss Room")`)
|
||||||
|
|
||||||
|
Keep in mind that entrances and locations implicitly check for the accessibility of their parent region, so you do not need to check explicitly for it.
|
||||||
|
|
||||||
|
#### An important note on Entrance access rules:
|
||||||
|
When using `state.can_reach` within an entrance access condition, you must also use `multiworld.register_indirect_condition`.
|
||||||
|
|
||||||
|
For efficiency reasons, every time reachable regions are searched, every entrance is only checked once in a somewhat non-deterministic order.
|
||||||
|
This is fine when checking for items using `state.has`, because items do not change during a region sweep.
|
||||||
|
However, `state.can_reach` checks for the very same thing we are updating: Regions.
|
||||||
|
This can lead to non-deterministic behavior and, in the worst case, even generation failures.
|
||||||
|
Even doing `state.can_reach_location` or `state.can_reach_entrance` is problematic, as these functions call `state.can_reach_region` on the respective parent region.
|
||||||
|
|
||||||
|
**Therefore, it is considered unsafe to perform `state.can_reach` from within an access condition for an entrance**, unless you are checking for something that sits in the source region of the entrance.
|
||||||
|
You can use `multiworld.register_indirect_condition(region, entrance)` to explicitly tell the generator that, when a given region becomes accessible, it is necessary to re-check a specific entrance.
|
||||||
|
You **must** use `multiworld.register_indirect_condition` if you perform this kind of `can_reach` from an entrance access rule, unless you have a **very** good technical understanding of the relevant code and can reason why it will never lead to problems in your case.
|
||||||
|
|
||||||
### Item Rules
|
### Item Rules
|
||||||
|
|
||||||
An item rule is a function that returns `True` or `False` for a `Location` based on a single item. It can be used to
|
An item rule is a function that returns `True` or `False` for a `Location` based on a single item. It can be used to
|
||||||
@@ -630,7 +655,7 @@ def set_rules(self) -> None:
|
|||||||
|
|
||||||
Custom methods can be defined for your logic rules. The access rule that ultimately gets assigned to the Location or
|
Custom methods can be defined for your logic rules. The access rule that ultimately gets assigned to the Location or
|
||||||
Entrance should be
|
Entrance should be
|
||||||
a [`CollectionRule`](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/generic/Rules.py#L9).
|
a [`CollectionRule`](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/generic/Rules.py#L10).
|
||||||
Typically, this is done by defining a lambda expression on demand at the relevant bit, typically calling other
|
Typically, this is done by defining a lambda expression on demand at the relevant bit, typically calling other
|
||||||
functions, but this can also be achieved by defining a method with the appropriate format and assigning it directly.
|
functions, but this can also be achieved by defining a method with the appropriate format and assigning it directly.
|
||||||
For an example, see [The Messenger](/worlds/messenger/rules.py).
|
For an example, see [The Messenger](/worlds/messenger/rules.py).
|
||||||
|
|||||||
@@ -26,8 +26,17 @@ Unless these are shared between multiple people, we expect the following from ea
|
|||||||
### Adding a World
|
### Adding a World
|
||||||
|
|
||||||
When we merge your world into the core Archipelago repository, you automatically become world maintainer unless you
|
When we merge your world into the core Archipelago repository, you automatically become world maintainer unless you
|
||||||
nominate someone else (i.e. there are multiple devs). You can define who is allowed to approve changes to your world
|
nominate someone else (i.e. there are multiple devs).
|
||||||
in the [CODEOWNERS](/docs/CODEOWNERS) document.
|
|
||||||
|
### Being added as a maintainer to an existing implementation
|
||||||
|
|
||||||
|
At any point, a world maintainer can approve the addition of another maintainer to their world.
|
||||||
|
In order to do this, either an existing maintainer or the new maintainer must open a PR updating the
|
||||||
|
[CODEOWNERS](/docs/CODEOWNERS) file.
|
||||||
|
This change must be approved by all existing maintainers of the affected world, the new maintainer candidate, and
|
||||||
|
one core maintainer.
|
||||||
|
To help the core team review the change, information about the new maintainer and their contributions should be
|
||||||
|
included in the PR description.
|
||||||
|
|
||||||
### Getting Voted
|
### Getting Voted
|
||||||
|
|
||||||
@@ -35,7 +44,7 @@ When a world is unmaintained, the [core maintainers](https://github.com/orgs/Arc
|
|||||||
can vote for a new maintainer if there is a candidate.
|
can vote for a new maintainer if there is a candidate.
|
||||||
For a vote to pass, the majority of participating core maintainers must vote in the affirmative.
|
For a vote to pass, the majority of participating core maintainers must vote in the affirmative.
|
||||||
The time limit is 1 week, but can end early if the majority is reached earlier.
|
The time limit is 1 week, but can end early if the majority is reached earlier.
|
||||||
Voting shall be conducted on Discord in #archipelago-dev.
|
Voting shall be conducted on Discord in #ap-core-dev.
|
||||||
|
|
||||||
## Dropping out
|
## Dropping out
|
||||||
|
|
||||||
@@ -51,7 +60,7 @@ for example when they become unreachable.
|
|||||||
For a vote to pass, the majority of participating core maintainers must vote in the affirmative.
|
For a vote to pass, the majority of participating core maintainers must vote in the affirmative.
|
||||||
The time limit is 2 weeks, but can end early if the majority is reached earlier AND the world maintainer was pinged and
|
The time limit is 2 weeks, but can end early if the majority is reached earlier AND the world maintainer was pinged and
|
||||||
made their case or was pinged and has been unreachable for more than 2 weeks already.
|
made their case or was pinged and has been unreachable for more than 2 weeks already.
|
||||||
Voting shall be conducted on Discord in #archipelago-dev. Commits that are a direct result of the voting shall include
|
Voting shall be conducted on Discord in #ap-core-dev. Commits that are a direct result of the voting shall include
|
||||||
date, voting members and final result in the commit message.
|
date, voting members and final result in the commit message.
|
||||||
|
|
||||||
## Handling of Unmaintained Worlds
|
## Handling of Unmaintained Worlds
|
||||||
|
|||||||
@@ -292,6 +292,14 @@ class World(metaclass=AutoWorldRegister):
|
|||||||
web: ClassVar[WebWorld] = WebWorld()
|
web: ClassVar[WebWorld] = WebWorld()
|
||||||
"""see WebWorld for options"""
|
"""see WebWorld for options"""
|
||||||
|
|
||||||
|
origin_region_name: str = "Menu"
|
||||||
|
"""Name of the Region from which accessibility is tested."""
|
||||||
|
|
||||||
|
explicit_indirect_conditions: bool = True
|
||||||
|
"""If True, the world implementation is supposed to use MultiWorld.register_indirect_condition() correctly.
|
||||||
|
If False, everything is rechecked at every step, which is slower computationally,
|
||||||
|
but may be desirable in complex/dynamic worlds."""
|
||||||
|
|
||||||
multiworld: "MultiWorld"
|
multiworld: "MultiWorld"
|
||||||
"""autoset on creation. The MultiWorld object for the currently generating multiworld."""
|
"""autoset on creation. The MultiWorld object for the currently generating multiworld."""
|
||||||
player: int
|
player: int
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ class Factorio(World):
|
|||||||
tech_tree_layout_prerequisites: typing.Dict[FactorioScienceLocation, typing.Set[FactorioScienceLocation]]
|
tech_tree_layout_prerequisites: typing.Dict[FactorioScienceLocation, typing.Set[FactorioScienceLocation]]
|
||||||
tech_mix: int = 0
|
tech_mix: int = 0
|
||||||
skip_silo: bool = False
|
skip_silo: bool = False
|
||||||
|
origin_region_name = "Nauvis"
|
||||||
science_locations: typing.List[FactorioScienceLocation]
|
science_locations: typing.List[FactorioScienceLocation]
|
||||||
|
|
||||||
settings: typing.ClassVar[FactorioSettings]
|
settings: typing.ClassVar[FactorioSettings]
|
||||||
@@ -125,9 +126,6 @@ class Factorio(World):
|
|||||||
def create_regions(self):
|
def create_regions(self):
|
||||||
player = self.player
|
player = self.player
|
||||||
random = self.multiworld.random
|
random = self.multiworld.random
|
||||||
menu = Region("Menu", player, self.multiworld)
|
|
||||||
crash = Entrance(player, "Crash Land", menu)
|
|
||||||
menu.exits.append(crash)
|
|
||||||
nauvis = Region("Nauvis", player, self.multiworld)
|
nauvis = Region("Nauvis", player, self.multiworld)
|
||||||
|
|
||||||
location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo + \
|
location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo + \
|
||||||
@@ -184,8 +182,7 @@ class Factorio(World):
|
|||||||
event = FactorioItem(f"Automated {ingredient}", ItemClassification.progression, None, player)
|
event = FactorioItem(f"Automated {ingredient}", ItemClassification.progression, None, player)
|
||||||
location.place_locked_item(event)
|
location.place_locked_item(event)
|
||||||
|
|
||||||
crash.connect(nauvis)
|
self.multiworld.regions.append(nauvis)
|
||||||
self.multiworld.regions += [menu, nauvis]
|
|
||||||
|
|
||||||
def create_items(self) -> None:
|
def create_items(self) -> None:
|
||||||
player = self.player
|
player = self.player
|
||||||
|
|||||||
@@ -601,11 +601,11 @@ class HKWorld(World):
|
|||||||
if change:
|
if change:
|
||||||
for effect_name, effect_value in item_effects.get(item.name, {}).items():
|
for effect_name, effect_value in item_effects.get(item.name, {}).items():
|
||||||
state.prog_items[item.player][effect_name] += effect_value
|
state.prog_items[item.player][effect_name] += effect_value
|
||||||
if item.name in {"Left_Mothwing_Cloak", "Right_Mothwing_Cloak"}:
|
if item.name in {"Left_Mothwing_Cloak", "Right_Mothwing_Cloak"}:
|
||||||
if state.prog_items[item.player].get('RIGHTDASH', 0) and \
|
if state.prog_items[item.player].get('RIGHTDASH', 0) and \
|
||||||
state.prog_items[item.player].get('LEFTDASH', 0):
|
state.prog_items[item.player].get('LEFTDASH', 0):
|
||||||
(state.prog_items[item.player]["RIGHTDASH"], state.prog_items[item.player]["LEFTDASH"]) = \
|
(state.prog_items[item.player]["RIGHTDASH"], state.prog_items[item.player]["LEFTDASH"]) = \
|
||||||
([max(state.prog_items[item.player]["RIGHTDASH"], state.prog_items[item.player]["LEFTDASH"])] * 2)
|
([max(state.prog_items[item.player]["RIGHTDASH"], state.prog_items[item.player]["LEFTDASH"])] * 2)
|
||||||
return change
|
return change
|
||||||
|
|
||||||
def remove(self, state, item: HKItem) -> bool:
|
def remove(self, state, item: HKItem) -> bool:
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class SubnauticaWorld(World):
|
|||||||
options_dataclass = options.SubnauticaOptions
|
options_dataclass = options.SubnauticaOptions
|
||||||
options: options.SubnauticaOptions
|
options: options.SubnauticaOptions
|
||||||
required_client_version = (0, 5, 0)
|
required_client_version = (0, 5, 0)
|
||||||
|
origin_region_name = "Planet 4546B"
|
||||||
creatures_to_scan: List[str]
|
creatures_to_scan: List[str]
|
||||||
|
|
||||||
def generate_early(self) -> None:
|
def generate_early(self) -> None:
|
||||||
@@ -66,13 +66,9 @@ class SubnauticaWorld(World):
|
|||||||
creature_pool, self.options.creature_scans.value)
|
creature_pool, self.options.creature_scans.value)
|
||||||
|
|
||||||
def create_regions(self):
|
def create_regions(self):
|
||||||
# Create Regions
|
# Create Region
|
||||||
menu_region = Region("Menu", self.player, self.multiworld)
|
|
||||||
planet_region = Region("Planet 4546B", self.player, self.multiworld)
|
planet_region = Region("Planet 4546B", self.player, self.multiworld)
|
||||||
|
|
||||||
# Link regions together
|
|
||||||
menu_region.connect(planet_region, "Lifepod 5")
|
|
||||||
|
|
||||||
# Create regular locations
|
# Create regular locations
|
||||||
location_names = itertools.chain((location["name"] for location in locations.location_table.values()),
|
location_names = itertools.chain((location["name"] for location in locations.location_table.values()),
|
||||||
(creature + creatures.suffix for creature in self.creatures_to_scan))
|
(creature + creatures.suffix for creature in self.creatures_to_scan))
|
||||||
@@ -93,11 +89,8 @@ class SubnauticaWorld(World):
|
|||||||
# make the goal event the victory "item"
|
# make the goal event the victory "item"
|
||||||
location.item.name = "Victory"
|
location.item.name = "Victory"
|
||||||
|
|
||||||
# Register regions to multiworld
|
# Register region to multiworld
|
||||||
self.multiworld.regions += [
|
self.multiworld.regions.append(planet_region)
|
||||||
menu_region,
|
|
||||||
planet_region
|
|
||||||
]
|
|
||||||
|
|
||||||
# refer to rules.py
|
# refer to rules.py
|
||||||
set_rules = set_rules
|
set_rules = set_rules
|
||||||
|
|||||||
@@ -204,11 +204,10 @@ class WitnessWorld(World):
|
|||||||
]
|
]
|
||||||
if early_items:
|
if early_items:
|
||||||
random_early_item = self.random.choice(early_items)
|
random_early_item = self.random.choice(early_items)
|
||||||
if (
|
mode = self.options.puzzle_randomization
|
||||||
self.options.puzzle_randomization == "sigma_expert"
|
if mode == "sigma_expert" or mode == "umbra_variety" or self.options.victory_condition == "panel_hunt":
|
||||||
or self.options.victory_condition == "panel_hunt"
|
# In Expert and Variety, only tag the item as early, rather than forcing it onto the gate.
|
||||||
):
|
# Same with panel hunt, since the Tutorial Gate Open panel is used for something else
|
||||||
# In Expert and Panel Hunt, only tag the item as early, rather than forcing it onto the gate.
|
|
||||||
self.multiworld.local_early_items[self.player][random_early_item] = 1
|
self.multiworld.local_early_items[self.player][random_early_item] = 1
|
||||||
else:
|
else:
|
||||||
# Force the item onto the tutorial gate check and remove it from our random pool.
|
# Force the item onto the tutorial gate check and remove it from our random pool.
|
||||||
@@ -255,7 +254,7 @@ class WitnessWorld(World):
|
|||||||
self.get_region(region).add_locations({loc: self.location_name_to_id[loc]})
|
self.get_region(region).add_locations({loc: self.location_name_to_id[loc]})
|
||||||
|
|
||||||
warning(
|
warning(
|
||||||
f"""Location "{loc}" had to be added to {self.player_name}'s world
|
f"""Location "{loc}" had to be added to {self.player_name}'s world
|
||||||
due to insufficient sphere 1 size."""
|
due to insufficient sphere 1 size."""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
1223
worlds/witness/data/WitnessLogicVariety.txt
Normal file
1223
worlds/witness/data/WitnessLogicVariety.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,22 @@ ITEM_GROUPS: Dict[str, Set[str]] = {}
|
|||||||
# item list during get_progression_items.
|
# item list during get_progression_items.
|
||||||
_special_usefuls: List[str] = ["Puzzle Skip"]
|
_special_usefuls: List[str] = ["Puzzle Skip"]
|
||||||
|
|
||||||
|
ALWAYS_GOOD_SYMBOL_ITEMS: Set[str] = {"Dots", "Black/White Squares", "Symmetry", "Shapers", "Stars"}
|
||||||
|
|
||||||
|
MODE_SPECIFIC_GOOD_ITEMS: Dict[str, Set[str]] = {
|
||||||
|
"none": set(),
|
||||||
|
"sigma_normal": set(),
|
||||||
|
"sigma_expert": {"Triangles"},
|
||||||
|
"umbra_variety": {"Triangles"}
|
||||||
|
}
|
||||||
|
|
||||||
|
MODE_SPECIFIC_GOOD_DISCARD_ITEMS: Dict[str, Set[str]] = {
|
||||||
|
"none": {"Triangles"},
|
||||||
|
"sigma_normal": {"Triangles"},
|
||||||
|
"sigma_expert": {"Arrows"},
|
||||||
|
"umbra_variety": set() # Variety Discards use both Arrows and Triangles, so neither of them are that useful alone
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def populate_items() -> None:
|
def populate_items() -> None:
|
||||||
for item_name, definition in static_witness_logic.ALL_ITEMS.items():
|
for item_name, definition in static_witness_logic.ALL_ITEMS.items():
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from .utils import (
|
|||||||
get_items,
|
get_items,
|
||||||
get_sigma_expert_logic,
|
get_sigma_expert_logic,
|
||||||
get_sigma_normal_logic,
|
get_sigma_normal_logic,
|
||||||
|
get_umbra_variety_logic,
|
||||||
get_vanilla_logic,
|
get_vanilla_logic,
|
||||||
logical_or_witness_rules,
|
logical_or_witness_rules,
|
||||||
parse_lambda,
|
parse_lambda,
|
||||||
@@ -292,6 +293,11 @@ def get_sigma_expert() -> StaticWitnessLogicObj:
|
|||||||
return StaticWitnessLogicObj(get_sigma_expert_logic())
|
return StaticWitnessLogicObj(get_sigma_expert_logic())
|
||||||
|
|
||||||
|
|
||||||
|
@cache_argsless
|
||||||
|
def get_umbra_variety() -> StaticWitnessLogicObj:
|
||||||
|
return StaticWitnessLogicObj(get_umbra_variety_logic())
|
||||||
|
|
||||||
|
|
||||||
def __getattr__(name: str) -> StaticWitnessLogicObj:
|
def __getattr__(name: str) -> StaticWitnessLogicObj:
|
||||||
if name == "vanilla":
|
if name == "vanilla":
|
||||||
return get_vanilla()
|
return get_vanilla()
|
||||||
@@ -299,6 +305,8 @@ def __getattr__(name: str) -> StaticWitnessLogicObj:
|
|||||||
return get_sigma_normal()
|
return get_sigma_normal()
|
||||||
if name == "sigma_expert":
|
if name == "sigma_expert":
|
||||||
return get_sigma_expert()
|
return get_sigma_expert()
|
||||||
|
if name == "umbra_variety":
|
||||||
|
return get_umbra_variety()
|
||||||
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
|
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -215,6 +215,10 @@ def get_sigma_expert_logic() -> List[str]:
|
|||||||
return get_adjustment_file("WitnessLogicExpert.txt")
|
return get_adjustment_file("WitnessLogicExpert.txt")
|
||||||
|
|
||||||
|
|
||||||
|
def get_umbra_variety_logic() -> List[str]:
|
||||||
|
return get_adjustment_file("WitnessLogicVariety.txt")
|
||||||
|
|
||||||
|
|
||||||
def get_vanilla_logic() -> List[str]:
|
def get_vanilla_logic() -> List[str]:
|
||||||
return get_adjustment_file("WitnessLogicVanilla.txt")
|
return get_adjustment_file("WitnessLogicVanilla.txt")
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ def get_always_hint_items(world: "WitnessWorld") -> List[str]:
|
|||||||
wincon = world.options.victory_condition
|
wincon = world.options.victory_condition
|
||||||
|
|
||||||
if discards:
|
if discards:
|
||||||
if difficulty == "sigma_expert":
|
if difficulty == "sigma_expert" or difficulty == "umbra_variety":
|
||||||
always.append("Arrows")
|
always.append("Arrows")
|
||||||
else:
|
else:
|
||||||
always.append("Triangles")
|
always.append("Triangles")
|
||||||
|
|||||||
@@ -250,10 +250,15 @@ class PanelHuntDiscourageSameAreaFactor(Range):
|
|||||||
class PuzzleRandomization(Choice):
|
class PuzzleRandomization(Choice):
|
||||||
"""
|
"""
|
||||||
Puzzles in this randomizer are randomly generated. This option changes the difficulty/types of puzzles.
|
Puzzles in this randomizer are randomly generated. This option changes the difficulty/types of puzzles.
|
||||||
|
"Sigma Normal" randomizes puzzles close to their original mechanics and difficulty.
|
||||||
|
"Sigma Expert" is an entirely new experience with extremely difficult random puzzles. Do not underestimate this mode, it is brutal.
|
||||||
|
"Umbra Variety" focuses on unique symbol combinations not featured in the original game. It is harder than Sigma Normal, but easier than Sigma Expert.
|
||||||
|
"None" means that the puzzles are unchanged from the original game.
|
||||||
"""
|
"""
|
||||||
display_name = "Puzzle Randomization"
|
display_name = "Puzzle Randomization"
|
||||||
option_sigma_normal = 0
|
option_sigma_normal = 0
|
||||||
option_sigma_expert = 1
|
option_sigma_expert = 1
|
||||||
|
option_umbra_variety = 3
|
||||||
option_none = 2
|
option_none = 2
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ from typing import TYPE_CHECKING, Dict, List, Set, cast
|
|||||||
from BaseClasses import Item, ItemClassification, MultiWorld
|
from BaseClasses import Item, ItemClassification, MultiWorld
|
||||||
|
|
||||||
from .data import static_items as static_witness_items
|
from .data import static_items as static_witness_items
|
||||||
from .data import static_logic as static_witness_logic
|
|
||||||
from .data.item_definition_classes import (
|
from .data.item_definition_classes import (
|
||||||
DoorItemDefinition,
|
DoorItemDefinition,
|
||||||
ItemCategory,
|
ItemCategory,
|
||||||
@@ -155,16 +154,12 @@ class WitnessPlayerItems:
|
|||||||
"""
|
"""
|
||||||
output: Set[str] = set()
|
output: Set[str] = set()
|
||||||
if self._world.options.shuffle_symbols:
|
if self._world.options.shuffle_symbols:
|
||||||
output = {"Dots", "Black/White Squares", "Symmetry", "Shapers", "Stars"}
|
discards_on = self._world.options.shuffle_discarded_panels
|
||||||
|
mode = self._world.options.puzzle_randomization.current_key
|
||||||
|
|
||||||
if self._world.options.shuffle_discarded_panels:
|
output = static_witness_items.ALWAYS_GOOD_SYMBOL_ITEMS | static_witness_items.MODE_SPECIFIC_GOOD_ITEMS[mode]
|
||||||
if self._world.options.puzzle_randomization == "sigma_expert":
|
if discards_on:
|
||||||
output.add("Arrows")
|
output |= static_witness_items.MODE_SPECIFIC_GOOD_DISCARD_ITEMS[mode]
|
||||||
else:
|
|
||||||
output.add("Triangles")
|
|
||||||
|
|
||||||
# Replace progressive items with their parents.
|
|
||||||
output = {static_witness_logic.get_parent_progressive_item(item) for item in output}
|
|
||||||
|
|
||||||
# Remove items that are mentioned in any plando options. (Hopefully, in the future, plando will get resolved
|
# Remove items that are mentioned in any plando options. (Hopefully, in the future, plando will get resolved
|
||||||
# before create_items so that we'll be able to check placed items instead of just removing all items mentioned
|
# before create_items so that we'll be able to check placed items instead of just removing all items mentioned
|
||||||
|
|||||||
@@ -87,12 +87,14 @@ class WitnessPlayerLogic:
|
|||||||
self.DIFFICULTY = world.options.puzzle_randomization
|
self.DIFFICULTY = world.options.puzzle_randomization
|
||||||
|
|
||||||
self.REFERENCE_LOGIC: StaticWitnessLogicObj
|
self.REFERENCE_LOGIC: StaticWitnessLogicObj
|
||||||
if self.DIFFICULTY == "sigma_expert":
|
if self.DIFFICULTY == "sigma_normal":
|
||||||
|
self.REFERENCE_LOGIC = static_witness_logic.sigma_normal
|
||||||
|
elif self.DIFFICULTY == "sigma_expert":
|
||||||
self.REFERENCE_LOGIC = static_witness_logic.sigma_expert
|
self.REFERENCE_LOGIC = static_witness_logic.sigma_expert
|
||||||
|
elif self.DIFFICULTY == "umbra_variety":
|
||||||
|
self.REFERENCE_LOGIC = static_witness_logic.umbra_variety
|
||||||
elif self.DIFFICULTY == "none":
|
elif self.DIFFICULTY == "none":
|
||||||
self.REFERENCE_LOGIC = static_witness_logic.vanilla
|
self.REFERENCE_LOGIC = static_witness_logic.vanilla
|
||||||
else:
|
|
||||||
self.REFERENCE_LOGIC = static_witness_logic.sigma_normal
|
|
||||||
|
|
||||||
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL: Dict[str, Set[Tuple[str, WitnessRule]]] = copy.deepcopy(
|
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL: Dict[str, Set[Tuple[str, WitnessRule]]] = copy.deepcopy(
|
||||||
self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME
|
self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ class WitnessPlayerRegions:
|
|||||||
self.reference_logic = static_witness_logic.sigma_normal
|
self.reference_logic = static_witness_logic.sigma_normal
|
||||||
elif difficulty == "sigma_expert":
|
elif difficulty == "sigma_expert":
|
||||||
self.reference_logic = static_witness_logic.sigma_expert
|
self.reference_logic = static_witness_logic.sigma_expert
|
||||||
|
elif difficulty == "umbra_variety":
|
||||||
|
self.reference_logic = static_witness_logic.umbra_variety
|
||||||
else:
|
else:
|
||||||
self.reference_logic = static_witness_logic.vanilla
|
self.reference_logic = static_witness_logic.vanilla
|
||||||
|
|
||||||
|
|||||||
@@ -96,6 +96,39 @@ class TestSymbolsRequiredToWinElevatorVanilla(WitnessTestBase):
|
|||||||
self.assert_can_beat_with_minimally(exact_requirement)
|
self.assert_can_beat_with_minimally(exact_requirement)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSymbolsRequiredToWinElevatorVariety(WitnessTestBase):
|
||||||
|
options = {
|
||||||
|
"shuffle_lasers": True,
|
||||||
|
"mountain_lasers": 1,
|
||||||
|
"victory_condition": "elevator",
|
||||||
|
"early_symbol_item": False,
|
||||||
|
"puzzle_randomization": "umbra_variety",
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_symbols_to_win(self) -> None:
|
||||||
|
"""
|
||||||
|
In symbol shuffle, the only way to reach the Elevator is through Mountain Entry by descending the Mountain.
|
||||||
|
This requires a very specific set of symbol items per puzzle randomization mode.
|
||||||
|
In this case, we check Variety Puzzles.
|
||||||
|
"""
|
||||||
|
|
||||||
|
exact_requirement = {
|
||||||
|
"Monastery Laser": 1,
|
||||||
|
"Progressive Dots": 2,
|
||||||
|
"Progressive Stars": 2,
|
||||||
|
"Progressive Symmetry": 1,
|
||||||
|
"Black/White Squares": 1,
|
||||||
|
"Colored Squares": 1,
|
||||||
|
"Shapers": 1,
|
||||||
|
"Rotated Shapers": 1,
|
||||||
|
"Eraser": 1,
|
||||||
|
"Triangles": 1,
|
||||||
|
"Arrows": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.assert_can_beat_with_minimally(exact_requirement)
|
||||||
|
|
||||||
|
|
||||||
class TestPanelsRequiredToWinElevator(WitnessTestBase):
|
class TestPanelsRequiredToWinElevator(WitnessTestBase):
|
||||||
options = {
|
options = {
|
||||||
"shuffle_lasers": True,
|
"shuffle_lasers": True,
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ class TestMaxEntityShuffle(WitnessTestBase):
|
|||||||
|
|
||||||
class TestPostgameGroupedDoors(WitnessTestBase):
|
class TestPostgameGroupedDoors(WitnessTestBase):
|
||||||
options = {
|
options = {
|
||||||
|
"puzzle_randomization": "umbra_variety",
|
||||||
"shuffle_postgame": True,
|
"shuffle_postgame": True,
|
||||||
"shuffle_discarded_panels": True,
|
"shuffle_discarded_panels": True,
|
||||||
"shuffle_doors": "doors",
|
"shuffle_doors": "doors",
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ class TestSymbolRequirementsMultiworld(WitnessMultiworldTestBase):
|
|||||||
{
|
{
|
||||||
"puzzle_randomization": "none",
|
"puzzle_randomization": "none",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"puzzle_randomization": "umbra_variety",
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
common_options = {
|
common_options = {
|
||||||
@@ -63,12 +66,15 @@ class TestSymbolRequirementsMultiworld(WitnessMultiworldTestBase):
|
|||||||
self.assertFalse(self.get_items_by_name("Arrows", 1))
|
self.assertFalse(self.get_items_by_name("Arrows", 1))
|
||||||
self.assertTrue(self.get_items_by_name("Arrows", 2))
|
self.assertTrue(self.get_items_by_name("Arrows", 2))
|
||||||
self.assertFalse(self.get_items_by_name("Arrows", 3))
|
self.assertFalse(self.get_items_by_name("Arrows", 3))
|
||||||
|
self.assertTrue(self.get_items_by_name("Arrows", 4))
|
||||||
|
|
||||||
with self.subTest("Test that Discards ask for Triangles in normal, but Arrows in expert."):
|
with self.subTest("Test that Discards ask for Triangles in normal, but Arrows in expert."):
|
||||||
desert_discard = "0x17CE7"
|
desert_discard = "0x17CE7"
|
||||||
triangles = frozenset({frozenset({"Triangles"})})
|
triangles = frozenset({frozenset({"Triangles"})})
|
||||||
arrows = frozenset({frozenset({"Arrows"})})
|
arrows = frozenset({frozenset({"Arrows"})})
|
||||||
|
both = frozenset({frozenset({"Triangles", "Arrows"})})
|
||||||
|
|
||||||
self.assertEqual(self.multiworld.worlds[1].player_logic.REQUIREMENTS_BY_HEX[desert_discard], triangles)
|
self.assertEqual(self.multiworld.worlds[1].player_logic.REQUIREMENTS_BY_HEX[desert_discard], triangles)
|
||||||
self.assertEqual(self.multiworld.worlds[2].player_logic.REQUIREMENTS_BY_HEX[desert_discard], arrows)
|
self.assertEqual(self.multiworld.worlds[2].player_logic.REQUIREMENTS_BY_HEX[desert_discard], arrows)
|
||||||
self.assertEqual(self.multiworld.worlds[3].player_logic.REQUIREMENTS_BY_HEX[desert_discard], triangles)
|
self.assertEqual(self.multiworld.worlds[3].player_logic.REQUIREMENTS_BY_HEX[desert_discard], triangles)
|
||||||
|
self.assertEqual(self.multiworld.worlds[4].player_logic.REQUIREMENTS_BY_HEX[desert_discard], both)
|
||||||
|
|||||||
Reference in New Issue
Block a user