The Witness: Event System & Item Classification System revamp (#2652)

Two things have been happening.

**Incorrect Events**
Spoiler logs containing events that just straight up have an incorrect name and shouldn't be there. E.g. "Symmetry Island Yellow 3 solved - Monastery Laser Activation" when playing Laser Shuffle where this event should not exist, because Laser Activations are governed by the Laser items.

Now to be clear - There are no logic issues with it. The event will be in the spoiler log, but it won't actually be used in the way that its name suggests.
Basically, every panel in the game has exactly one event name. If the panel is referenced by another panel, it will reference the event instead. So, the Symmetry Laser Panel location will reference Symmetry Island Yellow 3, and an event is created for Symmetry Island Yellow 3. The only problem is the **name**: The canonical name for the event is related to "Symmetry Island Yellow 3" is "Monastery Laser Activation", because that's another thing that panel does sometimes.

From now on, event names are tied to both the panel referencing and the panel being referenced. Only once the referincing panel actually references the dependent panel (during the dependency reduction process in generate_early), is the event actually created.

This also removes some spoiler log clutter where unused events were just in the location list.

**Item classifications**
When playing shuffle_doors, there are a lot of doors in the game that are logically useless depending on settings. When that happens, they should get downgraded from progression to useful. The previous system for this was jank and terrible. Now there is a better system for it, and many items have been added to it. :)
This commit is contained in:
NewSoupVi 2024-02-13 22:47:19 +01:00 committed by GitHub
parent 3ca3417172
commit 74e79bff06
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 108 additions and 53 deletions

View File

@ -112,30 +112,12 @@ class WitnessPlayerItems:
or name in logic.PROG_ITEMS_ACTUALLY_IN_THE_GAME or name in logic.PROG_ITEMS_ACTUALLY_IN_THE_GAME
} }
# Adjust item classifications based on game settings. # Downgrade door items
eps_shuffled = self._world.options.shuffle_EPs
come_to_you = self._world.options.elevators_come_to_you
difficulty = self._world.options.puzzle_randomization
for item_name, item_data in self.item_data.items(): for item_name, item_data in self.item_data.items():
if not eps_shuffled and item_name in {"Monastery Garden Entry (Door)", if not isinstance(item_data.definition, DoorItemDefinition):
"Monastery Shortcuts", continue
"Quarry Boathouse Hook Control (Panel)",
"Windmill Turn Control (Panel)"}: if all(not self._logic.solvability_guaranteed(e_hex) for e_hex in item_data.definition.panel_id_hexes):
# Downgrade doors that only gate progress in EP shuffle.
item_data.classification = ItemClassification.useful
elif not come_to_you and not eps_shuffled and item_name in {"Quarry Elevator Control (Panel)",
"Swamp Long Bridge (Panel)"}:
# These Bridges/Elevators are not logical access because they may leave you stuck.
item_data.classification = ItemClassification.useful
elif item_name in {"River Monastery Garden Shortcut (Door)",
"Monastery Laser Shortcut (Door)",
"Orchard Second Gate (Door)",
"Jungle Bamboo Laser Shortcut (Door)",
"Caves Elevator Controls (Panel)"}:
# Downgrade doors that don't gate progress.
item_data.classification = ItemClassification.useful
elif item_name == "Keep Pressure Plates 2 Exit (Door)" and not (difficulty == "none" and eps_shuffled):
# PP2EP requires the door in vanilla puzzles, otherwise it's unnecessary
item_data.classification = ItemClassification.useful item_data.classification = ItemClassification.useful
# Build the mandatory item list. # Build the mandatory item list.

View File

@ -543,7 +543,7 @@ class WitnessPlayerLocations:
) )
event_locations = { event_locations = {
p for p in player_logic.EVENT_PANELS p for p in player_logic.USED_EVENT_NAMES_BY_HEX
} }
self.EVENT_LOCATION_TABLE = { self.EVENT_LOCATION_TABLE = {

View File

@ -101,8 +101,11 @@ class WitnessPlayerLogic:
for option_entity in option: for option_entity in option:
dep_obj = self.REFERENCE_LOGIC.ENTITIES_BY_HEX.get(option_entity) dep_obj = self.REFERENCE_LOGIC.ENTITIES_BY_HEX.get(option_entity)
if option_entity in self.EVENT_NAMES_BY_HEX: if option_entity in self.ALWAYS_EVENT_NAMES_BY_HEX:
new_items = frozenset({frozenset([option_entity])}) new_items = frozenset({frozenset([option_entity])})
elif (panel_hex, option_entity) in self.CONDITIONAL_EVENTS:
new_items = frozenset({frozenset([option_entity])})
self.USED_EVENT_NAMES_BY_HEX[option_entity] = self.CONDITIONAL_EVENTS[(panel_hex, option_entity)]
elif option_entity in {"7 Lasers", "11 Lasers", "7 Lasers + Redirect", "11 Lasers + Redirect", elif option_entity in {"7 Lasers", "11 Lasers", "7 Lasers + Redirect", "11 Lasers + Redirect",
"PP2 Weirdness", "Theater to Tunnels"}: "PP2 Weirdness", "Theater to Tunnels"}:
new_items = frozenset({frozenset([option_entity])}) new_items = frozenset({frozenset([option_entity])})
@ -170,14 +173,11 @@ class WitnessPlayerLogic:
if adj_type == "Event Items": if adj_type == "Event Items":
line_split = line.split(" - ") line_split = line.split(" - ")
new_event_name = line_split[0] new_event_name = line_split[0]
hex_set = line_split[1].split(",") entity_hex = line_split[1]
dependent_hex_set = line_split[2].split(",")
for entity, event_name in self.EVENT_NAMES_BY_HEX.items(): for dependent_hex in dependent_hex_set:
if event_name == new_event_name: self.CONDITIONAL_EVENTS[(entity_hex, dependent_hex)] = new_event_name
self.DONT_MAKE_EVENTS.add(entity)
for hex_code in hex_set:
self.EVENT_NAMES_BY_HEX[hex_code] = new_event_name
return return
@ -437,7 +437,7 @@ class WitnessPlayerLogic:
obelisk = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[self.REFERENCE_LOGIC.EP_TO_OBELISK_SIDE[ep_hex]] obelisk = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[self.REFERENCE_LOGIC.EP_TO_OBELISK_SIDE[ep_hex]]
obelisk_name = obelisk["checkName"] obelisk_name = obelisk["checkName"]
ep_name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[ep_hex]["checkName"] ep_name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[ep_hex]["checkName"]
self.EVENT_NAMES_BY_HEX[ep_hex] = f"{obelisk_name} - {ep_name}" self.ALWAYS_EVENT_NAMES_BY_HEX[ep_hex] = f"{obelisk_name} - {ep_name}"
else: else:
adjustment_linesets_in_order.append(["Disabled Locations:"] + get_ep_obelisks()[1:]) adjustment_linesets_in_order.append(["Disabled Locations:"] + get_ep_obelisks()[1:])
@ -505,7 +505,8 @@ class WitnessPlayerLogic:
for option in connection[1]: for option in connection[1]:
individual_entity_requirements = [] individual_entity_requirements = []
for entity in option: for entity in option:
if entity in self.EVENT_NAMES_BY_HEX or entity not in self.REFERENCE_LOGIC.ENTITIES_BY_HEX: 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})})) individual_entity_requirements.append(frozenset({frozenset({entity})}))
else: else:
entity_req = self.reduce_req_within_region(entity) entity_req = self.reduce_req_within_region(entity)
@ -522,6 +523,72 @@ class WitnessPlayerLogic:
self.CONNECTIONS_BY_REGION_NAME[region] = new_connections self.CONNECTIONS_BY_REGION_NAME[region] = new_connections
def solvability_guaranteed(self, entity_hex: str):
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"):
"""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): def make_event_item_pair(self, panel: str):
""" """
Makes a pair of an event panel and its event item Makes a pair of an event panel and its event item
@ -529,21 +596,23 @@ class WitnessPlayerLogic:
action = " Opened" if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[panel]["entityType"] == "Door" else " Solved" action = " Opened" if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[panel]["entityType"] == "Door" else " Solved"
name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[panel]["checkName"] + action name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[panel]["checkName"] + action
if panel not in self.EVENT_NAMES_BY_HEX: if panel not in self.USED_EVENT_NAMES_BY_HEX:
warning("Panel \"" + name + "\" does not have an associated event name.") warning("Panel \"" + name + "\" does not have an associated event name.")
self.EVENT_NAMES_BY_HEX[panel] = name + " Event" self.USED_EVENT_NAMES_BY_HEX[panel] = name + " Event"
pair = (name, self.EVENT_NAMES_BY_HEX[panel]) pair = (name, self.USED_EVENT_NAMES_BY_HEX[panel])
return pair return pair
def make_event_panel_lists(self): def make_event_panel_lists(self):
self.EVENT_NAMES_BY_HEX[self.VICTORY_LOCATION] = "Victory" self.ALWAYS_EVENT_NAMES_BY_HEX[self.VICTORY_LOCATION] = "Victory"
for event_hex, event_name in self.EVENT_NAMES_BY_HEX.items(): self.USED_EVENT_NAMES_BY_HEX.update(self.ALWAYS_EVENT_NAMES_BY_HEX)
if event_hex in self.COMPLETELY_DISABLED_ENTITIES or event_hex in self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES:
continue
self.EVENT_PANELS.add(event_hex)
for panel in self.EVENT_PANELS: 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) pair = self.make_event_item_pair(panel)
self.EVENT_ITEM_PAIRS[pair[0]] = pair[1] self.EVENT_ITEM_PAIRS[pair[0]] = pair[1]
@ -556,6 +625,8 @@ class WitnessPlayerLogic:
self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES = set() self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES = set()
self.ENTITIES_WITHOUT_ENSURED_SOLVABILITY = set()
self.THEORETICAL_ITEMS = set() self.THEORETICAL_ITEMS = set()
self.THEORETICAL_ITEMS_NO_MULTI = set() self.THEORETICAL_ITEMS_NO_MULTI = set()
self.MULTI_AMOUNTS = defaultdict(lambda: 1) self.MULTI_AMOUNTS = defaultdict(lambda: 1)
@ -580,16 +651,14 @@ class WitnessPlayerLogic:
# Determining which panels need to be events is a difficult process. # 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. # At the end, we will have EVENT_ITEM_PAIRS for all the necessary ones.
self.EVENT_PANELS = set()
self.EVENT_ITEM_PAIRS = dict() self.EVENT_ITEM_PAIRS = dict()
self.DONT_MAKE_EVENTS = set()
self.COMPLETELY_DISABLED_ENTITIES = set() self.COMPLETELY_DISABLED_ENTITIES = set()
self.PRECOMPLETED_LOCATIONS = set() self.PRECOMPLETED_LOCATIONS = set()
self.EXCLUDED_LOCATIONS = set() self.EXCLUDED_LOCATIONS = set()
self.ADDED_CHECKS = set() self.ADDED_CHECKS = set()
self.VICTORY_LOCATION = "0x0356B" self.VICTORY_LOCATION = "0x0356B"
self.EVENT_NAMES_BY_HEX = { self.ALWAYS_EVENT_NAMES_BY_HEX = {
"0x00509": "+1 Laser (Symmetry Laser)", "0x00509": "+1 Laser (Symmetry Laser)",
"0x012FB": "+1 Laser (Desert Laser)", "0x012FB": "+1 Laser (Desert Laser)",
"0x09F98": "Desert Laser Redirection", "0x09F98": "Desert Laser Redirection",
@ -602,10 +671,14 @@ class WitnessPlayerLogic:
"0x0C2B2": "+1 Laser (Bunker Laser)", "0x0C2B2": "+1 Laser (Bunker Laser)",
"0x00BF6": "+1 Laser (Swamp Laser)", "0x00BF6": "+1 Laser (Swamp Laser)",
"0x028A4": "+1 Laser (Treehouse Laser)", "0x028A4": "+1 Laser (Treehouse Laser)",
"0x09F7F": "Mountain Entry", "0x17C34": "Mountain Entry",
"0xFFF00": "Bottom Floor Discard Turns On", "0xFFF00": "Bottom Floor Discard Turns On",
} }
self.USED_EVENT_NAMES_BY_HEX = {}
self.CONDITIONAL_EVENTS = {}
self.make_options_adjustments(world) self.make_options_adjustments(world)
self.determine_unrequired_entities(world)
self.make_dependency_reduced_checklist() self.make_dependency_reduced_checklist()
self.make_event_panel_lists() self.make_event_panel_lists()

View File

@ -170,7 +170,7 @@ def _has_item(item: str, world: "WitnessWorld", player: int,
return lambda state: _can_do_expert_pp2(state, world) return lambda state: _can_do_expert_pp2(state, world)
elif item == "Theater to Tunnels": elif item == "Theater to Tunnels":
return lambda state: _can_do_theater_to_tunnels(state, world) return lambda state: _can_do_theater_to_tunnels(state, world)
if item in player_logic.EVENT_PANELS: if item in player_logic.USED_EVENT_NAMES_BY_HEX:
return _can_solve_panel(item, world, player, player_logic, locat) return _can_solve_panel(item, world, player, player_logic, locat)
prog_item = StaticWitnessLogic.get_parent_progressive_item(item) prog_item = StaticWitnessLogic.get_parent_progressive_item(item)

View File

@ -1,9 +1,9 @@
Event Items: Event Items:
Monastery Laser Activation - 0x00A5B,0x17CE7,0x17FA9 Monastery Laser Activation - 0x17C65 - 0x00A5B,0x17CE7,0x17FA9
Bunker Laser Activation - 0x00061,0x17D01,0x17C42 Bunker Laser Activation - 0x0C2B2 - 0x00061,0x17D01,0x17C42
Shadows Laser Activation - 0x00021,0x17D28,0x17C71 Shadows Laser Activation - 0x181B3 - 0x00021,0x17D28,0x17C71
Town Tower 4th Door Opens - 0x17CFB,0x3C12B,0x17CF7 Town Tower 4th Door Opens - 0x2779A - 0x17CFB,0x3C12B,0x17CF7
Jungle Popup Wall Lifts - 0x17FA0,0x17D27,0x17F9B,0x17CAB Jungle Popup Wall Lifts - 0x1475B - 0x17FA0,0x17D27,0x17F9B,0x17CAB
Requirement Changes: Requirement Changes:
0x17C65 - 0x00A5B | 0x17CE7 | 0x17FA9 0x17C65 - 0x00A5B | 0x17CE7 | 0x17FA9