diff --git a/client/src/events.tsx b/client/src/events.tsx
index 05cc99a..459e770 100644
--- a/client/src/events.tsx
+++ b/client/src/events.tsx
@@ -119,6 +119,32 @@ export function SnapshotEventItem(props: EventItemProps) {
}
export function ReplyEventItem(props: EventItemProps) {
+ const { event } = props;
+ const { audience, speaker, text } = event;
+
+ return
+
+
+
+
+
+
+ {speaker.name} replies to {audience.name}: {text}
+
+ }
+ />
+ ;
+}
+
+export function StatusEventItem(props: EventItemProps) {
const { event } = props;
const { text } = event;
@@ -295,8 +321,9 @@ export function EventItem(props: EventItemProps) {
case 'result':
return ;
case 'reply':
- case 'status': // TODO: should have a different component
return ;
+ case 'status':
+ return ;
case 'player':
return ;
case 'render':
diff --git a/taleweave/actions/base.py b/taleweave/actions/base.py
index 51d9574..104a099 100644
--- a/taleweave/actions/base.py
+++ b/taleweave/actions/base.py
@@ -13,6 +13,7 @@ from taleweave.utils.search import (
find_character_in_room,
find_item_in_character,
find_item_in_room,
+ find_portal_in_room,
find_room,
)
from taleweave.utils.string import normalize_name
@@ -20,12 +21,12 @@ from taleweave.utils.world import describe_entity
logger = getLogger(__name__)
-MAX_CONVERSATION_STEPS = 3
+MAX_CONVERSATION_STEPS = 2
-def action_look(target: str) -> str:
+def action_examine(target: str) -> str:
"""
- Look at a target in the room or your inventory.
+ Examine the room, a character, or an item (in the room or in your inventory).
Args:
target: The name of the target to look at.
@@ -71,14 +72,7 @@ def action_move(direction: str) -> str:
"""
with world_context() as (action_world, action_room, action_character):
- portal = next(
- (
- p
- for p in action_room.portals
- if normalize_name(p.name) == normalize_name(direction)
- ),
- None,
- )
+ portal = find_portal_in_room(action_room, direction)
if not portal:
raise ActionError(f"You cannot move {direction} from here.")
@@ -151,7 +145,7 @@ def action_ask(character: str, question: str) -> str:
)
action_agent = get_agent_for_character(action_character)
- answer = loop_conversation(
+ result = loop_conversation(
action_room,
[question_character, action_character],
[question_agent, action_agent],
@@ -165,9 +159,8 @@ def action_ask(character: str, question: str) -> str:
max_length=MAX_CONVERSATION_STEPS,
)
- if answer:
- broadcast(f"{character} responds to {action_character.name}: {answer}")
- return f"{character} responds: {answer}"
+ if result:
+ return result
return f"{character} does not respond."
@@ -209,7 +202,7 @@ def action_tell(character: str, message: str) -> str:
)
action_agent = get_agent_for_character(action_character)
- answer = loop_conversation(
+ result = loop_conversation(
action_room,
[question_character, action_character],
[question_agent, action_agent],
@@ -223,9 +216,8 @@ def action_tell(character: str, message: str) -> str:
max_length=MAX_CONVERSATION_STEPS,
)
- if answer:
- broadcast(f"{character} responds to {action_character.name}: {answer}")
- return f"{character} responds: {answer}"
+ if result:
+ return result
return f"{character} does not respond."
diff --git a/taleweave/actions/planning.py b/taleweave/actions/planning.py
index 6e56d42..b1e0793 100644
--- a/taleweave/actions/planning.py
+++ b/taleweave/actions/planning.py
@@ -18,7 +18,10 @@ def take_note(fact: str):
with action_context() as (_, action_character):
if fact in action_character.planner.notes:
- raise ActionError("You already have a note about that fact.")
+ raise ActionError(
+ "You already have a note about that fact. You do not need to take duplicate notes. "
+ "If you have too many notes, consider erasing, replacing, or summarizing them."
+ )
if len(action_character.planner.notes) >= character_config.note_limit:
raise ActionError(
@@ -103,7 +106,8 @@ def summarize_notes(limit: int) -> str:
"If a newer note contradicts an older note, keep the newer note. "
"Clean up your notes so you can focus on the most important facts. "
"Respond with one note per line. You can have up to {limit} notes, "
- "so make sure you reply with less than {limit} lines. "
+ "so make sure you reply with less than {limit} lines. Do not number the lines "
+ "in your response. Do not include any JSON or other information. "
"Your notes are:\n{notes}",
limit=limit,
notes=notes,
diff --git a/taleweave/bot/discord.py b/taleweave/bot/discord.py
index 9bad720..7e1b147 100644
--- a/taleweave/bot/discord.py
+++ b/taleweave/bot/discord.py
@@ -330,7 +330,7 @@ def embed_from_event(event: GameEvent) -> Embed | None:
def embed_from_action(event: ActionEvent | ReplyEvent):
- action_embed = Embed(title=event.room.name, description=event.character.name)
+ action_embed = Embed(title=event.room.name, description=event.speaker.name)
if isinstance(event, ActionEvent):
action_name = event.action.replace("action_", "").title()
diff --git a/taleweave/main.py b/taleweave/main.py
index fe9722d..04253ba 100644
--- a/taleweave/main.py
+++ b/taleweave/main.py
@@ -200,11 +200,14 @@ def load_or_initialize_system_data(args, systems: List[GameSystem], world: World
logger.info(f"loading system data from {system_data_file}")
data = system.data.load(system_data_file)
set_system_data(system.name, data)
+ continue
else:
logger.info(f"no system data found at {system_data_file}")
- if system.initialize:
- data = system.initialize(world)
- set_system_data(system.name, data)
+
+ if system.initialize:
+ logger.info(f"initializing system data for {system.name}")
+ data = system.initialize(world)
+ set_system_data(system.name, data)
def save_system_data(args, systems: List[GameSystem]):
diff --git a/taleweave/models/event.py b/taleweave/models/event.py
index 6d2da41..5b5b12f 100644
--- a/taleweave/models/event.py
+++ b/taleweave/models/event.py
@@ -71,22 +71,15 @@ class PromptEvent(BaseModel):
class ReplyEvent(BaseModel):
"""
A character has replied with text.
-
- This is the non-JSON version of an ActionEvent.
-
- TODO: add the character being replied to.
"""
- text: str
room: Room
- character: Character
+ speaker: Character
+ audience: Character | Room
+ text: str
id: str = Field(default_factory=uuid)
type: Literal["reply"] = "reply"
- @staticmethod
- def from_text(text: str, room: Room, character: Character) -> "ReplyEvent":
- return ReplyEvent(text=text, room=room, character=character)
-
@dataclass
class ResultEvent(BaseModel):
diff --git a/taleweave/player.py b/taleweave/player.py
index 519fad8..d412720 100644
--- a/taleweave/player.py
+++ b/taleweave/player.py
@@ -193,6 +193,7 @@ class RemotePlayer(BasePlayer):
logger.exception("error getting reply from remote player")
if self.fallback_agent:
+ logger.info("prompting fallback agent: {self.fallback_agent.name}")
return self.fallback_agent(prompt, **kwargs)
return ""
diff --git a/taleweave/render/comfy.py b/taleweave/render/comfy.py
index fffb039..ced7dad 100644
--- a/taleweave/render/comfy.py
+++ b/taleweave/render/comfy.py
@@ -230,7 +230,7 @@ def get_image_prefix(event: GameEvent | WorldEntity) -> str:
if isinstance(event, ReplyEvent):
return sanitize_name(
- f"event-reply-{event.character.name}-{fast_hash(event.text)}"
+ f"event-reply-{event.speaker.name}-{fast_hash(event.text)}"
)
if isinstance(event, ResultEvent):
diff --git a/taleweave/render/prompt.py b/taleweave/render/prompt.py
index 924abdf..767fe8a 100644
--- a/taleweave/render/prompt.py
+++ b/taleweave/render/prompt.py
@@ -97,7 +97,7 @@ def scene_from_event(event: GameEvent) -> str | None:
)
if isinstance(event, ReplyEvent):
- return f"{event.character.name} replies: {event.text}. {describe_entity(event.character)}. {describe_entity(event.room)}."
+ return f"{event.speaker.name} replies: {event.text}. {describe_entity(event.speaker)}. {describe_entity(event.room)}."
if isinstance(event, ResultEvent):
return f"{event.result}. {describe_entity(event.character)}. {describe_entity(event.room)}."
diff --git a/taleweave/simulate.py b/taleweave/simulate.py
index 08e404b..ce25aea 100644
--- a/taleweave/simulate.py
+++ b/taleweave/simulate.py
@@ -8,14 +8,14 @@ from typing import Callable, Sequence
from packit.agent import Agent
from packit.conditions import condition_or, condition_threshold
from packit.loops import loop_retry
-from packit.results import multi_function_or_str_result
+from packit.results import function_result
from packit.toolbox import Toolbox
from packit.utils import could_be_json
from taleweave.actions.base import (
action_ask,
+ action_examine,
action_give,
- action_look,
action_move,
action_take,
action_tell,
@@ -45,11 +45,11 @@ from taleweave.context import (
from taleweave.game_system import GameSystem
from taleweave.models.config import DEFAULT_CONFIG
from taleweave.models.entity import Character, Room, World
-from taleweave.models.event import ActionEvent, ReplyEvent, ResultEvent
+from taleweave.models.event import ActionEvent, ResultEvent
from taleweave.utils.conversation import make_keyword_condition, summarize_room
from taleweave.utils.effect import expire_effects
from taleweave.utils.planning import expire_events, get_upcoming_events
-from taleweave.utils.search import find_room_with_character
+from taleweave.utils.search import find_containing_room
from taleweave.utils.world import describe_entity, format_attributes
logger = getLogger(__name__)
@@ -76,7 +76,7 @@ def world_result_parser(value, agent, **kwargs):
set_current_room(current_room)
set_current_character(current_character)
- return multi_function_or_str_result(value, agent=agent, **kwargs)
+ return function_result(value, agent=agent, **kwargs)
def prompt_character_action(
@@ -94,7 +94,7 @@ def prompt_character_action(
character_items = [item.name for item in character.items]
# set up a result parser for the agent
- def result_parser(value, agent, **kwargs):
+ def result_parser(value, **kwargs):
if not room or not character:
raise ValueError("Room and character must be set before parsing results")
@@ -117,11 +117,12 @@ def prompt_character_action(
if could_be_json(value):
event = ActionEvent.from_json(value, room, character)
else:
- event = ReplyEvent.from_text(value, room, character)
+ # TODO: this should be removed and throw
+ event = ResultEvent(value, room, character)
broadcast(event)
- return world_result_parser(value, agent, **kwargs)
+ return world_result_parser(value, **kwargs)
# prompt and act
logger.info("starting turn for character: %s", character.name)
@@ -255,7 +256,7 @@ def simulate_world(
[
action_ask,
action_give,
- action_look,
+ action_examine,
action_move,
action_take,
action_tell,
@@ -288,7 +289,7 @@ def simulate_world(
logger.error(f"agent or character not found for name {character_name}")
continue
- room = find_room_with_character(world, character)
+ room = find_containing_room(world, character)
if not room:
logger.error(f"character {character_name} is not in a room")
continue
diff --git a/taleweave/systems/weather/__init__.py b/taleweave/systems/weather/__init__.py
index 3d74ee8..3d5b368 100644
--- a/taleweave/systems/weather/__init__.py
+++ b/taleweave/systems/weather/__init__.py
@@ -1,8 +1,19 @@
+from functools import partial
from typing import List
+from taleweave.context import get_dungeon_master
from taleweave.models.base import dataclass
from taleweave.models.entity import World
from taleweave.systems.logic import load_logic
from taleweave.game_system import GameSystem
+from packit.agent import Agent
+from taleweave.models.entity import Room, WorldEntity
+from taleweave.utils.string import or_list
+from packit.results import enum_result
+from packit.loops import loop_retry
+from logging import getLogger
+
+logger = getLogger(__name__)
+
LOGIC_FILES = [
"./taleweave/systems/weather/weather_logic.yaml",
@@ -39,10 +50,36 @@ def get_time_of_day(turn: int) -> TimeOfDay:
def initialize_weather(world: World):
time_of_day = get_time_of_day(0)
for room in world.rooms:
+ logger.info(f"initializing weather for {room.name}")
room.attributes["time"] = time_of_day.name
+ if "environment" not in room.attributes:
+ dungeon_master = get_dungeon_master()
+ generate_room_weather(dungeon_master, world.theme, room)
-# TODO: generate indoor/outdoor attributes
+
+def generate_room_weather(agent: Agent, theme: str, entity: Room) -> None:
+ environment_options = ["indoor", "outdoor"]
+ environment_result = partial(enum_result, enum=environment_options)
+ environment = loop_retry(
+ agent,
+ "Is this room indoors or outdoors?"
+ "Reply with a single word: {environment_list}.\n\n"
+ "{description}",
+ context={
+ "environment_list": or_list(environment_options),
+ "description": entity.description,
+ },
+ result_parser=environment_result,
+ )
+ entity.attributes["environment"] = environment
+ logger.info(f"generated environment for {entity.name}: {environment}")
+
+
+def generate_weather(agent: Agent, theme: str, entity: WorldEntity) -> None:
+ if isinstance(entity, Room):
+ if "environment" not in entity.attributes:
+ generate_room_weather(agent, theme, entity)
def simulate_weather(world: World, turn: int, data: None = None):
@@ -55,5 +92,10 @@ def init():
logic_systems = [load_logic(filename) for filename in LOGIC_FILES]
return [
*logic_systems,
- GameSystem("weather", initialize=initialize_weather, simulate=simulate_weather),
+ GameSystem(
+ "weather",
+ generate=generate_weather,
+ initialize=initialize_weather,
+ simulate=simulate_weather,
+ ),
]
diff --git a/taleweave/systems/weather/weather_logic.yaml b/taleweave/systems/weather/weather_logic.yaml
index d4f1a4b..1aa0cd5 100644
--- a/taleweave/systems/weather/weather_logic.yaml
+++ b/taleweave/systems/weather/weather_logic.yaml
@@ -3,7 +3,7 @@ rules:
- group: weather
match:
type: room
- outdoor: true
+ environment: outdoor
weather: clear
chance: 0.1
set:
@@ -12,7 +12,7 @@ rules:
- group: weather
match:
type: room
- outdoor: true
+ environment: outdoor
weather: clouds
chance: 0.1
set:
@@ -21,7 +21,7 @@ rules:
- group: weather
match:
type: room
- outdoor: true
+ environment: outdoor
weather: rain
chance: 0.1
set:
@@ -30,7 +30,7 @@ rules:
- group: weather
match:
type: room
- outdoor: true
+ environment: outdoor
weather: clouds
chance: 0.1
set:
@@ -40,7 +40,7 @@ rules:
- group: weather
match:
type: room
- outdoor: true
+ environment: outdoor
rule: |
"weather" not in attributes
set:
diff --git a/taleweave/utils/conversation.py b/taleweave/utils/conversation.py
index 9b0e6bc..6cd5643 100644
--- a/taleweave/utils/conversation.py
+++ b/taleweave/utils/conversation.py
@@ -148,11 +148,11 @@ def loop_conversation(
response = result_parser(response)
logger.info(f"{character.name} responds: {response}")
- reply_event = ReplyEvent.from_text(response, room, character)
+ reply_event = ReplyEvent(room, character, last_character, response)
broadcast(reply_event)
# increment the step counter
i += 1
last_character = character
- return response
+ return f"{last_character.name} ends the conversation for now"
diff --git a/taleweave/utils/search.py b/taleweave/utils/search.py
index 1b85e5e..4b57acc 100644
--- a/taleweave/utils/search.py
+++ b/taleweave/utils/search.py
@@ -23,9 +23,9 @@ def find_room(world: World, room_name: str) -> Room | None:
def find_portal(world: World, portal_name: str) -> Portal | None:
for room in world.rooms:
- for portal in room.portals:
- if normalize_name(portal.name) == normalize_name(portal_name):
- return portal
+ portal = find_portal_in_room(room, portal_name)
+ if portal:
+ return portal
return None
@@ -47,6 +47,14 @@ def find_character_in_room(room: Room, character_name: str) -> Character | None:
return None
+def find_portal_in_room(room: Room, portal_name: str) -> Portal | None:
+ for portal in room.portals:
+ if normalize_name(portal.name) == normalize_name(portal_name):
+ return portal
+
+ return None
+
+
# TODO: allow item or str
def find_item(
world: World,
@@ -109,15 +117,6 @@ def find_item_in_room(
return None
-def find_room_with_character(world: World, character: Character) -> Room | None:
- for room in world.rooms:
- for room_character in room.characters:
- if normalize_name(character.name) == normalize_name(room_character.name):
- return room
-
- return None
-
-
def find_containing_room(world: World, entity: Room | Character | Item) -> Room | None:
if isinstance(entity, Room):
return entity