The Witness: Add some unit tests (#3328)

* Add hidden early symbol item option, make some unit tests

* Add early symbol item false to the arrows test

* I guess it's not an issue

* more tests

* assertEqual

* cleanup

* add minimum symbols test for all 3 modes

* Formatting

* Add more minimal beatability tests

* one more for the road

* I HATE THIS AAAAAAAAAAAHHHHHHHHHHH WHY DID WE GO WITH OPTIONS

* loiaqeäsdhgalikSDGHjasDÖKHGASKLDÖGHJASKLJGHJSAÖkfaöslifjasöfASGJÖASDLFGJ'sklgösLGIKsdhJLGÖsdfjälghklDASFJghjladshfgjasdfälkjghasdöLfghasd-kjgjASDLÖGHAESKDLJGJÖsdaLGJHsadöKGjFDSLAkgjölSÄDghbASDFKGjasdLJGhjLÖSDGHLJASKDkgjldafjghjÖLADSFghäasdökgjäsadjlgkjsadkLHGsaDÖLGSADGÖLwSdlgkJLwDSFÄLHBJsaöfdkHweaFGIoeWjvlkdösmVJÄlsafdJKhvjdsJHFGLsdaövhWDsköLV-ksdFJHGVöSEKD

* fix imports (within apworld needs to be relative)

* Update worlds/witness/options.py

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

* Sure

* good suggestion

* subtest

* Add some EP shuffle unit tests, also an explicit event-checking unit test

* add more tests yay

* oops

* mypy

* Update worlds/witness/options.py

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Collapse into one test :(

* More efficiency

* line length

* More collapsing

* Cleanup and docstrings

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
This commit is contained in:
NewSoupVi 2024-07-06 13:40:55 +02:00 committed by GitHub
parent bfac100567
commit f99ee77325
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 685 additions and 16 deletions

View File

@ -185,6 +185,7 @@ class WitnessWorld(World):
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()

View File

@ -2,7 +2,7 @@ from dataclasses import dataclass
from schema import And, Schema
from Options import Choice, DefaultOnToggle, OptionDict, OptionGroup, PerGameCommonOptions, Range, Toggle
from Options import Choice, DefaultOnToggle, OptionDict, OptionGroup, PerGameCommonOptions, Range, Toggle, Visibility
from .data import static_logic as static_witness_logic
from .data.item_definition_classes import ItemCategory, WeightedItemDefinition
@ -35,6 +35,14 @@ class EarlyCaves(Choice):
alias_on = 2
class EarlySymbolItem(DefaultOnToggle):
"""
Put a random helpful symbol item on an early check, specifically Tutorial Gate Open if it is available early.
"""
visibility = Visibility.none
class ShuffleSymbols(DefaultOnToggle):
"""
If on, you will need to unlock puzzle symbols as items to be able to solve the panels that contain those symbols.
@ -325,6 +333,7 @@ class TheWitnessOptions(PerGameCommonOptions):
mountain_lasers: MountainLasers
challenge_lasers: ChallengeLasers
early_caves: EarlyCaves
early_symbol_item: EarlySymbolItem
elevators_come_to_you: ElevatorsComeToYou
trap_percentage: TrapPercentage
trap_weights: TrapWeights

View File

@ -0,0 +1,161 @@
from test.bases import WorldTestBase
from test.general import gen_steps, setup_multiworld
from test.multiworld.test_multiworlds import MultiworldTestBase
from typing import Any, ClassVar, Dict, Iterable, List, Mapping, Union, cast
from BaseClasses import CollectionState, Entrance, Item, Location, Region
from .. import WitnessWorld
class WitnessTestBase(WorldTestBase):
game = "The Witness"
player: ClassVar[int] = 1
world: WitnessWorld
def can_beat_game_with_items(self, items: Iterable[Item]) -> bool:
"""
Check that the items listed are enough to beat the game.
"""
state = CollectionState(self.multiworld)
for item in items:
state.collect(item)
return state.multiworld.can_beat_game(state)
def assert_dependency_on_event_item(self, spot: Union[Location, Region, Entrance], item_name: str) -> None:
"""
WorldTestBase.assertAccessDependency, but modified & simplified to work with event items
"""
event_items = [item for item in self.multiworld.get_items() if item.name == item_name]
self.assertTrue(event_items, f"Event item {item_name} does not exist.")
event_locations = [cast(Location, event_item.location) for event_item in event_items]
# Checking for an access dependency on an event item requires a bit of extra work,
# as state.remove forces a sweep, which will pick up the event item again right after we tried to remove it.
# So, we temporarily set the access rules of the event locations to be impossible.
original_rules = {event_location.name: event_location.access_rule for event_location in event_locations}
for event_location in event_locations:
event_location.access_rule = lambda _: False
# We can't use self.assertAccessDependency here, it doesn't work for event items. (As of 2024-06-30)
test_state = self.multiworld.get_all_state(False)
self.assertFalse(spot.can_reach(test_state), f"{spot.name} is reachable without {item_name}")
test_state.collect(event_items[0])
self.assertTrue(spot.can_reach(test_state), f"{spot.name} is not reachable despite having {item_name}")
# Restore original access rules.
for event_location in event_locations:
event_location.access_rule = original_rules[event_location.name]
def assert_location_exists(self, location_name: str, strict_check: bool = True) -> None:
"""
Assert that a location exists in this world.
If strict_check, also make sure that this (non-event) location COULD exist.
"""
if strict_check:
self.assertIn(location_name, self.world.location_name_to_id, f"Location {location_name} can never exist")
try:
self.world.get_location(location_name)
except KeyError:
self.fail(f"Location {location_name} does not exist.")
def assert_location_does_not_exist(self, location_name: str, strict_check: bool = True) -> None:
"""
Assert that a location exists in this world.
If strict_check, be explicit about whether the location could exist in the first place.
"""
if strict_check:
self.assertIn(location_name, self.world.location_name_to_id, f"Location {location_name} can never exist")
self.assertRaises(
KeyError,
lambda _: self.world.get_location(location_name),
f"Location {location_name} exists, but is not supposed to.",
)
def assert_can_beat_with_minimally(self, required_item_counts: Mapping[str, int]) -> None:
"""
Assert that the specified mapping of items is enough to beat the game,
and that having one less of any item would result in the game being unbeatable.
"""
# Find the actual items
found_items = [item for item in self.multiworld.get_items() if item.name in required_item_counts]
actual_items: Dict[str, List[Item]] = {item_name: [] for item_name in required_item_counts}
for item in found_items:
if len(actual_items[item.name]) < required_item_counts[item.name]:
actual_items[item.name].append(item)
# Assert that enough items exist in the item pool to satisfy the specified required counts
for item_name, item_objects in actual_items.items():
self.assertEqual(
len(item_objects),
required_item_counts[item_name],
f"Couldn't find {required_item_counts[item_name]} copies of item {item_name} available in the pool, "
f"only found {len(item_objects)}",
)
# assert that multiworld is beatable with the items specified
self.assertTrue(
self.can_beat_game_with_items(item for items in actual_items.values() for item in items),
f"Could not beat game with items: {required_item_counts}",
)
# assert that one less copy of any item would result in the multiworld being unbeatable
for item_name, item_objects in actual_items.items():
with self.subTest(f"Verify cannot beat game with one less copy of {item_name}"):
removed_item = item_objects.pop()
self.assertFalse(
self.can_beat_game_with_items(item for items in actual_items.values() for item in items),
f"Game was beatable despite having {len(item_objects)} copies of {item_name} "
f"instead of the specified {required_item_counts[item_name]}",
)
item_objects.append(removed_item)
class WitnessMultiworldTestBase(MultiworldTestBase):
options_per_world: List[Dict[str, Any]]
common_options: Dict[str, Any] = {}
def setUp(self) -> None:
"""
Set up a multiworld with multiple players, each using different options.
"""
self.multiworld = setup_multiworld([WitnessWorld] * len(self.options_per_world), ())
for world, options in zip(self.multiworld.worlds.values(), self.options_per_world):
for option_name, option_value in {**self.common_options, **options}.items():
option = getattr(world.options, option_name)
self.assertIsNotNone(option)
option.value = option.from_any(option_value).value
self.assertSteps(gen_steps)
def collect_by_name(self, item_names: Union[str, Iterable[str]], player: int) -> List[Item]:
"""
Collect all copies of a specified item name (or list of item names) for a player in the multiworld item pool.
"""
items = self.get_items_by_name(item_names, player)
for item in items:
self.multiworld.state.collect(item)
return items
def get_items_by_name(self, item_names: Union[str, Iterable[str]], player: int) -> List[Item]:
"""
Return all copies of a specified item name (or list of item names) for a player in the multiworld item pool.
"""
if isinstance(item_names, str):
item_names = (item_names,)
return [item for item in self.multiworld.itempool if item.name in item_names and item.player == player]

View File

@ -0,0 +1,66 @@
from ..test import WitnessMultiworldTestBase, WitnessTestBase
class TestElevatorsComeToYou(WitnessTestBase):
options = {
"elevators_come_to_you": True,
"shuffle_doors": "mixed",
"shuffle_symbols": False,
}
def test_bunker_laser(self) -> None:
"""
In elevators_come_to_you, Bunker can be entered from the back.
This means that you can access the laser with just Bunker Elevator Control (Panel).
It also means that you can, for example, access UV Room with the Control and the Elevator Room Entry Door.
"""
self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", self.player))
self.collect_by_name("Bunker Elevator Control (Panel)")
self.assertTrue(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", self.player))
self.assertFalse(self.multiworld.state.can_reach("Bunker UV Room 2", "Location", self.player))
self.collect_by_name("Bunker Elevator Room Entry (Door)")
self.collect_by_name("Bunker Drop-Down Door Controls (Panel)")
self.assertTrue(self.multiworld.state.can_reach("Bunker UV Room 2", "Location", self.player))
class TestElevatorsComeToYouBleed(WitnessMultiworldTestBase):
options_per_world = [
{
"elevators_come_to_you": False,
},
{
"elevators_come_to_you": True,
},
{
"elevators_come_to_you": False,
},
]
common_options = {
"shuffle_symbols": False,
"shuffle_doors": "panels",
}
def test_correct_access_per_player(self) -> None:
"""
Test that in a multiworld with players that alternate the elevators_come_to_you option,
the actual behavior alternates as well and doesn't bleed over from slot to slot.
(This is essentially a "does connection info bleed over" test).
"""
self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 1))
self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 2))
self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 3))
self.collect_by_name(["Bunker Elevator Control (Panel)"], 1)
self.collect_by_name(["Bunker Elevator Control (Panel)"], 2)
self.collect_by_name(["Bunker Elevator Control (Panel)"], 3)
self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 1))
self.assertTrue(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 2))
self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 3))

View File

@ -0,0 +1,37 @@
from ..rules import _has_lasers
from ..test import WitnessTestBase
class TestDisableNonRandomized(WitnessTestBase):
options = {
"disable_non_randomized_puzzles": True,
"shuffle_doors": "panels",
"early_symbol_item": False,
}
def test_locations_got_disabled_and_alternate_activation_triggers_work(self) -> None:
"""
Test the different behaviors of the disable_non_randomized mode:
1. Unrandomized locations like Orchard Apple Tree 5 are disabled.
2. Certain doors or lasers that would usually be activated by unrandomized panels depend on event items instead.
3. These alternate activations are tied to solving Discarded Panels.
"""
with self.subTest("Test that unrandomized locations are disabled."):
self.assert_location_does_not_exist("Orchard Apple Tree 5")
with self.subTest("Test that alternate activation trigger events exist."):
self.assert_dependency_on_event_item(
self.world.get_entrance("Town Tower After Third Door to Town Tower Top"),
"Town Tower 4th Door Opens",
)
with self.subTest("Test that alternate activation triggers award lasers."):
self.assertFalse(_has_lasers(1, self.world, False)(self.multiworld.state))
self.collect_by_name("Triangles")
# Alternate triggers yield Bunker Laser (Mountainside Discard) and Monastery Laser (Desert Discard)
self.assertTrue(_has_lasers(2, self.world, False)(self.multiworld.state))
self.assertFalse(_has_lasers(3, self.world, False)(self.multiworld.state))

View File

@ -0,0 +1,24 @@
from ..test import WitnessTestBase
class TestIndividualDoors(WitnessTestBase):
options = {
"shuffle_doors": "doors",
"door_groupings": "off",
}
def test_swamp_laser_shortcut(self) -> None:
"""
Test that Door Shuffle grants early access to Swamp Laser from the back shortcut.
"""
self.assertTrue(self.get_items_by_name("Swamp Laser Shortcut (Door)"))
self.assertAccessDependency(
["Swamp Laser Panel"],
[
["Swamp Laser Shortcut (Door)"],
["Swamp Red Underwater Exit (Door)"],
],
only_check_listed=True,
)

View File

@ -0,0 +1,54 @@
from ..test import WitnessTestBase
class TestIndividualEPs(WitnessTestBase):
options = {
"shuffle_EPs": "individual",
"EP_difficulty": "normal",
"obelisk_keys": True,
"disable_non_randomized_puzzles": True,
"shuffle_postgame": False,
"victory_condition": "mountain_box_short",
"early_caves": "off",
}
def test_correct_eps_exist_and_are_locked(self) -> None:
"""
Test that EP locations exist in shuffle_EPs, but only the ones that actually should (based on options)
"""
# Test Tutorial First Hallways EP as a proxy for "EPs exist at all"
# Don't wrap in a subtest - If this fails, there is no point.
self.assert_location_exists("Tutorial First Hallway EP")
with self.subTest("Test that disable_non_randomized disables Monastery Garden Left EP"):
self.assert_location_does_not_exist("Monastery Garden Left EP")
with self.subTest("Test that shuffle_postgame being off disables postgame EPs."):
self.assert_location_does_not_exist("Caves Skylight EP")
with self.subTest("Test that ep_difficulty being set to normal excludes tedious EPs."):
self.assert_location_does_not_exist("Shipwreck Couch EP")
with self.subTest("Test that EPs are being locked by Obelisk Keys."):
self.assertAccessDependency(["Desert Sand Snake EP"], [["Desert Obelisk Key"]], True)
class TestObeliskSides(WitnessTestBase):
options = {
"shuffle_EPs": "obelisk_sides",
"EP_difficulty": "eclipse",
"shuffle_vault_boxes": True,
"shuffle_postgame": True,
}
def test_eclipse_required_for_town_side_6(self) -> None:
"""
Test that Obelisk Sides require the appropriate event items from the individual EPs.
Specifically, assert that Town Obelisk Side 6 needs Theater Eclipse EP.
This doubles as a test for Theater Eclipse EP existing with the right options.
"""
self.assert_dependency_on_event_item(
self.world.get_location("Town Obelisk Side 6"), "Town Obelisk Side 6 - Theater Eclipse EP"
)

View File

@ -0,0 +1,185 @@
from ..test import WitnessTestBase
class TestSymbolsRequiredToWinElevatorNormal(WitnessTestBase):
options = {
"shuffle_lasers": True,
"puzzle_randomization": "sigma_normal",
"mountain_lasers": 1,
"victory_condition": "elevator",
"early_symbol_item": False,
}
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 Sigma Normal Puzzles.
"""
exact_requirement = {
"Monastery Laser": 1,
"Progressive Dots": 2,
"Progressive Stars": 2,
"Progressive Symmetry": 2,
"Black/White Squares": 1,
"Colored Squares": 1,
"Shapers": 1,
"Rotated Shapers": 1,
"Eraser": 1,
}
self.assert_can_beat_with_minimally(exact_requirement)
class TestSymbolsRequiredToWinElevatorExpert(WitnessTestBase):
options = {
"shuffle_lasers": True,
"mountain_lasers": 1,
"victory_condition": "elevator",
"early_symbol_item": False,
"puzzle_randomization": "sigma_expert",
}
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 Sigma Expert Puzzles.
"""
exact_requirement = {
"Monastery Laser": 1,
"Progressive Dots": 2,
"Progressive Stars": 2,
"Progressive Symmetry": 2,
"Black/White Squares": 1,
"Colored Squares": 1,
"Shapers": 1,
"Rotated Shapers": 1,
"Negative Shapers": 1,
"Eraser": 1,
"Triangles": 1,
}
self.assert_can_beat_with_minimally(exact_requirement)
class TestSymbolsRequiredToWinElevatorVanilla(WitnessTestBase):
options = {
"shuffle_lasers": True,
"mountain_lasers": 1,
"victory_condition": "elevator",
"early_symbol_item": False,
"puzzle_randomization": "none",
}
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 Vanilla 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,
}
self.assert_can_beat_with_minimally(exact_requirement)
class TestPanelsRequiredToWinElevator(WitnessTestBase):
options = {
"shuffle_lasers": True,
"mountain_lasers": 1,
"victory_condition": "elevator",
"early_symbol_item": False,
"shuffle_symbols": False,
"shuffle_doors": "panels",
"door_groupings": "off",
}
def test_panels_to_win(self) -> None:
"""
In door panel shuffle , the only way to reach the Elevator is through Mountain Entry by descending the Mountain.
This requires some control panels for each of the Mountain Floors.
"""
exact_requirement = {
"Desert Laser": 1,
"Town Desert Laser Redirect Control (Panel)": 1,
"Mountain Floor 1 Light Bridge (Panel)": 1,
"Mountain Floor 2 Light Bridge Near (Panel)": 1,
"Mountain Floor 2 Light Bridge Far (Panel)": 1,
"Mountain Floor 2 Elevator Control (Panel)": 1,
}
self.assert_can_beat_with_minimally(exact_requirement)
class TestDoorsRequiredToWinElevator(WitnessTestBase):
options = {
"shuffle_lasers": True,
"mountain_lasers": 1,
"victory_condition": "elevator",
"early_symbol_item": False,
"shuffle_symbols": False,
"shuffle_doors": "doors",
"door_groupings": "off",
}
def test_doors_to_elevator_paths(self) -> None:
"""
In remote door shuffle, there are three ways to win.
- Through the normal route (Mountain Entry -> Descend through Mountain -> Reach Bottom Floor)
- Through the Caves using the Caves Shortcuts (Caves -> Reach Bottom Floor)
- Through the Caves via Challenge (Tunnels -> Challenge -> Caves -> Reach Bottom Floor)
"""
with self.subTest("Test Elevator victory in shuffle_doors through Mountain Entry."):
exact_requirement = {
"Monastery Laser": 1,
"Mountain Floor 1 Exit (Door)": 1,
"Mountain Floor 2 Staircase Near (Door)": 1,
"Mountain Floor 2 Staircase Far (Door)": 1,
"Mountain Floor 2 Exit (Door)": 1,
"Mountain Bottom Floor Giant Puzzle Exit (Door)": 1,
"Mountain Bottom Floor Pillars Room Entry (Door)": 1,
}
self.assert_can_beat_with_minimally(exact_requirement)
with self.subTest("Test Elevator victory in shuffle_doors through Caves Shortcuts."):
exact_requirement = {
"Monastery Laser": 1, # Elevator Panel itself has a laser lock
"Caves Mountain Shortcut (Door)": 1,
"Caves Entry (Door)": 1,
"Mountain Bottom Floor Rock (Door)": 1,
"Mountain Bottom Floor Pillars Room Entry (Door)": 1,
}
self.assert_can_beat_with_minimally(exact_requirement)
with self.subTest("Test Elevator victory in shuffle_doors through Tunnels->Challenge->Caves."):
exact_requirement = {
"Monastery Laser": 1, # Elevator Panel itself has a laser lock
"Windmill Entry (Door)": 1,
"Tunnels Theater Shortcut (Door)": 1,
"Tunnels Entry (Door)": 1,
"Challenge Entry (Door)": 1,
"Caves Pillar Door": 1,
"Caves Entry (Door)": 1,
"Mountain Bottom Floor Rock (Door)": 1,
"Mountain Bottom Floor Pillars Room Entry (Door)": 1,
}
self.assert_can_beat_with_minimally(exact_requirement)

View File

@ -0,0 +1,58 @@
from ..test import WitnessTestBase
# These are just some random options combinations, just to catch whether I broke anything obvious
class TestExpertNonRandomizedEPs(WitnessTestBase):
options = {
"disable_non_randomized": True,
"puzzle_randomization": "sigma_expert",
"shuffle_EPs": "individual",
"ep_difficulty": "eclipse",
"victory_condition": "challenge",
"shuffle_discarded_panels": False,
"shuffle_boat": False,
}
class TestVanillaAutoElevatorsPanels(WitnessTestBase):
options = {
"puzzle_randomization": "none",
"elevators_come_to_you": True,
"shuffle_doors": "panels",
"victory_condition": "mountain_box_short",
"early_caves": True,
"shuffle_vault_boxes": True,
"mountain_lasers": 11,
}
class TestMiscOptions(WitnessTestBase):
options = {
"death_link": True,
"death_link_amnesty": 3,
"laser_hints": True,
"hint_amount": 40,
"area_hint_percentage": 100,
}
class TestMaxEntityShuffle(WitnessTestBase):
options = {
"shuffle_symbols": False,
"shuffle_doors": "mixed",
"shuffle_EPs": "individual",
"obelisk_keys": True,
"shuffle_lasers": "anywhere",
"victory_condition": "mountain_box_long",
}
class TestPostgameGroupedDoors(WitnessTestBase):
options = {
"shuffle_postgame": True,
"shuffle_discarded_panels": True,
"shuffle_doors": "doors",
"door_groupings": "regional",
"victory_condition": "elevator",
}

View File

@ -0,0 +1,74 @@
from ..test import WitnessMultiworldTestBase, WitnessTestBase
class TestSymbols(WitnessTestBase):
options = {
"early_symbol_item": False,
}
def test_progressive_symbols(self) -> None:
"""
Test that Dots & Full Dots are correctly replaced by 2x Progressive Dots,
and test that Dots puzzles and Full Dots puzzles require 1 and 2 copies of this item respectively.
"""
progressive_dots = self.get_items_by_name("Progressive Dots")
self.assertEqual(len(progressive_dots), 2)
self.assertFalse(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player))
self.assertFalse(
self.multiworld.state.can_reach("Outside Tutorial Outpost Entry Panel", "Location", self.player)
)
self.collect(progressive_dots.pop())
self.assertTrue(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player))
self.assertFalse(
self.multiworld.state.can_reach("Outside Tutorial Outpost Entry Panel", "Location", self.player)
)
self.collect(progressive_dots.pop())
self.assertTrue(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player))
self.assertTrue(
self.multiworld.state.can_reach("Outside Tutorial Outpost Entry Panel", "Location", self.player)
)
class TestSymbolRequirementsMultiworld(WitnessMultiworldTestBase):
options_per_world = [
{
"puzzle_randomization": "sigma_normal",
},
{
"puzzle_randomization": "sigma_expert",
},
{
"puzzle_randomization": "none",
},
]
common_options = {
"shuffle_discarded_panels": True,
"early_symbol_item": False,
}
def test_arrows_exist_and_are_required_in_expert_seeds_only(self) -> None:
"""
In sigma_expert, Discarded Panels require Arrows.
In sigma_normal, Discarded Panels require Triangles, and Arrows shouldn't exist at all as an item.
"""
with self.subTest("Test that Arrows exist only in the expert seed."):
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))
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"})})
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)