From 2706f7b25b80586a94e319fc89fe12605c9ba6d3 Mon Sep 17 00:00:00 2001 From: KansaiGaijin <83641841+KansaiGaijin@users.noreply.github.com> Date: Tue, 7 Jul 2026 12:24:58 +1200 Subject: [PATCH] Add disambiguate() helper for examine/loot container ambiguity resolution --- main.py | 315 ++++++++++++++++++++++++++++++++------------------------ 1 file changed, 182 insertions(+), 133 deletions(-) diff --git a/main.py b/main.py index 09a3ffb..db71862 100644 --- a/main.py +++ b/main.py @@ -7,6 +7,37 @@ from function_list import startGame, parse, error_message, get_instructions, wai from scene import * +def disambiguate(query, candidates, key_fn=str): + """ + Given a query string and a list of candidates, find candidates whose + key (extracted by key_fn) contains query (case-insensitive substring match). + Returns the single matched candidate, or prompts the user on ambiguity. + Returns None if no match or user backs out. + """ + query_lower = query.lower() + matches = [c for c in candidates if query_lower in key_fn(c).lower()] + + if len(matches) == 0: + return None + elif len(matches) == 1: + return matches[0] + + print("Which do you mean?") + for i, candidate in enumerate(matches, 1): + print(f" {i}) {key_fn(candidate).title()}") + print(" b) Back") + + while True: + choice = input(">> ").strip().lower() + if choice in ("b", "back"): + return None + if choice.isdigit(): + idx = int(choice) + if 1 <= idx <= len(matches): + return matches[idx - 1] + print("Invalid choice.") + + class Engine: """ The main game engine class to manage game state, current scene, and player. @@ -116,65 +147,80 @@ class Engine: if not found_in_scene: # Check lootable containers in the current scene - found_in_loot = False - for container_name, container in self.current_scene.lootable_items.items(): - if object_name.lower() in container_name.lower(): - print(f"\n--- {container.name.title()} ---") - print(container.description) + pairs = list(self.current_scene.lootable_items.items()) + match = disambiguate(object_name, pairs, key_fn=lambda kv: kv[0]) - int_mod = self.player.mods["Intelligence"] - noticed_something = False + if match: + container_name, container = match + self._examine_container(container_name, container) + else: + # Check items inside containers + candidates = [] + for container_name, container in self.current_scene.lootable_items.items(): + for item_obj, count in container.contents.items(): + if count > 0 and object_name.lower() in item_obj.name.lower(): + candidates.append((container_name, container, item_obj)) - if container.lock_dc: - roll = randint(1, 20) + int_mod - print(f"\nInvestigation check: 1d20{int_mod:+} = {roll} vs DC {container.lock_dc}") - if roll >= container.lock_dc: - print("It appears to be locked.") - noticed_something = True - elif not container.trap_dc: - print("You don't notice anything unusual about it.") + seen = set() + unique_candidates = [] + for cn, c, item in candidates: + key = item.name.lower() + if key not in seen: + seen.add(key) + unique_candidates.append((cn, c, item)) - if container.trap_dc: - roll = randint(1, 20) + int_mod - print(f"\nInvestigation check: 1d20{int_mod:+} = {roll} vs DC {container.trap_dc}") - if roll >= container.trap_dc: - print("You notice a trap mechanism on it.") - container.trap_detected = True - noticed_something = True - elif not container.lock_dc and not noticed_something: - print("You don't notice anything unusual about it.") + match = disambiguate(object_name, unique_candidates, key_fn=lambda t: t[2].name) + if match: + container_name, container, item_obj = match + print(f"\n--- {item_obj.name} ---") + print(item_obj.description) + if isinstance(item_obj, Weapon): + print(f"Damage: {item_obj.damage1H or item_obj.damage2H or item_obj.versatiledmg}") + print(f"Damage Type: {item_obj.dmgType.name.title()}") + elif isinstance(item_obj, Armor): + print(f"AC: {item_obj.ac}") + print(f"Grade: {item_obj.grade}") + print(f"Value: {item_obj.value if item_obj.value is not None else 'N/A'}") + print(f"Magical: {item_obj.magical}") + print(f"Attunement: {'Required' if item_obj.attunement else 'Not Required'}") + else: + print(f"You don't see or have '{object_name}' to examine.") - if not container.is_locked: - if container.contents: - print("\nInside, you see:") - for item_obj, count in container.contents.items(): - if count > 0: - print(f" - {item_obj.name} (x{count})") - else: - print("It appears to be empty.") - found_in_loot = True - break + def _examine_container(self, container_name, container): + """Print details of a container and its contents.""" + print(f"\n--- {container.name.title()} ---") + print(container.description) - for item_obj, count in container.contents.items(): - if object_name.lower() in item_obj.name.lower() and count > 0: - print(f"\n--- {item_obj.name} ---") - print(item_obj.description) - if isinstance(item_obj, Weapon): - print(f"Damage: {item_obj.damage1H or item_obj.damage2H or item_obj.versatiledmg}") - print(f"Damage Type: {item_obj.dmgType.name.title()}") - elif isinstance(item_obj, Armor): - print(f"AC: {item_obj.ac}") - print(f"Grade: {item_obj.grade}") - print(f"Value: {item_obj.value if item_obj.value is not None else 'N/A'}") - print(f"Magical: {item_obj.magical}") - print(f"Attunement: {'Required' if item_obj.attunement else 'Not Required'}") - found_in_loot = True - break - if found_in_loot: - break + int_mod = self.player.mods["Intelligence"] + noticed_something = False - if not found_in_loot: - print(f"You don't see or have '{object_name}' to examine.") + if container.lock_dc: + roll = randint(1, 20) + int_mod + print(f"\nInvestigation check: 1d20{int_mod:+} = {roll} vs DC {container.lock_dc}") + if roll >= container.lock_dc: + print("It appears to be locked.") + noticed_something = True + elif not container.trap_dc: + print("You don't notice anything unusual about it.") + + if container.trap_dc: + roll = randint(1, 20) + int_mod + print(f"\nInvestigation check: 1d20{int_mod:+} = {roll} vs DC {container.trap_dc}") + if roll >= container.trap_dc: + print("You notice a trap mechanism on it.") + container.trap_detected = True + noticed_something = True + elif not container.lock_dc and not noticed_something: + print("You don't notice anything unusual about it.") + + if not container.is_locked: + if container.contents: + print("\nInside, you see:") + for item_obj, count in container.contents.items(): + if count > 0: + print(f" - {item_obj.name} (x{count})") + else: + print("It appears to be empty.") def take_item(self, item_name): """ @@ -212,91 +258,94 @@ class Engine: def loot_container(self, container_name): """Allows the player to loot items from a specified container.""" - for current_name, container in list(self.current_scene.lootable_items.items()): - if current_name.lower() == container_name.lower(): - if container.is_locked: - print(f"The {container_name} is locked.") - return + pairs = list(self.current_scene.lootable_items.items()) + match = disambiguate(container_name, pairs, key_fn=lambda kv: kv[0]) + if not match: + print(f"You don't see a '{container_name}' here to loot.") + return - if container.is_trapped and not container.trap_disarmed: - if container.trap_detected: - print(f"The {container_name} is trapped. You'll need to disable the trap first.") - return - else: - print(f"You open the {container_name}. A trap is triggered!") - # TODO: trap damage/effects - return + current_name, container = match - if not container.contents: - print(f"The {container_name} is empty.") - return + if container.is_locked: + print(f"The {current_name} is locked.") + return - print(f"You look inside the {container_name}. You see:") - loot_list = list(container.contents.items()) - - while True: - for i, (item_obj, count) in enumerate(loot_list, 1): - print(f"{i}) {item_obj.name} (x{count})") - - take_all_option = len(loot_list) + 1 - leave_option = len(loot_list) + 2 - - print(f"{take_all_option}) Take all") - print(f"{leave_option}) Leave") - - choice = input("What would you like to take? (number or 'take all'/'leave')\n>> ").lower().strip() - - if choice == "take all" or (choice.isdigit() and int(choice) == take_all_option): - for item_obj, count in list(container.contents.items()): - self.player.inventory.add_item(item_obj, count) - print(f"- Took {count} {item_obj.name}") - container.remove_item(item_obj, count) - print(f"The {container_name} is now empty.") - if not container.has_items(): - del self.current_scene.lootable_items[current_name] - return - elif choice == "leave" or (choice.isdigit() and int(choice) == leave_option): - print(f"You leave the {container_name} untouched.") - return - else: - try: - selection_index = int(choice) - 1 - if 0 <= selection_index < len(loot_list): - item_obj, current_count = loot_list[selection_index] - take_count_input = input( - f"How many {item_obj.name} will you take? (enter 'all' or a number)\n>> ").lower().strip() - - if take_count_input == "all": - take_count = current_count - else: - try: - take_count = int(take_count_input) - except ValueError: - print("Invalid amount. Please enter 'all' or a number.") - continue - - if 0 < take_count <= current_count: - self.player.inventory.add_item(item_obj, take_count) - container.remove_item(item_obj, take_count) - print(f"You took {take_count} {item_obj.name}.") - - if item_obj not in container.contents: - loot_list = list(container.contents.items()) - - if not container.contents: - print(f"The {container_name} is now empty.") - if not container.has_items(): - del self.current_scene.lootable_items[current_name] - return - else: - print("Invalid amount or not enough items.") - else: - print("Invalid selection. Please choose a valid item number, 'take all', or 'leave'.") - except ValueError: - print("Invalid selection. Please enter a number, 'take all', or 'leave'.") + if container.is_trapped and not container.trap_disarmed: + if container.trap_detected: + print(f"The {current_name} is trapped. You'll need to disable the trap first.") + return + else: + print(f"You open the {current_name}. A trap is triggered!") + # TODO: trap damage/effects return - print(f"You don't see a '{container_name}' here to loot.") + if not container.contents: + print(f"The {current_name} is empty.") + return + + print(f"You look inside the {current_name}. You see:") + loot_list = list(container.contents.items()) + + while True: + for i, (item_obj, count) in enumerate(loot_list, 1): + print(f"{i}) {item_obj.name} (x{count})") + + take_all_option = len(loot_list) + 1 + leave_option = len(loot_list) + 2 + + print(f"{take_all_option}) Take all") + print(f"{leave_option}) Leave") + + choice = input("What would you like to take? (number or 'take all'/'leave')\n>> ").lower().strip() + + if choice == "take all" or (choice.isdigit() and int(choice) == take_all_option): + for item_obj, count in list(container.contents.items()): + self.player.inventory.add_item(item_obj, count) + print(f"- Took {count} {item_obj.name}") + container.remove_item(item_obj, count) + print(f"The {current_name} is now empty.") + if not container.has_items(): + del self.current_scene.lootable_items[current_name] + return + elif choice == "leave" or (choice.isdigit() and int(choice) == leave_option): + print(f"You leave the {current_name} untouched.") + return + else: + try: + selection_index = int(choice) - 1 + if 0 <= selection_index < len(loot_list): + item_obj, current_count = loot_list[selection_index] + take_count_input = input( + f"How many {item_obj.name} will you take? (enter 'all' or a number)\n>> ").lower().strip() + + if take_count_input == "all": + take_count = current_count + else: + try: + take_count = int(take_count_input) + except ValueError: + print("Invalid amount. Please enter 'all' or a number.") + continue + + if 0 < take_count <= current_count: + self.player.inventory.add_item(item_obj, take_count) + container.remove_item(item_obj, take_count) + print(f"You took {take_count} {item_obj.name}.") + + if item_obj not in container.contents: + loot_list = list(container.contents.items()) + + if not container.contents: + print(f"The {current_name} is now empty.") + if not container.has_items(): + del self.current_scene.lootable_items[current_name] + return + else: + print("Invalid amount or not enough items.") + else: + print("Invalid selection. Please choose a valid item number, 'take all', or 'leave'.") + except ValueError: + print("Invalid selection. Please enter a number, 'take all', or 'leave'.") # Initialize the game engine