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:
parent
0fb69dce33
commit
906b23088c
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue