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,
|
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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
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
|
||||||
|
|
|
@ -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