1
0
Fork 0

improve search, prevent duplicate names, validate effect operations

This commit is contained in:
Sean Sube 2024-05-18 23:30:17 -05:00
parent 71d2be85f1
commit 2bb842a559
Signed by: ssube
GPG Key ID: 3EED7B957D362AF1
10 changed files with 218 additions and 161 deletions

View File

@ -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:

View File

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

View File

@ -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"

View File

@ -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

View File

@ -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)

View 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),

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -21,4 +21,28 @@ render:
height: 1024 height: 1024
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