The Witness: Make "Early Good Item" an OptionSet allowing Symbols, Doors and Obelisk Keys (#3804)

* New solution to that plando issue

* better

* Another warning

* better comment

* Best of both worlds I guess?

* oops

* Smarter code reuse

* better comment

* oop

* lint

* mypy

* player_name

* add unit test

* oh

* Rebrand time baby

* This fits on one line

* oop

* Update worlds/witness/__init__.py

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

* mypy

* Reorganize some doors according to medic's suggestions

* This should make it much faster (Thanks Medic)

* Town Doors works because of Church being a check always, Church Entry feels really bad tho

* Only add Desert Entry if there are no control panels stopping you

* No overlap here, so why is it a set

* Idk everything's kinda good without symbol shuffle

* Just make sure, I guess

* This makes way more sense doesn't it

* Oh, this is probably important

* oop

* loc

* oops 2

* ruff

* that was already in there

* Change the door picking a bit further

* some renaming

* slight wording change

* Fix

* Move it all to a new file

* ruff

* mypy

* .

* Make sure we're only adding as many tutorial checks as necessary

* ruff

* These checks aren't necessary, as the final list gets culled to only existing items anyway. It saves CPU cycles, but this is nicer for future compatibility

* Special handling for caves shortcuts

* 120 chars

* Update worlds/witness/place_early_item.py

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

* Clean up Windmill & Theater cases

* Make early_symbol_item removed instead

* Add early_good_item to presets

* replace double None checks with casts

* That doesn't exist anymore

* Mypy thing

* Update the doors again a bit

* Pycharm pls

* ruff

* forgot one

* oop

* Is it finally right?

* Update options.py

* Fix with new Panel Keys

* Hopefully fix crash when one of the types runs out when the others haven't yet

* oops

* Medic suggestion

* unused import

* Update place_early_item.py

* Update __init__.py

* Update __init__.py

* Update options.py

* Add possible types to option desc

* Make that include all Tutorial (Inside) checks

* Update __init__.py

* Update test_early_good_item.py

* Update test_early_good_item.py

* Slight cleanup

* fix no tutorial locations being picked up by tutorial location size check

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
This commit is contained in:
NewSoupVi
2026-04-18 12:52:38 +01:00
committed by GitHub
parent 0a742b6c98
commit a6c1347102
10 changed files with 445 additions and 70 deletions

View File

@@ -2,10 +2,10 @@
Archipelago init file for The Witness
"""
import dataclasses
from logging import error, warning
from logging import error, info, warning
from typing import Any, Dict, List, Optional, cast
from BaseClasses import CollectionState, Entrance, Location, LocationProgressType, Region, Tutorial
from BaseClasses import CollectionState, Entrance, Item, Location, LocationProgressType, Region, Tutorial
from Options import OptionError, PerGameCommonOptions, Toggle
from worlds.AutoWorld import WebWorld, World
@@ -18,6 +18,7 @@ from .data.utils import cast_not_none, get_audio_logs
from .hints import CompactHintData, create_all_hints, make_compact_hint_data, make_laser_hints
from .locations import WitnessPlayerLocations
from .options import TheWitnessOptions, witness_option_groups
from .place_early_item import place_early_items
from .player_items import WitnessItem, WitnessPlayerItems
from .player_logic import WitnessPlayerLogic
from .presets import witness_option_presets
@@ -94,6 +95,7 @@ class WitnessWorld(World):
items_placed_early: List[str]
own_itempool: List[WitnessItem]
reachable_early_locations: List[str]
panel_hunt_required_count: int
def _get_slot_data(self) -> Dict[str, Any]:
@@ -218,25 +220,6 @@ class WitnessWorld(World):
self.own_itempool.append(dog_puzzle_skip)
self.items_placed_early.append("Puzzle Skip")
if self.options.early_symbol_item:
# Pick an early item to place on the tutorial gate.
early_items = [
item for item in self.player_items.get_early_items() if item in self.player_items.get_mandatory_items()
]
if early_items:
random_early_item = self.random.choice(early_items)
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.
gate_item = self.create_item(random_early_item)
self.get_location("Tutorial Gate Open").place_locked_item(gate_item)
self.own_itempool.append(gate_item)
self.items_placed_early.append(random_early_item)
# There are some really restrictive options in The Witness.
# They are rarely played, but when they are, we add some extra sphere 1 locations.
# This is done both to prevent generation failures, but also to make the early game less linear.
@@ -245,20 +228,39 @@ class WitnessWorld(World):
state = CollectionState(self.multiworld)
state.sweep_for_advancements(locations=event_locations)
num_early_locs = sum(
1 for loc in self.multiworld.get_reachable_locations(state, self.player)
if loc.address and not loc.item
# Adjust the needed size for sphere 1 based on how restrictive the options are in terms of items
early_locations = [
location for location in self.multiworld.get_reachable_locations(state, self.player)
if not location.is_event and not location.item
]
self.reachable_early_locations = [location.name for location in early_locations]
num_reachable_tutorial_locations = sum(
static_witness_logic.ALL_REGIONS_BY_NAME[
cast(Region, location.parent_region).name
].area.name == "Tutorial (Inside)"
for location in early_locations
)
# Adjust the needed size for sphere 1 based on how restrictive the options are in terms of items
needed_size = 2
needed_size += self.options.puzzle_randomization == "sigma_expert"
needed_size += self.options.shuffle_symbols
needed_size += self.options.shuffle_doors != "off"
needed_size_overall = 2
needed_size_overall += self.options.puzzle_randomization == "sigma_expert"
needed_size_overall += self.options.shuffle_symbols
needed_size_overall += self.options.shuffle_doors != "off"
needed_size_to_hold_tutorial_items = len(
self.player_items.get_early_items(set(self.player_items.get_mandatory_items()))
)
# Then, add checks in order until the required amount of sphere 1 checks is met.
extra_tutorial_checks = [
("Tutorial First Hallway Room", "Tutorial First Hallway Bend"),
("Tutorial First Hallway", "Tutorial First Hallway Straight")
]
extra_checks = [
("Tutorial First Hallway Room", "Tutorial First Hallway Bend"),
("Tutorial First Hallway", "Tutorial First Hallway Straight"),
@@ -266,13 +268,30 @@ class WitnessWorld(World):
("Desert Outside", "Desert Surface 2"),
]
for i in range(num_early_locs, needed_size):
for _ in range(num_reachable_tutorial_locations, needed_size_to_hold_tutorial_items):
if not extra_tutorial_checks:
break
region, loc = extra_tutorial_checks.pop(0)
extra_checks.pop(0)
self.player_locations.add_location_late(loc)
self.get_region(region).add_locations({loc: self.location_name_to_id[loc]}, WitnessLocation)
self.reachable_early_locations.append(loc)
player = self.player_name
info(
f"""Location "{loc}" had to be added to {player}'s world to hold the requested early good items."""
)
for _ in range(len(self.reachable_early_locations), needed_size_overall):
if not extra_checks:
break
region, loc = extra_checks.pop(0)
self.player_locations.add_location_late(loc)
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]}, WitnessLocation)
self.reachable_early_locations.append(loc)
warning(
f"""Location "{loc}" had to be added to {self.player_name}'s world
@@ -341,6 +360,10 @@ class WitnessWorld(World):
self.own_itempool += new_items
self.multiworld.itempool += new_items
def fill_hook(self, progitempool: List[Item], _: List[Item], _2: List[Item],
fill_locations: List[Location]) -> None:
place_early_items(self, progitempool, fill_locations)
def fill_slot_data(self) -> Dict[str, Any]:
already_hinted_locations = set()

View File

@@ -72,5 +72,5 @@ class WitnessPlayerLocations:
def add_location_late(self, entity_name: str) -> None:
entity_hex = static_witness_logic.ENTITIES_BY_NAME[entity_name]["entity_hex"]
self.CHECK_LOCATION_TABLE[entity_hex] = static_witness_locations.get_id(entity_hex)
self.CHECK_LOCATION_TABLE[entity_name] = static_witness_locations.get_id(entity_hex)
self.CHECK_PANELHEX_TO_ID[entity_hex] = static_witness_locations.get_id(entity_hex)

View File

@@ -13,6 +13,7 @@ from Options import (
OptionSet,
PerGameCommonOptions,
Range,
Removed,
Toggle,
Visibility,
)
@@ -50,12 +51,19 @@ class EarlyCaves(Choice):
alias_on = 2
class EarlySymbolItem(DefaultOnToggle):
class EarlyGoodItems(OptionSet):
"""
Put a random helpful symbol item on an early check, specifically Tutorial Gate Open if it is available early.
Put one random helpful item of each of the chosen types on an early check, specifically a sphere 1 Tutorial location.
If a type is chosen, but no items of that type exist in the itempool, it is skipped.
The possible types are: "Symbol", "Door / Door Panel", "Obelisk Key".
If there aren't enough sphere 1 Tutorial locations, Tutorial First Hallway Straight and Tutorial First Hallway Bend may be added as locations.
If there still aren't enough sphere 1 Tutorial locations, a random local sphere 1 location is picked.
If no local sphere 1 locations are available, there are no further attempts to place the item.
"""
visibility = Visibility.none
valid_keys = {"Symbol", "Door / Door Panel", "Obelisk Key"}
default = frozenset({"Symbol"})
class ShuffleSymbols(DefaultOnToggle):
@@ -550,7 +558,7 @@ class TheWitnessOptions(PerGameCommonOptions):
panel_hunt_discourage_same_area_factor: PanelHuntDiscourageSameAreaFactor
panel_hunt_plando: PanelHuntPlando
early_caves: EarlyCaves
early_symbol_item: EarlySymbolItem
early_good_items: EarlyGoodItems
elevators_come_to_you: ElevatorsComeToYou
trap_percentage: TrapPercentage
trap_weights: TrapWeights
@@ -565,6 +573,8 @@ class TheWitnessOptions(PerGameCommonOptions):
shuffle_dog: ShuffleDog
easter_egg_hunt: EasterEggHunt
early_symbol_item: Removed
witness_option_groups = [
OptionGroup("Puzzles & Goal", [
@@ -611,6 +621,7 @@ witness_option_groups = [
LaserHints
]),
OptionGroup("Misc", [
EarlyGoodItems,
EarlyCaves,
ElevatorsComeToYou,
DeathLink,

View File

@@ -0,0 +1,151 @@
from logging import debug, error
from typing import TYPE_CHECKING, Dict, List, Set, Tuple
from BaseClasses import CollectionState, Item, Location, LocationProgressType
from .data import static_logic as static_witness_logic
from .data.utils import cast_not_none
if TYPE_CHECKING:
from . import WitnessWorld
def get_available_early_locations(world: "WitnessWorld") -> List[Location]:
# Pick an early item to put on Tutorial Gate Open.
# Done after plando to avoid conflicting with it.
# Done in fill_hook because multiworld itempool manipulation is not allowed in pre_fill.
# Prioritize Tutorial locations in a specific order
tutorial_checks_in_order = [
"Tutorial Gate Open",
"Tutorial Back Left",
"Tutorial Back Right",
"Tutorial Front Left",
"Tutorial First Hallway Straight",
"Tutorial First Hallway Bend",
"Tutorial Patio Floor",
"Tutorial First Hallway EP",
"Tutorial Cloud EP",
"Tutorial Patio Flowers EP",
]
available_locations = [
world.get_location(location_name) for location_name in tutorial_checks_in_order
if location_name in world.reachable_early_locations # May not actually be sphere 1 (e.g. Obelisk Keys for EPs)
]
# Then, add the rest of sphere 1 in "game order"
available_locations += sorted(
(
world.get_location(location_name) for location_name in world.reachable_early_locations
if location_name not in tutorial_checks_in_order
),
key=lambda location_object: static_witness_logic.ENTITIES_BY_NAME[location_object.name]["order"]
)
return [
location for location in available_locations
if not location.item and location.progress_type != LocationProgressType.EXCLUDED
]
def get_eligible_items_by_type_in_random_order(world: "WitnessWorld") -> Dict[str, List[str]]:
eligible_early_items_by_type = world.player_items.get_early_items({item.name for item in world.own_itempool})
for item_list in eligible_early_items_by_type.values():
world.random.shuffle(item_list)
return eligible_early_items_by_type
def grab_own_items_from_itempool(world: "WitnessWorld", itempool: List[Item], ids_to_find: Set[int]) -> List[Item]:
found_early_items = []
def keep_or_take_out(item: Item) -> bool:
if item.code not in ids_to_find:
return True # Keep
ids_to_find.remove(item.code)
found_early_items.append(item)
return False # Take out
local_player = world.player
itempool[:] = [item for item in itempool if item.player != local_player or keep_or_take_out(item)]
return found_early_items
def place_items_onto_locations(world: "WitnessWorld", items: List[Item],
locations: List[Location]) -> Tuple[List[Item], List[Item]]:
fake_state = CollectionState(world.multiworld)
placed_items = []
unplaced_items = []
for item in items:
location = next(
(location for location in locations if location.can_fill(fake_state, item, check_access=False)),
None,
)
if location is not None:
location.place_locked_item(item)
placed_items.append(item)
locations.remove(location)
else:
unplaced_items.append(item)
return placed_items, unplaced_items
def place_early_items(world: "WitnessWorld", prog_itempool: List[Item], fill_locations: List[Location]) -> None:
if not world.options.early_good_items.value:
return
# Get a list of good early locations in a determinstic order
eligible_early_locations = get_available_early_locations(world)
# Get a list of good early items of each desired item type
eligible_early_items_by_type = get_eligible_items_by_type_in_random_order(world)
if not eligible_early_items_by_type:
return
while any(eligible_early_items_by_type.values()) and eligible_early_locations:
# Get one item of each type
next_findable_items_dict = {
item_list.pop(): item_type
for item_type, item_list in eligible_early_items_by_type.items()
if item_list
}
# Get their IDs as a set
next_findable_item_ids = {world.item_name_to_id[item_name] for item_name in next_findable_items_dict}
# Grab items from itempool
found_early_items = grab_own_items_from_itempool(world, prog_itempool, next_findable_item_ids)
# Bring found items back into Symbol -> Door -> Obelisk Key order
# The intent is that the Symbol is always on Tutorial Gate Open / generally that the order is predictable
correct_order = {item_name: i for i, item_name in enumerate(next_findable_items_dict)}
found_early_items.sort(key=lambda item: correct_order[item.name])
# Place found early items on eligible early locations.
placed_items, unplaced_items = place_items_onto_locations(world, found_early_items, eligible_early_locations)
for item in placed_items:
debug(f"Placed early good item {item} on early location {item.location}.")
# Item type is satisfied
del eligible_early_items_by_type[next_findable_items_dict[item.name]]
fill_locations.remove(cast_not_none(item.location))
for item in unplaced_items:
debug(f"Could not find a suitable placement for item {item}.")
unfilled_types = list(eligible_early_items_by_type)
if unfilled_types:
if not eligible_early_locations:
error(
f'Could not find a suitable location for "early good items" of types {unfilled_types} in '
f"{world.player_name}'s world. They are excluded or already contain plandoed items.\n"
)
else:
error(
f"Could not find any \"early good item\" of types {unfilled_types} in {world.player_name}'s world, "
"they were all plandoed elsewhere."
)

View File

@@ -198,36 +198,118 @@ class WitnessPlayerItems:
return output
def get_early_items(self) -> List[str]:
def get_early_items(self, existing_items: Set[str]) -> Dict[str, List[str]]:
"""
Returns items that are ideal for placing on extremely early checks, like the tutorial gate.
"""
output: Set[str] = set()
if self._world.options.shuffle_symbols:
discards_on = self._world.options.shuffle_discarded_panels
mode = self._world.options.puzzle_randomization.current_key
output: Dict[str, List[str]] = {}
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]
if "Symbol" in self._world.options.early_good_items.value:
good_symbols = ["Dots", "Black/White Squares", "Symmetry", "Shapers", "Stars"]
# 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
# regardless of whether or not they actually wind up being manually placed.
for plando_setting in self._world.options.plando_items:
if plando_setting.from_pool:
if isinstance(plando_setting.items, dict):
output -= {item for item, weight in plando_setting.items.items() if weight}
if self._world.options.shuffle_discarded_panels:
if self._world.options.puzzle_randomization == "sigma_expert":
good_symbols.append("Arrows")
else:
# Assume this is some other kind of iterable.
for inner_item in plando_setting.items:
if isinstance(inner_item, str):
output -= {inner_item}
elif isinstance(inner_item, dict):
output -= {item for item, weight in inner_item.items() if weight}
good_symbols.append("Triangles")
# Sort the output for consistency across versions if the implementation changes but the logic does not.
return sorted(output)
# Replace progressive items with their parents.
good_symbols = [
static_witness_logic.get_parent_progressive_item(item) for item in good_symbols
]
output["Symbol"] = [symbol for symbol in good_symbols if symbol in existing_items]
if "Door / Door Panel" in self._world.options.early_good_items.value:
good_doors = [
"Desert Doors & Elevator", "Keep Hedge Maze Doors", "Keep Pressure Plates Doors",
"Shadows Lower Doors", "Tunnels Doors", "Quarry Stoneworks Doors",
"Keep Tower Shortcut (Door)", "Shadows Timed Door", "Tunnels Town Shortcut (Door)",
"Quarry Stoneworks Roof Exit (Door)",
"Desert Panels", "Keep Hedge Maze Panels",
"Shadows Door Timer (Panel)",
]
# While Caves Shortcuts don't unlock anything in symbol shuffle, you'd still rather have them early.
# But, we need to do some special handling for them.
if self._world.options.shuffle_doors in ("doors", "mixed"):
# Caves Shortcuts might exist in vanilla/panel doors because of "early caves: add to pool".
# But if the player wanted them early, they would have chosen "early caves: starting inventory".
# This is why we make sure these are "natural" Caves Shortcuts.
good_doors.append("Caves Shortcuts")
# These two doors are logically equivalent, so we choose a random one as to not give them twice the chance.
good_doors.append(
self._world.random.choice(["Caves Mountain Shortcut (Door)", "Caves Swamp Shortcut (Door)"])
)
if self._world.options.shuffle_vault_boxes and not self._world.options.disable_non_randomized_puzzles:
good_doors.append("Windmill & Theater Doors")
if not self._world.options.shuffle_symbols:
good_doors += [
"Windmill & Theater Panels",
"Windmill & Theater Control Panels",
]
if self._world.options.shuffle_doors == "doors": # It's not as good in mixed doors because of Light Control
good_doors.append("Desert Light Room Entry (Door)")
if not self._world.options.shuffle_symbols:
good_doors += [
"Bunker Doors", "Swamp Doors", "Glass Factory Doors", "Town Doors",
"Bunker Entry (Door)", "Glass Factory Entry (Door)", "Symmetry Island Lower (Door)",
"Bunker Panels", "Swamp Panels", "Quarry Outside Panels", "Glass Factory Panels"
"Glass Factory Entry (Panel)",
]
existing_doors = [door for door in good_doors if door in existing_items]
# On some options combinations with doors, there just aren't a lot of doors that unlock much early.
# In this case, we add some doors that aren't great, but are at least guaranteed to unlock 1 location.
fallback_doors = [
"Keep Shadows Shortcut (Door)", # Always Keep Shadows Shortcut Panel
"Keep Shortcuts", # -"-
"Keep Pressure Plates Doors", # -"-
"Keep Hedge Maze 1 (Panel)", # Always Hedge 1
"Town Maze Stairs (Panel)", # Always Maze Panel
"Shadows Laser Room Doors", # Always Shadows Laser Panel
"Swamp Laser Shortcut (Door)", # Always Swamp Laser
"Town Maze Panels", # Always Town Maze Panel
"Town Doors", # Always Town Church Lattice
"Town Church Entry (Door)", # -"-
"Town Tower Doors", # Always Town Laser
]
self._world.random.shuffle(fallback_doors)
while len(existing_doors) < 4 and fallback_doors:
fallback_door = fallback_doors.pop()
if fallback_door in existing_items and fallback_door not in existing_doors:
existing_doors.append(fallback_door)
output["Door"] = existing_doors
if "Obelisk Key" in self._world.options.early_good_items.value:
obelisk_keys = [
"Desert Obelisk Key", "Town Obelisk Key", "Quarry Obelisk Key",
"Treehouse Obelisk Key", "Monastery Obelisk Key", "Mountainside Obelisk Key"
]
output["Obelisk Key"] = [key for key in obelisk_keys if key in existing_items]
assert all(item in self._world.item_names for sublist in output.values() for item in sublist), (
[item for sublist in output.values() for item in sublist if item not in self._world.item_names]
)
# Cull empty lists
return {item_type: item_list for item_type, item_list in output.items() if item_list}
def get_door_item_ids_in_pool(self) -> List[int]:
"""

View File

@@ -35,7 +35,7 @@ witness_option_presets: Dict[str, Dict[str, Any]] = {
"challenge_lasers": 11,
"early_caves": EarlyCaves.option_off,
"early_good_items": {"Door / Door Panel"},
"elevators_come_to_you": ElevatorsComeToYou.default,
"trap_percentage": TrapPercentage.default,
@@ -76,7 +76,7 @@ witness_option_presets: Dict[str, Dict[str, Any]] = {
"challenge_lasers": 9,
"early_caves": EarlyCaves.option_off,
"early_good_items": {"Symbol", "Door / Door Panel"}, # Not Obelisk Key bc I want EPs to open slowly in this one
"elevators_come_to_you": ElevatorsComeToYou.default,
"trap_percentage": TrapPercentage.default,
@@ -117,7 +117,7 @@ witness_option_presets: Dict[str, Dict[str, Any]] = {
"challenge_lasers": 9,
"early_caves": EarlyCaves.option_off,
"early_good_items": {"Symbol", "Door / Door Panel", "Obelisk Key"},
"elevators_come_to_you": ElevatorsComeToYou.valid_keys,
"trap_percentage": TrapPercentage.default,

View File

@@ -8,7 +8,7 @@ class TestDisableNonRandomized(WitnessTestBase):
options = {
"disable_non_randomized_puzzles": True,
"shuffle_doors": "panels",
"early_symbol_item": False,
"early_good_items": {},
}
def test_locations_got_disabled_and_alternate_activation_triggers_work(self) -> None:

View File

@@ -0,0 +1,109 @@
from BaseClasses import ItemClassification, LocationProgressType
from Fill import distribute_items_restrictive
from ..data.utils import cast_not_none
from ..test.bases import WitnessTestBase
class TestEarlySymbolItemFalse(WitnessTestBase):
options = {
"early_good_items": {},
"shuffle_symbols": True,
"shuffle_doors": "off",
"shuffle_boat": False,
"shuffle_lasers": False,
"obelisk_keys": False,
}
def setUp(self) -> None:
if self.auto_construct:
self.world_setup(seed=1) # Magic seed to prevent false positive
def test_early_good_item(self) -> None:
distribute_items_restrictive(self.multiworld)
gate_open = self.multiworld.get_location("Tutorial Gate Open", 1)
self.assertFalse(
cast_not_none(gate_open.item).classification & ItemClassification.progression,
"Early Good Item was off, yet a Symbol item ended up on Tutorial Gate Open.",
)
class TestEarlySymbolItemTrue(WitnessTestBase):
options = {
"early_good_items": {"Symbol", "Door / Door Panel", "Obelisk Key"},
"shuffle_symbols": True,
"shuffle_doors": "panels",
"shuffle_EPs": "individual",
"obelisk_keys": True,
}
def setUp(self) -> None:
super().setUp()
def test_early_good_item(self) -> None:
"""
The items should be in the order:
Symbol item on Tutorial Gate Open
Door item on Tutorial Back Left
Obelisk Key on Tutorial Back Right
"""
distribute_items_restrictive(self.multiworld)
gate_open = self.multiworld.get_location("Tutorial Gate Open", 1)
self.assertTrue(gate_open.item is not None, "Somehow, no item got placed on Tutorial Gate Open.")
self.assertTrue(
cast_not_none(gate_open.item).name in self.world.item_name_groups["Symbols"],
"Early Good Item was on, yet no Symbol item ended up on Tutorial Gate Open.",
)
back_left = self.multiworld.get_location("Tutorial Back Left", 1)
self.assertTrue(back_left.item is not None, "Somehow, no item got placed on Tutorial Back Left.")
doors_and_panel_keys = self.world.item_name_groups["Doors"] | self.world.item_name_groups["Panel Keys"]
self.assertTrue(
cast_not_none(back_left.item).name in doors_and_panel_keys,
"Early Good Item was on, yet no Door item ended up on Tutorial Back Left.",
)
back_right = self.multiworld.get_location("Tutorial Back Right", 1)
self.assertTrue(back_right.item is not None, "Somehow, no item got placed on Tutorial Back Right.")
self.assertTrue(
cast_not_none(back_right.item).name in self.world.item_name_groups["Obelisk Keys"],
"Early Good Item was on, yet no Obelisk Key item ended up on Tutorial Back Right.",
)
class TestEarlySymbolItemTrueButExcluded(WitnessTestBase):
options = {
"shuffle_symbols": True,
"shuffle_doors": "off",
"shuffle_boat": False,
"shuffle_lasers": False,
"obelisk_keys": False,
}
def setUp(self) -> None:
super().setUp()
self.multiworld.get_location("Tutorial Gate Open", 1).progress_type = LocationProgressType.EXCLUDED
def test_early_good_item(self) -> None:
distribute_items_restrictive(self.multiworld)
gate_open = self.multiworld.get_location("Tutorial Gate Open", 1)
self.assertTrue(gate_open.item is not None, "Somehow, no item got placed on Tutorial Gate Open.")
self.assertFalse(
cast_not_none(gate_open.item).classification & ItemClassification.progression,
"Tutorial Gate Open was excluded, yet it still received an early Symbol item.",
)

View File

@@ -7,7 +7,7 @@ class TestSymbolsRequiredToWinElevatorNormal(WitnessTestBase):
"puzzle_randomization": "sigma_normal",
"mountain_lasers": 1,
"victory_condition": "elevator",
"early_symbol_item": False,
"early_good_items": {},
}
def test_symbols_to_win(self) -> None:
@@ -37,7 +37,7 @@ class TestSymbolsRequiredToWinElevatorExpert(WitnessTestBase):
"shuffle_lasers": True,
"mountain_lasers": 1,
"victory_condition": "elevator",
"early_symbol_item": False,
"early_good_items": {},
"puzzle_randomization": "sigma_expert",
}
@@ -70,7 +70,7 @@ class TestSymbolsRequiredToWinElevatorVanilla(WitnessTestBase):
"shuffle_lasers": True,
"mountain_lasers": 1,
"victory_condition": "elevator",
"early_symbol_item": False,
"early_good_items": {},
"puzzle_randomization": "none",
}
@@ -101,7 +101,6 @@ class TestSymbolsRequiredToWinElevatorVariety(WitnessTestBase):
"shuffle_lasers": True,
"mountain_lasers": 1,
"victory_condition": "elevator",
"early_symbol_item": False,
"puzzle_randomization": "umbra_variety",
}
@@ -134,7 +133,7 @@ class TestPanelsRequiredToWinElevator(WitnessTestBase):
"shuffle_lasers": True,
"mountain_lasers": 1,
"victory_condition": "elevator",
"early_symbol_item": False,
"early_good_items": {},
"shuffle_symbols": False,
"shuffle_doors": "panels",
"door_groupings": "off",
@@ -163,7 +162,7 @@ class TestDoorsRequiredToWinElevator(WitnessTestBase):
"shuffle_lasers": True,
"mountain_lasers": 1,
"victory_condition": "elevator",
"early_symbol_item": False,
"early_good_items": {},
"shuffle_symbols": False,
"shuffle_doors": "doors",
"door_groupings": "off",

View File

@@ -3,7 +3,7 @@ from ..test.bases import WitnessMultiworldTestBase, WitnessTestBase
class TestSymbols(WitnessTestBase):
options = {
"early_symbol_item": False,
"early_good_items": {},
}
def test_progressive_symbols(self) -> None:
@@ -53,7 +53,7 @@ class TestSymbolRequirementsMultiworld(WitnessMultiworldTestBase):
common_options = {
"shuffle_discarded_panels": True,
"early_symbol_item": False,
"early_good_items": {},
}
def test_arrows_exist_and_are_required_in_expert_seeds_only(self) -> None: