From 3220b3aa18bac0ddff8f82ed4e4dd8d2fa671f93 Mon Sep 17 00:00:00 2001 From: Fenhl Date: Sat, 22 Nov 2025 10:58:14 +0000 Subject: [PATCH] Important Check hints don't prevent Sometimes hints from generating --- Hints.py | 162 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 82 insertions(+), 80 deletions(-) diff --git a/Hints.py b/Hints.py index d03be879be..accb222f48 100644 --- a/Hints.py +++ b/Hints.py @@ -1,4 +1,5 @@ from __future__ import annotations +import copy import itertools import json import logging @@ -8,7 +9,7 @@ import urllib.request from collections import OrderedDict, defaultdict from collections.abc import Callable, Iterable -from enum import Enum +from enum import Enum, auto from typing import TYPE_CHECKING, Optional from urllib.error import URLError, HTTPError @@ -544,7 +545,13 @@ def text(self, clearer_hints: bool, preposition: bool = False, use_2nd_person: b return text -def get_woth_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintReturn: +class CheckedKind(Enum): + IMPORTANT_CHECK = auto() + ALWAYS = auto() + OTHER = auto() + + +def get_woth_hint(spoiler: Spoiler, world: World, checked: dict[HintArea | str, set[CheckedKind]]) -> HintReturn: locations = spoiler.required_locations[world.id] locations = list(filter(lambda location: location.name not in checked @@ -559,7 +566,7 @@ def get_woth_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintRetu return None location = random.choice(locations) - checked.add(location.name) + mark_checked(checked, location.name) hint_area = HintArea.at(location) if hint_area.is_dungeon: @@ -569,20 +576,6 @@ def get_woth_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintRetu return GossipText('%s is on the way of the hero.' % location_text, ['Light Blue'], [location.name], [location.item.name]), [location] -def get_checked_areas(world: World, checked: set[str]) -> set[HintArea | str]: - def get_area_from_name(check: str) -> HintArea | str: - try: - location = world.get_location(check) - except Exception: - return check - # Don't consider dungeons as already hinted from the reward hint on the Temple of Time altar - if location.type == 'Boss' and world.settings.shuffle_dungeon_rewards in ('vanilla', 'reward'): - return None - return HintArea.at(location) - - return set(get_area_from_name(check) for check in checked) - - def get_goal_category(spoiler: Spoiler, world: World, goal_categories: dict[str, GoalCategory]) -> GoalCategory: cat_sizes = [] cat_names = [] @@ -618,7 +611,7 @@ def get_goal_category(spoiler: Spoiler, world: World, goal_categories: dict[str, return goal_category -def get_goal_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintReturn: +def get_goal_hint(spoiler: Spoiler, world: World, checked: dict[HintArea | str, set[CheckedKind]]) -> HintReturn: goal_category = get_goal_category(spoiler, world, world.goal_categories) # check if no goals were generated (and thus no categories available) @@ -670,7 +663,7 @@ def get_goal_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintRetu location, goal_list = random.choice(list(required_location_reverse_map.items())) goal, world_id = random.choice(goal_list) - checked.add(location.name) + mark_checked(checked, location.name) # Make sure this wasn't the last hintable location for other goals. # If so, set weights to zero. This is important for one-hint-per-goal. @@ -708,18 +701,29 @@ def get_goal_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintRetu return GossipText('%s is on %s %s.' % (location_text, player_text, goal_text), ['Light Blue', goal.color], [location.name], [location.item.name]), [location] -def get_barren_hint(spoiler: Spoiler, world: World, checked: set[str], all_checked: set[str]) -> HintReturn: +def get_barren_hint(spoiler: Spoiler, world: World, checked: dict[HintArea | str, set[CheckedKind]]) -> HintReturn: if not hasattr(world, 'get_barren_hint_prev'): world.get_barren_hint_prev = RegionRestriction.NONE - checked_areas = get_checked_areas(world, checked) + def get_area_from_name(check: HintArea | str) -> HintArea | str: + try: + location = world.get_location(check) + except Exception: + return check + # Don't consider dungeons as already hinted from the reward hint on the Temple of Time altar + if location.type == 'Boss' and world.settings.shuffle_dungeon_rewards in ('vanilla', 'reward'): + return None + return HintArea.at(location) + + checked_areas = {get_area_from_name(check) for check, kinds in checked.items() if any(kind is not CheckedKind.ALWAYS for kind in kinds)} + areas = list(filter(lambda area: area not in checked_areas and str(area) not in world.hint_type_overrides['barren'] and not world.precompleted_dungeons.get(area.dungeon_name, False) and not (world.barren_dungeon >= world.hint_dist_user['dungeons_barren_limit'] and world.empty_areas[area]['dungeon']) and any( - location.name not in all_checked + location.name not in checked and location.name not in world.hint_exclusions and location.name not in hint_exclusions(world) and HintArea.at(location) == area @@ -763,17 +767,17 @@ def get_barren_hint(spoiler: Spoiler, world: World, checked: set[str], all_check if world.empty_areas[area]['dungeon']: world.barren_dungeon += 1 - checked.add(area) + mark_checked(checked, area) return GossipText("plundering %s is a foolish choice." % area.text(world.settings.clearer_hints), ['Pink']), None -def is_checked(locations: Iterable[Location], checked: set[HintArea | str]) -> bool: +def is_checked(locations: Iterable[Location], checked: dict[HintArea | str, set[CheckedKind]], *, ignore: Iterable[CheckedKind] = ()) -> bool: for location in locations: - if location.name in checked: + if any(kind not in ignore for kind in checked.get(location.name, set())): return True hint_area = HintArea.at(location) - if hint_area in checked: + if any(kind not in ignore for kind in checked.get(hint_area, set())): return True if location.world.precompleted_dungeons.get(hint_area.dungeon_name, False): # don't hint locations in precompleted dungeons @@ -781,7 +785,11 @@ def is_checked(locations: Iterable[Location], checked: set[HintArea | str]) -> b return False -def get_good_item_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintReturn: +def mark_checked(checked: dict[HintArea | str, set[CheckedKind]], check: HintArea | str, kind: CheckedKind = CheckedKind.OTHER) -> None: + checked.setdefault(check, set()).add(kind) + + +def get_good_item_hint(spoiler: Spoiler, world: World, checked: dict[HintArea | str, set[CheckedKind]]) -> HintReturn: locations = list(filter(lambda location: not is_checked([location], checked) and ((location.item.majoritem @@ -797,7 +805,7 @@ def get_good_item_hint(spoiler: Spoiler, world: World, checked: set[str]) -> Hin return None location = random.choice(locations) - checked.add(location.name) + mark_checked(checked, location.name) item_text = get_hint(get_item_generic_name(location.item), world.settings.clearer_hints).text hint_area = HintArea.at(location) @@ -809,7 +817,7 @@ def get_good_item_hint(spoiler: Spoiler, world: World, checked: set[str]) -> Hin return GossipText('#%s# can be found %s.' % (item_text, location_text), ['Green', 'Red'], [location.name], [location.item.name]), [location] -def get_specific_item_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintReturn: +def get_specific_item_hint(spoiler: Spoiler, world: World, checked: dict[HintArea | str, set[CheckedKind]]) -> HintReturn: if len(world.named_item_pool) == 0: logger = logging.getLogger('') logger.info("Named item hint requested, but pool is empty.") @@ -850,7 +858,7 @@ def get_specific_item_hint(spoiler: Spoiler, world: World, checked: set[str]) -> return None location = random.choice(locations) - checked.add(location.name) + mark_checked(checked, location.name) item_text = get_hint(get_item_generic_name(location.item), world.settings.clearer_hints).text hint_area = HintArea.at(location) @@ -931,7 +939,7 @@ def get_specific_item_hint(spoiler: Spoiler, world: World, checked: set[str]) -> return None location = random.choice(locations) - checked.add(location.name) + mark_checked(checked, location.name) item_text = get_hint(get_item_generic_name(location.item), world.settings.clearer_hints).text hint_area = HintArea.at(location) @@ -946,7 +954,7 @@ def get_specific_item_hint(spoiler: Spoiler, world: World, checked: set[str]) -> return GossipText('#%s# can be found %s.' % (item_text, location_text), ['Green', 'Red'], [location.name], [location.item.name]), [location] -def get_random_location_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintReturn: +def get_random_location_hint(spoiler: Spoiler, world: World, checked: dict[HintArea | str, set[CheckedKind]]) -> HintReturn: locations = list(filter(lambda location: not is_checked([location], checked) and location.item.type not in ('Drop', 'Event', 'Shop') @@ -960,7 +968,7 @@ def get_random_location_hint(spoiler: Spoiler, world: World, checked: set[str]) return None location = random.choice(locations) - checked.add(location.name) + mark_checked(checked, location.name) item_text = get_hint(get_item_generic_name(location.item), world.settings.clearer_hints).text hint_area = HintArea.at(location) @@ -972,9 +980,9 @@ def get_random_location_hint(spoiler: Spoiler, world: World, checked: set[str]) return GossipText('#%s# can be found %s.' % (item_text, location_text), ['Green', 'Red'], [location.name], [location.item.name]), [location] -def get_specific_hint(spoiler: Spoiler, world: World, checked: set[str], hint_type: str) -> HintReturn: +def get_specific_hint(spoiler: Spoiler, world: World, checked: dict[HintArea | str, set[CheckedKind]], hint_type: str) -> HintReturn: hint_group = get_hint_group(hint_type, world) - hint_group = list(filter(lambda hint: not is_checked([world.get_location(hint.name)], checked), hint_group)) + hint_group = list(filter(lambda hint: not is_checked([world.get_location(hint.name)], checked, ignore={CheckedKind.IMPORTANT_CHECK}), hint_group)) if not hint_group: return None @@ -983,7 +991,7 @@ def get_specific_hint(spoiler: Spoiler, world: World, checked: set[str], hint_ty if world.hint_dist_user['upgrade_hints'] in ('on', 'limited'): upgrade_list = get_upgrade_hint_list(world, [hint.name]) upgrade_list = list(filter( - lambda upgrade: not is_checked([world.get_location(location) for location in get_multi(upgrade.name).locations], checked), + lambda upgrade: not is_checked([world.get_location(location) for location in get_multi(upgrade.name).locations], checked, ignore={CheckedKind.IMPORTANT_CHECK}), upgrade_list, )) @@ -1001,7 +1009,7 @@ def get_specific_hint(spoiler: Spoiler, world: World, checked: set[str], hint_ty return get_specific_multi_hint(spoiler, world, checked, hint) location = world.get_location(hint.name) - checked.add(location.name) + mark_checked(checked, location.name) if location.name in world.hint_text_overrides: location_text = world.hint_text_overrides[location.name] @@ -1014,26 +1022,26 @@ def get_specific_hint(spoiler: Spoiler, world: World, checked: set[str], hint_ty return GossipText('%s #%s#.' % (location_text, item_text), ['Red', 'Green'], [location.name], [location.item.name]), [location] -def get_sometimes_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintReturn: +def get_sometimes_hint(spoiler: Spoiler, world: World, checked: dict[HintArea | str, set[CheckedKind]]) -> HintReturn: return get_specific_hint(spoiler, world, checked, 'sometimes') -def get_song_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintReturn: +def get_song_hint(spoiler: Spoiler, world: World, checked: dict[HintArea | str, set[CheckedKind]]) -> HintReturn: return get_specific_hint(spoiler, world, checked, 'song') -def get_overworld_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintReturn: +def get_overworld_hint(spoiler: Spoiler, world: World, checked: dict[HintArea | str, set[CheckedKind]]) -> HintReturn: return get_specific_hint(spoiler, world, checked, 'overworld') -def get_dungeon_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintReturn: +def get_dungeon_hint(spoiler: Spoiler, world: World, checked: dict[HintArea | str, set[CheckedKind]]) -> HintReturn: return get_specific_hint(spoiler, world, checked, 'dungeon') -def get_random_multi_hint(spoiler: Spoiler, world: World, checked: set[str], hint_type: str) -> HintReturn: +def get_random_multi_hint(spoiler: Spoiler, world: World, checked: dict[HintArea | str, set[CheckedKind]], hint_type: str) -> HintReturn: hint_group = get_hint_group(hint_type, world) multi_hints = list(filter( - lambda hint: not is_checked([world.get_location(location) for location in get_multi(hint.name).locations], checked), + lambda hint: not is_checked([world.get_location(location) for location in get_multi(hint.name).locations], checked, ignore={CheckedKind.IMPORTANT_CHECK}), hint_group, )) @@ -1047,7 +1055,7 @@ def get_random_multi_hint(spoiler: Spoiler, world: World, checked: set[str], hin upgrade_list = get_upgrade_hint_list(world, multi.locations) upgrade_list = list(filter( - lambda upgrade: not is_checked([world.get_location(location) for location in get_multi(upgrade.name).locations], checked), + lambda upgrade: not is_checked([world.get_location(location) for location in get_multi(upgrade.name).locations], checked, ignore={CheckedKind.IMPORTANT_CHECK}), upgrade_list, )) @@ -1062,12 +1070,12 @@ def get_random_multi_hint(spoiler: Spoiler, world: World, checked: set[str], hin return get_specific_multi_hint(spoiler, world, checked, hint) -def get_specific_multi_hint(spoiler: Spoiler, world: World, checked: set[str], hint: Hint) -> HintReturn: +def get_specific_multi_hint(spoiler: Spoiler, world: World, checked: dict[HintArea | str, set[CheckedKind]], hint: Hint) -> HintReturn: multi = get_multi(hint.name) locations = [world.get_location(location) for location in multi.locations] for location in locations: - checked.add(location.name) + mark_checked(checked, location.name) if hint.name in world.hint_text_overrides: multi_text = world.hint_text_overrides[hint.name] @@ -1091,11 +1099,11 @@ def get_specific_multi_hint(spoiler: Spoiler, world: World, checked: set[str], h return GossipText(gossip_string % tuple(text_segments), colors, [location.name for location in locations], [item.name for item in items]), locations -def get_dual_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintReturn: +def get_dual_hint(spoiler: Spoiler, world: World, checked: dict[HintArea | str, set[CheckedKind]]) -> HintReturn: return get_random_multi_hint(spoiler, world, checked, 'dual') -def get_entrance_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintReturn: +def get_entrance_hint(spoiler: Spoiler, world: World, checked: dict[HintArea | str, set[CheckedKind]]) -> HintReturn: if not world.entrance_shuffle: return None @@ -1112,7 +1120,7 @@ def get_entrance_hint(spoiler: Spoiler, world: World, checked: set[str]) -> Hint entrance_hint = random.choice(valid_entrance_hints) entrance = world.get_entrance(entrance_hint.name) - checked.add(entrance.name) + mark_checked(checked, entrance.name) entrance_text = entrance_hint.text @@ -1131,19 +1139,19 @@ def get_entrance_hint(spoiler: Spoiler, world: World, checked: set[str]) -> Hint return GossipText('%s %s.' % (entrance_text, region_text), ['Green', 'Light Blue']), None -def get_junk_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintReturn: +def get_junk_hint(spoiler: Spoiler, world: World, checked: dict[HintArea | str, set[CheckedKind]]) -> HintReturn: hints = get_hint_group('junk', world) hints = list(filter(lambda hint: hint.name not in checked, hints)) if not hints: return None hint = random.choice(hints) - checked.add(hint.name) + mark_checked(checked, hint.name) return GossipText(hint.text, prefix=''), None -def get_important_check_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintReturn: +def get_important_check_hint(spoiler: Spoiler, world: World, checked: dict[HintArea | str, set[CheckedKind]]) -> HintReturn: top_level_locations = [] empty_dungeons = [dungeon for dungeon in world.precompleted_dungeons if world.precompleted_dungeons[dungeon]] for location in world.get_filled_locations(): @@ -1182,7 +1190,7 @@ def get_important_check_hint(spoiler: Spoiler, world: World, checked: set[str]) or world.settings.shuffle_ganon_bosskey == 'dungeons' or world.settings.shuffle_ganon_bosskey == 'tokens'))): item_count = item_count + 1 - checked.add(hint_area) + mark_checked(checked, hint_area, CheckedKind.IMPORTANT_CHECK) if item_count == 0: numcolor = 'Red' @@ -1316,28 +1324,28 @@ def build_gossip_hints(spoiler: Spoiler, worlds: list[World]) -> None: if can_reach_hint(worlds, compass_location, location): item_world = location.world if item_world.id not in checked_locations: - checked_locations[item_world.id] = set() - checked_locations[item_world.id].add(location.name) + checked_locations[item_world.id] = {} + mark_checked(checked_locations[item_world.id], location.name) break else: if 'altar' in world.settings.misc_hints and can_reach_hint(worlds, world.get_location('ToT Child Altar Hint' if location.item.info.stone else 'ToT Adult Altar Hint'), location): item_world = location.world if item_world.id not in checked_locations: - checked_locations[item_world.id] = set() - checked_locations[item_world.id].add(location.name) + checked_locations[item_world.id] = {} + mark_checked(checked_locations[item_world.id], location.name) for hint_type, location in world.misc_hint_item_locations.items(): if hint_type in world.settings.misc_hints and can_reach_hint(worlds, world.get_location(misc_item_hint_table[hint_type]['hint_location']), location): item_world = location.world if item_world.id not in checked_locations: - checked_locations[item_world.id] = set() - checked_locations[item_world.id].add(location.name) + checked_locations[item_world.id] = {} + mark_checked(checked_locations[item_world.id], location.name) for hint_type in world.misc_hint_location_items.keys(): location = world.get_location(misc_location_hint_table[hint_type]['item_location']) if hint_type in world.settings.misc_hints and can_reach_hint(worlds, world.get_location(misc_location_hint_table[hint_type]['hint_location']), location): item_world = location.world if item_world.id not in checked_locations: - checked_locations[item_world.id] = set() - checked_locations[item_world.id].add(location.name) + checked_locations[item_world.id] = {} + mark_checked(checked_locations[item_world.id], location.name) # Build all the hints. for world in worlds: @@ -1346,7 +1354,7 @@ def build_gossip_hints(spoiler: Spoiler, worlds: list[World]) -> None: # builds out general hints based on location and whether an item is required or not -def build_world_gossip_hints(spoiler: Spoiler, world: World, checked_locations: Optional[set[str]] = None) -> None: +def build_world_gossip_hints(spoiler: Spoiler, world: World, checked_locations: Optional[dict[HintArea | str, set[CheckedKind]]] = None) -> None: world.barren_dungeon = 0 world.woth_dungeon = 0 @@ -1357,8 +1365,7 @@ def build_world_gossip_hints(spoiler: Spoiler, world: World, checked_locations: and search.state_list[world.id].guarantee_hint()) if checked_locations is None: - checked_locations = set() - checked_always_locations = set() + checked_locations = {} stone_ids = list(gossipLocations.keys()) @@ -1477,8 +1484,8 @@ def build_world_gossip_hints(spoiler: Spoiler, world: World, checked_locations: multi = get_multi(hint.name) first_location = world.get_location(multi.locations[0]) second_location = world.get_location(multi.locations[1]) - checked_always_locations.add(first_location.name) - checked_always_locations.add(second_location.name) + mark_checked(checked_locations, first_location.name, CheckedKind.ALWAYS) + mark_checked(checked_locations, second_location.name, CheckedKind.ALWAYS) always_named_item(world, [first_location, second_location]) @@ -1496,12 +1503,12 @@ def build_world_gossip_hints(spoiler: Spoiler, world: World, checked_locations: # Add required location hints, only if hint copies > 0 if hint_dist['always'][1] > 0: always_locations = list(filter( - lambda hint: not is_checked([world.get_location(hint.name)], checked_always_locations), + lambda hint: not is_checked([world.get_location(hint.name)], checked_locations, ignore={CheckedKind.ALWAYS, CheckedKind.OTHER}), get_hint_group('always', world), )) for hint in always_locations: location = world.get_location(hint.name) - checked_always_locations.add(hint.name) + mark_checked(checked_locations, hint.name, CheckedKind.ALWAYS) always_named_item(world, [location]) @@ -1523,7 +1530,7 @@ def build_world_gossip_hints(spoiler: Spoiler, world: World, checked_locations: connected_region = entrance.connected_region if entrance.shuffled and (connected_region.dungeon or any(hint.name == connected_region.name for hint in get_hint_group('region', world))): - checked_always_locations.add(entrance.name) + mark_checked(checked_locations, entrance.name, CheckedKind.ALWAYS) entrance_text = entrance_hint.text if '#' not in entrance_text: @@ -1568,17 +1575,17 @@ def build_world_gossip_hints(spoiler: Spoiler, world: World, checked_locations: # Prevent conflict between Ganondorf Light Arrows hint and required named item hints. # Assumes that a "wasted" hint is desired since Light Arrows have to be added # explicitly to the list for named item hints. - filtered_checked = set(checked_locations | checked_always_locations) - for location in (checked_locations | checked_always_locations): + filtered_checked = copy.copy(checked_locations) + for location in checked_locations: try: if world.get_location(location).item.name == 'Light Arrows': - filtered_checked.remove(location) + del filtered_checked[location] except KeyError: - pass # checked_always_locations can also contain entrances from entrance_always hints, ignore those here + pass # checked_locations can also contain entrances from entrance_always hints, ignore those here for i in range(0, len(world.named_item_pool)): hint = get_specific_item_hint(spoiler, world, filtered_checked) if hint: - checked_locations.update(filtered_checked - checked_always_locations) + checked_locations.update(filtered_checked) gossip_text, location = hint place_ok = add_hint(spoiler, world, stone_groups, gossip_text, hint_dist['named-item'][1], location, hint_type='named-item') if not place_ok: @@ -1634,12 +1641,7 @@ def build_world_gossip_hints(spoiler: Spoiler, world: World, checked_locations: except IndexError: raise Exception('Not enough valid hints to fill gossip stone locations.') - all_checked_locations = checked_locations | checked_always_locations - if hint_type == 'barren': - hint = hint_func[hint_type](spoiler, world, checked_locations, all_checked_locations) - else: - hint = hint_func[hint_type](spoiler, world, all_checked_locations) - checked_locations.update(all_checked_locations - checked_always_locations) + hint = hint_func[hint_type](spoiler, world, checked_locations) if hint is None: index = hint_types.index(hint_type)