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 worlds.AutoWorld import WebWorld, World
from .data import static_items as static_witness_items 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 import static_logic as static_witness_logic
from .data.item_definition_classes import DoorItemDefinition, ItemData from .data.item_definition_classes import DoorItemDefinition, ItemData
from .data.utils import get_audio_logs from .data.utils import get_audio_logs
from .hints import CompactItemData, create_all_hints, make_compact_hint_data, make_laser_hints 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 .options import TheWitnessOptions, witness_option_groups
from .player_items import WitnessItem, WitnessPlayerItems from .player_items import WitnessItem, WitnessPlayerItems
from .player_logic import WitnessPlayerLogic from .player_logic import WitnessPlayerLogic
@ -53,7 +54,8 @@ class WitnessWorld(World):
options: TheWitnessOptions options: TheWitnessOptions
item_name_to_id = { 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 location_name_to_id = static_witness_locations.ALL_LOCATIONS_TO_ID
item_name_groups = static_witness_items.ITEM_GROUPS 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.player_regions: WitnessPlayerRegions = WitnessPlayerRegions(self.player_locations, self)
self.log_ids_to_hints = dict() self.log_ids_to_hints = {}
self.determine_sufficient_progression() self.determine_sufficient_progression()
@ -279,7 +281,7 @@ class WitnessWorld(World):
remaining_item_slots = pool_size - sum(item_pool.values()) remaining_item_slots = pool_size - sum(item_pool.values())
# Add puzzle skips. # 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: if num_puzzle_skips > remaining_item_slots:
warning(f"{self.multiworld.get_player_name(self.player)}'s Witness world has insufficient locations" 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: if self.player_items.item_data[item_name].local_only:
self.options.local_items.value.add(item_name) self.options.local_items.value.add(item_name)
def fill_slot_data(self) -> dict: def fill_slot_data(self) -> Dict[str, Any]:
self.log_ids_to_hints: Dict[int, CompactItemData] = dict() self.log_ids_to_hints: Dict[int, CompactItemData] = {}
self.laser_ids_to_hints: Dict[int, CompactItemData] = dict() self.laser_ids_to_hints: Dict[int, CompactItemData] = {}
already_hinted_locations = set() already_hinted_locations = set()
# Laser hints # Laser hints
if self.options.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(): for item_name, hint in laser_hints.items():
item_def = cast(DoorItemDefinition, static_witness_logic.ALL_ITEMS[item_name]) 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) 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 # Audio Log Hints
@ -378,13 +380,13 @@ class WitnessLocation(Location):
game: str = "The Witness" game: str = "The Witness"
entity_hex: int = -1 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) super().__init__(player, name, address, parent)
self.entity_hex = ch_hex self.entity_hex = ch_hex
def create_region(world: WitnessWorld, name: str, player_locations: WitnessPlayerLocations, 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 Create an Archipelago Region for The Witness
""" """
@ -399,11 +401,11 @@ def create_region(world: WitnessWorld, name: str, player_locations: WitnessPlaye
entity_hex = int( entity_hex = int(
static_witness_logic.ENTITIES_BY_NAME[location]["entity_hex"], 0 static_witness_logic.ENTITIES_BY_NAME[location]["entity_hex"], 0
) )
location = WitnessLocation( location_obj = WitnessLocation(
world.player, location, loc_id, ret, entity_hex world.player, location, loc_id, ret, entity_hex
) )
ret.locations.append(location) ret.locations.append(location_obj)
if exits: if exits:
for single_exit in exits: for single_exit in exits:
ret.exits.append(Entrance(world.player, single_exit, ret)) 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 from BaseClasses import ItemClassification
@ -7,7 +7,7 @@ from .item_definition_classes import DoorItemDefinition, ItemCategory, ItemData
from .static_locations import ID_START from .static_locations import ID_START
ITEM_DATA: Dict[str, ItemData] = {} 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 # 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. # item list during get_progression_items.
@ -22,13 +22,13 @@ def populate_items() -> None:
if definition.category is ItemCategory.SYMBOL: if definition.category is ItemCategory.SYMBOL:
classification = ItemClassification.progression classification = ItemClassification.progression
ITEM_GROUPS.setdefault("Symbols", []).append(item_name) ITEM_GROUPS.setdefault("Symbols", set()).add(item_name)
elif definition.category is ItemCategory.DOOR: elif definition.category is ItemCategory.DOOR:
classification = ItemClassification.progression classification = ItemClassification.progression
ITEM_GROUPS.setdefault("Doors", []).append(item_name) ITEM_GROUPS.setdefault("Doors", set()).add(item_name)
elif definition.category is ItemCategory.LASER: elif definition.category is ItemCategory.LASER:
classification = ItemClassification.progression_skip_balancing 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: elif definition.category is ItemCategory.USEFUL:
classification = ItemClassification.useful classification = ItemClassification.useful
elif definition.category is ItemCategory.FILLER: elif definition.category is ItemCategory.FILLER:
@ -47,7 +47,7 @@ def populate_items() -> None:
def get_item_to_door_mappings() -> Dict[int, List[int]]: def get_item_to_door_mappings() -> Dict[int, List[int]]:
output: Dict[int, List[int]] = {} output: Dict[int, List[int]] = {}
for item_name, item_data in ITEM_DATA.items(): 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 continue
output[item_data.ap_code] = [int(hex_string, 16) for hex_string in item_data.definition.panel_id_hexes] output[item_data.ap_code] = [int(hex_string, 16) for hex_string in item_data.definition.panel_id_hexes]
return output return output

View File

@ -1,3 +1,5 @@
from typing import Dict, Set, cast
from . import static_logic as static_witness_logic from . import static_logic as static_witness_logic
ID_START = 158000 ID_START = 158000
@ -441,17 +443,17 @@ OBELISK_SIDES = {
"Town Obelisk Side 6", "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 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: 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" 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 = { ALL_LOCATIONS_TO_IDS = {
@ -479,4 +481,4 @@ for key, item in ALL_LOCATIONS_TO_IDS.items():
for loc in ALL_LOCATIONS_TO_IDS: for loc in ALL_LOCATIONS_TO_IDS:
area = static_witness_logic.ENTITIES_BY_NAME[loc]["area"]["name"] 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 collections import defaultdict
from typing import Dict, List, Set, Tuple from typing import Any, Dict, List, Optional, Set, Tuple
from Utils import cache_argsless from Utils import cache_argsless
@ -24,13 +24,37 @@ from .utils import (
class StaticWitnessLogicObj: 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 Reads the logic file and does the initial population of data structures
""" """
current_region = dict() current_region = {}
current_area = { current_area: Dict[str, Any] = {
"name": "Misc", "name": "Misc",
"regions": [], "regions": [],
} }
@ -155,7 +179,7 @@ class StaticWitnessLogicObj:
current_region["entities"].append(entity_hex) current_region["entities"].append(entity_hex)
current_region["physical_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] target = connection[0]
traversal_options = connection[1] traversal_options = connection[1]
@ -169,13 +193,13 @@ class StaticWitnessLogicObj:
if remaining_options: if remaining_options:
self.CONNECTIONS_WITH_DUPLICATES[target][source_region].add(frozenset(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 # Iterate all connections
for region_name, connections in list(self.CONNECTIONS_WITH_DUPLICATES.items()): for region_name, connections in list(self.CONNECTIONS_WITH_DUPLICATES.items()):
for connection in connections.items(): for connection in connections.items():
self.reverse_connection(region_name, connection) 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. # 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} 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) combined_req = logical_or_witness_rules(requirement)
self.STATIC_CONNECTIONS_BY_REGION_NAME[source].add((target, combined_req)) 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 # Item data parsed from WitnessItems.txt
ALL_ITEMS: Dict[str, ItemDefinition] = {} ALL_ITEMS: Dict[str, ItemDefinition] = {}
@ -276,12 +276,12 @@ def get_sigma_expert() -> StaticWitnessLogicObj:
return StaticWitnessLogicObj(get_sigma_expert_logic()) return StaticWitnessLogicObj(get_sigma_expert_logic())
def __getattr__(name): def __getattr__(name: str) -> StaticWitnessLogicObj:
if name == "vanilla": if name == "vanilla":
return get_vanilla() return get_vanilla()
elif name == "sigma_normal": if name == "sigma_normal":
return get_sigma_normal() return get_sigma_normal()
elif name == "sigma_expert": if name == "sigma_expert":
return get_sigma_expert() return get_sigma_expert()
raise AttributeError(f"module '{__name__}' has no attribute '{name}'") raise AttributeError(f"module '{__name__}' has no attribute '{name}'")

View File

@ -1,7 +1,9 @@
from math import floor from math import floor
from pkgutil import get_data from pkgutil import get_data
from random import random from random import Random
from typing import Any, Collection, Dict, FrozenSet, Iterable, List, Set, Tuple 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. # A WitnessRule is just an or-chain of and-conditions.
# It represents the set of all options that could fulfill this requirement. # 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]] 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)) positions = range(len(population))
indices = [] indices: List[int] = []
while True: while True:
needed = k - len(indices) needed = k - len(indices)
if not needed: if not needed:
@ -82,13 +84,13 @@ def define_new_region(region_string: str) -> Tuple[Dict[str, Any], Set[Tuple[str
region_obj = { region_obj = {
"name": region_name, "name": region_name,
"shortName": region_name_simple, "shortName": region_name_simple,
"entities": list(), "entities": [],
"physical_entities": list(), "physical_entities": [],
} }
return region_obj, options 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 Turns a lambda String literal like this: a | b & c
into a set of sets 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": if lambda_string == "True":
return frozenset([frozenset()]) return frozenset([frozenset()])
split_ands = set(lambda_string.split(" | ")) split_ands = set(lambda_string.split(" | "))
lambda_set = frozenset({frozenset(a.split(" & ")) for a in split_ands}) return frozenset({frozenset(a.split(" & ")) for a in split_ands})
return lambda_set
_adjustment_file_cache = dict() _adjustment_file_cache = {}
def get_adjustment_file(adjustment_file: str) -> List[str]: def get_adjustment_file(adjustment_file: str) -> List[str]:
if adjustment_file not in _adjustment_file_cache: if adjustment_file not in _adjustment_file_cache:
data = get_data(__name__, adjustment_file).decode("utf-8") data = get_data(__name__, adjustment_file)
_adjustment_file_cache[adjustment_file] = [line.strip() for line in data.split("\n")] 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] 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". 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. 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: for next_dnf_requirement in witness_rules:
new_requirement: Set[FrozenSet[str]] = set() new_requirement: Set[FrozenSet[str]] = set()

View File

@ -1,11 +1,12 @@
import logging import logging
from dataclasses import dataclass 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 BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld
from .data import static_logic as static_witness_logic from .data import static_logic as static_witness_logic
from .data.utils import weighted_sample from .data.utils import weighted_sample
from .player_items import WitnessItem
if TYPE_CHECKING: if TYPE_CHECKING:
from . import WitnessWorld from . import WitnessWorld
@ -22,7 +23,9 @@ class WitnessLocationHint:
def __hash__(self) -> int: def __hash__(self) -> int:
return hash(self.location) 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 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) + ")" location_name += " (" + world.multiworld.get_player_name(hint.location.player) + ")"
item = hint.location.item item = hint.location.item
item_name = item.name
if item.player != world.player: item_name = "Nothing"
item_name += " (" + world.multiworld.get_player_name(item.player) + ")" 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: if hint.hint_came_from_location:
hint_text = f"{location_name} contains {item_name}." 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) return WitnessWordedHint(hint_text, hint.location)
def hint_from_item(world: "WitnessWorld", item_name: str, own_itempool: List[Item]) -> Optional[WitnessLocationHint]: def hint_from_item(world: "WitnessWorld", item_name: str,
def get_real_location(multiworld: MultiWorld, location: Location): 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. """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.""" Return the original location otherwise / as a fallback."""
if location.player not in world.multiworld.groups: if location.player not in world.multiworld.groups:
return location return location
try: try:
if not location.item:
return location
return multiworld.find_item(location.item.name, location.player) return multiworld.find_item(location.item.name, location.player)
except StopIteration: except StopIteration:
return location 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]: def hint_from_location(world: "WitnessWorld", location: str) -> Optional[WitnessLocationHint]:
location_obj = world.get_location(location) return WitnessLocationHint(world.get_location(location), True)
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)
def get_items_and_locations_in_random_order(world: "WitnessWorld", 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( prog_items_in_this_world = sorted(
item.name for item in own_itempool item.name for item in own_itempool
if item.advancement and item.code and item.location 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 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] already_hinted_locations: Set[Location]
) -> Tuple[List[WitnessLocationHint], List[WitnessLocationHint]]: ) -> Tuple[List[WitnessLocationHint], List[WitnessLocationHint]]:
prog_items_in_this_world, loc_in_this_world = get_items_and_locations_in_random_order(world, own_itempool) 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 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], already_hinted_locations: Set[Location], hints_to_use_first: List[WitnessLocationHint],
unhinted_locations_for_hinted_areas: Dict[str, Set[Location]]) -> List[WitnessWordedHint]: 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) 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) 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] # This is a way to reverse a Dict[a,List[b]] to a Dict[b,a]
area_reverse_lookup = { 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}.") logging.warning(f"Ran out of items/locations to hint for player {player_name}.")
break break
location_hint: Optional[WitnessLocationHint]
if hints_to_use_first: if hints_to_use_first:
location_hint = hints_to_use_first.pop() location_hint = hints_to_use_first.pop()
elif next_random_hint_is_location and locations_in_this_world: 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 next_random_hint_is_location = not next_random_hint_is_location
continue 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 continue
# Don't hint locations in areas that are almost fully hinted out already # 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. When this happens, they are made less likely to receive an area hint.
""" """
unhinted_locations_per_area = dict() unhinted_locations_per_area = {}
unhinted_location_percentage_per_area = dict() unhinted_location_percentage_per_area = {}
for area_name, locations in locations_per_area.items(): for area_name, locations in locations_per_area.items():
not_yet_hinted_locations = sum(location not in already_hinted_locations for location in locations) 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]]]: 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()) potential_areas = list(static_witness_logic.ALL_AREAS_BY_NAME.keys())
locations_per_area = dict() locations_per_area = {}
items_per_area = dict() items_per_area = {}
for area in potential_areas: for area in potential_areas:
regions = [ 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) 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. # Then, make area hints.
if area_hints: if area_hints:
@ -584,17 +589,29 @@ def make_compact_hint_data(hint: WitnessWordedHint, local_player_number: int) ->
location = hint.location location = hint.location
area_amount = hint.area_amount area_amount = hint.area_amount
# None if junk hint, address if location hint, area string if area hint # -1 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) 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 # 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 return hint.wording, arg_1, arg_2
def make_laser_hints(world: "WitnessWorld", laser_names: List[str]) -> Dict[str, WitnessWordedHint]: 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: for item_name in laser_names:
location_hint = hint_from_item(world, item_name, world.own_itempool) 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]) sorted(self.CHECK_PANELHEX_TO_ID.items(), key=lambda item: item[1])
) )
event_locations = { event_locations = set(player_logic.USED_EVENT_NAMES_BY_HEX)
p for p in player_logic.USED_EVENT_NAMES_BY_HEX
}
self.EVENT_LOCATION_TABLE = { self.EVENT_LOCATION_TABLE = {
static_witness_locations.get_event_name(entity_hex): None static_witness_locations.get_event_name(entity_hex): None
@ -80,5 +78,5 @@ class WitnessPlayerLocations:
def add_location_late(self, entity_name: str) -> None: def add_location_late(self, entity_name: str) -> None:
entity_hex = static_witness_logic.ENTITIES_BY_NAME[entity_name]["entity_hex"] 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) 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 Defines progression, junk and event items for The Witness
""" """
import copy 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 from BaseClasses import Item, ItemClassification, MultiWorld
@ -87,7 +87,8 @@ class WitnessPlayerItems:
if data.classification == ItemClassification.useful}.items(): if data.classification == ItemClassification.useful}.items():
if item_name in static_witness_items._special_usefuls: if item_name in static_witness_items._special_usefuls:
continue continue
elif item_name == "Energy Capacity":
if item_name == "Energy Capacity":
self._mandatory_items[item_name] = NUM_ENERGY_UPGRADES self._mandatory_items[item_name] = NUM_ENERGY_UPGRADES
elif isinstance(item_data.classification, ProgressiveItemDefinition): elif isinstance(item_data.classification, ProgressiveItemDefinition):
self._mandatory_items[item_name] = len(item_data.mappings) 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} 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. # 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]: 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. Returns the total set of all door IDs that are controlled by items in the pool.
""" """
output: List[int] = [] output: List[int] = []
for item_name, item_data in {name: data for name, data in self.item_data.items() for item_name, item_data in dict(self.item_data.items()).items():
if isinstance(data.definition, DoorItemDefinition)}.items(): if not isinstance(item_data.definition, DoorItemDefinition):
continue
output += [int(hex_string, 16) for hex_string in item_data.definition.panel_id_hexes] output += [int(hex_string, 16) for hex_string in item_data.definition.panel_id_hexes]
return output 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. 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() return [
if name not in self.item_data.keys() and data.definition.category is ItemCategory.SYMBOL] # 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]]: def get_progressive_item_ids_in_pool(self) -> Dict[int, List[int]]:
output: 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] item = self.item_data[item_name]
if isinstance(item.definition, ProgressiveItemDefinition): if isinstance(item.definition, ProgressiveItemDefinition):
# Note: we need to reference the static table here rather than the player-specific one because the child # 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. # 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 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] for child_item in item.definition.child_item_names]
return output 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 import static_logic as static_witness_logic
from .data.item_definition_classes import DoorItemDefinition, ItemCategory, ProgressiveItemDefinition from .data.item_definition_classes import DoorItemDefinition, ItemCategory, ProgressiveItemDefinition
from .data.static_logic import StaticWitnessLogicObj
from .data.utils import ( from .data.utils import (
WitnessRule, WitnessRule,
define_new_region, define_new_region,
@ -58,6 +59,95 @@ if TYPE_CHECKING:
class WitnessPlayerLogic: class WitnessPlayerLogic:
"""WITNESS LOGIC CLASS""" """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: def reduce_req_within_region(self, entity_hex: str) -> WitnessRule:
""" """
Panels in this game often only turn on when other panels are solved. 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: # For the requirement of an entity, we consider two things:
# 1. Any items this entity needs (e.g. Symbols or Door Items) # 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) # 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) # Remove any items that don't actually exist in the settings (e.g. Symbol Shuffle turned off)
these_items = frozenset({ these_items = frozenset({
@ -91,47 +181,49 @@ class WitnessPlayerLogic:
for subset in these_items: for subset in these_items:
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(subset) 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. # Handle door entities (door shuffle)
# Also, remove any original power requirements this entity might have had.
if entity_hex in self.DOOR_ITEMS_BY_ID: 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]}) door_items = frozenset({frozenset([item]) for item in self.DOOR_ITEMS_BY_ID[entity_hex]})
for dependent_item in door_items: for dependent_item in door_items:
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(dependent_item) 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 # A door entity is opened by its door item instead of previous entities powering it.
if static_witness_logic.ENTITIES_BY_HEX[entity_hex]["entityType"] != "EP": # 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, # 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. # 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. # 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): or entity_hex == "0x28A0D" and not any("0x28998" in option for option in these_panels)
these_items = all_options
# Another dependency that is not power-based: The Symmetry Island Upper Panel latches # Another dependency that is not power-based: The Symmetry Island Upper Panel latches
elif entity_hex == "0x1C349": or entity_hex == "0x1C349"
these_items = all_options )
else: # If this is not one of those special cases, solving this door entity only needs its own item requirement.
return frozenset(all_options) # Dependent entities from these_panels are ignored, and we just return these_items directly.
if not entity_dependencies_need_to_be_preserved:
else: return these_items
these_items = all_options
# Now that we have item requirements and entity dependencies, it's time for the dependency reduction. # 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), # For each entity that this entity depends on (e.g. a panel turning on another panel),
# Add that entities requirements to this entity. # Add that entities requirements to this entity.
# If there are multiple options, consider each, and then or-chain them. # If there are multiple options, consider each, and then or-chain them.
all_options = list() all_options = []
for option in these_panels: 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 each entity in this option, resolve it to its actual requirement.
for option_entity in option: 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", if option_entity in {"7 Lasers", "11 Lasers", "7 Lasers + Redirect", "11 Lasers + Redirect",
"PP2 Weirdness", "Theater to Tunnels"}: "PP2 Weirdness", "Theater to Tunnels"}:
@ -525,13 +617,16 @@ class WitnessPlayerLogic:
current_adjustment_type = line[:-1] current_adjustment_type = line[:-1]
continue continue
if current_adjustment_type is None:
raise ValueError(f"Adjustment lineset {adjustment_lineset} is malformed")
self.make_single_adjustment(current_adjustment_type, line) self.make_single_adjustment(current_adjustment_type, line)
for entity_id in self.COMPLETELY_DISABLED_ENTITIES: for entity_id in self.COMPLETELY_DISABLED_ENTITIES:
if entity_id in self.DOOR_ITEMS_BY_ID: if entity_id in self.DOOR_ITEMS_BY_ID:
del self.DOOR_ITEMS_BY_ID[entity_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. Some options disable panels or remove specific items.
This can make entire regions completely unreachable, because all their incoming connections are invalid. 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 # Check each traversal option individually
for option in connection[1]: for option in connection[1]:
individual_entity_requirements = [] individual_entity_requirements: List[WitnessRule] = []
for entity in option: for entity in option:
# If a connection requires solving a disabled entity, it is not valid. # 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: 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) 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. 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. 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. # Requirements are cached per entity. However, we might redo the whole reduction process multiple times.
# So, we first clear this cache. # 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 # We also clear any data structures that we might have filled in a previous dependency reduction
self.REQUIREMENTS_BY_HEX = dict() self.REQUIREMENTS_BY_HEX = {}
self.USED_EVENT_NAMES_BY_HEX = dict() self.USED_EVENT_NAMES_BY_HEX = {}
self.CONNECTIONS_BY_REGION_NAME = dict() self.CONNECTIONS_BY_REGION_NAME = {}
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI = set() self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI = set()
# Make independent requirements for entities # Make independent requirements for entities
@ -695,22 +790,18 @@ class WitnessPlayerLogic:
# Make independent region connection requirements based on the entities they require # Make independent region connection requirements based on the entities they require
for region, connections in self.CONNECTIONS_BY_REGION_NAME_THEORETICAL.items(): for region, connections in self.CONNECTIONS_BY_REGION_NAME_THEORETICAL.items():
self.CONNECTIONS_BY_REGION_NAME[region] = [] new_connections = set()
new_connections = []
for connection in connections: for connection in connections:
overall_requirement = self.reduce_connection_requirement(connection) overall_requirement = self.reduce_connection_requirement(connection)
# If there is a way to use this connection, add it. # If there is a way to use this connection, add it.
if overall_requirement: 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. self.CONNECTIONS_BY_REGION_NAME[region] = new_connections
if 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. 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: if entity_hex not in self.USED_EVENT_NAMES_BY_HEX:
warning(f'Entity "{name}" does not have an associated event name.') warning(f'Entity "{name}" does not have an associated event name.')
self.USED_EVENT_NAMES_BY_HEX[entity_hex] = name + " Event" self.USED_EVENT_NAMES_BY_HEX[entity_hex] = name + " Event"
pair = (name, self.USED_EVENT_NAMES_BY_HEX[entity_hex]) return (name, self.USED_EVENT_NAMES_BY_HEX[entity_hex])
return pair
def make_event_panel_lists(self) -> None: def make_event_panel_lists(self) -> None:
""" """
@ -828,85 +918,3 @@ class WitnessPlayerLogic:
for panel in self.USED_EVENT_NAMES_BY_HEX: for panel in self.USED_EVENT_NAMES_BY_HEX:
pair = self.make_event_item_pair(panel) pair = self.make_event_item_pair(panel)
self.EVENT_ITEM_PAIRS[pair[0]] = pair[1] 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 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 import static_logic as static_witness_logic
from .data.static_logic import StaticWitnessLogicObj
from .data.utils import WitnessRule, optimize_witness_rule from .data.utils import WitnessRule, optimize_witness_rule
from .locations import WitnessPlayerLocations, static_witness_locations from .locations import WitnessPlayerLocations
from .player_logic import WitnessPlayerLogic from .player_logic import WitnessPlayerLogic
if TYPE_CHECKING: if TYPE_CHECKING:
@ -21,8 +23,20 @@ if TYPE_CHECKING:
class WitnessPlayerRegions: class WitnessPlayerRegions:
"""Class that defines Witness Regions""" """Class that defines Witness Regions"""
player_locations = None def __init__(self, player_locations: WitnessPlayerLocations, world: "WitnessWorld") -> None:
logic = 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 @staticmethod
def make_lambda(item_requirement: WitnessRule, world: "WitnessWorld") -> CollectionRule: def make_lambda(item_requirement: WitnessRule, world: "WitnessWorld") -> CollectionRule:
@ -36,7 +50,7 @@ class WitnessPlayerRegions:
return _meets_item_requirements(item_requirement, world) return _meets_item_requirements(item_requirement, world)
def connect_if_possible(self, world: "WitnessWorld", source: str, target: str, req: WitnessRule, 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 connect two regions and set the corresponding requirement
""" """
@ -89,8 +103,8 @@ class WitnessPlayerRegions:
""" """
from . import create_region from . import create_region
all_locations = set() all_locations: Set[str] = set()
regions_by_name = dict() regions_by_name: Dict[str, Region] = {}
regions_to_create = { regions_to_create = {
k: v for k, v in self.reference_logic.ALL_REGIONS_BY_NAME.items() 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 region_name, region in regions_to_create.items():
for connection in player_logic.CONNECTIONS_BY_REGION_NAME[region_name]: 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) 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 line-length = 120
[lint] [lint]
select = ["E", "F", "W", "I", "N", "Q", "UP", "RUF", "ISC", "T20"] select = ["C", "E", "F", "R", "W", "I", "N", "Q", "UP", "RUF", "ISC", "T20"]
ignore = ["RUF012", "RUF100"] 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. # The way options definitions work right now, I am forced to break line length requirements.
"options.py" = ["E501"] "options.py" = ["E501"]
# The import list would just be so big if I imported every option individually in presets.py # 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) _can_solve_panel(laser_hex, world, world.player, world.player_logic, world.player_locations)(state)
and state.has("Desert Laser Redirection", player) 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: 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: if entity_name + " Solved" in player_locations.EVENT_LOCATION_TABLE:
return lambda state: state.has(player_logic.EVENT_ITEM_PAIRS[entity_name + " Solved"], player) 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: 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. # 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. # 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"] 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: 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. # 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"]) 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"]) 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"]) 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, def _has_item(item: str, world: "WitnessWorld", player: int,
player_logic: WitnessPlayerLogic, player_locations: WitnessPlayerLocations) -> CollectionRule: 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": if item == "11 Lasers + Redirect":
laser_req = world.options.challenge_lasers.value laser_req = world.options.challenge_lasers.value
return _has_lasers(laser_req, world, True) return _has_lasers(laser_req, world, True)
elif item == "PP2 Weirdness": if item == "PP2 Weirdness":
return lambda state: _can_do_expert_pp2(state, world) 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) return lambda state: _can_do_theater_to_tunnels(state, world)
if item in player_logic.USED_EVENT_NAMES_BY_HEX: if item in player_logic.USED_EVENT_NAMES_BY_HEX:
return _can_solve_panel(item, world, player, player_logic, player_locations) return _can_solve_panel(item, world, player, player_logic, player_locations)