The Witness: Hint distribution changes, added locations, misc fixes (#1785)
Changes:
* Hints should feel a lot less same-y now ("Priority hints" are no longer always hints in disguise)
* Keep Hedge Mazes 1-3 and Pressure Plates 1-3 are added as locations in all settings
* Desert Final Room Hexagonal & Desert Final Room Bent 3 are added as locations
* Entries in exclude_locations that are referring to panels are now sent through slot data. This means they can be pre-skipped on the client side.
Fixes:
* Logic error in the Stoneworks that led to more restrictive seeds than necessary
* Logic error for Theater Flowers EP that led to more restrictive seeds than necessary
* Fixed crash in plando when "item" is a dict with weights
* Spoiler log locations were in random order per region, now they are consistent
			
			
This commit is contained in:
		
							parent
							
								
									afe9e12ef4
								
							
						
					
					
						commit
						845502ad39
					
				|  | @ -264,7 +264,7 @@ Quarry Stoneworks Upper Floor (Quarry Stoneworks) - Quarry Stoneworks Middle Flo | |||
| 158141 - 0x014E9 (Upper Row 8) - 0x03686 - Colored Squares & Eraser | ||||
| 158142 - 0x03677 (Stair Control) - True - Colored Squares & Eraser | ||||
| Door - 0x0368A (Stairs) - 0x03677 | ||||
| 158143 - 0x3C125 (Control Room Left) - 0x0367C - Black/White Squares & Dots & Eraser | ||||
| 158143 - 0x3C125 (Control Room Left) - 0x014E9 - Black/White Squares & Dots & Eraser | ||||
| 158144 - 0x0367C (Control Room Right) - 0x014E9 - Colored Squares & Dots & Eraser | ||||
| 159411 - 0x0069D (Ramp EP) - 0x03676 & 0x275FF - True | ||||
| 159413 - 0x00614 (Lift EP) - 0x275FF & 0x03675 - True | ||||
|  |  | |||
|  | @ -264,7 +264,7 @@ Quarry Stoneworks Upper Floor (Quarry Stoneworks) - Quarry Stoneworks Middle Flo | |||
| 158141 - 0x014E9 (Upper Row 8) - 0x03686 - Squares & Colored Squares & Eraser & Stars & Stars + Same Colored Symbol | ||||
| 158142 - 0x03677 (Stair Control) - True - Squares & Colored Squares & Eraser | ||||
| Door - 0x0368A (Stairs) - 0x03677 | ||||
| 158143 - 0x3C125 (Control Room Left) - 0x0367C - Squares & Black/White Squares & Dots & Full Dots & Eraser | ||||
| 158143 - 0x3C125 (Control Room Left) - 0x014E9 - Squares & Black/White Squares & Dots & Full Dots & Eraser | ||||
| 158144 - 0x0367C (Control Room Right) - 0x014E9 - Squares & Colored Squares & Triangles & Eraser & Stars & Stars + Same Colored Symbol | ||||
| 159411 - 0x0069D (Ramp EP) - 0x03676 & 0x275FF - True | ||||
| 159413 - 0x00614 (Lift EP) - 0x275FF & 0x03675 - True | ||||
|  |  | |||
|  | @ -15,7 +15,7 @@ Tutorial (Tutorial) - Outside Tutorial - 0x03629: | |||
| 158006 - 0x0A3B2 (Back Right) - True - True | ||||
| 158007 - 0x03629 (Gate Open) - 0x002C2 & 0x0A3B5 & 0x0A3B2 - True | ||||
| 158008 - 0x03505 (Gate Close) - 0x2FAF6 & 0x03629 - True | ||||
| 158009 - 0x0C335 (Pillar) - True - Triangles - True | ||||
| 158009 - 0x0C335 (Pillar) - True - Triangles | ||||
| 158010 - 0x0C373 (Patio Floor) - 0x0C335 - Dots | ||||
| 159512 - 0x33530 (Cloud EP) - True - True | ||||
| 159513 - 0x33600 (Patio Flowers EP) - 0x0C373 - True | ||||
|  | @ -264,7 +264,7 @@ Quarry Stoneworks Upper Floor (Quarry Stoneworks) - Quarry Stoneworks Middle Flo | |||
| 158141 - 0x014E9 (Upper Row 8) - 0x03686 - Colored Squares & Eraser | ||||
| 158142 - 0x03677 (Stair Control) - True - Colored Squares & Eraser | ||||
| Door - 0x0368A (Stairs) - 0x03677 | ||||
| 158143 - 0x3C125 (Control Room Left) - 0x0367C - Black/White Squares & Dots & Eraser | ||||
| 158143 - 0x3C125 (Control Room Left) - 0x014E9 - Black/White Squares & Dots & Eraser | ||||
| 158144 - 0x0367C (Control Room Right) - 0x014E9 - Colored Squares & Dots & Eraser | ||||
| 159411 - 0x0069D (Ramp EP) - 0x03676 & 0x275FF - True | ||||
| 159413 - 0x00614 (Lift EP) - 0x275FF & 0x03675 - True | ||||
|  |  | |||
|  | @ -62,11 +62,11 @@ class WitnessWorld(World): | |||
|             'item_id_to_door_hexes': self.static_items.ITEM_ID_TO_DOOR_HEX_ALL, | ||||
|             'door_hexes_in_the_pool': self.items.DOORS, | ||||
|             'symbols_not_in_the_game': self.items.SYMBOLS_NOT_IN_THE_GAME, | ||||
|             'disabled_panels': self.player_logic.COMPLETELY_DISABLED_CHECKS, | ||||
|             'disabled_panels': list(self.player_logic.COMPLETELY_DISABLED_CHECKS), | ||||
|             'log_ids_to_hints': self.log_ids_to_hints, | ||||
|             'progressive_item_lists': self.items.MULTI_LISTS_BY_CODE, | ||||
|             'obelisk_side_id_to_EPs': self.static_logic.OBELISK_SIDE_ID_TO_EP_HEXES, | ||||
|             'precompleted_puzzles': {int(h, 16) for h in self.player_logic.PRECOMPLETED_LOCATIONS}, | ||||
|             'precompleted_puzzles': [int(h, 16) for h in self.player_logic.EXCLUDED_LOCATIONS], | ||||
|             'entity_to_name': self.static_logic.ENTITY_ID_TO_NAME, | ||||
|         } | ||||
| 
 | ||||
|  | @ -143,14 +143,19 @@ class WitnessWorld(World): | |||
| 
 | ||||
|         for v in self.multiworld.plando_items[self.player]: | ||||
|             if v.get("from_pool", True): | ||||
|                 plandoed_items.update({self.items_by_name[i] for i in v.get("items", dict()).keys() | ||||
|                                        if i in self.items_by_name}) | ||||
|                 if "item" in v and v["item"] in self.items_by_name: | ||||
|                     plandoed_items.add(self.items_by_name[v["item"]]) | ||||
|                 for item_key in {"item", "items"}: | ||||
|                     if item_key in v: | ||||
|                         if type(v[item_key]) is str: | ||||
|                             plandoed_items.add(v[item_key]) | ||||
|                         elif type(v[item_key]) is dict: | ||||
|                             plandoed_items.update(item for item, weight in v[item_key].items() if weight) | ||||
|                         else: | ||||
|                             # Other type of iterable | ||||
|                             plandoed_items.update(v[item_key]) | ||||
| 
 | ||||
|         for symbol in self.items.GOOD_ITEMS: | ||||
|             item = self.items_by_name[symbol] | ||||
|             if item in pool and item not in plandoed_items: | ||||
|             if item in pool and symbol not in plandoed_items: | ||||
|                 # for now, any item that is mentioned in any plando option, even if it's a list of items, is ineligible. | ||||
|                 # Hopefully, in the future, plando gets resolved before create_items. | ||||
|                 # I could also partially resolve lists myself, but this could introduce errors if not done carefully. | ||||
|  | @ -255,7 +260,7 @@ class WitnessWorld(World): | |||
| 
 | ||||
|             self.multiworld.per_slot_randoms[self.player].shuffle(audio_logs) | ||||
| 
 | ||||
|             duplicates = len(audio_logs) // hint_amount | ||||
|             duplicates = min(3, len(audio_logs) // hint_amount) | ||||
| 
 | ||||
|             for _ in range(0, hint_amount): | ||||
|                 hint = generated_hints.pop(0) | ||||
|  |  | |||
|  | @ -96,31 +96,30 @@ joke_hints = [ | |||
| 
 | ||||
| 
 | ||||
| def get_always_hint_items(multiworld: MultiWorld, player: int): | ||||
|     priority = [ | ||||
|     always = [ | ||||
|         "Boat", | ||||
|         "Mountain Bottom Floor Final Room Entry (Door)", | ||||
|         "Caves Mountain Shortcut (Door)", | ||||
|         "Caves Swamp Shortcut (Door)", | ||||
|         "Caves Exits to Main Island", | ||||
|         "Progressive Dots", | ||||
|     ] | ||||
| 
 | ||||
|     difficulty = get_option_value(multiworld, player, "puzzle_randomization") | ||||
|     discards = is_option_enabled(multiworld, player, "shuffle_discarded_panels") | ||||
|     wincon = get_option_value(multiworld, player, "victory_condition") | ||||
| 
 | ||||
|     if discards: | ||||
|         if difficulty == 1: | ||||
|             priority.append("Arrows") | ||||
|             always.append("Arrows") | ||||
|         else: | ||||
|             priority.append("Triangles") | ||||
|             always.append("Triangles") | ||||
| 
 | ||||
|     return priority | ||||
|     if wincon == 0: | ||||
|         always.append("Mountain Bottom Floor Final Room Entry (Door)") | ||||
| 
 | ||||
|     return always | ||||
| 
 | ||||
| 
 | ||||
| def get_always_hint_locations(multiworld: MultiWorld, player: int): | ||||
|     return { | ||||
|         "Swamp Purple Underwater", | ||||
|         "Shipwreck Vault Box", | ||||
|         "Challenge Vault Box", | ||||
|         "Mountain Bottom Floor Discard", | ||||
|         "Theater Eclipse EP", | ||||
|  | @ -131,6 +130,8 @@ def get_always_hint_locations(multiworld: MultiWorld, player: int): | |||
| 
 | ||||
| def get_priority_hint_items(multiworld: MultiWorld, player: int): | ||||
|     priority = { | ||||
|         "Caves Mountain Shortcut (Door)", | ||||
|         "Caves Swamp Shortcut (Door)", | ||||
|         "Negative Shapers", | ||||
|         "Sound Dots", | ||||
|         "Colored Dots", | ||||
|  | @ -157,16 +158,18 @@ def get_priority_hint_items(multiworld: MultiWorld, player: int): | |||
|         if get_option_value(multiworld, player, "doors") >= 2: | ||||
|             priority.add("Desert Laser") | ||||
|             lasers.remove("Desert Laser") | ||||
|             priority.update(multiworld.per_slot_randoms[player].sample(lasers, 2)) | ||||
|             priority.update(multiworld.per_slot_randoms[player].sample(lasers, 5)) | ||||
| 
 | ||||
|         else: | ||||
|             priority.update(multiworld.per_slot_randoms[player].sample(lasers, 3)) | ||||
|             priority.update(multiworld.per_slot_randoms[player].sample(lasers, 6)) | ||||
| 
 | ||||
|     return priority | ||||
| 
 | ||||
| 
 | ||||
| def get_priority_hint_locations(multiworld: MultiWorld, player: int): | ||||
|     return { | ||||
|         "Swamp Purple Underwater", | ||||
|         "Shipwreck Vault Box", | ||||
|         "Town RGB Room Left", | ||||
|         "Town RGB Room Right", | ||||
|         "Treehouse Green Bridge 7", | ||||
|  | @ -264,7 +267,8 @@ def make_hints(multiworld: MultiWorld, player: int, hint_amount: int): | |||
| 
 | ||||
|     multiworld.per_slot_randoms[player].shuffle(hints)  # shuffle always hint order in case of low hint amount | ||||
| 
 | ||||
|     next_random_hint_is_item = multiworld.per_slot_randoms[player].randint(0, 2) | ||||
|     remaining_hints = hint_amount - len(hints) | ||||
|     priority_hint_amount = int(max(0.0, min(len(priority_hint_pairs) / 2, remaining_hints / 2))) | ||||
| 
 | ||||
|     prog_items_in_this_world = sorted(list(prog_items_in_this_world)) | ||||
|     locations_in_this_world = sorted(list(loc_in_this_world)) | ||||
|  | @ -272,18 +276,21 @@ def make_hints(multiworld: MultiWorld, player: int, hint_amount: int): | |||
|     multiworld.per_slot_randoms[player].shuffle(prog_items_in_this_world) | ||||
|     multiworld.per_slot_randoms[player].shuffle(locations_in_this_world) | ||||
| 
 | ||||
|     while len(hints) < hint_amount: | ||||
|         if priority_hint_pairs: | ||||
|             loc = multiworld.per_slot_randoms[player].choice(list(priority_hint_pairs.keys())) | ||||
|             item = priority_hint_pairs[loc] | ||||
|             del priority_hint_pairs[loc] | ||||
|     priority_hint_list = list(priority_hint_pairs.items()) | ||||
|     multiworld.per_slot_randoms[player].shuffle(priority_hint_list) | ||||
|     for _ in range(0, priority_hint_amount): | ||||
|         next_priority_hint = priority_hint_list.pop() | ||||
|         loc = next_priority_hint[0] | ||||
|         item = next_priority_hint[1] | ||||
| 
 | ||||
|         if item[1]: | ||||
|             hints.append((f"{item[0]} can be found at {loc}.", item[2])) | ||||
|         else: | ||||
|             hints.append((f"{loc} contains {item[0]}.", item[2])) | ||||
|             continue | ||||
| 
 | ||||
|     next_random_hint_is_item = multiworld.per_slot_randoms[player].randint(0, 2) | ||||
| 
 | ||||
|     while len(hints) < hint_amount: | ||||
|         if next_random_hint_is_item: | ||||
|             if not prog_items_in_this_world: | ||||
|                 next_random_hint_is_item = not next_random_hint_is_item | ||||
|  |  | |||
|  | @ -143,11 +143,11 @@ class WitnessPlayerItems: | |||
|         self.PROGRESSION_TABLE = dict() | ||||
| 
 | ||||
|         self.ITEM_ID_TO_DOOR_HEX = dict() | ||||
|         self.DOORS = set() | ||||
|         self.DOORS = list() | ||||
| 
 | ||||
|         self.PROG_ITEM_AMOUNTS = defaultdict(lambda: 1) | ||||
| 
 | ||||
|         self.SYMBOLS_NOT_IN_THE_GAME = set() | ||||
|         self.SYMBOLS_NOT_IN_THE_GAME = list() | ||||
| 
 | ||||
|         self.EXTRA_AMOUNTS = { | ||||
|             "Functioning Brain": 1, | ||||
|  | @ -162,7 +162,7 @@ class WitnessPlayerItems: | |||
|             if item[0] not in logic.PROG_ITEMS_ACTUALLY_IN_THE_GAME: | ||||
|                 del self.ITEM_TABLE[item[0]] | ||||
|                 if item in StaticWitnessLogic.ALL_SYMBOL_ITEMS: | ||||
|                     self.SYMBOLS_NOT_IN_THE_GAME.add(StaticWitnessItems.ALL_ITEM_TABLE[item[0]].code) | ||||
|                     self.SYMBOLS_NOT_IN_THE_GAME.append(StaticWitnessItems.ALL_ITEM_TABLE[item[0]].code) | ||||
|             else: | ||||
|                 if item[0] in StaticWitnessLogic.PROGRESSIVE_TO_ITEMS: | ||||
|                     self.PROG_ITEM_AMOUNTS[item[0]] = len(logic.MULTI_LISTS[item[0]]) | ||||
|  | @ -178,7 +178,7 @@ class WitnessPlayerItems: | |||
|         for entity_hex, items in logic.DOOR_ITEMS_BY_ID.items(): | ||||
|             entity_hex_int = int(entity_hex, 16) | ||||
| 
 | ||||
|             self.DOORS.add(entity_hex_int) | ||||
|             self.DOORS.append(entity_hex_int) | ||||
| 
 | ||||
|             for item in items: | ||||
|                 item_id = StaticWitnessItems.ALL_ITEM_TABLE[item].code | ||||
|  |  | |||
|  | @ -43,6 +43,8 @@ class StaticWitnessLocations: | |||
|         "Desert Light Room 3", | ||||
|         "Desert Pond Room 5", | ||||
|         "Desert Flood Room 6", | ||||
|         "Desert Final Hexagonal", | ||||
|         "Desert Final Bent 3", | ||||
|         "Desert Laser Panel", | ||||
| 
 | ||||
|         "Quarry Stoneworks Lower Row 6", | ||||
|  | @ -61,7 +63,13 @@ class StaticWitnessLocations: | |||
|         "Shadows Near 5", | ||||
|         "Shadows Laser Panel", | ||||
| 
 | ||||
|         "Keep Hedge Maze 1", | ||||
|         "Keep Hedge Maze 2", | ||||
|         "Keep Hedge Maze 3", | ||||
|         "Keep Hedge Maze 4", | ||||
|         "Keep Pressure Plates 1", | ||||
|         "Keep Pressure Plates 2", | ||||
|         "Keep Pressure Plates 3", | ||||
|         "Keep Pressure Plates 4", | ||||
|         "Keep Discard", | ||||
|         "Keep Laser Panel Hedges", | ||||
|  |  | |||
|  | @ -296,7 +296,6 @@ class WitnessPlayerLogic: | |||
|         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: | ||||
|  | @ -305,11 +304,12 @@ class WitnessPlayerLogic: | |||
| 
 | ||||
|             loc_obj = StaticWitnessLogic.CHECKS_BY_NAME[yaml_disabled_location] | ||||
| 
 | ||||
|                 if loc_obj["panelType"] != "EP": | ||||
|                     continue | ||||
| 
 | ||||
|             if loc_obj["panelType"] == "EP" and get_option_value(world, player, "shuffle_EPs") == 2: | ||||
|                 yaml_disabled_eps.append(loc_obj["checkHex"]) | ||||
| 
 | ||||
|             if loc_obj["panelType"] in {"EP", "General"}: | ||||
|                 self.EXCLUDED_LOCATIONS.add(loc_obj["checkHex"]) | ||||
| 
 | ||||
|         adjustment_linesets_in_order.append(["Precompleted Locations:"] + yaml_disabled_eps) | ||||
| 
 | ||||
|         for adjustment_lineset in adjustment_linesets_in_order: | ||||
|  | @ -430,6 +430,7 @@ class WitnessPlayerLogic: | |||
|         self.ALWAYS_EVENT_HEX_CODES = set() | ||||
|         self.COMPLETELY_DISABLED_CHECKS = set() | ||||
|         self.PRECOMPLETED_LOCATIONS = set() | ||||
|         self.EXCLUDED_LOCATIONS = set() | ||||
|         self.ADDED_CHECKS = set() | ||||
|         self.VICTORY_LOCATION = "0x0356B" | ||||
|         self.EVENT_ITEM_NAMES = { | ||||
|  |  | |||
|  | @ -141,14 +141,19 @@ class WitnessLogic(LogicMixin): | |||
|                         and self.can_reach("Windmill Interior to Theater", "Entrance", player) | ||||
|                     ) | ||||
| 
 | ||||
|                     exit_to_town = self.can_reach("Theater to Town", "Entrance", player) | ||||
|                     entrance_to_town = ( | ||||
|                     theater_from_town = ( | ||||
|                         self.can_reach("Town to Windmill Interior", "Entrance", player) | ||||
|                         and self.can_reach("Windmill Interior to Theater", "Entrance", player) | ||||
|                         or self.can_reach("Theater to Town", "Entrance", player) | ||||
|                     ) | ||||
|                     tunnels_to_town = self.can_reach("Tunnels to Town", "Entrance", player) | ||||
| 
 | ||||
|                     if not (direct_access or (exit_to_town or entrance_to_town) and tunnels_to_town): | ||||
|                     tunnels_from_town = ( | ||||
|                         self.can_reach("Tunnels to Windmill Interior", "Entrance", player) | ||||
|                         and self.can_reach("Town to Windmill Interior", "Entrance", player) | ||||
|                         or self.can_reach("Tunnels to Town", "Entrance", player) | ||||
|                     ) | ||||
| 
 | ||||
|                     if not (direct_access or theater_from_town and tunnels_from_town): | ||||
|                         valid_option = False | ||||
|                         break | ||||
| 
 | ||||
|  |  | |||
|  | @ -84,8 +84,6 @@ Disabled Locations: | |||
| 0x0070F (Second Row 2) | ||||
| 0x0087D (Second Row 3) | ||||
| 0x002C7 (Second Row 4) | ||||
| 0x15ADD (River Outside Vault) | ||||
| 0x03702 (River Vault Box) | ||||
| 0x17CAA (Monastery Shortcut Panel) | ||||
| 0x0C2A4 (Bunker Entry) | ||||
| 0x17C79 (Tinted Glass Door) | ||||
|  |  | |||
|  | @ -57,7 +57,7 @@ class StaticWitnessLogicObj: | |||
|                         "panels": parse_lambda(required_panel_lambda) | ||||
|                     } | ||||
| 
 | ||||
|                     current_region["panels"].add(check_hex) | ||||
|                     current_region["panels"].append(check_hex) | ||||
|                     continue | ||||
| 
 | ||||
|                 required_item_lambda = line_split.pop(0) | ||||
|  | @ -117,7 +117,7 @@ class StaticWitnessLogicObj: | |||
|                 self.CHECKS_BY_NAME[self.CHECKS_BY_HEX[check_hex]["checkName"]] = self.CHECKS_BY_HEX[check_hex] | ||||
|                 self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX[check_hex] = requirement | ||||
| 
 | ||||
|                 current_region["panels"].add(check_hex) | ||||
|                 current_region["panels"].append(check_hex) | ||||
| 
 | ||||
|     def __init__(self, file_path="WitnessLogic.txt"): | ||||
|         # All regions with a list of panels in them and the connections to other regions, before logic adjustments | ||||
|  |  | |||
|  | @ -106,7 +106,7 @@ def define_new_region(region_string): | |||
|     region_obj = { | ||||
|         "name": region_name, | ||||
|         "shortName": region_name_simple, | ||||
|         "panels": set() | ||||
|         "panels": list() | ||||
|     } | ||||
|     return region_obj, options | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue