normalize names, reverse player function syntax, link new rooms
This commit is contained in:
parent
2637fcc7cc
commit
8a6fcfc7a5
|
@ -8,11 +8,12 @@ from adventure.context import (
|
|||
broadcast,
|
||||
get_agent_for_actor,
|
||||
get_dungeon_master,
|
||||
get_game_systems,
|
||||
has_dungeon_master,
|
||||
set_dungeon_master,
|
||||
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.search import find_actor_in_room
|
||||
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.
|
||||
|
||||
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):
|
||||
|
@ -47,15 +48,13 @@ def action_explore(direction: str) -> str:
|
|||
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."
|
||||
|
||||
existing_rooms = [room.name for room in action_world.rooms]
|
||||
try:
|
||||
new_room = generate_room(
|
||||
dungeon_master, action_world.theme, existing_rooms=existing_rooms
|
||||
)
|
||||
systems = get_game_systems()
|
||||
new_room = generate_room(dungeon_master, action_world, systems)
|
||||
action_world.rooms.append(new_room)
|
||||
|
||||
# link the rooms together
|
||||
# TODO: generate portals
|
||||
link_rooms(dungeon_master, action_world, systems, [new_room])
|
||||
|
||||
broadcast(
|
||||
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."
|
||||
)
|
||||
|
||||
existing_items = [item.name for item in action_room.items]
|
||||
|
||||
try:
|
||||
systems = get_game_systems()
|
||||
new_item = generate_item(
|
||||
dungeon_master,
|
||||
action_world.theme,
|
||||
existing_items=existing_items,
|
||||
dest_room=action_room.name,
|
||||
action_world,
|
||||
systems,
|
||||
dest_room=action_room,
|
||||
)
|
||||
action_room.items.append(new_item)
|
||||
|
||||
|
|
|
@ -22,7 +22,15 @@ from adventure.models.entity import (
|
|||
)
|
||||
from adventure.models.event import GenerateEvent
|
||||
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__)
|
||||
|
||||
|
@ -238,10 +246,13 @@ def generate_item(
|
|||
world, include_actor_inventory=True, include_item_inventory=True
|
||||
)
|
||||
]
|
||||
|
||||
if dest_actor:
|
||||
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:
|
||||
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:
|
||||
dest_note = "The item will be placed in the world"
|
||||
|
||||
|
@ -291,7 +302,10 @@ def generate_actor(
|
|||
systems: List[GameSystem],
|
||||
dest_room: Room,
|
||||
) -> 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(
|
||||
agent,
|
||||
"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 = []
|
||||
for attribute_name in attribute_names.split(","):
|
||||
attribute_name = attribute_name.strip()
|
||||
attribute_name = normalize_name(attribute_name)
|
||||
if attribute_name:
|
||||
operation = loop_retry(
|
||||
agent,
|
||||
|
@ -445,34 +459,15 @@ def generate_effect(agent: Agent, world: World, entity: Item) -> Effect:
|
|||
return Effect(name=name, description=description, attributes=attributes)
|
||||
|
||||
|
||||
def generate_world(
|
||||
def link_rooms(
|
||||
agent: Agent,
|
||||
name: str,
|
||||
theme: str,
|
||||
world: World,
|
||||
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
|
||||
)
|
||||
rooms: List[Room] | None = None,
|
||||
) -> None:
|
||||
rooms = rooms or world.rooms
|
||||
|
||||
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
|
||||
for room in world.rooms:
|
||||
for room in rooms:
|
||||
num_portals = randint(
|
||||
world_config.size.portals.min, world_config.size.portals.max
|
||||
)
|
||||
|
@ -512,6 +507,36 @@ def generate_world(
|
|||
logger.exception("error generating portal")
|
||||
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
|
||||
world.order = [actor.name for room in world.rooms for actor in room.actors]
|
||||
return world
|
||||
|
|
|
@ -6,7 +6,6 @@ from typing import Any, Callable, Dict, List, Optional, Sequence
|
|||
|
||||
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage
|
||||
from packit.agent import Agent
|
||||
from packit.utils import could_be_json
|
||||
|
||||
from adventure.context import action_context
|
||||
from adventure.models.event import PromptEvent
|
||||
|
@ -92,18 +91,7 @@ class BasePlayer:
|
|||
|
||||
return self(prompt, **context)
|
||||
|
||||
def parse_input(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
|
||||
|
||||
def parse_pseudo_function(self, reply: str):
|
||||
# turn other replies into a JSON function call
|
||||
action, *param_rest = reply.split(":", 1)
|
||||
param_str = ",".join(param_rest or [])
|
||||
|
@ -133,9 +121,16 @@ class BasePlayer:
|
|||
"parameters": params,
|
||||
}
|
||||
)
|
||||
self.memory.append(AIMessage(content=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:
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
|
|
|
@ -203,6 +203,10 @@ async def handler(websocket):
|
|||
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):
|
||||
world = get_current_world()
|
||||
if not world:
|
||||
|
@ -211,7 +215,7 @@ def render_input(data):
|
|||
|
||||
if "event" in data:
|
||||
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:
|
||||
render_event(event)
|
||||
else:
|
||||
|
|
|
@ -31,6 +31,7 @@ from adventure.context import (
|
|||
from adventure.game_system import GameSystem
|
||||
from adventure.models.entity import World
|
||||
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
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
@ -91,7 +92,7 @@ def simulate_world(
|
|||
logger.error(f"Agent or actor not found for name {actor_name}")
|
||||
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:
|
||||
logger.error(f"Actor {actor_name} is not in a room")
|
||||
continue
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
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.models.base import dataclass
|
||||
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
|
||||
else:
|
||||
dungeon_master = get_dungeon_master()
|
||||
systems = get_game_systems()
|
||||
new_item = generate_item(
|
||||
dungeon_master, action_world.theme
|
||||
) # TODO: pass recipe item
|
||||
dungeon_master, action_world, systems
|
||||
) # TODO: pass crafting recipe and generate from that
|
||||
|
||||
action_actor.items.append(new_item)
|
||||
|
||||
|
|
|
@ -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"
|
|
@ -100,6 +100,26 @@ def find_item_in_room(
|
|||
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]:
|
||||
for room in world.rooms:
|
||||
yield room
|
||||
|
@ -118,14 +138,8 @@ def list_actors(world: World) -> Generator[Actor, Any, None]:
|
|||
|
||||
|
||||
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]:
|
||||
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 item in room.items:
|
||||
|
@ -138,3 +152,45 @@ def list_items(
|
|||
for actor in room.actors:
|
||||
for item in actor.items:
|
||||
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
|
||||
|
|
|
@ -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(".")
|
Loading…
Reference in New Issue