The Witness: Automatic Postgame & Disabled Panels Calculation (#2698)
* Refactor postgame code to be more readable * Change all references to options to strings * oops * Fix some outdated code related to yaml-disabled EPs * Small fixes to short/longbox stuff (thanks Medic) * comment * fix duplicate * Removed triplicate lmfao * Better comment * added another 'unfun' postgame consideration * comment * more option strings * oops * Remove an unnecessary comparison * another string missed * New classification changes (Credit: Exempt-Medic) * Don't need to pass world * Comments * Replace it with another magic system because why not at this point :DDDDDD * oops * Oops * Another was missed * Make events conditions. Disable_Non_Randomized will no longer just 'have all events' * What the fuck? Has this just always been broken? * Don't have boolean function with 'not' in the name * Another useful classification * slight code refactor * Funny haha booleans * This would create a really bad merge error * I can't believe this actually kind of works * And here's the punchline. + some bugfixes * Comment dat code * Comments galore * LMAO OOPS * so nice I did it twice * debug x2 * Careful * Add more comments * That comment is a bit unnecessary now * Fix overriding region connections * Correct a comment * Correct again * Rename variable * Idk I guess this is in this branch now * More tweaking of postgame & comments * This is commit just exists to fix that grammar error * I think I can just fucking delete this now??? * Forgot to reset something here * Delete dead codepath * Obelisk Keys were getting yote erroneously * More comments * Fix duplicate connections * Oopsington III * performance improvements & cleanup * More rules cleanup and performance improvements * Oh cool I can do this huh * Okay but this is even more swag tho * Lazy eval * remove some implicit checks * Is this too magical yet * more guard magic * Maaaaaaaagiccccccccc * Laaaaaaaaaaaaaaaazzzzzzyyyyyyyyyyy * Make it docstring * Newline bc I like that better * this is a little spooky lol * lol * Wait * spoO * Better variable name and comment * Improved comment again * better API * oops I deleted a deepcopy * lol help * Help??? * player_regionsns lmao * Add some comments * Make doors disabled properly again. I hope this works * Don't disable lasers * Omega oops * Make Floor 2 Exit not exist * Make a fix that's warps compatible * I think this was an oversight, I tested a seed and it seems to have the same result * This is definitely less Violet than before * Does this feel more violet lol * Exception if a laser gets disabled, cleanup * Ruff * >:( * consistent utils import * Make autopostgame more reviewable (hopefully) * more reviewability * WitnessRule * replace another instance of it * lint * style * comment * found the bug * Move comment * Get rid of cache and ugly allow_victory * comments and lint
This commit is contained in:
parent
da33d1576a
commit
e49b1f9fbb
|
@ -1028,7 +1028,7 @@ Mountain Floor 1 Bridge (Mountain Floor 1) - Mountain Floor 1 At Door - TrueOneW
|
|||
Mountain Floor 1 At Door (Mountain Floor 1) - Mountain Floor 2 - 0x09E54:
|
||||
Door - 0x09E54 (Exit) - 0x09EAF & 0x09F6E & 0x09E6B & 0x09E7B
|
||||
|
||||
Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near - 0x09FFB - Mountain Floor 2 Beyond Bridge - 0x09E86 - Mountain Floor 2 At Door - 0x09ED8 & 0x09E86 - Mountain Pink Bridge EP - TrueOneWay:
|
||||
Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near - 0x09FFB - Mountain Floor 2 Beyond Bridge - 0x09E86 - Mountain Floor 2 Above The Abyss - True - Mountain Pink Bridge EP - TrueOneWay:
|
||||
158426 - 0x09FD3 (Near Row 1) - True - Stars & Colored Squares & Stars + Same Colored Symbol
|
||||
158427 - 0x09FD4 (Near Row 2) - 0x09FD3 - Stars & Colored Squares & Stars + Same Colored Symbol
|
||||
158428 - 0x09FD6 (Near Row 3) - 0x09FD4 - Stars & Colored Squares & Stars + Same Colored Symbol
|
||||
|
@ -1036,7 +1036,7 @@ Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near -
|
|||
158430 - 0x09FD8 (Near Row 5) - 0x09FD7 - Symmetry & Colored Dots
|
||||
Door - 0x09FFB (Staircase Near) - 0x09FD8
|
||||
|
||||
Mountain Floor 2 At Door (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EDD:
|
||||
Mountain Floor 2 Above The Abyss (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EDD & 0x09ED8 & 0x09E86:
|
||||
Door - 0x09EDD (Elevator Room Entry) - 0x09ED8 & 0x09E86
|
||||
|
||||
Mountain Floor 2 Light Bridge Room Near (Mountain Floor 2):
|
||||
|
|
|
@ -1028,7 +1028,7 @@ Mountain Floor 1 Bridge (Mountain Floor 1) - Mountain Floor 1 At Door - TrueOneW
|
|||
Mountain Floor 1 At Door (Mountain Floor 1) - Mountain Floor 2 - 0x09E54:
|
||||
Door - 0x09E54 (Exit) - 0x09EAF & 0x09F6E & 0x09E6B & 0x09E7B
|
||||
|
||||
Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near - 0x09FFB - Mountain Floor 2 Beyond Bridge - 0x09E86 - Mountain Floor 2 At Door - 0x09ED8 & 0x09E86 - Mountain Pink Bridge EP - TrueOneWay:
|
||||
Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near - 0x09FFB - Mountain Floor 2 Beyond Bridge - 0x09E86 - Mountain Floor 2 Above The Abyss - True - Mountain Pink Bridge EP - TrueOneWay:
|
||||
158426 - 0x09FD3 (Near Row 1) - True - Stars & Colored Squares & Stars + Same Colored Symbol
|
||||
158427 - 0x09FD4 (Near Row 2) - 0x09FD3 - Stars & Triangles & Stars + Same Colored Symbol
|
||||
158428 - 0x09FD6 (Near Row 3) - 0x09FD4 - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol
|
||||
|
@ -1036,7 +1036,7 @@ Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near -
|
|||
158430 - 0x09FD8 (Near Row 5) - 0x09FD7 - Stars & Stars + Same Colored Symbol & Rotated Shapers & Eraser
|
||||
Door - 0x09FFB (Staircase Near) - 0x09FD8
|
||||
|
||||
Mountain Floor 2 At Door (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EDD:
|
||||
Mountain Floor 2 Above The Abyss (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EDD & 0x09ED8 & 0x09E86:
|
||||
Door - 0x09EDD (Elevator Room Entry) - 0x09ED8 & 0x09E86
|
||||
|
||||
Mountain Floor 2 Light Bridge Room Near (Mountain Floor 2):
|
||||
|
|
|
@ -1028,7 +1028,7 @@ Mountain Floor 1 Bridge (Mountain Floor 1) - Mountain Floor 1 At Door - TrueOneW
|
|||
Mountain Floor 1 At Door (Mountain Floor 1) - Mountain Floor 2 - 0x09E54:
|
||||
Door - 0x09E54 (Exit) - 0x09EAF & 0x09F6E & 0x09E6B & 0x09E7B
|
||||
|
||||
Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near - 0x09FFB - Mountain Floor 2 Beyond Bridge - 0x09E86 - Mountain Floor 2 At Door - 0x09ED8 & 0x09E86 - Mountain Pink Bridge EP - TrueOneWay:
|
||||
Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near - 0x09FFB - Mountain Floor 2 Beyond Bridge - 0x09E86 - Mountain Floor 2 Above The Abyss - True - Mountain Pink Bridge EP - TrueOneWay:
|
||||
158426 - 0x09FD3 (Near Row 1) - True - Colored Squares
|
||||
158427 - 0x09FD4 (Near Row 2) - 0x09FD3 - Colored Squares & Dots
|
||||
158428 - 0x09FD6 (Near Row 3) - 0x09FD4 - Stars & Colored Squares & Stars + Same Colored Symbol
|
||||
|
@ -1036,7 +1036,7 @@ Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near -
|
|||
158430 - 0x09FD8 (Near Row 5) - 0x09FD7 - Colored Squares
|
||||
Door - 0x09FFB (Staircase Near) - 0x09FD8
|
||||
|
||||
Mountain Floor 2 At Door (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EDD:
|
||||
Mountain Floor 2 Above The Abyss (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EDD & 0x09ED8 & 0x09E86:
|
||||
Door - 0x09EDD (Elevator Room Entry) - 0x09ED8 & 0x09E86
|
||||
|
||||
Mountain Floor 2 Light Bridge Room Near (Mountain Floor 2):
|
||||
|
|
|
@ -134,17 +134,3 @@ Disabled Locations:
|
|||
0x17E67 (Bunker UV Room 2)
|
||||
0x09DE0 (Bunker Laser)
|
||||
0x0A079 (Bunker Elevator Control)
|
||||
|
||||
0x034A7 (Monastery Left Shutter EP)
|
||||
0x034AD (Monastery Middle Shutter EP)
|
||||
0x034AF (Monastery Right Shutter EP)
|
||||
0x339B6 (Theater Eclipse EP)
|
||||
0x33A29 (Theater Window EP)
|
||||
0x33A2A (Theater Door EP)
|
||||
0x33B06 (Theater Church EP)
|
||||
0x3352F (Tutorial Gate EP)
|
||||
0x33600 (Tutorial Patio Flowers EP)
|
||||
0x035F5 (Bunker Tinted Door EP)
|
||||
0x000D3 (Bunker Green Room Flowers EP)
|
||||
0x33A20 (Theater Flowers EP)
|
||||
0x03BE2 (Monastery Garden Left EP)
|
||||
|
|
|
@ -1,31 +1,8 @@
|
|||
Disabled Locations:
|
||||
0x033D4 (Outside Tutorial Vault)
|
||||
0x03481 (Outside Tutorial Vault Box)
|
||||
0x033D0 (Outside Tutorial Vault Door)
|
||||
0x0CC7B (Desert Vault)
|
||||
0x0339E (Desert Vault Box)
|
||||
0x03444 (Desert Vault Door)
|
||||
0x00AFB (Shipwreck Vault)
|
||||
0x03535 (Shipwreck Vault Box)
|
||||
0x17BB4 (Shipwreck Vault Door)
|
||||
0x15ADD (Jungle Vault)
|
||||
0x03702 (Jungle Vault Box)
|
||||
0x15287 (Jungle Vault Door)
|
||||
0x002A6 (Mountainside Vault)
|
||||
0x03542 (Mountainside Vault Box)
|
||||
0x00085 (Mountainside Vault Door)
|
||||
0x2FAF6 (Tunnels Vault Box)
|
||||
0x00815 (Theater Video Input)
|
||||
0x03553 (Theater Tutorial Video)
|
||||
0x03552 (Theater Desert Video)
|
||||
0x0354E (Theater Jungle Video)
|
||||
0x03549 (Theater Challenge Video)
|
||||
0x0354F (Theater Shipwreck Video)
|
||||
0x03545 (Theater Mountain Video)
|
||||
0x03505 (Tutorial Gate Close)
|
||||
0x339B6 (Theater clipse EP)
|
||||
0x33A29 (Theater Window EP)
|
||||
0x33A2A (Theater Door EP)
|
||||
0x33B06 (Theater Church EP)
|
||||
0x33A20 (Theater Flowers EP)
|
||||
0x3352F (Tutorial Gate EP)
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
Disabled Locations:
|
||||
0x03549 (Challenge Video)
|
||||
|
||||
0x339B6 (Eclipse EP)
|
|
@ -1,2 +0,0 @@
|
|||
Disabled Locations:
|
||||
0x17FA2 (Mountain Bottom Floor Discard)
|
|
@ -1,6 +0,0 @@
|
|||
Disabled Locations:
|
||||
0x17FA2 (Mountain Bottom Floor Discard)
|
||||
0x17F33 (Rock Open Door)
|
||||
0x00FF8 (Caves Entry Panel)
|
||||
0x334E1 (Rock Control)
|
||||
0x2D77D (Caves Entry Door)
|
|
@ -1,22 +0,0 @@
|
|||
Disabled Locations:
|
||||
0x0356B (Challenge Vault Box)
|
||||
0x04D75 (Vault Door)
|
||||
0x0A332 (Start Timer)
|
||||
0x0088E (Small Basic)
|
||||
0x00BAF (Big Basic)
|
||||
0x00BF3 (Square)
|
||||
0x00C09 (Maze Map)
|
||||
0x00CDB (Stars and Dots)
|
||||
0x0051F (Symmetry)
|
||||
0x00524 (Stars and Shapers)
|
||||
0x00CD4 (Big Basic 2)
|
||||
0x00CB9 (Choice Squares Right)
|
||||
0x00CA1 (Choice Squares Middle)
|
||||
0x00C80 (Choice Squares Left)
|
||||
0x00C68 (Choice Squares 2 Right)
|
||||
0x00C59 (Choice Squares 2 Middle)
|
||||
0x00C22 (Choice Squares 2 Left)
|
||||
0x034F4 (Maze Hidden 1)
|
||||
0x034EC (Maze Hidden 2)
|
||||
0x1C31A (Dots Pillar)
|
||||
0x1C319 (Squares Pillar)
|
|
@ -1,27 +0,0 @@
|
|||
Disabled Locations:
|
||||
0x17F93 (Elevator Discard)
|
||||
0x09EEB (Elevator Control Panel)
|
||||
0x09FC1 (Giant Puzzle Bottom Left)
|
||||
0x09F8E (Giant Puzzle Bottom Right)
|
||||
0x09F01 (Giant Puzzle Top Right)
|
||||
0x09EFF (Giant Puzzle Top Left)
|
||||
0x09FDA (Giant Puzzle)
|
||||
0x09F89 (Exit Door)
|
||||
0x01983 (Pillars Room Entry Left)
|
||||
0x01987 (Pillars Room Entry Right)
|
||||
0x0C141 (Pillars Room Entry Door)
|
||||
0x0383A (Right Pillar 1)
|
||||
0x09E56 (Right Pillar 2)
|
||||
0x09E5A (Right Pillar 3)
|
||||
0x33961 (Right Pillar 4)
|
||||
0x0383D (Left Pillar 1)
|
||||
0x0383F (Left Pillar 2)
|
||||
0x03859 (Left Pillar 3)
|
||||
0x339BB (Left Pillar 4)
|
||||
0x3D9A6 (Elevator Door Closer Left)
|
||||
0x3D9A7 (Elevator Door Close Right)
|
||||
0x3C113 (Elevator Entry Left)
|
||||
0x3C114 (Elevator Entry Right)
|
||||
0x3D9AA (Back Wall Left)
|
||||
0x3D9A8 (Back Wall Right)
|
||||
0x3D9A9 (Elevator Start)
|
|
@ -1,41 +0,0 @@
|
|||
Disabled Locations:
|
||||
0x17C34 (Mountain Entry Panel)
|
||||
0x09E39 (Light Bridge Controller)
|
||||
0x09E7A (Right Row 1)
|
||||
0x09E71 (Right Row 2)
|
||||
0x09E72 (Right Row 3)
|
||||
0x09E69 (Right Row 4)
|
||||
0x09E7B (Right Row 5)
|
||||
0x09E73 (Left Row 1)
|
||||
0x09E75 (Left Row 2)
|
||||
0x09E78 (Left Row 3)
|
||||
0x09E79 (Left Row 4)
|
||||
0x09E6C (Left Row 5)
|
||||
0x09E6F (Left Row 6)
|
||||
0x09E6B (Left Row 7)
|
||||
0x33AF5 (Back Row 1)
|
||||
0x33AF7 (Back Row 2)
|
||||
0x09F6E (Back Row 3)
|
||||
0x09EAD (Trash Pillar 1)
|
||||
0x09EAF (Trash Pillar 2)
|
||||
0x09E54 (Mountain Floor 1 Exit Door)
|
||||
0x09FD3 (Near Row 1)
|
||||
0x09FD4 (Near Row 2)
|
||||
0x09FD6 (Near Row 3)
|
||||
0x09FD7 (Near Row 4)
|
||||
0x09FD8 (Near Row 5)
|
||||
0x09FFB (Staircase Near Door)
|
||||
0x09EDD (Elevator Room Entry Door)
|
||||
0x09E86 (Light Bridge Controller Near)
|
||||
0x09FCC (Far Row 1)
|
||||
0x09FCE (Far Row 2)
|
||||
0x09FCF (Far Row 3)
|
||||
0x09FD0 (Far Row 4)
|
||||
0x09FD1 (Far Row 5)
|
||||
0x09FD2 (Far Row 6)
|
||||
0x09E07 (Staircase Far Door)
|
||||
0x09ED8 (Light Bridge Controller Far)
|
||||
|
||||
0x09D63 (Pink Bridge EP)
|
||||
0x09D5D (Yellow Bridge EP)
|
||||
0x09D5E (Blue Bridge EP)
|
|
@ -1,30 +0,0 @@
|
|||
Disabled Locations:
|
||||
0x0356B (Vault Box)
|
||||
0x04D75 (Vault Door)
|
||||
0x17F33 (Rock Open Door)
|
||||
0x00FF8 (Caves Entry Panel)
|
||||
0x334E1 (Rock Control)
|
||||
0x2D77D (Caves Entry Door)
|
||||
0x09DD5 (Lone Pillar)
|
||||
0x019A5 (Caves Pillar Door)
|
||||
0x0A16E (Challenge Entry Panel)
|
||||
0x0A19A (Challenge Entry Door)
|
||||
0x0A332 (Start Timer)
|
||||
0x0088E (Small Basic)
|
||||
0x00BAF (Big Basic)
|
||||
0x00BF3 (Square)
|
||||
0x00C09 (Maze Map)
|
||||
0x00CDB (Stars and Dots)
|
||||
0x0051F (Symmetry)
|
||||
0x00524 (Stars and Shapers)
|
||||
0x00CD4 (Big Basic 2)
|
||||
0x00CB9 (Choice Squares Right)
|
||||
0x00CA1 (Choice Squares Middle)
|
||||
0x00C80 (Choice Squares Left)
|
||||
0x00C68 (Choice Squares 2 Right)
|
||||
0x00C59 (Choice Squares 2 Middle)
|
||||
0x00C22 (Choice Squares 2 Left)
|
||||
0x034F4 (Maze Hidden 1)
|
||||
0x034EC (Maze Hidden 2)
|
||||
0x1C31A (Dots Pillar)
|
||||
0x1C319 (Squares Pillar)
|
|
@ -1,5 +1,6 @@
|
|||
from collections import defaultdict
|
||||
from functools import lru_cache
|
||||
from typing import Dict, List
|
||||
from typing import Dict, List, Set, Tuple
|
||||
|
||||
from .item_definition_classes import (
|
||||
CATEGORY_NAME_MAPPINGS,
|
||||
|
@ -10,11 +11,13 @@ from .item_definition_classes import (
|
|||
WeightedItemDefinition,
|
||||
)
|
||||
from .utils import (
|
||||
WitnessRule,
|
||||
define_new_region,
|
||||
get_items,
|
||||
get_sigma_expert_logic,
|
||||
get_sigma_normal_logic,
|
||||
get_vanilla_logic,
|
||||
logical_or_witness_rules,
|
||||
parse_lambda,
|
||||
)
|
||||
|
||||
|
@ -41,7 +44,8 @@ class StaticWitnessLogicObj:
|
|||
current_region = new_region_and_connections[0]
|
||||
region_name = current_region["name"]
|
||||
self.ALL_REGIONS_BY_NAME[region_name] = current_region
|
||||
self.STATIC_CONNECTIONS_BY_REGION_NAME[region_name] = new_region_and_connections[1]
|
||||
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)
|
||||
continue
|
||||
|
||||
|
@ -80,13 +84,15 @@ 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] = {
|
||||
"panels": parse_lambda(required_panel_lambda)
|
||||
"entities": parse_lambda(required_panel_lambda)
|
||||
}
|
||||
|
||||
# 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"]["panels"].append(entity_hex)
|
||||
self.ALL_REGIONS_BY_NAME["Entry"]["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)
|
||||
continue
|
||||
|
||||
required_item_lambda = line_split.pop(0)
|
||||
|
@ -117,7 +123,7 @@ class StaticWitnessLogicObj:
|
|||
required_items = frozenset(required_items)
|
||||
|
||||
requirement = {
|
||||
"panels": required_panels,
|
||||
"entities": required_panels,
|
||||
"items": required_items
|
||||
}
|
||||
|
||||
|
@ -145,7 +151,37 @@ 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["panels"].append(entity_hex)
|
||||
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]]):
|
||||
target = connection[0]
|
||||
traversal_options = connection[1]
|
||||
|
||||
# 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)
|
||||
|
||||
if remaining_options:
|
||||
self.CONNECTIONS_WITH_DUPLICATES[target][source_region].add(frozenset(remaining_options))
|
||||
|
||||
def reverse_connections(self):
|
||||
# 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):
|
||||
# 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}
|
||||
|
||||
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))
|
||||
|
||||
def __init__(self, lines=None) -> None:
|
||||
if lines is None:
|
||||
|
@ -154,6 +190,7 @@ class StaticWitnessLogicObj:
|
|||
# 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()
|
||||
|
@ -167,6 +204,8 @@ class StaticWitnessLogicObj:
|
|||
self.ENTITY_ID_TO_NAME = dict()
|
||||
|
||||
self.read_logic_file(lines)
|
||||
self.reverse_connections()
|
||||
self.combine_connections()
|
||||
|
||||
|
||||
# Item data parsed from WitnessItems.txt
|
||||
|
|
|
@ -2,7 +2,14 @@ from functools import lru_cache
|
|||
from math import floor
|
||||
from pkgutil import get_data
|
||||
from random import random
|
||||
from typing import Any, Collection, Dict, FrozenSet, List, Set, Tuple
|
||||
from typing import Any, Collection, Dict, FrozenSet, Iterable, List, Set, Tuple
|
||||
|
||||
# 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 weighted_sample(world_random: random, population: List, weights: List[float], k: int) -> List:
|
||||
|
@ -48,7 +55,7 @@ def build_weighted_int_list(inputs: Collection[float], total: int) -> List[int]:
|
|||
return rounded_output
|
||||
|
||||
|
||||
def define_new_region(region_string: str) -> Tuple[Dict[str, Any], Set[Tuple[str, FrozenSet[FrozenSet[str]]]]]:
|
||||
def define_new_region(region_string: str) -> Tuple[Dict[str, Any], Set[Tuple[str, WitnessRule]]]:
|
||||
"""
|
||||
Returns a region object by parsing a line in the logic file
|
||||
"""
|
||||
|
@ -76,12 +83,13 @@ def define_new_region(region_string: str) -> Tuple[Dict[str, Any], Set[Tuple[str
|
|||
region_obj = {
|
||||
"name": region_name,
|
||||
"shortName": region_name_simple,
|
||||
"panels": list()
|
||||
"entities": list(),
|
||||
"physical_entities": list(),
|
||||
}
|
||||
return region_obj, options
|
||||
|
||||
|
||||
def parse_lambda(lambda_string) -> FrozenSet[FrozenSet[str]]:
|
||||
def parse_lambda(lambda_string) -> WitnessRule:
|
||||
"""
|
||||
Turns a lambda String literal like this: a | b & c
|
||||
into a set of sets like this: {{a}, {b, c}}
|
||||
|
@ -181,36 +189,8 @@ def get_discard_exclusion_list() -> List[str]:
|
|||
return get_adjustment_file("settings/Exclusions/Discards.txt")
|
||||
|
||||
|
||||
def get_caves_exclusion_list() -> List[str]:
|
||||
return get_adjustment_file("settings/Postgame/Caves.txt")
|
||||
|
||||
|
||||
def get_beyond_challenge_exclusion_list() -> List[str]:
|
||||
return get_adjustment_file("settings/Postgame/Beyond_Challenge.txt")
|
||||
|
||||
|
||||
def get_bottom_floor_discard_exclusion_list() -> List[str]:
|
||||
return get_adjustment_file("settings/Postgame/Bottom_Floor_Discard.txt")
|
||||
|
||||
|
||||
def get_bottom_floor_discard_nondoors_exclusion_list() -> List[str]:
|
||||
return get_adjustment_file("settings/Postgame/Bottom_Floor_Discard_NonDoors.txt")
|
||||
|
||||
|
||||
def get_mountain_upper_exclusion_list() -> List[str]:
|
||||
return get_adjustment_file("settings/Postgame/Mountain_Upper.txt")
|
||||
|
||||
|
||||
def get_challenge_vault_box_exclusion_list() -> List[str]:
|
||||
return get_adjustment_file("settings/Postgame/Challenge_Vault_Box.txt")
|
||||
|
||||
|
||||
def get_path_to_challenge_exclusion_list() -> List[str]:
|
||||
return get_adjustment_file("settings/Postgame/Path_To_Challenge.txt")
|
||||
|
||||
|
||||
def get_mountain_lower_exclusion_list() -> List[str]:
|
||||
return get_adjustment_file("settings/Postgame/Mountain_Lower.txt")
|
||||
def get_caves_except_path_to_challenge_exclusion_list() -> List[str]:
|
||||
return get_adjustment_file("settings/Exclusions/Caves_Except_Path_To_Challenge.txt")
|
||||
|
||||
|
||||
def get_elevators_come_to_you() -> List[str]:
|
||||
|
@ -233,21 +213,21 @@ def get_items() -> List[str]:
|
|||
return get_adjustment_file("WitnessItems.txt")
|
||||
|
||||
|
||||
def dnf_remove_redundancies(dnf_requirement: FrozenSet[FrozenSet[str]]) -> FrozenSet[FrozenSet[str]]:
|
||||
def optimize_witness_rule(witness_rule: WitnessRule) -> WitnessRule:
|
||||
"""Removes any redundant terms from a logical formula in disjunctive normal form.
|
||||
This means removing any terms that are a superset of any other term get removed.
|
||||
This is possible because of the boolean absorption law: a | (a & b) = a"""
|
||||
to_remove = set()
|
||||
|
||||
for option1 in dnf_requirement:
|
||||
for option2 in dnf_requirement:
|
||||
for option1 in witness_rule:
|
||||
for option2 in witness_rule:
|
||||
if option2 < option1:
|
||||
to_remove.add(option1)
|
||||
|
||||
return dnf_requirement - to_remove
|
||||
return witness_rule - to_remove
|
||||
|
||||
|
||||
def dnf_and(dnf_requirements: List[FrozenSet[FrozenSet[str]]]) -> FrozenSet[FrozenSet[str]]:
|
||||
def logical_and_witness_rules(witness_rules: Iterable[WitnessRule]) -> WitnessRule:
|
||||
"""
|
||||
performs the "and" operator on a list of logical formula in disjunctive normal form, represented as a set of sets.
|
||||
A logical formula might look like this: {{a, b}, {c, d}}, which would mean "a & b | c & d".
|
||||
|
@ -255,7 +235,7 @@ def dnf_and(dnf_requirements: List[FrozenSet[FrozenSet[str]]]) -> FrozenSet[Froz
|
|||
"""
|
||||
current_overall_requirement = frozenset({frozenset()})
|
||||
|
||||
for next_dnf_requirement in dnf_requirements:
|
||||
for next_dnf_requirement in witness_rules:
|
||||
new_requirement: Set[FrozenSet[str]] = set()
|
||||
|
||||
for option1 in current_overall_requirement:
|
||||
|
@ -264,4 +244,8 @@ def dnf_and(dnf_requirements: List[FrozenSet[FrozenSet[str]]]) -> FrozenSet[Froz
|
|||
|
||||
current_overall_requirement = frozenset(new_requirement)
|
||||
|
||||
return dnf_remove_redundancies(current_overall_requirement)
|
||||
return optimize_witness_rule(current_overall_requirement)
|
||||
|
||||
|
||||
def logical_or_witness_rules(witness_rules: Iterable[WitnessRule]) -> WitnessRule:
|
||||
return optimize_witness_rule(frozenset.union(*witness_rules))
|
||||
|
|
|
@ -373,9 +373,9 @@ def get_hintable_areas(world: "WitnessWorld") -> Tuple[Dict[str, List[Location]]
|
|||
|
||||
for area in potential_areas:
|
||||
regions = [
|
||||
world.player_regions.created_regions[region]
|
||||
world.get_region(region)
|
||||
for region in static_witness_logic.ALL_AREAS_BY_NAME[area]["regions"]
|
||||
if region in world.player_regions.created_regions
|
||||
if region in world.player_regions.created_region_names
|
||||
]
|
||||
locations = [location for region in regions for location in region.get_locations() if location.address]
|
||||
|
||||
|
|
|
@ -17,13 +17,39 @@ When the world has parsed its options, a second function is called to finalize t
|
|||
|
||||
import copy
|
||||
from collections import defaultdict
|
||||
from functools import lru_cache
|
||||
from logging import warning
|
||||
from typing import TYPE_CHECKING, Dict, FrozenSet, List, Set, Tuple, cast
|
||||
from typing import TYPE_CHECKING, Dict, List, Set, Tuple, cast
|
||||
|
||||
from .data import static_logic as static_witness_logic
|
||||
from .data import utils
|
||||
from .data.item_definition_classes import DoorItemDefinition, ItemCategory, ProgressiveItemDefinition
|
||||
from .data.utils import (
|
||||
WitnessRule,
|
||||
define_new_region,
|
||||
get_boat,
|
||||
get_caves_except_path_to_challenge_exclusion_list,
|
||||
get_complex_additional_panels,
|
||||
get_complex_door_panels,
|
||||
get_complex_doors,
|
||||
get_disable_unrandomized_list,
|
||||
get_discard_exclusion_list,
|
||||
get_early_caves_list,
|
||||
get_early_caves_start_list,
|
||||
get_elevators_come_to_you,
|
||||
get_ep_all_individual,
|
||||
get_ep_easy,
|
||||
get_ep_no_eclipse,
|
||||
get_ep_obelisks,
|
||||
get_laser_shuffle,
|
||||
get_obelisk_keys,
|
||||
get_simple_additional_panels,
|
||||
get_simple_doors,
|
||||
get_simple_panels,
|
||||
get_symbol_shuffle_list,
|
||||
get_vault_exclusion_list,
|
||||
logical_and_witness_rules,
|
||||
logical_or_witness_rules,
|
||||
parse_lambda,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import WitnessWorld
|
||||
|
@ -32,8 +58,7 @@ if TYPE_CHECKING:
|
|||
class WitnessPlayerLogic:
|
||||
"""WITNESS LOGIC CLASS"""
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def reduce_req_within_region(self, entity_hex: str) -> FrozenSet[FrozenSet[str]]:
|
||||
def reduce_req_within_region(self, entity_hex: str) -> WitnessRule:
|
||||
"""
|
||||
Panels in this game often only turn on when other panels are solved.
|
||||
Those other panels may have different item requirements.
|
||||
|
@ -42,35 +67,39 @@ class WitnessPlayerLogic:
|
|||
Panels outside of the same region will still be checked manually.
|
||||
"""
|
||||
|
||||
if entity_hex in self.COMPLETELY_DISABLED_ENTITIES or entity_hex in self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES:
|
||||
if self.is_disabled(entity_hex):
|
||||
return frozenset()
|
||||
|
||||
entity_obj = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity_hex]
|
||||
|
||||
these_items = frozenset({frozenset()})
|
||||
if entity_obj["region"] is not None and entity_obj["region"]["name"] in self.UNREACHABLE_REGIONS:
|
||||
return frozenset()
|
||||
|
||||
if entity_obj["id"]:
|
||||
these_items = self.DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex]["items"]
|
||||
# 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()}))
|
||||
# 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"]
|
||||
|
||||
# Remove any items that don't actually exist in the settings (e.g. Symbol Shuffle turned off)
|
||||
these_items = frozenset({
|
||||
subset.intersection(self.THEORETICAL_ITEMS_NO_MULTI)
|
||||
for subset in these_items
|
||||
})
|
||||
|
||||
# Update the list of "items that are actually being used by any entity"
|
||||
for subset in these_items:
|
||||
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(subset)
|
||||
|
||||
these_panels = self.DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex]["panels"]
|
||||
|
||||
# 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.
|
||||
if entity_hex in self.DOOR_ITEMS_BY_ID:
|
||||
door_items = frozenset({frozenset([item]) for item in self.DOOR_ITEMS_BY_ID[entity_hex]})
|
||||
|
||||
all_options: Set[FrozenSet[str]] = set()
|
||||
|
||||
for dependent_item in door_items:
|
||||
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(dependent_item)
|
||||
for items_option in these_items:
|
||||
all_options.add(items_option.union(dependent_item))
|
||||
|
||||
all_options = 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":
|
||||
|
@ -90,46 +119,70 @@ class WitnessPlayerLogic:
|
|||
else:
|
||||
these_items = all_options
|
||||
|
||||
disabled_eps = {eHex for eHex in self.COMPLETELY_DISABLED_ENTITIES
|
||||
if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[eHex]["entityType"] == "EP"}
|
||||
# Now that we have item requirements and entity dependencies, it's time for the dependency reduction.
|
||||
|
||||
these_panels = frozenset({panels - disabled_eps
|
||||
for panels in these_panels})
|
||||
|
||||
if these_panels == frozenset({frozenset()}):
|
||||
return these_items
|
||||
|
||||
all_options = set()
|
||||
# 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()
|
||||
|
||||
for option in these_panels:
|
||||
dependent_items_for_option = 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)
|
||||
|
||||
if option_entity in self.ALWAYS_EVENT_NAMES_BY_HEX:
|
||||
new_items = frozenset({frozenset([option_entity])})
|
||||
elif (entity_hex, option_entity) in self.CONDITIONAL_EVENTS:
|
||||
new_items = frozenset({frozenset([option_entity])})
|
||||
self.USED_EVENT_NAMES_BY_HEX[option_entity] = self.CONDITIONAL_EVENTS[(entity_hex, option_entity)]
|
||||
elif option_entity in {"7 Lasers", "11 Lasers", "7 Lasers + Redirect", "11 Lasers + Redirect",
|
||||
"PP2 Weirdness", "Theater to Tunnels"}:
|
||||
if option_entity in {"7 Lasers", "11 Lasers", "7 Lasers + Redirect", "11 Lasers + Redirect",
|
||||
"PP2 Weirdness", "Theater to Tunnels"}:
|
||||
new_items = frozenset({frozenset([option_entity])})
|
||||
elif option_entity in self.DISABLE_EVERYTHING_BEHIND:
|
||||
new_items = frozenset()
|
||||
else:
|
||||
new_items = self.reduce_req_within_region(option_entity)
|
||||
if dep_obj["region"] and entity_obj["region"] != dep_obj["region"]:
|
||||
new_items = frozenset(
|
||||
frozenset(possibility | {dep_obj["region"]["name"]})
|
||||
for possibility in new_items
|
||||
)
|
||||
theoretical_new_items = self.get_entity_requirement(option_entity)
|
||||
|
||||
dependent_items_for_option = utils.dnf_and([dependent_items_for_option, new_items])
|
||||
if not theoretical_new_items:
|
||||
# If the dependent entity is unsolvable & it is an EP, the current entity is an Obelisk Side.
|
||||
# In this case, we actually have to skip it because it will just become pre-solved instead.
|
||||
if dep_obj["entityType"] == "EP":
|
||||
continue
|
||||
# If the dependent entity is unsolvable and is NOT an EP, this requirement option is invalid.
|
||||
new_items = frozenset()
|
||||
elif option_entity in self.ALWAYS_EVENT_NAMES_BY_HEX:
|
||||
new_items = frozenset({frozenset([option_entity])})
|
||||
elif (entity_hex, option_entity) in self.CONDITIONAL_EVENTS:
|
||||
new_items = frozenset({frozenset([option_entity])})
|
||||
self.USED_EVENT_NAMES_BY_HEX[option_entity] = self.CONDITIONAL_EVENTS[
|
||||
(entity_hex, option_entity)
|
||||
]
|
||||
else:
|
||||
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"]})
|
||||
for possibility in new_items
|
||||
)
|
||||
|
||||
for items_option in these_items:
|
||||
for dependent_item in dependent_items_for_option:
|
||||
all_options.add(items_option.union(dependent_item))
|
||||
dependent_items_for_option = logical_and_witness_rules([dependent_items_for_option, new_items])
|
||||
|
||||
return utils.dnf_remove_redundancies(frozenset(all_options))
|
||||
# Combine the resolved dependent entity requirements with the item requirements of this entity.
|
||||
all_options.append(logical_and_witness_rules([these_items, dependent_items_for_option]))
|
||||
|
||||
# or-chain all separate dependent entity options.
|
||||
return logical_or_witness_rules(all_options)
|
||||
|
||||
def get_entity_requirement(self, entity_hex: str) -> WitnessRule:
|
||||
"""
|
||||
Get requirement of entity by its hex code.
|
||||
These requirements are cached, with the actual function calculating them being reduce_req_within_region.
|
||||
"""
|
||||
requirement = self.REQUIREMENTS_BY_HEX.get(entity_hex)
|
||||
|
||||
if requirement is None:
|
||||
requirement = self.reduce_req_within_region(entity_hex)
|
||||
self.REQUIREMENTS_BY_HEX[entity_hex] = requirement
|
||||
|
||||
return requirement
|
||||
|
||||
def make_single_adjustment(self, adj_type: str, line: str) -> None:
|
||||
from .data import static_items as static_witness_items
|
||||
|
@ -191,11 +244,11 @@ class WitnessPlayerLogic:
|
|||
line_split = line.split(" - ")
|
||||
|
||||
requirement = {
|
||||
"panels": utils.parse_lambda(line_split[1]),
|
||||
"entities": parse_lambda(line_split[1]),
|
||||
}
|
||||
|
||||
if len(line_split) > 2:
|
||||
required_items = utils.parse_lambda(line_split[2])
|
||||
required_items = parse_lambda(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
|
||||
|
@ -226,9 +279,9 @@ class WitnessPlayerLogic:
|
|||
return
|
||||
|
||||
if adj_type == "Region Changes":
|
||||
new_region_and_options = utils.define_new_region(line + ":")
|
||||
new_region_and_options = define_new_region(line + ":")
|
||||
|
||||
self.CONNECTIONS_BY_REGION_NAME[new_region_and_options[0]["name"]] = new_region_and_options[1]
|
||||
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[new_region_and_options[0]["name"]] = new_region_and_options[1]
|
||||
|
||||
return
|
||||
|
||||
|
@ -238,102 +291,99 @@ class WitnessPlayerLogic:
|
|||
target_region = line_split[1]
|
||||
panel_set_string = line_split[2]
|
||||
|
||||
for connection in self.CONNECTIONS_BY_REGION_NAME[source_region]:
|
||||
for connection in self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region]:
|
||||
if connection[0] == target_region:
|
||||
self.CONNECTIONS_BY_REGION_NAME[source_region].remove(connection)
|
||||
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region].remove(connection)
|
||||
|
||||
if panel_set_string == "TrueOneWay":
|
||||
self.CONNECTIONS_BY_REGION_NAME[source_region].add(
|
||||
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region].add(
|
||||
(target_region, frozenset({frozenset(["TrueOneWay"])}))
|
||||
)
|
||||
else:
|
||||
new_lambda = connection[1] | utils.parse_lambda(panel_set_string)
|
||||
self.CONNECTIONS_BY_REGION_NAME[source_region].add((target_region, new_lambda))
|
||||
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))
|
||||
break
|
||||
else: # Execute if loop did not break. TIL this is a thing you can do!
|
||||
new_conn = (target_region, utils.parse_lambda(panel_set_string))
|
||||
self.CONNECTIONS_BY_REGION_NAME[source_region].add(new_conn)
|
||||
else:
|
||||
new_conn = (target_region, parse_lambda(panel_set_string))
|
||||
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region].add(new_conn)
|
||||
|
||||
if adj_type == "Added Locations":
|
||||
if "0x" in line:
|
||||
line = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[line]["checkName"]
|
||||
self.ADDED_CHECKS.add(line)
|
||||
|
||||
@staticmethod
|
||||
def handle_postgame(world: "WitnessWorld") -> List[List[str]]:
|
||||
# In shuffle_postgame, panels that become accessible "after or at the same time as the goal" are disabled.
|
||||
# This has a lot of complicated considerations, which I'll try my best to explain.
|
||||
def handle_postgame(self, world: "WitnessWorld") -> List[List[str]]:
|
||||
"""
|
||||
In shuffle_postgame, panels that become accessible "after or at the same time as the goal" are disabled.
|
||||
This mostly involves the disabling of key panels (e.g. long box when the goal is short box).
|
||||
These will then hava a cascading effect on other entities that are locked "behind" them.
|
||||
"""
|
||||
|
||||
postgame_adjustments = []
|
||||
|
||||
# Make some quick references to some options
|
||||
doors = world.options.shuffle_doors >= 2 # "Panels" mode has no overarching region accessibility implications.
|
||||
remote_doors = world.options.shuffle_doors >= 2 # "Panels" mode has no region accessibility implications.
|
||||
early_caves = world.options.early_caves
|
||||
victory = world.options.victory_condition
|
||||
mnt_lasers = world.options.mountain_lasers
|
||||
chal_lasers = world.options.challenge_lasers
|
||||
|
||||
# Goal is "short box" but short box requires more lasers than long box
|
||||
reverse_shortbox_goal = victory == "mountain_box_short" and mnt_lasers > chal_lasers
|
||||
|
||||
# Goal is "short box", and long box requires at least as many lasers as short box (as god intended)
|
||||
proper_shortbox_goal = victory == "mountain_box_short" and chal_lasers >= mnt_lasers
|
||||
|
||||
# Goal is "long box", but short box requires at least as many lasers than long box.
|
||||
reverse_longbox_goal = victory == "mountain_box_long" and mnt_lasers >= chal_lasers
|
||||
|
||||
# If goal is shortbox or "reverse longbox", you will never enter the mountain from the top before winning.
|
||||
mountain_enterable_from_top = not (victory == "mountain_box_short" or reverse_longbox_goal)
|
||||
# ||| Section 1: Proper postgame cases |||
|
||||
# When something only comes into logic after the goal, e.g. "longbox is postgame if the goal is shortbox".
|
||||
|
||||
# Caves & Challenge should never have anything if doors are vanilla - definitionally "post-game"
|
||||
# This is technically imprecise, but it matches player expectations better.
|
||||
if not (early_caves or doors):
|
||||
postgame_adjustments.append(utils.get_caves_exclusion_list())
|
||||
postgame_adjustments.append(utils.get_beyond_challenge_exclusion_list())
|
||||
# Disable anything directly locked by the victory panel
|
||||
self.DISABLE_EVERYTHING_BEHIND.add(self.VICTORY_LOCATION)
|
||||
|
||||
# If Challenge is the goal, some panels on the way need to be left on, as well as Challenge Vault box itself
|
||||
if not victory == "challenge":
|
||||
postgame_adjustments.append(utils.get_path_to_challenge_exclusion_list())
|
||||
postgame_adjustments.append(utils.get_challenge_vault_box_exclusion_list())
|
||||
# If we have a long box goal, Challenge is behind the amount of lasers required to just win.
|
||||
# This is technically slightly incorrect as the Challenge Vault Box could contain a *symbol* that is required
|
||||
# to open Mountain Entry (Stars 2). However, since there is a very easy sphere 1 snipe, this is not considered.
|
||||
if victory == "mountain_box_long":
|
||||
postgame_adjustments.append(["Disabled Locations:", "0x0A332 (Challenge Timer Start)"])
|
||||
|
||||
# Challenge can only have something if the goal is not challenge or longbox itself.
|
||||
# In case of shortbox, it'd have to be a "reverse shortbox" situation where shortbox requires *more* lasers.
|
||||
# In that case, it'd also have to be a doors mode, but that's already covered by the previous block.
|
||||
if not (victory == "elevator" or reverse_shortbox_goal):
|
||||
postgame_adjustments.append(utils.get_beyond_challenge_exclusion_list())
|
||||
if not victory == "challenge":
|
||||
postgame_adjustments.append(utils.get_challenge_vault_box_exclusion_list())
|
||||
|
||||
# Mountain can't be reached if the goal is shortbox (or "reverse long box")
|
||||
if not mountain_enterable_from_top:
|
||||
postgame_adjustments.append(utils.get_mountain_upper_exclusion_list())
|
||||
|
||||
# Same goes for lower mountain, but that one *can* be reached in remote doors modes.
|
||||
if not doors:
|
||||
postgame_adjustments.append(utils.get_mountain_lower_exclusion_list())
|
||||
|
||||
# The Mountain Bottom Floor Discard is a bit complicated, so we handle it separately. ("it" == the Discard)
|
||||
# In Elevator Goal, it is definitionally in the post-game, unless remote doors is played.
|
||||
# In Challenge Goal, it is before the Challenge, so it is not post-game.
|
||||
# In Short Box Goal, you can win before turning it on, UNLESS Short Box requires MORE lasers than long box.
|
||||
# In Long Box Goal, it is always in the post-game because solving long box is what turns it on.
|
||||
if not ((victory == "elevator" and doors) or victory == "challenge" or (reverse_shortbox_goal and doors)):
|
||||
# We now know Bottom Floor Discard is in the post-game.
|
||||
# This has different consequences depending on whether remote doors is being played.
|
||||
# If doors are vanilla, Bottom Floor Discard locks a door to an area, which has to be disabled as well.
|
||||
if doors:
|
||||
postgame_adjustments.append(utils.get_bottom_floor_discard_exclusion_list())
|
||||
else:
|
||||
postgame_adjustments.append(utils.get_bottom_floor_discard_nondoors_exclusion_list())
|
||||
|
||||
# In Challenge goal + early_caves + vanilla doors, you could find something important on Bottom Floor Discard,
|
||||
# including the Caves Shortcuts themselves if playing "early_caves: start_inventory".
|
||||
# This is another thing that was deemed "unfun" more than fitting the actual definition of post-game.
|
||||
if victory == "challenge" and early_caves and not doors:
|
||||
postgame_adjustments.append(utils.get_bottom_floor_discard_nondoors_exclusion_list())
|
||||
|
||||
# If we have a proper short box goal, long box will never be activated first.
|
||||
# If we have a proper short box goal, anything based on challenge lasers will never have something required.
|
||||
if proper_shortbox_goal:
|
||||
postgame_adjustments.append(["Disabled Locations:", "0xFFF00 (Mountain Box Long)"])
|
||||
postgame_adjustments.append(["Disabled Locations:", "0x0A332 (Challenge Timer Start)"])
|
||||
|
||||
# In a case where long box can be activated before short box, short box is postgame.
|
||||
if reverse_longbox_goal:
|
||||
postgame_adjustments.append(["Disabled Locations:", "0x09F7F (Mountain Box Short)"])
|
||||
|
||||
# ||| Section 2: "Fun" considerations |||
|
||||
# These are cases in which it was deemed "unfun" to have an "oops, all lasers" situation, especially when
|
||||
# it's for a single possible item.
|
||||
|
||||
mbfd_extra_exclusions = (
|
||||
# Progressive Dots 2 behind 11 lasers in an Elevator seed with vanilla doors = :(
|
||||
victory == "elevator" and not remote_doors
|
||||
|
||||
# Caves Shortcuts / Challenge Entry (Panel) on MBFD in a Challenge seed with vanilla doors = :(
|
||||
or victory == "challenge" and early_caves and not remote_doors
|
||||
)
|
||||
|
||||
if mbfd_extra_exclusions:
|
||||
postgame_adjustments.append(["Disabled Locations:", "0xFFF00 (Mountain Box Long)"])
|
||||
|
||||
# Another big postgame case that is missed is "Desert Laser Redirect (Panel)".
|
||||
# An 11 lasers longbox seed could technically have this item on Challenge Vault Box.
|
||||
# This case is not considered and we will act like Desert Laser Redirect (Panel) is always accessible.
|
||||
# (Which means we do no additional work, this comment just exists to document that case)
|
||||
|
||||
# ||| Section 3: "Post-or-equal-game" cases |||
|
||||
# These are cases in which something comes into logic *at the same time* as your goal and thus also can't
|
||||
# possibly have a required item. These can be a bit awkward.
|
||||
|
||||
# When your victory is Challenge, but you have to get to it the vanilla way, there are no required items
|
||||
# that can show up in the Caves that aren't also needed on the descent through Mountain.
|
||||
# So, we should disable all entities in the Caves and Tunnels *except* for those that are required to enter.
|
||||
if not (early_caves or remote_doors) and victory == "challenge":
|
||||
postgame_adjustments.append(get_caves_except_path_to_challenge_exclusion_list())
|
||||
|
||||
return postgame_adjustments
|
||||
|
||||
|
@ -343,7 +393,7 @@ class WitnessPlayerLogic:
|
|||
|
||||
# Make condensed references to some options
|
||||
|
||||
doors = world.options.shuffle_doors >= 2 # "Panels" mode has no overarching region accessibility implications.
|
||||
remote_doors = world.options.shuffle_doors >= 2 # "Panels" mode has no overarching region access implications.
|
||||
lasers = world.options.shuffle_lasers
|
||||
victory = world.options.victory_condition
|
||||
mnt_lasers = world.options.mountain_lasers
|
||||
|
@ -357,16 +407,16 @@ class WitnessPlayerLogic:
|
|||
if not world.options.shuffle_discarded_panels:
|
||||
# In disable_non_randomized, the discards are needed for alternate activation triggers, UNLESS both
|
||||
# (remote) doors and lasers are shuffled.
|
||||
if not world.options.disable_non_randomized_puzzles or (doors and lasers):
|
||||
adjustment_linesets_in_order.append(utils.get_discard_exclusion_list())
|
||||
if not world.options.disable_non_randomized_puzzles or (remote_doors and lasers):
|
||||
adjustment_linesets_in_order.append(get_discard_exclusion_list())
|
||||
|
||||
if doors:
|
||||
adjustment_linesets_in_order.append(utils.get_bottom_floor_discard_exclusion_list())
|
||||
if remote_doors:
|
||||
adjustment_linesets_in_order.append(["Disabled Locations:", "0x17FA2"])
|
||||
|
||||
if not world.options.shuffle_vault_boxes:
|
||||
adjustment_linesets_in_order.append(utils.get_vault_exclusion_list())
|
||||
adjustment_linesets_in_order.append(get_vault_exclusion_list())
|
||||
if not victory == "challenge":
|
||||
adjustment_linesets_in_order.append(utils.get_challenge_vault_box_exclusion_list())
|
||||
adjustment_linesets_in_order.append(["Disabled Locations:", "0x0A332"])
|
||||
|
||||
# Victory Condition
|
||||
|
||||
|
@ -389,54 +439,54 @@ class WitnessPlayerLogic:
|
|||
])
|
||||
|
||||
if world.options.disable_non_randomized_puzzles:
|
||||
adjustment_linesets_in_order.append(utils.get_disable_unrandomized_list())
|
||||
adjustment_linesets_in_order.append(get_disable_unrandomized_list())
|
||||
|
||||
if world.options.shuffle_symbols:
|
||||
adjustment_linesets_in_order.append(utils.get_symbol_shuffle_list())
|
||||
adjustment_linesets_in_order.append(get_symbol_shuffle_list())
|
||||
|
||||
if world.options.EP_difficulty == "normal":
|
||||
adjustment_linesets_in_order.append(utils.get_ep_easy())
|
||||
adjustment_linesets_in_order.append(get_ep_easy())
|
||||
elif world.options.EP_difficulty == "tedious":
|
||||
adjustment_linesets_in_order.append(utils.get_ep_no_eclipse())
|
||||
adjustment_linesets_in_order.append(get_ep_no_eclipse())
|
||||
|
||||
if world.options.door_groupings == "regional":
|
||||
if world.options.shuffle_doors == "panels":
|
||||
adjustment_linesets_in_order.append(utils.get_simple_panels())
|
||||
adjustment_linesets_in_order.append(get_simple_panels())
|
||||
elif world.options.shuffle_doors == "doors":
|
||||
adjustment_linesets_in_order.append(utils.get_simple_doors())
|
||||
adjustment_linesets_in_order.append(get_simple_doors())
|
||||
elif world.options.shuffle_doors == "mixed":
|
||||
adjustment_linesets_in_order.append(utils.get_simple_doors())
|
||||
adjustment_linesets_in_order.append(utils.get_simple_additional_panels())
|
||||
adjustment_linesets_in_order.append(get_simple_doors())
|
||||
adjustment_linesets_in_order.append(get_simple_additional_panels())
|
||||
else:
|
||||
if world.options.shuffle_doors == "panels":
|
||||
adjustment_linesets_in_order.append(utils.get_complex_door_panels())
|
||||
adjustment_linesets_in_order.append(utils.get_complex_additional_panels())
|
||||
adjustment_linesets_in_order.append(get_complex_door_panels())
|
||||
adjustment_linesets_in_order.append(get_complex_additional_panels())
|
||||
elif world.options.shuffle_doors == "doors":
|
||||
adjustment_linesets_in_order.append(utils.get_complex_doors())
|
||||
adjustment_linesets_in_order.append(get_complex_doors())
|
||||
elif world.options.shuffle_doors == "mixed":
|
||||
adjustment_linesets_in_order.append(utils.get_complex_doors())
|
||||
adjustment_linesets_in_order.append(utils.get_complex_additional_panels())
|
||||
adjustment_linesets_in_order.append(get_complex_doors())
|
||||
adjustment_linesets_in_order.append(get_complex_additional_panels())
|
||||
|
||||
if world.options.shuffle_boat:
|
||||
adjustment_linesets_in_order.append(utils.get_boat())
|
||||
adjustment_linesets_in_order.append(get_boat())
|
||||
|
||||
if world.options.early_caves == "starting_inventory":
|
||||
adjustment_linesets_in_order.append(utils.get_early_caves_start_list())
|
||||
adjustment_linesets_in_order.append(get_early_caves_start_list())
|
||||
|
||||
if world.options.early_caves == "add_to_pool" and not doors:
|
||||
adjustment_linesets_in_order.append(utils.get_early_caves_list())
|
||||
if world.options.early_caves == "add_to_pool" and not remote_doors:
|
||||
adjustment_linesets_in_order.append(get_early_caves_list())
|
||||
|
||||
if world.options.elevators_come_to_you:
|
||||
adjustment_linesets_in_order.append(utils.get_elevators_come_to_you())
|
||||
adjustment_linesets_in_order.append(get_elevators_come_to_you())
|
||||
|
||||
for item in self.YAML_ADDED_ITEMS:
|
||||
adjustment_linesets_in_order.append(["Items:", item])
|
||||
|
||||
if lasers:
|
||||
adjustment_linesets_in_order.append(utils.get_laser_shuffle())
|
||||
adjustment_linesets_in_order.append(get_laser_shuffle())
|
||||
|
||||
if world.options.shuffle_EPs and world.options.obelisk_keys:
|
||||
adjustment_linesets_in_order.append(utils.get_obelisk_keys())
|
||||
adjustment_linesets_in_order.append(get_obelisk_keys())
|
||||
|
||||
if world.options.shuffle_EPs == "obelisk_sides":
|
||||
ep_gen = ((ep_hex, ep_obj) for (ep_hex, ep_obj) in self.REFERENCE_LOGIC.ENTITIES_BY_HEX.items()
|
||||
|
@ -448,10 +498,10 @@ class WitnessPlayerLogic:
|
|||
ep_name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[ep_hex]["checkName"]
|
||||
self.ALWAYS_EVENT_NAMES_BY_HEX[ep_hex] = f"{obelisk_name} - {ep_name}"
|
||||
else:
|
||||
adjustment_linesets_in_order.append(["Disabled Locations:"] + utils.get_ep_obelisks()[1:])
|
||||
adjustment_linesets_in_order.append(["Disabled Locations:"] + get_ep_obelisks()[1:])
|
||||
|
||||
if not world.options.shuffle_EPs:
|
||||
adjustment_linesets_in_order.append(["Disabled Locations:"] + utils.get_ep_all_individual()[1:])
|
||||
adjustment_linesets_in_order.append(["Disabled Locations:"] + get_ep_all_individual()[1:])
|
||||
|
||||
for yaml_disabled_location in self.YAML_DISABLED_LOCATIONS:
|
||||
if yaml_disabled_location not in self.REFERENCE_LOGIC.ENTITIES_BY_NAME:
|
||||
|
@ -482,16 +532,189 @@ class WitnessPlayerLogic:
|
|||
if entity_id in self.DOOR_ITEMS_BY_ID:
|
||||
del self.DOOR_ITEMS_BY_ID[entity_id]
|
||||
|
||||
def make_dependency_reduced_checklist(self) -> None:
|
||||
def discover_reachable_regions(self):
|
||||
"""
|
||||
Turns dependent check set into semi-independent check set
|
||||
Some options disable panels or remove specific items.
|
||||
This can make entire regions completely unreachable, because all their incoming connections are invalid.
|
||||
This function starts from the Entry region and performs a graph search to discover all reachable regions.
|
||||
"""
|
||||
reachable_regions = {"Entry"}
|
||||
new_regions_found = True
|
||||
|
||||
# This for loop "floods" the region graph until no more new regions are discovered.
|
||||
# Note that connections that rely on disabled entities are considered invalid.
|
||||
# This fact may lead to unreachable regions being discovered.
|
||||
while new_regions_found:
|
||||
new_regions_found = False
|
||||
regions_to_check = reachable_regions.copy()
|
||||
|
||||
# Find new regions through connections from currently reachable regions
|
||||
while regions_to_check:
|
||||
next_region = regions_to_check.pop()
|
||||
|
||||
for region_exit in self.CONNECTIONS_BY_REGION_NAME[next_region]:
|
||||
target = region_exit[0]
|
||||
|
||||
if target in reachable_regions:
|
||||
continue
|
||||
|
||||
# There may be multiple conncetions between two regions. We should check all of them to see if
|
||||
# any of them are valid.
|
||||
for option in region_exit[1]:
|
||||
# If a connection requires having access to a not-yet-reached region, do not consider it.
|
||||
# Otherwise, this connection is valid, and the target region is reachable -> break for loop
|
||||
if not any(req in self.CONNECTIONS_BY_REGION_NAME and req not in reachable_regions
|
||||
for req in option):
|
||||
break
|
||||
# If none of the connections were valid, this region is not reachable this way, for now.
|
||||
else:
|
||||
continue
|
||||
|
||||
new_regions_found = True
|
||||
regions_to_check.add(target)
|
||||
reachable_regions.add(target)
|
||||
|
||||
return reachable_regions
|
||||
|
||||
def find_unsolvable_entities(self, world: "WitnessWorld") -> None:
|
||||
"""
|
||||
Settings like "shuffle_postgame: False" may disable certain panels.
|
||||
This may make panels or regions logically locked by those panels unreachable.
|
||||
We will determine these automatically and disable them as well.
|
||||
"""
|
||||
|
||||
all_regions = set(self.CONNECTIONS_BY_REGION_NAME_THEORETICAL)
|
||||
|
||||
while True:
|
||||
# Re-make the dependency reduced entity requirements dict, which depends on currently
|
||||
self.make_dependency_reduced_checklist()
|
||||
|
||||
# Check if any regions have become unreachable.
|
||||
reachable_regions = self.discover_reachable_regions()
|
||||
new_unreachable_regions = all_regions - reachable_regions - self.UNREACHABLE_REGIONS
|
||||
if new_unreachable_regions:
|
||||
self.UNREACHABLE_REGIONS.update(new_unreachable_regions)
|
||||
|
||||
# Then, discover unreachable entities.
|
||||
newly_discovered_disabled_entities = set()
|
||||
|
||||
# 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"]:
|
||||
# Never disable the Victory Location.
|
||||
if entity == self.VICTORY_LOCATION:
|
||||
continue
|
||||
|
||||
# Never disable a laser (They should still function even if you can't walk up to them).
|
||||
if static_witness_logic.ENTITIES_BY_HEX[entity]["entityType"] == "Laser":
|
||||
continue
|
||||
|
||||
newly_discovered_disabled_entities.add(entity)
|
||||
|
||||
# Secondly, any entities that depend on disabled entities are unreachable as well.
|
||||
for entity, req in self.REQUIREMENTS_BY_HEX.items():
|
||||
# If the requirement is empty (unsolvable) and it isn't disabled already, add it to "newly disabled"
|
||||
if not req and not self.is_disabled(entity):
|
||||
# Never disable the Victory Location.
|
||||
if entity == self.VICTORY_LOCATION:
|
||||
continue
|
||||
|
||||
# If we are disabling a laser, something has gone wrong.
|
||||
if static_witness_logic.ENTITIES_BY_HEX[entity]["entityType"] == "Laser":
|
||||
laser_name = static_witness_logic.ENTITIES_BY_HEX[entity]["checkName"]
|
||||
player_name = world.multiworld.get_player_name(world.player)
|
||||
raise RuntimeError(f"Somehow, {laser_name} was disabled for player {player_name}."
|
||||
f" This is not allowed to happen, please report to Violet.")
|
||||
|
||||
newly_discovered_disabled_entities.add(entity)
|
||||
|
||||
# Disable the newly determined unreachable entities.
|
||||
self.COMPLETELY_DISABLED_ENTITIES.update(newly_discovered_disabled_entities)
|
||||
|
||||
# If we didn't find any new unreachable regions or entities this cycle, we are done.
|
||||
# If we did, we need to do another cycle to see if even more regions or entities became unreachable.
|
||||
if not new_unreachable_regions and not newly_discovered_disabled_entities:
|
||||
return
|
||||
|
||||
def reduce_connection_requirement(self, connection: Tuple[str, WitnessRule]) -> WitnessRule:
|
||||
all_possibilities = []
|
||||
|
||||
# Check each traversal option individually
|
||||
for option in connection[1]:
|
||||
individual_entity_requirements = []
|
||||
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:
|
||||
individual_entity_requirements.append(frozenset())
|
||||
# If a connection requires acquiring an event, add that event to its requirements.
|
||||
elif (entity in self.ALWAYS_EVENT_NAMES_BY_HEX
|
||||
or entity not in self.REFERENCE_LOGIC.ENTITIES_BY_HEX):
|
||||
individual_entity_requirements.append(frozenset({frozenset({entity})}))
|
||||
# If a connection requires entities, use their newly calculated independent requirements.
|
||||
else:
|
||||
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"]
|
||||
entity_req = logical_and_witness_rules([entity_req, frozenset({frozenset({region_name})})])
|
||||
|
||||
individual_entity_requirements.append(entity_req)
|
||||
|
||||
# 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)
|
||||
|
||||
def make_dependency_reduced_checklist(self):
|
||||
"""
|
||||
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.
|
||||
These dependencies are specified in the logic files (e.g. "WitnessLogic.txt") and may be modified by options.
|
||||
|
||||
Recursively having to check the requirements of every dependent entity would be very slow, so we go through this
|
||||
recursion once and make a single, independent requirement for each entity.
|
||||
|
||||
This requirement may include symbol items, door items, regions, or events.
|
||||
A requirement is saved as a two-dimensional set that represents a disjuntive normal form.
|
||||
"""
|
||||
|
||||
# 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()
|
||||
|
||||
# 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.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI = set()
|
||||
|
||||
# Make independent requirements for entities
|
||||
for entity_hex in self.DEPENDENT_REQUIREMENTS_BY_HEX.keys():
|
||||
indep_requirement = self.reduce_req_within_region(entity_hex)
|
||||
indep_requirement = self.get_entity_requirement(entity_hex)
|
||||
|
||||
self.REQUIREMENTS_BY_HEX[entity_hex] = indep_requirement
|
||||
|
||||
# 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 = []
|
||||
|
||||
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))
|
||||
|
||||
# If there are any usable outgoing connections from this region, add them.
|
||||
if new_connections:
|
||||
self.CONNECTIONS_BY_REGION_NAME[region] = new_connections
|
||||
|
||||
def finalize_items(self):
|
||||
"""
|
||||
Finalise which items are used in the world, and handle their progressive versions.
|
||||
"""
|
||||
for item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI:
|
||||
if item not in self.THEORETICAL_ITEMS:
|
||||
progressive_item_name = static_witness_logic.get_parent_progressive_item(item)
|
||||
|
@ -505,33 +728,6 @@ class WitnessPlayerLogic:
|
|||
else:
|
||||
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.add(item)
|
||||
|
||||
for region, connections in self.CONNECTIONS_BY_REGION_NAME.items():
|
||||
new_connections = []
|
||||
|
||||
for connection in connections:
|
||||
overall_requirement = frozenset()
|
||||
|
||||
for option in connection[1]:
|
||||
individual_entity_requirements = []
|
||||
for entity in option:
|
||||
if (entity in self.ALWAYS_EVENT_NAMES_BY_HEX
|
||||
or entity not in self.REFERENCE_LOGIC.ENTITIES_BY_HEX):
|
||||
individual_entity_requirements.append(frozenset({frozenset({entity})}))
|
||||
else:
|
||||
entity_req = self.reduce_req_within_region(entity)
|
||||
|
||||
if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity]["region"]:
|
||||
region_name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity]["region"]["name"]
|
||||
entity_req = utils.dnf_and([entity_req, frozenset({frozenset({region_name})})])
|
||||
|
||||
individual_entity_requirements.append(entity_req)
|
||||
|
||||
overall_requirement |= utils.dnf_and(individual_entity_requirements)
|
||||
|
||||
new_connections.append((connection[0], overall_requirement))
|
||||
|
||||
self.CONNECTIONS_BY_REGION_NAME[region] = new_connections
|
||||
|
||||
def solvability_guaranteed(self, entity_hex: str) -> bool:
|
||||
return not (
|
||||
entity_hex in self.ENTITIES_WITHOUT_ENSURED_SOLVABILITY
|
||||
|
@ -539,6 +735,12 @@ class WitnessPlayerLogic:
|
|||
or entity_hex in self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES
|
||||
)
|
||||
|
||||
def is_disabled(self, entity_hex: str) -> bool:
|
||||
return (
|
||||
entity_hex in self.COMPLETELY_DISABLED_ENTITIES
|
||||
or entity_hex in self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES
|
||||
)
|
||||
|
||||
def determine_unrequired_entities(self, world: "WitnessWorld") -> None:
|
||||
"""Figure out which major items are actually useless in this world's settings"""
|
||||
|
||||
|
@ -588,7 +790,6 @@ class WitnessPlayerLogic:
|
|||
"0x01BEA": difficulty == "none" and eps_shuffled, # Keep PP2
|
||||
"0x0A0C9": eps_shuffled or discards_shuffled or disable_non_randomized, # Cargo Box Entry Door
|
||||
"0x09EEB": discards_shuffled or mountain_upper_included, # Mountain Floor 2 Elevator Control Panel
|
||||
"0x09EDD": mountain_upper_included, # Mountain Floor 2 Exit Door
|
||||
"0x17CAB": symbols_shuffled or not disable_non_randomized or "0x17CAB" not in self.DOOR_ITEMS_BY_ID,
|
||||
# Jungle Popup Wall Panel
|
||||
}
|
||||
|
@ -598,20 +799,24 @@ class WitnessPlayerLogic:
|
|||
item_name for item_name, is_required in is_item_required_dict.items() if not is_required
|
||||
}
|
||||
|
||||
def make_event_item_pair(self, panel: str) -> Tuple[str, str]:
|
||||
def make_event_item_pair(self, entity_hex: str) -> Tuple[str, str]:
|
||||
"""
|
||||
Makes a pair of an event panel and its event item
|
||||
"""
|
||||
action = " Opened" if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[panel]["entityType"] == "Door" else " Solved"
|
||||
action = " Opened" if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity_hex]["entityType"] == "Door" else " Solved"
|
||||
|
||||
name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[panel]["checkName"] + action
|
||||
if panel not in self.USED_EVENT_NAMES_BY_HEX:
|
||||
warning(f'Panel "{name}" does not have an associated event name.')
|
||||
self.USED_EVENT_NAMES_BY_HEX[panel] = name + " Event"
|
||||
pair = (name, self.USED_EVENT_NAMES_BY_HEX[panel])
|
||||
name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity_hex]["checkName"] + action
|
||||
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
|
||||
|
||||
def make_event_panel_lists(self) -> None:
|
||||
"""
|
||||
Makes event-item pairs for entities with associated events, unless these entities are disabled.
|
||||
"""
|
||||
|
||||
self.ALWAYS_EVENT_NAMES_BY_HEX[self.VICTORY_LOCATION] = "Victory"
|
||||
|
||||
self.USED_EVENT_NAMES_BY_HEX.update(self.ALWAYS_EVENT_NAMES_BY_HEX)
|
||||
|
@ -636,6 +841,8 @@ class WitnessPlayerLogic:
|
|||
|
||||
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)
|
||||
|
@ -654,14 +861,16 @@ class WitnessPlayerLogic:
|
|||
elif self.DIFFICULTY == "none":
|
||||
self.REFERENCE_LOGIC = static_witness_logic.vanilla
|
||||
|
||||
self.CONNECTIONS_BY_REGION_NAME = copy.deepcopy(self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME)
|
||||
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()
|
||||
|
||||
# Determining which panels need to be events is a difficult process.
|
||||
# At the end, we will have EVENT_ITEM_PAIRS for all the necessary ones.
|
||||
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()
|
||||
|
@ -687,7 +896,18 @@ class WitnessPlayerLogic:
|
|||
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()
|
||||
|
|
|
@ -3,13 +3,14 @@ Defines Region for The Witness, assigns locations to them,
|
|||
and connects them with the proper requirements
|
||||
"""
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Dict, FrozenSet, List, Tuple
|
||||
from typing import TYPE_CHECKING, Dict, List, Set, Tuple
|
||||
|
||||
from BaseClasses import Entrance, Region
|
||||
|
||||
from worlds.generic.Rules import CollectionRule
|
||||
|
||||
from .data import static_logic as static_witness_logic
|
||||
from .data.utils import WitnessRule, optimize_witness_rule
|
||||
from .locations import WitnessPlayerLocations, static_witness_locations
|
||||
from .player_logic import WitnessPlayerLogic
|
||||
|
||||
|
@ -24,7 +25,7 @@ class WitnessPlayerRegions:
|
|||
logic = None
|
||||
|
||||
@staticmethod
|
||||
def make_lambda(item_requirement: FrozenSet[FrozenSet[str]], world: "WitnessWorld") -> CollectionRule:
|
||||
def make_lambda(item_requirement: WitnessRule, world: "WitnessWorld") -> CollectionRule:
|
||||
from .rules import _meets_item_requirements
|
||||
|
||||
"""
|
||||
|
@ -34,8 +35,8 @@ class WitnessPlayerRegions:
|
|||
|
||||
return _meets_item_requirements(item_requirement, world)
|
||||
|
||||
def connect_if_possible(self, world: "WitnessWorld", source: str, target: str, req: FrozenSet[FrozenSet[str]],
|
||||
regions_by_name: Dict[str, Region], backwards: bool = False):
|
||||
def connect_if_possible(self, world: "WitnessWorld", source: str, target: str, req: WitnessRule,
|
||||
regions_by_name: Dict[str, Region]):
|
||||
"""
|
||||
connect two regions and set the corresponding requirement
|
||||
"""
|
||||
|
@ -43,10 +44,6 @@ class WitnessPlayerRegions:
|
|||
# Remove any possibilities where being in the target region would be required anyway.
|
||||
real_requirement = frozenset({option for option in req if target not in option})
|
||||
|
||||
# There are some connections that should only be done one way. If this is a backwards connection, check for that
|
||||
if backwards:
|
||||
real_requirement = frozenset({option for option in real_requirement if "TrueOneWay" not in option})
|
||||
|
||||
# Dissolve any "True" or "TrueOneWay"
|
||||
real_requirement = frozenset({option - {"True", "TrueOneWay"} for option in real_requirement})
|
||||
|
||||
|
@ -56,12 +53,12 @@ class WitnessPlayerRegions:
|
|||
|
||||
# We don't need to check for the accessibility of the source region.
|
||||
final_requirement = frozenset({option - frozenset({source}) for option in real_requirement})
|
||||
final_requirement = optimize_witness_rule(final_requirement)
|
||||
|
||||
source_region = regions_by_name[source]
|
||||
target_region = regions_by_name[target]
|
||||
|
||||
backwards = " Backwards" if backwards else ""
|
||||
connection_name = source + " to " + target + backwards
|
||||
connection_name = source + " to " + target
|
||||
|
||||
connection = Entrance(
|
||||
world.player,
|
||||
|
@ -74,7 +71,8 @@ class WitnessPlayerRegions:
|
|||
source_region.exits.append(connection)
|
||||
connection.connect(target_region)
|
||||
|
||||
self.created_entrances[source, target].append(connection)
|
||||
self.two_way_entrance_register[source, target].append(connection)
|
||||
self.two_way_entrance_register[target, source].append(connection)
|
||||
|
||||
# Register any necessary indirect connections
|
||||
mentioned_regions = {
|
||||
|
@ -94,14 +92,19 @@ class WitnessPlayerRegions:
|
|||
all_locations = set()
|
||||
regions_by_name = dict()
|
||||
|
||||
for region_name, region in self.reference_logic.ALL_REGIONS_BY_NAME.items():
|
||||
regions_to_create = {
|
||||
k: v for k, v in self.reference_logic.ALL_REGIONS_BY_NAME.items()
|
||||
if k not in player_logic.UNREACHABLE_REGIONS
|
||||
}
|
||||
|
||||
for region_name, region in regions_to_create.items():
|
||||
locations_for_this_region = [
|
||||
self.reference_logic.ENTITIES_BY_HEX[panel]["checkName"] for panel in region["panels"]
|
||||
self.reference_logic.ENTITIES_BY_HEX[panel]["checkName"] for panel in region["entities"]
|
||||
if self.reference_logic.ENTITIES_BY_HEX[panel]["checkName"]
|
||||
in self.player_locations.CHECK_LOCATION_TABLE
|
||||
]
|
||||
locations_for_this_region += [
|
||||
static_witness_locations.get_event_name(panel) for panel in region["panels"]
|
||||
static_witness_locations.get_event_name(panel) for panel in region["entities"]
|
||||
if static_witness_locations.get_event_name(panel) in self.player_locations.EVENT_LOCATION_TABLE
|
||||
]
|
||||
|
||||
|
@ -111,31 +114,13 @@ class WitnessPlayerRegions:
|
|||
|
||||
regions_by_name[region_name] = new_region
|
||||
|
||||
for region_name, region in self.reference_logic.ALL_REGIONS_BY_NAME.items():
|
||||
self.created_region_names = set(regions_by_name)
|
||||
|
||||
world.multiworld.regions += regions_by_name.values()
|
||||
|
||||
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)
|
||||
self.connect_if_possible(world, connection[0], region_name, connection[1], regions_by_name, True)
|
||||
|
||||
# find regions that are completely disconnected from the start node and remove them
|
||||
regions_to_check = {"Menu"}
|
||||
reachable_regions = {"Menu"}
|
||||
|
||||
while regions_to_check:
|
||||
next_region = regions_to_check.pop()
|
||||
region_obj = regions_by_name[next_region]
|
||||
|
||||
for exit in region_obj.exits:
|
||||
target = exit.connected_region
|
||||
|
||||
if target.name in reachable_regions:
|
||||
continue
|
||||
|
||||
regions_to_check.add(target.name)
|
||||
reachable_regions.add(target.name)
|
||||
|
||||
self.created_regions = {k: v for k, v in regions_by_name.items() if k in reachable_regions}
|
||||
|
||||
world.multiworld.regions += self.created_regions.values()
|
||||
|
||||
def __init__(self, player_locations: WitnessPlayerLocations, world: "WitnessWorld") -> None:
|
||||
difficulty = world.options.puzzle_randomization
|
||||
|
@ -148,5 +133,5 @@ class WitnessPlayerRegions:
|
|||
self.reference_logic = static_witness_logic.vanilla
|
||||
|
||||
self.player_locations = player_locations
|
||||
self.created_entrances: Dict[Tuple[str, str], List[Entrance]] = defaultdict(lambda: [])
|
||||
self.created_regions: Dict[str, Region] = dict()
|
||||
self.two_way_entrance_register: Dict[Tuple[str, str], List[Entrance]] = defaultdict(lambda: [])
|
||||
self.created_region_names: Set[str] = set()
|
||||
|
|
|
@ -2,15 +2,14 @@
|
|||
Defines the rules by which locations can be accessed,
|
||||
depending on the items received
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING, FrozenSet
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from BaseClasses import CollectionState
|
||||
|
||||
from worlds.generic.Rules import CollectionRule, set_rule
|
||||
|
||||
from . import WitnessPlayerRegions
|
||||
from .data import static_logic as static_witness_logic
|
||||
from .data.utils import WitnessRule
|
||||
from .locations import WitnessPlayerLocations
|
||||
from .player_logic import WitnessPlayerLogic
|
||||
|
||||
|
@ -32,8 +31,7 @@ laser_hexes = [
|
|||
]
|
||||
|
||||
|
||||
def _has_laser(laser_hex: str, world: "WitnessWorld", player: int,
|
||||
redirect_required: bool) -> CollectionRule:
|
||||
def _has_laser(laser_hex: str, world: "WitnessWorld", player: int, redirect_required: bool) -> CollectionRule:
|
||||
if laser_hex == "0x012FB" and redirect_required:
|
||||
return lambda state: (
|
||||
_can_solve_panel(laser_hex, world, world.player, world.player_logic, world.player_locations)(state)
|
||||
|
@ -69,95 +67,164 @@ def _can_solve_panel(panel: str, world: "WitnessWorld", player: int, player_logi
|
|||
return make_lambda(panel, world)
|
||||
|
||||
|
||||
def _can_move_either_direction(state: CollectionState, source: str, target: str,
|
||||
player_regions: WitnessPlayerRegions) -> bool:
|
||||
entrance_forward = player_regions.created_entrances[source, target]
|
||||
entrance_backward = player_regions.created_entrances[target, source]
|
||||
|
||||
return (
|
||||
any(entrance.can_reach(state) for entrance in entrance_forward)
|
||||
or
|
||||
any(entrance.can_reach(state) for entrance in entrance_backward)
|
||||
)
|
||||
|
||||
|
||||
def _can_do_expert_pp2(state: CollectionState, world: "WitnessWorld") -> bool:
|
||||
"""
|
||||
For Expert PP2, you need a way to access PP2 from the front, and a separate way from the back.
|
||||
This condition is quite complicated. We'll attempt to evaluate it as lazily as possible.
|
||||
"""
|
||||
|
||||
player = world.player
|
||||
|
||||
hedge_2_access = (
|
||||
_can_move_either_direction(state, "Keep 2nd Maze", "Keep", world.player_regions)
|
||||
)
|
||||
|
||||
hedge_3_access = (
|
||||
_can_move_either_direction(state, "Keep 3rd Maze", "Keep", world.player_regions)
|
||||
or _can_move_either_direction(state, "Keep 3rd Maze", "Keep 2nd Maze", world.player_regions)
|
||||
and hedge_2_access
|
||||
)
|
||||
|
||||
hedge_4_access = (
|
||||
_can_move_either_direction(state, "Keep 4th Maze", "Keep", world.player_regions)
|
||||
or _can_move_either_direction(state, "Keep 4th Maze", "Keep 3rd Maze", world.player_regions)
|
||||
and hedge_3_access
|
||||
)
|
||||
|
||||
hedge_access = (
|
||||
_can_move_either_direction(state, "Keep 4th Maze", "Keep Tower", world.player_regions)
|
||||
and state.can_reach("Keep", "Region", player)
|
||||
and hedge_4_access
|
||||
)
|
||||
|
||||
backwards_to_fourth = (
|
||||
state.can_reach("Keep", "Region", player)
|
||||
and _can_move_either_direction(state, "Keep 4th Pressure Plate", "Keep Tower", world.player_regions)
|
||||
and (
|
||||
_can_move_either_direction(state, "Keep", "Keep Tower", world.player_regions)
|
||||
or hedge_access
|
||||
)
|
||||
)
|
||||
|
||||
shadows_shortcut = (
|
||||
state.can_reach("Main Island", "Region", player)
|
||||
and _can_move_either_direction(state, "Keep 4th Pressure Plate", "Shadows", world.player_regions)
|
||||
)
|
||||
|
||||
backwards_access = (
|
||||
_can_move_either_direction(state, "Keep 3rd Pressure Plate", "Keep 4th Pressure Plate", world.player_regions)
|
||||
and (backwards_to_fourth or shadows_shortcut)
|
||||
)
|
||||
player_regions = world.player_regions
|
||||
|
||||
front_access = (
|
||||
_can_move_either_direction(state, "Keep 2nd Pressure Plate", "Keep", world.player_regions)
|
||||
and state.can_reach("Keep", "Region", player)
|
||||
any(e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep 2nd Pressure Plate", "Keep"])
|
||||
and state.can_reach_region("Keep", player)
|
||||
)
|
||||
|
||||
return front_access and backwards_access
|
||||
# If we don't have front access, we can't do PP2.
|
||||
if not front_access:
|
||||
return False
|
||||
|
||||
# Front access works. Now, we need to check for the many ways to access PP2 from the back.
|
||||
# All of those ways lead through the PP3 exit door from PP4. So we check this first.
|
||||
|
||||
fourth_to_third = any(e.can_reach(state) for e in player_regions.two_way_entrance_register[
|
||||
"Keep 3rd Pressure Plate", "Keep 4th Pressure Plate"
|
||||
])
|
||||
|
||||
# If we can't get from PP4 to PP3, we can't do PP2.
|
||||
if not fourth_to_third:
|
||||
return False
|
||||
|
||||
# We can go from PP4 to PP3. We now need to find a way to PP4.
|
||||
# The shadows shortcut is the simplest way.
|
||||
|
||||
shadows_shortcut = (
|
||||
any(e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep 4th Pressure Plate", "Shadows"])
|
||||
)
|
||||
|
||||
if shadows_shortcut:
|
||||
return True
|
||||
|
||||
# We don't have the Shadows shortcut. This means we need to come in through the PP4 exit door instead.
|
||||
|
||||
tower_to_pp4 = any(
|
||||
e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep 4th Pressure Plate", "Keep Tower"]
|
||||
)
|
||||
|
||||
# If we don't have the PP4 exit door, we've run out of options.
|
||||
if not tower_to_pp4:
|
||||
return False
|
||||
|
||||
# We have the PP4 exit door. If we can get to Keep Tower from behind, we can do PP2.
|
||||
# The simplest way would be the Tower Shortcut.
|
||||
|
||||
tower_shortcut = any(e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep", "Keep Tower"])
|
||||
|
||||
if tower_shortcut:
|
||||
return True
|
||||
|
||||
# We don't have the Tower shortcut. At this point, there is one possibility remaining:
|
||||
# Getting to Keep Tower through the hedge mazes. This can be done in a multitude of ways.
|
||||
# No matter what, though, we would need Hedge Maze 4 Exit to Keep Tower.
|
||||
|
||||
tower_access_from_hedges = any(
|
||||
e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep 4th Maze", "Keep Tower"]
|
||||
)
|
||||
|
||||
if not tower_access_from_hedges:
|
||||
return False
|
||||
|
||||
# We can reach Keep Tower from Hedge Maze 4. If we now have the Hedge 4 Shortcut, we are immediately good.
|
||||
|
||||
hedge_4_shortcut = any(
|
||||
e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep 4th Maze", "Keep"]
|
||||
)
|
||||
|
||||
# If we have the hedge 4 shortcut, that works.
|
||||
if hedge_4_shortcut:
|
||||
return True
|
||||
|
||||
# We don't have the hedge 4 shortcut. This means we would now need to come through Hedge Maze 3.
|
||||
|
||||
hedge_3_to_4 = any(
|
||||
e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep 4th Maze", "Keep 3rd Maze"]
|
||||
)
|
||||
|
||||
if not hedge_3_to_4:
|
||||
return False
|
||||
|
||||
# We can get to Hedge 4 from Hedge 3. If we have the Hedge 3 Shortcut, we're good.
|
||||
|
||||
hedge_3_shortcut = any(
|
||||
e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep 3rd Maze", "Keep"]
|
||||
)
|
||||
|
||||
if hedge_3_shortcut:
|
||||
return True
|
||||
|
||||
# We don't have Hedge 3 Shortcut. This means we would now need to come through Hedge Maze 2.
|
||||
|
||||
hedge_2_to_3 = any(
|
||||
e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep 3rd Maze", "Keep 2nd Maze"]
|
||||
)
|
||||
|
||||
if not hedge_2_to_3:
|
||||
return False
|
||||
|
||||
# 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(
|
||||
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:
|
||||
"""
|
||||
To do Tunnels Theater Flowers EP, you need to quickly move from Theater to Tunnels.
|
||||
This condition is a little tricky. We'll attempt to evaluate it as lazily as possible.
|
||||
"""
|
||||
|
||||
# Checking for access to Theater is not necessary, as solvability of Tutorial Video is checked in the other half
|
||||
# of the Theater Flowers EP condition.
|
||||
|
||||
player_regions = world.player_regions
|
||||
|
||||
direct_access = (
|
||||
_can_move_either_direction(state, "Tunnels", "Windmill Interior", world.player_regions)
|
||||
and _can_move_either_direction(state, "Theater", "Windmill Interior", world.player_regions)
|
||||
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["Theater", "Windmill Interior"])
|
||||
)
|
||||
|
||||
theater_from_town = (
|
||||
_can_move_either_direction(state, "Town", "Windmill Interior", world.player_regions)
|
||||
and _can_move_either_direction(state, "Theater", "Windmill Interior", world.player_regions)
|
||||
or _can_move_either_direction(state, "Town", "Theater", world.player_regions)
|
||||
)
|
||||
if direct_access:
|
||||
return True
|
||||
|
||||
# We don't have direct access through the shortest path.
|
||||
# This means we somehow need to exit Theater to the Main Island, and then enter Tunnels from the Main Island.
|
||||
# Getting to Tunnels through Mountain -> Caves -> Tunnels is way too slow, so we only expect paths through Town.
|
||||
|
||||
# We need a way from Theater to Town. This is actually guaranteed, otherwise we wouldn't be in Theater.
|
||||
# The only ways to Theater are through Town and Tunnels. We just checked the Tunnels way.
|
||||
# This might need to be changed when warps are implemented.
|
||||
|
||||
# We also need a way from Town to Tunnels.
|
||||
|
||||
tunnels_from_town = (
|
||||
_can_move_either_direction(state, "Tunnels", "Windmill Interior", world.player_regions)
|
||||
and _can_move_either_direction(state, "Town", "Windmill Interior", world.player_regions)
|
||||
or _can_move_either_direction(state, "Tunnels", "Town", world.player_regions)
|
||||
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 direct_access or theater_from_town and tunnels_from_town
|
||||
return tunnels_from_town
|
||||
|
||||
|
||||
def _has_item(item: str, world: "WitnessWorld", player: int,
|
||||
player_logic: WitnessPlayerLogic, player_locations: WitnessPlayerLocations) -> CollectionRule:
|
||||
if item in player_logic.REFERENCE_LOGIC.ALL_REGIONS_BY_NAME:
|
||||
return lambda state: state.can_reach(item, "Region", player)
|
||||
region = world.get_region(item)
|
||||
return region.can_reach
|
||||
if item == "7 Lasers":
|
||||
laser_req = world.options.mountain_lasers.value
|
||||
return _has_lasers(laser_req, world, False)
|
||||
|
@ -181,8 +248,7 @@ def _has_item(item: str, world: "WitnessWorld", player: int,
|
|||
return lambda state: state.has(prog_item, player, player_logic.MULTI_AMOUNTS[item])
|
||||
|
||||
|
||||
def _meets_item_requirements(requirements: FrozenSet[FrozenSet[str]],
|
||||
world: "WitnessWorld") -> CollectionRule:
|
||||
def _meets_item_requirements(requirements: WitnessRule, world: "WitnessWorld") -> CollectionRule:
|
||||
"""
|
||||
Checks whether item and panel requirements are met for
|
||||
a panel
|
||||
|
|
Loading…
Reference in New Issue