The Witness: mypy compliance (#3112)

* Make witness apworld mostly pass mypy

* Fix all remaining mypy errors except the core ones

* I'm a goofy stupid poopoo head

* Two more fixes

* ruff after merge

* Mypy for new stuff

* Oops

* Stricter ruff rules (that I already comply with :3)

* Deprecated ruff thing

* wait no i lied

* lol super nevermind

* I can actually be slightly more specific

* lint
This commit is contained in:
NewSoupVi 2024-07-02 23:59:26 +02:00 committed by GitHub
parent b6925c593e
commit 93617fa546
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 299 additions and 269 deletions

View File

@ -11,11 +11,12 @@ from Options import OptionError, PerGameCommonOptions, Toggle
from worlds.AutoWorld import WebWorld, World
from .data import static_items as static_witness_items
from .data import static_locations as static_witness_locations
from .data import static_logic as static_witness_logic
from .data.item_definition_classes import DoorItemDefinition, ItemData
from .data.utils import get_audio_logs
from .hints import CompactItemData, create_all_hints, make_compact_hint_data, make_laser_hints
from .locations import WitnessPlayerLocations, static_witness_locations
from .locations import WitnessPlayerLocations
from .options import TheWitnessOptions, witness_option_groups
from .player_items import WitnessItem, WitnessPlayerItems
from .player_logic import WitnessPlayerLogic
@ -53,7 +54,8 @@ class WitnessWorld(World):
options: TheWitnessOptions
item_name_to_id = {
name: data.ap_code for name, data in static_witness_items.ITEM_DATA.items()
# ITEM_DATA doesn't have any event items in it
name: cast(int, data.ap_code) for name, data in static_witness_items.ITEM_DATA.items()
}
location_name_to_id = static_witness_locations.ALL_LOCATIONS_TO_ID
item_name_groups = static_witness_items.ITEM_GROUPS
@ -142,7 +144,7 @@ class WitnessWorld(World):
)
self.player_regions: WitnessPlayerRegions = WitnessPlayerRegions(self.player_locations, self)
self.log_ids_to_hints = dict()
self.log_ids_to_hints = {}
self.determine_sufficient_progression()
@ -279,7 +281,7 @@ class WitnessWorld(World):
remaining_item_slots = pool_size - sum(item_pool.values())
# Add puzzle skips.
num_puzzle_skips = self.options.puzzle_skip_amount
num_puzzle_skips = self.options.puzzle_skip_amount.value
if num_puzzle_skips > remaining_item_slots:
warning(f"{self.multiworld.get_player_name(self.player)}'s Witness world has insufficient locations"
@ -301,21 +303,21 @@ class WitnessWorld(World):
if self.player_items.item_data[item_name].local_only:
self.options.local_items.value.add(item_name)
def fill_slot_data(self) -> dict:
self.log_ids_to_hints: Dict[int, CompactItemData] = dict()
self.laser_ids_to_hints: Dict[int, CompactItemData] = dict()
def fill_slot_data(self) -> Dict[str, Any]:
self.log_ids_to_hints: Dict[int, CompactItemData] = {}
self.laser_ids_to_hints: Dict[int, CompactItemData] = {}
already_hinted_locations = set()
# Laser hints
if self.options.laser_hints:
laser_hints = make_laser_hints(self, static_witness_items.ITEM_GROUPS["Lasers"])
laser_hints = make_laser_hints(self, sorted(static_witness_items.ITEM_GROUPS["Lasers"]))
for item_name, hint in laser_hints.items():
item_def = cast(DoorItemDefinition, static_witness_logic.ALL_ITEMS[item_name])
self.laser_ids_to_hints[int(item_def.panel_id_hexes[0], 16)] = make_compact_hint_data(hint, self.player)
already_hinted_locations.add(hint.location)
already_hinted_locations.add(cast(Location, hint.location))
# Audio Log Hints
@ -378,13 +380,13 @@ class WitnessLocation(Location):
game: str = "The Witness"
entity_hex: int = -1
def __init__(self, player: int, name: str, address: Optional[int], parent, ch_hex: int = -1) -> None:
def __init__(self, player: int, name: str, address: Optional[int], parent: Region, ch_hex: int = -1) -> None:
super().__init__(player, name, address, parent)
self.entity_hex = ch_hex
def create_region(world: WitnessWorld, name: str, player_locations: WitnessPlayerLocations,
region_locations=None, exits=None) -> Region:
region_locations: Optional[List[str]] = None, exits: Optional[List[str]] = None) -> Region:
"""
Create an Archipelago Region for The Witness
"""
@ -399,11 +401,11 @@ def create_region(world: WitnessWorld, name: str, player_locations: WitnessPlaye
entity_hex = int(
static_witness_logic.ENTITIES_BY_NAME[location]["entity_hex"], 0
)
location = WitnessLocation(
location_obj = WitnessLocation(
world.player, location, loc_id, ret, entity_hex
)
ret.locations.append(location)
ret.locations.append(location_obj)
if exits:
for single_exit in exits:
ret.exits.append(Entrance(world.player, single_exit, ret))

View File

@ -1,4 +1,4 @@
from typing import Dict, List
from typing import Dict, List, Set
from BaseClasses import ItemClassification
@ -7,7 +7,7 @@ from .item_definition_classes import DoorItemDefinition, ItemCategory, ItemData
from .static_locations import ID_START
ITEM_DATA: Dict[str, ItemData] = {}
ITEM_GROUPS: Dict[str, List[str]] = {}
ITEM_GROUPS: Dict[str, Set[str]] = {}
# Useful items that are treated specially at generation time and should not be automatically added to the player's
# item list during get_progression_items.
@ -22,13 +22,13 @@ def populate_items() -> None:
if definition.category is ItemCategory.SYMBOL:
classification = ItemClassification.progression
ITEM_GROUPS.setdefault("Symbols", []).append(item_name)
ITEM_GROUPS.setdefault("Symbols", set()).add(item_name)
elif definition.category is ItemCategory.DOOR:
classification = ItemClassification.progression
ITEM_GROUPS.setdefault("Doors", []).append(item_name)
ITEM_GROUPS.setdefault("Doors", set()).add(item_name)
elif definition.category is ItemCategory.LASER:
classification = ItemClassification.progression_skip_balancing
ITEM_GROUPS.setdefault("Lasers", []).append(item_name)
ITEM_GROUPS.setdefault("Lasers", set()).add(item_name)
elif definition.category is ItemCategory.USEFUL:
classification = ItemClassification.useful
elif definition.category is ItemCategory.FILLER:
@ -47,7 +47,7 @@ def populate_items() -> None:
def get_item_to_door_mappings() -> Dict[int, List[int]]:
output: Dict[int, List[int]] = {}
for item_name, item_data in ITEM_DATA.items():
if not isinstance(item_data.definition, DoorItemDefinition):
if not isinstance(item_data.definition, DoorItemDefinition) or item_data.ap_code is None:
continue
output[item_data.ap_code] = [int(hex_string, 16) for hex_string in item_data.definition.panel_id_hexes]
return output

View File

@ -1,3 +1,5 @@
from typing import Dict, Set, cast
from . import static_logic as static_witness_logic
ID_START = 158000
@ -441,17 +443,17 @@ OBELISK_SIDES = {
"Town Obelisk Side 6",
}
ALL_LOCATIONS_TO_ID = dict()
ALL_LOCATIONS_TO_ID: Dict[str, int] = {}
AREA_LOCATION_GROUPS = dict()
AREA_LOCATION_GROUPS: Dict[str, Set[str]] = {}
def get_id(entity_hex: str) -> str:
def get_id(entity_hex: str) -> int:
"""
Calculates the location ID for any given location
"""
return static_witness_logic.ENTITIES_BY_HEX[entity_hex]["id"]
return cast(int, static_witness_logic.ENTITIES_BY_HEX[entity_hex]["id"])
def get_event_name(entity_hex: str) -> str:
@ -461,7 +463,7 @@ def get_event_name(entity_hex: str) -> str:
action = " Opened" if static_witness_logic.ENTITIES_BY_HEX[entity_hex]["entityType"] == "Door" else " Solved"
return static_witness_logic.ENTITIES_BY_HEX[entity_hex]["checkName"] + action
return cast(str, static_witness_logic.ENTITIES_BY_HEX[entity_hex]["checkName"]) + action
ALL_LOCATIONS_TO_IDS = {
@ -479,4 +481,4 @@ for key, item in ALL_LOCATIONS_TO_IDS.items():
for loc in ALL_LOCATIONS_TO_IDS:
area = static_witness_logic.ENTITIES_BY_NAME[loc]["area"]["name"]
AREA_LOCATION_GROUPS.setdefault(area, []).append(loc)
AREA_LOCATION_GROUPS.setdefault(area, set()).add(loc)

View File

@ -1,5 +1,5 @@
from collections import defaultdict
from typing import Dict, List, Set, Tuple
from typing import Any, Dict, List, Optional, Set, Tuple
from Utils import cache_argsless
@ -24,13 +24,37 @@ from .utils import (
class StaticWitnessLogicObj:
def read_logic_file(self, lines) -> None:
def __init__(self, lines: Optional[List[str]] = None) -> None:
if lines is None:
lines = get_sigma_normal_logic()
# All regions with a list of panels in them and the connections to other regions, before logic adjustments
self.ALL_REGIONS_BY_NAME: Dict[str, Dict[str, Any]] = {}
self.ALL_AREAS_BY_NAME: Dict[str, Dict[str, Any]] = {}
self.CONNECTIONS_WITH_DUPLICATES: Dict[str, Dict[str, Set[WitnessRule]]] = defaultdict(lambda: defaultdict(set))
self.STATIC_CONNECTIONS_BY_REGION_NAME: Dict[str, Set[Tuple[str, WitnessRule]]] = {}
self.ENTITIES_BY_HEX: Dict[str, Dict[str, Any]] = {}
self.ENTITIES_BY_NAME: Dict[str, Dict[str, Any]] = {}
self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX: Dict[str, Dict[str, WitnessRule]] = {}
self.OBELISK_SIDE_ID_TO_EP_HEXES: Dict[int, Set[int]] = {}
self.EP_TO_OBELISK_SIDE: Dict[str, str] = {}
self.ENTITY_ID_TO_NAME: Dict[str, str] = {}
self.read_logic_file(lines)
self.reverse_connections()
self.combine_connections()
def read_logic_file(self, lines: List[str]) -> None:
"""
Reads the logic file and does the initial population of data structures
"""
current_region = dict()
current_area = {
current_region = {}
current_area: Dict[str, Any] = {
"name": "Misc",
"regions": [],
}
@ -155,7 +179,7 @@ class StaticWitnessLogicObj:
current_region["entities"].append(entity_hex)
current_region["physical_entities"].append(entity_hex)
def reverse_connection(self, source_region: str, connection: Tuple[str, Set[WitnessRule]]):
def reverse_connection(self, source_region: str, connection: Tuple[str, Set[WitnessRule]]) -> None:
target = connection[0]
traversal_options = connection[1]
@ -169,13 +193,13 @@ class StaticWitnessLogicObj:
if remaining_options:
self.CONNECTIONS_WITH_DUPLICATES[target][source_region].add(frozenset(remaining_options))
def reverse_connections(self):
def reverse_connections(self) -> None:
# Iterate all connections
for region_name, connections in list(self.CONNECTIONS_WITH_DUPLICATES.items()):
for connection in connections.items():
self.reverse_connection(region_name, connection)
def combine_connections(self):
def combine_connections(self) -> None:
# All regions need to be present, and this dict is copied later - Thus, defaultdict is not the correct choice.
self.STATIC_CONNECTIONS_BY_REGION_NAME = {region_name: set() for region_name in self.ALL_REGIONS_BY_NAME}
@ -184,30 +208,6 @@ class StaticWitnessLogicObj:
combined_req = logical_or_witness_rules(requirement)
self.STATIC_CONNECTIONS_BY_REGION_NAME[source].add((target, combined_req))
def __init__(self, lines=None) -> None:
if lines is None:
lines = get_sigma_normal_logic()
# All regions with a list of panels in them and the connections to other regions, before logic adjustments
self.ALL_REGIONS_BY_NAME = dict()
self.ALL_AREAS_BY_NAME = dict()
self.CONNECTIONS_WITH_DUPLICATES = defaultdict(lambda: defaultdict(lambda: set()))
self.STATIC_CONNECTIONS_BY_REGION_NAME = dict()
self.ENTITIES_BY_HEX = dict()
self.ENTITIES_BY_NAME = dict()
self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX = dict()
self.OBELISK_SIDE_ID_TO_EP_HEXES = dict()
self.EP_TO_OBELISK_SIDE = dict()
self.ENTITY_ID_TO_NAME = dict()
self.read_logic_file(lines)
self.reverse_connections()
self.combine_connections()
# Item data parsed from WitnessItems.txt
ALL_ITEMS: Dict[str, ItemDefinition] = {}
@ -276,12 +276,12 @@ def get_sigma_expert() -> StaticWitnessLogicObj:
return StaticWitnessLogicObj(get_sigma_expert_logic())
def __getattr__(name):
def __getattr__(name: str) -> StaticWitnessLogicObj:
if name == "vanilla":
return get_vanilla()
elif name == "sigma_normal":
if name == "sigma_normal":
return get_sigma_normal()
elif name == "sigma_expert":
if name == "sigma_expert":
return get_sigma_expert()
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")

View File

@ -1,7 +1,9 @@
from math import floor
from pkgutil import get_data
from random import random
from typing import Any, Collection, Dict, FrozenSet, Iterable, List, Set, Tuple
from random import Random
from typing import Any, Collection, Dict, FrozenSet, Iterable, List, Set, Tuple, TypeVar
T = TypeVar("T")
# A WitnessRule is just an or-chain of and-conditions.
# It represents the set of all options that could fulfill this requirement.
@ -11,9 +13,9 @@ from typing import Any, Collection, Dict, FrozenSet, Iterable, List, Set, Tuple
WitnessRule = FrozenSet[FrozenSet[str]]
def weighted_sample(world_random: random, population: List, weights: List[float], k: int) -> List:
def weighted_sample(world_random: Random, population: List[T], weights: List[float], k: int) -> List[T]:
positions = range(len(population))
indices = []
indices: List[int] = []
while True:
needed = k - len(indices)
if not needed:
@ -82,13 +84,13 @@ def define_new_region(region_string: str) -> Tuple[Dict[str, Any], Set[Tuple[str
region_obj = {
"name": region_name,
"shortName": region_name_simple,
"entities": list(),
"physical_entities": list(),
"entities": [],
"physical_entities": [],
}
return region_obj, options
def parse_lambda(lambda_string) -> WitnessRule:
def parse_lambda(lambda_string: str) -> WitnessRule:
"""
Turns a lambda String literal like this: a | b & c
into a set of sets like this: {{a}, {b, c}}
@ -97,18 +99,18 @@ def parse_lambda(lambda_string) -> WitnessRule:
if lambda_string == "True":
return frozenset([frozenset()])
split_ands = set(lambda_string.split(" | "))
lambda_set = frozenset({frozenset(a.split(" & ")) for a in split_ands})
return lambda_set
return frozenset({frozenset(a.split(" & ")) for a in split_ands})
_adjustment_file_cache = dict()
_adjustment_file_cache = {}
def get_adjustment_file(adjustment_file: str) -> List[str]:
if adjustment_file not in _adjustment_file_cache:
data = get_data(__name__, adjustment_file).decode("utf-8")
_adjustment_file_cache[adjustment_file] = [line.strip() for line in data.split("\n")]
data = get_data(__name__, adjustment_file)
if data is None:
raise FileNotFoundError(f"Could not find {adjustment_file}")
_adjustment_file_cache[adjustment_file] = [line.strip() for line in data.decode("utf-8").split("\n")]
return _adjustment_file_cache[adjustment_file]
@ -237,7 +239,7 @@ def logical_and_witness_rules(witness_rules: Iterable[WitnessRule]) -> WitnessRu
A logical formula might look like this: {{a, b}, {c, d}}, which would mean "a & b | c & d".
These can be easily and-ed by just using the boolean distributive law: (a | b) & c = a & c | a & b.
"""
current_overall_requirement = frozenset({frozenset()})
current_overall_requirement: FrozenSet[FrozenSet[str]] = frozenset({frozenset()})
for next_dnf_requirement in witness_rules:
new_requirement: Set[FrozenSet[str]] = set()

View File

@ -1,11 +1,12 @@
import logging
from dataclasses import dataclass
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Union
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Union
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld
from .data import static_logic as static_witness_logic
from .data.utils import weighted_sample
from .player_items import WitnessItem
if TYPE_CHECKING:
from . import WitnessWorld
@ -22,7 +23,9 @@ class WitnessLocationHint:
def __hash__(self) -> int:
return hash(self.location)
def __eq__(self, other) -> bool:
def __eq__(self, other: Any) -> bool:
if not isinstance(other, WitnessLocationHint):
return False
return self.location == other.location
@ -171,9 +174,13 @@ def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint) -> Witnes
location_name += " (" + world.multiworld.get_player_name(hint.location.player) + ")"
item = hint.location.item
item_name = item.name
if item.player != world.player:
item_name += " (" + world.multiworld.get_player_name(item.player) + ")"
item_name = "Nothing"
if item is not None:
item_name = item.name
if item.player != world.player:
item_name += " (" + world.multiworld.get_player_name(item.player) + ")"
if hint.hint_came_from_location:
hint_text = f"{location_name} contains {item_name}."
@ -183,14 +190,17 @@ def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint) -> Witnes
return WitnessWordedHint(hint_text, hint.location)
def hint_from_item(world: "WitnessWorld", item_name: str, own_itempool: List[Item]) -> Optional[WitnessLocationHint]:
def get_real_location(multiworld: MultiWorld, location: Location):
def hint_from_item(world: "WitnessWorld", item_name: str,
own_itempool: List["WitnessItem"]) -> Optional[WitnessLocationHint]:
def get_real_location(multiworld: MultiWorld, location: Location) -> Location:
"""If this location is from an item_link pseudo-world, get the location that the item_link item is on.
Return the original location otherwise / as a fallback."""
if location.player not in world.multiworld.groups:
return location
try:
if not location.item:
return location
return multiworld.find_item(location.item.name, location.player)
except StopIteration:
return location
@ -209,17 +219,11 @@ def hint_from_item(world: "WitnessWorld", item_name: str, own_itempool: List[Ite
def hint_from_location(world: "WitnessWorld", location: str) -> Optional[WitnessLocationHint]:
location_obj = world.get_location(location)
item_obj = location_obj.item
item_name = item_obj.name
if item_obj.player != world.player:
item_name += " (" + world.multiworld.get_player_name(item_obj.player) + ")"
return WitnessLocationHint(location_obj, True)
return WitnessLocationHint(world.get_location(location), True)
def get_items_and_locations_in_random_order(world: "WitnessWorld",
own_itempool: List[Item]) -> Tuple[List[str], List[str]]:
own_itempool: List["WitnessItem"]) -> Tuple[List[str], List[str]]:
prog_items_in_this_world = sorted(
item.name for item in own_itempool
if item.advancement and item.code and item.location
@ -235,7 +239,7 @@ def get_items_and_locations_in_random_order(world: "WitnessWorld",
return prog_items_in_this_world, locations_in_this_world
def make_always_and_priority_hints(world: "WitnessWorld", own_itempool: List[Item],
def make_always_and_priority_hints(world: "WitnessWorld", own_itempool: List["WitnessItem"],
already_hinted_locations: Set[Location]
) -> Tuple[List[WitnessLocationHint], List[WitnessLocationHint]]:
prog_items_in_this_world, loc_in_this_world = get_items_and_locations_in_random_order(world, own_itempool)
@ -282,14 +286,14 @@ def make_always_and_priority_hints(world: "WitnessWorld", own_itempool: List[Ite
return always_hints, priority_hints
def make_extra_location_hints(world: "WitnessWorld", hint_amount: int, own_itempool: List[Item],
def make_extra_location_hints(world: "WitnessWorld", hint_amount: int, own_itempool: List["WitnessItem"],
already_hinted_locations: Set[Location], hints_to_use_first: List[WitnessLocationHint],
unhinted_locations_for_hinted_areas: Dict[str, Set[Location]]) -> List[WitnessWordedHint]:
prog_items_in_this_world, locations_in_this_world = get_items_and_locations_in_random_order(world, own_itempool)
next_random_hint_is_location = world.random.randrange(0, 2)
hints = []
hints: List[WitnessWordedHint] = []
# This is a way to reverse a Dict[a,List[b]] to a Dict[b,a]
area_reverse_lookup = {
@ -304,6 +308,7 @@ def make_extra_location_hints(world: "WitnessWorld", hint_amount: int, own_itemp
logging.warning(f"Ran out of items/locations to hint for player {player_name}.")
break
location_hint: Optional[WitnessLocationHint]
if hints_to_use_first:
location_hint = hints_to_use_first.pop()
elif next_random_hint_is_location and locations_in_this_world:
@ -317,7 +322,7 @@ def make_extra_location_hints(world: "WitnessWorld", hint_amount: int, own_itemp
next_random_hint_is_location = not next_random_hint_is_location
continue
if not location_hint or location_hint.location in already_hinted_locations:
if location_hint is None or location_hint.location in already_hinted_locations:
continue
# Don't hint locations in areas that are almost fully hinted out already
@ -344,8 +349,8 @@ def choose_areas(world: "WitnessWorld", amount: int, locations_per_area: Dict[st
When this happens, they are made less likely to receive an area hint.
"""
unhinted_locations_per_area = dict()
unhinted_location_percentage_per_area = dict()
unhinted_locations_per_area = {}
unhinted_location_percentage_per_area = {}
for area_name, locations in locations_per_area.items():
not_yet_hinted_locations = sum(location not in already_hinted_locations for location in locations)
@ -368,8 +373,8 @@ def choose_areas(world: "WitnessWorld", amount: int, locations_per_area: Dict[st
def get_hintable_areas(world: "WitnessWorld") -> Tuple[Dict[str, List[Location]], Dict[str, List[Item]]]:
potential_areas = list(static_witness_logic.ALL_AREAS_BY_NAME.keys())
locations_per_area = dict()
items_per_area = dict()
locations_per_area = {}
items_per_area = {}
for area in potential_areas:
regions = [
@ -533,7 +538,7 @@ def create_all_hints(world: "WitnessWorld", hint_amount: int, area_hints: int,
location_hints_created_in_round_1 = len(generated_hints)
unhinted_locations_per_area: Dict[str, Set[Location]] = dict()
unhinted_locations_per_area: Dict[str, Set[Location]] = {}
# Then, make area hints.
if area_hints:
@ -584,17 +589,29 @@ def make_compact_hint_data(hint: WitnessWordedHint, local_player_number: int) ->
location = hint.location
area_amount = hint.area_amount
# None if junk hint, address if location hint, area string if area hint
arg_1 = location.address if location else (hint.area if hint.area else None)
# -1 if junk hint, address if location hint, area string if area hint
arg_1: Union[str, int]
if location and location.address is not None:
arg_1 = location.address
elif hint.area is not None:
arg_1 = hint.area
else:
arg_1 = -1
# self.player if junk hint, player if location hint, progression amount if area hint
arg_2 = area_amount if area_amount is not None else (location.player if location else local_player_number)
arg_2: int
if area_amount is not None:
arg_2 = area_amount
elif location is not None:
arg_2 = location.player
else:
arg_2 = local_player_number
return hint.wording, arg_1, arg_2
def make_laser_hints(world: "WitnessWorld", laser_names: List[str]) -> Dict[str, WitnessWordedHint]:
laser_hints_by_name = dict()
laser_hints_by_name = {}
for item_name in laser_names:
location_hint = hint_from_item(world, item_name, world.own_itempool)

View File

@ -61,9 +61,7 @@ class WitnessPlayerLocations:
sorted(self.CHECK_PANELHEX_TO_ID.items(), key=lambda item: item[1])
)
event_locations = {
p for p in player_logic.USED_EVENT_NAMES_BY_HEX
}
event_locations = set(player_logic.USED_EVENT_NAMES_BY_HEX)
self.EVENT_LOCATION_TABLE = {
static_witness_locations.get_event_name(entity_hex): None
@ -80,5 +78,5 @@ class WitnessPlayerLocations:
def add_location_late(self, entity_name: str) -> None:
entity_hex = static_witness_logic.ENTITIES_BY_NAME[entity_name]["entity_hex"]
self.CHECK_LOCATION_TABLE[entity_hex] = entity_name
self.CHECK_LOCATION_TABLE[entity_hex] = static_witness_locations.get_id(entity_hex)
self.CHECK_PANELHEX_TO_ID[entity_hex] = static_witness_locations.get_id(entity_hex)

View File

@ -2,7 +2,7 @@
Defines progression, junk and event items for The Witness
"""
import copy
from typing import TYPE_CHECKING, Dict, List, Set
from typing import TYPE_CHECKING, Dict, List, Set, cast
from BaseClasses import Item, ItemClassification, MultiWorld
@ -87,7 +87,8 @@ class WitnessPlayerItems:
if data.classification == ItemClassification.useful}.items():
if item_name in static_witness_items._special_usefuls:
continue
elif item_name == "Energy Capacity":
if item_name == "Energy Capacity":
self._mandatory_items[item_name] = NUM_ENERGY_UPGRADES
elif isinstance(item_data.classification, ProgressiveItemDefinition):
self._mandatory_items[item_name] = len(item_data.mappings)
@ -184,15 +185,16 @@ class WitnessPlayerItems:
output -= {item for item, weight in inner_item.items() if weight}
# Sort the output for consistency across versions if the implementation changes but the logic does not.
return sorted(list(output))
return sorted(output)
def get_door_ids_in_pool(self) -> List[int]:
"""
Returns the total set of all door IDs that are controlled by items in the pool.
"""
output: List[int] = []
for item_name, item_data in {name: data for name, data in self.item_data.items()
if isinstance(data.definition, DoorItemDefinition)}.items():
for item_name, item_data in dict(self.item_data.items()).items():
if not isinstance(item_data.definition, DoorItemDefinition):
continue
output += [int(hex_string, 16) for hex_string in item_data.definition.panel_id_hexes]
return output
@ -201,18 +203,21 @@ class WitnessPlayerItems:
"""
Returns the item IDs of symbol items that were defined in the configuration file but are not in the pool.
"""
return [data.ap_code for name, data in static_witness_items.ITEM_DATA.items()
if name not in self.item_data.keys() and data.definition.category is ItemCategory.SYMBOL]
return [
# data.ap_code is guaranteed for a symbol definition
cast(int, data.ap_code) for name, data in static_witness_items.ITEM_DATA.items()
if name not in self.item_data.keys() and data.definition.category is ItemCategory.SYMBOL
]
def get_progressive_item_ids_in_pool(self) -> Dict[int, List[int]]:
output: Dict[int, List[int]] = {}
for item_name, quantity in {name: quantity for name, quantity in self._mandatory_items.items()}.items():
for item_name, quantity in dict(self._mandatory_items.items()).items():
item = self.item_data[item_name]
if isinstance(item.definition, ProgressiveItemDefinition):
# Note: we need to reference the static table here rather than the player-specific one because the child
# items were removed from the pool when we pruned out all progression items not in the settings.
output[item.ap_code] = [static_witness_items.ITEM_DATA[child_item].ap_code
for child_item in item.definition.child_item_names]
output[cast(int, item.ap_code)] = [cast(int, static_witness_items.ITEM_DATA[child_item].ap_code)
for child_item in item.definition.child_item_names]
return output

View File

@ -22,6 +22,7 @@ from typing import TYPE_CHECKING, Dict, List, Set, Tuple, cast
from .data import static_logic as static_witness_logic
from .data.item_definition_classes import DoorItemDefinition, ItemCategory, ProgressiveItemDefinition
from .data.static_logic import StaticWitnessLogicObj
from .data.utils import (
WitnessRule,
define_new_region,
@ -58,6 +59,95 @@ if TYPE_CHECKING:
class WitnessPlayerLogic:
"""WITNESS LOGIC CLASS"""
VICTORY_LOCATION: str
def __init__(self, world: "WitnessWorld", disabled_locations: Set[str], start_inv: Dict[str, int]) -> None:
self.YAML_DISABLED_LOCATIONS: Set[str] = disabled_locations
self.YAML_ADDED_ITEMS: Dict[str, int] = start_inv
self.EVENT_PANELS_FROM_PANELS: Set[str] = set()
self.EVENT_PANELS_FROM_REGIONS: Set[str] = set()
self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES: Set[str] = set()
self.ENTITIES_WITHOUT_ENSURED_SOLVABILITY: Set[str] = set()
self.UNREACHABLE_REGIONS: Set[str] = set()
self.THEORETICAL_ITEMS: Set[str] = set()
self.THEORETICAL_ITEMS_NO_MULTI: Set[str] = set()
self.MULTI_AMOUNTS: Dict[str, int] = defaultdict(lambda: 1)
self.MULTI_LISTS: Dict[str, List[str]] = {}
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI: Set[str] = set()
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME: Set[str] = set()
self.DOOR_ITEMS_BY_ID: Dict[str, List[str]] = {}
self.STARTING_INVENTORY: Set[str] = set()
self.DIFFICULTY = world.options.puzzle_randomization
self.REFERENCE_LOGIC: StaticWitnessLogicObj
if self.DIFFICULTY == "sigma_expert":
self.REFERENCE_LOGIC = static_witness_logic.sigma_expert
elif self.DIFFICULTY == "none":
self.REFERENCE_LOGIC = static_witness_logic.vanilla
else:
self.REFERENCE_LOGIC = static_witness_logic.sigma_normal
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL: Dict[str, Set[Tuple[str, WitnessRule]]] = copy.deepcopy(
self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME
)
self.CONNECTIONS_BY_REGION_NAME: Dict[str, Set[Tuple[str, WitnessRule]]] = copy.deepcopy(
self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME
)
self.DEPENDENT_REQUIREMENTS_BY_HEX: Dict[str, Dict[str, WitnessRule]] = copy.deepcopy(
self.REFERENCE_LOGIC.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX
)
self.REQUIREMENTS_BY_HEX: Dict[str, WitnessRule] = {}
self.EVENT_ITEM_PAIRS: Dict[str, str] = {}
self.COMPLETELY_DISABLED_ENTITIES: Set[str] = set()
self.DISABLE_EVERYTHING_BEHIND: Set[str] = set()
self.PRECOMPLETED_LOCATIONS: Set[str] = set()
self.EXCLUDED_LOCATIONS: Set[str] = set()
self.ADDED_CHECKS: Set[str] = set()
self.VICTORY_LOCATION = "0x0356B"
self.ALWAYS_EVENT_NAMES_BY_HEX = {
"0x00509": "+1 Laser (Symmetry Laser)",
"0x012FB": "+1 Laser (Desert Laser)",
"0x09F98": "Desert Laser Redirection",
"0x01539": "+1 Laser (Quarry Laser)",
"0x181B3": "+1 Laser (Shadows Laser)",
"0x014BB": "+1 Laser (Keep Laser)",
"0x17C65": "+1 Laser (Monastery Laser)",
"0x032F9": "+1 Laser (Town Laser)",
"0x00274": "+1 Laser (Jungle Laser)",
"0x0C2B2": "+1 Laser (Bunker Laser)",
"0x00BF6": "+1 Laser (Swamp Laser)",
"0x028A4": "+1 Laser (Treehouse Laser)",
"0x17C34": "Mountain Entry",
"0xFFF00": "Bottom Floor Discard Turns On",
}
self.USED_EVENT_NAMES_BY_HEX: Dict[str, str] = {}
self.CONDITIONAL_EVENTS: Dict[Tuple[str, str], str] = {}
# The basic requirements to solve each entity come from StaticWitnessLogic.
# However, for any given world, the options (e.g. which item shuffles are enabled) affect the requirements.
self.make_options_adjustments(world)
self.determine_unrequired_entities(world)
self.find_unsolvable_entities(world)
# After we have adjusted the raw requirements, we perform a dependency reduction for the entity requirements.
# This will make the access conditions way faster, instead of recursively checking dependent entities each time.
self.make_dependency_reduced_checklist()
# Finalize which items actually exist in the MultiWorld and which get grouped into progressive items.
self.finalize_items()
# Create event-item pairs for specific panels in the game.
self.make_event_panel_lists()
def reduce_req_within_region(self, entity_hex: str) -> WitnessRule:
"""
Panels in this game often only turn on when other panels are solved.
@ -77,9 +167,9 @@ class WitnessPlayerLogic:
# For the requirement of an entity, we consider two things:
# 1. Any items this entity needs (e.g. Symbols or Door Items)
these_items = self.DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex].get("items", frozenset({frozenset()}))
these_items: WitnessRule = self.DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex].get("items", frozenset({frozenset()}))
# 2. Any entities that this entity depends on (e.g. one panel powering on the next panel in a set)
these_panels = self.DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex]["entities"]
these_panels: WitnessRule = self.DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex]["entities"]
# Remove any items that don't actually exist in the settings (e.g. Symbol Shuffle turned off)
these_items = frozenset({
@ -91,47 +181,49 @@ class WitnessPlayerLogic:
for subset in these_items:
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(subset)
# If this entity is opened by a door item that exists in the itempool, add that item to its requirements.
# Also, remove any original power requirements this entity might have had.
# Handle door entities (door shuffle)
if entity_hex in self.DOOR_ITEMS_BY_ID:
# If this entity is opened by a door item that exists in the itempool, add that item to its requirements.
door_items = frozenset({frozenset([item]) for item in self.DOOR_ITEMS_BY_ID[entity_hex]})
for dependent_item in door_items:
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(dependent_item)
all_options = logical_and_witness_rules([door_items, these_items])
these_items = logical_and_witness_rules([door_items, these_items])
# If this entity is not an EP, and it has an associated door item, ignore the original power dependencies
if static_witness_logic.ENTITIES_BY_HEX[entity_hex]["entityType"] != "EP":
# A door entity is opened by its door item instead of previous entities powering it.
# That means we need to ignore any dependent requirements.
# However, there are some entities that depend on other entities because of an environmental reason.
# Those requirements need to be preserved even in door shuffle.
entity_dependencies_need_to_be_preserved = (
# EPs keep all their entity dependencies
static_witness_logic.ENTITIES_BY_HEX[entity_hex]["entityType"] != "EP"
# 0x28A0D depends on another entity for *non-power* reasons -> This dependency needs to be preserved,
# except in Expert, where that dependency doesn't exist, but now there *is* a power dependency.
# In the future, it'd be wise to make a distinction between "power dependencies" and other dependencies.
if entity_hex == "0x28A0D" and not any("0x28998" in option for option in these_panels):
these_items = all_options
or entity_hex == "0x28A0D" and not any("0x28998" in option for option in these_panels)
# Another dependency that is not power-based: The Symmetry Island Upper Panel latches
elif entity_hex == "0x1C349":
these_items = all_options
or entity_hex == "0x1C349"
)
else:
return frozenset(all_options)
else:
these_items = all_options
# If this is not one of those special cases, solving this door entity only needs its own item requirement.
# Dependent entities from these_panels are ignored, and we just return these_items directly.
if not entity_dependencies_need_to_be_preserved:
return these_items
# Now that we have item requirements and entity dependencies, it's time for the dependency reduction.
# For each entity that this entity depends on (e.g. a panel turning on another panel),
# Add that entities requirements to this entity.
# If there are multiple options, consider each, and then or-chain them.
all_options = list()
all_options = []
for option in these_panels:
dependent_items_for_option = frozenset({frozenset()})
dependent_items_for_option: WitnessRule = frozenset({frozenset()})
# For each entity in this option, resolve it to its actual requirement.
for option_entity in option:
dep_obj = self.REFERENCE_LOGIC.ENTITIES_BY_HEX.get(option_entity)
dep_obj = self.REFERENCE_LOGIC.ENTITIES_BY_HEX.get(option_entity, {})
if option_entity in {"7 Lasers", "11 Lasers", "7 Lasers + Redirect", "11 Lasers + Redirect",
"PP2 Weirdness", "Theater to Tunnels"}:
@ -525,13 +617,16 @@ class WitnessPlayerLogic:
current_adjustment_type = line[:-1]
continue
if current_adjustment_type is None:
raise ValueError(f"Adjustment lineset {adjustment_lineset} is malformed")
self.make_single_adjustment(current_adjustment_type, line)
for entity_id in self.COMPLETELY_DISABLED_ENTITIES:
if entity_id in self.DOOR_ITEMS_BY_ID:
del self.DOOR_ITEMS_BY_ID[entity_id]
def discover_reachable_regions(self):
def discover_reachable_regions(self) -> Set[str]:
"""
Some options disable panels or remove specific items.
This can make entire regions completely unreachable, because all their incoming connections are invalid.
@ -640,7 +735,7 @@ class WitnessPlayerLogic:
# Check each traversal option individually
for option in connection[1]:
individual_entity_requirements = []
individual_entity_requirements: List[WitnessRule] = []
for entity in option:
# If a connection requires solving a disabled entity, it is not valid.
if not self.solvability_guaranteed(entity) or entity in self.DISABLE_EVERYTHING_BEHIND:
@ -664,7 +759,7 @@ class WitnessPlayerLogic:
return logical_or_witness_rules(all_possibilities)
def make_dependency_reduced_checklist(self):
def make_dependency_reduced_checklist(self) -> None:
"""
Every entity has a requirement. This requirement may involve other entities.
Example: Solving a panel powers a cable, and that cable turns on the next panel.
@ -679,12 +774,12 @@ class WitnessPlayerLogic:
# Requirements are cached per entity. However, we might redo the whole reduction process multiple times.
# So, we first clear this cache.
self.REQUIREMENTS_BY_HEX = dict()
self.REQUIREMENTS_BY_HEX = {}
# We also clear any data structures that we might have filled in a previous dependency reduction
self.REQUIREMENTS_BY_HEX = dict()
self.USED_EVENT_NAMES_BY_HEX = dict()
self.CONNECTIONS_BY_REGION_NAME = dict()
self.REQUIREMENTS_BY_HEX = {}
self.USED_EVENT_NAMES_BY_HEX = {}
self.CONNECTIONS_BY_REGION_NAME = {}
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI = set()
# Make independent requirements for entities
@ -695,22 +790,18 @@ class WitnessPlayerLogic:
# Make independent region connection requirements based on the entities they require
for region, connections in self.CONNECTIONS_BY_REGION_NAME_THEORETICAL.items():
self.CONNECTIONS_BY_REGION_NAME[region] = []
new_connections = []
new_connections = set()
for connection in connections:
overall_requirement = self.reduce_connection_requirement(connection)
# If there is a way to use this connection, add it.
if overall_requirement:
new_connections.append((connection[0], overall_requirement))
new_connections.add((connection[0], overall_requirement))
# If there are any usable outgoing connections from this region, add them.
if new_connections:
self.CONNECTIONS_BY_REGION_NAME[region] = new_connections
self.CONNECTIONS_BY_REGION_NAME[region] = new_connections
def finalize_items(self):
def finalize_items(self) -> None:
"""
Finalise which items are used in the world, and handle their progressive versions.
"""
@ -808,8 +899,7 @@ class WitnessPlayerLogic:
if entity_hex not in self.USED_EVENT_NAMES_BY_HEX:
warning(f'Entity "{name}" does not have an associated event name.')
self.USED_EVENT_NAMES_BY_HEX[entity_hex] = name + " Event"
pair = (name, self.USED_EVENT_NAMES_BY_HEX[entity_hex])
return pair
return (name, self.USED_EVENT_NAMES_BY_HEX[entity_hex])
def make_event_panel_lists(self) -> None:
"""
@ -828,85 +918,3 @@ class WitnessPlayerLogic:
for panel in self.USED_EVENT_NAMES_BY_HEX:
pair = self.make_event_item_pair(panel)
self.EVENT_ITEM_PAIRS[pair[0]] = pair[1]
def __init__(self, world: "WitnessWorld", disabled_locations: Set[str], start_inv: Dict[str, int]) -> None:
self.YAML_DISABLED_LOCATIONS = disabled_locations
self.YAML_ADDED_ITEMS = start_inv
self.EVENT_PANELS_FROM_PANELS = set()
self.EVENT_PANELS_FROM_REGIONS = set()
self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES = set()
self.ENTITIES_WITHOUT_ENSURED_SOLVABILITY = set()
self.UNREACHABLE_REGIONS = set()
self.THEORETICAL_ITEMS = set()
self.THEORETICAL_ITEMS_NO_MULTI = set()
self.MULTI_AMOUNTS = defaultdict(lambda: 1)
self.MULTI_LISTS = dict()
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI = set()
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME = set()
self.DOOR_ITEMS_BY_ID: Dict[str, List[str]] = {}
self.STARTING_INVENTORY = set()
self.DIFFICULTY = world.options.puzzle_randomization
if self.DIFFICULTY == "sigma_normal":
self.REFERENCE_LOGIC = static_witness_logic.sigma_normal
elif self.DIFFICULTY == "sigma_expert":
self.REFERENCE_LOGIC = static_witness_logic.sigma_expert
elif self.DIFFICULTY == "none":
self.REFERENCE_LOGIC = static_witness_logic.vanilla
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL = copy.deepcopy(
self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME
)
self.CONNECTIONS_BY_REGION_NAME = dict()
self.DEPENDENT_REQUIREMENTS_BY_HEX = copy.deepcopy(self.REFERENCE_LOGIC.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX)
self.REQUIREMENTS_BY_HEX = dict()
self.EVENT_ITEM_PAIRS = dict()
self.COMPLETELY_DISABLED_ENTITIES = set()
self.DISABLE_EVERYTHING_BEHIND = set()
self.PRECOMPLETED_LOCATIONS = set()
self.EXCLUDED_LOCATIONS = set()
self.ADDED_CHECKS = set()
self.VICTORY_LOCATION: str
self.ALWAYS_EVENT_NAMES_BY_HEX = {
"0x00509": "+1 Laser (Symmetry Laser)",
"0x012FB": "+1 Laser (Desert Laser)",
"0x09F98": "Desert Laser Redirection",
"0x01539": "+1 Laser (Quarry Laser)",
"0x181B3": "+1 Laser (Shadows Laser)",
"0x014BB": "+1 Laser (Keep Laser)",
"0x17C65": "+1 Laser (Monastery Laser)",
"0x032F9": "+1 Laser (Town Laser)",
"0x00274": "+1 Laser (Jungle Laser)",
"0x0C2B2": "+1 Laser (Bunker Laser)",
"0x00BF6": "+1 Laser (Swamp Laser)",
"0x028A4": "+1 Laser (Treehouse Laser)",
"0x17C34": "Mountain Entry",
"0xFFF00": "Bottom Floor Discard Turns On",
}
self.USED_EVENT_NAMES_BY_HEX = {}
self.CONDITIONAL_EVENTS = {}
# The basic requirements to solve each entity come from StaticWitnessLogic.
# However, for any given world, the options (e.g. which item shuffles are enabled) affect the requirements.
self.make_options_adjustments(world)
self.determine_unrequired_entities(world)
self.find_unsolvable_entities(world)
# After we have adjusted the raw requirements, we perform a dependency reduction for the entity requirements.
# This will make the access conditions way faster, instead of recursively checking dependent entities each time.
self.make_dependency_reduced_checklist()
# Finalize which items actually exist in the MultiWorld and which get grouped into progressive items.
self.finalize_items()
# Create event-item pairs for specific panels in the game.
self.make_event_panel_lists()

View File

@ -9,9 +9,11 @@ from BaseClasses import Entrance, Region
from worlds.generic.Rules import CollectionRule
from .data import static_locations as static_witness_locations
from .data import static_logic as static_witness_logic
from .data.static_logic import StaticWitnessLogicObj
from .data.utils import WitnessRule, optimize_witness_rule
from .locations import WitnessPlayerLocations, static_witness_locations
from .locations import WitnessPlayerLocations
from .player_logic import WitnessPlayerLogic
if TYPE_CHECKING:
@ -21,8 +23,20 @@ if TYPE_CHECKING:
class WitnessPlayerRegions:
"""Class that defines Witness Regions"""
player_locations = None
logic = None
def __init__(self, player_locations: WitnessPlayerLocations, world: "WitnessWorld") -> None:
difficulty = world.options.puzzle_randomization
self.reference_logic: StaticWitnessLogicObj
if difficulty == "sigma_normal":
self.reference_logic = static_witness_logic.sigma_normal
elif difficulty == "sigma_expert":
self.reference_logic = static_witness_logic.sigma_expert
else:
self.reference_logic = static_witness_logic.vanilla
self.player_locations = player_locations
self.two_way_entrance_register: Dict[Tuple[str, str], List[Entrance]] = defaultdict(lambda: [])
self.created_region_names: Set[str] = set()
@staticmethod
def make_lambda(item_requirement: WitnessRule, world: "WitnessWorld") -> CollectionRule:
@ -36,7 +50,7 @@ class WitnessPlayerRegions:
return _meets_item_requirements(item_requirement, world)
def connect_if_possible(self, world: "WitnessWorld", source: str, target: str, req: WitnessRule,
regions_by_name: Dict[str, Region]):
regions_by_name: Dict[str, Region]) -> None:
"""
connect two regions and set the corresponding requirement
"""
@ -89,8 +103,8 @@ class WitnessPlayerRegions:
"""
from . import create_region
all_locations = set()
regions_by_name = dict()
all_locations: Set[str] = set()
regions_by_name: Dict[str, Region] = {}
regions_to_create = {
k: v for k, v in self.reference_logic.ALL_REGIONS_BY_NAME.items()
@ -121,17 +135,3 @@ class WitnessPlayerRegions:
for region_name, region in regions_to_create.items():
for connection in player_logic.CONNECTIONS_BY_REGION_NAME[region_name]:
self.connect_if_possible(world, region_name, connection[0], connection[1], regions_by_name)
def __init__(self, player_locations: WitnessPlayerLocations, world: "WitnessWorld") -> None:
difficulty = world.options.puzzle_randomization
if difficulty == "sigma_normal":
self.reference_logic = static_witness_logic.sigma_normal
elif difficulty == "sigma_expert":
self.reference_logic = static_witness_logic.sigma_expert
elif difficulty == "none":
self.reference_logic = static_witness_logic.vanilla
self.player_locations = player_locations
self.two_way_entrance_register: Dict[Tuple[str, str], List[Entrance]] = defaultdict(lambda: [])
self.created_region_names: Set[str] = set()

View File

@ -1,10 +1,10 @@
line-length = 120
[lint]
select = ["E", "F", "W", "I", "N", "Q", "UP", "RUF", "ISC", "T20"]
ignore = ["RUF012", "RUF100"]
select = ["C", "E", "F", "R", "W", "I", "N", "Q", "UP", "RUF", "ISC", "T20"]
ignore = ["C9", "RUF012", "RUF100"]
[per-file-ignores]
[lint.per-file-ignores]
# The way options definitions work right now, I am forced to break line length requirements.
"options.py" = ["E501"]
# The import list would just be so big if I imported every option individually in presets.py

View File

@ -37,8 +37,8 @@ def _has_laser(laser_hex: str, world: "WitnessWorld", player: int, redirect_requ
_can_solve_panel(laser_hex, world, world.player, world.player_logic, world.player_locations)(state)
and state.has("Desert Laser Redirection", player)
)
else:
return _can_solve_panel(laser_hex, world, world.player, world.player_logic, world.player_locations)
return _can_solve_panel(laser_hex, world, world.player, world.player_logic, world.player_locations)
def _has_lasers(amount: int, world: "WitnessWorld", redirect_required: bool) -> CollectionRule:
@ -63,8 +63,8 @@ def _can_solve_panel(panel: str, world: "WitnessWorld", player: int, player_logi
if entity_name + " Solved" in player_locations.EVENT_LOCATION_TABLE:
return lambda state: state.has(player_logic.EVENT_ITEM_PAIRS[entity_name + " Solved"], player)
else:
return make_lambda(panel, world)
return make_lambda(panel, world)
def _can_do_expert_pp2(state: CollectionState, world: "WitnessWorld") -> bool:
@ -175,12 +175,10 @@ def _can_do_expert_pp2(state: CollectionState, world: "WitnessWorld") -> bool:
# We can get to Hedge 3 from Hedge 2. If we can get from Keep to Hedge 2, we're good.
# This covers both Hedge 1 Exit and Hedge 2 Shortcut, because Hedge 1 is just part of the Keep region.
hedge_2_from_keep = any(
return any(
e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep 2nd Maze", "Keep"]
)
return hedge_2_from_keep
def _can_do_theater_to_tunnels(state: CollectionState, world: "WitnessWorld") -> bool:
"""
@ -211,14 +209,12 @@ def _can_do_theater_to_tunnels(state: CollectionState, world: "WitnessWorld") ->
# We also need a way from Town to Tunnels.
tunnels_from_town = (
return (
any(e.can_reach(state) for e in player_regions.two_way_entrance_register["Tunnels", "Windmill Interior"])
and any(e.can_reach(state) for e in player_regions.two_way_entrance_register["Town", "Windmill Interior"])
or any(e.can_reach(state) for e in player_regions.two_way_entrance_register["Tunnels", "Town"])
)
return tunnels_from_town
def _has_item(item: str, world: "WitnessWorld", player: int,
player_logic: WitnessPlayerLogic, player_locations: WitnessPlayerLocations) -> CollectionRule:
@ -237,9 +233,9 @@ def _has_item(item: str, world: "WitnessWorld", player: int,
if item == "11 Lasers + Redirect":
laser_req = world.options.challenge_lasers.value
return _has_lasers(laser_req, world, True)
elif item == "PP2 Weirdness":
if item == "PP2 Weirdness":
return lambda state: _can_do_expert_pp2(state, world)
elif item == "Theater to Tunnels":
if item == "Theater to Tunnels":
return lambda state: _can_do_theater_to_tunnels(state, world)
if item in player_logic.USED_EVENT_NAMES_BY_HEX:
return _can_solve_panel(item, world, player, player_logic, player_locations)