diff --git a/adventure/generate.py b/adventure/generate.py index 5fc78d9..926e410 100644 --- a/adventure/generate.py +++ b/adventure/generate.py @@ -20,6 +20,8 @@ OPPOSITE_DIRECTIONS = { def duplicate_name_parser(existing_names: List[str]): def name_parser(name: str, **kwargs): + logger.debug(f"validating name: {name}") + if name in existing_names: raise ValueError(f'"{name}" has already been used.') diff --git a/adventure/optional_actions.py b/adventure/optional_actions.py index 70907bb..3cb9e28 100644 --- a/adventure/optional_actions.py +++ b/adventure/optional_actions.py @@ -12,6 +12,7 @@ from adventure.context import ( set_dungeon_master, ) from adventure.generate import OPPOSITE_DIRECTIONS, generate_item, generate_room +from adventure.search import find_actor_in_room logger = getLogger(__name__) @@ -47,19 +48,23 @@ def action_explore(direction: str) -> str: return f"You cannot explore {direction} from here, that direction already leads to {dest_room}. Please use the move action to go there." existing_rooms = [room.name for room in current_world.rooms] - new_room = generate_room( - dungeon_master, current_world.theme, existing_rooms=existing_rooms - ) - current_world.rooms.append(new_room) + try: + new_room = generate_room( + dungeon_master, current_world.theme, existing_rooms=existing_rooms + ) + current_world.rooms.append(new_room) - # link the rooms together - current_room.portals[direction] = new_room.name - new_room.portals[OPPOSITE_DIRECTIONS[direction]] = current_room.name + # link the rooms together + current_room.portals[direction] = new_room.name + new_room.portals[OPPOSITE_DIRECTIONS[direction]] = current_room.name - broadcast( - f"{current_actor.name} explores {direction} of {current_room.name} and finds a new room: {new_room.name}" - ) - return f"You explore {direction} and find a new room: {new_room.name}" + broadcast( + f"{current_actor.name} explores {direction} of {current_room.name} and finds a new room: {new_room.name}" + ) + return f"You explore {direction} and find a new room: {new_room.name}" + except Exception: + logger.exception("error generating room") + return f"You cannot explore {direction} from here, there is no room in that direction." def action_search(unused: bool) -> str: @@ -71,22 +76,26 @@ def action_search(unused: bool) -> str: dungeon_master = get_dungeon_master() if len(action_room.items) > 2: - return "You find nothing hidden in the room." + return "You find nothing hidden in the room. There is no room for more items." existing_items = [item.name for item in action_room.items] - new_item = generate_item( - dungeon_master, - action_world.theme, - existing_items=existing_items, - dest_room=action_room.name, - ) - action_room.items.append(new_item) + try: + new_item = generate_item( + dungeon_master, + action_world.theme, + existing_items=existing_items, + dest_room=action_room.name, + ) + action_room.items.append(new_item) - broadcast( - f"{action_actor.name} searches {action_room.name} and finds a new item: {new_item.name}" - ) - return f"You search the room and find a new item: {new_item.name}" + broadcast( + f"{action_actor.name} searches {action_room.name} and finds a new item: {new_item.name}" + ) + return f"You search the room and find a new item: {new_item.name}" + except Exception: + logger.exception("error generating item") + return "You find nothing hidden in the room." def action_use(item: str, target: str) -> str: @@ -115,9 +124,7 @@ def action_use(item: str, target: str) -> str: target_actor = action_actor target = action_actor.name else: - target_actor = next( - (actor for actor in action_room.actors if actor.name == target), None - ) + target_actor = find_actor_in_room(action_room, target) if not target_actor: return f"The {target} character is not in the room." diff --git a/adventure/render_comfy.py b/adventure/render_comfy.py index 1833e65..bec483b 100644 --- a/adventure/render_comfy.py +++ b/adventure/render_comfy.py @@ -12,6 +12,7 @@ from typing import List from uuid import uuid4 import websocket # NOTE: websocket-client (https://github.com/websocket-client/websocket-client) +from fnvhash import fnv1a_32 from jinja2 import Environment, FileSystemLoader, select_autoescape from PIL import Image @@ -225,7 +226,8 @@ def prompt_from_event(event: GameEvent) -> str | None: if event.item: return f"{event.actor.name} uses the {event.item.name}. {event.item.description}. {event.actor.description}. {event.room.description}." - return f"{event.actor.name} {event.action}. {event.actor.description}. {event.room.description}." + action_name = event.action.removeprefix("action_") + return f"{event.actor.name} uses {action_name}. {event.actor.description}. {event.room.description}." if isinstance(event, ReplyEvent): return event.text @@ -262,18 +264,24 @@ def sanitize_name(name: str) -> str: return valid_name.lower() +def fast_hash(text: str) -> str: + return hex(fnv1a_32(text.encode("utf-8"))) + + def get_image_prefix(event: GameEvent | WorldEntity) -> str: if isinstance(event, ActionEvent): return sanitize_name(f"event-action-{event.actor.name}-{event.action}") if isinstance(event, ReplyEvent): - return sanitize_name(f"event-reply-{event.actor.name}") + return sanitize_name(f"event-reply-{event.actor.name}-{fast_hash(event.text)}") if isinstance(event, ResultEvent): - return sanitize_name(f"event-result-{event.actor.name}") + return sanitize_name( + f"event-result-{event.actor.name}-{fast_hash(event.result)}" + ) if isinstance(event, StatusEvent): - return "status" + return sanitize_name(f"event-status-{fast_hash(event.text)}") if isinstance(event, WorldEntity): return sanitize_name(f"entity-{event.__class__.__name__.lower()}-{event.name}") diff --git a/adventure/server_socket.py b/adventure/server_socket.py index e2176c8..6bab3ff 100644 --- a/adventure/server_socket.py +++ b/adventure/server_socket.py @@ -35,6 +35,7 @@ from adventure.player import ( set_player, ) from adventure.render_comfy import render_entity, render_event +from adventure.search import find_actor, find_item, find_room from adventure.state import snapshot_world, world_json logger = getLogger(__name__) @@ -214,33 +215,23 @@ def render_input(data): logger.error(f"failed to find event {event_id}") elif "actor" in data: actor_name = data["actor"] - actor = next( - (a for r in world.rooms for a in r.actors if a.name == actor_name), None - ) + actor = find_actor(world, actor_name) if actor: render_entity(actor) else: logger.error(f"failed to find actor {actor_name}") elif "room" in data: room_name = data["room"] - room = next((r for r in world.rooms if r.name == room_name), None) + room = find_room(world, room_name) if room: render_entity(room) else: logger.error(f"failed to find room {room_name}") elif "item" in data: item_name = data["item"] - item = None - for room in world.rooms: - item = next((i for i in room.items if i.name == item_name), None) - if item: - break - - for actor in room.actors: - item = next((i for i in actor.items if i.name == item_name), None) - if item: - break - + item = find_item( + world, item_name, include_actor_inventory=True, include_room_inventory=True + ) if item: render_entity(item) else: diff --git a/adventure/sim_systems/combat_actions.py b/adventure/sim_systems/combat_actions.py index ede4b80..58fe6ac 100644 --- a/adventure/sim_systems/combat_actions.py +++ b/adventure/sim_systems/combat_actions.py @@ -4,6 +4,7 @@ from adventure.context import ( get_current_context, get_dungeon_master, ) +from adventure.search import find_actor_in_room, find_item_in_room def action_attack(target: str) -> str: @@ -17,12 +18,8 @@ def action_attack(target: str) -> str: _, action_room, action_actor = get_current_context() # make sure the target is in the room - target_actor = next( - (actor for actor in action_room.actors if actor.name == target), None - ) - target_item = next( - (item for item in action_room.items if item.name == target), None - ) + target_actor = find_actor_in_room(action_room, target) + target_item = find_item_in_room(action_room, target) dungeon_master = get_dungeon_master() if target_actor: @@ -73,12 +70,8 @@ def action_cast(target: str, spell: str) -> str: _, action_room, action_actor = get_current_context() # make sure the target is in the room - target_actor = next( - (actor for actor in action_room.actors if actor.name == target), None - ) - target_item = next( - (item for item in action_room.items if item.name == target), None - ) + target_actor = find_actor_in_room(action_room, target) + target_item = find_item_in_room(action_room, target) if not target_actor and not target_item: return f"{target} is not in the {action_room.name}." diff --git a/adventure/sim_systems/hunger_actions.py b/adventure/sim_systems/hunger_actions.py index e7034f1..cb906c3 100644 --- a/adventure/sim_systems/hunger_actions.py +++ b/adventure/sim_systems/hunger_actions.py @@ -1,4 +1,5 @@ from adventure.context import get_current_context +from adventure.search import find_item_in_actor def action_cook(item: str) -> str: @@ -10,7 +11,7 @@ def action_cook(item: str) -> str: """ _, _, action_actor = get_current_context() - target_item = next((i for i in action_actor.items if i.name == item), None) + target_item = find_item_in_actor(action_actor, item) if target_item is None: return "You don't have the item to cook." @@ -38,7 +39,7 @@ def action_eat(item: str) -> str: """ _, _, action_actor = get_current_context() - target_item = next((i for i in action_actor.items if i.name == item), None) + target_item = find_item_in_actor(action_actor, item) if target_item is None: return "You don't have the item to eat."