improve search, prevent duplicate names, validate effect operations
This commit is contained in:
parent
71d2be85f1
commit
2bb842a559
|
@ -15,6 +15,7 @@ from adventure.utils.search import (
|
||||||
find_item_in_room,
|
find_item_in_room,
|
||||||
find_room,
|
find_room,
|
||||||
)
|
)
|
||||||
|
from adventure.utils.string import normalize_name
|
||||||
from adventure.utils.world import describe_entity
|
from adventure.utils.world import describe_entity
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
@ -31,7 +32,7 @@ def action_look(target: str) -> str:
|
||||||
with action_context() as (action_room, action_actor):
|
with action_context() as (action_room, action_actor):
|
||||||
broadcast(f"{action_actor.name} looks at {target}")
|
broadcast(f"{action_actor.name} looks at {target}")
|
||||||
|
|
||||||
if target.lower() == action_room.name.lower():
|
if normalize_name(target) == normalize_name(action_room.name):
|
||||||
broadcast(f"{action_actor.name} saw the {action_room.name} room")
|
broadcast(f"{action_actor.name} saw the {action_room.name} room")
|
||||||
return describe_entity(action_room)
|
return describe_entity(action_room)
|
||||||
|
|
||||||
|
@ -69,7 +70,11 @@ def action_move(direction: str) -> str:
|
||||||
|
|
||||||
with world_context() as (action_world, action_room, action_actor):
|
with world_context() as (action_world, action_room, action_actor):
|
||||||
portal = next(
|
portal = next(
|
||||||
(p for p in action_room.portals if p.name.lower() == direction.lower()),
|
(
|
||||||
|
p
|
||||||
|
for p in action_room.portals
|
||||||
|
if normalize_name(p.name) == normalize_name(direction)
|
||||||
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
if not portal:
|
if not portal:
|
||||||
|
|
|
@ -19,6 +19,7 @@ from pyee.base import EventEmitter
|
||||||
from adventure.game_system import GameSystem
|
from adventure.game_system import GameSystem
|
||||||
from adventure.models.entity import Actor, Room, World
|
from adventure.models.entity import Actor, Room, World
|
||||||
from adventure.models.event import GameEvent
|
from adventure.models.event import GameEvent
|
||||||
|
from adventure.utils.string import normalize_name
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
@ -223,7 +224,7 @@ def get_actor_agent_for_name(name):
|
||||||
(
|
(
|
||||||
(actor, agent)
|
(actor, agent)
|
||||||
for actor, agent in actor_agents.values()
|
for actor, agent in actor_agents.values()
|
||||||
if actor.name.lower() == name.lower()
|
if normalize_name(actor.name) == normalize_name(name)
|
||||||
),
|
),
|
||||||
(None, None),
|
(None, None),
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Callable, Protocol
|
from typing import Protocol
|
||||||
|
|
||||||
from packit.agent import Agent
|
from packit.agent import Agent
|
||||||
|
|
||||||
from adventure.models.entity import World, WorldEntity
|
from adventure.models.entity import World, WorldEntity
|
||||||
|
from adventure.utils import format_callable
|
||||||
|
|
||||||
|
|
||||||
class FormatPerspective(Enum):
|
class FormatPerspective(Enum):
|
||||||
|
@ -57,10 +58,5 @@ class GameSystem:
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"GameSystem(format={format_callable(self.format)}, generate={format_callable(self.generate)}, simulate={format_callable(self.simulate)})"
|
return f"GameSystem(format={format_callable(self.format)}, generate={format_callable(self.generate)}, simulate={format_callable(self.simulate)})"
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
# TODO: move to utils
|
return str(self)
|
||||||
def format_callable(fn: Callable | None) -> str:
|
|
||||||
if fn:
|
|
||||||
return f"{fn.__module__}:{fn.__name__}"
|
|
||||||
|
|
||||||
return "None"
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ from packit.agent import Agent
|
||||||
from packit.loops import loop_retry
|
from packit.loops import loop_retry
|
||||||
from packit.utils import could_be_json
|
from packit.utils import could_be_json
|
||||||
|
|
||||||
from adventure.context import broadcast
|
from adventure.context import broadcast, set_current_world
|
||||||
from adventure.game_system import GameSystem
|
from adventure.game_system import GameSystem
|
||||||
from adventure.models.config import DEFAULT_CONFIG, WorldConfig
|
from adventure.models.config import DEFAULT_CONFIG, WorldConfig
|
||||||
from adventure.models.entity import (
|
from adventure.models.entity import (
|
||||||
|
@ -21,11 +21,31 @@ from adventure.models.entity import (
|
||||||
WorldEntity,
|
WorldEntity,
|
||||||
)
|
)
|
||||||
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.search import list_actors, list_items, list_rooms
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
world_config: WorldConfig = DEFAULT_CONFIG.world
|
world_config: WorldConfig = DEFAULT_CONFIG.world
|
||||||
|
|
||||||
|
PROMPT_TYPE_FRAGMENTS = {
|
||||||
|
"both": "Enter a positive or negative number, or a string value",
|
||||||
|
"number": "Enter a positive or negative number",
|
||||||
|
"string": "Enter a string value",
|
||||||
|
}
|
||||||
|
|
||||||
|
PROMPT_OPERATION_TYPES = {
|
||||||
|
"set": "both",
|
||||||
|
"add": "number",
|
||||||
|
"subtract": "number",
|
||||||
|
"multiply": "number",
|
||||||
|
"divide": "number",
|
||||||
|
"append": "string",
|
||||||
|
"prepend": "string",
|
||||||
|
}
|
||||||
|
|
||||||
|
OPERATIONS = list(PROMPT_OPERATION_TYPES.keys())
|
||||||
|
|
||||||
|
|
||||||
def duplicate_name_parser(existing_names: List[str]):
|
def duplicate_name_parser(existing_names: List[str]):
|
||||||
def name_parser(value: str, **kwargs):
|
def name_parser(value: str, **kwargs):
|
||||||
|
@ -62,19 +82,28 @@ def broadcast_generated(
|
||||||
broadcast(event)
|
broadcast(event)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_system_attributes(
|
||||||
|
agent: Agent, world: World, entity: WorldEntity, systems: List[GameSystem]
|
||||||
|
) -> None:
|
||||||
|
for system in systems:
|
||||||
|
if system.generate:
|
||||||
|
system.generate(agent, world.theme, entity)
|
||||||
|
|
||||||
|
|
||||||
def generate_room(
|
def generate_room(
|
||||||
agent: Agent,
|
agent: Agent,
|
||||||
world_theme: str,
|
world: World,
|
||||||
existing_rooms: List[str] = [],
|
systems: List[GameSystem],
|
||||||
systems: List[GameSystem] = [],
|
|
||||||
) -> Room:
|
) -> Room:
|
||||||
|
existing_rooms = [room.name for room in list_rooms(world)]
|
||||||
|
|
||||||
name = loop_retry(
|
name = loop_retry(
|
||||||
agent,
|
agent,
|
||||||
"Generate one room, area, or location that would make sense in the world of {world_theme}. "
|
"Generate one room, area, or location that would make sense in the world of {world_theme}. "
|
||||||
"Only respond with the room name in title case, do not include the description or any other text. "
|
"Only respond with the room name in title case, do not include the description or any other text. "
|
||||||
'Do not prefix the name with "the", do not wrap it in quotes. The existing rooms are: {existing_rooms}',
|
'Do not prefix the name with "the", do not wrap it in quotes. The existing rooms are: {existing_rooms}',
|
||||||
context={
|
context={
|
||||||
"world_theme": world_theme,
|
"world_theme": world.theme,
|
||||||
"existing_rooms": existing_rooms,
|
"existing_rooms": existing_rooms,
|
||||||
},
|
},
|
||||||
result_parser=duplicate_name_parser(existing_rooms),
|
result_parser=duplicate_name_parser(existing_rooms),
|
||||||
|
@ -88,27 +117,24 @@ def generate_room(
|
||||||
)
|
)
|
||||||
|
|
||||||
actions = {}
|
actions = {}
|
||||||
|
room = Room(name=name, description=desc, items=[], actors=[], actions=actions)
|
||||||
|
|
||||||
item_count = randint(
|
item_count = randint(
|
||||||
world_config.size.room_items.min, world_config.size.room_items.max
|
world_config.size.room_items.min, world_config.size.room_items.max
|
||||||
)
|
)
|
||||||
broadcast_generated(f"Generating {item_count} items for room: {name}")
|
broadcast_generated(f"Generating {item_count} items for room: {name}")
|
||||||
|
|
||||||
items = []
|
for _ in range(item_count):
|
||||||
for j in range(item_count):
|
|
||||||
existing_items = [item.name for item in items]
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
item = generate_item(
|
item = generate_item(
|
||||||
agent,
|
agent,
|
||||||
world_theme,
|
world,
|
||||||
dest_room=name,
|
systems=systems,
|
||||||
existing_items=existing_items,
|
dest_room=room,
|
||||||
)
|
)
|
||||||
generate_system_attributes(agent, world_theme, item, systems)
|
|
||||||
broadcast_generated(entity=item)
|
broadcast_generated(entity=item)
|
||||||
|
|
||||||
items.append(item)
|
room.items.append(item)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("error generating item")
|
logger.exception("error generating item")
|
||||||
|
|
||||||
|
@ -117,35 +143,30 @@ def generate_room(
|
||||||
)
|
)
|
||||||
broadcast_generated(message=f"Generating {actor_count} actors for room: {name}")
|
broadcast_generated(message=f"Generating {actor_count} actors for room: {name}")
|
||||||
|
|
||||||
actors = []
|
for _ in range(actor_count):
|
||||||
for j in range(actor_count):
|
|
||||||
existing_actors = [actor.name for actor in actors]
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
actor = generate_actor(
|
actor = generate_actor(
|
||||||
agent,
|
agent,
|
||||||
world_theme,
|
world,
|
||||||
dest_room=name,
|
systems=systems,
|
||||||
existing_actors=existing_actors,
|
dest_room=room,
|
||||||
)
|
)
|
||||||
generate_system_attributes(agent, world_theme, actor, systems)
|
|
||||||
broadcast_generated(entity=actor)
|
broadcast_generated(entity=actor)
|
||||||
|
|
||||||
actors.append(actor)
|
room.actors.append(actor)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("error generating actor")
|
logger.exception("error generating actor")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
return Room(
|
return room
|
||||||
name=name, description=desc, items=items, actors=actors, actions=actions
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def generate_portals(
|
def generate_portals(
|
||||||
agent: Agent,
|
agent: Agent,
|
||||||
world_theme: str,
|
world: World,
|
||||||
source_room: Room,
|
source_room: Room,
|
||||||
dest_room: Room,
|
dest_room: Room,
|
||||||
|
systems: List[GameSystem],
|
||||||
) -> Tuple[Portal, Portal]:
|
) -> Tuple[Portal, Portal]:
|
||||||
existing_source_portals = [portal.name for portal in source_room.portals]
|
existing_source_portals = [portal.name for portal in source_room.portals]
|
||||||
existing_dest_portals = [portal.name for portal in dest_room.portals]
|
existing_dest_portals = [portal.name for portal in dest_room.portals]
|
||||||
|
@ -161,7 +182,7 @@ def generate_portals(
|
||||||
"source_room": source_room.name,
|
"source_room": source_room.name,
|
||||||
"dest_room": dest_room.name,
|
"dest_room": dest_room.name,
|
||||||
"existing_portals": existing_source_portals,
|
"existing_portals": existing_source_portals,
|
||||||
"world_theme": world_theme,
|
"world_theme": world.theme,
|
||||||
},
|
},
|
||||||
result_parser=duplicate_name_parser(existing_source_portals),
|
result_parser=duplicate_name_parser(existing_source_portals),
|
||||||
)
|
)
|
||||||
|
@ -179,7 +200,7 @@ def generate_portals(
|
||||||
"source_room": source_room.name,
|
"source_room": source_room.name,
|
||||||
"dest_room": dest_room.name,
|
"dest_room": dest_room.name,
|
||||||
"existing_portals": existing_dest_portals,
|
"existing_portals": existing_dest_portals,
|
||||||
"world_theme": world_theme,
|
"world_theme": world.theme,
|
||||||
"outgoing_name": outgoing_name,
|
"outgoing_name": outgoing_name,
|
||||||
},
|
},
|
||||||
result_parser=duplicate_name_parser(existing_dest_portals),
|
result_parser=duplicate_name_parser(existing_dest_portals),
|
||||||
|
@ -192,26 +213,35 @@ def generate_portals(
|
||||||
description=f"A {outgoing_name} leads to the {dest_room.name} room.",
|
description=f"A {outgoing_name} leads to the {dest_room.name} room.",
|
||||||
destination=dest_room.name,
|
destination=dest_room.name,
|
||||||
)
|
)
|
||||||
|
generate_system_attributes(agent, world, outgoing_portal, systems)
|
||||||
|
|
||||||
incoming_portal = Portal(
|
incoming_portal = Portal(
|
||||||
name=incoming_name,
|
name=incoming_name,
|
||||||
description=f"A {incoming_name} leads to the {source_room.name} room.",
|
description=f"A {incoming_name} leads to the {source_room.name} room.",
|
||||||
destination=source_room.name,
|
destination=source_room.name,
|
||||||
)
|
)
|
||||||
|
generate_system_attributes(agent, world, incoming_portal, systems)
|
||||||
|
|
||||||
return (outgoing_portal, incoming_portal)
|
return (outgoing_portal, incoming_portal)
|
||||||
|
|
||||||
|
|
||||||
def generate_item(
|
def generate_item(
|
||||||
agent: Agent,
|
agent: Agent,
|
||||||
world_theme: str,
|
world: World,
|
||||||
dest_room: str | None = None,
|
systems: List[GameSystem],
|
||||||
dest_actor: str | None = None,
|
dest_room: Room | None = None,
|
||||||
existing_items: List[str] = [],
|
dest_actor: Actor | None = None,
|
||||||
) -> Item:
|
) -> Item:
|
||||||
|
existing_items = [
|
||||||
|
item.name
|
||||||
|
for item in list_items(
|
||||||
|
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} character"
|
dest_note = f"The item will be held by the {dest_actor.name} character"
|
||||||
elif dest_room:
|
elif dest_room:
|
||||||
dest_note = f"The item will be placed in the {dest_room} room"
|
dest_note = f"The item will be placed in the {dest_room.name} room"
|
||||||
else:
|
else:
|
||||||
dest_note = "The item will be placed in the world"
|
dest_note = "The item will be placed in the world"
|
||||||
|
|
||||||
|
@ -225,7 +255,7 @@ def generate_item(
|
||||||
context={
|
context={
|
||||||
"dest_note": dest_note,
|
"dest_note": dest_note,
|
||||||
"existing_items": existing_items,
|
"existing_items": existing_items,
|
||||||
"world_theme": world_theme,
|
"world_theme": world.theme,
|
||||||
},
|
},
|
||||||
result_parser=duplicate_name_parser(existing_items),
|
result_parser=duplicate_name_parser(existing_items),
|
||||||
)
|
)
|
||||||
|
@ -238,35 +268,30 @@ def generate_item(
|
||||||
|
|
||||||
actions = {}
|
actions = {}
|
||||||
item = Item(name=name, description=desc, actions=actions)
|
item = Item(name=name, description=desc, actions=actions)
|
||||||
|
generate_system_attributes(agent, world, item, systems)
|
||||||
|
|
||||||
effect_count = randint(
|
effect_count = randint(
|
||||||
world_config.size.item_effects.min, world_config.size.item_effects.max
|
world_config.size.item_effects.min, world_config.size.item_effects.max
|
||||||
)
|
)
|
||||||
broadcast_generated(message=f"Generating {effect_count} effects for item: {name}")
|
broadcast_generated(message=f"Generating {effect_count} effects for item: {name}")
|
||||||
|
|
||||||
effects = []
|
for _ in range(effect_count):
|
||||||
for i in range(effect_count):
|
|
||||||
existing_effects = [effect.name for effect in effects]
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
effect = generate_effect(
|
effect = generate_effect(agent, world, entity=item)
|
||||||
agent, world_theme, entity=item, existing_effects=existing_effects
|
item.effects.append(effect)
|
||||||
)
|
|
||||||
effects.append(effect)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("error generating effect")
|
logger.exception("error generating effect")
|
||||||
|
|
||||||
item.effects = effects
|
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
|
||||||
def generate_actor(
|
def generate_actor(
|
||||||
agent: Agent,
|
agent: Agent,
|
||||||
world_theme: str,
|
world: World,
|
||||||
dest_room: str,
|
systems: List[GameSystem],
|
||||||
existing_actors: List[str] = [],
|
dest_room: Room,
|
||||||
systems: List[GameSystem] = [],
|
|
||||||
) -> Actor:
|
) -> Actor:
|
||||||
|
existing_actors = [actor.name for actor in list_actors(world)]
|
||||||
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}. "
|
||||||
|
@ -276,9 +301,9 @@ def generate_actor(
|
||||||
"Do not include the name of the room. Do not give characters any duplicate names."
|
"Do not include the name of the room. Do not give characters any duplicate names."
|
||||||
"Do not create any duplicate characters. The existing characters are: {existing_actors}",
|
"Do not create any duplicate characters. The existing characters are: {existing_actors}",
|
||||||
context={
|
context={
|
||||||
"dest_room": dest_room,
|
"dest_room": dest_room.name,
|
||||||
"existing_actors": existing_actors,
|
"existing_actors": existing_actors,
|
||||||
"world_theme": world_theme,
|
"world_theme": world.theme,
|
||||||
},
|
},
|
||||||
result_parser=duplicate_name_parser(existing_actors),
|
result_parser=duplicate_name_parser(existing_actors),
|
||||||
)
|
)
|
||||||
|
@ -296,59 +321,38 @@ def generate_actor(
|
||||||
name=name,
|
name=name,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
actor = Actor(
|
||||||
|
name=name, backstory=backstory, description=description, actions={}, items=[]
|
||||||
|
)
|
||||||
|
generate_system_attributes(agent, world, actor, systems)
|
||||||
|
|
||||||
# generate the actor's inventory
|
# generate the actor's inventory
|
||||||
item_count = randint(
|
item_count = randint(
|
||||||
world_config.size.actor_items.min, world_config.size.actor_items.max
|
world_config.size.actor_items.min, world_config.size.actor_items.max
|
||||||
)
|
)
|
||||||
broadcast_generated(f"Generating {item_count} items for actor {name}")
|
broadcast_generated(f"Generating {item_count} items for actor {name}")
|
||||||
|
|
||||||
items = []
|
|
||||||
for k in range(item_count):
|
for k in range(item_count):
|
||||||
existing_items = [item.name for item in items]
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
item = generate_item(
|
item = generate_item(
|
||||||
agent,
|
agent,
|
||||||
world_theme,
|
world,
|
||||||
dest_actor=name,
|
systems,
|
||||||
existing_items=existing_items,
|
dest_actor=actor,
|
||||||
)
|
)
|
||||||
generate_system_attributes(agent, world_theme, item, systems)
|
generate_system_attributes(agent, world, item, systems)
|
||||||
broadcast_generated(entity=item)
|
broadcast_generated(entity=item)
|
||||||
|
|
||||||
items.append(item)
|
actor.items.append(item)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("error generating item")
|
logger.exception("error generating item")
|
||||||
|
|
||||||
return Actor(
|
return actor
|
||||||
name=name,
|
|
||||||
backstory=backstory,
|
|
||||||
description=description,
|
|
||||||
actions={},
|
|
||||||
items=items,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: move to utils
|
def generate_effect(agent: Agent, world: World, entity: Item) -> Effect:
|
||||||
def try_parse_int(value: str) -> int | None:
|
|
||||||
try:
|
|
||||||
return int(value)
|
|
||||||
except ValueError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: move to utils
|
|
||||||
def try_parse_float(value: str) -> float | None:
|
|
||||||
try:
|
|
||||||
return float(value)
|
|
||||||
except ValueError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def generate_effect(
|
|
||||||
agent: Agent, theme: str, entity: Item, existing_effects: List[str] = []
|
|
||||||
) -> Effect:
|
|
||||||
entity_type = entity.type
|
entity_type = entity.type
|
||||||
|
existing_effects = [effect.name for effect in entity.effects]
|
||||||
|
|
||||||
name = loop_retry(
|
name = loop_retry(
|
||||||
agent,
|
agent,
|
||||||
|
@ -361,7 +365,7 @@ def generate_effect(
|
||||||
"entity_name": entity.name,
|
"entity_name": entity.name,
|
||||||
"entity_type": entity_type,
|
"entity_type": entity_type,
|
||||||
"existing_effects": existing_effects,
|
"existing_effects": existing_effects,
|
||||||
"theme": theme,
|
"theme": world.theme,
|
||||||
},
|
},
|
||||||
result_parser=duplicate_name_parser(existing_effects),
|
result_parser=duplicate_name_parser(existing_effects),
|
||||||
)
|
)
|
||||||
|
@ -374,52 +378,40 @@ def generate_effect(
|
||||||
)
|
)
|
||||||
|
|
||||||
attribute_names = agent(
|
attribute_names = agent(
|
||||||
"Generate a list of attributes that the {name} effect modifies. "
|
"Generate a short list of attributes that the {name} effect modifies. Include 1 to 3 attributes. "
|
||||||
"For example, 'heal' increases the target's 'health' attribute, while 'poison' decreases it. "
|
"For example, 'heal' increases the target's 'health' attribute, while 'poison' decreases it. "
|
||||||
"Use a comma-separated list of attribute names, such as 'health, strength, speed'. "
|
"Use a comma-separated list of attribute names, such as 'health, strength, speed'. "
|
||||||
"Only include the attribute names, do not include the question or any JSON.",
|
"Only include the attribute names, do not include the question or any JSON.",
|
||||||
name=name,
|
name=name,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def operation_parser(value: str, **kwargs):
|
||||||
|
if value not in OPERATIONS:
|
||||||
|
raise ValueError(
|
||||||
|
f'"{value}" is not a valid operation. Choose from: {OPERATIONS}'
|
||||||
|
)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
attributes = []
|
attributes = []
|
||||||
for attribute_name in attribute_names.split(","):
|
for attribute_name in attribute_names.split(","):
|
||||||
attribute_name = attribute_name.strip()
|
attribute_name = attribute_name.strip()
|
||||||
if attribute_name:
|
if attribute_name:
|
||||||
operation = agent(
|
operation = loop_retry(
|
||||||
|
agent,
|
||||||
f"How does the {name} effect modify the {attribute_name} attribute? "
|
f"How does the {name} effect modify the {attribute_name} attribute? "
|
||||||
"For example, 'heal' might 'add' to the 'health' attribute, while 'poison' might 'subtract' from it."
|
"For example, 'heal' might 'add' to the 'health' attribute, while 'poison' might 'subtract' from it."
|
||||||
"Another example is 'writing' might 'set' the 'text' attribute, while 'break' might 'set' the 'condition' attribute."
|
"Another example is 'writing' might 'set' the 'text' attribute, while 'break' might 'set' the 'condition' attribute."
|
||||||
"Reply with the operation only, without any other text. Give a single word."
|
"Reply with the operation only, without any other text. Respond with a single word for the list of operations."
|
||||||
"Choose from the following operations: {operations}",
|
"Choose from the following operations: {operations}",
|
||||||
name=name,
|
context={
|
||||||
attribute_name=attribute_name,
|
"name": name,
|
||||||
operations=[
|
"attribute_name": attribute_name,
|
||||||
"set",
|
"operations": OPERATIONS,
|
||||||
"add",
|
},
|
||||||
"subtract",
|
result_parser=operation_parser,
|
||||||
"multiply",
|
|
||||||
"divide",
|
|
||||||
"append",
|
|
||||||
"prepend",
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
PROMPT_TYPE_FRAGMENTS = {
|
|
||||||
"both": "Enter a positive or negative number, or a string value",
|
|
||||||
"number": "Enter a positive or negative number",
|
|
||||||
"string": "Enter a string value",
|
|
||||||
}
|
|
||||||
|
|
||||||
PROMPT_OPERATION_TYPES = {
|
|
||||||
"set": "both",
|
|
||||||
"add": "number",
|
|
||||||
"subtract": "number",
|
|
||||||
"multiply": "number",
|
|
||||||
"divide": "number",
|
|
||||||
"append": "string",
|
|
||||||
"prepend": "string",
|
|
||||||
}
|
|
||||||
|
|
||||||
operation_type = PROMPT_OPERATION_TYPES[operation]
|
operation_type = PROMPT_OPERATION_TYPES[operation]
|
||||||
operation_prompt = PROMPT_TYPE_FRAGMENTS[operation_type]
|
operation_prompt = PROMPT_TYPE_FRAGMENTS[operation_type]
|
||||||
|
|
||||||
|
@ -453,43 +445,34 @@ def generate_effect(
|
||||||
return Effect(name=name, description=description, attributes=attributes)
|
return Effect(name=name, description=description, attributes=attributes)
|
||||||
|
|
||||||
|
|
||||||
def generate_system_attributes(
|
|
||||||
agent: Agent, theme: str, entity: WorldEntity, systems: List[GameSystem] = []
|
|
||||||
) -> None:
|
|
||||||
for system in systems:
|
|
||||||
if system.generate:
|
|
||||||
system.generate(agent, theme, entity)
|
|
||||||
|
|
||||||
|
|
||||||
def generate_world(
|
def generate_world(
|
||||||
agent: Agent,
|
agent: Agent,
|
||||||
name: str,
|
name: str,
|
||||||
theme: str,
|
theme: str,
|
||||||
|
systems: List[GameSystem],
|
||||||
room_count: int | None = None,
|
room_count: int | None = None,
|
||||||
systems: List[GameSystem] = [],
|
|
||||||
) -> World:
|
) -> World:
|
||||||
room_count = room_count or randint(
|
room_count = room_count or randint(
|
||||||
world_config.size.rooms.min, world_config.size.rooms.max
|
world_config.size.rooms.min, world_config.size.rooms.max
|
||||||
)
|
)
|
||||||
|
|
||||||
broadcast_generated(message=f"Generating a {theme} with {room_count} 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
|
# generate the rooms
|
||||||
rooms = []
|
for _ in range(room_count):
|
||||||
for i in range(room_count):
|
|
||||||
existing_rooms = [room.name for room in rooms]
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
room = generate_room(agent, theme, existing_rooms=existing_rooms)
|
room = generate_room(agent, world, systems)
|
||||||
generate_system_attributes(agent, theme, room, systems)
|
generate_system_attributes(agent, world, room, systems)
|
||||||
broadcast_generated(entity=room)
|
broadcast_generated(entity=room)
|
||||||
rooms.append(room)
|
world.rooms.append(room)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("error generating room")
|
logger.exception("error generating room")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# generate portals to link the rooms together
|
# generate portals to link the rooms together
|
||||||
for room in rooms:
|
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
|
||||||
)
|
)
|
||||||
|
@ -502,23 +485,25 @@ def generate_world(
|
||||||
message=f"Generating {num_portals} portals for room: {room.name}"
|
message=f"Generating {num_portals} portals for room: {room.name}"
|
||||||
)
|
)
|
||||||
|
|
||||||
for i in range(num_portals):
|
for _ in range(num_portals):
|
||||||
previous_destinations = [portal.destination for portal in room.portals] + [
|
previous_destinations = [portal.destination for portal in room.portals] + [
|
||||||
room.name
|
room.name
|
||||||
]
|
]
|
||||||
remaining_rooms = [r for r in rooms if r.name not in previous_destinations]
|
remaining_rooms = [
|
||||||
|
r for r in world.rooms if r.name not in previous_destinations
|
||||||
|
]
|
||||||
if len(remaining_rooms) == 0:
|
if len(remaining_rooms) == 0:
|
||||||
logger.info(f"no more rooms to link to from {room.name}")
|
logger.info(f"no more rooms to link to from {room.name}")
|
||||||
break
|
break
|
||||||
|
|
||||||
# TODO: prompt the DM to choose a destination room
|
# TODO: prompt the DM to choose a destination room
|
||||||
dest_room = choice(
|
dest_room = choice(
|
||||||
[r for r in rooms if r.name not in previous_destinations]
|
[r for r in world.rooms if r.name not in previous_destinations]
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
outgoing_portal, incoming_portal = generate_portals(
|
outgoing_portal, incoming_portal = generate_portals(
|
||||||
agent, theme, room, dest_room
|
agent, world, room, dest_room, systems
|
||||||
)
|
)
|
||||||
|
|
||||||
room.portals.append(outgoing_portal)
|
room.portals.append(outgoing_portal)
|
||||||
|
@ -528,5 +513,5 @@ def generate_world(
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# ensure actors act in a stable order
|
# ensure actors act in a stable order
|
||||||
order = [actor.name for room in rooms for actor in room.actors]
|
world.order = [actor.name for room in world.rooms for actor in room.actors]
|
||||||
return World(name=name, rooms=rooms, theme=theme, order=order)
|
return world
|
||||||
|
|
|
@ -209,6 +209,7 @@ def load_or_generate_world(args, players, systems, world_prompt: WorldPrompt):
|
||||||
world_builder,
|
world_builder,
|
||||||
args.world,
|
args.world,
|
||||||
world_prompt.theme,
|
world_prompt.theme,
|
||||||
|
systems,
|
||||||
room_count=args.rooms,
|
room_count=args.rooms,
|
||||||
)
|
)
|
||||||
save_world(world, world_file)
|
save_world(world, world_file)
|
||||||
|
|
|
@ -88,8 +88,8 @@ DEFAULT_CONFIG = Config(
|
||||||
server=ServerConfig(websocket=WebsocketServerConfig(host="localhost", port=8001)),
|
server=ServerConfig(websocket=WebsocketServerConfig(host="localhost", port=8001)),
|
||||||
world=WorldConfig(
|
world=WorldConfig(
|
||||||
size=WorldSizeConfig(
|
size=WorldSizeConfig(
|
||||||
actor_items=Range(min=1, max=3),
|
actor_items=Range(min=0, max=2),
|
||||||
item_effects=Range(min=1, max=3),
|
item_effects=Range(min=1, max=2),
|
||||||
portals=Range(min=1, max=3),
|
portals=Range(min=1, max=3),
|
||||||
rooms=Range(min=3, max=6),
|
rooms=Range(min=3, max=6),
|
||||||
room_actors=Range(min=1, max=3),
|
room_actors=Range(min=1, max=3),
|
||||||
|
|
|
@ -110,4 +110,4 @@ class WorldState(BaseModel):
|
||||||
type: Literal["world_state"] = "world_state"
|
type: Literal["world_state"] = "world_state"
|
||||||
|
|
||||||
|
|
||||||
WorldEntity = Room | Actor | Item
|
WorldEntity = Room | Actor | Item | Portal
|
||||||
|
|
|
@ -38,7 +38,8 @@ def graph_world(world: World, step: int):
|
||||||
graph_name = f"{path.basename(world.name)}-{step}"
|
graph_name = f"{path.basename(world.name)}-{step}"
|
||||||
graph = graphviz.Digraph(graph_name, format="png")
|
graph = graphviz.Digraph(graph_name, format="png")
|
||||||
for room in world.rooms:
|
for room in world.rooms:
|
||||||
room_label = "\n".join([room.name, *[actor.name for actor in room.actors]])
|
actors = [actor.name for actor in room.actors]
|
||||||
|
room_label = "\n".join([room.name, *actors])
|
||||||
graph.node(room.name, room_label)
|
graph.node(room.name, room_label)
|
||||||
for portal in room.portals:
|
for portal in room.portals:
|
||||||
graph.edge(room.name, portal.destination, label=portal.name)
|
graph.edge(room.name, portal.destination, label=portal.name)
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
from adventure.models.entity import Actor, Item, Room, World
|
from typing import Any, Generator
|
||||||
|
|
||||||
|
from adventure.models.entity import Actor, Item, Portal, Room, World
|
||||||
|
|
||||||
|
from .string import normalize_name
|
||||||
|
|
||||||
|
|
||||||
def find_room(world: World, room_name: str) -> Room | None:
|
def find_room(world: World, room_name: str) -> Room | None:
|
||||||
for room in world.rooms:
|
for room in world.rooms:
|
||||||
if room.name.lower() == room_name.lower():
|
if normalize_name(room.name) == normalize_name(room_name):
|
||||||
return room
|
return room
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
@ -20,7 +24,7 @@ def find_actor(world: World, actor_name: str) -> Actor | None:
|
||||||
|
|
||||||
def find_actor_in_room(room: Room, actor_name: str) -> Actor | None:
|
def find_actor_in_room(room: Room, actor_name: str) -> Actor | None:
|
||||||
for actor in room.actors:
|
for actor in room.actors:
|
||||||
if actor.name.lower() == actor_name.lower():
|
if normalize_name(actor.name) == normalize_name(actor_name):
|
||||||
return actor
|
return actor
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
@ -46,7 +50,7 @@ def find_item_in_actor(
|
||||||
actor: Actor, item_name: str, include_item_inventory=False
|
actor: Actor, item_name: str, include_item_inventory=False
|
||||||
) -> Item | None:
|
) -> Item | None:
|
||||||
for item in actor.items:
|
for item in actor.items:
|
||||||
if item.name.lower() == item_name.lower():
|
if normalize_name(item.name) == normalize_name(item_name):
|
||||||
return item
|
return item
|
||||||
|
|
||||||
if include_item_inventory:
|
if include_item_inventory:
|
||||||
|
@ -61,7 +65,7 @@ def find_item_in_container(
|
||||||
container: Item, item_name: str, include_item_inventory=False
|
container: Item, item_name: str, include_item_inventory=False
|
||||||
) -> Item | None:
|
) -> Item | None:
|
||||||
for item in container.items:
|
for item in container.items:
|
||||||
if item.name.lower() == item_name.lower():
|
if normalize_name(item.name) == normalize_name(item_name):
|
||||||
return item
|
return item
|
||||||
|
|
||||||
if include_item_inventory:
|
if include_item_inventory:
|
||||||
|
@ -79,7 +83,7 @@ def find_item_in_room(
|
||||||
include_item_inventory=False,
|
include_item_inventory=False,
|
||||||
) -> Item | None:
|
) -> Item | None:
|
||||||
for item in room.items:
|
for item in room.items:
|
||||||
if item.name.lower() == item_name.lower():
|
if normalize_name(item.name) == normalize_name(item_name):
|
||||||
return item
|
return item
|
||||||
|
|
||||||
if include_item_inventory:
|
if include_item_inventory:
|
||||||
|
@ -94,3 +98,43 @@ def find_item_in_room(
|
||||||
return item
|
return item
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def list_rooms(world: World) -> Generator[Room, Any, None]:
|
||||||
|
for room in world.rooms:
|
||||||
|
yield room
|
||||||
|
|
||||||
|
|
||||||
|
def list_portals(world: World) -> Generator[Portal, Any, None]:
|
||||||
|
for room in world.rooms:
|
||||||
|
for portal in room.portals:
|
||||||
|
yield portal
|
||||||
|
|
||||||
|
|
||||||
|
def list_actors(world: World) -> Generator[Actor, Any, None]:
|
||||||
|
for room in world.rooms:
|
||||||
|
for actor in room.actors:
|
||||||
|
yield actor
|
||||||
|
|
||||||
|
|
||||||
|
def list_items(
|
||||||
|
world: World, include_actor_inventory=False, include_item_inventory=False
|
||||||
|
) -> 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:
|
||||||
|
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
|
||||||
|
|
24
config.yml
24
config.yml
|
@ -22,3 +22,27 @@ render:
|
||||||
steps:
|
steps:
|
||||||
min: 30
|
min: 30
|
||||||
max: 50
|
max: 50
|
||||||
|
server:
|
||||||
|
websocket:
|
||||||
|
host: 0.0.0.0
|
||||||
|
port: 8001
|
||||||
|
world:
|
||||||
|
size:
|
||||||
|
actor_items:
|
||||||
|
min: 0
|
||||||
|
max: 3
|
||||||
|
item_effects:
|
||||||
|
min: 0
|
||||||
|
max: 1
|
||||||
|
portals:
|
||||||
|
min: 1
|
||||||
|
max: 3
|
||||||
|
rooms:
|
||||||
|
min: 3
|
||||||
|
max: 6
|
||||||
|
room_actors:
|
||||||
|
min: 1
|
||||||
|
max: 3
|
||||||
|
room_items:
|
||||||
|
min: 0
|
||||||
|
max: 3
|
||||||
|
|
Loading…
Reference in New Issue