diff --git a/worlds/witness/data/definition_classes.py b/worlds/witness/data/definition_classes.py new file mode 100644 index 00000000..281fbfcd --- /dev/null +++ b/worlds/witness/data/definition_classes.py @@ -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) diff --git a/worlds/witness/data/static_locations.py b/worlds/witness/data/static_locations.py index 5c5ad554..a5cfc3b4 100644 --- a/worlds/witness/data/static_locations.py +++ b/worlds/witness/data/static_locations.py @@ -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) diff --git a/worlds/witness/data/static_logic.py b/worlds/witness/data/static_logic.py index 4f4786a3..bfe92467 100644 --- a/worlds/witness/data/static_logic.py +++ b/worlds/witness/data/static_logic.py @@ -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 diff --git a/worlds/witness/data/utils.py b/worlds/witness/data/utils.py index aca45738..5f562281 100644 --- a/worlds/witness/data/utils.py +++ b/worlds/witness/data/utils.py @@ -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}) diff --git a/worlds/witness/entity_hunt.py b/worlds/witness/entity_hunt.py index 9549246c..de2f7dd6 100644 --- a/worlds/witness/entity_hunt.py +++ b/worlds/witness/entity_hunt.py @@ -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 diff --git a/worlds/witness/generate_data_file.py b/worlds/witness/generate_data_file.py index cc05015c..679aa80b 100644 --- a/worlds/witness/generate_data_file.py +++ b/worlds/witness/generate_data_file.py @@ -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: diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index 6f274f5e..c82024cc 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -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 diff --git a/worlds/witness/options.py b/worlds/witness/options.py index c56209b2..1c2bc932 100644 --- a/worlds/witness/options.py +++ b/worlds/witness/options.py @@ -1,5 +1,4 @@ from dataclasses import dataclass -from datetime import datetime from typing import Tuple from schema import And, Schema diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index 1276d55d..52bddde1 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -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 diff --git a/worlds/witness/regions.py b/worlds/witness/regions.py index 8cb3678a..c057134a 100644 --- a/worlds/witness/regions.py +++ b/worlds/witness/regions.py @@ -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 diff --git a/worlds/witness/rules.py b/worlds/witness/rules.py index 866f4690..545c3e7d 100644 --- a/worlds/witness/rules.py +++ b/worlds/witness/rules.py @@ -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: