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:
parent
bfac100567
commit
f99ee77325
|
@ -185,21 +185,22 @@ class WitnessWorld(World):
|
||||||
|
|
||||||
self.items_placed_early.append("Puzzle Skip")
|
self.items_placed_early.append("Puzzle Skip")
|
||||||
|
|
||||||
# Pick an early item to place on the tutorial gate.
|
if self.options.early_symbol_item:
|
||||||
early_items = [
|
# Pick an early item to place on the tutorial gate.
|
||||||
item for item in self.player_items.get_early_items() if item in self.player_items.get_mandatory_items()
|
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)
|
if early_items:
|
||||||
if self.options.puzzle_randomization == "sigma_expert":
|
random_early_item = self.random.choice(early_items)
|
||||||
# In Expert, only tag the item as early, rather than forcing it onto the gate.
|
if self.options.puzzle_randomization == "sigma_expert":
|
||||||
self.multiworld.local_early_items[self.player][random_early_item] = 1
|
# In Expert, only tag the item as early, rather than forcing it onto the gate.
|
||||||
else:
|
self.multiworld.local_early_items[self.player][random_early_item] = 1
|
||||||
# Force the item onto the tutorial gate check and remove it from our random pool.
|
else:
|
||||||
gate_item = self.create_item(random_early_item)
|
# Force the item onto the tutorial gate check and remove it from our random pool.
|
||||||
self.get_location("Tutorial Gate Open").place_locked_item(gate_item)
|
gate_item = self.create_item(random_early_item)
|
||||||
self.own_itempool.append(gate_item)
|
self.get_location("Tutorial Gate Open").place_locked_item(gate_item)
|
||||||
self.items_placed_early.append(random_early_item)
|
self.own_itempool.append(gate_item)
|
||||||
|
self.items_placed_early.append(random_early_item)
|
||||||
|
|
||||||
# There are some really restrictive settings in The Witness.
|
# There are some really restrictive settings in The Witness.
|
||||||
# They are rarely played, but when they are, we add some extra sphere 1 locations.
|
# They are rarely played, but when they are, we add some extra sphere 1 locations.
|
||||||
|
|
|
@ -2,7 +2,7 @@ from dataclasses import dataclass
|
||||||
|
|
||||||
from schema import And, Schema
|
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 import static_logic as static_witness_logic
|
||||||
from .data.item_definition_classes import ItemCategory, WeightedItemDefinition
|
from .data.item_definition_classes import ItemCategory, WeightedItemDefinition
|
||||||
|
@ -35,6 +35,14 @@ class EarlyCaves(Choice):
|
||||||
alias_on = 2
|
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):
|
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.
|
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
|
mountain_lasers: MountainLasers
|
||||||
challenge_lasers: ChallengeLasers
|
challenge_lasers: ChallengeLasers
|
||||||
early_caves: EarlyCaves
|
early_caves: EarlyCaves
|
||||||
|
early_symbol_item: EarlySymbolItem
|
||||||
elevators_come_to_you: ElevatorsComeToYou
|
elevators_come_to_you: ElevatorsComeToYou
|
||||||
trap_percentage: TrapPercentage
|
trap_percentage: TrapPercentage
|
||||||
trap_weights: TrapWeights
|
trap_weights: TrapWeights
|
||||||
|
|
|
@ -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]
|
|
@ -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))
|
|
@ -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))
|
|
@ -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,
|
||||||
|
)
|
|
@ -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"
|
||||||
|
)
|
|
@ -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)
|
|
@ -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",
|
||||||
|
}
|
|
@ -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)
|
Loading…
Reference in New Issue