Add disambiguate() helper for examine/loot container ambiguity resolution

This commit is contained in:
KansaiGaijin
2026-07-07 12:24:58 +12:00
parent 7935983707
commit 2706f7b25b

125
main.py
View File

@@ -7,6 +7,37 @@ from function_list import startGame, parse, error_message, get_instructions, wai
from scene import * 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: class Engine:
""" """
The main game engine class to manage game state, current scene, and player. The main game engine class to manage game state, current scene, and player.
@@ -116,9 +147,47 @@ class Engine:
if not found_in_scene: if not found_in_scene:
# Check lootable containers in the current scene # Check lootable containers in the current scene
found_in_loot = False pairs = list(self.current_scene.lootable_items.items())
match = disambiguate(object_name, pairs, key_fn=lambda kv: kv[0])
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 container_name, container in self.current_scene.lootable_items.items():
if object_name.lower() in container_name.lower(): 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))
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))
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.")
def _examine_container(self, container_name, container):
"""Print details of a container and its contents."""
print(f"\n--- {container.name.title()} ---") print(f"\n--- {container.name.title()} ---")
print(container.description) print(container.description)
@@ -152,29 +221,6 @@ class Engine:
print(f" - {item_obj.name} (x{count})") print(f" - {item_obj.name} (x{count})")
else: else:
print("It appears to be empty.") print("It appears to be empty.")
found_in_loot = True
break
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
if not found_in_loot:
print(f"You don't see or have '{object_name}' to examine.")
def take_item(self, item_name): def take_item(self, item_name):
""" """
@@ -212,26 +258,32 @@ class Engine:
def loot_container(self, container_name): def loot_container(self, container_name):
"""Allows the player to loot items from a specified container.""" """Allows the player to loot items from a specified container."""
for current_name, container in list(self.current_scene.lootable_items.items()): pairs = list(self.current_scene.lootable_items.items())
if current_name.lower() == container_name.lower(): 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
current_name, container = match
if container.is_locked: if container.is_locked:
print(f"The {container_name} is locked.") print(f"The {current_name} is locked.")
return return
if container.is_trapped and not container.trap_disarmed: if container.is_trapped and not container.trap_disarmed:
if container.trap_detected: if container.trap_detected:
print(f"The {container_name} is trapped. You'll need to disable the trap first.") print(f"The {current_name} is trapped. You'll need to disable the trap first.")
return return
else: else:
print(f"You open the {container_name}. A trap is triggered!") print(f"You open the {current_name}. A trap is triggered!")
# TODO: trap damage/effects # TODO: trap damage/effects
return return
if not container.contents: if not container.contents:
print(f"The {container_name} is empty.") print(f"The {current_name} is empty.")
return return
print(f"You look inside the {container_name}. You see:") print(f"You look inside the {current_name}. You see:")
loot_list = list(container.contents.items()) loot_list = list(container.contents.items())
while True: while True:
@@ -251,12 +303,12 @@ class Engine:
self.player.inventory.add_item(item_obj, count) self.player.inventory.add_item(item_obj, count)
print(f"- Took {count} {item_obj.name}") print(f"- Took {count} {item_obj.name}")
container.remove_item(item_obj, count) container.remove_item(item_obj, count)
print(f"The {container_name} is now empty.") print(f"The {current_name} is now empty.")
if not container.has_items(): if not container.has_items():
del self.current_scene.lootable_items[current_name] del self.current_scene.lootable_items[current_name]
return return
elif choice == "leave" or (choice.isdigit() and int(choice) == leave_option): elif choice == "leave" or (choice.isdigit() and int(choice) == leave_option):
print(f"You leave the {container_name} untouched.") print(f"You leave the {current_name} untouched.")
return return
else: else:
try: try:
@@ -284,7 +336,7 @@ class Engine:
loot_list = list(container.contents.items()) loot_list = list(container.contents.items())
if not container.contents: if not container.contents:
print(f"The {container_name} is now empty.") print(f"The {current_name} is now empty.")
if not container.has_items(): if not container.has_items():
del self.current_scene.lootable_items[current_name] del self.current_scene.lootable_items[current_name]
return return
@@ -294,9 +346,6 @@ class Engine:
print("Invalid selection. Please choose a valid item number, 'take all', or 'leave'.") print("Invalid selection. Please choose a valid item number, 'take all', or 'leave'.")
except ValueError: except ValueError:
print("Invalid selection. Please enter a number, 'take all', or 'leave'.") print("Invalid selection. Please enter a number, 'take all', or 'leave'.")
return
print(f"You don't see a '{container_name}' here to loot.")
# Initialize the game engine # Initialize the game engine