Compare commits

...

8 Commits

Author SHA1 Message Date
NewSoupVi
d729b00b22 CODEOWNERS: Move OoT to "unmaintained"
https://discord.com/channels/731205301247803413/1214608557077700720/1253206955879694336

Espeon might come back, but still, this world acts as unmaintained right now, so we should make this change, and then change it back if/when he's back.

@espeon65536 Just so you're aware of this change as well
2024-09-06 18:35:50 +02:00
Danaël V.
f9fc6944d3 Docs: Removing #archipelago-dev from places (#3876)
* Cleaning up (#4)

Cleanup

* Changed channel name

* Changed channel name

* Update docs/world maintainer.md

* Update docs/world maintainer.md
2024-09-05 22:55:19 +02:00
qwint
e984583e5e HK: speed up collect (a bit) (#3886)
* speed up collect, will be obsolete after #3786

* vi's a meanie
2024-09-05 21:19:37 +02:00
Exempt-Medic
7e03a87608 DOCS: Option Visibility and removing SpecialRange (#3889)
* Update options api.md

* Update options api.md

* Update docs/options api.md

Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>

---------

Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>
2024-09-05 21:18:58 +02:00
NewSoupVi
456bc481a3 Docs: Specify process for adding a world maintainer to an existing world (#3884)
* Docs: Specify process for adding a world maintainer to an existing world

* Update world maintainer.md

* Update world maintainer.md

* Update world maintainer.md

* Update world maintainer.md

* Update world maintainer.md

* Update world maintainer.md

* Update world maintainer.md

* Update world maintainer.md

* Update world maintainer.md

* Update world maintainer.md

* Update world maintainer.md

* Rewrite by BadMagic

* Update docs/world maintainer.md

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-09-05 21:16:44 +02:00
NewSoupVi
b4752cd32d The Witness: Implement "Variety" puzzles mode (#3239)
* Variety Rando (But WitnessLogicVariety.txt is wrong

* Actually variety the variety file (Ty Exempt-Medic <3)

* This will be preopened

* Tooltip explaining the different difficulties

* Remove ?, those were correct

* Less efficient but easier to follow

* Parentheses

* Fix some reqs

* Not Arrows in Variety

* Oops

* Happy medic, I made a wacky solution

* there we go

* Lint oops

* There

* that copy is unnecessary

* Turns out that copy is necessary still

* yes

* lol

* Rename to Umbra Variety

* missed one

* Erase the Eraser

* Fix remaining instances of 'variety' and don't have a symbol item on the gate in variety

* reorder difficulties

* inbetween

* ruff

* Fix Variety Invis requirements

* Fix wooden beams variety

* Fix PP2 variety

* Mirror changes from 'Variety Mode Puzzle Change 3.2.3'

* These also have Symmetry

* merge error prevention

* Update worlds/witness/data/static_items.py

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* no elif after return

* add variety to the symbol requirement bleed test

* Add variety to one of the 'other settings' unit tests

* Add Variety minimal symbols unittest

* oops

* I did the dumb again

* .

* Incorporate changes from other PR into WitnesLogicVariety.txt

* Update worlds/witness/data/WitnessLogicVariety.txt

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* Update worlds/witness/data/WitnessLogicVariety.txt

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* Update the reqs as well haha

* Another difference, thanks Medic :§

* Wait no, this one was right

* lol

* apply changes to WitnessLogicVariety.txt

* Add most recent Variety changes

* oof

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-09-05 17:10:09 +02:00
Fabian Dill
ceec51b9e1 Core: Region handling customization (#3682) 2024-09-05 16:32:45 +02:00
NewSoupVi
d3312287a8 Docs: Mention indirect_conditions and that they are a *hard requirement* (with a few sharp exception cases) (#3552)
* Docs: Mention indirect_conditions and that they are a *hard requirement* (with hard exception cases)

I definitely don't feel like I wrote this in the best way, or in the best place, but it is a precedent that I think is necessary so we can treat it as "the law of the land".

* oops

* Update world api.md

* Update world api.md

* Update world api.md

* Update docs/world api.md

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* I like within more here

* Update docs/world api.md

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* Update world api.md

---------

Co-authored-by: Scipio Wright <scipiowright@gmail.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-09-05 13:53:34 +02:00
24 changed files with 1429 additions and 55 deletions

View File

@@ -692,17 +692,25 @@ class CollectionState():
def update_reachable_regions(self, player: int):
self.stale[player] = False
world: AutoWorld.World = self.multiworld.worlds[player]
reachable_regions = self.reachable_regions[player]
blocked_connections = 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
if start not in reachable_regions:
reachable_regions.add(start)
blocked_connections.update(start.exits)
self.blocked_connections[player].update(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
while queue:
connection = queue.popleft()
@@ -722,6 +730,29 @@ class CollectionState():
if new_entrance in blocked_connections and new_entrance not in queue:
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:
ret = CollectionState(self.multiworld)
ret.prog_items = {player: counter.copy() for player, counter in self.prog_items.items()}

View File

@@ -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:
[/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.

View File

@@ -118,9 +118,6 @@
# Noita
/worlds/noita/ @ScipioWright @heinermann
# Ocarina of Time
/worlds/oot/ @espeon65536
# Old School Runescape
/worlds/osrs @digiholic
@@ -230,6 +227,9 @@
# Links Awakening DX
# /worlds/ladx/
# Ocarina of Time
# /worlds/oot/
## Disabled Unmaintained Worlds
# The following worlds in this repo are currently unmaintained and disabled as they do not work in core. If you are

View File

@@ -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
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
`(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`.
However, you can override `from_text` and handle `text == "random"` to customize its behavior or
implement it for additional option types.
@@ -129,6 +129,23 @@ class Difficulty(Choice):
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
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

View File

@@ -38,7 +38,7 @@ Recommended steps
* 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.
* 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/)
* Run Generate.py which will prompt installation of missing modules, press enter to confirm

View File

@@ -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`
(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
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
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
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).

View File

@@ -26,8 +26,17 @@ Unless these are shared between multiple people, we expect the following from ea
### Adding a World
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
in the [CODEOWNERS](/docs/CODEOWNERS) document.
nominate someone else (i.e. there are multiple devs).
### 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
@@ -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.
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.
Voting shall be conducted on Discord in #archipelago-dev.
Voting shall be conducted on Discord in #ap-core-dev.
## 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.
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.
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.
## Handling of Unmaintained Worlds

View File

@@ -292,6 +292,14 @@ class World(metaclass=AutoWorldRegister):
web: ClassVar[WebWorld] = WebWorld()
"""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"
"""autoset on creation. The MultiWorld object for the currently generating multiworld."""
player: int

View File

@@ -101,6 +101,7 @@ class Factorio(World):
tech_tree_layout_prerequisites: typing.Dict[FactorioScienceLocation, typing.Set[FactorioScienceLocation]]
tech_mix: int = 0
skip_silo: bool = False
origin_region_name = "Nauvis"
science_locations: typing.List[FactorioScienceLocation]
settings: typing.ClassVar[FactorioSettings]
@@ -125,9 +126,6 @@ class Factorio(World):
def create_regions(self):
player = self.player
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)
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)
location.place_locked_item(event)
crash.connect(nauvis)
self.multiworld.regions += [menu, nauvis]
self.multiworld.regions.append(nauvis)
def create_items(self) -> None:
player = self.player

View File

@@ -601,11 +601,11 @@ class HKWorld(World):
if change:
for effect_name, effect_value in item_effects.get(item.name, {}).items():
state.prog_items[item.player][effect_name] += effect_value
if item.name in {"Left_Mothwing_Cloak", "Right_Mothwing_Cloak"}:
if state.prog_items[item.player].get('RIGHTDASH', 0) and \
state.prog_items[item.player].get('LEFTDASH', 0):
(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)
if item.name in {"Left_Mothwing_Cloak", "Right_Mothwing_Cloak"}:
if state.prog_items[item.player].get('RIGHTDASH', 0) and \
state.prog_items[item.player].get('LEFTDASH', 0):
(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)
return change
def remove(self, state, item: HKItem) -> bool:

View File

@@ -45,7 +45,7 @@ class SubnauticaWorld(World):
options_dataclass = options.SubnauticaOptions
options: options.SubnauticaOptions
required_client_version = (0, 5, 0)
origin_region_name = "Planet 4546B"
creatures_to_scan: List[str]
def generate_early(self) -> None:
@@ -66,13 +66,9 @@ class SubnauticaWorld(World):
creature_pool, self.options.creature_scans.value)
def create_regions(self):
# Create Regions
menu_region = Region("Menu", self.player, self.multiworld)
# Create Region
planet_region = Region("Planet 4546B", self.player, self.multiworld)
# Link regions together
menu_region.connect(planet_region, "Lifepod 5")
# Create regular locations
location_names = itertools.chain((location["name"] for location in locations.location_table.values()),
(creature + creatures.suffix for creature in self.creatures_to_scan))
@@ -93,11 +89,8 @@ class SubnauticaWorld(World):
# make the goal event the victory "item"
location.item.name = "Victory"
# Register regions to multiworld
self.multiworld.regions += [
menu_region,
planet_region
]
# Register region to multiworld
self.multiworld.regions.append(planet_region)
# refer to rules.py
set_rules = set_rules

View File

@@ -204,11 +204,10 @@ class WitnessWorld(World):
]
if early_items:
random_early_item = self.random.choice(early_items)
if (
self.options.puzzle_randomization == "sigma_expert"
or self.options.victory_condition == "panel_hunt"
):
# In Expert and Panel Hunt, only tag the item as early, rather than forcing it onto the gate.
mode = self.options.puzzle_randomization
if mode == "sigma_expert" or mode == "umbra_variety" 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
self.multiworld.local_early_items[self.player][random_early_item] = 1
else:
# 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]})
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."""
)

File diff suppressed because it is too large Load Diff

View File

@@ -13,6 +13,22 @@ ITEM_GROUPS: Dict[str, Set[str]] = {}
# item list during get_progression_items.
_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:
for item_name, definition in static_witness_logic.ALL_ITEMS.items():

View File

@@ -17,6 +17,7 @@ from .utils import (
get_items,
get_sigma_expert_logic,
get_sigma_normal_logic,
get_umbra_variety_logic,
get_vanilla_logic,
logical_or_witness_rules,
parse_lambda,
@@ -292,6 +293,11 @@ def get_sigma_expert() -> StaticWitnessLogicObj:
return StaticWitnessLogicObj(get_sigma_expert_logic())
@cache_argsless
def get_umbra_variety() -> StaticWitnessLogicObj:
return StaticWitnessLogicObj(get_umbra_variety_logic())
def __getattr__(name: str) -> StaticWitnessLogicObj:
if name == "vanilla":
return get_vanilla()
@@ -299,6 +305,8 @@ def __getattr__(name: str) -> StaticWitnessLogicObj:
return get_sigma_normal()
if name == "sigma_expert":
return get_sigma_expert()
if name == "umbra_variety":
return get_umbra_variety()
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")

View File

@@ -215,6 +215,10 @@ def get_sigma_expert_logic() -> List[str]:
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]:
return get_adjustment_file("WitnessLogicVanilla.txt")

View File

@@ -53,7 +53,7 @@ def get_always_hint_items(world: "WitnessWorld") -> List[str]:
wincon = world.options.victory_condition
if discards:
if difficulty == "sigma_expert":
if difficulty == "sigma_expert" or difficulty == "umbra_variety":
always.append("Arrows")
else:
always.append("Triangles")

View File

@@ -250,10 +250,15 @@ class PanelHuntDiscourageSameAreaFactor(Range):
class PuzzleRandomization(Choice):
"""
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"
option_sigma_normal = 0
option_sigma_expert = 1
option_umbra_variety = 3
option_none = 2

View File

@@ -7,7 +7,6 @@ from typing import TYPE_CHECKING, Dict, List, Set, cast
from BaseClasses import Item, ItemClassification, MultiWorld
from .data import static_items as static_witness_items
from .data import static_logic as static_witness_logic
from .data.item_definition_classes import (
DoorItemDefinition,
ItemCategory,
@@ -155,16 +154,12 @@ class WitnessPlayerItems:
"""
output: Set[str] = set()
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:
if self._world.options.puzzle_randomization == "sigma_expert":
output.add("Arrows")
else:
output.add("Triangles")
# Replace progressive items with their parents.
output = {static_witness_logic.get_parent_progressive_item(item) for item in output}
output = static_witness_items.ALWAYS_GOOD_SYMBOL_ITEMS | static_witness_items.MODE_SPECIFIC_GOOD_ITEMS[mode]
if discards_on:
output |= static_witness_items.MODE_SPECIFIC_GOOD_DISCARD_ITEMS[mode]
# 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

View File

@@ -87,12 +87,14 @@ class WitnessPlayerLogic:
self.DIFFICULTY = world.options.puzzle_randomization
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
elif self.DIFFICULTY == "umbra_variety":
self.REFERENCE_LOGIC = static_witness_logic.umbra_variety
elif self.DIFFICULTY == "none":
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.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME

View File

@@ -30,6 +30,8 @@ class WitnessPlayerRegions:
self.reference_logic = static_witness_logic.sigma_normal
elif difficulty == "sigma_expert":
self.reference_logic = static_witness_logic.sigma_expert
elif difficulty == "umbra_variety":
self.reference_logic = static_witness_logic.umbra_variety
else:
self.reference_logic = static_witness_logic.vanilla

View File

@@ -96,6 +96,39 @@ class TestSymbolsRequiredToWinElevatorVanilla(WitnessTestBase):
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):
options = {
"shuffle_lasers": True,

View File

@@ -54,6 +54,7 @@ class TestMaxEntityShuffle(WitnessTestBase):
class TestPostgameGroupedDoors(WitnessTestBase):
options = {
"puzzle_randomization": "umbra_variety",
"shuffle_postgame": True,
"shuffle_discarded_panels": True,
"shuffle_doors": "doors",

View File

@@ -46,6 +46,9 @@ class TestSymbolRequirementsMultiworld(WitnessMultiworldTestBase):
{
"puzzle_randomization": "none",
},
{
"puzzle_randomization": "umbra_variety",
}
]
common_options = {
@@ -63,12 +66,15 @@ class TestSymbolRequirementsMultiworld(WitnessMultiworldTestBase):
self.assertFalse(self.get_items_by_name("Arrows", 1))
self.assertTrue(self.get_items_by_name("Arrows", 2))
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."):
desert_discard = "0x17CE7"
triangles = frozenset({frozenset({"Triangles"})})
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[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[4].player_logic.REQUIREMENTS_BY_HEX[desert_discard], both)