1
0
Fork 0

normalize names, reverse player function syntax, link new rooms

This commit is contained in:
Sean Sube 2024-05-19 13:09:52 -05:00
parent 2637fcc7cc
commit 8a6fcfc7a5
Signed by: ssube
GPG Key ID: 3EED7B957D362AF1
9 changed files with 181 additions and 66 deletions

View File

@ -8,11 +8,12 @@ from adventure.context import (
broadcast, broadcast,
get_agent_for_actor, get_agent_for_actor,
get_dungeon_master, get_dungeon_master,
get_game_systems,
has_dungeon_master, has_dungeon_master,
set_dungeon_master, set_dungeon_master,
world_context, world_context,
) )
from adventure.generate import generate_item, generate_room from adventure.generate import generate_item, generate_room, link_rooms
from adventure.utils.effect import apply_effect from adventure.utils.effect import apply_effect
from adventure.utils.search import find_actor_in_room from adventure.utils.search import find_actor_in_room
from adventure.utils.world import describe_actor, describe_entity from adventure.utils.world import describe_actor, describe_entity
@ -37,7 +38,7 @@ def action_explore(direction: str) -> str:
Explore the room in a new direction. You can only explore directions that do not already have a portal. Explore the room in a new direction. You can only explore directions that do not already have a portal.
Args: Args:
direction: The direction to explore: north, south, east, or west. direction: The direction to explore. For example: inside, outside, upstairs, downstairs, trapdoor, portal, etc.
""" """
with world_context() as (action_world, action_room, action_actor): with world_context() as (action_world, action_room, action_actor):
@ -47,15 +48,13 @@ def action_explore(direction: str) -> str:
dest_room = action_room.portals[direction] dest_room = action_room.portals[direction]
return f"You cannot explore {direction} from here, that direction already leads to {dest_room}. Please use the move action to go there." 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 action_world.rooms]
try: try:
new_room = generate_room( systems = get_game_systems()
dungeon_master, action_world.theme, existing_rooms=existing_rooms new_room = generate_room(dungeon_master, action_world, systems)
)
action_world.rooms.append(new_room) action_world.rooms.append(new_room)
# link the rooms together # link the rooms together
# TODO: generate portals link_rooms(dungeon_master, action_world, systems, [new_room])
broadcast( broadcast(
f"{action_actor.name} explores {direction} of {action_room.name} and finds a new room: {new_room.name}" f"{action_actor.name} explores {direction} of {action_room.name} and finds a new room: {new_room.name}"
@ -79,14 +78,13 @@ def action_search(unused: bool) -> str:
"You find nothing hidden in the room. There is no room for more items." "You find nothing hidden in the room. There is no room for more items."
) )
existing_items = [item.name for item in action_room.items]
try: try:
systems = get_game_systems()
new_item = generate_item( new_item = generate_item(
dungeon_master, dungeon_master,
action_world.theme, action_world,
existing_items=existing_items, systems,
dest_room=action_room.name, dest_room=action_room,
) )
action_room.items.append(new_item) action_room.items.append(new_item)

View File

@ -22,7 +22,15 @@ from adventure.models.entity import (
) )
from adventure.models.event import GenerateEvent from adventure.models.event import GenerateEvent
from adventure.utils import try_parse_float, try_parse_int from adventure.utils import try_parse_float, try_parse_int
from adventure.utils.search import list_actors, list_items, list_rooms from adventure.utils.search import (
list_actors,
list_actors_in_room,
list_items,
list_items_in_actor,
list_items_in_room,
list_rooms,
)
from adventure.utils.string import normalize_name
logger = getLogger(__name__) logger = getLogger(__name__)
@ -238,10 +246,13 @@ def generate_item(
world, include_actor_inventory=True, include_item_inventory=True world, include_actor_inventory=True, include_item_inventory=True
) )
] ]
if dest_actor: if dest_actor:
dest_note = f"The item will be held by the {dest_actor.name} character" dest_note = f"The item will be held by the {dest_actor.name} character"
existing_items += [item.name for item in list_items_in_actor(dest_actor)]
elif dest_room: elif dest_room:
dest_note = f"The item will be placed in the {dest_room.name} room" dest_note = f"The item will be placed in the {dest_room.name} room"
existing_items += [item.name for item in list_items_in_room(dest_room)]
else: else:
dest_note = "The item will be placed in the world" dest_note = "The item will be placed in the world"
@ -291,7 +302,10 @@ def generate_actor(
systems: List[GameSystem], systems: List[GameSystem],
dest_room: Room, dest_room: Room,
) -> Actor: ) -> Actor:
existing_actors = [actor.name for actor in list_actors(world)] existing_actors = [actor.name for actor in list_actors(world)] + [
actor.name for actor in list_actors_in_room(dest_room)
]
name = loop_retry( name = loop_retry(
agent, agent,
"Generate one person or creature that would make sense in the world of {world_theme}. " "Generate one person or creature that would make sense in the world of {world_theme}. "
@ -395,7 +409,7 @@ def generate_effect(agent: Agent, world: World, entity: Item) -> Effect:
attributes = [] attributes = []
for attribute_name in attribute_names.split(","): for attribute_name in attribute_names.split(","):
attribute_name = attribute_name.strip() attribute_name = normalize_name(attribute_name)
if attribute_name: if attribute_name:
operation = loop_retry( operation = loop_retry(
agent, agent,
@ -445,34 +459,15 @@ def generate_effect(agent: Agent, world: World, entity: Item) -> Effect:
return Effect(name=name, description=description, attributes=attributes) return Effect(name=name, description=description, attributes=attributes)
def generate_world( def link_rooms(
agent: Agent, agent: Agent,
name: str, world: World,
theme: str,
systems: List[GameSystem], systems: List[GameSystem],
room_count: int | None = None, rooms: List[Room] | None = None,
) -> World: ) -> None:
room_count = room_count or randint( rooms = rooms or world.rooms
world_config.size.rooms.min, world_config.size.rooms.max
)
broadcast_generated(message=f"Generating a {theme} with {room_count} rooms") for room in rooms:
world = World(name=name, rooms=[], theme=theme, order=[])
set_current_world(world)
# generate the rooms
for _ in range(room_count):
try:
room = generate_room(agent, world, systems)
generate_system_attributes(agent, world, room, systems)
broadcast_generated(entity=room)
world.rooms.append(room)
except Exception:
logger.exception("error generating room")
continue
# generate portals to link the rooms together
for room in world.rooms:
num_portals = randint( num_portals = randint(
world_config.size.portals.min, world_config.size.portals.max world_config.size.portals.min, world_config.size.portals.max
) )
@ -512,6 +507,36 @@ def generate_world(
logger.exception("error generating portal") logger.exception("error generating portal")
continue continue
def generate_world(
agent: Agent,
name: str,
theme: str,
systems: List[GameSystem],
room_count: int | None = None,
) -> World:
room_count = room_count or randint(
world_config.size.rooms.min, world_config.size.rooms.max
)
broadcast_generated(message=f"Generating a {theme} with {room_count} rooms")
world = World(name=name, rooms=[], theme=theme, order=[])
set_current_world(world)
# generate the rooms
for _ in range(room_count):
try:
room = generate_room(agent, world, systems)
generate_system_attributes(agent, world, room, systems)
broadcast_generated(entity=room)
world.rooms.append(room)
except Exception:
logger.exception("error generating room")
continue
# generate portals to link the rooms together
link_rooms(agent, world, systems)
# ensure actors act in a stable order # ensure actors act in a stable order
world.order = [actor.name for room in world.rooms for actor in room.actors] world.order = [actor.name for room in world.rooms for actor in room.actors]
return world return world

View File

@ -6,7 +6,6 @@ from typing import Any, Callable, Dict, List, Optional, Sequence
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage from langchain_core.messages import AIMessage, BaseMessage, HumanMessage
from packit.agent import Agent from packit.agent import Agent
from packit.utils import could_be_json
from adventure.context import action_context from adventure.context import action_context
from adventure.models.event import PromptEvent from adventure.models.event import PromptEvent
@ -92,18 +91,7 @@ class BasePlayer:
return self(prompt, **context) return self(prompt, **context)
def parse_input(self, reply: str): def parse_pseudo_function(self, reply: str):
# if the reply starts with a tilde, it is a literal response and should be returned without the tilde
if reply.startswith("~"):
reply = reply[1:]
self.memory.append(AIMessage(content=reply))
return reply
# if the reply is JSON or a special command, return it as-is
if could_be_json(reply) or reply.lower() in ["end", ""]:
self.memory.append(AIMessage(content=reply))
return reply
# turn other replies into a JSON function call # turn other replies into a JSON function call
action, *param_rest = reply.split(":", 1) action, *param_rest = reply.split(":", 1)
param_str = ",".join(param_rest or []) param_str = ",".join(param_rest or [])
@ -133,9 +121,16 @@ class BasePlayer:
"parameters": params, "parameters": params,
} }
) )
self.memory.append(AIMessage(content=reply_json))
return reply_json return reply_json
def parse_input(self, reply: str):
# if the reply starts with a tilde, it is a function response and should be parsed without the tilde
if reply.startswith("~"):
reply = self.parse_pseudo_function(reply[1:])
self.memory.append(AIMessage(content=reply))
return reply
def __call__(self, prompt: str, **kwargs) -> str: def __call__(self, prompt: str, **kwargs) -> str:
raise NotImplementedError("Subclasses must implement this method") raise NotImplementedError("Subclasses must implement this method")

View File

@ -203,6 +203,10 @@ async def handler(websocket):
logger.info("client disconnected: %s", id) logger.info("client disconnected: %s", id)
def find_recent_event(event_id: str) -> GameEvent | None:
return next((e for e in recent_events if e.id == event_id), None)
def render_input(data): def render_input(data):
world = get_current_world() world = get_current_world()
if not world: if not world:
@ -211,7 +215,7 @@ def render_input(data):
if "event" in data: if "event" in data:
event_id = data["event"] event_id = data["event"]
event = next((e for e in recent_events if e.id == event_id), None) event = find_recent_event(event_id)
if event: if event:
render_event(event) render_event(event)
else: else:

View File

@ -31,6 +31,7 @@ from adventure.context import (
from adventure.game_system import GameSystem from adventure.game_system import GameSystem
from adventure.models.entity import World from adventure.models.entity import World
from adventure.models.event import ActionEvent, ReplyEvent, ResultEvent from adventure.models.event import ActionEvent, ReplyEvent, ResultEvent
from adventure.utils.search import find_room_with_actor
from adventure.utils.world import describe_entity, format_attributes from adventure.utils.world import describe_entity, format_attributes
logger = getLogger(__name__) logger = getLogger(__name__)
@ -91,7 +92,7 @@ def simulate_world(
logger.error(f"Agent or actor not found for name {actor_name}") logger.error(f"Agent or actor not found for name {actor_name}")
continue continue
room = next((room for room in world.rooms if actor in room.actors), None) room = find_room_with_actor(world, actor)
if not room: if not room:
logger.error(f"Actor {actor_name} is not in a room") logger.error(f"Actor {actor_name} is not in a room")
continue continue

View File

@ -1,6 +1,11 @@
from random import randint from random import randint
from adventure.context import broadcast, get_dungeon_master, world_context from adventure.context import (
broadcast,
get_dungeon_master,
get_game_systems,
world_context,
)
from adventure.generate import generate_item from adventure.generate import generate_item
from adventure.models.base import dataclass from adventure.models.base import dataclass
from adventure.models.entity import Item from adventure.models.entity import Item
@ -64,9 +69,10 @@ def action_craft(item_name: str) -> str:
new_item = Item(**vars(result_item)) # Copying the item new_item = Item(**vars(result_item)) # Copying the item
else: else:
dungeon_master = get_dungeon_master() dungeon_master = get_dungeon_master()
systems = get_game_systems()
new_item = generate_item( new_item = generate_item(
dungeon_master, action_world.theme dungeon_master, action_world, systems
) # TODO: pass recipe item ) # TODO: pass crafting recipe and generate from that
action_actor.items.append(new_item) action_actor.items.append(new_item)

View File

@ -0,0 +1,22 @@
from typing import Callable
def try_parse_int(value: str) -> int | None:
try:
return int(value)
except ValueError:
return None
def try_parse_float(value: str) -> float | None:
try:
return float(value)
except ValueError:
return None
def format_callable(fn: Callable | None) -> str:
if fn:
return f"{fn.__module__}:{fn.__name__}"
return "None"

View File

@ -100,6 +100,26 @@ def find_item_in_room(
return None return None
def find_room_with_actor(world: World, actor: Actor) -> Room | None:
for room in world.rooms:
for room_actor in room.actors:
if normalize_name(actor.name) == normalize_name(room_actor.name):
return room
return None
def find_containing_room(world: World, entity: Room | Actor | Item) -> Room | None:
if isinstance(entity, Room):
return entity
for room in world.rooms:
if entity in room.actors or entity in room.items:
return room
return None
def list_rooms(world: World) -> Generator[Room, Any, None]: def list_rooms(world: World) -> Generator[Room, Any, None]:
for room in world.rooms: for room in world.rooms:
yield room yield room
@ -118,14 +138,8 @@ def list_actors(world: World) -> Generator[Actor, Any, None]:
def list_items( def list_items(
world: World, include_actor_inventory=False, include_item_inventory=False world: World, include_actor_inventory=True, include_item_inventory=True
) -> Generator[Item, Any, None]: ) -> Generator[Item, Any, None]:
def list_items_in_container(container: Item) -> Generator[Item, Any, None]:
for item in container.items:
yield item
if include_item_inventory:
yield from list_items_in_container(item)
for room in world.rooms: for room in world.rooms:
for item in room.items: for item in room.items:
@ -138,3 +152,45 @@ def list_items(
for actor in room.actors: for actor in room.actors:
for item in actor.items: for item in actor.items:
yield item yield item
def list_actors_in_room(room: Room) -> Generator[Actor, Any, None]:
for actor in room.actors:
yield actor
def list_items_in_actor(
actor: Actor, include_item_inventory=True
) -> Generator[Item, Any, None]:
for item in actor.items:
yield item
if include_item_inventory:
yield from list_items_in_container(item)
def list_items_in_container(
container: Item, include_item_inventory=True
) -> Generator[Item, Any, None]:
for item in container.items:
yield item
if include_item_inventory:
yield from list_items_in_container(item)
def list_items_in_room(
room: Room,
include_actor_inventory=True,
include_item_inventory=True,
) -> Generator[Item, Any, None]:
for item in room.items:
yield item
if include_item_inventory:
yield from list_items_in_container(item)
if include_actor_inventory:
for actor in room.actors:
for item in actor.items:
yield item

View File

@ -0,0 +1,8 @@
from functools import lru_cache
@lru_cache(maxsize=1024)
def normalize_name(name: str) -> str:
name = name.lower().strip()
name = name.strip('"').strip("'")
return name.removesuffix(".")