The Witness: Change Regions, Areas and Connections from Dict[str, Any] to dataclasses&NamedTuples (#4415)

* Change Regions, Areas and Connections to dataclasses/NamedTuples

* Move to new file

* we do a little renaming

* Purge the 'lambda' naming in favor of 'rule' or 'WitnessRule'

* missed one

* unnecessary change

* omega oops

* NOOOOOOOO

* Merge error

* mypy thing
This commit is contained in:
NewSoupVi
2025-03-13 23:59:09 +01:00
committed by GitHub
parent 3192799bbf
commit 1de411ec89
11 changed files with 148 additions and 125 deletions

View File

@@ -0,0 +1,33 @@
from dataclasses import dataclass, field
from typing import FrozenSet, List, NamedTuple
# A WitnessRule is just an or-chain of and-conditions.
# It represents the set of all options that could fulfill this requirement.
# E.g. if something requires "Dots or (Shapers and Stars)", it'd be represented as: {{"Dots"}, {"Shapers, "Stars"}}
# {} is an unusable requirement.
# {{}} is an always usable requirement.
WitnessRule = FrozenSet[FrozenSet[str]]
@dataclass
class AreaDefinition:
name: str
regions: List[str] = field(default_factory=list)
@dataclass
class RegionDefinition:
name: str
short_name: str
area: AreaDefinition
logical_entities: List[str] = field(default_factory=list)
physical_entities: List[str] = field(default_factory=list)
class ConnectionDefinition(NamedTuple):
target_region: str
traversal_rule: WitnessRule
@property
def can_be_traversed(self) -> bool:
return bool(self.traversal_rule)

View File

@@ -486,5 +486,5 @@ for key, item in ALL_LOCATIONS_TO_IDS.items():
ALL_LOCATIONS_TO_ID[key] = item
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, set()).add(loc)

View File

@@ -1,8 +1,9 @@
from collections import Counter, defaultdict
from typing import Any, Dict, List, Optional, Set, Tuple
from typing import Any, Dict, FrozenSet, List, Optional, Set
from Utils import cache_argsless
from .definition_classes import AreaDefinition, ConnectionDefinition, RegionDefinition, WitnessRule
from .item_definition_classes import (
CATEGORY_NAME_MAPPINGS,
DoorItemDefinition,
@@ -13,7 +14,6 @@ from .item_definition_classes import (
)
from .settings.easter_eggs import EASTER_EGGS
from .utils import (
WitnessRule,
define_new_region,
get_items,
get_sigma_expert_logic,
@@ -21,7 +21,7 @@ from .utils import (
get_umbra_variety_logic,
get_vanilla_logic,
logical_or_witness_rules,
parse_lambda,
parse_witness_rule,
)
@@ -31,10 +31,10 @@ class StaticWitnessLogicObj:
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.ALL_REGIONS_BY_NAME: Dict[str, RegionDefinition] = {}
self.ALL_AREAS_BY_NAME: Dict[str, AreaDefinition] = {}
self.CONNECTIONS_WITH_DUPLICATES: Dict[str, List[ConnectionDefinition]] = defaultdict(list)
self.STATIC_CONNECTIONS_BY_REGION_NAME: Dict[str, List[ConnectionDefinition]] = {}
self.ENTITIES_BY_HEX: Dict[str, Dict[str, Any]] = {}
self.ENTITIES_BY_NAME: Dict[str, Dict[str, Any]] = {}
@@ -55,15 +55,15 @@ class StaticWitnessLogicObj:
area_counts: Dict[str, int] = Counter()
for region_name, entity_amount in EASTER_EGGS.items():
region_object = self.ALL_REGIONS_BY_NAME[region_name]
correct_area = region_object["area"]
correct_area = region_object.area
for _ in range(entity_amount):
location_id = 160200 + egg_counter
entity_hex = hex(0xEE000 + egg_counter)
egg_counter += 1
area_counts[correct_area["name"]] += 1
full_entity_name = f"{correct_area['name']} Easter Egg {area_counts[correct_area['name']]}"
area_counts[correct_area.name] += 1
full_entity_name = f"{correct_area.name} Easter Egg {area_counts[correct_area.name]}"
self.ENTITIES_BY_HEX[entity_hex] = {
"checkName": full_entity_name,
@@ -81,11 +81,11 @@ class StaticWitnessLogicObj:
self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex] = {
"entities": frozenset({frozenset({})})
}
region_object["entities"].append(entity_hex)
region_object["physical_entities"].append(entity_hex)
region_object.logical_entities.append(entity_hex)
region_object.physical_entities.append(entity_hex)
easter_egg_region = self.ALL_REGIONS_BY_NAME["Easter Eggs"]
easter_egg_area = easter_egg_region["area"]
easter_egg_area = easter_egg_region.area
for i in range(sum(EASTER_EGGS.values())):
location_id = 160000 + i
entity_hex = hex(0xEE200 + i)
@@ -111,19 +111,15 @@ class StaticWitnessLogicObj:
self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex] = {
"entities": frozenset({frozenset({})})
}
easter_egg_region["entities"].append(entity_hex)
easter_egg_region["physical_entities"].append(entity_hex)
easter_egg_region.logical_entities.append(entity_hex)
easter_egg_region.physical_entities.append(entity_hex)
def read_logic_file(self, lines: List[str]) -> None:
"""
Reads the logic file and does the initial population of data structures
"""
current_region = {}
current_area: Dict[str, Any] = {
"name": "Misc",
"regions": [],
}
current_area = AreaDefinition("Misc")
current_region = RegionDefinition("Fake", "Fake", current_area) # Unused, but makes PyCharm & mypy shut up
self.ALL_AREAS_BY_NAME["Misc"] = current_area
for line in lines:
@@ -133,19 +129,16 @@ class StaticWitnessLogicObj:
if line[-1] == ":":
new_region_and_connections = define_new_region(line, current_area)
current_region = new_region_and_connections[0]
region_name = current_region["name"]
region_name = current_region.name
self.ALL_REGIONS_BY_NAME[region_name] = current_region
for connection in new_region_and_connections[1]:
self.CONNECTIONS_WITH_DUPLICATES[region_name][connection[0]].add(connection[1])
current_area["regions"].append(region_name)
self.CONNECTIONS_WITH_DUPLICATES[region_name].append(connection)
current_area.regions.append(region_name)
continue
if line[0] == "=":
area_name = line[2:-2]
current_area = {
"name": area_name,
"regions": [],
}
current_area = AreaDefinition(area_name, [])
self.ALL_AREAS_BY_NAME[area_name] = current_area
continue
@@ -158,9 +151,9 @@ class StaticWitnessLogicObj:
entity_hex = entity_name_full[0:7]
entity_name = entity_name_full[9:-1]
required_panel_lambda = line_split.pop(0)
entity_requirement_string = line_split.pop(0)
full_entity_name = current_region["shortName"] + " " + entity_name
full_entity_name = current_region.short_name + " " + entity_name
if location_id == "Door" or location_id == "Laser":
self.ENTITIES_BY_HEX[entity_hex] = {
@@ -177,18 +170,18 @@ class StaticWitnessLogicObj:
self.ENTITIES_BY_NAME[self.ENTITIES_BY_HEX[entity_hex]["checkName"]] = self.ENTITIES_BY_HEX[entity_hex]
self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex] = {
"entities": parse_lambda(required_panel_lambda)
"entities": parse_witness_rule(entity_requirement_string)
}
# Lasers and Doors exist in a region, but don't have a regional *requirement*
# If a laser is activated, you don't need to physically walk up to it for it to count
# As such, logically, they behave more as if they were part of the "Entry" region
self.ALL_REGIONS_BY_NAME["Entry"]["entities"].append(entity_hex)
self.ALL_REGIONS_BY_NAME["Entry"].logical_entities.append(entity_hex)
# However, it will also be important to keep track of their physical location for postgame purposes.
current_region["physical_entities"].append(entity_hex)
current_region.physical_entities.append(entity_hex)
continue
required_item_lambda = line_split.pop(0)
item_requirement_string = line_split.pop(0)
laser_names = {
"Laser",
@@ -224,18 +217,18 @@ class StaticWitnessLogicObj:
entity_type = "Panel"
location_type = "General"
required_items = parse_lambda(required_item_lambda)
required_panels = parse_lambda(required_panel_lambda)
required_items = parse_witness_rule(item_requirement_string)
required_entities = parse_witness_rule(entity_requirement_string)
required_items = frozenset(required_items)
requirement = {
"entities": required_panels,
"entities": required_entities,
"items": required_items
}
if entity_type == "Obelisk Side":
eps = set(next(iter(required_panels)))
eps = set(next(iter(required_entities)))
eps -= {"Theater to Tunnels"}
eps_ints = {int(h, 16) for h in eps}
@@ -260,39 +253,43 @@ class StaticWitnessLogicObj:
self.ENTITIES_BY_NAME[self.ENTITIES_BY_HEX[entity_hex]["checkName"]] = self.ENTITIES_BY_HEX[entity_hex]
self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex] = requirement
current_region["entities"].append(entity_hex)
current_region["physical_entities"].append(entity_hex)
current_region.logical_entities.append(entity_hex)
current_region.physical_entities.append(entity_hex)
self.add_easter_eggs()
def reverse_connection(self, source_region: str, connection: Tuple[str, Set[WitnessRule]]) -> None:
target = connection[0]
traversal_options = connection[1]
def reverse_connection(self, source_region: str, connection: ConnectionDefinition) -> None:
# Reverse this connection with all its possibilities, except the ones marked as "OneWay".
for requirement in traversal_options:
remaining_options = set()
for option in requirement:
if not any(req == "TrueOneWay" for req in option):
remaining_options.add(option)
remaining_options: Set[FrozenSet[str]] = set()
for sub_option in connection.traversal_rule:
if not any(req == "TrueOneWay" for req in sub_option):
remaining_options.add(sub_option)
if remaining_options:
self.CONNECTIONS_WITH_DUPLICATES[target][source_region].add(frozenset(remaining_options))
reversed_connection = ConnectionDefinition(source_region, frozenset(remaining_options))
if reversed_connection.can_be_traversed:
self.CONNECTIONS_WITH_DUPLICATES[connection.target_region].append(reversed_connection)
def reverse_connections(self) -> None:
# Iterate all connections
for region_name, connections in list(self.CONNECTIONS_WITH_DUPLICATES.items()):
for connection in connections.items():
for connection in connections:
self.reverse_connection(region_name, connection)
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}
self.STATIC_CONNECTIONS_BY_REGION_NAME = {region_name: [] for region_name in self.ALL_REGIONS_BY_NAME}
for source, connections in self.CONNECTIONS_WITH_DUPLICATES.items():
for target, requirement in connections.items():
combined_req = logical_or_witness_rules(requirement)
self.STATIC_CONNECTIONS_BY_REGION_NAME[source].add((target, combined_req))
# Organize rules by target region
traversal_options_by_target_region = defaultdict(list)
for target_region, traversal_option in connections:
traversal_options_by_target_region[target_region].append(traversal_option)
# Combine connections to the same target region into one connection
for target, traversal_rules in traversal_options_by_target_region.items():
combined_rule = logical_or_witness_rules(traversal_rules)
combined_connection = ConnectionDefinition(target, combined_rule)
self.STATIC_CONNECTIONS_BY_REGION_NAME[source].append(combined_connection)
# Item data parsed from WitnessItems.txt

View File

@@ -2,17 +2,12 @@ from datetime import date
from math import floor
from pkgutil import get_data
from random import Random
from typing import Any, Collection, Dict, FrozenSet, Iterable, List, Optional, Set, Tuple, TypeVar
from typing import Collection, FrozenSet, Iterable, List, Optional, Set, Tuple, TypeVar
from .definition_classes import AreaDefinition, ConnectionDefinition, RegionDefinition, WitnessRule
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.
# E.g. if something requires "Dots or (Shapers and Stars)", it'd be represented as: {{"Dots"}, {"Shapers, "Stars"}}
# {} is an unusable requirement.
# {{}} is an always usable requirement.
WitnessRule = FrozenSet[FrozenSet[str]]
def cast_not_none(value: Optional[T]) -> T:
assert value is not None
@@ -62,7 +57,7 @@ def build_weighted_int_list(inputs: Collection[float], total: int) -> List[int]:
return rounded_output
def define_new_region(region_string: str, area: dict[str, Any]) -> Tuple[Dict[str, Any], Set[Tuple[str, WitnessRule]]]:
def define_new_region(region_string: str, area: AreaDefinition) -> Tuple[RegionDefinition, List[ConnectionDefinition]]:
"""
Returns a region object by parsing a line in the logic file
"""
@@ -77,35 +72,28 @@ def define_new_region(region_string: str, area: dict[str, Any]) -> Tuple[Dict[st
region_name = region_name_split[0]
region_name_simple = region_name_split[1][:-1]
options = set()
options = []
for _ in range(len(line_split) // 2):
connected_region = line_split.pop(0)
corresponding_lambda = line_split.pop(0)
traversal_rule_string = line_split.pop(0)
options.add(
(connected_region, parse_lambda(corresponding_lambda))
)
options.append(ConnectionDefinition(connected_region, parse_witness_rule(traversal_rule_string)))
region_obj = RegionDefinition(region_name, region_name_simple, area)
region_obj = {
"name": region_name,
"shortName": region_name_simple,
"entities": [],
"physical_entities": [],
"area": area,
}
return region_obj, options
def parse_lambda(lambda_string: str) -> WitnessRule:
def parse_witness_rule(rule_string: str) -> WitnessRule:
"""
Turns a lambda String literal like this: a | b & c
into a set of sets like this: {{a}, {b, c}}
The lambda has to be in DNF.
Turns a rule string literal like this: a | b & c
into a set of sets (called "WitnessRule") like this: {{a}, {b, c}}
The rule string has to be in DNF.
"""
if lambda_string == "True":
if rule_string == "True":
return frozenset([frozenset()])
split_ands = set(lambda_string.split(" | "))
split_ands = set(rule_string.split(" | "))
return frozenset({frozenset(a.split(" & ")) for a in split_ands})

View File

@@ -129,7 +129,7 @@ class EntityHuntPicker:
eligible_panels_by_area = defaultdict(set)
for eligible_panel in all_eligible_panels:
associated_area = static_witness_logic.ENTITIES_BY_HEX[eligible_panel]["area"]["name"]
associated_area = static_witness_logic.ENTITIES_BY_HEX[eligible_panel]["area"].name
eligible_panels_by_area[associated_area].add(eligible_panel)
return all_eligible_panels, eligible_panels_by_area

View File

@@ -18,7 +18,7 @@ if __name__ == "__main__":
for entity_id, entity_object in static_witness_logic.ENTITIES_BY_HEX.items():
location_id = entity_object["id"]
area = entity_object["area"]["name"]
area = entity_object["area"].name
area_to_entity_ids[area].append(entity_id)
if location_id is None:

View File

@@ -464,7 +464,7 @@ 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())
potential_areas = list(static_witness_logic.ALL_AREAS_BY_NAME.values())
locations_per_area = {}
items_per_area = {}
@@ -472,14 +472,14 @@ def get_hintable_areas(world: "WitnessWorld") -> Tuple[Dict[str, List[Location]]
for area in potential_areas:
regions = [
world.get_region(region)
for region in static_witness_logic.ALL_AREAS_BY_NAME[area]["regions"]
for region in area.regions
if region in world.player_regions.created_region_names
]
locations = [location for region in regions for location in region.get_locations() if not location.is_event]
if locations:
locations_per_area[area] = locations
items_per_area[area] = [location.item for location in locations]
locations_per_area[area.name] = locations
items_per_area[area.name] = [location.item for location in locations]
return locations_per_area, items_per_area
@@ -516,7 +516,7 @@ def word_area_hint(world: "WitnessWorld", hinted_area: str, area_items: List[Ite
hunt_panels = None
if world.options.victory_condition == "panel_hunt" and hinted_area != "Easter Eggs":
hunt_panels = sum(
static_witness_logic.ENTITIES_BY_HEX[hunt_entity]["area"]["name"] == hinted_area
static_witness_logic.ENTITIES_BY_HEX[hunt_entity]["area"].name == hinted_area
for hunt_entity in world.player_logic.HUNT_ENTITIES
)
@@ -620,7 +620,7 @@ def create_all_hints(world: "WitnessWorld", hint_amount: int, area_hints: int,
already_hinted_locations |= {
loc for loc in world.multiworld.get_reachable_locations(state, world.player)
if loc.address and static_witness_logic.ENTITIES_BY_NAME[loc.name]["area"]["name"] == "Tutorial (Inside)"
if loc.address and static_witness_logic.ENTITIES_BY_NAME[loc.name]["area"].name == "Tutorial (Inside)"
}
intended_location_hints = hint_amount - area_hints

View File

@@ -1,5 +1,4 @@
from dataclasses import dataclass
from datetime import datetime
from typing import Tuple
from schema import And, Schema

View File

@@ -20,10 +20,10 @@ from collections import defaultdict
from typing import TYPE_CHECKING, Dict, List, Set, Tuple, cast
from .data import static_logic as static_witness_logic
from .data.definition_classes import ConnectionDefinition, WitnessRule
from .data.item_definition_classes import DoorItemDefinition, ItemCategory, ProgressiveItemDefinition
from .data.static_logic import StaticWitnessLogicObj
from .data.utils import (
WitnessRule,
get_boat,
get_caves_except_path_to_challenge_exclusion_list,
get_complex_additional_panels,
@@ -47,7 +47,7 @@ from .data.utils import (
get_vault_exclusion_list,
logical_and_witness_rules,
logical_or_witness_rules,
parse_lambda,
parse_witness_rule,
)
from .entity_hunt import EntityHuntPicker
@@ -97,10 +97,10 @@ class WitnessPlayerLogic:
elif self.DIFFICULTY == "none":
self.REFERENCE_LOGIC = static_witness_logic.vanilla
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL: Dict[str, Set[Tuple[str, WitnessRule]]] = copy.deepcopy(
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL: Dict[str, List[ConnectionDefinition]] = copy.deepcopy(
self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME
)
self.CONNECTIONS_BY_REGION_NAME: Dict[str, Set[Tuple[str, WitnessRule]]] = copy.deepcopy(
self.CONNECTIONS_BY_REGION_NAME: Dict[str, List[ConnectionDefinition]] = copy.deepcopy(
self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME
)
self.DEPENDENT_REQUIREMENTS_BY_HEX: Dict[str, Dict[str, WitnessRule]] = copy.deepcopy(
@@ -178,7 +178,7 @@ class WitnessPlayerLogic:
entity_obj = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity_hex]
if entity_obj["region"] is not None and entity_obj["region"]["name"] in self.UNREACHABLE_REGIONS:
if entity_obj["region"] is not None and entity_obj["region"].name in self.UNREACHABLE_REGIONS:
return frozenset()
# For the requirement of an entity, we consider two things:
@@ -270,7 +270,7 @@ class WitnessPlayerLogic:
new_items = theoretical_new_items
if dep_obj["region"] and entity_obj["region"] != dep_obj["region"]:
new_items = frozenset(
frozenset(possibility | {dep_obj["region"]["name"]})
frozenset(possibility | {dep_obj["region"].name})
for possibility in new_items
)
@@ -359,11 +359,11 @@ class WitnessPlayerLogic:
line_split = line.split(" - ")
requirement = {
"entities": parse_lambda(line_split[1]),
"entities": parse_witness_rule(line_split[1]),
}
if len(line_split) > 2:
required_items = parse_lambda(line_split[2])
required_items = parse_witness_rule(line_split[2])
items_actually_in_the_game = [
item_name for item_name, item_definition in static_witness_logic.ALL_ITEMS.items()
if item_definition.category is ItemCategory.SYMBOL
@@ -394,26 +394,31 @@ class WitnessPlayerLogic:
return
if adj_type == "New Connections":
# This adjustment type does not actually reverse the connection if it could be reversed.
# If needed, this might be added later
line_split = line.split(" - ")
source_region = line_split[0]
target_region = line_split[1]
panel_set_string = line_split[2]
for connection in self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region]:
if connection[0] == target_region:
if connection.target_region == target_region:
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region].remove(connection)
if panel_set_string == "TrueOneWay":
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region].add(
(target_region, frozenset({frozenset(["TrueOneWay"])}))
)
# This means the connection can be completely replaced
only_connection = ConnectionDefinition(target_region, frozenset({frozenset(["TrueOneWay"])}))
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region].append(only_connection)
else:
new_lambda = logical_or_witness_rules([connection[1], parse_lambda(panel_set_string)])
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region].add((target_region, new_lambda))
combined_rule = logical_or_witness_rules(
[connection.traversal_rule, parse_witness_rule(panel_set_string)]
)
combined_connection = ConnectionDefinition(target_region, combined_rule)
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region].append(combined_connection)
break
else:
new_conn = (target_region, parse_lambda(panel_set_string))
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region].add(new_conn)
new_connection = ConnectionDefinition(target_region, parse_witness_rule(panel_set_string))
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region].append(new_connection)
if adj_type == "Added Locations":
if "0x" in line:
@@ -558,7 +563,7 @@ class WitnessPlayerLogic:
self.AVAILABLE_EASTER_EGGS_PER_REGION = defaultdict(int)
for entity_hex in self.AVAILABLE_EASTER_EGGS:
region_name = static_witness_logic.ENTITIES_BY_HEX[entity_hex]["region"]["name"]
region_name = static_witness_logic.ENTITIES_BY_HEX[entity_hex]["region"].name
self.AVAILABLE_EASTER_EGGS_PER_REGION[region_name] += 1
eggs_per_check, logically_required_eggs_per_check = world.options.easter_egg_hunt.get_step_and_logical_step()
@@ -796,7 +801,7 @@ class WitnessPlayerLogic:
next_region = regions_to_check.pop()
for region_exit in self.CONNECTIONS_BY_REGION_NAME[next_region]:
target = region_exit[0]
target = region_exit.target_region
if target in reachable_regions:
continue
@@ -844,7 +849,7 @@ class WitnessPlayerLogic:
# First, entities in unreachable regions are obviously themselves unreachable.
for region in new_unreachable_regions:
for entity in static_witness_logic.ALL_REGIONS_BY_NAME[region]["physical_entities"]:
for entity in static_witness_logic.ALL_REGIONS_BY_NAME[region].physical_entities:
# Never disable the Victory Location.
if entity == self.VICTORY_LOCATION:
continue
@@ -879,11 +884,11 @@ class WitnessPlayerLogic:
if not new_unreachable_regions and not newly_discovered_disabled_entities:
return
def reduce_connection_requirement(self, connection: Tuple[str, WitnessRule]) -> WitnessRule:
def reduce_connection_requirement(self, connection: ConnectionDefinition) -> ConnectionDefinition:
all_possibilities = []
# Check each traversal option individually
for option in connection[1]:
for option in connection.traversal_rule:
individual_entity_requirements: List[WitnessRule] = []
for entity in option:
# If a connection requires solving a disabled entity, it is not valid.
@@ -901,7 +906,7 @@ class WitnessPlayerLogic:
entity_req = self.get_entity_requirement(entity)
if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity]["region"]:
region_name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity]["region"]["name"]
region_name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity]["region"].name
entity_req = logical_and_witness_rules([entity_req, frozenset({frozenset({region_name})})])
individual_entity_requirements.append(entity_req)
@@ -909,7 +914,7 @@ class WitnessPlayerLogic:
# Merge all possible requirements into one DNF condition.
all_possibilities.append(logical_and_witness_rules(individual_entity_requirements))
return logical_or_witness_rules(all_possibilities)
return ConnectionDefinition(connection.target_region, logical_or_witness_rules(all_possibilities))
def make_dependency_reduced_checklist(self) -> None:
"""
@@ -942,14 +947,14 @@ class WitnessPlayerLogic:
# Make independent region connection requirements based on the entities they require
for region, connections in self.CONNECTIONS_BY_REGION_NAME_THEORETICAL.items():
new_connections = set()
new_connections = []
for connection in connections:
overall_requirement = self.reduce_connection_requirement(connection)
reduced_connection = self.reduce_connection_requirement(connection)
# If there is a way to use this connection, add it.
if overall_requirement:
new_connections.add((connection[0], overall_requirement))
if reduced_connection.can_be_traversed:
new_connections.append(reduced_connection)
self.CONNECTIONS_BY_REGION_NAME[region] = new_connections

View File

@@ -10,8 +10,9 @@ from BaseClasses import Entrance, Region
from worlds.generic.Rules import CollectionRule
from .data import static_logic as static_witness_logic
from .data.definition_classes import WitnessRule
from .data.static_logic import StaticWitnessLogicObj
from .data.utils import WitnessRule, optimize_witness_rule
from .data.utils import optimize_witness_rule
from .locations import WitnessPlayerLocations
from .player_logic import WitnessPlayerLogic
@@ -114,7 +115,7 @@ class WitnessPlayerRegions:
if k not in player_logic.UNREACHABLE_REGIONS
}
event_locations_per_region = defaultdict(dict)
event_locations_per_region: Dict[str, Dict[str, int]] = defaultdict(dict)
for event_location, event_item_and_entity in player_logic.EVENT_ITEM_PAIRS.items():
entity_or_region = event_item_and_entity[1]
@@ -126,13 +127,13 @@ class WitnessPlayerRegions:
if region is None:
region_name = "Entry"
else:
region_name = region["name"]
region_name = region.name
order = self.reference_logic.ENTITIES_BY_HEX[entity_or_region]["order"]
event_locations_per_region[region_name][event_location] = order
for region_name, region in regions_to_create.items():
location_entities_for_this_region = [
self.reference_logic.ENTITIES_BY_HEX[entity] for entity in region["entities"]
self.reference_logic.ENTITIES_BY_HEX[entity] for entity in region.logical_entities
]
locations_for_this_region = {
entity["checkName"]: entity["order"] for entity in location_entities_for_this_region

View File

@@ -10,7 +10,7 @@ from BaseClasses import CollectionState
from worlds.generic.Rules import CollectionRule, set_rule
from .data import static_logic as static_witness_logic
from .data.utils import WitnessRule
from .data.definition_classes import WitnessRule
from .player_logic import WitnessPlayerLogic
if TYPE_CHECKING: