2024-05-02 11:56:57 +00:00
|
|
|
from logging import getLogger
|
|
|
|
from random import choice, randint
|
2024-05-19 00:48:18 +00:00
|
|
|
from typing import List, Tuple
|
2024-05-02 11:56:57 +00:00
|
|
|
|
|
|
|
from packit.agent import Agent
|
2024-05-04 22:17:56 +00:00
|
|
|
from packit.loops import loop_retry
|
2024-05-16 04:12:06 +00:00
|
|
|
from packit.utils import could_be_json
|
|
|
|
|
2024-05-19 04:30:17 +00:00
|
|
|
from adventure.context import broadcast, set_current_world
|
2024-05-16 04:12:06 +00:00
|
|
|
from adventure.game_system import GameSystem
|
2024-05-19 00:48:18 +00:00
|
|
|
from adventure.models.config import DEFAULT_CONFIG, WorldConfig
|
2024-05-16 04:12:06 +00:00
|
|
|
from adventure.models.entity import (
|
|
|
|
Actor,
|
|
|
|
Effect,
|
|
|
|
Item,
|
|
|
|
NumberAttributeEffect,
|
2024-05-19 00:48:18 +00:00
|
|
|
Portal,
|
2024-05-16 04:12:06 +00:00
|
|
|
Room,
|
|
|
|
StringAttributeEffect,
|
|
|
|
World,
|
|
|
|
WorldEntity,
|
|
|
|
)
|
2024-05-18 21:58:11 +00:00
|
|
|
from adventure.models.event import GenerateEvent
|
2024-05-19 04:30:17 +00:00
|
|
|
from adventure.utils import try_parse_float, try_parse_int
|
2024-05-19 18:09:52 +00:00
|
|
|
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
|
2024-05-02 11:56:57 +00:00
|
|
|
|
|
|
|
logger = getLogger(__name__)
|
|
|
|
|
2024-05-19 00:48:18 +00:00
|
|
|
world_config: WorldConfig = DEFAULT_CONFIG.world
|
2024-05-04 22:57:24 +00:00
|
|
|
|
2024-05-19 04:30:17 +00:00
|
|
|
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())
|
|
|
|
|
2024-05-02 11:56:57 +00:00
|
|
|
|
2024-05-13 04:33:47 +00:00
|
|
|
def duplicate_name_parser(existing_names: List[str]):
|
2024-05-16 04:12:06 +00:00
|
|
|
def name_parser(value: str, **kwargs):
|
2024-05-18 21:58:11 +00:00
|
|
|
logger.debug(f"validating generated name: {value}")
|
2024-05-16 04:12:06 +00:00
|
|
|
|
|
|
|
if value in existing_names:
|
|
|
|
raise ValueError(f'"{value}" has already been used.')
|
2024-05-14 01:08:19 +00:00
|
|
|
|
2024-05-16 04:12:06 +00:00
|
|
|
if could_be_json(value):
|
|
|
|
raise ValueError("The name cannot contain JSON or other commands.")
|
2024-05-13 04:33:47 +00:00
|
|
|
|
2024-05-16 04:12:06 +00:00
|
|
|
if '"' in value or ":" in value:
|
2024-05-13 04:33:47 +00:00
|
|
|
raise ValueError("The name cannot contain quotes or colons.")
|
|
|
|
|
2024-05-16 04:12:06 +00:00
|
|
|
if len(value) > 50:
|
2024-05-13 04:33:47 +00:00
|
|
|
raise ValueError("The name cannot be longer than 50 characters.")
|
|
|
|
|
2024-05-16 04:12:06 +00:00
|
|
|
return value
|
2024-05-13 04:33:47 +00:00
|
|
|
|
|
|
|
return name_parser
|
|
|
|
|
|
|
|
|
2024-05-18 21:58:11 +00:00
|
|
|
def broadcast_generated(
|
2024-05-13 04:33:47 +00:00
|
|
|
message: str | None = None,
|
|
|
|
entity: WorldEntity | None = None,
|
|
|
|
):
|
|
|
|
if message:
|
|
|
|
event = GenerateEvent.from_name(message)
|
|
|
|
elif entity:
|
|
|
|
event = GenerateEvent.from_entity(entity)
|
|
|
|
else:
|
|
|
|
raise ValueError("Either message or entity must be provided")
|
|
|
|
|
2024-05-18 21:58:11 +00:00
|
|
|
broadcast(event)
|
2024-05-13 04:33:47 +00:00
|
|
|
|
|
|
|
|
2024-05-19 04:30:17 +00:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
2024-05-04 20:35:42 +00:00
|
|
|
def generate_room(
|
2024-05-05 14:14:54 +00:00
|
|
|
agent: Agent,
|
2024-05-19 04:30:17 +00:00
|
|
|
world: World,
|
|
|
|
systems: List[GameSystem],
|
2024-05-04 20:35:42 +00:00
|
|
|
) -> Room:
|
2024-05-19 04:30:17 +00:00
|
|
|
existing_rooms = [room.name for room in list_rooms(world)]
|
|
|
|
|
2024-05-04 22:17:56 +00:00
|
|
|
name = loop_retry(
|
|
|
|
agent,
|
2024-05-02 11:56:57 +00:00
|
|
|
"Generate one room, area, or location that would make sense in the world of {world_theme}. "
|
2024-05-08 01:39:58 +00:00
|
|
|
"Only respond with the room name in title case, do not include the description or any other text. "
|
2024-05-02 11:56:57 +00:00
|
|
|
'Do not prefix the name with "the", do not wrap it in quotes. The existing rooms are: {existing_rooms}',
|
2024-05-04 22:17:56 +00:00
|
|
|
context={
|
2024-05-19 04:30:17 +00:00
|
|
|
"world_theme": world.theme,
|
2024-05-04 22:17:56 +00:00
|
|
|
"existing_rooms": existing_rooms,
|
|
|
|
},
|
2024-05-13 04:33:47 +00:00
|
|
|
result_parser=duplicate_name_parser(existing_rooms),
|
2024-05-19 19:06:09 +00:00
|
|
|
toolbox=None,
|
2024-05-02 11:56:57 +00:00
|
|
|
)
|
2024-05-05 14:14:54 +00:00
|
|
|
|
2024-05-18 21:58:11 +00:00
|
|
|
broadcast_generated(message=f"Generating room: {name}")
|
2024-05-02 11:56:57 +00:00
|
|
|
desc = agent(
|
|
|
|
"Generate a detailed description of the {name} area. What does it look like? "
|
|
|
|
"What does it smell like? What can be seen or heard?",
|
|
|
|
name=name,
|
|
|
|
)
|
|
|
|
|
2024-05-19 00:48:18 +00:00
|
|
|
actions = {}
|
2024-05-19 04:30:17 +00:00
|
|
|
room = Room(name=name, description=desc, items=[], actors=[], actions=actions)
|
2024-05-19 00:48:18 +00:00
|
|
|
|
|
|
|
item_count = randint(
|
|
|
|
world_config.size.room_items.min, world_config.size.room_items.max
|
|
|
|
)
|
|
|
|
broadcast_generated(f"Generating {item_count} items for room: {name}")
|
|
|
|
|
2024-05-19 04:30:17 +00:00
|
|
|
for _ in range(item_count):
|
2024-05-19 00:48:18 +00:00
|
|
|
try:
|
|
|
|
item = generate_item(
|
|
|
|
agent,
|
2024-05-19 04:30:17 +00:00
|
|
|
world,
|
|
|
|
systems=systems,
|
|
|
|
dest_room=room,
|
2024-05-19 00:48:18 +00:00
|
|
|
)
|
|
|
|
broadcast_generated(entity=item)
|
|
|
|
|
2024-05-19 04:30:17 +00:00
|
|
|
room.items.append(item)
|
2024-05-19 00:48:18 +00:00
|
|
|
except Exception:
|
|
|
|
logger.exception("error generating item")
|
|
|
|
|
|
|
|
actor_count = randint(
|
|
|
|
world_config.size.room_actors.min, world_config.size.room_actors.max
|
|
|
|
)
|
|
|
|
broadcast_generated(message=f"Generating {actor_count} actors for room: {name}")
|
|
|
|
|
2024-05-19 04:30:17 +00:00
|
|
|
for _ in range(actor_count):
|
2024-05-19 00:48:18 +00:00
|
|
|
try:
|
|
|
|
actor = generate_actor(
|
|
|
|
agent,
|
2024-05-19 04:30:17 +00:00
|
|
|
world,
|
|
|
|
systems=systems,
|
|
|
|
dest_room=room,
|
2024-05-19 00:48:18 +00:00
|
|
|
)
|
|
|
|
broadcast_generated(entity=actor)
|
|
|
|
|
2024-05-19 04:30:17 +00:00
|
|
|
room.actors.append(actor)
|
2024-05-19 00:48:18 +00:00
|
|
|
except Exception:
|
|
|
|
logger.exception("error generating actor")
|
|
|
|
continue
|
2024-05-02 11:56:57 +00:00
|
|
|
|
2024-05-19 04:30:17 +00:00
|
|
|
return room
|
2024-05-02 11:56:57 +00:00
|
|
|
|
|
|
|
|
2024-05-19 00:48:18 +00:00
|
|
|
def generate_portals(
|
|
|
|
agent: Agent,
|
2024-05-19 04:30:17 +00:00
|
|
|
world: World,
|
2024-05-19 00:48:18 +00:00
|
|
|
source_room: Room,
|
|
|
|
dest_room: Room,
|
2024-05-19 04:30:17 +00:00
|
|
|
systems: List[GameSystem],
|
2024-05-19 00:48:18 +00:00
|
|
|
) -> Tuple[Portal, Portal]:
|
|
|
|
existing_source_portals = [portal.name for portal in source_room.portals]
|
|
|
|
existing_dest_portals = [portal.name for portal in dest_room.portals]
|
|
|
|
|
|
|
|
outgoing_name = loop_retry(
|
|
|
|
agent,
|
|
|
|
"Generate the name of a portal that leads from the {source_room} room to the {dest_room} room and fits the world theme of {world_theme}. "
|
|
|
|
"Some example portal names are: 'door', 'gate', 'archway', 'staircase', 'trapdoor', 'mirror', and 'magic circle'. "
|
|
|
|
"Only respond with the portal name in title case, do not include a description or any other text. "
|
|
|
|
'Do not prefix the name with "the", do not wrap it in quotes. Use a unique name. '
|
|
|
|
"Do not create any duplicate portals in the same room. The existing portals are: {existing_portals}",
|
|
|
|
context={
|
|
|
|
"source_room": source_room.name,
|
|
|
|
"dest_room": dest_room.name,
|
|
|
|
"existing_portals": existing_source_portals,
|
2024-05-19 04:30:17 +00:00
|
|
|
"world_theme": world.theme,
|
2024-05-19 00:48:18 +00:00
|
|
|
},
|
|
|
|
result_parser=duplicate_name_parser(existing_source_portals),
|
2024-05-19 19:06:09 +00:00
|
|
|
toolbox=None,
|
2024-05-19 00:48:18 +00:00
|
|
|
)
|
|
|
|
broadcast_generated(message=f"Generating portal: {outgoing_name}")
|
|
|
|
|
|
|
|
incoming_name = loop_retry(
|
|
|
|
agent,
|
|
|
|
"Generate the opposite name of the portal that leads from the {dest_room} room to the {source_room} room. "
|
|
|
|
"The name should be the opposite of the {outgoing_name} portal and should fit the world theme of {world_theme}. "
|
|
|
|
"Some example portal names are: 'door', 'gate', 'archway', 'staircase', 'trapdoor', 'mirror', and 'magic circle'. "
|
|
|
|
"Only respond with the portal name in title case, do not include a description or any other text. "
|
|
|
|
'Do not prefix the name with "the", do not wrap it in quotes. Use a unique name. '
|
|
|
|
"Do not create any duplicate portals in the same room. The existing portals are: {existing_portals}",
|
|
|
|
context={
|
|
|
|
"source_room": source_room.name,
|
|
|
|
"dest_room": dest_room.name,
|
|
|
|
"existing_portals": existing_dest_portals,
|
2024-05-19 04:30:17 +00:00
|
|
|
"world_theme": world.theme,
|
2024-05-19 00:48:18 +00:00
|
|
|
"outgoing_name": outgoing_name,
|
|
|
|
},
|
|
|
|
result_parser=duplicate_name_parser(existing_dest_portals),
|
2024-05-19 19:06:09 +00:00
|
|
|
toolbox=None,
|
2024-05-19 00:48:18 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
broadcast_generated(message=f"Linking {outgoing_name} to {incoming_name}")
|
|
|
|
|
|
|
|
outgoing_portal = Portal(
|
|
|
|
name=outgoing_name,
|
|
|
|
description=f"A {outgoing_name} leads to the {dest_room.name} room.",
|
|
|
|
destination=dest_room.name,
|
|
|
|
)
|
2024-05-19 04:30:17 +00:00
|
|
|
generate_system_attributes(agent, world, outgoing_portal, systems)
|
|
|
|
|
2024-05-19 00:48:18 +00:00
|
|
|
incoming_portal = Portal(
|
|
|
|
name=incoming_name,
|
|
|
|
description=f"A {incoming_name} leads to the {source_room.name} room.",
|
|
|
|
destination=source_room.name,
|
|
|
|
)
|
2024-05-19 04:30:17 +00:00
|
|
|
generate_system_attributes(agent, world, incoming_portal, systems)
|
2024-05-19 00:48:18 +00:00
|
|
|
|
|
|
|
return (outgoing_portal, incoming_portal)
|
|
|
|
|
|
|
|
|
2024-05-02 11:56:57 +00:00
|
|
|
def generate_item(
|
|
|
|
agent: Agent,
|
2024-05-19 04:30:17 +00:00
|
|
|
world: World,
|
|
|
|
systems: List[GameSystem],
|
|
|
|
dest_room: Room | None = None,
|
|
|
|
dest_actor: Actor | None = None,
|
2024-05-02 11:56:57 +00:00
|
|
|
) -> Item:
|
2024-05-19 04:30:17 +00:00
|
|
|
existing_items = [
|
|
|
|
item.name
|
|
|
|
for item in list_items(
|
|
|
|
world, include_actor_inventory=True, include_item_inventory=True
|
|
|
|
)
|
|
|
|
]
|
2024-05-19 18:09:52 +00:00
|
|
|
|
2024-05-02 11:56:57 +00:00
|
|
|
if dest_actor:
|
2024-05-19 04:30:17 +00:00
|
|
|
dest_note = f"The item will be held by the {dest_actor.name} character"
|
2024-05-19 18:09:52 +00:00
|
|
|
existing_items += [item.name for item in list_items_in_actor(dest_actor)]
|
2024-05-02 11:56:57 +00:00
|
|
|
elif dest_room:
|
2024-05-19 04:30:17 +00:00
|
|
|
dest_note = f"The item will be placed in the {dest_room.name} room"
|
2024-05-19 18:09:52 +00:00
|
|
|
existing_items += [item.name for item in list_items_in_room(dest_room)]
|
2024-05-02 11:56:57 +00:00
|
|
|
else:
|
|
|
|
dest_note = "The item will be placed in the world"
|
|
|
|
|
2024-05-04 22:17:56 +00:00
|
|
|
name = loop_retry(
|
|
|
|
agent,
|
2024-05-02 11:56:57 +00:00
|
|
|
"Generate one item or object that would make sense in the world of {world_theme}. {dest_note}. "
|
2024-05-08 01:39:58 +00:00
|
|
|
"Only respond with the item name in title case, do not include a description or any other text. Do not prefix the "
|
|
|
|
'name with "the", do not wrap it in quotes. Do not include the name of the room. Use a unique name. '
|
|
|
|
"Do not create any duplicate items in the same room. Do not give characters any duplicate items. "
|
|
|
|
"The existing items are: {existing_items}",
|
2024-05-04 22:17:56 +00:00
|
|
|
context={
|
|
|
|
"dest_note": dest_note,
|
|
|
|
"existing_items": existing_items,
|
2024-05-19 04:30:17 +00:00
|
|
|
"world_theme": world.theme,
|
2024-05-04 22:17:56 +00:00
|
|
|
},
|
2024-05-13 04:33:47 +00:00
|
|
|
result_parser=duplicate_name_parser(existing_items),
|
2024-05-19 19:06:09 +00:00
|
|
|
toolbox=None,
|
2024-05-02 11:56:57 +00:00
|
|
|
)
|
2024-05-05 14:14:54 +00:00
|
|
|
|
2024-05-18 21:58:11 +00:00
|
|
|
broadcast_generated(message=f"Generating item: {name}")
|
2024-05-02 11:56:57 +00:00
|
|
|
desc = agent(
|
|
|
|
"Generate a detailed description of the {name} item. What does it look like? What is it made of? What does it do?",
|
|
|
|
name=name,
|
|
|
|
)
|
|
|
|
|
|
|
|
actions = {}
|
2024-05-16 04:12:06 +00:00
|
|
|
item = Item(name=name, description=desc, actions=actions)
|
2024-05-19 04:30:17 +00:00
|
|
|
generate_system_attributes(agent, world, item, systems)
|
2024-05-02 11:56:57 +00:00
|
|
|
|
2024-05-19 00:48:18 +00:00
|
|
|
effect_count = randint(
|
|
|
|
world_config.size.item_effects.min, world_config.size.item_effects.max
|
|
|
|
)
|
2024-05-18 21:58:11 +00:00
|
|
|
broadcast_generated(message=f"Generating {effect_count} effects for item: {name}")
|
2024-05-16 04:12:06 +00:00
|
|
|
|
2024-05-19 04:30:17 +00:00
|
|
|
for _ in range(effect_count):
|
2024-05-16 04:12:06 +00:00
|
|
|
try:
|
2024-05-19 04:30:17 +00:00
|
|
|
effect = generate_effect(agent, world, entity=item)
|
|
|
|
item.effects.append(effect)
|
2024-05-16 04:12:06 +00:00
|
|
|
except Exception:
|
|
|
|
logger.exception("error generating effect")
|
|
|
|
|
|
|
|
return item
|
2024-05-02 11:56:57 +00:00
|
|
|
|
|
|
|
|
|
|
|
def generate_actor(
|
2024-05-05 14:14:54 +00:00
|
|
|
agent: Agent,
|
2024-05-19 04:30:17 +00:00
|
|
|
world: World,
|
|
|
|
systems: List[GameSystem],
|
|
|
|
dest_room: Room,
|
2024-05-02 11:56:57 +00:00
|
|
|
) -> Actor:
|
2024-05-19 18:09:52 +00:00
|
|
|
existing_actors = [actor.name for actor in list_actors(world)] + [
|
|
|
|
actor.name for actor in list_actors_in_room(dest_room)
|
|
|
|
]
|
|
|
|
|
2024-05-04 22:17:56 +00:00
|
|
|
name = loop_retry(
|
|
|
|
agent,
|
2024-05-08 01:39:58 +00:00
|
|
|
"Generate one person or creature that would make sense in the world of {world_theme}. "
|
|
|
|
"The character will be placed in the {dest_room} room. "
|
|
|
|
"Only respond with the character name in title case, do not include a description or any other text. "
|
|
|
|
'Do not prefix the name with "the", do not wrap it in quotes. '
|
2024-05-04 04:18:21 +00:00
|
|
|
"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}",
|
2024-05-04 22:17:56 +00:00
|
|
|
context={
|
2024-05-19 04:30:17 +00:00
|
|
|
"dest_room": dest_room.name,
|
2024-05-04 22:17:56 +00:00
|
|
|
"existing_actors": existing_actors,
|
2024-05-19 04:30:17 +00:00
|
|
|
"world_theme": world.theme,
|
2024-05-04 22:17:56 +00:00
|
|
|
},
|
2024-05-13 04:33:47 +00:00
|
|
|
result_parser=duplicate_name_parser(existing_actors),
|
2024-05-19 19:06:09 +00:00
|
|
|
toolbox=None,
|
2024-05-02 11:56:57 +00:00
|
|
|
)
|
2024-05-05 14:14:54 +00:00
|
|
|
|
2024-05-18 21:58:11 +00:00
|
|
|
broadcast_generated(message=f"Generating actor: {name}")
|
2024-05-02 11:56:57 +00:00
|
|
|
description = agent(
|
|
|
|
"Generate a detailed description of the {name} character. What do they look like? What are they wearing? "
|
2024-05-03 01:57:11 +00:00
|
|
|
"What are they doing? Describe their appearance from the perspective of an outside observer."
|
|
|
|
"Do not include the room or any other characters in the description, because they will move around.",
|
2024-05-02 11:56:57 +00:00
|
|
|
name=name,
|
|
|
|
)
|
|
|
|
backstory = agent(
|
|
|
|
"Generate a backstory for the {name} actor. Where are they from? What are they doing here? What are their "
|
|
|
|
'goals? Make sure to phrase the backstory in the second person, starting with "you are" and speaking directly to {name}.',
|
|
|
|
name=name,
|
|
|
|
)
|
|
|
|
|
2024-05-19 04:30:17 +00:00
|
|
|
actor = Actor(
|
|
|
|
name=name, backstory=backstory, description=description, actions={}, items=[]
|
|
|
|
)
|
|
|
|
generate_system_attributes(agent, world, actor, systems)
|
|
|
|
|
2024-05-19 00:48:18 +00:00
|
|
|
# generate the actor's inventory
|
|
|
|
item_count = randint(
|
|
|
|
world_config.size.actor_items.min, world_config.size.actor_items.max
|
|
|
|
)
|
|
|
|
broadcast_generated(f"Generating {item_count} items for actor {name}")
|
|
|
|
|
|
|
|
for k in range(item_count):
|
|
|
|
try:
|
|
|
|
item = generate_item(
|
|
|
|
agent,
|
2024-05-19 04:30:17 +00:00
|
|
|
world,
|
|
|
|
systems,
|
|
|
|
dest_actor=actor,
|
2024-05-19 00:48:18 +00:00
|
|
|
)
|
2024-05-19 04:30:17 +00:00
|
|
|
generate_system_attributes(agent, world, item, systems)
|
2024-05-19 00:48:18 +00:00
|
|
|
broadcast_generated(entity=item)
|
|
|
|
|
2024-05-19 04:30:17 +00:00
|
|
|
actor.items.append(item)
|
2024-05-19 00:48:18 +00:00
|
|
|
except Exception:
|
|
|
|
logger.exception("error generating item")
|
|
|
|
|
2024-05-19 04:30:17 +00:00
|
|
|
return actor
|
2024-05-19 00:48:18 +00:00
|
|
|
|
2024-05-16 04:12:06 +00:00
|
|
|
|
2024-05-19 04:30:17 +00:00
|
|
|
def generate_effect(agent: Agent, world: World, entity: Item) -> Effect:
|
2024-05-19 00:48:18 +00:00
|
|
|
entity_type = entity.type
|
2024-05-19 04:30:17 +00:00
|
|
|
existing_effects = [effect.name for effect in entity.effects]
|
2024-05-16 04:12:06 +00:00
|
|
|
|
|
|
|
name = loop_retry(
|
|
|
|
agent,
|
2024-05-18 21:20:47 +00:00
|
|
|
"Generate one effect for an {entity_type} named {entity_name} that would make sense in the world of {theme}. "
|
2024-05-16 04:12:06 +00:00
|
|
|
"Only respond with the effect name in title case, do not include a description or any other text. "
|
|
|
|
'Do not prefix the name with "the", do not wrap it in quotes. Use a unique name. '
|
|
|
|
"Do not create any duplicate effects on the same item. The existing effects are: {existing_effects}. "
|
|
|
|
"Some example effects are: 'fire', 'poison', 'frost', 'haste', 'slow', and 'heal'.",
|
|
|
|
context={
|
2024-05-18 21:20:47 +00:00
|
|
|
"entity_name": entity.name,
|
2024-05-16 04:12:06 +00:00
|
|
|
"entity_type": entity_type,
|
|
|
|
"existing_effects": existing_effects,
|
2024-05-19 04:30:17 +00:00
|
|
|
"theme": world.theme,
|
2024-05-16 04:12:06 +00:00
|
|
|
},
|
|
|
|
result_parser=duplicate_name_parser(existing_effects),
|
2024-05-19 19:06:09 +00:00
|
|
|
toolbox=None,
|
2024-05-16 04:12:06 +00:00
|
|
|
)
|
2024-05-18 21:58:11 +00:00
|
|
|
broadcast_generated(message=f"Generating effect: {name}")
|
2024-05-16 04:12:06 +00:00
|
|
|
|
|
|
|
description = agent(
|
|
|
|
"Generate a detailed description of the {name} effect. What does it look like? What does it do? "
|
|
|
|
"How does it affect the target? Describe the effect from the perspective of an outside observer.",
|
|
|
|
name=name,
|
|
|
|
)
|
|
|
|
|
|
|
|
attribute_names = agent(
|
2024-05-19 04:30:17 +00:00
|
|
|
"Generate a short list of attributes that the {name} effect modifies. Include 1 to 3 attributes. "
|
2024-05-16 04:12:06 +00:00
|
|
|
"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'. "
|
|
|
|
"Only include the attribute names, do not include the question or any JSON.",
|
|
|
|
name=name,
|
|
|
|
)
|
|
|
|
|
2024-05-19 04:30:17 +00:00
|
|
|
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
|
|
|
|
|
2024-05-16 04:12:06 +00:00
|
|
|
attributes = []
|
|
|
|
for attribute_name in attribute_names.split(","):
|
2024-05-19 18:09:52 +00:00
|
|
|
attribute_name = normalize_name(attribute_name)
|
2024-05-16 04:12:06 +00:00
|
|
|
if attribute_name:
|
2024-05-19 04:30:17 +00:00
|
|
|
operation = loop_retry(
|
|
|
|
agent,
|
2024-05-16 04:12:06 +00:00
|
|
|
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."
|
|
|
|
"Another example is 'writing' might 'set' the 'text' attribute, while 'break' might 'set' the 'condition' attribute."
|
2024-05-19 04:30:17 +00:00
|
|
|
"Reply with the operation only, without any other text. Respond with a single word for the list of operations."
|
2024-05-16 04:12:06 +00:00
|
|
|
"Choose from the following operations: {operations}",
|
2024-05-19 04:30:17 +00:00
|
|
|
context={
|
|
|
|
"name": name,
|
|
|
|
"attribute_name": attribute_name,
|
|
|
|
"operations": OPERATIONS,
|
|
|
|
},
|
|
|
|
result_parser=operation_parser,
|
2024-05-19 19:06:09 +00:00
|
|
|
toolbox=None,
|
2024-05-16 04:12:06 +00:00
|
|
|
)
|
2024-05-19 00:48:18 +00:00
|
|
|
|
|
|
|
operation_type = PROMPT_OPERATION_TYPES[operation]
|
|
|
|
operation_prompt = PROMPT_TYPE_FRAGMENTS[operation_type]
|
|
|
|
|
2024-05-16 04:12:06 +00:00
|
|
|
value = agent(
|
|
|
|
f"How much does the {name} effect modify the {attribute_name} attribute? "
|
2024-05-18 21:20:47 +00:00
|
|
|
"For example, heal might add '10' to the health attribute, while poison might subtract '5' from it."
|
2024-05-19 00:48:18 +00:00
|
|
|
f"{operation_prompt}. Do not include any other text. Do not use JSON.",
|
2024-05-16 04:12:06 +00:00
|
|
|
name=name,
|
|
|
|
attribute_name=attribute_name,
|
|
|
|
)
|
|
|
|
value = value.strip()
|
2024-05-19 00:48:18 +00:00
|
|
|
|
|
|
|
int_value = try_parse_int(value)
|
|
|
|
if int_value is not None:
|
2024-05-16 04:12:06 +00:00
|
|
|
attribute_effect = NumberAttributeEffect(
|
2024-05-19 00:48:18 +00:00
|
|
|
name=attribute_name, operation=operation, value=int_value
|
2024-05-16 04:12:06 +00:00
|
|
|
)
|
|
|
|
else:
|
2024-05-19 00:48:18 +00:00
|
|
|
float_value = try_parse_float(value)
|
|
|
|
if float_value is not None:
|
|
|
|
attribute_effect = NumberAttributeEffect(
|
|
|
|
name=attribute_name, operation=operation, value=float_value
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
attribute_effect = StringAttributeEffect(
|
|
|
|
name=attribute_name, operation=operation, value=value
|
|
|
|
)
|
2024-05-16 04:12:06 +00:00
|
|
|
|
|
|
|
attributes.append(attribute_effect)
|
|
|
|
|
2024-05-18 21:20:47 +00:00
|
|
|
return Effect(name=name, description=description, attributes=attributes)
|
2024-05-16 04:12:06 +00:00
|
|
|
|
|
|
|
|
2024-05-19 18:09:52 +00:00
|
|
|
def link_rooms(
|
2024-05-04 04:18:21 +00:00
|
|
|
agent: Agent,
|
2024-05-19 18:09:52 +00:00
|
|
|
world: World,
|
2024-05-19 04:30:17 +00:00
|
|
|
systems: List[GameSystem],
|
2024-05-19 18:09:52 +00:00
|
|
|
rooms: List[Room] | None = None,
|
|
|
|
) -> None:
|
|
|
|
rooms = rooms or world.rooms
|
2024-05-02 11:56:57 +00:00
|
|
|
|
2024-05-19 18:09:52 +00:00
|
|
|
for room in rooms:
|
2024-05-19 00:48:18 +00:00
|
|
|
num_portals = randint(
|
|
|
|
world_config.size.portals.min, world_config.size.portals.max
|
|
|
|
)
|
2024-05-12 20:47:18 +00:00
|
|
|
|
2024-05-19 00:48:18 +00:00
|
|
|
if len(room.portals) >= num_portals:
|
|
|
|
logger.info(f"room {room.name} already has enough portals")
|
|
|
|
continue
|
2024-05-02 11:56:57 +00:00
|
|
|
|
2024-05-18 21:58:11 +00:00
|
|
|
broadcast_generated(
|
2024-05-19 00:48:18 +00:00
|
|
|
message=f"Generating {num_portals} portals for room: {room.name}"
|
2024-05-12 20:47:18 +00:00
|
|
|
)
|
2024-05-04 20:35:42 +00:00
|
|
|
|
2024-05-19 04:30:17 +00:00
|
|
|
for _ in range(num_portals):
|
2024-05-19 00:48:18 +00:00
|
|
|
previous_destinations = [portal.destination for portal in room.portals] + [
|
|
|
|
room.name
|
|
|
|
]
|
2024-05-19 04:30:17 +00:00
|
|
|
remaining_rooms = [
|
|
|
|
r for r in world.rooms if r.name not in previous_destinations
|
|
|
|
]
|
2024-05-19 00:48:18 +00:00
|
|
|
if len(remaining_rooms) == 0:
|
|
|
|
logger.info(f"no more rooms to link to from {room.name}")
|
|
|
|
break
|
|
|
|
|
|
|
|
# TODO: prompt the DM to choose a destination room
|
|
|
|
dest_room = choice(
|
2024-05-19 04:30:17 +00:00
|
|
|
[r for r in world.rooms if r.name not in previous_destinations]
|
2024-05-19 00:48:18 +00:00
|
|
|
)
|
|
|
|
|
2024-05-13 04:33:47 +00:00
|
|
|
try:
|
2024-05-19 00:48:18 +00:00
|
|
|
outgoing_portal, incoming_portal = generate_portals(
|
2024-05-19 04:30:17 +00:00
|
|
|
agent, world, room, dest_room, systems
|
2024-05-02 11:56:57 +00:00
|
|
|
)
|
2024-05-12 20:47:18 +00:00
|
|
|
|
2024-05-19 00:48:18 +00:00
|
|
|
room.portals.append(outgoing_portal)
|
|
|
|
dest_room.portals.append(incoming_portal)
|
2024-05-13 04:33:47 +00:00
|
|
|
except Exception:
|
2024-05-19 00:48:18 +00:00
|
|
|
logger.exception("error generating portal")
|
2024-05-02 11:56:57 +00:00
|
|
|
continue
|
|
|
|
|
2024-05-19 18:09:52 +00:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2024-05-04 04:18:21 +00:00
|
|
|
# ensure actors act in a stable order
|
2024-05-19 04:30:17 +00:00
|
|
|
world.order = [actor.name for room in world.rooms for actor in room.actors]
|
|
|
|
return world
|