diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index ecab25db3d71..455c87d8e0d1 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -11,11 +11,12 @@ 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 @@ def generate_early(self) -> None: ) 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 @@ def create_items(self) -> None: 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 @@ def create_items(self) -> None: 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)) diff --git a/worlds/witness/data/static_items.py b/worlds/witness/data/static_items.py index 8eb889f8203a..b0d8fc3c4f6e 100644 --- a/worlds/witness/data/static_items.py +++ b/worlds/witness/data/static_items.py @@ -1,4 +1,4 @@ -from typing import Dict, List +from typing import Dict, List, Set from BaseClasses import ItemClassification @@ -7,7 +7,7 @@ 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 diff --git a/worlds/witness/data/static_locations.py b/worlds/witness/data/static_locations.py index e11544235ffc..de321d20c0f9 100644 --- a/worlds/witness/data/static_locations.py +++ b/worlds/witness/data/static_locations.py @@ -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 @@ "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 @@ def get_event_name(entity_hex: str) -> str: 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) diff --git a/worlds/witness/data/static_logic.py b/worlds/witness/data/static_logic.py index ecd95ea6c0fa..a9175c0c30b3 100644 --- a/worlds/witness/data/static_logic.py +++ b/worlds/witness/data/static_logic.py @@ -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 @@ 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 @@ def read_logic_file(self, lines) -> None: 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 @@ def reverse_connection(self, source_region: str, connection: Tuple[str, Set[Witn 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 @@ def combine_connections(self): 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}'") diff --git a/worlds/witness/data/utils.py b/worlds/witness/data/utils.py index 2934308df3ec..f89aaf7d3e18 100644 --- a/worlds/witness/data/utils.py +++ b/worlds/witness/data/utils.py @@ -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 @@ 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() diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index 535a36e13b6f..a1ca1b081d3c 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -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 get_real_location(multiworld: MultiWorld, location: Location): 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) diff --git a/worlds/witness/locations.py b/worlds/witness/locations.py index df8214ac9221..1796f051b896 100644 --- a/worlds/witness/locations.py +++ b/worlds/witness/locations.py @@ -61,9 +61,7 @@ def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic) -> N 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 @@ def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic) -> N 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) diff --git a/worlds/witness/player_items.py b/worlds/witness/player_items.py index 627e5acccb90..718fd7d172ba 100644 --- a/worlds/witness/player_items.py +++ b/worlds/witness/player_items.py @@ -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 @@ def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic, 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 @@ def get_early_items(self) -> List[str]: 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 @@ def get_symbol_ids_not_in_pool(self) -> List[int]: """ 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 diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index 05b3cf3a98e4..b62c59b00ae1 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -22,6 +22,7 @@ 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 @@ 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 @@ def reduce_req_within_region(self, entity_hex: str) -> WitnessRule: # 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 @@ def reduce_req_within_region(self, entity_hex: str) -> WitnessRule: 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 - - else: - return frozenset(all_options) + or entity_hex == "0x1C349" + ) - 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 @@ def make_options_adjustments(self, world: "WitnessWorld") -> None: 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 @@ def reduce_connection_requirement(self, connection: Tuple[str, WitnessRule]) -> # 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 @@ def reduce_connection_requirement(self, connection: Tuple[str, WitnessRule]) -> 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 @@ def make_dependency_reduced_checklist(self): # 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 @@ def make_dependency_reduced_checklist(self): # 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 @@ def make_event_item_pair(self, entity_hex: str) -> Tuple[str, str]: 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 @@ def make_event_panel_lists(self) -> None: 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() diff --git a/worlds/witness/regions.py b/worlds/witness/regions.py index 35f4e9544212..2528c8abe22b 100644 --- a/worlds/witness/regions.py +++ b/worlds/witness/regions.py @@ -9,9 +9,11 @@ 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 @@ 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 @@ def make_lambda(item_requirement: WitnessRule, world: "WitnessWorld") -> Collect 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 @@ def create_regions(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic """ 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 @@ def create_regions(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic 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() diff --git a/worlds/witness/ruff.toml b/worlds/witness/ruff.toml index d42361a4aaa9..a35711cce66d 100644 --- a/worlds/witness/ruff.toml +++ b/worlds/witness/ruff.toml @@ -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 diff --git a/worlds/witness/rules.py b/worlds/witness/rules.py index b4982d1830b2..12a9a1ed4b59 100644 --- a/worlds/witness/rules.py +++ b/worlds/witness/rules.py @@ -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)