use DM to generate portals between rooms
This commit is contained in:
parent
ea5ac0cd10
commit
71d2be85f1
|
@ -68,19 +68,22 @@ 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):
|
||||||
destination_name = action_room.portals.get(direction.lower())
|
portal = next(
|
||||||
if not destination_name:
|
(p for p in action_room.portals if p.name.lower() == direction.lower()),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if not portal:
|
||||||
return f"You cannot move {direction} from here."
|
return f"You cannot move {direction} from here."
|
||||||
|
|
||||||
destination_room = find_room(action_world, destination_name)
|
destination_room = find_room(action_world, portal.destination)
|
||||||
if not destination_room:
|
if not destination_room:
|
||||||
return f"The {destination_name} room does not exist."
|
return f"The {portal.destination} room does not exist."
|
||||||
|
|
||||||
broadcast(f"{action_actor.name} moves {direction} to {destination_name}")
|
broadcast(f"{action_actor.name} moves {direction} to {destination_room.name}")
|
||||||
action_room.actors.remove(action_actor)
|
action_room.actors.remove(action_actor)
|
||||||
destination_room.actors.append(action_actor)
|
destination_room.actors.append(action_actor)
|
||||||
|
|
||||||
return f"You move {direction} and arrive at {destination_name}."
|
return f"You move {direction} and arrive at {destination_room.name}."
|
||||||
|
|
||||||
|
|
||||||
def action_take(item_name: str) -> str:
|
def action_take(item_name: str) -> str:
|
||||||
|
|
|
@ -12,7 +12,7 @@ from adventure.context import (
|
||||||
set_dungeon_master,
|
set_dungeon_master,
|
||||||
world_context,
|
world_context,
|
||||||
)
|
)
|
||||||
from adventure.generate import OPPOSITE_DIRECTIONS, generate_item, generate_room
|
from adventure.generate import generate_item, generate_room
|
||||||
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
|
||||||
|
@ -55,8 +55,7 @@ def action_explore(direction: str) -> str:
|
||||||
action_world.rooms.append(new_room)
|
action_world.rooms.append(new_room)
|
||||||
|
|
||||||
# link the rooms together
|
# link the rooms together
|
||||||
action_room.portals[direction] = new_room.name
|
# TODO: generate portals
|
||||||
new_room.portals[OPPOSITE_DIRECTIONS[direction]] = action_room.name
|
|
||||||
|
|
||||||
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}"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from random import choice, randint
|
from random import choice, randint
|
||||||
from typing import List
|
from typing import List, Tuple
|
||||||
|
|
||||||
from packit.agent import Agent
|
from packit.agent import Agent
|
||||||
from packit.loops import loop_retry
|
from packit.loops import loop_retry
|
||||||
|
@ -8,11 +8,13 @@ from packit.utils import could_be_json
|
||||||
|
|
||||||
from adventure.context import broadcast
|
from adventure.context import broadcast
|
||||||
from adventure.game_system import GameSystem
|
from adventure.game_system import GameSystem
|
||||||
|
from adventure.models.config import DEFAULT_CONFIG, WorldConfig
|
||||||
from adventure.models.entity import (
|
from adventure.models.entity import (
|
||||||
Actor,
|
Actor,
|
||||||
Effect,
|
Effect,
|
||||||
Item,
|
Item,
|
||||||
NumberAttributeEffect,
|
NumberAttributeEffect,
|
||||||
|
Portal,
|
||||||
Room,
|
Room,
|
||||||
StringAttributeEffect,
|
StringAttributeEffect,
|
||||||
World,
|
World,
|
||||||
|
@ -22,12 +24,7 @@ from adventure.models.event import GenerateEvent
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
OPPOSITE_DIRECTIONS = {
|
world_config: WorldConfig = DEFAULT_CONFIG.world
|
||||||
"north": "south",
|
|
||||||
"south": "north",
|
|
||||||
"east": "west",
|
|
||||||
"west": "east",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def duplicate_name_parser(existing_names: List[str]):
|
def duplicate_name_parser(existing_names: List[str]):
|
||||||
|
@ -69,6 +66,7 @@ def generate_room(
|
||||||
agent: Agent,
|
agent: Agent,
|
||||||
world_theme: str,
|
world_theme: str,
|
||||||
existing_rooms: List[str] = [],
|
existing_rooms: List[str] = [],
|
||||||
|
systems: List[GameSystem] = [],
|
||||||
) -> Room:
|
) -> Room:
|
||||||
name = loop_retry(
|
name = loop_retry(
|
||||||
agent,
|
agent,
|
||||||
|
@ -89,15 +87,120 @@ def generate_room(
|
||||||
name=name,
|
name=name,
|
||||||
)
|
)
|
||||||
|
|
||||||
items = []
|
|
||||||
actors = []
|
|
||||||
actions = {}
|
actions = {}
|
||||||
|
|
||||||
|
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}")
|
||||||
|
|
||||||
|
items = []
|
||||||
|
for j in range(item_count):
|
||||||
|
existing_items = [item.name for item in items]
|
||||||
|
|
||||||
|
try:
|
||||||
|
item = generate_item(
|
||||||
|
agent,
|
||||||
|
world_theme,
|
||||||
|
dest_room=name,
|
||||||
|
existing_items=existing_items,
|
||||||
|
)
|
||||||
|
generate_system_attributes(agent, world_theme, item, systems)
|
||||||
|
broadcast_generated(entity=item)
|
||||||
|
|
||||||
|
items.append(item)
|
||||||
|
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}")
|
||||||
|
|
||||||
|
actors = []
|
||||||
|
for j in range(actor_count):
|
||||||
|
existing_actors = [actor.name for actor in actors]
|
||||||
|
|
||||||
|
try:
|
||||||
|
actor = generate_actor(
|
||||||
|
agent,
|
||||||
|
world_theme,
|
||||||
|
dest_room=name,
|
||||||
|
existing_actors=existing_actors,
|
||||||
|
)
|
||||||
|
generate_system_attributes(agent, world_theme, actor, systems)
|
||||||
|
broadcast_generated(entity=actor)
|
||||||
|
|
||||||
|
actors.append(actor)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("error generating actor")
|
||||||
|
continue
|
||||||
|
|
||||||
return Room(
|
return Room(
|
||||||
name=name, description=desc, items=items, actors=actors, actions=actions
|
name=name, description=desc, items=items, actors=actors, actions=actions
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_portals(
|
||||||
|
agent: Agent,
|
||||||
|
world_theme: str,
|
||||||
|
source_room: Room,
|
||||||
|
dest_room: Room,
|
||||||
|
) -> 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,
|
||||||
|
"world_theme": world_theme,
|
||||||
|
},
|
||||||
|
result_parser=duplicate_name_parser(existing_source_portals),
|
||||||
|
)
|
||||||
|
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,
|
||||||
|
"world_theme": world_theme,
|
||||||
|
"outgoing_name": outgoing_name,
|
||||||
|
},
|
||||||
|
result_parser=duplicate_name_parser(existing_dest_portals),
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
incoming_portal = Portal(
|
||||||
|
name=incoming_name,
|
||||||
|
description=f"A {incoming_name} leads to the {source_room.name} room.",
|
||||||
|
destination=source_room.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
return (outgoing_portal, incoming_portal)
|
||||||
|
|
||||||
|
|
||||||
def generate_item(
|
def generate_item(
|
||||||
agent: Agent,
|
agent: Agent,
|
||||||
world_theme: str,
|
world_theme: str,
|
||||||
|
@ -136,13 +239,19 @@ def generate_item(
|
||||||
actions = {}
|
actions = {}
|
||||||
item = Item(name=name, description=desc, actions=actions)
|
item = Item(name=name, description=desc, actions=actions)
|
||||||
|
|
||||||
effect_count = randint(1, 2)
|
effect_count = randint(
|
||||||
|
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 = []
|
effects = []
|
||||||
for i in range(effect_count):
|
for i in range(effect_count):
|
||||||
|
existing_effects = [effect.name for effect in effects]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
effect = generate_effect(agent, world_theme, entity=item)
|
effect = generate_effect(
|
||||||
|
agent, world_theme, entity=item, existing_effects=existing_effects
|
||||||
|
)
|
||||||
effects.append(effect)
|
effects.append(effect)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("error generating effect")
|
logger.exception("error generating effect")
|
||||||
|
@ -156,6 +265,7 @@ def generate_actor(
|
||||||
world_theme: str,
|
world_theme: str,
|
||||||
dest_room: str,
|
dest_room: str,
|
||||||
existing_actors: List[str] = [],
|
existing_actors: List[str] = [],
|
||||||
|
systems: List[GameSystem] = [],
|
||||||
) -> Actor:
|
) -> Actor:
|
||||||
name = loop_retry(
|
name = loop_retry(
|
||||||
agent,
|
agent,
|
||||||
|
@ -186,18 +296,59 @@ def generate_actor(
|
||||||
name=name,
|
name=name,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 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}")
|
||||||
|
|
||||||
|
items = []
|
||||||
|
for k in range(item_count):
|
||||||
|
existing_items = [item.name for item in items]
|
||||||
|
|
||||||
|
try:
|
||||||
|
item = generate_item(
|
||||||
|
agent,
|
||||||
|
world_theme,
|
||||||
|
dest_actor=name,
|
||||||
|
existing_items=existing_items,
|
||||||
|
)
|
||||||
|
generate_system_attributes(agent, world_theme, item, systems)
|
||||||
|
broadcast_generated(entity=item)
|
||||||
|
|
||||||
|
items.append(item)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("error generating item")
|
||||||
|
|
||||||
return Actor(
|
return Actor(
|
||||||
name=name,
|
name=name,
|
||||||
backstory=backstory,
|
backstory=backstory,
|
||||||
description=description,
|
description=description,
|
||||||
actions={},
|
actions={},
|
||||||
|
items=items,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def generate_effect(agent: Agent, theme: str, entity: Item) -> Effect:
|
# TODO: move to utils
|
||||||
entity_type = entity.type
|
def try_parse_int(value: str) -> int | None:
|
||||||
|
try:
|
||||||
|
return int(value)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
existing_effects = [effect.name for effect in entity.effects]
|
|
||||||
|
# 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
|
||||||
|
|
||||||
name = loop_retry(
|
name = loop_retry(
|
||||||
agent,
|
agent,
|
||||||
|
@ -252,28 +403,50 @@ def generate_effect(agent: Agent, theme: str, entity: Item) -> Effect:
|
||||||
"prepend",
|
"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_prompt = PROMPT_TYPE_FRAGMENTS[operation_type]
|
||||||
|
|
||||||
value = agent(
|
value = agent(
|
||||||
f"How much does the {name} effect modify the {attribute_name} attribute? "
|
f"How much does the {name} effect modify the {attribute_name} attribute? "
|
||||||
"For example, heal might add '10' to the health attribute, while poison might subtract '5' from it."
|
"For example, heal might add '10' to the health attribute, while poison might subtract '5' from it."
|
||||||
"Enter a positive or negative number, or a string value. Do not include any other text. Do not use JSON.",
|
f"{operation_prompt}. Do not include any other text. Do not use JSON.",
|
||||||
name=name,
|
name=name,
|
||||||
attribute_name=attribute_name,
|
attribute_name=attribute_name,
|
||||||
)
|
)
|
||||||
value = value.strip()
|
value = value.strip()
|
||||||
if value.isdigit():
|
|
||||||
value = int(value)
|
int_value = try_parse_int(value)
|
||||||
|
if int_value is not None:
|
||||||
attribute_effect = NumberAttributeEffect(
|
attribute_effect = NumberAttributeEffect(
|
||||||
name=attribute_name, operation=operation, value=value
|
name=attribute_name, operation=operation, value=int_value
|
||||||
)
|
|
||||||
elif value.isdecimal():
|
|
||||||
value = float(value)
|
|
||||||
attribute_effect = NumberAttributeEffect(
|
|
||||||
name=attribute_name, operation=operation, value=value
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
attribute_effect = StringAttributeEffect(
|
float_value = try_parse_float(value)
|
||||||
name=attribute_name, operation=operation, value=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
|
||||||
|
)
|
||||||
|
|
||||||
attributes.append(attribute_effect)
|
attributes.append(attribute_effect)
|
||||||
|
|
||||||
|
@ -293,121 +466,67 @@ def generate_world(
|
||||||
name: str,
|
name: str,
|
||||||
theme: str,
|
theme: str,
|
||||||
room_count: int | None = None,
|
room_count: int | None = None,
|
||||||
max_rooms: int = 5,
|
|
||||||
systems: List[GameSystem] = [],
|
systems: List[GameSystem] = [],
|
||||||
) -> World:
|
) -> World:
|
||||||
room_count = room_count or randint(3, max_rooms)
|
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")
|
broadcast_generated(message=f"Generating a {theme} with {room_count} rooms")
|
||||||
|
|
||||||
existing_actors: List[str] = []
|
|
||||||
existing_items: List[str] = []
|
|
||||||
existing_rooms: List[str] = []
|
|
||||||
|
|
||||||
# generate the rooms
|
# generate the rooms
|
||||||
rooms = []
|
rooms = []
|
||||||
for i 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, theme, existing_rooms=existing_rooms)
|
||||||
generate_system_attributes(agent, theme, room, systems)
|
generate_system_attributes(agent, theme, room, systems)
|
||||||
broadcast_generated(entity=room)
|
broadcast_generated(entity=room)
|
||||||
rooms.append(room)
|
rooms.append(room)
|
||||||
existing_rooms.append(room.name)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("error generating room")
|
logger.exception("error generating room")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
item_count = randint(1, 3)
|
|
||||||
broadcast_generated(f"Generating {item_count} items for room: {room.name}")
|
|
||||||
|
|
||||||
for j in range(item_count):
|
|
||||||
try:
|
|
||||||
item = generate_item(
|
|
||||||
agent,
|
|
||||||
theme,
|
|
||||||
dest_room=room.name,
|
|
||||||
existing_items=existing_items,
|
|
||||||
)
|
|
||||||
generate_system_attributes(agent, theme, item, systems)
|
|
||||||
broadcast_generated(entity=item)
|
|
||||||
|
|
||||||
room.items.append(item)
|
|
||||||
existing_items.append(item.name)
|
|
||||||
except Exception:
|
|
||||||
logger.exception("error generating item")
|
|
||||||
|
|
||||||
actor_count = randint(1, 3)
|
|
||||||
broadcast_generated(
|
|
||||||
message=f"Generating {actor_count} actors for room: {room.name}"
|
|
||||||
)
|
|
||||||
|
|
||||||
for j in range(actor_count):
|
|
||||||
try:
|
|
||||||
actor = generate_actor(
|
|
||||||
agent,
|
|
||||||
theme,
|
|
||||||
dest_room=room.name,
|
|
||||||
existing_actors=existing_actors,
|
|
||||||
)
|
|
||||||
generate_system_attributes(agent, theme, actor, systems)
|
|
||||||
broadcast_generated(entity=actor)
|
|
||||||
|
|
||||||
room.actors.append(actor)
|
|
||||||
existing_actors.append(actor.name)
|
|
||||||
except Exception:
|
|
||||||
logger.exception("error generating actor")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# generate the actor's inventory
|
|
||||||
item_count = randint(0, 2)
|
|
||||||
broadcast_generated(f"Generating {item_count} items for actor {actor.name}")
|
|
||||||
|
|
||||||
for k in range(item_count):
|
|
||||||
try:
|
|
||||||
item = generate_item(
|
|
||||||
agent,
|
|
||||||
theme,
|
|
||||||
dest_room=room.name,
|
|
||||||
existing_items=existing_items,
|
|
||||||
)
|
|
||||||
generate_system_attributes(agent, theme, item, systems)
|
|
||||||
broadcast_generated(entity=item)
|
|
||||||
|
|
||||||
actor.items.append(item)
|
|
||||||
existing_items.append(item.name)
|
|
||||||
except Exception:
|
|
||||||
logger.exception("error generating item")
|
|
||||||
|
|
||||||
# generate portals to link the rooms together
|
# generate portals to link the rooms together
|
||||||
for room in rooms:
|
for room in rooms:
|
||||||
directions = ["north", "south", "east", "west"]
|
num_portals = randint(
|
||||||
for direction in directions:
|
world_config.size.portals.min, world_config.size.portals.max
|
||||||
if direction in room.portals:
|
)
|
||||||
logger.debug(f"Room {room.name} already has a {direction} portal")
|
|
||||||
|
if len(room.portals) >= num_portals:
|
||||||
|
logger.info(f"room {room.name} already has enough portals")
|
||||||
|
continue
|
||||||
|
|
||||||
|
broadcast_generated(
|
||||||
|
message=f"Generating {num_portals} portals for room: {room.name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
for i in range(num_portals):
|
||||||
|
previous_destinations = [portal.destination for portal in room.portals] + [
|
||||||
|
room.name
|
||||||
|
]
|
||||||
|
remaining_rooms = [r for r in rooms if r.name not in previous_destinations]
|
||||||
|
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(
|
||||||
|
[r for r in rooms if r.name not in previous_destinations]
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
outgoing_portal, incoming_portal = generate_portals(
|
||||||
|
agent, theme, room, dest_room
|
||||||
|
)
|
||||||
|
|
||||||
|
room.portals.append(outgoing_portal)
|
||||||
|
dest_room.portals.append(incoming_portal)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("error generating portal")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
opposite_direction = OPPOSITE_DIRECTIONS[direction]
|
|
||||||
|
|
||||||
if randint(0, 1):
|
|
||||||
dest_room = choice([r for r in rooms if r.name != room.name])
|
|
||||||
|
|
||||||
# make sure not to create duplicate links
|
|
||||||
if room.name in dest_room.portals.values():
|
|
||||||
logger.debug(
|
|
||||||
f"Room {dest_room.name} already has a portal to {room.name}"
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if opposite_direction in dest_room.portals:
|
|
||||||
logger.debug(
|
|
||||||
f"Room {dest_room.name} already has a {opposite_direction} portal"
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# create bidirectional links
|
|
||||||
room.portals[direction] = dest_room.name
|
|
||||||
dest_room.portals[OPPOSITE_DIRECTIONS[direction]] = room.name
|
|
||||||
|
|
||||||
# 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]
|
order = [actor.name for room in rooms for actor in room.actors]
|
||||||
return World(name=name, rooms=rooms, theme=theme, order=order)
|
return World(name=name, rooms=rooms, theme=theme, order=order)
|
||||||
|
|
|
@ -91,12 +91,6 @@ def parse_args():
|
||||||
default="",
|
default="",
|
||||||
help="Some additional flavor text for the generated world",
|
help="Some additional flavor text for the generated world",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
|
||||||
"--max-rooms",
|
|
||||||
default=6,
|
|
||||||
type=int,
|
|
||||||
help="The maximum number of rooms to generate",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--optional-actions",
|
"--optional-actions",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
|
@ -216,7 +210,6 @@ def load_or_generate_world(args, players, systems, world_prompt: WorldPrompt):
|
||||||
args.world,
|
args.world,
|
||||||
world_prompt.theme,
|
world_prompt.theme,
|
||||||
room_count=args.rooms,
|
room_count=args.rooms,
|
||||||
max_rooms=args.max_rooms,
|
|
||||||
)
|
)
|
||||||
save_world(world, world_file)
|
save_world(world, world_file)
|
||||||
|
|
||||||
|
|
|
@ -47,11 +47,27 @@ class ServerConfig:
|
||||||
websocket: WebsocketServerConfig
|
websocket: WebsocketServerConfig
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WorldSizeConfig:
|
||||||
|
actor_items: Range
|
||||||
|
item_effects: Range
|
||||||
|
portals: Range
|
||||||
|
room_actors: Range
|
||||||
|
room_items: Range
|
||||||
|
rooms: Range
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WorldConfig:
|
||||||
|
size: WorldSizeConfig
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Config:
|
class Config:
|
||||||
bot: BotConfig
|
bot: BotConfig
|
||||||
render: RenderConfig
|
render: RenderConfig
|
||||||
server: ServerConfig
|
server: ServerConfig
|
||||||
|
world: WorldConfig
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_CONFIG = Config(
|
DEFAULT_CONFIG = Config(
|
||||||
|
@ -69,5 +85,15 @@ DEFAULT_CONFIG = Config(
|
||||||
},
|
},
|
||||||
steps=Range(min=30, max=30),
|
steps=Range(min=30, max=30),
|
||||||
),
|
),
|
||||||
server=ServerConfig(websocket=WebsocketServerConfig(host="localhost", port=8000)),
|
server=ServerConfig(websocket=WebsocketServerConfig(host="localhost", port=8001)),
|
||||||
|
world=WorldConfig(
|
||||||
|
size=WorldSizeConfig(
|
||||||
|
actor_items=Range(min=1, max=3),
|
||||||
|
item_effects=Range(min=1, max=3),
|
||||||
|
portals=Range(min=1, max=3),
|
||||||
|
rooms=Range(min=3, max=6),
|
||||||
|
room_actors=Range(min=1, max=3),
|
||||||
|
room_items=Range(min=1, max=3),
|
||||||
|
)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
|
@ -67,6 +67,17 @@ class Actor(BaseModel):
|
||||||
type: Literal["actor"] = "actor"
|
type: Literal["actor"] = "actor"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Portal(BaseModel):
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
destination: str
|
||||||
|
actions: Actions = Field(default_factory=dict)
|
||||||
|
attributes: Attributes = Field(default_factory=dict)
|
||||||
|
id: str = Field(default_factory=uuid)
|
||||||
|
type: Literal["portal"] = "portal"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Room(BaseModel):
|
class Room(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
|
@ -75,7 +86,7 @@ class Room(BaseModel):
|
||||||
actions: Actions = Field(default_factory=dict)
|
actions: Actions = Field(default_factory=dict)
|
||||||
attributes: Attributes = Field(default_factory=dict)
|
attributes: Attributes = Field(default_factory=dict)
|
||||||
items: List[Item] = Field(default_factory=list)
|
items: List[Item] = Field(default_factory=list)
|
||||||
portals: Dict[str, str] = Field(default_factory=dict)
|
portals: List[Portal] = Field(default_factory=list)
|
||||||
id: str = Field(default_factory=uuid)
|
id: str = Field(default_factory=uuid)
|
||||||
type: Literal["room"] = "room"
|
type: Literal["room"] = "room"
|
||||||
|
|
||||||
|
|
|
@ -280,7 +280,7 @@ def launch_server(config: WebsocketServerConfig):
|
||||||
|
|
||||||
|
|
||||||
async def server_main():
|
async def server_main():
|
||||||
async with websockets.serve(handler, "", 8001):
|
async with websockets.serve(handler, server_config.host, server_config.port):
|
||||||
logger.info("websocket server started")
|
logger.info("websocket server started")
|
||||||
await asyncio.Future() # run forever
|
await asyncio.Future() # run forever
|
||||||
|
|
||||||
|
|
|
@ -98,7 +98,7 @@ def simulate_world(
|
||||||
|
|
||||||
room_actors = [actor.name for actor in room.actors]
|
room_actors = [actor.name for actor in room.actors]
|
||||||
room_items = [item.name for item in room.items]
|
room_items = [item.name for item in room.items]
|
||||||
room_directions = list(room.portals.keys())
|
room_directions = [portal.name for portal in room.portals]
|
||||||
|
|
||||||
actor_attributes = format_attributes(actor)
|
actor_attributes = format_attributes(actor)
|
||||||
actor_items = [item.name for item in actor.items]
|
actor_items = [item.name for item in actor.items]
|
||||||
|
|
|
@ -40,8 +40,8 @@ def graph_world(world: World, step: int):
|
||||||
for room in world.rooms:
|
for room in world.rooms:
|
||||||
room_label = "\n".join([room.name, *[actor.name for actor in room.actors]])
|
room_label = "\n".join([room.name, *[actor.name for actor in room.actors]])
|
||||||
graph.node(room.name, room_label)
|
graph.node(room.name, room_label)
|
||||||
for direction, destination in room.portals.items():
|
for portal in room.portals:
|
||||||
graph.edge(room.name, destination, label=direction)
|
graph.edge(room.name, portal.destination, label=portal.name)
|
||||||
|
|
||||||
graph_path = path.dirname(world.name)
|
graph_path = path.dirname(world.name)
|
||||||
graph.render(directory=graph_path)
|
graph.render(directory=graph_path)
|
||||||
|
|
Loading…
Reference in New Issue