The Witness: Rules Optimisation (#3617)

* Attempt at optimizing rules

* docstrings

* Python 3.8

* Lasers optimisation

* Simplify conversion code and make it even faster

* mypy

* ruff

* Neat

* Add redirect to the other two modes

* Update WitnessLogic.txt

* Update WitnessLogicExpert.txt

* Update WitnessLogicVanilla.txt

* Use NamedTuple

* Ruff

* mypy thing

* Mypy stuff

* Move Redirect Event to Desert Region so it has a better name
This commit is contained in:
NewSoupVi 2024-08-28 18:31:49 +02:00 committed by GitHub
parent 0fb69dce33
commit 906b23088c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 139 additions and 85 deletions

View File

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

View File

@ -176,6 +176,7 @@ Door - 0x03444 (Vault Door) - 0x0CC7B
Door - 0x09FEE (Light Room Entry) - 0x0C339 Door - 0x09FEE (Light Room Entry) - 0x0C339
158701 - 0x03608 (Laser Panel) - 0x012D7 & 0x0A15F - True 158701 - 0x03608 (Laser Panel) - 0x012D7 & 0x0A15F - True
Laser - 0x012FB (Laser) - 0x03608 Laser - 0x012FB (Laser) - 0x03608
159804 - 0xFFD03 (Laser Activated + Redirected) - 0x09F98 & 0x012FB - True
159020 - 0x3351D (Sand Snake EP) - True - True 159020 - 0x3351D (Sand Snake EP) - True - True
159030 - 0x0053C (Facade Right EP) - True - True 159030 - 0x0053C (Facade Right EP) - True - True
159031 - 0x00771 (Facade Left EP) - True - True 159031 - 0x00771 (Facade Left EP) - True - True
@ -980,7 +981,7 @@ Mountainside Obelisk (Mountainside) - Entry - True:
159739 - 0x00367 (Obelisk) - True - True 159739 - 0x00367 (Obelisk) - True - True
Mountainside (Mountainside) - Main Island - True - Mountaintop - True - Mountainside Vault - 0x00085: Mountainside (Mountainside) - Main Island - True - Mountaintop - True - Mountainside Vault - 0x00085:
159550 - 0x28B91 (Thundercloud EP) - 0x09F98 & 0x012FB - True 159550 - 0x28B91 (Thundercloud EP) - 0xFFD03 - True
158612 - 0x17C42 (Discard) - True - Triangles 158612 - 0x17C42 (Discard) - True - Triangles
158665 - 0x002A6 (Vault Panel) - True - Symmetry & Colored Dots & Black/White Squares & Dots 158665 - 0x002A6 (Vault Panel) - True - Symmetry & Colored Dots & Black/White Squares & Dots
Door - 0x00085 (Vault Door) - 0x002A6 Door - 0x00085 (Vault Door) - 0x002A6

View File

@ -176,6 +176,7 @@ Door - 0x03444 (Vault Door) - 0x0CC7B
Door - 0x09FEE (Light Room Entry) - 0x0C339 Door - 0x09FEE (Light Room Entry) - 0x0C339
158701 - 0x03608 (Laser Panel) - 0x012D7 & 0x0A15F - True 158701 - 0x03608 (Laser Panel) - 0x012D7 & 0x0A15F - True
Laser - 0x012FB (Laser) - 0x03608 Laser - 0x012FB (Laser) - 0x03608
159804 - 0xFFD03 (Laser Activated + Redirected) - 0x09F98 & 0x012FB - True
159020 - 0x3351D (Sand Snake EP) - True - True 159020 - 0x3351D (Sand Snake EP) - True - True
159030 - 0x0053C (Facade Right EP) - True - True 159030 - 0x0053C (Facade Right EP) - True - True
159031 - 0x00771 (Facade Left EP) - True - True 159031 - 0x00771 (Facade Left EP) - True - True
@ -980,7 +981,7 @@ Mountainside Obelisk (Mountainside) - Entry - True:
159739 - 0x00367 (Obelisk) - True - True 159739 - 0x00367 (Obelisk) - True - True
Mountainside (Mountainside) - Main Island - True - Mountaintop - True - Mountainside Vault - 0x00085: Mountainside (Mountainside) - Main Island - True - Mountaintop - True - Mountainside Vault - 0x00085:
159550 - 0x28B91 (Thundercloud EP) - 0x09F98 & 0x012FB - True 159550 - 0x28B91 (Thundercloud EP) - 0xFFD03 - True
158612 - 0x17C42 (Discard) - True - Arrows 158612 - 0x17C42 (Discard) - True - Arrows
158665 - 0x002A6 (Vault Panel) - True - Symmetry & Colored Squares & Triangles & Stars & Stars + Same Colored Symbol 158665 - 0x002A6 (Vault Panel) - True - Symmetry & Colored Squares & Triangles & Stars & Stars + Same Colored Symbol
Door - 0x00085 (Vault Door) - 0x002A6 Door - 0x00085 (Vault Door) - 0x002A6

View File

@ -176,6 +176,7 @@ Door - 0x03444 (Vault Door) - 0x0CC7B
Door - 0x09FEE (Light Room Entry) - 0x0C339 Door - 0x09FEE (Light Room Entry) - 0x0C339
158701 - 0x03608 (Laser Panel) - 0x012D7 & 0x0A15F - True 158701 - 0x03608 (Laser Panel) - 0x012D7 & 0x0A15F - True
Laser - 0x012FB (Laser) - 0x03608 Laser - 0x012FB (Laser) - 0x03608
159804 - 0xFFD03 (Laser Activated + Redirected) - 0x09F98 & 0x012FB - True
159020 - 0x3351D (Sand Snake EP) - True - True 159020 - 0x3351D (Sand Snake EP) - True - True
159030 - 0x0053C (Facade Right EP) - True - True 159030 - 0x0053C (Facade Right EP) - True - True
159031 - 0x00771 (Facade Left EP) - True - True 159031 - 0x00771 (Facade Left EP) - True - True
@ -980,7 +981,7 @@ Mountainside Obelisk (Mountainside) - Entry - True:
159739 - 0x00367 (Obelisk) - True - True 159739 - 0x00367 (Obelisk) - True - True
Mountainside (Mountainside) - Main Island - True - Mountaintop - True - Mountainside Vault - 0x00085: Mountainside (Mountainside) - Main Island - True - Mountaintop - True - Mountainside Vault - 0x00085:
159550 - 0x28B91 (Thundercloud EP) - 0x09F98 & 0x012FB - True 159550 - 0x28B91 (Thundercloud EP) - 0xFFD03 - True
158612 - 0x17C42 (Discard) - True - Triangles 158612 - 0x17C42 (Discard) - True - Triangles
158665 - 0x002A6 (Vault Panel) - True - Symmetry & Colored Dots & Black/White Squares 158665 - 0x002A6 (Vault Panel) - True - Symmetry & Colored Dots & Black/White Squares
Door - 0x00085 (Vault Door) - 0x002A6 Door - 0x00085 (Vault Door) - 0x002A6

View File

@ -712,7 +712,6 @@ def get_compact_hint_args(hint: WitnessWordedHint, local_player_number: int) ->
if hint.vague_location_hint and location.player == local_player_number: if hint.vague_location_hint and location.player == local_player_number:
assert hint.area is not None # A local vague location hint should have an area argument assert hint.area is not None # A local vague location hint should have an area argument
return location.address, "containing_area:" + hint.area return location.address, "containing_area:" + hint.area
else:
return location.address, location.player # Scouting does not matter for other players (currently) return location.address, location.player # Scouting does not matter for other players (currently)
# Is junk / undefined hint # Is junk / undefined hint

View File

@ -42,7 +42,7 @@ class WitnessPlayerItems:
player_locations: WitnessPlayerLocations) -> None: player_locations: WitnessPlayerLocations) -> None:
"""Adds event items after logic changes due to options""" """Adds event items after logic changes due to options"""
self._world: "WitnessWorld" = world self._world: WitnessWorld = world
self._multiworld: MultiWorld = world.multiworld self._multiworld: MultiWorld = world.multiworld
self._player_id: int = world.player self._player_id: int = world.player
self._logic: WitnessPlayerLogic = player_logic self._logic: WitnessPlayerLogic = player_logic

View File

@ -116,18 +116,19 @@ class WitnessPlayerLogic:
self.HUNT_ENTITIES: Set[str] = set() self.HUNT_ENTITIES: Set[str] = set()
self.ALWAYS_EVENT_NAMES_BY_HEX = { self.ALWAYS_EVENT_NAMES_BY_HEX = {
"0x00509": "+1 Laser (Symmetry Laser)", "0x00509": "+1 Laser",
"0x012FB": "+1 Laser (Desert Laser)", "0x012FB": "+1 Laser (Unredirected)",
"0x09F98": "Desert Laser Redirection", "0x09F98": "Desert Laser Redirection",
"0x01539": "+1 Laser (Quarry Laser)", "0xFFD03": "+1 Laser (Redirected)",
"0x181B3": "+1 Laser (Shadows Laser)", "0x01539": "+1 Laser",
"0x014BB": "+1 Laser (Keep Laser)", "0x181B3": "+1 Laser",
"0x17C65": "+1 Laser (Monastery Laser)", "0x014BB": "+1 Laser",
"0x032F9": "+1 Laser (Town Laser)", "0x17C65": "+1 Laser",
"0x00274": "+1 Laser (Jungle Laser)", "0x032F9": "+1 Laser",
"0x0C2B2": "+1 Laser (Bunker Laser)", "0x00274": "+1 Laser",
"0x00BF6": "+1 Laser (Swamp Laser)", "0x0C2B2": "+1 Laser",
"0x028A4": "+1 Laser (Treehouse Laser)", "0x00BF6": "+1 Laser",
"0x028A4": "+1 Laser",
"0x17C34": "Mountain Entry", "0x17C34": "Mountain Entry",
"0xFFF00": "Bottom Floor Discard Turns On", "0xFFF00": "Bottom Floor Discard Turns On",
} }

View File

@ -3,7 +3,7 @@ Defines Region for The Witness, assigns locations to them,
and connects them with the proper requirements and connects them with the proper requirements
""" """
from collections import defaultdict from collections import defaultdict
from typing import TYPE_CHECKING, Dict, List, Set, Tuple from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
from BaseClasses import Entrance, Region from BaseClasses import Entrance, Region
@ -38,7 +38,7 @@ class WitnessPlayerRegions:
self.created_region_names: Set[str] = set() self.created_region_names: Set[str] = set()
@staticmethod @staticmethod
def make_lambda(item_requirement: WitnessRule, world: "WitnessWorld") -> CollectionRule: def make_lambda(item_requirement: WitnessRule, world: "WitnessWorld") -> Optional[CollectionRule]:
from .rules import _meets_item_requirements from .rules import _meets_item_requirements
""" """
@ -79,7 +79,9 @@ class WitnessPlayerRegions:
source_region source_region
) )
connection.access_rule = self.make_lambda(final_requirement, world) rule = self.make_lambda(final_requirement, world)
if rule is not None:
connection.access_rule = rule
source_region.exits.append(connection) source_region.exits.append(connection)
connection.connect(target_region) connection.connect(target_region)

View File

@ -2,7 +2,8 @@
Defines the rules by which locations can be accessed, Defines the rules by which locations can be accessed,
depending on the items received depending on the items received
""" """
from typing import TYPE_CHECKING from collections import Counter
from typing import TYPE_CHECKING, Dict, List, NamedTuple, Optional, Union
from BaseClasses import CollectionState from BaseClasses import CollectionState
@ -15,50 +16,22 @@ from .player_logic import WitnessPlayerLogic
if TYPE_CHECKING: if TYPE_CHECKING:
from . import WitnessWorld from . import WitnessWorld
laser_hexes = [
"0x028A4", class SimpleItemRepresentation(NamedTuple):
"0x00274", item_name: str
"0x032F9", item_count: int
"0x01539",
"0x181B3",
"0x0C2B2",
"0x00509",
"0x00BF6",
"0x014BB",
"0x012FB",
"0x17C65",
]
def _can_do_panel_hunt(world: "WitnessWorld") -> CollectionRule: def _can_do_panel_hunt(world: "WitnessWorld") -> SimpleItemRepresentation:
required = world.panel_hunt_required_count required = world.panel_hunt_required_count
player = world.player return SimpleItemRepresentation("+1 Panel Hunt", required)
return lambda state: state.has("+1 Panel Hunt", player, required)
def _has_laser(laser_hex: str, world: "WitnessWorld", redirect_required: bool) -> CollectionRule:
player = world.player
laser_name = static_witness_logic.ENTITIES_BY_HEX[laser_hex]["checkName"]
# Workaround for intentional naming inconsistency
if laser_name == "Symmetry Island Laser":
laser_name = "Symmetry Laser"
if laser_hex == "0x012FB" and redirect_required:
return lambda state: state.has_all([f"+1 Laser ({laser_name})", "Desert Laser Redirection"], player)
return lambda state: state.has(f"+1 Laser ({laser_name})", player)
def _has_lasers(amount: int, world: "WitnessWorld", redirect_required: bool) -> CollectionRule: def _has_lasers(amount: int, world: "WitnessWorld", redirect_required: bool) -> CollectionRule:
laser_lambdas = [] if redirect_required:
return lambda state: state.has_from_list(["+1 Laser", "+1 Laser (Redirected)"], world.player, amount)
for laser_hex in laser_hexes: return lambda state: state.has_from_list(["+1 Laser", "+1 Laser (Unredirected)"], world.player, amount)
has_laser_lambda = _has_laser(laser_hex, world, redirect_required)
laser_lambdas.append(has_laser_lambda)
return lambda state: sum(laser_lambda(state) for laser_lambda in laser_lambdas) >= amount
def _can_do_expert_pp2(state: CollectionState, world: "WitnessWorld") -> bool: def _can_do_expert_pp2(state: CollectionState, world: "WitnessWorld") -> bool:
@ -196,7 +169,13 @@ def _can_do_theater_to_tunnels(state: CollectionState, world: "WitnessWorld") ->
) )
def _has_item(item: str, world: "WitnessWorld", player: int, player_logic: WitnessPlayerLogic) -> CollectionRule: def _has_item(item: str, world: "WitnessWorld",
player_logic: WitnessPlayerLogic) -> Union[CollectionRule, SimpleItemRepresentation]:
"""
Convert a single element of a WitnessRule into a CollectionRule, unless it is referring to an item,
in which case we return it as an item-count pair ("SimpleItemRepresentation"). This allows some optimisation later.
"""
assert item not in static_witness_logic.ENTITIES_BY_HEX, "Requirements can no longer contain entity hexes directly." assert item not in static_witness_logic.ENTITIES_BY_HEX, "Requirements can no longer contain entity hexes directly."
if item in player_logic.REFERENCE_LOGIC.ALL_REGIONS_BY_NAME: if item in player_logic.REFERENCE_LOGIC.ALL_REGIONS_BY_NAME:
@ -223,27 +202,90 @@ def _has_item(item: str, world: "WitnessWorld", player: int, player_logic: Witne
return lambda state: _can_do_theater_to_tunnels(state, world) return lambda state: _can_do_theater_to_tunnels(state, world)
prog_item = static_witness_logic.get_parent_progressive_item(item) prog_item = static_witness_logic.get_parent_progressive_item(item)
return lambda state: state.has(prog_item, player, player_logic.MULTI_AMOUNTS[item]) needed_amount = player_logic.MULTI_AMOUNTS[item]
simple_rule: SimpleItemRepresentation = SimpleItemRepresentation(prog_item, needed_amount)
return simple_rule
def _meets_item_requirements(requirements: WitnessRule, world: "WitnessWorld") -> CollectionRule: def optimize_requirement_option(requirement_option: List[Union[CollectionRule, SimpleItemRepresentation]])\
-> List[Union[CollectionRule, SimpleItemRepresentation]]:
""" """
Checks whether item and panel requirements are met for This optimises out a requirement like [("Progressive Dots": 1), ("Progressive Dots": 2)] to only the "2" version.
a panel
""" """
lambda_conversion = [ direct_items = [rule for rule in requirement_option if isinstance(rule, tuple)]
[_has_item(item, world, world.player, world.player_logic) for item in subset] if not direct_items:
return requirement_option
max_per_item: Dict[str, int] = Counter()
for item_rule in direct_items:
max_per_item[item_rule[0]] = max(max_per_item[item_rule[0]], item_rule[1])
return [
rule for rule in requirement_option
if not (isinstance(rule, tuple) and rule[1] < max_per_item[rule[0]])
]
def convert_requirement_option(requirement: List[Union[CollectionRule, SimpleItemRepresentation]],
player: int) -> List[CollectionRule]:
"""
Converts a list of CollectionRules and SimpleItemRepresentations to just a list of CollectionRules.
If the list is ONLY SimpleItemRepresentations, we can just return a CollectionRule based on state.has_all_counts()
"""
converted_sublist = []
for rule in requirement:
if not isinstance(rule, tuple):
converted_sublist.append(rule)
continue
collection_rules = [rule for rule in requirement if not isinstance(rule, SimpleItemRepresentation)]
item_rules = [rule for rule in requirement if isinstance(rule, SimpleItemRepresentation)]
if len(item_rules) == 0:
item_rules_converted = []
elif len(item_rules) == 1:
item = item_rules[0][0]
count = item_rules[0][1]
item_rules_converted = [lambda state: state.has(item, player, count)]
else:
item_counts = {item_rule.item_name: item_rule.item_count for item_rule in item_rules}
item_rules_converted = [lambda state: state.has_all_counts(item_counts, player)]
return collection_rules + item_rules_converted
def _meets_item_requirements(requirements: WitnessRule, world: "WitnessWorld") -> Optional[CollectionRule]:
"""
Converts a WitnessRule into a CollectionRule.
"""
player = world.player
if requirements == frozenset({frozenset()}):
return None
rule_conversion = [
[_has_item(item, world, world.player_logic) for item in subset]
for subset in requirements for subset in requirements
] ]
optimized_rule_conversion = [optimize_requirement_option(sublist) for sublist in rule_conversion]
fully_converted_rules = [convert_requirement_option(sublist, player) for sublist in optimized_rule_conversion]
if len(fully_converted_rules) == 1:
if len(fully_converted_rules[0]) == 1:
return fully_converted_rules[0][0]
return lambda state: all(condition(state) for condition in fully_converted_rules[0])
return lambda state: any( return lambda state: any(
all(condition(state) for condition in sub_requirement) all(condition(state) for condition in sub_requirement)
for sub_requirement in lambda_conversion for sub_requirement in fully_converted_rules
) )
def make_lambda(entity_hex: str, world: "WitnessWorld") -> CollectionRule: def make_lambda(entity_hex: str, world: "WitnessWorld") -> Optional[CollectionRule]:
""" """
Lambdas are created in a for loop so values need to be captured Lambdas are created in a for loop so values need to be captured
""" """
@ -268,6 +310,8 @@ def set_rules(world: "WitnessWorld") -> None:
entity_hex = associated_entity["entity_hex"] entity_hex = associated_entity["entity_hex"]
rule = make_lambda(entity_hex, world) rule = make_lambda(entity_hex, world)
if rule is None:
continue
location = world.get_location(location) location = world.get_location(location)

View File

@ -1,10 +1,11 @@
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 typing import Any, ClassVar, Dict, Iterable, List, Mapping, Union, cast
from BaseClasses import CollectionState, Entrance, Item, Location, Region from BaseClasses import CollectionState, Entrance, Item, Location, Region
from test.bases import WorldTestBase
from test.general import gen_steps, setup_multiworld
from test.multiworld.test_multiworlds import MultiworldTestBase
from .. import WitnessWorld from .. import WitnessWorld

View File

@ -1,5 +1,6 @@
from BaseClasses import CollectionState, Item from BaseClasses import CollectionState
from worlds.witness.test import WitnessTestBase, WitnessMultiworldTestBase
from worlds.witness.test import WitnessMultiworldTestBase, WitnessTestBase
class TestMaxPanelHuntMinChecks(WitnessTestBase): class TestMaxPanelHuntMinChecks(WitnessTestBase):
@ -13,7 +14,7 @@ class TestMaxPanelHuntMinChecks(WitnessTestBase):
"shuffle_vault_boxes": False, "shuffle_vault_boxes": False,
} }
def test_correct_panels_were_picked(self): def test_correct_panels_were_picked(self) -> None:
with self.subTest("Check that 100 Hunt Panels were actually picked."): with self.subTest("Check that 100 Hunt Panels were actually picked."):
self.assertEqual(len(self.multiworld.find_item_locations("+1 Panel Hunt", self.player)), 100) self.assertEqual(len(self.multiworld.find_item_locations("+1 Panel Hunt", self.player)), 100)
@ -63,45 +64,45 @@ class TestPanelHuntPostgame(WitnessMultiworldTestBase):
"shuffle_discarded_panels": True, "shuffle_discarded_panels": True,
} }
def test_panel_hunt_postgame(self): def test_panel_hunt_postgame(self) -> None:
for player_minus_one, options in enumerate(self.options_per_world): for player_minus_one, options in enumerate(self.options_per_world):
player = player_minus_one + 1 player = player_minus_one + 1
postgame_option = options["panel_hunt_postgame"] postgame_option = options["panel_hunt_postgame"]
with self.subTest(f"Test that \"{postgame_option}\" results in 40 Hunt Panels."): with self.subTest(f'Test that "{postgame_option}" results in 40 Hunt Panels.'):
self.assertEqual(len(self.multiworld.find_item_locations("+1 Panel Hunt", player)), 40) self.assertEqual(len(self.multiworld.find_item_locations("+1 Panel Hunt", player)), 40)
# Test that the box gets extra checks from panel_hunt_postgame # Test that the box gets extra checks from panel_hunt_postgame
with self.subTest("Test that \"everything_is_eligible\" has no Mountaintop Box Hunt Panels."): with self.subTest('Test that "everything_is_eligible" has no Mountaintop Box Hunt Panels.'):
self.assert_location_does_not_exist("Mountaintop Box Short (Panel Hunt)", 1, strict_check=False) self.assert_location_does_not_exist("Mountaintop Box Short (Panel Hunt)", 1, strict_check=False)
self.assert_location_does_not_exist("Mountaintop Box Long (Panel Hunt)", 1, strict_check=False) self.assert_location_does_not_exist("Mountaintop Box Long (Panel Hunt)", 1, strict_check=False)
with self.subTest("Test that \"disable_mountain_lasers_locations\" has a Hunt Panel for Short, but not Long."): with self.subTest('Test that "disable_mountain_lasers_locations" has a Hunt Panel for Short, but not Long.'):
self.assert_location_exists("Mountaintop Box Short (Panel Hunt)", 2, strict_check=False) self.assert_location_exists("Mountaintop Box Short (Panel Hunt)", 2, strict_check=False)
self.assert_location_does_not_exist("Mountaintop Box Long (Panel Hunt)", 2, strict_check=False) self.assert_location_does_not_exist("Mountaintop Box Long (Panel Hunt)", 2, strict_check=False)
with self.subTest("Test that \"disable_challenge_lasers_locations\" has a Hunt Panel for Long, but not Short."): with self.subTest('Test that "disable_challenge_lasers_locations" has a Hunt Panel for Long, but not Short.'):
self.assert_location_does_not_exist("Mountaintop Box Short (Panel Hunt)", 3, strict_check=False) self.assert_location_does_not_exist("Mountaintop Box Short (Panel Hunt)", 3, strict_check=False)
self.assert_location_exists("Mountaintop Box Long (Panel Hunt)", 3, strict_check=False) self.assert_location_exists("Mountaintop Box Long (Panel Hunt)", 3, strict_check=False)
with self.subTest("Test that \"disable_anything_locked_by_lasers\" has both Mountaintop Box Hunt Panels."): with self.subTest('Test that "disable_anything_locked_by_lasers" has both Mountaintop Box Hunt Panels.'):
self.assert_location_exists("Mountaintop Box Short (Panel Hunt)", 4, strict_check=False) self.assert_location_exists("Mountaintop Box Short (Panel Hunt)", 4, strict_check=False)
self.assert_location_exists("Mountaintop Box Long (Panel Hunt)", 4, strict_check=False) self.assert_location_exists("Mountaintop Box Long (Panel Hunt)", 4, strict_check=False)
# Check panel_hunt_postgame locations get disabled # Check panel_hunt_postgame locations get disabled
with self.subTest("Test that \"everything_is_eligible\" does not disable any locked-by-lasers panels."): with self.subTest('Test that "everything_is_eligible" does not disable any locked-by-lasers panels.'):
self.assert_location_exists("Mountain Floor 1 Right Row 5", 1) self.assert_location_exists("Mountain Floor 1 Right Row 5", 1)
self.assert_location_exists("Mountain Bottom Floor Discard", 1) self.assert_location_exists("Mountain Bottom Floor Discard", 1)
with self.subTest("Test that \"disable_mountain_lasers_locations\" disables only Shortbox-Locked panels."): with self.subTest('Test that "disable_mountain_lasers_locations" disables only Shortbox-Locked panels.'):
self.assert_location_does_not_exist("Mountain Floor 1 Right Row 5", 2) self.assert_location_does_not_exist("Mountain Floor 1 Right Row 5", 2)
self.assert_location_exists("Mountain Bottom Floor Discard", 2) self.assert_location_exists("Mountain Bottom Floor Discard", 2)
with self.subTest("Test that \"disable_challenge_lasers_locations\" disables only Longbox-Locked panels."): with self.subTest('Test that "disable_challenge_lasers_locations" disables only Longbox-Locked panels.'):
self.assert_location_exists("Mountain Floor 1 Right Row 5", 3) self.assert_location_exists("Mountain Floor 1 Right Row 5", 3)
self.assert_location_does_not_exist("Mountain Bottom Floor Discard", 3) self.assert_location_does_not_exist("Mountain Bottom Floor Discard", 3)
with self.subTest("Test that \"everything_is_eligible\" disables only Shortbox-Locked panels."): with self.subTest('Test that "everything_is_eligible" disables only Shortbox-Locked panels.'):
self.assert_location_does_not_exist("Mountain Floor 1 Right Row 5", 4) self.assert_location_does_not_exist("Mountain Floor 1 Right Row 5", 4)
self.assert_location_does_not_exist("Mountain Bottom Floor Discard", 4) self.assert_location_does_not_exist("Mountain Bottom Floor Discard", 4)