Add disambiguate() helper for examine/loot container ambiguity resolution
This commit is contained in:
315
main.py
315
main.py
@@ -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,65 +147,80 @@ 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())
|
||||||
for container_name, container in self.current_scene.lootable_items.items():
|
match = disambiguate(object_name, pairs, key_fn=lambda kv: kv[0])
|
||||||
if object_name.lower() in container_name.lower():
|
|
||||||
print(f"\n--- {container.name.title()} ---")
|
|
||||||
print(container.description)
|
|
||||||
|
|
||||||
int_mod = self.player.mods["Intelligence"]
|
if match:
|
||||||
noticed_something = False
|
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:
|
seen = set()
|
||||||
roll = randint(1, 20) + int_mod
|
unique_candidates = []
|
||||||
print(f"\nInvestigation check: 1d20{int_mod:+} = {roll} vs DC {container.lock_dc}")
|
for cn, c, item in candidates:
|
||||||
if roll >= container.lock_dc:
|
key = item.name.lower()
|
||||||
print("It appears to be locked.")
|
if key not in seen:
|
||||||
noticed_something = True
|
seen.add(key)
|
||||||
elif not container.trap_dc:
|
unique_candidates.append((cn, c, item))
|
||||||
print("You don't notice anything unusual about it.")
|
|
||||||
|
|
||||||
if container.trap_dc:
|
match = disambiguate(object_name, unique_candidates, key_fn=lambda t: t[2].name)
|
||||||
roll = randint(1, 20) + int_mod
|
if match:
|
||||||
print(f"\nInvestigation check: 1d20{int_mod:+} = {roll} vs DC {container.trap_dc}")
|
container_name, container, item_obj = match
|
||||||
if roll >= container.trap_dc:
|
print(f"\n--- {item_obj.name} ---")
|
||||||
print("You notice a trap mechanism on it.")
|
print(item_obj.description)
|
||||||
container.trap_detected = True
|
if isinstance(item_obj, Weapon):
|
||||||
noticed_something = True
|
print(f"Damage: {item_obj.damage1H or item_obj.damage2H or item_obj.versatiledmg}")
|
||||||
elif not container.lock_dc and not noticed_something:
|
print(f"Damage Type: {item_obj.dmgType.name.title()}")
|
||||||
print("You don't notice anything unusual about it.")
|
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:
|
def _examine_container(self, container_name, container):
|
||||||
if container.contents:
|
"""Print details of a container and its contents."""
|
||||||
print("\nInside, you see:")
|
print(f"\n--- {container.name.title()} ---")
|
||||||
for item_obj, count in container.contents.items():
|
print(container.description)
|
||||||
if count > 0:
|
|
||||||
print(f" - {item_obj.name} (x{count})")
|
|
||||||
else:
|
|
||||||
print("It appears to be empty.")
|
|
||||||
found_in_loot = True
|
|
||||||
break
|
|
||||||
|
|
||||||
for item_obj, count in container.contents.items():
|
int_mod = self.player.mods["Intelligence"]
|
||||||
if object_name.lower() in item_obj.name.lower() and count > 0:
|
noticed_something = False
|
||||||
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:
|
if container.lock_dc:
|
||||||
print(f"You don't see or have '{object_name}' to examine.")
|
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):
|
def take_item(self, item_name):
|
||||||
"""
|
"""
|
||||||
@@ -212,91 +258,94 @@ 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 container.is_locked:
|
if not match:
|
||||||
print(f"The {container_name} is locked.")
|
print(f"You don't see a '{container_name}' here to loot.")
|
||||||
return
|
return
|
||||||
|
|
||||||
if container.is_trapped and not container.trap_disarmed:
|
current_name, container = match
|
||||||
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
|
|
||||||
|
|
||||||
if not container.contents:
|
if container.is_locked:
|
||||||
print(f"The {container_name} is empty.")
|
print(f"The {current_name} is locked.")
|
||||||
return
|
return
|
||||||
|
|
||||||
print(f"You look inside the {container_name}. You see:")
|
if container.is_trapped and not container.trap_disarmed:
|
||||||
loot_list = list(container.contents.items())
|
if container.trap_detected:
|
||||||
|
print(f"The {current_name} is trapped. You'll need to disable the trap first.")
|
||||||
while True:
|
return
|
||||||
for i, (item_obj, count) in enumerate(loot_list, 1):
|
else:
|
||||||
print(f"{i}) {item_obj.name} (x{count})")
|
print(f"You open the {current_name}. A trap is triggered!")
|
||||||
|
# TODO: trap damage/effects
|
||||||
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'.")
|
|
||||||
return
|
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
|
# Initialize the game engine
|
||||||
|
|||||||
Reference in New Issue
Block a user