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:
parent
b6925c593e
commit
93617fa546
|
@ -11,11 +11,12 @@ from Options import OptionError, PerGameCommonOptions, Toggle
|
|||
from worlds.AutoWorld import WebWorld, World
|
||||
|
||||
from .data import static_items as static_witness_items
|
||||
from .data import static_locations as static_witness_locations
|
||||
from .data import static_logic as static_witness_logic
|
||||
from .data.item_definition_classes import DoorItemDefinition, ItemData
|
||||
from .data.utils import get_audio_logs
|
||||
from .hints import CompactItemData, create_all_hints, make_compact_hint_data, make_laser_hints
|
||||
from .locations import WitnessPlayerLocations, static_witness_locations
|
||||
from .locations import WitnessPlayerLocations
|
||||
from .options import TheWitnessOptions, witness_option_groups
|
||||
from .player_items import WitnessItem, WitnessPlayerItems
|
||||
from .player_logic import WitnessPlayerLogic
|
||||
|
@ -53,7 +54,8 @@ class WitnessWorld(World):
|
|||
options: TheWitnessOptions
|
||||
|
||||
item_name_to_id = {
|
||||
name: data.ap_code for name, data in static_witness_items.ITEM_DATA.items()
|
||||
# ITEM_DATA doesn't have any event items in it
|
||||
name: cast(int, data.ap_code) for name, data in static_witness_items.ITEM_DATA.items()
|
||||
}
|
||||
location_name_to_id = static_witness_locations.ALL_LOCATIONS_TO_ID
|
||||
item_name_groups = static_witness_items.ITEM_GROUPS
|
||||
|
@ -142,7 +144,7 @@ class WitnessWorld(World):
|
|||
)
|
||||
self.player_regions: WitnessPlayerRegions = WitnessPlayerRegions(self.player_locations, self)
|
||||
|
||||
self.log_ids_to_hints = dict()
|
||||
self.log_ids_to_hints = {}
|
||||
|
||||
self.determine_sufficient_progression()
|
||||
|
||||
|
@ -279,7 +281,7 @@ class WitnessWorld(World):
|
|||
remaining_item_slots = pool_size - sum(item_pool.values())
|
||||
|
||||
# Add puzzle skips.
|
||||
num_puzzle_skips = self.options.puzzle_skip_amount
|
||||
num_puzzle_skips = self.options.puzzle_skip_amount.value
|
||||
|
||||
if num_puzzle_skips > remaining_item_slots:
|
||||
warning(f"{self.multiworld.get_player_name(self.player)}'s Witness world has insufficient locations"
|
||||
|
@ -301,21 +303,21 @@ class WitnessWorld(World):
|
|||
if self.player_items.item_data[item_name].local_only:
|
||||
self.options.local_items.value.add(item_name)
|
||||
|
||||
def fill_slot_data(self) -> dict:
|
||||
self.log_ids_to_hints: Dict[int, CompactItemData] = dict()
|
||||
self.laser_ids_to_hints: Dict[int, CompactItemData] = dict()
|
||||
def fill_slot_data(self) -> Dict[str, Any]:
|
||||
self.log_ids_to_hints: Dict[int, CompactItemData] = {}
|
||||
self.laser_ids_to_hints: Dict[int, CompactItemData] = {}
|
||||
|
||||
already_hinted_locations = set()
|
||||
|
||||
# Laser hints
|
||||
|
||||
if self.options.laser_hints:
|
||||
laser_hints = make_laser_hints(self, static_witness_items.ITEM_GROUPS["Lasers"])
|
||||
laser_hints = make_laser_hints(self, sorted(static_witness_items.ITEM_GROUPS["Lasers"]))
|
||||
|
||||
for item_name, hint in laser_hints.items():
|
||||
item_def = cast(DoorItemDefinition, static_witness_logic.ALL_ITEMS[item_name])
|
||||
self.laser_ids_to_hints[int(item_def.panel_id_hexes[0], 16)] = make_compact_hint_data(hint, self.player)
|
||||
already_hinted_locations.add(hint.location)
|
||||
already_hinted_locations.add(cast(Location, hint.location))
|
||||
|
||||
# Audio Log Hints
|
||||
|
||||
|
@ -378,13 +380,13 @@ class WitnessLocation(Location):
|
|||
game: str = "The Witness"
|
||||
entity_hex: int = -1
|
||||
|
||||
def __init__(self, player: int, name: str, address: Optional[int], parent, ch_hex: int = -1) -> None:
|
||||
def __init__(self, player: int, name: str, address: Optional[int], parent: Region, ch_hex: int = -1) -> None:
|
||||
super().__init__(player, name, address, parent)
|
||||
self.entity_hex = ch_hex
|
||||
|
||||
|
||||
def create_region(world: WitnessWorld, name: str, player_locations: WitnessPlayerLocations,
|
||||
region_locations=None, exits=None) -> Region:
|
||||
region_locations: Optional[List[str]] = None, exits: Optional[List[str]] = None) -> Region:
|
||||
"""
|
||||
Create an Archipelago Region for The Witness
|
||||
"""
|
||||
|
@ -399,11 +401,11 @@ def create_region(world: WitnessWorld, name: str, player_locations: WitnessPlaye
|
|||
entity_hex = int(
|
||||
static_witness_logic.ENTITIES_BY_NAME[location]["entity_hex"], 0
|
||||
)
|
||||
location = WitnessLocation(
|
||||
location_obj = WitnessLocation(
|
||||
world.player, location, loc_id, ret, entity_hex
|
||||
)
|
||||
|
||||
ret.locations.append(location)
|
||||
ret.locations.append(location_obj)
|
||||
if exits:
|
||||
for single_exit in exits:
|
||||
ret.exits.append(Entrance(world.player, single_exit, ret))
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Dict, List
|
||||
from typing import Dict, List, Set
|
||||
|
||||
from BaseClasses import ItemClassification
|
||||
|
||||
|
@ -7,7 +7,7 @@ from .item_definition_classes import DoorItemDefinition, ItemCategory, ItemData
|
|||
from .static_locations import ID_START
|
||||
|
||||
ITEM_DATA: Dict[str, ItemData] = {}
|
||||
ITEM_GROUPS: Dict[str, List[str]] = {}
|
||||
ITEM_GROUPS: Dict[str, Set[str]] = {}
|
||||
|
||||
# Useful items that are treated specially at generation time and should not be automatically added to the player's
|
||||
# item list during get_progression_items.
|
||||
|
@ -22,13 +22,13 @@ def populate_items() -> None:
|
|||
|
||||
if definition.category is ItemCategory.SYMBOL:
|
||||
classification = ItemClassification.progression
|
||||
ITEM_GROUPS.setdefault("Symbols", []).append(item_name)
|
||||
ITEM_GROUPS.setdefault("Symbols", set()).add(item_name)
|
||||
elif definition.category is ItemCategory.DOOR:
|
||||
classification = ItemClassification.progression
|
||||
ITEM_GROUPS.setdefault("Doors", []).append(item_name)
|
||||
ITEM_GROUPS.setdefault("Doors", set()).add(item_name)
|
||||
elif definition.category is ItemCategory.LASER:
|
||||
classification = ItemClassification.progression_skip_balancing
|
||||
ITEM_GROUPS.setdefault("Lasers", []).append(item_name)
|
||||
ITEM_GROUPS.setdefault("Lasers", set()).add(item_name)
|
||||
elif definition.category is ItemCategory.USEFUL:
|
||||
classification = ItemClassification.useful
|
||||
elif definition.category is ItemCategory.FILLER:
|
||||
|
@ -47,7 +47,7 @@ def populate_items() -> None:
|
|||
def get_item_to_door_mappings() -> Dict[int, List[int]]:
|
||||
output: Dict[int, List[int]] = {}
|
||||
for item_name, item_data in ITEM_DATA.items():
|
||||
if not isinstance(item_data.definition, DoorItemDefinition):
|
||||
if not isinstance(item_data.definition, DoorItemDefinition) or item_data.ap_code is None:
|
||||
continue
|
||||
output[item_data.ap_code] = [int(hex_string, 16) for hex_string in item_data.definition.panel_id_hexes]
|
||||
return output
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from typing import Dict, Set, cast
|
||||
|
||||
from . import static_logic as static_witness_logic
|
||||
|
||||
ID_START = 158000
|
||||
|
@ -441,17 +443,17 @@ OBELISK_SIDES = {
|
|||
"Town Obelisk Side 6",
|
||||
}
|
||||
|
||||
ALL_LOCATIONS_TO_ID = dict()
|
||||
ALL_LOCATIONS_TO_ID: Dict[str, int] = {}
|
||||
|
||||
AREA_LOCATION_GROUPS = dict()
|
||||
AREA_LOCATION_GROUPS: Dict[str, Set[str]] = {}
|
||||
|
||||
|
||||
def get_id(entity_hex: str) -> str:
|
||||
def get_id(entity_hex: str) -> int:
|
||||
"""
|
||||
Calculates the location ID for any given location
|
||||
"""
|
||||
|
||||
return static_witness_logic.ENTITIES_BY_HEX[entity_hex]["id"]
|
||||
return cast(int, static_witness_logic.ENTITIES_BY_HEX[entity_hex]["id"])
|
||||
|
||||
|
||||
def get_event_name(entity_hex: str) -> str:
|
||||
|
@ -461,7 +463,7 @@ def get_event_name(entity_hex: str) -> str:
|
|||
|
||||
action = " Opened" if static_witness_logic.ENTITIES_BY_HEX[entity_hex]["entityType"] == "Door" else " Solved"
|
||||
|
||||
return static_witness_logic.ENTITIES_BY_HEX[entity_hex]["checkName"] + action
|
||||
return cast(str, static_witness_logic.ENTITIES_BY_HEX[entity_hex]["checkName"]) + action
|
||||
|
||||
|
||||
ALL_LOCATIONS_TO_IDS = {
|
||||
|
@ -479,4 +481,4 @@ for key, item in ALL_LOCATIONS_TO_IDS.items():
|
|||
|
||||
for loc in ALL_LOCATIONS_TO_IDS:
|
||||
area = static_witness_logic.ENTITIES_BY_NAME[loc]["area"]["name"]
|
||||
AREA_LOCATION_GROUPS.setdefault(area, []).append(loc)
|
||||
AREA_LOCATION_GROUPS.setdefault(area, set()).add(loc)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from collections import defaultdict
|
||||
from typing import Dict, List, Set, Tuple
|
||||
from typing import Any, Dict, List, Optional, Set, Tuple
|
||||
|
||||
from Utils import cache_argsless
|
||||
|
||||
|
@ -24,13 +24,37 @@ from .utils import (
|
|||
|
||||
|
||||
class StaticWitnessLogicObj:
|
||||
def read_logic_file(self, lines) -> None:
|
||||
def __init__(self, lines: Optional[List[str]] = None) -> None:
|
||||
if lines is None:
|
||||
lines = get_sigma_normal_logic()
|
||||
|
||||
# All regions with a list of panels in them and the connections to other regions, before logic adjustments
|
||||
self.ALL_REGIONS_BY_NAME: Dict[str, Dict[str, Any]] = {}
|
||||
self.ALL_AREAS_BY_NAME: Dict[str, Dict[str, Any]] = {}
|
||||
self.CONNECTIONS_WITH_DUPLICATES: Dict[str, Dict[str, Set[WitnessRule]]] = defaultdict(lambda: defaultdict(set))
|
||||
self.STATIC_CONNECTIONS_BY_REGION_NAME: Dict[str, Set[Tuple[str, WitnessRule]]] = {}
|
||||
|
||||
self.ENTITIES_BY_HEX: Dict[str, Dict[str, Any]] = {}
|
||||
self.ENTITIES_BY_NAME: Dict[str, Dict[str, Any]] = {}
|
||||
self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX: Dict[str, Dict[str, WitnessRule]] = {}
|
||||
|
||||
self.OBELISK_SIDE_ID_TO_EP_HEXES: Dict[int, Set[int]] = {}
|
||||
|
||||
self.EP_TO_OBELISK_SIDE: Dict[str, str] = {}
|
||||
|
||||
self.ENTITY_ID_TO_NAME: Dict[str, str] = {}
|
||||
|
||||
self.read_logic_file(lines)
|
||||
self.reverse_connections()
|
||||
self.combine_connections()
|
||||
|
||||
def read_logic_file(self, lines: List[str]) -> None:
|
||||
"""
|
||||
Reads the logic file and does the initial population of data structures
|
||||
"""
|
||||
|
||||
current_region = dict()
|
||||
current_area = {
|
||||
current_region = {}
|
||||
current_area: Dict[str, Any] = {
|
||||
"name": "Misc",
|
||||
"regions": [],
|
||||
}
|
||||
|
@ -155,7 +179,7 @@ class StaticWitnessLogicObj:
|
|||
current_region["entities"].append(entity_hex)
|
||||
current_region["physical_entities"].append(entity_hex)
|
||||
|
||||
def reverse_connection(self, source_region: str, connection: Tuple[str, Set[WitnessRule]]):
|
||||
def reverse_connection(self, source_region: str, connection: Tuple[str, Set[WitnessRule]]) -> None:
|
||||
target = connection[0]
|
||||
traversal_options = connection[1]
|
||||
|
||||
|
@ -169,13 +193,13 @@ class StaticWitnessLogicObj:
|
|||
if remaining_options:
|
||||
self.CONNECTIONS_WITH_DUPLICATES[target][source_region].add(frozenset(remaining_options))
|
||||
|
||||
def reverse_connections(self):
|
||||
def reverse_connections(self) -> None:
|
||||
# Iterate all connections
|
||||
for region_name, connections in list(self.CONNECTIONS_WITH_DUPLICATES.items()):
|
||||
for connection in connections.items():
|
||||
self.reverse_connection(region_name, connection)
|
||||
|
||||
def combine_connections(self):
|
||||
def combine_connections(self) -> None:
|
||||
# All regions need to be present, and this dict is copied later - Thus, defaultdict is not the correct choice.
|
||||
self.STATIC_CONNECTIONS_BY_REGION_NAME = {region_name: set() for region_name in self.ALL_REGIONS_BY_NAME}
|
||||
|
||||
|
@ -184,30 +208,6 @@ class StaticWitnessLogicObj:
|
|||
combined_req = logical_or_witness_rules(requirement)
|
||||
self.STATIC_CONNECTIONS_BY_REGION_NAME[source].add((target, combined_req))
|
||||
|
||||
def __init__(self, lines=None) -> None:
|
||||
if lines is None:
|
||||
lines = get_sigma_normal_logic()
|
||||
|
||||
# All regions with a list of panels in them and the connections to other regions, before logic adjustments
|
||||
self.ALL_REGIONS_BY_NAME = dict()
|
||||
self.ALL_AREAS_BY_NAME = dict()
|
||||
self.CONNECTIONS_WITH_DUPLICATES = defaultdict(lambda: defaultdict(lambda: set()))
|
||||
self.STATIC_CONNECTIONS_BY_REGION_NAME = dict()
|
||||
|
||||
self.ENTITIES_BY_HEX = dict()
|
||||
self.ENTITIES_BY_NAME = dict()
|
||||
self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX = dict()
|
||||
|
||||
self.OBELISK_SIDE_ID_TO_EP_HEXES = dict()
|
||||
|
||||
self.EP_TO_OBELISK_SIDE = dict()
|
||||
|
||||
self.ENTITY_ID_TO_NAME = dict()
|
||||
|
||||
self.read_logic_file(lines)
|
||||
self.reverse_connections()
|
||||
self.combine_connections()
|
||||
|
||||
|
||||
# Item data parsed from WitnessItems.txt
|
||||
ALL_ITEMS: Dict[str, ItemDefinition] = {}
|
||||
|
@ -276,12 +276,12 @@ def get_sigma_expert() -> StaticWitnessLogicObj:
|
|||
return StaticWitnessLogicObj(get_sigma_expert_logic())
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
def __getattr__(name: str) -> StaticWitnessLogicObj:
|
||||
if name == "vanilla":
|
||||
return get_vanilla()
|
||||
elif name == "sigma_normal":
|
||||
if name == "sigma_normal":
|
||||
return get_sigma_normal()
|
||||
elif name == "sigma_expert":
|
||||
if name == "sigma_expert":
|
||||
return get_sigma_expert()
|
||||
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
|
||||
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
from math import floor
|
||||
from pkgutil import get_data
|
||||
from random import random
|
||||
from typing import Any, Collection, Dict, FrozenSet, Iterable, List, Set, Tuple
|
||||
from random import Random
|
||||
from typing import Any, Collection, Dict, FrozenSet, Iterable, List, Set, Tuple, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
# A WitnessRule is just an or-chain of and-conditions.
|
||||
# It represents the set of all options that could fulfill this requirement.
|
||||
|
@ -11,9 +13,9 @@ from typing import Any, Collection, Dict, FrozenSet, Iterable, List, Set, Tuple
|
|||
WitnessRule = FrozenSet[FrozenSet[str]]
|
||||
|
||||
|
||||
def weighted_sample(world_random: random, population: List, weights: List[float], k: int) -> List:
|
||||
def weighted_sample(world_random: Random, population: List[T], weights: List[float], k: int) -> List[T]:
|
||||
positions = range(len(population))
|
||||
indices = []
|
||||
indices: List[int] = []
|
||||
while True:
|
||||
needed = k - len(indices)
|
||||
if not needed:
|
||||
|
@ -82,13 +84,13 @@ def define_new_region(region_string: str) -> Tuple[Dict[str, Any], Set[Tuple[str
|
|||
region_obj = {
|
||||
"name": region_name,
|
||||
"shortName": region_name_simple,
|
||||
"entities": list(),
|
||||
"physical_entities": list(),
|
||||
"entities": [],
|
||||
"physical_entities": [],
|
||||
}
|
||||
return region_obj, options
|
||||
|
||||
|
||||
def parse_lambda(lambda_string) -> WitnessRule:
|
||||
def parse_lambda(lambda_string: str) -> WitnessRule:
|
||||
"""
|
||||
Turns a lambda String literal like this: a | b & c
|
||||
into a set of sets like this: {{a}, {b, c}}
|
||||
|
@ -97,18 +99,18 @@ def parse_lambda(lambda_string) -> WitnessRule:
|
|||
if lambda_string == "True":
|
||||
return frozenset([frozenset()])
|
||||
split_ands = set(lambda_string.split(" | "))
|
||||
lambda_set = frozenset({frozenset(a.split(" & ")) for a in split_ands})
|
||||
|
||||
return lambda_set
|
||||
return frozenset({frozenset(a.split(" & ")) for a in split_ands})
|
||||
|
||||
|
||||
_adjustment_file_cache = dict()
|
||||
_adjustment_file_cache = {}
|
||||
|
||||
|
||||
def get_adjustment_file(adjustment_file: str) -> List[str]:
|
||||
if adjustment_file not in _adjustment_file_cache:
|
||||
data = get_data(__name__, adjustment_file).decode("utf-8")
|
||||
_adjustment_file_cache[adjustment_file] = [line.strip() for line in data.split("\n")]
|
||||
data = get_data(__name__, adjustment_file)
|
||||
if data is None:
|
||||
raise FileNotFoundError(f"Could not find {adjustment_file}")
|
||||
_adjustment_file_cache[adjustment_file] = [line.strip() for line in data.decode("utf-8").split("\n")]
|
||||
|
||||
return _adjustment_file_cache[adjustment_file]
|
||||
|
||||
|
@ -237,7 +239,7 @@ def logical_and_witness_rules(witness_rules: Iterable[WitnessRule]) -> WitnessRu
|
|||
A logical formula might look like this: {{a, b}, {c, d}}, which would mean "a & b | c & d".
|
||||
These can be easily and-ed by just using the boolean distributive law: (a | b) & c = a & c | a & b.
|
||||
"""
|
||||
current_overall_requirement = frozenset({frozenset()})
|
||||
current_overall_requirement: FrozenSet[FrozenSet[str]] = frozenset({frozenset()})
|
||||
|
||||
for next_dnf_requirement in witness_rules:
|
||||
new_requirement: Set[FrozenSet[str]] = set()
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Union
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Union
|
||||
|
||||
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld
|
||||
|
||||
from .data import static_logic as static_witness_logic
|
||||
from .data.utils import weighted_sample
|
||||
from .player_items import WitnessItem
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import WitnessWorld
|
||||
|
@ -22,7 +23,9 @@ class WitnessLocationHint:
|
|||
def __hash__(self) -> int:
|
||||
return hash(self.location)
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
if not isinstance(other, WitnessLocationHint):
|
||||
return False
|
||||
return self.location == other.location
|
||||
|
||||
|
||||
|
@ -171,9 +174,13 @@ def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint) -> Witnes
|
|||
location_name += " (" + world.multiworld.get_player_name(hint.location.player) + ")"
|
||||
|
||||
item = hint.location.item
|
||||
item_name = item.name
|
||||
if item.player != world.player:
|
||||
item_name += " (" + world.multiworld.get_player_name(item.player) + ")"
|
||||
|
||||
item_name = "Nothing"
|
||||
if item is not None:
|
||||
item_name = item.name
|
||||
|
||||
if item.player != world.player:
|
||||
item_name += " (" + world.multiworld.get_player_name(item.player) + ")"
|
||||
|
||||
if hint.hint_came_from_location:
|
||||
hint_text = f"{location_name} contains {item_name}."
|
||||
|
@ -183,14 +190,17 @@ def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint) -> Witnes
|
|||
return WitnessWordedHint(hint_text, hint.location)
|
||||
|
||||
|
||||
def hint_from_item(world: "WitnessWorld", item_name: str, own_itempool: List[Item]) -> Optional[WitnessLocationHint]:
|
||||
def get_real_location(multiworld: MultiWorld, location: Location):
|
||||
def hint_from_item(world: "WitnessWorld", item_name: str,
|
||||
own_itempool: List["WitnessItem"]) -> Optional[WitnessLocationHint]:
|
||||
def get_real_location(multiworld: MultiWorld, location: Location) -> Location:
|
||||
"""If this location is from an item_link pseudo-world, get the location that the item_link item is on.
|
||||
Return the original location otherwise / as a fallback."""
|
||||
if location.player not in world.multiworld.groups:
|
||||
return location
|
||||
|
||||
try:
|
||||
if not location.item:
|
||||
return location
|
||||
return multiworld.find_item(location.item.name, location.player)
|
||||
except StopIteration:
|
||||
return location
|
||||
|
@ -209,17 +219,11 @@ def hint_from_item(world: "WitnessWorld", item_name: str, own_itempool: List[Ite
|
|||
|
||||
|
||||
def hint_from_location(world: "WitnessWorld", location: str) -> Optional[WitnessLocationHint]:
|
||||
location_obj = world.get_location(location)
|
||||
item_obj = location_obj.item
|
||||
item_name = item_obj.name
|
||||
if item_obj.player != world.player:
|
||||
item_name += " (" + world.multiworld.get_player_name(item_obj.player) + ")"
|
||||
|
||||
return WitnessLocationHint(location_obj, True)
|
||||
return WitnessLocationHint(world.get_location(location), True)
|
||||
|
||||
|
||||
def get_items_and_locations_in_random_order(world: "WitnessWorld",
|
||||
own_itempool: List[Item]) -> Tuple[List[str], List[str]]:
|
||||
own_itempool: List["WitnessItem"]) -> Tuple[List[str], List[str]]:
|
||||
prog_items_in_this_world = sorted(
|
||||
item.name for item in own_itempool
|
||||
if item.advancement and item.code and item.location
|
||||
|
@ -235,7 +239,7 @@ def get_items_and_locations_in_random_order(world: "WitnessWorld",
|
|||
return prog_items_in_this_world, locations_in_this_world
|
||||
|
||||
|
||||
def make_always_and_priority_hints(world: "WitnessWorld", own_itempool: List[Item],
|
||||
def make_always_and_priority_hints(world: "WitnessWorld", own_itempool: List["WitnessItem"],
|
||||
already_hinted_locations: Set[Location]
|
||||
) -> Tuple[List[WitnessLocationHint], List[WitnessLocationHint]]:
|
||||
prog_items_in_this_world, loc_in_this_world = get_items_and_locations_in_random_order(world, own_itempool)
|
||||
|
@ -282,14 +286,14 @@ def make_always_and_priority_hints(world: "WitnessWorld", own_itempool: List[Ite
|
|||
return always_hints, priority_hints
|
||||
|
||||
|
||||
def make_extra_location_hints(world: "WitnessWorld", hint_amount: int, own_itempool: List[Item],
|
||||
def make_extra_location_hints(world: "WitnessWorld", hint_amount: int, own_itempool: List["WitnessItem"],
|
||||
already_hinted_locations: Set[Location], hints_to_use_first: List[WitnessLocationHint],
|
||||
unhinted_locations_for_hinted_areas: Dict[str, Set[Location]]) -> List[WitnessWordedHint]:
|
||||
prog_items_in_this_world, locations_in_this_world = get_items_and_locations_in_random_order(world, own_itempool)
|
||||
|
||||
next_random_hint_is_location = world.random.randrange(0, 2)
|
||||
|
||||
hints = []
|
||||
hints: List[WitnessWordedHint] = []
|
||||
|
||||
# This is a way to reverse a Dict[a,List[b]] to a Dict[b,a]
|
||||
area_reverse_lookup = {
|
||||
|
@ -304,6 +308,7 @@ def make_extra_location_hints(world: "WitnessWorld", hint_amount: int, own_itemp
|
|||
logging.warning(f"Ran out of items/locations to hint for player {player_name}.")
|
||||
break
|
||||
|
||||
location_hint: Optional[WitnessLocationHint]
|
||||
if hints_to_use_first:
|
||||
location_hint = hints_to_use_first.pop()
|
||||
elif next_random_hint_is_location and locations_in_this_world:
|
||||
|
@ -317,7 +322,7 @@ def make_extra_location_hints(world: "WitnessWorld", hint_amount: int, own_itemp
|
|||
next_random_hint_is_location = not next_random_hint_is_location
|
||||
continue
|
||||
|
||||
if not location_hint or location_hint.location in already_hinted_locations:
|
||||
if location_hint is None or location_hint.location in already_hinted_locations:
|
||||
continue
|
||||
|
||||
# Don't hint locations in areas that are almost fully hinted out already
|
||||
|
@ -344,8 +349,8 @@ def choose_areas(world: "WitnessWorld", amount: int, locations_per_area: Dict[st
|
|||
When this happens, they are made less likely to receive an area hint.
|
||||
"""
|
||||
|
||||
unhinted_locations_per_area = dict()
|
||||
unhinted_location_percentage_per_area = dict()
|
||||
unhinted_locations_per_area = {}
|
||||
unhinted_location_percentage_per_area = {}
|
||||
|
||||
for area_name, locations in locations_per_area.items():
|
||||
not_yet_hinted_locations = sum(location not in already_hinted_locations for location in locations)
|
||||
|
@ -368,8 +373,8 @@ def choose_areas(world: "WitnessWorld", amount: int, locations_per_area: Dict[st
|
|||
def get_hintable_areas(world: "WitnessWorld") -> Tuple[Dict[str, List[Location]], Dict[str, List[Item]]]:
|
||||
potential_areas = list(static_witness_logic.ALL_AREAS_BY_NAME.keys())
|
||||
|
||||
locations_per_area = dict()
|
||||
items_per_area = dict()
|
||||
locations_per_area = {}
|
||||
items_per_area = {}
|
||||
|
||||
for area in potential_areas:
|
||||
regions = [
|
||||
|
@ -533,7 +538,7 @@ def create_all_hints(world: "WitnessWorld", hint_amount: int, area_hints: int,
|
|||
|
||||
location_hints_created_in_round_1 = len(generated_hints)
|
||||
|
||||
unhinted_locations_per_area: Dict[str, Set[Location]] = dict()
|
||||
unhinted_locations_per_area: Dict[str, Set[Location]] = {}
|
||||
|
||||
# Then, make area hints.
|
||||
if area_hints:
|
||||
|
@ -584,17 +589,29 @@ def make_compact_hint_data(hint: WitnessWordedHint, local_player_number: int) ->
|
|||
location = hint.location
|
||||
area_amount = hint.area_amount
|
||||
|
||||
# None if junk hint, address if location hint, area string if area hint
|
||||
arg_1 = location.address if location else (hint.area if hint.area else None)
|
||||
# -1 if junk hint, address if location hint, area string if area hint
|
||||
arg_1: Union[str, int]
|
||||
if location and location.address is not None:
|
||||
arg_1 = location.address
|
||||
elif hint.area is not None:
|
||||
arg_1 = hint.area
|
||||
else:
|
||||
arg_1 = -1
|
||||
|
||||
# self.player if junk hint, player if location hint, progression amount if area hint
|
||||
arg_2 = area_amount if area_amount is not None else (location.player if location else local_player_number)
|
||||
arg_2: int
|
||||
if area_amount is not None:
|
||||
arg_2 = area_amount
|
||||
elif location is not None:
|
||||
arg_2 = location.player
|
||||
else:
|
||||
arg_2 = local_player_number
|
||||
|
||||
return hint.wording, arg_1, arg_2
|
||||
|
||||
|
||||
def make_laser_hints(world: "WitnessWorld", laser_names: List[str]) -> Dict[str, WitnessWordedHint]:
|
||||
laser_hints_by_name = dict()
|
||||
laser_hints_by_name = {}
|
||||
|
||||
for item_name in laser_names:
|
||||
location_hint = hint_from_item(world, item_name, world.own_itempool)
|
||||
|
|
|
@ -61,9 +61,7 @@ class WitnessPlayerLocations:
|
|||
sorted(self.CHECK_PANELHEX_TO_ID.items(), key=lambda item: item[1])
|
||||
)
|
||||
|
||||
event_locations = {
|
||||
p for p in player_logic.USED_EVENT_NAMES_BY_HEX
|
||||
}
|
||||
event_locations = set(player_logic.USED_EVENT_NAMES_BY_HEX)
|
||||
|
||||
self.EVENT_LOCATION_TABLE = {
|
||||
static_witness_locations.get_event_name(entity_hex): None
|
||||
|
@ -80,5 +78,5 @@ class WitnessPlayerLocations:
|
|||
|
||||
def add_location_late(self, entity_name: str) -> None:
|
||||
entity_hex = static_witness_logic.ENTITIES_BY_NAME[entity_name]["entity_hex"]
|
||||
self.CHECK_LOCATION_TABLE[entity_hex] = entity_name
|
||||
self.CHECK_LOCATION_TABLE[entity_hex] = static_witness_locations.get_id(entity_hex)
|
||||
self.CHECK_PANELHEX_TO_ID[entity_hex] = static_witness_locations.get_id(entity_hex)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
Defines progression, junk and event items for The Witness
|
||||
"""
|
||||
import copy
|
||||
from typing import TYPE_CHECKING, Dict, List, Set
|
||||
from typing import TYPE_CHECKING, Dict, List, Set, cast
|
||||
|
||||
from BaseClasses import Item, ItemClassification, MultiWorld
|
||||
|
||||
|
@ -87,7 +87,8 @@ class WitnessPlayerItems:
|
|||
if data.classification == ItemClassification.useful}.items():
|
||||
if item_name in static_witness_items._special_usefuls:
|
||||
continue
|
||||
elif item_name == "Energy Capacity":
|
||||
|
||||
if item_name == "Energy Capacity":
|
||||
self._mandatory_items[item_name] = NUM_ENERGY_UPGRADES
|
||||
elif isinstance(item_data.classification, ProgressiveItemDefinition):
|
||||
self._mandatory_items[item_name] = len(item_data.mappings)
|
||||
|
@ -184,15 +185,16 @@ class WitnessPlayerItems:
|
|||
output -= {item for item, weight in inner_item.items() if weight}
|
||||
|
||||
# Sort the output for consistency across versions if the implementation changes but the logic does not.
|
||||
return sorted(list(output))
|
||||
return sorted(output)
|
||||
|
||||
def get_door_ids_in_pool(self) -> List[int]:
|
||||
"""
|
||||
Returns the total set of all door IDs that are controlled by items in the pool.
|
||||
"""
|
||||
output: List[int] = []
|
||||
for item_name, item_data in {name: data for name, data in self.item_data.items()
|
||||
if isinstance(data.definition, DoorItemDefinition)}.items():
|
||||
for item_name, item_data in dict(self.item_data.items()).items():
|
||||
if not isinstance(item_data.definition, DoorItemDefinition):
|
||||
continue
|
||||
output += [int(hex_string, 16) for hex_string in item_data.definition.panel_id_hexes]
|
||||
|
||||
return output
|
||||
|
@ -201,18 +203,21 @@ class WitnessPlayerItems:
|
|||
"""
|
||||
Returns the item IDs of symbol items that were defined in the configuration file but are not in the pool.
|
||||
"""
|
||||
return [data.ap_code for name, data in static_witness_items.ITEM_DATA.items()
|
||||
if name not in self.item_data.keys() and data.definition.category is ItemCategory.SYMBOL]
|
||||
return [
|
||||
# data.ap_code is guaranteed for a symbol definition
|
||||
cast(int, data.ap_code) for name, data in static_witness_items.ITEM_DATA.items()
|
||||
if name not in self.item_data.keys() and data.definition.category is ItemCategory.SYMBOL
|
||||
]
|
||||
|
||||
def get_progressive_item_ids_in_pool(self) -> Dict[int, List[int]]:
|
||||
output: Dict[int, List[int]] = {}
|
||||
for item_name, quantity in {name: quantity for name, quantity in self._mandatory_items.items()}.items():
|
||||
for item_name, quantity in dict(self._mandatory_items.items()).items():
|
||||
item = self.item_data[item_name]
|
||||
if isinstance(item.definition, ProgressiveItemDefinition):
|
||||
# Note: we need to reference the static table here rather than the player-specific one because the child
|
||||
# items were removed from the pool when we pruned out all progression items not in the settings.
|
||||
output[item.ap_code] = [static_witness_items.ITEM_DATA[child_item].ap_code
|
||||
for child_item in item.definition.child_item_names]
|
||||
output[cast(int, item.ap_code)] = [cast(int, static_witness_items.ITEM_DATA[child_item].ap_code)
|
||||
for child_item in item.definition.child_item_names]
|
||||
return output
|
||||
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ from typing import TYPE_CHECKING, Dict, List, Set, Tuple, cast
|
|||
|
||||
from .data import static_logic as static_witness_logic
|
||||
from .data.item_definition_classes import DoorItemDefinition, ItemCategory, ProgressiveItemDefinition
|
||||
from .data.static_logic import StaticWitnessLogicObj
|
||||
from .data.utils import (
|
||||
WitnessRule,
|
||||
define_new_region,
|
||||
|
@ -58,6 +59,95 @@ if TYPE_CHECKING:
|
|||
class WitnessPlayerLogic:
|
||||
"""WITNESS LOGIC CLASS"""
|
||||
|
||||
VICTORY_LOCATION: str
|
||||
|
||||
def __init__(self, world: "WitnessWorld", disabled_locations: Set[str], start_inv: Dict[str, int]) -> None:
|
||||
self.YAML_DISABLED_LOCATIONS: Set[str] = disabled_locations
|
||||
self.YAML_ADDED_ITEMS: Dict[str, int] = start_inv
|
||||
|
||||
self.EVENT_PANELS_FROM_PANELS: Set[str] = set()
|
||||
self.EVENT_PANELS_FROM_REGIONS: Set[str] = set()
|
||||
|
||||
self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES: Set[str] = set()
|
||||
|
||||
self.ENTITIES_WITHOUT_ENSURED_SOLVABILITY: Set[str] = set()
|
||||
|
||||
self.UNREACHABLE_REGIONS: Set[str] = set()
|
||||
|
||||
self.THEORETICAL_ITEMS: Set[str] = set()
|
||||
self.THEORETICAL_ITEMS_NO_MULTI: Set[str] = set()
|
||||
self.MULTI_AMOUNTS: Dict[str, int] = defaultdict(lambda: 1)
|
||||
self.MULTI_LISTS: Dict[str, List[str]] = {}
|
||||
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI: Set[str] = set()
|
||||
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME: Set[str] = set()
|
||||
self.DOOR_ITEMS_BY_ID: Dict[str, List[str]] = {}
|
||||
self.STARTING_INVENTORY: Set[str] = set()
|
||||
|
||||
self.DIFFICULTY = world.options.puzzle_randomization
|
||||
|
||||
self.REFERENCE_LOGIC: StaticWitnessLogicObj
|
||||
if self.DIFFICULTY == "sigma_expert":
|
||||
self.REFERENCE_LOGIC = static_witness_logic.sigma_expert
|
||||
elif self.DIFFICULTY == "none":
|
||||
self.REFERENCE_LOGIC = static_witness_logic.vanilla
|
||||
else:
|
||||
self.REFERENCE_LOGIC = static_witness_logic.sigma_normal
|
||||
|
||||
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL: Dict[str, Set[Tuple[str, WitnessRule]]] = copy.deepcopy(
|
||||
self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME
|
||||
)
|
||||
self.CONNECTIONS_BY_REGION_NAME: Dict[str, Set[Tuple[str, WitnessRule]]] = copy.deepcopy(
|
||||
self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME
|
||||
)
|
||||
self.DEPENDENT_REQUIREMENTS_BY_HEX: Dict[str, Dict[str, WitnessRule]] = copy.deepcopy(
|
||||
self.REFERENCE_LOGIC.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX
|
||||
)
|
||||
self.REQUIREMENTS_BY_HEX: Dict[str, WitnessRule] = {}
|
||||
|
||||
self.EVENT_ITEM_PAIRS: Dict[str, str] = {}
|
||||
self.COMPLETELY_DISABLED_ENTITIES: Set[str] = set()
|
||||
self.DISABLE_EVERYTHING_BEHIND: Set[str] = set()
|
||||
self.PRECOMPLETED_LOCATIONS: Set[str] = set()
|
||||
self.EXCLUDED_LOCATIONS: Set[str] = set()
|
||||
self.ADDED_CHECKS: Set[str] = set()
|
||||
self.VICTORY_LOCATION = "0x0356B"
|
||||
|
||||
self.ALWAYS_EVENT_NAMES_BY_HEX = {
|
||||
"0x00509": "+1 Laser (Symmetry Laser)",
|
||||
"0x012FB": "+1 Laser (Desert Laser)",
|
||||
"0x09F98": "Desert Laser Redirection",
|
||||
"0x01539": "+1 Laser (Quarry Laser)",
|
||||
"0x181B3": "+1 Laser (Shadows Laser)",
|
||||
"0x014BB": "+1 Laser (Keep Laser)",
|
||||
"0x17C65": "+1 Laser (Monastery Laser)",
|
||||
"0x032F9": "+1 Laser (Town Laser)",
|
||||
"0x00274": "+1 Laser (Jungle Laser)",
|
||||
"0x0C2B2": "+1 Laser (Bunker Laser)",
|
||||
"0x00BF6": "+1 Laser (Swamp Laser)",
|
||||
"0x028A4": "+1 Laser (Treehouse Laser)",
|
||||
"0x17C34": "Mountain Entry",
|
||||
"0xFFF00": "Bottom Floor Discard Turns On",
|
||||
}
|
||||
|
||||
self.USED_EVENT_NAMES_BY_HEX: Dict[str, str] = {}
|
||||
self.CONDITIONAL_EVENTS: Dict[Tuple[str, str], str] = {}
|
||||
|
||||
# The basic requirements to solve each entity come from StaticWitnessLogic.
|
||||
# However, for any given world, the options (e.g. which item shuffles are enabled) affect the requirements.
|
||||
self.make_options_adjustments(world)
|
||||
self.determine_unrequired_entities(world)
|
||||
self.find_unsolvable_entities(world)
|
||||
|
||||
# After we have adjusted the raw requirements, we perform a dependency reduction for the entity requirements.
|
||||
# This will make the access conditions way faster, instead of recursively checking dependent entities each time.
|
||||
self.make_dependency_reduced_checklist()
|
||||
|
||||
# Finalize which items actually exist in the MultiWorld and which get grouped into progressive items.
|
||||
self.finalize_items()
|
||||
|
||||
# Create event-item pairs for specific panels in the game.
|
||||
self.make_event_panel_lists()
|
||||
|
||||
def reduce_req_within_region(self, entity_hex: str) -> WitnessRule:
|
||||
"""
|
||||
Panels in this game often only turn on when other panels are solved.
|
||||
|
@ -77,9 +167,9 @@ class WitnessPlayerLogic:
|
|||
|
||||
# For the requirement of an entity, we consider two things:
|
||||
# 1. Any items this entity needs (e.g. Symbols or Door Items)
|
||||
these_items = self.DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex].get("items", frozenset({frozenset()}))
|
||||
these_items: WitnessRule = self.DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex].get("items", frozenset({frozenset()}))
|
||||
# 2. Any entities that this entity depends on (e.g. one panel powering on the next panel in a set)
|
||||
these_panels = self.DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex]["entities"]
|
||||
these_panels: WitnessRule = self.DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex]["entities"]
|
||||
|
||||
# Remove any items that don't actually exist in the settings (e.g. Symbol Shuffle turned off)
|
||||
these_items = frozenset({
|
||||
|
@ -91,47 +181,49 @@ class WitnessPlayerLogic:
|
|||
for subset in these_items:
|
||||
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(subset)
|
||||
|
||||
# If this entity is opened by a door item that exists in the itempool, add that item to its requirements.
|
||||
# Also, remove any original power requirements this entity might have had.
|
||||
# Handle door entities (door shuffle)
|
||||
if entity_hex in self.DOOR_ITEMS_BY_ID:
|
||||
# If this entity is opened by a door item that exists in the itempool, add that item to its requirements.
|
||||
door_items = frozenset({frozenset([item]) for item in self.DOOR_ITEMS_BY_ID[entity_hex]})
|
||||
|
||||
for dependent_item in door_items:
|
||||
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(dependent_item)
|
||||
|
||||
all_options = logical_and_witness_rules([door_items, these_items])
|
||||
these_items = logical_and_witness_rules([door_items, these_items])
|
||||
|
||||
# If this entity is not an EP, and it has an associated door item, ignore the original power dependencies
|
||||
if static_witness_logic.ENTITIES_BY_HEX[entity_hex]["entityType"] != "EP":
|
||||
# A door entity is opened by its door item instead of previous entities powering it.
|
||||
# That means we need to ignore any dependent requirements.
|
||||
# However, there are some entities that depend on other entities because of an environmental reason.
|
||||
# Those requirements need to be preserved even in door shuffle.
|
||||
entity_dependencies_need_to_be_preserved = (
|
||||
# EPs keep all their entity dependencies
|
||||
static_witness_logic.ENTITIES_BY_HEX[entity_hex]["entityType"] != "EP"
|
||||
# 0x28A0D depends on another entity for *non-power* reasons -> This dependency needs to be preserved,
|
||||
# except in Expert, where that dependency doesn't exist, but now there *is* a power dependency.
|
||||
# In the future, it'd be wise to make a distinction between "power dependencies" and other dependencies.
|
||||
if entity_hex == "0x28A0D" and not any("0x28998" in option for option in these_panels):
|
||||
these_items = all_options
|
||||
|
||||
or entity_hex == "0x28A0D" and not any("0x28998" in option for option in these_panels)
|
||||
# Another dependency that is not power-based: The Symmetry Island Upper Panel latches
|
||||
elif entity_hex == "0x1C349":
|
||||
these_items = all_options
|
||||
or entity_hex == "0x1C349"
|
||||
)
|
||||
|
||||
else:
|
||||
return frozenset(all_options)
|
||||
|
||||
else:
|
||||
these_items = all_options
|
||||
# If this is not one of those special cases, solving this door entity only needs its own item requirement.
|
||||
# Dependent entities from these_panels are ignored, and we just return these_items directly.
|
||||
if not entity_dependencies_need_to_be_preserved:
|
||||
return these_items
|
||||
|
||||
# Now that we have item requirements and entity dependencies, it's time for the dependency reduction.
|
||||
|
||||
# For each entity that this entity depends on (e.g. a panel turning on another panel),
|
||||
# Add that entities requirements to this entity.
|
||||
# If there are multiple options, consider each, and then or-chain them.
|
||||
all_options = list()
|
||||
all_options = []
|
||||
|
||||
for option in these_panels:
|
||||
dependent_items_for_option = frozenset({frozenset()})
|
||||
dependent_items_for_option: WitnessRule = frozenset({frozenset()})
|
||||
|
||||
# For each entity in this option, resolve it to its actual requirement.
|
||||
for option_entity in option:
|
||||
dep_obj = self.REFERENCE_LOGIC.ENTITIES_BY_HEX.get(option_entity)
|
||||
dep_obj = self.REFERENCE_LOGIC.ENTITIES_BY_HEX.get(option_entity, {})
|
||||
|
||||
if option_entity in {"7 Lasers", "11 Lasers", "7 Lasers + Redirect", "11 Lasers + Redirect",
|
||||
"PP2 Weirdness", "Theater to Tunnels"}:
|
||||
|
@ -525,13 +617,16 @@ class WitnessPlayerLogic:
|
|||
current_adjustment_type = line[:-1]
|
||||
continue
|
||||
|
||||
if current_adjustment_type is None:
|
||||
raise ValueError(f"Adjustment lineset {adjustment_lineset} is malformed")
|
||||
|
||||
self.make_single_adjustment(current_adjustment_type, line)
|
||||
|
||||
for entity_id in self.COMPLETELY_DISABLED_ENTITIES:
|
||||
if entity_id in self.DOOR_ITEMS_BY_ID:
|
||||
del self.DOOR_ITEMS_BY_ID[entity_id]
|
||||
|
||||
def discover_reachable_regions(self):
|
||||
def discover_reachable_regions(self) -> Set[str]:
|
||||
"""
|
||||
Some options disable panels or remove specific items.
|
||||
This can make entire regions completely unreachable, because all their incoming connections are invalid.
|
||||
|
@ -640,7 +735,7 @@ class WitnessPlayerLogic:
|
|||
|
||||
# Check each traversal option individually
|
||||
for option in connection[1]:
|
||||
individual_entity_requirements = []
|
||||
individual_entity_requirements: List[WitnessRule] = []
|
||||
for entity in option:
|
||||
# If a connection requires solving a disabled entity, it is not valid.
|
||||
if not self.solvability_guaranteed(entity) or entity in self.DISABLE_EVERYTHING_BEHIND:
|
||||
|
@ -664,7 +759,7 @@ class WitnessPlayerLogic:
|
|||
|
||||
return logical_or_witness_rules(all_possibilities)
|
||||
|
||||
def make_dependency_reduced_checklist(self):
|
||||
def make_dependency_reduced_checklist(self) -> None:
|
||||
"""
|
||||
Every entity has a requirement. This requirement may involve other entities.
|
||||
Example: Solving a panel powers a cable, and that cable turns on the next panel.
|
||||
|
@ -679,12 +774,12 @@ class WitnessPlayerLogic:
|
|||
|
||||
# Requirements are cached per entity. However, we might redo the whole reduction process multiple times.
|
||||
# So, we first clear this cache.
|
||||
self.REQUIREMENTS_BY_HEX = dict()
|
||||
self.REQUIREMENTS_BY_HEX = {}
|
||||
|
||||
# We also clear any data structures that we might have filled in a previous dependency reduction
|
||||
self.REQUIREMENTS_BY_HEX = dict()
|
||||
self.USED_EVENT_NAMES_BY_HEX = dict()
|
||||
self.CONNECTIONS_BY_REGION_NAME = dict()
|
||||
self.REQUIREMENTS_BY_HEX = {}
|
||||
self.USED_EVENT_NAMES_BY_HEX = {}
|
||||
self.CONNECTIONS_BY_REGION_NAME = {}
|
||||
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI = set()
|
||||
|
||||
# Make independent requirements for entities
|
||||
|
@ -695,22 +790,18 @@ class WitnessPlayerLogic:
|
|||
|
||||
# Make independent region connection requirements based on the entities they require
|
||||
for region, connections in self.CONNECTIONS_BY_REGION_NAME_THEORETICAL.items():
|
||||
self.CONNECTIONS_BY_REGION_NAME[region] = []
|
||||
|
||||
new_connections = []
|
||||
new_connections = set()
|
||||
|
||||
for connection in connections:
|
||||
overall_requirement = self.reduce_connection_requirement(connection)
|
||||
|
||||
# If there is a way to use this connection, add it.
|
||||
if overall_requirement:
|
||||
new_connections.append((connection[0], overall_requirement))
|
||||
new_connections.add((connection[0], overall_requirement))
|
||||
|
||||
# If there are any usable outgoing connections from this region, add them.
|
||||
if new_connections:
|
||||
self.CONNECTIONS_BY_REGION_NAME[region] = new_connections
|
||||
self.CONNECTIONS_BY_REGION_NAME[region] = new_connections
|
||||
|
||||
def finalize_items(self):
|
||||
def finalize_items(self) -> None:
|
||||
"""
|
||||
Finalise which items are used in the world, and handle their progressive versions.
|
||||
"""
|
||||
|
@ -808,8 +899,7 @@ class WitnessPlayerLogic:
|
|||
if entity_hex not in self.USED_EVENT_NAMES_BY_HEX:
|
||||
warning(f'Entity "{name}" does not have an associated event name.')
|
||||
self.USED_EVENT_NAMES_BY_HEX[entity_hex] = name + " Event"
|
||||
pair = (name, self.USED_EVENT_NAMES_BY_HEX[entity_hex])
|
||||
return pair
|
||||
return (name, self.USED_EVENT_NAMES_BY_HEX[entity_hex])
|
||||
|
||||
def make_event_panel_lists(self) -> None:
|
||||
"""
|
||||
|
@ -828,85 +918,3 @@ class WitnessPlayerLogic:
|
|||
for panel in self.USED_EVENT_NAMES_BY_HEX:
|
||||
pair = self.make_event_item_pair(panel)
|
||||
self.EVENT_ITEM_PAIRS[pair[0]] = pair[1]
|
||||
|
||||
def __init__(self, world: "WitnessWorld", disabled_locations: Set[str], start_inv: Dict[str, int]) -> None:
|
||||
self.YAML_DISABLED_LOCATIONS = disabled_locations
|
||||
self.YAML_ADDED_ITEMS = start_inv
|
||||
|
||||
self.EVENT_PANELS_FROM_PANELS = set()
|
||||
self.EVENT_PANELS_FROM_REGIONS = set()
|
||||
|
||||
self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES = set()
|
||||
|
||||
self.ENTITIES_WITHOUT_ENSURED_SOLVABILITY = set()
|
||||
|
||||
self.UNREACHABLE_REGIONS = set()
|
||||
|
||||
self.THEORETICAL_ITEMS = set()
|
||||
self.THEORETICAL_ITEMS_NO_MULTI = set()
|
||||
self.MULTI_AMOUNTS = defaultdict(lambda: 1)
|
||||
self.MULTI_LISTS = dict()
|
||||
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI = set()
|
||||
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME = set()
|
||||
self.DOOR_ITEMS_BY_ID: Dict[str, List[str]] = {}
|
||||
self.STARTING_INVENTORY = set()
|
||||
|
||||
self.DIFFICULTY = world.options.puzzle_randomization
|
||||
|
||||
if self.DIFFICULTY == "sigma_normal":
|
||||
self.REFERENCE_LOGIC = static_witness_logic.sigma_normal
|
||||
elif self.DIFFICULTY == "sigma_expert":
|
||||
self.REFERENCE_LOGIC = static_witness_logic.sigma_expert
|
||||
elif self.DIFFICULTY == "none":
|
||||
self.REFERENCE_LOGIC = static_witness_logic.vanilla
|
||||
|
||||
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL = copy.deepcopy(
|
||||
self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME
|
||||
)
|
||||
self.CONNECTIONS_BY_REGION_NAME = dict()
|
||||
self.DEPENDENT_REQUIREMENTS_BY_HEX = copy.deepcopy(self.REFERENCE_LOGIC.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX)
|
||||
self.REQUIREMENTS_BY_HEX = dict()
|
||||
|
||||
self.EVENT_ITEM_PAIRS = dict()
|
||||
self.COMPLETELY_DISABLED_ENTITIES = set()
|
||||
self.DISABLE_EVERYTHING_BEHIND = set()
|
||||
self.PRECOMPLETED_LOCATIONS = set()
|
||||
self.EXCLUDED_LOCATIONS = set()
|
||||
self.ADDED_CHECKS = set()
|
||||
self.VICTORY_LOCATION: str
|
||||
|
||||
self.ALWAYS_EVENT_NAMES_BY_HEX = {
|
||||
"0x00509": "+1 Laser (Symmetry Laser)",
|
||||
"0x012FB": "+1 Laser (Desert Laser)",
|
||||
"0x09F98": "Desert Laser Redirection",
|
||||
"0x01539": "+1 Laser (Quarry Laser)",
|
||||
"0x181B3": "+1 Laser (Shadows Laser)",
|
||||
"0x014BB": "+1 Laser (Keep Laser)",
|
||||
"0x17C65": "+1 Laser (Monastery Laser)",
|
||||
"0x032F9": "+1 Laser (Town Laser)",
|
||||
"0x00274": "+1 Laser (Jungle Laser)",
|
||||
"0x0C2B2": "+1 Laser (Bunker Laser)",
|
||||
"0x00BF6": "+1 Laser (Swamp Laser)",
|
||||
"0x028A4": "+1 Laser (Treehouse Laser)",
|
||||
"0x17C34": "Mountain Entry",
|
||||
"0xFFF00": "Bottom Floor Discard Turns On",
|
||||
}
|
||||
|
||||
self.USED_EVENT_NAMES_BY_HEX = {}
|
||||
self.CONDITIONAL_EVENTS = {}
|
||||
|
||||
# The basic requirements to solve each entity come from StaticWitnessLogic.
|
||||
# However, for any given world, the options (e.g. which item shuffles are enabled) affect the requirements.
|
||||
self.make_options_adjustments(world)
|
||||
self.determine_unrequired_entities(world)
|
||||
self.find_unsolvable_entities(world)
|
||||
|
||||
# After we have adjusted the raw requirements, we perform a dependency reduction for the entity requirements.
|
||||
# This will make the access conditions way faster, instead of recursively checking dependent entities each time.
|
||||
self.make_dependency_reduced_checklist()
|
||||
|
||||
# Finalize which items actually exist in the MultiWorld and which get grouped into progressive items.
|
||||
self.finalize_items()
|
||||
|
||||
# Create event-item pairs for specific panels in the game.
|
||||
self.make_event_panel_lists()
|
||||
|
|
|
@ -9,9 +9,11 @@ from BaseClasses import Entrance, Region
|
|||
|
||||
from worlds.generic.Rules import CollectionRule
|
||||
|
||||
from .data import static_locations as static_witness_locations
|
||||
from .data import static_logic as static_witness_logic
|
||||
from .data.static_logic import StaticWitnessLogicObj
|
||||
from .data.utils import WitnessRule, optimize_witness_rule
|
||||
from .locations import WitnessPlayerLocations, static_witness_locations
|
||||
from .locations import WitnessPlayerLocations
|
||||
from .player_logic import WitnessPlayerLogic
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -21,8 +23,20 @@ if TYPE_CHECKING:
|
|||
class WitnessPlayerRegions:
|
||||
"""Class that defines Witness Regions"""
|
||||
|
||||
player_locations = None
|
||||
logic = None
|
||||
def __init__(self, player_locations: WitnessPlayerLocations, world: "WitnessWorld") -> None:
|
||||
difficulty = world.options.puzzle_randomization
|
||||
|
||||
self.reference_logic: StaticWitnessLogicObj
|
||||
if difficulty == "sigma_normal":
|
||||
self.reference_logic = static_witness_logic.sigma_normal
|
||||
elif difficulty == "sigma_expert":
|
||||
self.reference_logic = static_witness_logic.sigma_expert
|
||||
else:
|
||||
self.reference_logic = static_witness_logic.vanilla
|
||||
|
||||
self.player_locations = player_locations
|
||||
self.two_way_entrance_register: Dict[Tuple[str, str], List[Entrance]] = defaultdict(lambda: [])
|
||||
self.created_region_names: Set[str] = set()
|
||||
|
||||
@staticmethod
|
||||
def make_lambda(item_requirement: WitnessRule, world: "WitnessWorld") -> CollectionRule:
|
||||
|
@ -36,7 +50,7 @@ class WitnessPlayerRegions:
|
|||
return _meets_item_requirements(item_requirement, world)
|
||||
|
||||
def connect_if_possible(self, world: "WitnessWorld", source: str, target: str, req: WitnessRule,
|
||||
regions_by_name: Dict[str, Region]):
|
||||
regions_by_name: Dict[str, Region]) -> None:
|
||||
"""
|
||||
connect two regions and set the corresponding requirement
|
||||
"""
|
||||
|
@ -89,8 +103,8 @@ class WitnessPlayerRegions:
|
|||
"""
|
||||
from . import create_region
|
||||
|
||||
all_locations = set()
|
||||
regions_by_name = dict()
|
||||
all_locations: Set[str] = set()
|
||||
regions_by_name: Dict[str, Region] = {}
|
||||
|
||||
regions_to_create = {
|
||||
k: v for k, v in self.reference_logic.ALL_REGIONS_BY_NAME.items()
|
||||
|
@ -121,17 +135,3 @@ class WitnessPlayerRegions:
|
|||
for region_name, region in regions_to_create.items():
|
||||
for connection in player_logic.CONNECTIONS_BY_REGION_NAME[region_name]:
|
||||
self.connect_if_possible(world, region_name, connection[0], connection[1], regions_by_name)
|
||||
|
||||
def __init__(self, player_locations: WitnessPlayerLocations, world: "WitnessWorld") -> None:
|
||||
difficulty = world.options.puzzle_randomization
|
||||
|
||||
if difficulty == "sigma_normal":
|
||||
self.reference_logic = static_witness_logic.sigma_normal
|
||||
elif difficulty == "sigma_expert":
|
||||
self.reference_logic = static_witness_logic.sigma_expert
|
||||
elif difficulty == "none":
|
||||
self.reference_logic = static_witness_logic.vanilla
|
||||
|
||||
self.player_locations = player_locations
|
||||
self.two_way_entrance_register: Dict[Tuple[str, str], List[Entrance]] = defaultdict(lambda: [])
|
||||
self.created_region_names: Set[str] = set()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue