Archipelago/worlds/witness/player_logic.py

532 lines
23 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 typing import Set, Dict
from logging import warning
from BaseClasses import MultiWorld
from .static_logic import StaticWitnessLogic
from .utils import define_new_region, get_disable_unrandomized_list, parse_lambda, get_early_utm_list, \
get_symbol_shuffle_list, get_door_panel_shuffle_list, get_doors_complex_list, get_doors_max_list, \
get_doors_simple_list, get_laser_shuffle, get_ep_all_individual, get_ep_obelisks, get_ep_easy, get_ep_no_eclipse, \
get_ep_no_caves, get_ep_no_mountain, get_ep_no_videos
from .Options import is_option_enabled, get_option_value, the_witness_options
class WitnessPlayerLogic:
"""WITNESS LOGIC CLASS"""
def reduce_req_within_region(self, panel_hex):
"""
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 panel_hex in self.COMPLETELY_DISABLED_CHECKS or panel_hex in self.PRECOMPLETED_LOCATIONS:
return frozenset()
check_obj = self.REFERENCE_LOGIC.CHECKS_BY_HEX[panel_hex]
these_items = frozenset({frozenset()})
if check_obj["id"]:
these_items = self.DEPENDENT_REQUIREMENTS_BY_HEX[panel_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)
if panel_hex in self.DOOR_ITEMS_BY_ID:
door_items = frozenset({frozenset([item]) for item in self.DOOR_ITEMS_BY_ID[panel_hex]})
all_options = set()
for dependentItem in door_items:
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(dependentItem)
for items_option in these_items:
all_options.add(items_option.union(dependentItem))
if panel_hex != "0x28A0D":
return frozenset(all_options)
else: # 0x28A0D depends on another entity for *non-power* reasons -> This dependency needs to be preserved
these_items = all_options
these_panels = self.DEPENDENT_REQUIREMENTS_BY_HEX[panel_hex]["panels"]
these_panels = frozenset({panels - self.PRECOMPLETED_LOCATIONS 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_panel in option:
dep_obj = self.REFERENCE_LOGIC.CHECKS_BY_HEX.get(option_panel)
if option_panel in self.COMPLETELY_DISABLED_CHECKS:
new_items = frozenset()
elif option_panel in {"7 Lasers", "11 Lasers", "PP2 Weirdness", "Theater to Tunnels"}:
new_items = frozenset({frozenset([option_panel])})
# If a panel turns on when a panel in a different region turns on,
# the latter panel will be an "event panel", unless it ends up being
# a location itself. This prevents generation failures.
elif dep_obj["region"]["name"] != check_obj["region"]["name"]:
new_items = frozenset({frozenset([option_panel])})
self.EVENT_PANELS_FROM_PANELS.add(option_panel)
elif option_panel in self.ALWAYS_EVENT_NAMES_BY_HEX.keys():
new_items = frozenset({frozenset([option_panel])})
self.EVENT_PANELS_FROM_PANELS.add(option_panel)
else:
new_items = self.reduce_req_within_region(option_panel)
updated_items = set()
for items_option in dependent_items_for_option:
for items_option2 in new_items:
updated_items.add(items_option.union(items_option2))
dependent_items_for_option = updated_items
for items_option in these_items:
for dependentItem in dependent_items_for_option:
all_options.add(items_option.union(dependentItem))
return frozenset(all_options)
def make_single_adjustment(self, adj_type, line):
from . import StaticWitnessItems
"""Makes a single logic adjustment based on additional logic file"""
if adj_type == "Items":
line_split = line.split(" - ")
item = line_split[0]
if item not in StaticWitnessItems.ALL_ITEM_TABLE:
raise RuntimeError("Item \"" + item + "\" does not exit.")
self.THEORETICAL_ITEMS.add(item)
self.THEORETICAL_ITEMS_NO_MULTI.update(StaticWitnessLogic.PROGRESSIVE_TO_ITEMS.get(item, [item]))
if item in StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT:
panel_hexes = StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT[item][2]
for panel_hex in panel_hexes:
self.DOOR_ITEMS_BY_ID.setdefault(panel_hex, set()).add(item)
return
if adj_type == "Remove Items":
self.THEORETICAL_ITEMS.discard(line)
for i in StaticWitnessLogic.PROGRESSIVE_TO_ITEMS.get(line, [line]):
self.THEORETICAL_ITEMS_NO_MULTI.discard(i)
if line in StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT:
panel_hexes = StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT[line][2]
for panel_hex in panel_hexes:
if panel_hex in self.DOOR_ITEMS_BY_ID:
self.DOOR_ITEMS_BY_ID[panel_hex].discard(line)
if adj_type == "Starting Inventory":
self.STARTING_INVENTORY.add(line)
if adj_type == "Event Items":
line_split = line.split(" - ")
hex_set = line_split[1].split(",")
for hex_code in hex_set:
self.ALWAYS_EVENT_NAMES_BY_HEX[hex_code] = line_split[0]
"""
Should probably do this differently...
Events right now depend on a panel.
That seems bad.
"""
to_remove = set()
for hex_code, event_name in self.ALWAYS_EVENT_NAMES_BY_HEX.items():
if hex_code not in hex_set and event_name == line_split[0]:
to_remove.add(hex_code)
for remove in to_remove:
del self.ALWAYS_EVENT_NAMES_BY_HEX[remove]
return
if adj_type == "Requirement Changes":
line_split = line.split(" - ")
requirement = {
"panels": parse_lambda(line_split[1]),
}
if len(line_split) > 2:
required_items = parse_lambda(line_split[2])
items_actually_in_the_game = {item[0] for item in StaticWitnessLogic.ALL_SYMBOL_ITEMS}
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":
panel_hex = line[:7]
self.COMPLETELY_DISABLED_CHECKS.add(panel_hex)
return
if adj_type == "Region Changes":
new_region_and_options = define_new_region(line + ":")
self.CONNECTIONS_BY_REGION_NAME[new_region_and_options[0]["name"]] = new_region_and_options[1]
return
if adj_type == "Added Locations":
if "0x" in line:
line = StaticWitnessLogic.CHECKS_BY_HEX[line]["checkName"]
self.ADDED_CHECKS.add(line)
if adj_type == "Precompleted Locations":
self.PRECOMPLETED_LOCATIONS.add(line)
def make_options_adjustments(self, world, player):
"""Makes logic adjustments based on options"""
adjustment_linesets_in_order = []
if get_option_value(world, player, "victory_condition") == 0:
self.VICTORY_LOCATION = "0x3D9A9"
elif get_option_value(world, player, "victory_condition") == 1:
self.VICTORY_LOCATION = "0x0356B"
elif get_option_value(world, player, "victory_condition") == 2:
self.VICTORY_LOCATION = "0x09F7F"
elif get_option_value(world, player, "victory_condition") == 3:
self.VICTORY_LOCATION = "0xFFF00"
if get_option_value(world, player, "challenge_lasers") <= 7:
adjustment_linesets_in_order.append([
"Requirement Changes:",
"0xFFF00 - 11 Lasers - True",
])
if is_option_enabled(world, player, "disable_non_randomized_puzzles"):
adjustment_linesets_in_order.append(get_disable_unrandomized_list())
if is_option_enabled(world, player, "shuffle_symbols") or "shuffle_symbols" not in the_witness_options.keys():
adjustment_linesets_in_order.append(get_symbol_shuffle_list())
if get_option_value(world, player, "EP_difficulty") == 0:
adjustment_linesets_in_order.append(get_ep_easy())
elif get_option_value(world, player, "EP_difficulty") == 1:
adjustment_linesets_in_order.append(get_ep_no_eclipse())
if not is_option_enabled(world, player, "shuffle_vault_boxes"):
adjustment_linesets_in_order.append(get_ep_no_videos())
doors = get_option_value(world, player, "shuffle_doors") >= 2
earlyutm = is_option_enabled(world, player, "early_secret_area")
victory = get_option_value(world, player, "victory_condition")
mount_lasers = get_option_value(world, player, "mountain_lasers")
chal_lasers = get_option_value(world, player, "challenge_lasers")
excluse_postgame = not is_option_enabled(world, player, "shuffle_postgame")
if excluse_postgame and not (earlyutm or doors):
adjustment_linesets_in_order.append(get_ep_no_caves())
mountain_enterable_from_top = victory == 0 or victory == 1 or (victory == 3 and chal_lasers > mount_lasers)
if excluse_postgame and not mountain_enterable_from_top:
adjustment_linesets_in_order.append(get_ep_no_mountain())
if get_option_value(world, player, "shuffle_doors") == 1:
adjustment_linesets_in_order.append(get_door_panel_shuffle_list())
if get_option_value(world, player, "shuffle_doors") == 2:
adjustment_linesets_in_order.append(get_doors_simple_list())
if get_option_value(world, player, "shuffle_doors") == 3:
adjustment_linesets_in_order.append(get_doors_complex_list())
if get_option_value(world, player, "shuffle_doors") == 4:
adjustment_linesets_in_order.append(get_doors_max_list())
if is_option_enabled(world, player, "early_secret_area"):
adjustment_linesets_in_order.append(get_early_utm_list())
for item in self.YAML_ADDED_ITEMS:
adjustment_linesets_in_order.append(["Items:", item])
if is_option_enabled(world, player, "shuffle_lasers"):
adjustment_linesets_in_order.append(get_laser_shuffle())
if get_option_value(world, player, "shuffle_EPs") == 0: # No EP Shuffle
adjustment_linesets_in_order.append(["Disabled Locations:"] + get_ep_all_individual()[1:])
adjustment_linesets_in_order.append(["Disabled Locations:"] + get_ep_obelisks()[1:])
elif get_option_value(world, player, "shuffle_EPs") == 1: # Individual EPs
adjustment_linesets_in_order.append(["Disabled Locations:"] + get_ep_obelisks()[1:])
else: # Obelisk Sides
yaml_disabled_eps = []
for yaml_disabled_location in self.YAML_DISABLED_LOCATIONS:
if yaml_disabled_location not in StaticWitnessLogic.CHECKS_BY_NAME:
continue
loc_obj = StaticWitnessLogic.CHECKS_BY_NAME[yaml_disabled_location]
if loc_obj["panelType"] != "EP":
continue
yaml_disabled_eps.append(loc_obj["checkHex"])
adjustment_linesets_in_order.append(["Precompleted Locations:"] + yaml_disabled_eps)
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)
def make_dependency_reduced_checklist(self):
"""
Turns dependent check set into semi-independent check set
"""
for check_hex in self.DEPENDENT_REQUIREMENTS_BY_HEX.keys():
indep_requirement = self.reduce_req_within_region(check_hex)
self.REQUIREMENTS_BY_HEX[check_hex] = indep_requirement
for item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI:
if item not in self.THEORETICAL_ITEMS:
corresponding_multi = StaticWitnessLogic.ITEMS_TO_PROGRESSIVE[item]
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.add(corresponding_multi)
multi_list = StaticWitnessLogic.PROGRESSIVE_TO_ITEMS[StaticWitnessLogic.ITEMS_TO_PROGRESSIVE[item]]
multi_list = [item for item in multi_list if item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI]
self.MULTI_AMOUNTS[item] = multi_list.index(item) + 1
self.MULTI_LISTS[corresponding_multi] = multi_list
else:
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.add(item)
def make_event_item_pair(self, panel):
"""
Makes a pair of an event panel and its event item
"""
action = " Opened" if StaticWitnessLogic.CHECKS_BY_HEX[panel]["panelType"] == "Door" else " Solved"
name = StaticWitnessLogic.CHECKS_BY_HEX[panel]["checkName"] + action
if panel not in self.EVENT_ITEM_NAMES:
if StaticWitnessLogic.CHECKS_BY_HEX[panel]["panelType"] == "EP":
obelisk = StaticWitnessLogic.CHECKS_BY_HEX[StaticWitnessLogic.EP_TO_OBELISK_SIDE[panel]]["checkName"]
self.EVENT_ITEM_NAMES[panel] = obelisk + " - " + StaticWitnessLogic.CHECKS_BY_HEX[panel]["checkName"]
else:
warning("Panel \"" + name + "\" does not have an associated event name.")
self.EVENT_ITEM_NAMES[panel] = name + " Event"
pair = (name, self.EVENT_ITEM_NAMES[panel])
return pair
def make_event_panel_lists(self):
"""
Special event panel data structures
"""
self.ALWAYS_EVENT_NAMES_BY_HEX[self.VICTORY_LOCATION] = "Victory"
for region_name, connections in self.CONNECTIONS_BY_REGION_NAME.items():
for connection in connections:
for panel_req in connection[1]:
for panel in panel_req:
if panel == "TrueOneWay":
continue
if self.REFERENCE_LOGIC.CHECKS_BY_HEX[panel]["region"]["name"] != region_name:
self.EVENT_PANELS_FROM_REGIONS.add(panel)
self.EVENT_PANELS.update(self.EVENT_PANELS_FROM_PANELS)
self.EVENT_PANELS.update(self.EVENT_PANELS_FROM_REGIONS)
for always_hex, always_item in self.ALWAYS_EVENT_NAMES_BY_HEX.items():
self.ALWAYS_EVENT_HEX_CODES.add(always_hex)
self.EVENT_PANELS.add(always_hex)
self.EVENT_ITEM_NAMES[always_hex] = always_item
for panel in self.EVENT_PANELS:
pair = self.make_event_item_pair(panel)
self.EVENT_ITEM_PAIRS[pair[0]] = pair[1]
def __init__(self, world: MultiWorld, player: int, disabled_locations: Set[str], start_inv: Dict[str, int]):
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.THEORETICAL_ITEMS = set()
self.THEORETICAL_ITEMS_NO_MULTI = set()
self.MULTI_AMOUNTS = dict()
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()
self.STARTING_INVENTORY = set()
self.DIFFICULTY = get_option_value(world, player, "puzzle_randomization")
if self.DIFFICULTY == 0:
self.REFERENCE_LOGIC = StaticWitnessLogic.sigma_normal
elif self.DIFFICULTY == 1:
self.REFERENCE_LOGIC = StaticWitnessLogic.sigma_expert
elif self.DIFFICULTY == 2:
self.REFERENCE_LOGIC = StaticWitnessLogic.vanilla
self.CONNECTIONS_BY_REGION_NAME = copy.copy(self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME)
self.DEPENDENT_REQUIREMENTS_BY_HEX = copy.copy(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_PANELS = set()
self.EVENT_ITEM_PAIRS = dict()
self.ALWAYS_EVENT_HEX_CODES = set()
self.COMPLETELY_DISABLED_CHECKS = set()
self.PRECOMPLETED_LOCATIONS = set()
self.ADDED_CHECKS = set()
self.VICTORY_LOCATION = "0x0356B"
self.EVENT_ITEM_NAMES = {
"0x09D9B": "Monastery Shutters Open",
"0x193A6": "Monastery Laser Panel Activates",
"0x00037": "Monastery Branch Panels Activate",
"0x0A079": "Access to Bunker Laser",
"0x0A3B5": "Door to Tutorial Discard Opens",
"0x00139": "Keep Hedges 1 Knowledge",
"0x019DC": "Keep Hedges 2 Knowledge",
"0x019E7": "Keep Hedges 3 Knowledge",
"0x01A0F": "Keep Hedges 4 Knowledge",
"0x033EA": "Pressure Plates 1 Knowledge",
"0x01BE9": "Pressure Plates 2 Knowledge",
"0x01CD3": "Pressure Plates 3 Knowledge",
"0x01D3F": "Pressure Plates 4 Knowledge",
"0x09F7F": "Mountain Access",
"0x0367C": "Quarry Laser Stoneworks Requirement Met",
"0x009A1": "Swamp Between Bridges Far 1 Activates",
"0x00006": "Swamp Cyan Water Drains",
"0x00990": "Swamp Between Bridges Near Row 1 Activates",
"0x0A8DC": "Intro 6 Activates",
"0x0000A": "Swamp Beyond Rotating Bridge 1 Access",
"0x09E86": "Mountain Floor 2 Blue Bridge Access",
"0x09ED8": "Mountain Floor 2 Yellow Bridge Access",
"0x0A3D0": "Quarry Laser Boathouse Requirement Met",
"0x00596": "Swamp Red Water Drains",
"0x00E3A": "Swamp Purple Water Drains",
"0x0343A": "Door to Symmetry Island Powers On",
"0xFFF00": "Mountain Bottom Floor Discard Turns On",
"0x17CA6": "All Boat Panels Turn On",
"0x17CDF": "All Boat Panels Turn On",
"0x09DB8": "All Boat Panels Turn On",
"0x17C95": "All Boat Panels Turn On",
"0x0A054": "Couch EP solvable",
"0x03BB0": "Town Church Lattice Vision From Outside",
"0x28AC1": "Town Wooden Rooftop Turns On",
"0x28A69": "Town Tower 1st Door Opens",
"0x28ACC": "Town Tower 2nd Door Opens",
"0x28AD9": "Town Tower 3rd Door Opens",
"0x28B39": "Town Tower 4th Door Opens",
"0x03675": "Quarry Stoneworks Ramp Activation From Above",
"0x03679": "Quarry Stoneworks Lift Lowering While Standing On It",
"0x2FAF6": "Tutorial Gate Secret Solution Knowledge",
"0x079DF": "Town Tall Hexagonal Turns On",
"0x17DA2": "Right Orange Bridge Fully Extended",
"0x19B24": "Shadows Intro Patterns Visible",
"0x2700B": "Open Door to Treehouse Laser House",
"0x00055": "Orchard Apple Trees 4 Turns On",
"0x17DDB": "Left Orange Bridge Fully Extended",
"0x03535": "Shipwreck Video Pattern Knowledge",
"0x03542": "Mountain Video Pattern Knowledge",
"0x0339E": "Desert Video Pattern Knowledge",
"0x03481": "Tutorial Video Pattern Knowledge",
"0x03702": "Jungle Video Pattern Knowledge",
"0x0356B": "Challenge Video Pattern Knowledge",
"0x0A15F": "Desert Laser Panel Shutters Open (1)",
"0x012D7": "Desert Laser Panel Shutters Open (2)",
"0x03613": "Treehouse Orange Bridge 13 Turns On",
"0x17DEC": "Treehouse Laser House Access Requirement",
"0x03C08": "Town Church Entry Opens",
"0x17D02": "Windmill Blades Spinning",
"0x0A0C9": "Cargo Box EP completable",
"0x09E39": "Pink Light Bridge Extended",
"0x17CC4": "Rails EP available",
"0x2896A": "Bridge Underside EP available",
"0x00064": "First Tunnel EP visible",
"0x03553": "Tutorial Video EPs availble",
"0x17C79": "Bunker Door EP available",
"0x275FF": "Stoneworks Light EPs available",
"0x17E2B": "Remaining Purple Sand EPs available",
"0x03852": "Ramp EPs requirement",
"0x334D8": "RGB panels & EPs solvable",
"0x03750": "Left Garden EP available",
"0x03C0C": "RGB Flowers EP requirement",
"0x01CD5": "Pressure Plates 3 EP requirement",
"0x3865F": "Ramp EPs access requirement",
}
self.ALWAYS_EVENT_NAMES_BY_HEX = {
"0x00509": "Symmetry Laser Activation",
"0x012FB": "Desert Laser Activation",
"0x09F98": "Desert Laser Redirection",
"0x01539": "Quarry Laser Activation",
"0x181B3": "Shadows Laser Activation",
"0x014BB": "Keep Laser Activation",
"0x17C65": "Monastery Laser Activation",
"0x032F9": "Town Laser Activation",
"0x00274": "Jungle Laser Activation",
"0x0C2B2": "Bunker Laser Activation",
"0x00BF6": "Swamp Laser Activation",
"0x028A4": "Treehouse Laser Activation",
"0x09F7F": "Mountaintop Trap Door Turns On",
"0x17C34": "Mountain Access",
}
self.make_options_adjustments(world, player)
self.make_dependency_reduced_checklist()
self.make_event_panel_lists()