694 lines
33 KiB
Python
694 lines
33 KiB
Python
"""
|
|
Parses the WitnessLogic.txt logic file into useful data structures.
|
|
This is the heart of the randomization.
|
|
|
|
In WitnessLogic.txt we have regions defined with their connections:
|
|
|
|
Region Name (Short name) - Connected Region 1 - Connection Requirement 1 - Connected Region 2...
|
|
|
|
And then panels in that region with the hex code used in the game
|
|
previous panels that are required to turn them on, as well as the symbols they require:
|
|
|
|
0x##### (Panel Name) - Required Panels - Required Items
|
|
|
|
On __init__, the base logic is read and all panels are given Location IDs.
|
|
When the world has parsed its options, a second function is called to finalize the logic.
|
|
"""
|
|
|
|
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 .data import static_logic as static_witness_logic
|
|
from .data import utils
|
|
from .data.item_definition_classes import DoorItemDefinition, ItemCategory, ProgressiveItemDefinition
|
|
|
|
if TYPE_CHECKING:
|
|
from . import WitnessWorld
|
|
|
|
|
|
class WitnessPlayerLogic:
|
|
"""WITNESS LOGIC CLASS"""
|
|
|
|
@lru_cache(maxsize=None)
|
|
def reduce_req_within_region(self, entity_hex: str) -> FrozenSet[FrozenSet[str]]:
|
|
"""
|
|
Panels in this game often only turn on when other panels are solved.
|
|
Those other panels may have different item requirements.
|
|
It would be slow to recursively check solvability each time.
|
|
This is why we reduce the item dependencies within the region.
|
|
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:
|
|
return frozenset()
|
|
|
|
entity_obj = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity_hex]
|
|
|
|
these_items = frozenset({frozenset()})
|
|
|
|
if entity_obj["id"]:
|
|
these_items = self.DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex]["items"]
|
|
|
|
these_items = frozenset({
|
|
subset.intersection(self.THEORETICAL_ITEMS_NO_MULTI)
|
|
for subset in these_items
|
|
})
|
|
|
|
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 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))
|
|
|
|
# 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":
|
|
# 0x28A0D depends on another entity for *non-power* reasons -> This dependency needs to be preserved,
|
|
# except in Expert, where that dependency doesn't exist, but now there *is* a power dependency.
|
|
# In the future, it'd be wise to make a distinction between "power dependencies" and other dependencies.
|
|
if entity_hex == "0x28A0D" and not any("0x28998" in option for option in these_panels):
|
|
these_items = all_options
|
|
|
|
# Another dependency that is not power-based: The Symmetry Island Upper Panel latches
|
|
elif entity_hex == "0x1C349":
|
|
these_items = all_options
|
|
|
|
else:
|
|
return frozenset(all_options)
|
|
|
|
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"}
|
|
|
|
these_panels = frozenset({panels - disabled_eps
|
|
for panels in these_panels})
|
|
|
|
if these_panels == frozenset({frozenset()}):
|
|
return these_items
|
|
|
|
all_options = set()
|
|
|
|
for option in these_panels:
|
|
dependent_items_for_option = frozenset({frozenset()})
|
|
|
|
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"}:
|
|
new_items = frozenset({frozenset([option_entity])})
|
|
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
|
|
)
|
|
|
|
dependent_items_for_option = utils.dnf_and([dependent_items_for_option, new_items])
|
|
|
|
for items_option in these_items:
|
|
for dependent_item in dependent_items_for_option:
|
|
all_options.add(items_option.union(dependent_item))
|
|
|
|
return utils.dnf_remove_redundancies(frozenset(all_options))
|
|
|
|
def make_single_adjustment(self, adj_type: str, line: str) -> None:
|
|
from .data import static_items as static_witness_items
|
|
"""Makes a single logic adjustment based on additional logic file"""
|
|
|
|
if adj_type == "Items":
|
|
line_split = line.split(" - ")
|
|
item_name = line_split[0]
|
|
|
|
if item_name not in static_witness_items.ITEM_DATA:
|
|
raise RuntimeError(f'Item "{item_name}" does not exist.')
|
|
|
|
self.THEORETICAL_ITEMS.add(item_name)
|
|
if isinstance(static_witness_logic.ALL_ITEMS[item_name], ProgressiveItemDefinition):
|
|
self.THEORETICAL_ITEMS_NO_MULTI.update(cast(ProgressiveItemDefinition,
|
|
static_witness_logic.ALL_ITEMS[item_name]).child_item_names)
|
|
else:
|
|
self.THEORETICAL_ITEMS_NO_MULTI.add(item_name)
|
|
|
|
if static_witness_logic.ALL_ITEMS[item_name].category in [ItemCategory.DOOR, ItemCategory.LASER]:
|
|
entity_hexes = cast(DoorItemDefinition, static_witness_logic.ALL_ITEMS[item_name]).panel_id_hexes
|
|
for entity_hex in entity_hexes:
|
|
self.DOOR_ITEMS_BY_ID.setdefault(entity_hex, []).append(item_name)
|
|
|
|
return
|
|
|
|
if adj_type == "Remove Items":
|
|
item_name = line
|
|
|
|
self.THEORETICAL_ITEMS.discard(item_name)
|
|
if isinstance(static_witness_logic.ALL_ITEMS[item_name], ProgressiveItemDefinition):
|
|
self.THEORETICAL_ITEMS_NO_MULTI.difference_update(
|
|
cast(ProgressiveItemDefinition, static_witness_logic.ALL_ITEMS[item_name]).child_item_names
|
|
)
|
|
else:
|
|
self.THEORETICAL_ITEMS_NO_MULTI.discard(item_name)
|
|
|
|
if static_witness_logic.ALL_ITEMS[item_name].category in [ItemCategory.DOOR, ItemCategory.LASER]:
|
|
entity_hexes = cast(DoorItemDefinition, static_witness_logic.ALL_ITEMS[item_name]).panel_id_hexes
|
|
for entity_hex in entity_hexes:
|
|
if entity_hex in self.DOOR_ITEMS_BY_ID and item_name in self.DOOR_ITEMS_BY_ID[entity_hex]:
|
|
self.DOOR_ITEMS_BY_ID[entity_hex].remove(item_name)
|
|
|
|
if adj_type == "Starting Inventory":
|
|
self.STARTING_INVENTORY.add(line)
|
|
|
|
if adj_type == "Event Items":
|
|
line_split = line.split(" - ")
|
|
new_event_name = line_split[0]
|
|
entity_hex = line_split[1]
|
|
dependent_hex_set = line_split[2].split(",")
|
|
|
|
for dependent_hex in dependent_hex_set:
|
|
self.CONDITIONAL_EVENTS[(entity_hex, dependent_hex)] = new_event_name
|
|
|
|
return
|
|
|
|
if adj_type == "Requirement Changes":
|
|
line_split = line.split(" - ")
|
|
|
|
requirement = {
|
|
"panels": utils.parse_lambda(line_split[1]),
|
|
}
|
|
|
|
if len(line_split) > 2:
|
|
required_items = utils.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
|
|
]
|
|
required_items = frozenset(
|
|
subset.intersection(items_actually_in_the_game)
|
|
for subset in required_items
|
|
)
|
|
|
|
requirement["items"] = required_items
|
|
|
|
self.DEPENDENT_REQUIREMENTS_BY_HEX[line_split[0]] = requirement
|
|
|
|
return
|
|
|
|
if adj_type == "Disabled Locations":
|
|
entity_hex = line[:7]
|
|
|
|
self.COMPLETELY_DISABLED_ENTITIES.add(entity_hex)
|
|
|
|
return
|
|
|
|
if adj_type == "Irrelevant Locations":
|
|
entity_hex = line[:7]
|
|
|
|
self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES.add(entity_hex)
|
|
|
|
return
|
|
|
|
if adj_type == "Region Changes":
|
|
new_region_and_options = utils.define_new_region(line + ":")
|
|
|
|
self.CONNECTIONS_BY_REGION_NAME[new_region_and_options[0]["name"]] = new_region_and_options[1]
|
|
|
|
return
|
|
|
|
if adj_type == "New Connections":
|
|
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[source_region]:
|
|
if connection[0] == target_region:
|
|
self.CONNECTIONS_BY_REGION_NAME[source_region].remove(connection)
|
|
|
|
if panel_set_string == "TrueOneWay":
|
|
self.CONNECTIONS_BY_REGION_NAME[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))
|
|
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)
|
|
|
|
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.
|
|
postgame_adjustments = []
|
|
|
|
# Make some quick references to some options
|
|
doors = world.options.shuffle_doors >= 2 # "Panels" mode has no overarching 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)
|
|
|
|
# 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())
|
|
|
|
# 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())
|
|
|
|
# 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 proper_shortbox_goal:
|
|
postgame_adjustments.append(["Disabled Locations:", "0xFFF00 (Mountain Box Long)"])
|
|
|
|
return postgame_adjustments
|
|
|
|
def make_options_adjustments(self, world: "WitnessWorld") -> None:
|
|
"""Makes logic adjustments based on options"""
|
|
adjustment_linesets_in_order = []
|
|
|
|
# Make condensed references to some options
|
|
|
|
doors = world.options.shuffle_doors >= 2 # "Panels" mode has no overarching region accessibility implications.
|
|
lasers = world.options.shuffle_lasers
|
|
victory = world.options.victory_condition
|
|
mnt_lasers = world.options.mountain_lasers
|
|
chal_lasers = world.options.challenge_lasers
|
|
|
|
# Exclude panels from the post-game if shuffle_postgame is false.
|
|
if not world.options.shuffle_postgame:
|
|
adjustment_linesets_in_order += self.handle_postgame(world)
|
|
|
|
# Exclude Discards / Vaults
|
|
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 doors:
|
|
adjustment_linesets_in_order.append(utils.get_bottom_floor_discard_exclusion_list())
|
|
|
|
if not world.options.shuffle_vault_boxes:
|
|
adjustment_linesets_in_order.append(utils.get_vault_exclusion_list())
|
|
if not victory == "challenge":
|
|
adjustment_linesets_in_order.append(utils.get_challenge_vault_box_exclusion_list())
|
|
|
|
# Victory Condition
|
|
|
|
if victory == "elevator":
|
|
self.VICTORY_LOCATION = "0x3D9A9"
|
|
elif victory == "challenge":
|
|
self.VICTORY_LOCATION = "0x0356B"
|
|
elif victory == "mountain_box_short":
|
|
self.VICTORY_LOCATION = "0x09F7F"
|
|
elif victory == "mountain_box_long":
|
|
self.VICTORY_LOCATION = "0xFFF00"
|
|
|
|
# Long box can usually only be solved by opening Mountain Entry. However, if it requires 7 lasers or less
|
|
# (challenge_lasers <= 7), you can now solve it without opening Mountain Entry first.
|
|
# Furthermore, if the user sets mountain_lasers > 7, the box is rotated to not require Mountain Entry either.
|
|
if chal_lasers <= 7 or mnt_lasers > 7:
|
|
adjustment_linesets_in_order.append([
|
|
"Requirement Changes:",
|
|
"0xFFF00 - 11 Lasers - True",
|
|
])
|
|
|
|
if world.options.disable_non_randomized_puzzles:
|
|
adjustment_linesets_in_order.append(utils.get_disable_unrandomized_list())
|
|
|
|
if world.options.shuffle_symbols:
|
|
adjustment_linesets_in_order.append(utils.get_symbol_shuffle_list())
|
|
|
|
if world.options.EP_difficulty == "normal":
|
|
adjustment_linesets_in_order.append(utils.get_ep_easy())
|
|
elif world.options.EP_difficulty == "tedious":
|
|
adjustment_linesets_in_order.append(utils.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())
|
|
elif world.options.shuffle_doors == "doors":
|
|
adjustment_linesets_in_order.append(utils.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())
|
|
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())
|
|
elif world.options.shuffle_doors == "doors":
|
|
adjustment_linesets_in_order.append(utils.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())
|
|
|
|
if world.options.shuffle_boat:
|
|
adjustment_linesets_in_order.append(utils.get_boat())
|
|
|
|
if world.options.early_caves == "starting_inventory":
|
|
adjustment_linesets_in_order.append(utils.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.elevators_come_to_you:
|
|
adjustment_linesets_in_order.append(utils.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())
|
|
|
|
if world.options.shuffle_EPs and world.options.obelisk_keys:
|
|
adjustment_linesets_in_order.append(utils.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()
|
|
if ep_obj["entityType"] == "EP")
|
|
|
|
for ep_hex, ep_obj in ep_gen:
|
|
obelisk = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[self.REFERENCE_LOGIC.EP_TO_OBELISK_SIDE[ep_hex]]
|
|
obelisk_name = obelisk["checkName"]
|
|
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:])
|
|
|
|
if not world.options.shuffle_EPs:
|
|
adjustment_linesets_in_order.append(["Disabled Locations:"] + utils.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:
|
|
continue
|
|
|
|
loc_obj = self.REFERENCE_LOGIC.ENTITIES_BY_NAME[yaml_disabled_location]
|
|
|
|
if loc_obj["entityType"] == "EP":
|
|
self.COMPLETELY_DISABLED_ENTITIES.add(loc_obj["entity_hex"])
|
|
|
|
elif loc_obj["entityType"] in {"General", "Vault", "Discard"}:
|
|
self.EXCLUDED_LOCATIONS.add(loc_obj["entity_hex"])
|
|
|
|
for adjustment_lineset in adjustment_linesets_in_order:
|
|
current_adjustment_type = None
|
|
|
|
for line in adjustment_lineset:
|
|
if len(line) == 0:
|
|
continue
|
|
|
|
if line[-1] == ":":
|
|
current_adjustment_type = line[:-1]
|
|
continue
|
|
|
|
self.make_single_adjustment(current_adjustment_type, line)
|
|
|
|
for entity_id in self.COMPLETELY_DISABLED_ENTITIES:
|
|
if entity_id in self.DOOR_ITEMS_BY_ID:
|
|
del self.DOOR_ITEMS_BY_ID[entity_id]
|
|
|
|
def make_dependency_reduced_checklist(self) -> None:
|
|
"""
|
|
Turns dependent check set into semi-independent check set
|
|
"""
|
|
|
|
for entity_hex in self.DEPENDENT_REQUIREMENTS_BY_HEX.keys():
|
|
indep_requirement = self.reduce_req_within_region(entity_hex)
|
|
|
|
self.REQUIREMENTS_BY_HEX[entity_hex] = indep_requirement
|
|
|
|
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)
|
|
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.add(progressive_item_name)
|
|
child_items = cast(ProgressiveItemDefinition,
|
|
static_witness_logic.ALL_ITEMS[progressive_item_name]).child_item_names
|
|
multi_list = [child_item for child_item in child_items
|
|
if child_item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI]
|
|
self.MULTI_AMOUNTS[item] = multi_list.index(item) + 1
|
|
self.MULTI_LISTS[progressive_item_name] = multi_list
|
|
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
|
|
or 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"""
|
|
|
|
# Gather quick references to relevant options
|
|
eps_shuffled = world.options.shuffle_EPs
|
|
come_to_you = world.options.elevators_come_to_you
|
|
difficulty = world.options.puzzle_randomization
|
|
discards_shuffled = world.options.shuffle_discarded_panels
|
|
boat_shuffled = world.options.shuffle_boat
|
|
symbols_shuffled = world.options.shuffle_symbols
|
|
disable_non_randomized = world.options.disable_non_randomized_puzzles
|
|
postgame_included = world.options.shuffle_postgame
|
|
goal = world.options.victory_condition
|
|
doors = world.options.shuffle_doors
|
|
shortbox_req = world.options.mountain_lasers
|
|
longbox_req = world.options.challenge_lasers
|
|
|
|
# Make some helper booleans so it is easier to follow what's going on
|
|
mountain_upper_is_in_postgame = (
|
|
goal == "mountain_box_short"
|
|
or goal == "mountain_box_long" and longbox_req <= shortbox_req
|
|
)
|
|
mountain_upper_included = postgame_included or not mountain_upper_is_in_postgame
|
|
remote_doors = doors >= 2
|
|
door_panels = doors == "panels" or doors == "mixed"
|
|
|
|
# It is easier to think about when these items *are* required, so we make that dict first
|
|
# If the entity is disabled anyway, we don't need to consider that case
|
|
is_item_required_dict = {
|
|
"0x03750": eps_shuffled, # Monastery Garden Entry Door
|
|
"0x275FA": eps_shuffled, # Boathouse Hook Control
|
|
"0x17D02": eps_shuffled, # Windmill Turn Control
|
|
"0x0368A": symbols_shuffled or door_panels, # Quarry Stoneworks Stairs Door
|
|
"0x3865F": symbols_shuffled or door_panels or eps_shuffled, # Quarry Boathouse 2nd Barrier
|
|
"0x17CC4": come_to_you or eps_shuffled, # Quarry Elevator Panel
|
|
"0x17E2B": come_to_you and boat_shuffled or eps_shuffled, # Swamp Long Bridge
|
|
"0x0CF2A": False, # Jungle Monastery Garden Shortcut
|
|
"0x17CAA": remote_doors, # Jungle Monastery Garden Shortcut Panel
|
|
"0x0364E": False, # Monastery Laser Shortcut Door
|
|
"0x03713": remote_doors, # Monastery Laser Shortcut Panel
|
|
"0x03313": False, # Orchard Second Gate
|
|
"0x337FA": remote_doors, # Jungle Bamboo Laser Shortcut Panel
|
|
"0x3873B": False, # Jungle Bamboo Laser Shortcut Door
|
|
"0x335AB": False, # Caves Elevator Controls
|
|
"0x335AC": False, # Caves Elevator Controls
|
|
"0x3369D": False, # Caves Elevator Controls
|
|
"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
|
|
}
|
|
|
|
# Now, return the keys of the dict entries where the result is False to get unrequired major items
|
|
self.ENTITIES_WITHOUT_ENSURED_SOLVABILITY |= {
|
|
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]:
|
|
"""
|
|
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"
|
|
|
|
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])
|
|
return pair
|
|
|
|
def make_event_panel_lists(self) -> None:
|
|
self.ALWAYS_EVENT_NAMES_BY_HEX[self.VICTORY_LOCATION] = "Victory"
|
|
|
|
self.USED_EVENT_NAMES_BY_HEX.update(self.ALWAYS_EVENT_NAMES_BY_HEX)
|
|
|
|
self.USED_EVENT_NAMES_BY_HEX = {
|
|
event_hex: event_name for event_hex, event_name in self.USED_EVENT_NAMES_BY_HEX.items()
|
|
if self.solvability_guaranteed(event_hex)
|
|
}
|
|
|
|
for panel in self.USED_EVENT_NAMES_BY_HEX:
|
|
pair = self.make_event_item_pair(panel)
|
|
self.EVENT_ITEM_PAIRS[pair[0]] = pair[1]
|
|
|
|
def __init__(self, world: "WitnessWorld", disabled_locations: Set[str], start_inv: Dict[str, int]) -> None:
|
|
self.YAML_DISABLED_LOCATIONS = disabled_locations
|
|
self.YAML_ADDED_ITEMS = start_inv
|
|
|
|
self.EVENT_PANELS_FROM_PANELS = set()
|
|
self.EVENT_PANELS_FROM_REGIONS = set()
|
|
|
|
self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES = set()
|
|
|
|
self.ENTITIES_WITHOUT_ENSURED_SOLVABILITY = set()
|
|
|
|
self.THEORETICAL_ITEMS = set()
|
|
self.THEORETICAL_ITEMS_NO_MULTI = set()
|
|
self.MULTI_AMOUNTS = defaultdict(lambda: 1)
|
|
self.MULTI_LISTS = dict()
|
|
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI = set()
|
|
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME = set()
|
|
self.DOOR_ITEMS_BY_ID: Dict[str, List[str]] = {}
|
|
self.STARTING_INVENTORY = set()
|
|
|
|
self.DIFFICULTY = world.options.puzzle_randomization
|
|
|
|
if self.DIFFICULTY == "sigma_normal":
|
|
self.REFERENCE_LOGIC = static_witness_logic.sigma_normal
|
|
elif self.DIFFICULTY == "sigma_expert":
|
|
self.REFERENCE_LOGIC = static_witness_logic.sigma_expert
|
|
elif self.DIFFICULTY == "none":
|
|
self.REFERENCE_LOGIC = static_witness_logic.vanilla
|
|
|
|
self.CONNECTIONS_BY_REGION_NAME = copy.deepcopy(self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME)
|
|
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.PRECOMPLETED_LOCATIONS = set()
|
|
self.EXCLUDED_LOCATIONS = set()
|
|
self.ADDED_CHECKS = set()
|
|
self.VICTORY_LOCATION = "0x0356B"
|
|
|
|
self.ALWAYS_EVENT_NAMES_BY_HEX = {
|
|
"0x00509": "+1 Laser (Symmetry Laser)",
|
|
"0x012FB": "+1 Laser (Desert Laser)",
|
|
"0x09F98": "Desert Laser Redirection",
|
|
"0x01539": "+1 Laser (Quarry Laser)",
|
|
"0x181B3": "+1 Laser (Shadows Laser)",
|
|
"0x014BB": "+1 Laser (Keep Laser)",
|
|
"0x17C65": "+1 Laser (Monastery Laser)",
|
|
"0x032F9": "+1 Laser (Town Laser)",
|
|
"0x00274": "+1 Laser (Jungle Laser)",
|
|
"0x0C2B2": "+1 Laser (Bunker Laser)",
|
|
"0x00BF6": "+1 Laser (Swamp Laser)",
|
|
"0x028A4": "+1 Laser (Treehouse Laser)",
|
|
"0x17C34": "Mountain Entry",
|
|
"0xFFF00": "Bottom Floor Discard Turns On",
|
|
}
|
|
|
|
self.USED_EVENT_NAMES_BY_HEX = {}
|
|
self.CONDITIONAL_EVENTS = {}
|
|
|
|
self.make_options_adjustments(world)
|
|
self.determine_unrequired_entities(world)
|
|
self.make_dependency_reduced_checklist()
|
|
self.make_event_panel_lists()
|