1
0
Fork 0

rename actor to character, add step limits to config

This commit is contained in:
Sean Sube 2024-05-26 20:32:03 -05:00
parent fc07ccd9df
commit c81d2ae3f2
Signed by: ssube
GPG Key ID: 3EED7B957D362AF1
49 changed files with 761 additions and 679 deletions

View File

@ -3,15 +3,15 @@ from logging import getLogger
from adventure.context import (
action_context,
broadcast,
get_actor_agent_for_name,
get_agent_for_actor,
get_agent_for_character,
get_character_agent_for_name,
world_context,
)
from adventure.errors import ActionError
from adventure.utils.conversation import loop_conversation
from adventure.utils.search import (
find_actor_in_room,
find_item_in_actor,
find_character_in_room,
find_item_in_character,
find_item_in_room,
find_room,
)
@ -31,31 +31,31 @@ def action_look(target: str) -> str:
target: The name of the target to look at.
"""
with action_context() as (action_room, action_actor):
broadcast(f"{action_actor.name} looks at {target}")
with action_context() as (action_room, action_character):
broadcast(f"{action_character.name} looks at {target}")
if normalize_name(target) == normalize_name(action_room.name):
broadcast(f"{action_actor.name} saw the {action_room.name} room")
broadcast(f"{action_character.name} saw the {action_room.name} room")
return describe_entity(action_room)
target_actor = find_actor_in_room(action_room, target)
if target_actor:
target_character = find_character_in_room(action_room, target)
if target_character:
broadcast(
f"{action_actor.name} saw the {target_actor.name} actor in the {action_room.name} room"
f"{action_character.name} saw the {target_character.name} character in the {action_room.name} room"
)
return describe_entity(target_actor)
return describe_entity(target_character)
target_item = find_item_in_room(action_room, target)
if target_item:
broadcast(
f"{action_actor.name} saw the {target_item.name} item in the {action_room.name} room"
f"{action_character.name} saw the {target_item.name} item in the {action_room.name} room"
)
return describe_entity(target_item)
target_item = find_item_in_actor(action_actor, target)
target_item = find_item_in_character(action_character, target)
if target_item:
broadcast(
f"{action_actor.name} saw the {target_item.name} item in their inventory"
f"{action_character.name} saw the {target_item.name} item in their inventory"
)
return describe_entity(target_item)
@ -70,7 +70,7 @@ def action_move(direction: str) -> str:
direction: The direction to move in.
"""
with world_context() as (action_world, action_room, action_actor):
with world_context() as (action_world, action_room, action_character):
portal = next(
(
p
@ -87,10 +87,10 @@ def action_move(direction: str) -> str:
raise ActionError(f"The {portal.destination} room does not exist.")
broadcast(
f"{action_actor.name} moves through {direction} to {destination_room.name}"
f"{action_character.name} moves through {direction} to {destination_room.name}"
)
action_room.actors.remove(action_actor)
destination_room.actors.append(action_actor)
action_room.characters.remove(action_character)
destination_room.characters.append(action_character)
return (
f"You move through the {direction} and arrive at {destination_room.name}."
@ -104,14 +104,14 @@ def action_take(item: str) -> str:
Args:
item: The name of the item to take.
"""
with action_context() as (action_room, action_actor):
with action_context() as (action_room, action_character):
action_item = find_item_in_room(action_room, item)
if not action_item:
raise ActionError(f"The {item} item is not in the room.")
broadcast(f"{action_actor.name} takes the {item} item")
broadcast(f"{action_character.name} takes the {item} item")
action_room.items.remove(action_item)
action_actor.items.append(action_item)
action_character.items.append(action_item)
return f"You take the {item} item and put it in your inventory."
@ -123,39 +123,39 @@ def action_ask(character: str, question: str) -> str:
character: The name of the character to ask. You cannot ask yourself questions.
question: The question to ask them.
"""
# capture references to the current actor and room, because they will be overwritten
with action_context() as (action_room, action_actor):
# capture references to the current character and room, because they will be overwritten
with action_context() as (action_room, action_character):
# sanity checks
question_actor, question_agent = get_actor_agent_for_name(character)
if question_actor == action_actor:
question_character, question_agent = get_character_agent_for_name(character)
if question_character == action_character:
raise ActionError(
"You cannot ask yourself a question. Stop talking to yourself. Try another action."
)
if not question_actor:
if not question_character:
raise ActionError(f"The {character} character is not in the room.")
if not question_agent:
raise ActionError(f"The {character} character does not exist.")
broadcast(f"{action_actor.name} asks {character}: {question}")
broadcast(f"{action_character.name} asks {character}: {question}")
first_prompt = (
"{last_actor.name} asks you: {response}\n"
"{last_character.name} asks you: {response}\n"
"Reply with your response to them. Reply with 'END' to end the conversation. "
"Do not include the question or any JSON. Only include your answer for {last_actor.name}."
"Do not include the question or any JSON. Only include your answer for {last_character.name}."
)
reply_prompt = (
"{last_actor.name} continues the conversation with you. They reply: {response}\n"
"{last_character.name} continues the conversation with you. They reply: {response}\n"
"Reply with your response to them. Reply with 'END' to end the conversation. "
"Do not include the question or any JSON. Only include your answer for {last_actor.name}."
"Do not include the question or any JSON. Only include your answer for {last_character.name}."
)
action_agent = get_agent_for_actor(action_actor)
action_agent = get_agent_for_character(action_character)
answer = loop_conversation(
action_room,
[question_actor, action_actor],
[question_character, action_character],
[question_agent, action_agent],
action_actor,
action_character,
first_prompt,
reply_prompt,
question,
@ -166,7 +166,7 @@ def action_ask(character: str, question: str) -> str:
)
if answer:
broadcast(f"{character} responds to {action_actor.name}: {answer}")
broadcast(f"{character} responds to {action_character.name}: {answer}")
return f"{character} responds: {answer}"
return f"{character} does not respond."
@ -180,40 +180,40 @@ def action_tell(character: str, message: str) -> str:
character: The name of the character to tell. You cannot talk to yourself.
message: The message to tell them.
"""
# capture references to the current actor and room, because they will be overwritten
# capture references to the current character and room, because they will be overwritten
with action_context() as (action_room, action_actor):
with action_context() as (action_room, action_character):
# sanity checks
question_actor, question_agent = get_actor_agent_for_name(character)
if question_actor == action_actor:
question_character, question_agent = get_character_agent_for_name(character)
if question_character == action_character:
raise ActionError(
"You cannot tell yourself a message. Stop talking to yourself. Try another action."
)
if not question_actor:
if not question_character:
raise ActionError(f"The {character} character is not in the room.")
if not question_agent:
raise ActionError(f"The {character} character does not exist.")
broadcast(f"{action_actor.name} tells {character}: {message}")
broadcast(f"{action_character.name} tells {character}: {message}")
first_prompt = (
"{last_actor.name} starts a conversation with you. They say: {response}\n"
"{last_character.name} starts a conversation with you. They say: {response}\n"
"Reply with your response to them. "
"Do not include the message or any JSON. Only include your reply to {last_actor.name}."
"Do not include the message or any JSON. Only include your reply to {last_character.name}."
)
reply_prompt = (
"{last_actor.name} continues the conversation with you. They reply: {response}\n"
"{last_character.name} continues the conversation with you. They reply: {response}\n"
"Reply with your response to them. "
"Do not include the message or any JSON. Only include your reply to {last_actor.name}."
"Do not include the message or any JSON. Only include your reply to {last_character.name}."
)
action_agent = get_agent_for_actor(action_actor)
action_agent = get_agent_for_character(action_character)
answer = loop_conversation(
action_room,
[question_actor, action_actor],
[question_character, action_character],
[question_agent, action_agent],
action_actor,
action_character,
first_prompt,
reply_prompt,
message,
@ -224,7 +224,7 @@ def action_tell(character: str, message: str) -> str:
)
if answer:
broadcast(f"{character} responds to {action_actor.name}: {answer}")
broadcast(f"{character} responds to {action_character.name}: {answer}")
return f"{character} responds: {answer}"
return f"{character} does not respond."
@ -238,23 +238,23 @@ def action_give(character: str, item: str) -> str:
character: The name of the character to give the item to.
item: The name of the item to give.
"""
with action_context() as (action_room, action_actor):
destination_actor = find_actor_in_room(action_room, character)
if not destination_actor:
with action_context() as (action_room, action_character):
destination_character = find_character_in_room(action_room, character)
if not destination_character:
raise ActionError(f"The {character} character is not in the room.")
if destination_actor == action_actor:
if destination_character == action_character:
raise ActionError(
"You cannot give an item to yourself. Try another action."
)
action_item = find_item_in_actor(action_actor, item)
action_item = find_item_in_character(action_character, item)
if not action_item:
raise ActionError(f"You do not have the {item} item in your inventory.")
broadcast(f"{action_actor.name} gives {character} the {item} item.")
action_actor.items.remove(action_item)
destination_actor.items.append(action_item)
broadcast(f"{action_character.name} gives {character} the {item} item.")
action_character.items.remove(action_item)
destination_character.items.append(action_item)
return f"You give the {item} item to {character}."
@ -267,13 +267,13 @@ def action_drop(item: str) -> str:
item: The name of the item to drop.
"""
with action_context() as (action_room, action_actor):
action_item = find_item_in_actor(action_actor, item)
with action_context() as (action_room, action_character):
action_item = find_item_in_character(action_character, item)
if not action_item:
raise ActionError(f"You do not have the {item} item in your inventory.")
broadcast(f"{action_actor.name} drops the {item} item")
action_actor.items.remove(action_item)
broadcast(f"{action_character.name} drops the {item} item")
action_character.items.remove(action_item)
action_room.items.append(action_item)
return f"You drop the {item} item."

View File

@ -6,7 +6,7 @@ from packit.agent import Agent, agent_easy_connect
from adventure.context import (
action_context,
broadcast,
get_agent_for_actor,
get_agent_for_character,
get_dungeon_master,
get_game_systems,
has_dungeon_master,
@ -16,9 +16,9 @@ from adventure.context import (
from adventure.errors import ActionError
from adventure.generate import generate_item, generate_room, link_rooms
from adventure.utils.effect import apply_effects
from adventure.utils.search import find_actor_in_room
from adventure.utils.search import find_character_in_room
from adventure.utils.string import normalize_name
from adventure.utils.world import describe_actor, describe_entity
from adventure.utils.world import describe_character, describe_entity
logger = getLogger(__name__)
@ -43,7 +43,7 @@ def action_explore(direction: str) -> str:
direction: The direction to explore. For example: inside, outside, upstairs, downstairs, trapdoor, portal, etc.
"""
with world_context() as (action_world, action_room, action_actor):
with world_context() as (action_world, action_room, action_character):
dungeon_master = get_dungeon_master()
if direction in action_room.portals:
@ -62,7 +62,7 @@ def action_explore(direction: str) -> str:
link_rooms(dungeon_master, action_world, systems, [new_room])
broadcast(
f"{action_actor.name} explores {direction} of {action_room.name} and finds a new room: {new_room.name}"
f"{action_character.name} explores {direction} of {action_room.name} and finds a new room: {new_room.name}"
)
return f"You explore {direction} and find a new room: {new_room.name}"
except Exception:
@ -75,7 +75,7 @@ def action_search(unused: bool) -> str:
Search the room for hidden items.
"""
with world_context() as (action_world, action_room, action_actor):
with world_context() as (action_world, action_room, action_character):
dungeon_master = get_dungeon_master()
if len(action_room.items) > 2:
@ -94,7 +94,7 @@ def action_search(unused: bool) -> str:
action_room.items.append(new_item)
broadcast(
f"{action_actor.name} searches {action_room.name} and finds a new item: {new_item.name}"
f"{action_character.name} searches {action_room.name} and finds a new item: {new_item.name}"
)
return f"You search the room and find a new item: {new_item.name}"
except Exception:
@ -110,13 +110,13 @@ def action_use(item: str, target: str) -> str:
item: The name of the item to use.
target: The name of the character to use the item on, or "self" to use the item on yourself.
"""
with action_context() as (action_room, action_actor):
with action_context() as (action_room, action_character):
dungeon_master = get_dungeon_master()
action_item = next(
(
search_item
for search_item in (action_actor.items + action_room.items)
for search_item in (action_character.items + action_room.items)
if search_item.name == item
),
None,
@ -125,17 +125,17 @@ def action_use(item: str, target: str) -> str:
raise ActionError(f"The {item} item is not available to use.")
if target == "self":
target_actor = action_actor
target = action_actor.name
target_character = action_character
target = action_character.name
else:
# TODO: allow targeting the room itself and items in the room
target_actor = find_actor_in_room(action_room, target)
if not target_actor:
target_character = find_character_in_room(action_room, target)
if not target_character:
return f"The {target} character is not in the room."
effect_names = [effect.name for effect in action_item.effects]
chosen_name = dungeon_master(
f"{action_actor.name} uses {item} on {target}. "
f"{action_character.name} uses {item} on {target}. "
f"{item} has the following effects: {effect_names}. "
"Which effect should be applied? Specify the name of the effect to apply."
"Do not include the question or any JSON. Only include the name of the effect to apply."
@ -155,7 +155,7 @@ def action_use(item: str, target: str) -> str:
raise ValueError(f"The {chosen_name} effect is not available to apply.")
try:
apply_effects(target_actor, [chosen_effect])
apply_effects(target_character, [chosen_effect])
except Exception:
logger.exception("error applying effect: %s", chosen_effect)
raise ValueError(
@ -163,11 +163,11 @@ def action_use(item: str, target: str) -> str:
)
broadcast(
f"{action_actor.name} uses the {chosen_name} effect of {item} on {target}"
f"{action_character.name} uses the {chosen_name} effect of {item} on {target}"
)
outcome = dungeon_master(
f"{action_actor.name} uses the {chosen_name} effect of {item} on {target}. "
f"{describe_actor(action_actor)}. {describe_actor(target_actor)}. {describe_entity(action_item)}. "
f"{action_character.name} uses the {chosen_name} effect of {item} on {target}. "
f"{describe_character(action_character)}. {describe_character(target_character)}. {describe_entity(action_item)}. "
f"What happens? How does {target} react? Be creative with the results. The outcome can be good, bad, or neutral."
"Decide based on the characters involved and the item being used."
"Specify the outcome of the action. Do not include the question or any JSON. Only include the outcome of the action."
@ -175,7 +175,7 @@ def action_use(item: str, target: str) -> str:
broadcast(f"The action resulted in: {outcome}")
# make sure both agents remember the outcome
target_agent = get_agent_for_actor(target_actor)
target_agent = get_agent_for_character(target_character)
if target_agent and target_agent.memory:
target_agent.memory.append(outcome)

View File

@ -1,10 +1,10 @@
from adventure.context import action_context, get_agent_for_actor, get_current_step
from adventure.context import action_context, get_agent_for_character, get_current_step
from adventure.errors import ActionError
from adventure.models.config import DEFAULT_CONFIG
from adventure.models.planning import CalendarEvent
from adventure.utils.planning import get_recent_notes
actor_config = DEFAULT_CONFIG.world.actor
character_config = DEFAULT_CONFIG.world.character
def take_note(fact: str):
@ -16,16 +16,16 @@ def take_note(fact: str):
fact: The fact to remember.
"""
with action_context() as (_, action_actor):
if fact in action_actor.planner.notes:
with action_context() as (_, action_character):
if fact in action_character.planner.notes:
raise ActionError("You already have a note about that fact.")
if len(action_actor.planner.notes) >= actor_config.note_limit:
if len(action_character.planner.notes) >= character_config.note_limit:
raise ActionError(
"You have reached the limit of notes you can take. Please erase, replace, or summarize some notes."
)
action_actor.planner.notes.append(fact)
action_character.planner.notes.append(fact)
return "You make a note of that fact."
@ -38,8 +38,8 @@ def read_notes(unused: bool, count: int = 10):
count: The number of recent notes to read. 10 is usually a good number.
"""
with action_context() as (_, action_actor):
facts = get_recent_notes(action_actor, count=count)
with action_context() as (_, action_character):
facts = get_recent_notes(action_character, count=count)
return "\n".join(facts)
@ -51,15 +51,15 @@ def erase_notes(prefix: str) -> str:
prefix: The prefix to match notes against.
"""
with action_context() as (_, action_actor):
with action_context() as (_, action_character):
matches = [
note for note in action_actor.planner.notes if note.startswith(prefix)
note for note in action_character.planner.notes if note.startswith(prefix)
]
if not matches:
return "No notes found with that prefix."
action_actor.planner.notes[:] = [
note for note in action_actor.planner.notes if note not in matches
action_character.planner.notes[:] = [
note for note in action_character.planner.notes if note not in matches
]
return f"Erased {len(matches)} notes."
@ -73,12 +73,12 @@ def replace_note(old: str, new: str) -> str:
new: The new note to replace it with.
"""
with action_context() as (_, action_actor):
if old not in action_actor.planner.notes:
with action_context() as (_, action_character):
if old not in action_character.planner.notes:
return "Note not found."
action_actor.planner.notes[:] = [
new if note == old else note for note in action_actor.planner.notes
action_character.planner.notes[:] = [
new if note == old else note for note in action_character.planner.notes
]
return "Note replaced."
@ -91,12 +91,12 @@ def summarize_notes(limit: int) -> str:
limit: The maximum number of notes to keep.
"""
with action_context() as (_, action_actor):
notes = action_actor.planner.notes
action_agent = get_agent_for_actor(action_actor)
with action_context() as (_, action_character):
notes = action_character.planner.notes
action_agent = get_agent_for_character(action_character)
if not action_agent:
raise ActionError("Agent missing for actor {action_actor.name}")
raise ActionError("Agent missing for character {action_character.name}")
summary = action_agent(
"Please summarize your notes. Remove any duplicates and combine similar notes. "
@ -110,12 +110,12 @@ def summarize_notes(limit: int) -> str:
)
new_notes = [note.strip() for note in summary.split("\n") if note.strip()]
if len(new_notes) > actor_config.note_limit:
if len(new_notes) > character_config.note_limit:
raise ActionError(
f"Too many notes. You can only have up to {actor_config.note_limit} notes."
f"Too many notes. You can only have up to {character_config.note_limit} notes."
)
action_actor.planner.notes[:] = new_notes
action_character.planner.notes[:] = new_notes
return "Notes were summarized successfully."
@ -130,10 +130,10 @@ def schedule_event(name: str, turns: int):
turns: The number of turns until the event happens.
"""
with action_context() as (_, action_actor):
with action_context() as (_, action_character):
# TODO: check for existing events with the same name
event = CalendarEvent(name, turns)
action_actor.planner.calendar.events.append(event)
action_character.planner.calendar.events.append(event)
return f"{name} is scheduled to happen in {turns} turns."
@ -144,8 +144,8 @@ def check_calendar(unused: bool, count: int = 10):
current_turn = get_current_step()
with action_context() as (_, action_actor):
events = action_actor.planner.calendar.events[:count]
with action_context() as (_, action_character):
events = action_character.planner.calendar.events[:count]
return "\n".join(
[
f"{event.name} will happen in {event.turn - current_turn} turns"

View File

@ -3,56 +3,56 @@ from adventure.systems.quest import (
QUEST_SYSTEM,
complete_quest,
get_active_quest,
get_quests_for_actor,
get_quests_for_character,
set_active_quest,
)
from adventure.utils.search import find_actor_in_room
from adventure.utils.search import find_character_in_room
def accept_quest(actor: str, quest: str) -> str:
def accept_quest(character: str, quest: str) -> str:
"""
Accept and start a quest being given by another character.
"""
with action_context() as (action_room, action_actor):
with action_context() as (action_room, action_character):
quests = get_system_data(QUEST_SYSTEM)
if not quests:
return "No quests available."
target_actor = find_actor_in_room(action_room, actor)
if not target_actor:
return f"{actor} is not in the room."
target_character = find_character_in_room(action_room, character)
if not target_character:
return f"{character} is not in the room."
available_quests = get_quests_for_actor(quests, target_actor)
available_quests = get_quests_for_character(quests, target_character)
for available_quest in available_quests:
if available_quest.name == quest:
set_active_quest(quests, action_actor, available_quest)
set_active_quest(quests, action_character, available_quest)
return f"You have accepted the quest: {quest}"
return f"{actor} does not have the quest: {quest}"
return f"{character} does not have the quest: {quest}"
def submit_quest(actor: str) -> str:
def submit_quest(character: str) -> str:
"""
Submit your active quest to the quest giver. If you have completed the quest, you will be rewarded.
"""
with action_context() as (action_room, action_actor):
with action_context() as (action_room, action_character):
quests = get_system_data(QUEST_SYSTEM)
if not quests:
return "No quests available."
active_quest = get_active_quest(quests, action_actor)
active_quest = get_active_quest(quests, action_character)
if not active_quest:
return "You do not have an active quest."
target_actor = find_actor_in_room(action_room, actor)
if not target_actor:
return f"{actor} is not in the room."
target_character = find_character_in_room(action_room, character)
if not target_character:
return f"{character} is not in the room."
if active_quest.giver.actor == target_actor.name:
complete_quest(quests, action_actor, active_quest)
if active_quest.giver.character == target_character.name:
complete_quest(quests, action_character, active_quest)
return f"You have completed the quest: {active_quest.name}"
return f"{actor} is not the quest giver for your active quest."
return f"{character} is not the quest giver for your active quest."

View File

@ -9,9 +9,9 @@ from discord import Client, Embed, File, Intents
from adventure.context import (
broadcast,
get_actor_agent_for_name,
get_character_agent_for_name,
get_current_world,
set_actor_agent,
set_character_agent,
subscribe,
)
from adventure.models.config import DEFAULT_CONFIG, DiscordBotConfig
@ -101,15 +101,15 @@ class AdventureClient(Client):
await channel.send(f"{character_name} has already been taken!")
return
actor, agent = get_actor_agent_for_name(character_name)
if not actor:
character, agent = get_character_agent_for_name(character_name)
if not character:
await channel.send(f"Character `{character_name}` not found!")
return
def prompt_player(event: PromptEvent):
logger.info(
"append prompt for character %s (user %s) to queue: %s",
event.actor.name,
event.character.name,
user_name,
event.prompt,
)
@ -118,12 +118,12 @@ class AdventureClient(Client):
return True
player = RemotePlayer(
actor.name, actor.backstory, prompt_player, fallback_agent=agent
character.name, character.backstory, prompt_player, fallback_agent=agent
)
set_actor_agent(character_name, actor, player)
set_character_agent(character_name, character, player)
set_player(user_name, player)
logger.info(f"{user_name} has joined the game as {actor.name}!")
logger.info(f"{user_name} has joined the game as {character.name}!")
join_event = PlayerEvent("join", character_name, user_name)
return broadcast(join_event)
@ -133,10 +133,12 @@ class AdventureClient(Client):
remove_player(user_name)
# revert to LLM agent
actor, _ = get_actor_agent_for_name(player.name)
if actor and player.fallback_agent:
character, _ = get_character_agent_for_name(player.name)
if character and player.fallback_agent:
logger.info("restoring LLM agent for %s", player.name)
set_actor_agent(actor.name, actor, player.fallback_agent)
set_character_agent(
character.name, character, player.fallback_agent
)
# broadcast leave event
logger.info("disconnecting player %s from %s", user_name, player.name)
@ -324,7 +326,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.actor.name)
action_embed = Embed(title=event.room.name, description=event.character.name)
if isinstance(event, ActionEvent):
action_name = event.action.replace("action_", "").title()
@ -350,7 +352,7 @@ def embed_from_result(event: ResultEvent):
if len(text) > 1000:
text = text[:1000] + "..."
result_embed = Embed(title=event.room.name, description=event.actor.name)
result_embed = Embed(title=event.room.name, description=event.character.name)
result_embed.add_field(name="Result", value=text)
return result_embed
@ -369,7 +371,7 @@ def embed_from_player(event: PlayerEvent):
def embed_from_prompt(event: PromptEvent):
# TODO: ping the player
prompt_embed = Embed(title=event.room.name, description=event.actor.name)
prompt_embed = Embed(title=event.room.name, description=event.character.name)
prompt_embed.add_field(name="Prompt", value=event.prompt)
return prompt_embed
@ -377,7 +379,7 @@ def embed_from_prompt(event: PromptEvent):
def embed_from_status(event: StatusEvent):
status_embed = Embed(
title=event.room.name if event.room else "",
description=event.actor.name if event.actor else "",
description=event.character.name if event.character else "",
)
status_embed.add_field(name="Status", value=event.text)
return status_embed

View File

@ -18,7 +18,7 @@ from packit.agent import Agent
from pyee.base import EventEmitter
from adventure.game_system import GameSystem
from adventure.models.entity import Actor, Room, World
from adventure.models.entity import Character, Room, World
from adventure.models.event import GameEvent
from adventure.utils.string import normalize_name
@ -28,7 +28,7 @@ logger = getLogger(__name__)
current_step = 0
current_world: World | None = None
current_room: Room | None = None
current_actor: Actor | None = None
current_character: Character | None = None
dungeon_master: Agent | None = None
# game context
@ -38,7 +38,7 @@ system_data: Dict[str, Any] = {}
# TODO: where should this one go?
actor_agents: Dict[str, Tuple[Actor, Agent]] = {}
character_agents: Dict[str, Tuple[Character, Agent]] = {}
STRING_EVENT_TYPE = "message"
@ -88,44 +88,44 @@ def has_dungeon_master():
# region context manager
@contextmanager
def action_context():
room, actor = get_action_context()
yield room, actor
room, character = get_action_context()
yield room, character
@contextmanager
def world_context():
world, room, actor = get_world_context()
yield world, room, actor
world, room, character = get_world_context()
yield world, room, character
# endregion
# region context getters
def get_action_context() -> Tuple[Room, Actor]:
def get_action_context() -> Tuple[Room, Character]:
if not current_room:
raise ValueError("The current room must be set before calling action functions")
if not current_actor:
if not current_character:
raise ValueError(
"The current actor must be set before calling action functions"
"The current character must be set before calling action functions"
)
return (current_room, current_actor)
return (current_room, current_character)
def get_world_context() -> Tuple[World, Room, Actor]:
def get_world_context() -> Tuple[World, Room, Character]:
if not current_world:
raise ValueError(
"The current world must be set before calling action functions"
)
if not current_room:
raise ValueError("The current room must be set before calling action functions")
if not current_actor:
if not current_character:
raise ValueError(
"The current actor must be set before calling action functions"
"The current character must be set before calling action functions"
)
return (current_world, current_room, current_actor)
return (current_world, current_room, current_character)
def get_current_world() -> World | None:
@ -136,8 +136,8 @@ def get_current_room() -> Room | None:
return current_room
def get_current_actor() -> Actor | None:
return current_actor
def get_current_character() -> Character | None:
return current_character
def get_current_step() -> int:
@ -175,9 +175,9 @@ def set_current_room(room: Room | None):
current_room = room
def set_current_actor(actor: Actor | None):
global current_actor
current_actor = actor
def set_current_character(character: Character | None):
global current_character
current_character = character
def set_current_step(step: int):
@ -185,8 +185,8 @@ def set_current_step(step: int):
current_step = step
def set_actor_agent(name, actor, agent):
actor_agents[name] = (actor, agent)
def set_character_agent(name, character, agent):
character_agents[name] = (character, agent)
def set_dungeon_master(agent):
@ -207,41 +207,41 @@ def set_system_data(system: str, data: Any):
# region search functions
def get_actor_for_agent(agent):
def get_character_for_agent(agent):
return next(
(
inner_actor
for inner_actor, inner_agent in actor_agents.values()
inner_character
for inner_character, inner_agent in character_agents.values()
if inner_agent == agent
),
None,
)
def get_agent_for_actor(actor):
def get_agent_for_character(character):
return next(
(
inner_agent
for inner_actor, inner_agent in actor_agents.values()
if inner_actor == actor
for inner_character, inner_agent in character_agents.values()
if inner_character == character
),
None,
)
def get_actor_agent_for_name(name):
def get_character_agent_for_name(name):
return next(
(
(actor, agent)
for actor, agent in actor_agents.values()
if normalize_name(actor.name) == normalize_name(name)
(character, agent)
for character, agent in character_agents.values()
if normalize_name(character.name) == normalize_name(name)
),
(None, None),
)
def get_all_actor_agents():
return list(actor_agents.values())
def get_all_character_agents():
return list(character_agents.values())
# endregion

View File

@ -16,15 +16,15 @@ from adventure.models.effect import (
IntEffectPattern,
StringEffectPattern,
)
from adventure.models.entity import Actor, Item, Portal, Room, World, WorldEntity
from adventure.models.entity import Character, Item, Portal, Room, World, WorldEntity
from adventure.models.event import GenerateEvent
from adventure.utils import try_parse_float, try_parse_int
from adventure.utils.effect import resolve_int_range
from adventure.utils.search import (
list_actors,
list_actors_in_room,
list_characters,
list_characters_in_room,
list_items,
list_items_in_actor,
list_items_in_character,
list_items_in_room,
list_rooms,
)
@ -107,7 +107,7 @@ def generate_room(
)
actions = {}
room = Room(name=name, description=desc, items=[], actors=[], actions=actions)
room = Room(name=name, description=desc, items=[], characters=[], actions=actions)
item_count = resolve_int_range(world_config.size.room_items) or 0
broadcast_generated(f"Generating {item_count} items for room: {name}")
@ -126,22 +126,24 @@ def generate_room(
except Exception:
logger.exception("error generating item")
actor_count = resolve_int_range(world_config.size.room_actors) or 0
broadcast_generated(message=f"Generating {actor_count} actors for room: {name}")
character_count = resolve_int_range(world_config.size.room_characters) or 0
broadcast_generated(
message=f"Generating {character_count} characters for room: {name}"
)
for _ in range(actor_count):
for _ in range(character_count):
try:
actor = generate_actor(
character = generate_character(
agent,
world,
systems=systems,
dest_room=room,
)
broadcast_generated(entity=actor)
broadcast_generated(entity=character)
room.actors.append(actor)
room.characters.append(character)
except Exception:
logger.exception("error generating actor")
logger.exception("error generating character")
continue
return room
@ -218,18 +220,20 @@ def generate_item(
world: World,
systems: List[GameSystem],
dest_room: Room | None = None,
dest_actor: Actor | None = None,
dest_character: Character | None = None,
) -> Item:
existing_items = [
item.name
for item in list_items(
world, include_actor_inventory=True, include_item_inventory=True
world, include_character_inventory=True, include_item_inventory=True
)
]
if dest_actor:
dest_note = f"The item will be held by the {dest_actor.name} character"
existing_items += [item.name for item in list_items_in_actor(dest_actor)]
if dest_character:
dest_note = f"The item will be held by the {dest_character.name} character"
existing_items += [
item.name for item in list_items_in_character(dest_character)
]
elif dest_room:
dest_note = f"The item will be placed in the {dest_room.name} room"
existing_items += [item.name for item in list_items_in_room(dest_room)]
@ -275,14 +279,14 @@ def generate_item(
return item
def generate_actor(
def generate_character(
agent: Agent,
world: World,
systems: List[GameSystem],
dest_room: Room,
) -> Actor:
existing_actors = [actor.name for actor in list_actors(world)] + [
actor.name for actor in list_actors_in_room(dest_room)
) -> Character:
existing_characters = [character.name for character in list_characters(world)] + [
character.name for character in list_characters_in_room(dest_room)
]
name = loop_retry(
@ -292,17 +296,17 @@ def generate_actor(
"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. '
"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_characters}",
context={
"dest_room": dest_room.name,
"existing_actors": existing_actors,
"existing_characters": existing_characters,
"world_theme": world.theme,
},
result_parser=duplicate_name_parser(existing_actors),
result_parser=duplicate_name_parser(existing_characters),
toolbox=None,
)
broadcast_generated(message=f"Generating actor: {name}")
broadcast_generated(message=f"Generating character: {name}")
description = agent(
"Generate a detailed description of the {name} character. What do they look like? What are they wearing? "
"What are they doing? Describe their appearance from the perspective of an outside observer."
@ -310,19 +314,19 @@ def generate_actor(
name=name,
)
backstory = agent(
"Generate a backstory for the {name} actor. Where are they from? What are they doing here? What are their "
"Generate a backstory for the {name} character. 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,
)
actor = Actor(
character = Character(
name=name, backstory=backstory, description=description, actions={}, items=[]
)
generate_system_attributes(agent, world, actor, systems)
generate_system_attributes(agent, world, character, systems)
# generate the actor's inventory
item_count = resolve_int_range(world_config.size.actor_items) or 0
broadcast_generated(f"Generating {item_count} items for actor {name}")
# generate the character's inventory
item_count = resolve_int_range(world_config.size.character_items) or 0
broadcast_generated(f"Generating {item_count} items for character {name}")
for k in range(item_count):
try:
@ -330,16 +334,16 @@ def generate_actor(
agent,
world,
systems,
dest_actor=actor,
dest_character=character,
)
generate_system_attributes(agent, world, item, systems)
broadcast_generated(entity=item)
actor.items.append(item)
character.items.append(item)
except Exception:
logger.exception("error generating item")
return actor
return character
def generate_effect(agent: Agent, world: World, entity: Item) -> EffectPattern:
@ -534,6 +538,8 @@ def generate_world(
# generate portals to link the rooms together
link_rooms(agent, world, systems)
# ensure actors act in a stable order
world.order = [actor.name for room in world.rooms for actor in room.actors]
# ensure characters act in a stable order
world.order = [
character.name for room in world.rooms for character in room.characters
]
return world

View File

@ -41,7 +41,7 @@ class ServerConfig:
@dataclass
class WorldActorConfig:
class WorldCharacterConfig:
conversation_limit: int
event_limit: int
note_limit: int
@ -49,18 +49,26 @@ class WorldActorConfig:
@dataclass
class WorldSizeConfig:
actor_items: int | IntRange
character_items: int | IntRange
item_effects: int | IntRange
portals: int | IntRange
room_actors: int | IntRange
room_characters: int | IntRange
room_items: int | IntRange
rooms: int | IntRange
@dataclass
class WorldStepConfig:
action_retries: int
planning_steps: int
planning_retries: int
@dataclass
class WorldConfig:
actor: WorldActorConfig
character: WorldCharacterConfig
size: WorldSizeConfig
step: WorldStepConfig
@dataclass
@ -88,18 +96,23 @@ DEFAULT_CONFIG = Config(
),
server=ServerConfig(websocket=WebsocketServerConfig(host="localhost", port=8001)),
world=WorldConfig(
actor=WorldActorConfig(
character=WorldCharacterConfig(
conversation_limit=2,
event_limit=5,
note_limit=10,
),
size=WorldSizeConfig(
actor_items=IntRange(min=0, max=2),
character_items=IntRange(min=0, max=2),
item_effects=IntRange(min=1, max=1),
portals=IntRange(min=1, max=3),
rooms=IntRange(min=3, max=6),
room_actors=IntRange(min=1, max=3),
room_characters=IntRange(min=1, max=3),
room_items=IntRange(min=1, max=3),
),
step=WorldStepConfig(
action_retries=5,
planning_steps=3,
planning_retries=3,
),
),
)

View File

@ -23,7 +23,7 @@ class Item(BaseModel):
@dataclass
class Actor(BaseModel):
class Character(BaseModel):
name: str
backstory: str
description: str
@ -33,7 +33,7 @@ class Actor(BaseModel):
attributes: Attributes = Field(default_factory=dict)
items: List[Item] = Field(default_factory=list)
id: str = Field(default_factory=uuid)
type: Literal["actor"] = "actor"
type: Literal["character"] = "character"
@dataclass
@ -51,7 +51,7 @@ class Portal(BaseModel):
class Room(BaseModel):
name: str
description: str
actors: List[Actor] = Field(default_factory=list)
characters: List[Character] = Field(default_factory=list)
actions: Actions = Field(default_factory=dict)
active_effects: List[EffectResult] = Field(default_factory=list)
attributes: Attributes = Field(default_factory=dict)
@ -80,12 +80,12 @@ class WorldState(BaseModel):
type: Literal["world_state"] = "world_state"
WorldEntity = Room | Actor | Item | Portal
WorldEntity = Room | Character | Item | Portal
@dataclass
class EntityReference:
actor: str | None = None
character: str | None = None
item: str | None = None
portal: str | None = None
room: str | None = None

View File

@ -4,7 +4,7 @@ from typing import Any, Callable, Dict, List, Literal, Union
from pydantic import Field
from .base import BaseModel, dataclass, uuid
from .entity import Actor, Item, Room, WorldEntity
from .entity import Character, Item, Room, WorldEntity
@dataclass
@ -30,26 +30,26 @@ class GenerateEvent(BaseModel):
@dataclass
class ActionEvent(BaseModel):
"""
An actor has taken an action.
A character has taken an action.
"""
action: str
parameters: Dict[str, bool | float | int | str]
room: Room
actor: Actor
character: Character
item: Item | None = None
id: str = Field(default_factory=uuid)
type: Literal["action"] = "action"
@staticmethod
def from_json(json: str, room: Room, actor: Actor) -> "ActionEvent":
def from_json(json: str, room: Room, character: Character) -> "ActionEvent":
openai_json = loads(json)
return ActionEvent(
action=openai_json["function"],
parameters=openai_json["parameters"],
room=room,
actor=actor,
character=character,
item=None,
)
@ -57,12 +57,12 @@ class ActionEvent(BaseModel):
@dataclass
class PromptEvent(BaseModel):
"""
A prompt for an actor to take an action.
A prompt for a character to take an action.
"""
prompt: str
room: Room
actor: Actor
character: Character
id: str = Field(default_factory=uuid)
type: Literal["prompt"] = "prompt"
@ -70,22 +70,22 @@ class PromptEvent(BaseModel):
@dataclass
class ReplyEvent(BaseModel):
"""
An actor has replied with text.
A character has replied with text.
This is the non-JSON version of an ActionEvent.
TODO: add the actor being replied to.
TODO: add the character being replied to.
"""
text: str
room: Room
actor: Actor
character: Character
id: str = Field(default_factory=uuid)
type: Literal["reply"] = "reply"
@staticmethod
def from_text(text: str, room: Room, actor: Actor) -> "ReplyEvent":
return ReplyEvent(text=text, room=room, actor=actor)
def from_text(text: str, room: Room, character: Character) -> "ReplyEvent":
return ReplyEvent(text=text, room=room, character=character)
@dataclass
@ -96,7 +96,7 @@ class ResultEvent(BaseModel):
result: str
room: Room
actor: Actor
character: Character
id: str = Field(default_factory=uuid)
type: Literal["result"] = "result"
@ -109,7 +109,7 @@ class StatusEvent(BaseModel):
text: str
room: Room | None = None
actor: Actor | None = None
character: Character | None = None
id: str = Field(default_factory=uuid)
type: Literal["status"] = "status"
@ -120,7 +120,7 @@ class SnapshotEvent(BaseModel):
A snapshot of the world state.
This one is slightly unusual, because the world has already been dumped to a JSON-compatible dictionary.
That is especially important for the memory, which is a dictionary of actor names to lists of messages.
That is especially important for the memory, which is a dictionary of character names to lists of messages.
"""
world: Dict[str, Any]

View File

@ -178,9 +178,9 @@ class RemotePlayer(BasePlayer):
formatted_prompt = prompt.format(**kwargs)
self.memory.append(HumanMessage(content=formatted_prompt))
with action_context() as (current_room, current_actor):
with action_context() as (current_room, current_character):
prompt_event = PromptEvent(
prompt=formatted_prompt, room=current_room, actor=current_actor
prompt=formatted_prompt, room=current_room, character=current_character
)
try:

View File

@ -226,14 +226,16 @@ def fast_hash(text: str) -> str:
def get_image_prefix(event: GameEvent | WorldEntity) -> str:
if isinstance(event, ActionEvent):
return sanitize_name(f"event-action-{event.actor.name}-{event.action}")
return sanitize_name(f"event-action-{event.character.name}-{event.action}")
if isinstance(event, ReplyEvent):
return sanitize_name(f"event-reply-{event.actor.name}-{fast_hash(event.text)}")
return sanitize_name(
f"event-reply-{event.character.name}-{fast_hash(event.text)}"
)
if isinstance(event, ResultEvent):
return sanitize_name(
f"event-result-{event.actor.name}-{fast_hash(event.result)}"
f"event-result-{event.character.name}-{fast_hash(event.result)}"
)
if isinstance(event, StatusEvent):

View File

@ -12,7 +12,7 @@ from adventure.models.event import (
ResultEvent,
StatusEvent,
)
from adventure.utils.search import find_actor_in_room, find_item_in_room, find_room
from adventure.utils.search import find_character_in_room, find_item_in_room, find_room
from adventure.utils.world import describe_entity
logger = getLogger(__name__)
@ -28,11 +28,11 @@ def prompt_from_parameters(
# look up the character
character_name = str(parameters["character"])
logger.debug("searching for parameter character: %s", character_name)
target_actor = find_actor_in_room(action_room, character_name)
if target_actor:
logger.debug("adding actor to prompt: %s", target_actor.name)
pre.append(f"with {target_actor.name}")
post.append(describe_entity(target_actor))
target_character = find_character_in_room(action_room, character_name)
if target_character:
logger.debug("adding character to prompt: %s", target_character.name)
pre.append(f"with {target_character.name}")
post.append(describe_entity(target_character))
if "item" in parameters:
# look up the item
@ -41,7 +41,7 @@ def prompt_from_parameters(
target_item = find_item_in_room(
action_room,
item_name,
include_actor_inventory=True,
include_character_inventory=True,
include_item_inventory=True,
)
if target_item:
@ -50,7 +50,7 @@ def prompt_from_parameters(
post.append(describe_entity(target_item))
if "target" in parameters:
# could be a room, actor, or item
# could be a room, character, or item
target_name = str(parameters["target"])
logger.debug("searching for parameter target: %s", target_name)
@ -62,16 +62,16 @@ def prompt_from_parameters(
pre.append(f"in the {target_room.name}")
post.append(describe_entity(target_room))
target_actor = find_actor_in_room(action_room, target_name)
if target_actor:
logger.debug("adding actor to prompt: %s", target_actor.name)
pre.append(f"with {target_actor.name}")
post.append(describe_entity(target_actor))
target_character = find_character_in_room(action_room, target_name)
if target_character:
logger.debug("adding character to prompt: %s", target_character.name)
pre.append(f"with {target_character.name}")
post.append(describe_entity(target_character))
target_item = find_item_in_room(
action_room,
target_name,
include_actor_inventory=True,
include_character_inventory=True,
include_item_inventory=True,
)
if target_item:
@ -92,20 +92,20 @@ def scene_from_event(event: GameEvent) -> str | None:
)
return (
f"{event.actor.name} uses the {action_name} action {parameter_pre}. "
"{describe_entity(event.actor)}. {describe_entity(event.room)}. {parameter_post}."
f"{event.character.name} uses the {action_name} action {parameter_pre}. "
"{describe_entity(event.character)}. {describe_entity(event.room)}. {parameter_post}."
)
if isinstance(event, ReplyEvent):
return f"{event.actor.name} replies: {event.text}. {describe_entity(event.actor)}. {describe_entity(event.room)}."
return f"{event.character.name} replies: {event.text}. {describe_entity(event.character)}. {describe_entity(event.room)}."
if isinstance(event, ResultEvent):
return f"{event.result}. {describe_entity(event.actor)}. {describe_entity(event.room)}."
return f"{event.result}. {describe_entity(event.character)}. {describe_entity(event.room)}."
if isinstance(event, StatusEvent):
if event.room:
if event.actor:
return f"{event.text}. {describe_entity(event.actor)}. {describe_entity(event.room)}."
if event.character:
return f"{event.text}. {describe_entity(event.character)}. {describe_entity(event.room)}."
return f"{event.text}. {describe_entity(event.room)}."

View File

@ -14,13 +14,13 @@ from pydantic import RootModel
from adventure.context import (
broadcast,
get_actor_agent_for_name,
get_character_agent_for_name,
get_current_world,
set_actor_agent,
set_character_agent,
subscribe,
)
from adventure.models.config import DEFAULT_CONFIG, WebsocketServerConfig
from adventure.models.entity import Actor, Item, Room, World
from adventure.models.entity import Character, Item, Room, World
from adventure.models.event import (
GameEvent,
PlayerEvent,
@ -38,7 +38,7 @@ from adventure.player import (
)
from adventure.render.comfy import render_entity, render_event
from adventure.state import snapshot_world, world_json
from adventure.utils.search import find_actor, find_item, find_portal, find_room
from adventure.utils.search import find_character, find_item, find_portal, find_room
logger = getLogger(__name__)
@ -76,8 +76,8 @@ async def handler(websocket):
def sync_turn(event: PromptEvent) -> bool:
# TODO: nothing about this is good
player = get_player(id)
if player and player.name == event.actor.name:
asyncio.run(next_turn(event.actor.name, event.prompt))
if player and player.name == event.character.name:
asyncio.run(next_turn(event.character.name, event.prompt))
return True
return False
@ -137,9 +137,11 @@ async def handler(websocket):
# TODO: should this always remove?
remove_player(id)
actor, llm_agent = get_actor_agent_for_name(character_name)
if not actor:
logger.error(f"Failed to find actor {character_name}")
character, llm_agent = get_character_agent_for_name(
character_name
)
if not character:
logger.error(f"Failed to find character {character_name}")
continue
# prevent any recursive fallback bugs
@ -150,8 +152,8 @@ async def handler(websocket):
llm_agent = llm_agent.fallback_agent
player = RemotePlayer(
actor.name,
actor.backstory,
character.name,
character.backstory,
sync_turn,
fallback_agent=llm_agent,
)
@ -161,7 +163,7 @@ async def handler(websocket):
)
# swap out the LLM agent
set_actor_agent(actor.name, actor, player)
set_character_agent(character.name, character, player)
# notify all clients that this character is now active
broadcast_player_event(character_name, player_name, "join")
@ -195,10 +197,10 @@ async def handler(websocket):
broadcast_player_event(player.name, player_name, "leave")
broadcast_player_list()
actor, _ = get_actor_agent_for_name(player.name)
if actor and player.fallback_agent:
character, _ = get_character_agent_for_name(player.name)
if character and player.fallback_agent:
logger.info("restoring LLM agent for %s", player.name)
set_actor_agent(player.name, actor, player.fallback_agent)
set_character_agent(player.name, character, player.fallback_agent)
logger.info("client disconnected: %s", id)
@ -220,17 +222,20 @@ def render_input(data):
render_event(event)
else:
logger.error(f"failed to find event {event_id}")
elif "actor" in data:
actor_name = data["actor"]
actor = find_actor(world, actor_name)
if actor:
render_entity(actor)
elif "character" in data:
character_name = data["character"]
character = find_character(world, character_name)
if character:
render_entity(character)
else:
logger.error(f"failed to find actor {actor_name}")
logger.error(f"failed to find character {character_name}")
elif "item" in data:
item_name = data["item"]
item = find_item(
world, item_name, include_actor_inventory=True, include_item_inventory=True
world,
item_name,
include_character_inventory=True,
include_item_inventory=True,
)
if item:
render_entity(item)
@ -258,7 +263,7 @@ socket_thread = None
def server_json(obj):
if isinstance(obj, (Actor, Item, Room)):
if isinstance(obj, (Character, Item, Room)):
return obj.name
return world_json(obj)

View File

@ -7,7 +7,7 @@ from typing import Callable, Sequence
from packit.agent import Agent
from packit.conditions import condition_or, condition_threshold
from packit.loops import loop_reduce, loop_retry
from packit.loops import loop_retry
from packit.results import multi_function_or_str_result
from packit.toolbox import Toolbox
from packit.utils import could_be_json
@ -32,28 +32,32 @@ from adventure.actions.planning import (
)
from adventure.context import (
broadcast,
get_actor_agent_for_name,
get_actor_for_agent,
get_character_agent_for_name,
get_character_for_agent,
get_current_step,
get_current_world,
set_current_actor,
set_current_character,
set_current_room,
set_current_step,
set_current_world,
set_game_systems,
)
from adventure.game_system import GameSystem
from adventure.models.entity import Actor, Room, World
from adventure.models.config import DEFAULT_CONFIG
from adventure.models.entity import Character, Room, World
from adventure.models.event import ActionEvent, ReplyEvent, ResultEvent
from adventure.utils.conversation import make_keyword_condition, summarize_room
from adventure.utils.effect import expire_effects
from adventure.utils.planning import expire_events, get_upcoming_events
from adventure.utils.search import find_room_with_actor
from adventure.utils.search import find_room_with_character
from adventure.utils.world import describe_entity, format_attributes
logger = getLogger(__name__)
step_config = DEFAULT_CONFIG.world.step
def world_result_parser(value, agent, **kwargs):
current_world = get_current_world()
if not current_world:
@ -63,35 +67,36 @@ def world_result_parser(value, agent, **kwargs):
logger.debug(f"parsing action for {agent.name}: {value}")
current_actor = get_actor_for_agent(agent)
current_character = get_character_for_agent(agent)
current_room = next(
(room for room in current_world.rooms if current_actor in room.actors), None
(room for room in current_world.rooms if current_character in room.characters),
None,
)
set_current_room(current_room)
set_current_actor(current_actor)
set_current_character(current_character)
return multi_function_or_str_result(value, agent=agent, **kwargs)
def prompt_actor_action(
room, actor, agent, action_names, action_toolbox, current_turn
def prompt_character_action(
room, character, agent, action_names, action_toolbox, current_turn
) -> str:
# collect data for the prompt
notes_prompt, events_prompt = get_notes_events(actor, current_turn)
notes_prompt, events_prompt = get_notes_events(character, current_turn)
room_actors = [actor.name for actor in room.actors]
room_characters = [character.name for character in room.characters]
room_items = [item.name for item in room.items]
room_directions = [portal.name for portal in room.portals]
actor_attributes = format_attributes(actor)
# actor_effects = [effect.name for effect in actor.active_effects]
actor_items = [item.name for item in actor.items]
character_attributes = format_attributes(character)
# character_effects = [effect.name for effect in character.active_effects]
character_items = [item.name for item in character.items]
# set up a result parser for the agent
def result_parser(value, agent, **kwargs):
if not room or not actor:
raise ValueError("Room and actor must be set before parsing results")
if not room or not character:
raise ValueError("Room and character must be set before parsing results")
# trim suffixes that are used elsewhere
value = value.removesuffix("END").strip()
@ -110,23 +115,23 @@ def prompt_actor_action(
pass
if could_be_json(value):
event = ActionEvent.from_json(value, room, actor)
event = ActionEvent.from_json(value, room, character)
else:
event = ReplyEvent.from_text(value, room, actor)
event = ReplyEvent.from_text(value, room, character)
broadcast(event)
return world_result_parser(value, agent, **kwargs)
# prompt and act
logger.info("starting turn for actor: %s", actor.name)
logger.info("starting turn for character: %s", character.name)
result = loop_retry(
agent,
(
"You are currently in the {room_name} room. {room_description}. {attributes}. "
"The room contains the following characters: {visible_actors}. "
"The room contains the following characters: {visible_characters}. "
"The room contains the following items: {visible_items}. "
"Your inventory contains the following items: {actor_items}."
"Your inventory contains the following items: {character_items}."
"You can take the following actions: {actions}. "
"You can move in the following directions: {directions}. "
"{notes_prompt} {events_prompt}"
@ -135,12 +140,12 @@ def prompt_actor_action(
),
context={
"actions": action_names,
"actor_items": actor_items,
"attributes": actor_attributes,
"character_items": character_items,
"attributes": character_attributes,
"directions": room_directions,
"room_name": room.name,
"room_description": describe_entity(room),
"visible_actors": room_actors,
"visible_characters": room_characters,
"visible_items": room_items,
"notes_prompt": notes_prompt,
"events_prompt": events_prompt,
@ -149,7 +154,7 @@ def prompt_actor_action(
toolbox=action_toolbox,
)
logger.debug(f"{actor.name} step result: {result}")
logger.debug(f"{character.name} step result: {result}")
if agent.memory:
# TODO: make sure this is not duplicating memories and wasting space
agent.memory.append(result)
@ -157,9 +162,9 @@ def prompt_actor_action(
return result
def get_notes_events(actor: Actor, current_turn: int):
recent_notes = get_recent_notes(actor)
upcoming_events = get_upcoming_events(actor, current_turn)
def get_notes_events(character: Character, current_turn: int):
recent_notes = get_recent_notes(character)
upcoming_events = get_upcoming_events(character, current_turn)
if len(recent_notes) > 0:
notes = "\n".join(recent_notes)
@ -181,19 +186,30 @@ def get_notes_events(actor: Actor, current_turn: int):
return notes_prompt, events_prompt
def prompt_actor_think(
room: Room, actor: Actor, agent: Agent, planner_toolbox: Toolbox, current_turn: int
def prompt_character_think(
room: Room,
character: Character,
agent: Agent,
planner_toolbox: Toolbox,
current_turn: int,
max_steps: int | None = None,
) -> str:
notes_prompt, events_prompt = get_notes_events(actor, current_turn)
max_steps = max_steps or step_config.planning_steps
event_count = len(actor.planner.calendar.events)
note_count = len(actor.planner.notes)
notes_prompt, events_prompt = get_notes_events(character, current_turn)
logger.info("starting planning for actor: %s", actor.name)
event_count = len(character.planner.calendar.events)
note_count = len(character.planner.notes)
logger.info("starting planning for character: %s", character.name)
_, condition_end, result_parser = make_keyword_condition("You are done planning.")
stop_condition = condition_or(condition_end, partial(condition_threshold, max=3))
stop_condition = condition_or(
condition_end, partial(condition_threshold, max=max_steps)
)
result = loop_reduce(
i = 0
while not stop_condition(current=i):
result = loop_retry(
agent,
"You are about to start your turn. Plan your next action carefully. Take notes and schedule events to help keep track of your goals. "
"You can check your notes for important facts or check your calendar for upcoming events. You have {note_count} notes. "
@ -209,7 +225,7 @@ def prompt_actor_think(
"events_prompt": events_prompt,
"note_count": note_count,
"notes_prompt": notes_prompt,
"room_summary": summarize_room(room, actor),
"room_summary": summarize_room(room, character),
},
result_parser=result_parser,
stop_condition=stop_condition,
@ -219,6 +235,8 @@ def prompt_actor_think(
if agent.memory:
agent.memory.append(result)
i += 1
return result
@ -259,44 +277,46 @@ def simulate_world(
]
)
# simulate each actor
# simulate each character
for i in count():
current_step = get_current_step()
logger.info(f"simulating step {i} of {steps} (world step {current_step})")
for actor_name in world.order:
actor, agent = get_actor_agent_for_name(actor_name)
if not agent or not actor:
logger.error(f"agent or actor not found for name {actor_name}")
for character_name in world.order:
character, agent = get_character_agent_for_name(character_name)
if not agent or not character:
logger.error(f"agent or character not found for name {character_name}")
continue
room = find_room_with_actor(world, actor)
room = find_room_with_character(world, character)
if not room:
logger.error(f"actor {actor_name} is not in a room")
logger.error(f"character {character_name} is not in a room")
continue
# prep context
set_current_room(room)
set_current_actor(actor)
set_current_character(character)
# decrement effects on the actor and remove any that have expired
expire_effects(actor)
expire_events(actor, current_step)
# decrement effects on the character and remove any that have expired
expire_effects(character)
expire_events(character, current_step)
# give the actor a chance to think and check their planner
# give the character a chance to think and check their planner
if agent.memory and len(agent.memory) > 0:
try:
thoughts = prompt_actor_think(
room, actor, agent, planner_toolbox, current_step
thoughts = prompt_character_think(
room, character, agent, planner_toolbox, current_step
)
logger.debug(f"{actor.name} thinks: {thoughts}")
logger.debug(f"{character.name} thinks: {thoughts}")
except Exception:
logger.exception(f"error during planning for actor {actor.name}")
result = prompt_actor_action(
room, actor, agent, action_names, action_tools, current_step
logger.exception(
f"error during planning for character {character.name}"
)
result_event = ResultEvent(result=result, room=room, actor=actor)
result = prompt_character_action(
room, character, agent, action_names, action_tools, current_step
)
result_event = ResultEvent(result=result, room=room, character=character)
broadcast(result_event)
for system in systems:

View File

@ -7,7 +7,7 @@ from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, System
from packit.agent import Agent, agent_easy_connect
from pydantic import RootModel
from adventure.context import get_all_actor_agents, set_actor_agent
from adventure.context import get_all_character_agents, set_character_agent
from adventure.models.entity import World
from adventure.player import LocalPlayer
@ -17,19 +17,19 @@ def create_agents(
memory: Dict[str, List[str | Dict[str, str]]] = {},
players: List[str] = [],
):
# set up agents for each actor
# set up agents for each character
llm = agent_easy_connect()
for room in world.rooms:
for actor in room.actors:
if actor.name in players:
agent = LocalPlayer(actor.name, actor.backstory)
agent_memory = restore_memory(memory.get(actor.name, []))
for character in room.characters:
if character.name in players:
agent = LocalPlayer(character.name, character.backstory)
agent_memory = restore_memory(memory.get(character.name, []))
agent.load_history(agent_memory)
else:
agent = Agent(actor.name, actor.backstory, {}, llm)
agent.memory = restore_memory(memory.get(actor.name, []))
set_actor_agent(actor.name, actor, agent)
agent = Agent(character.name, character.backstory, {}, llm)
agent.memory = restore_memory(memory.get(character.name, []))
set_character_agent(character.name, character, agent)
def graph_world(world: World, step: int):
@ -38,8 +38,8 @@ def graph_world(world: World, step: int):
graph_name = f"{path.basename(world.name)}-{step}"
graph = graphviz.Digraph(graph_name, format="png")
for room in world.rooms:
actors = [actor.name for actor in room.actors]
room_label = "\n".join([room.name, *actors])
characters = [character.name for character in room.characters]
room_label = "\n".join([room.name, *characters])
graph.node(room.name, room_label)
for portal in room.portals:
graph.edge(room.name, portal.destination, label=portal.name)
@ -54,8 +54,8 @@ def snapshot_world(world: World, step: int):
json_memory = {}
for actor, agent in get_all_actor_agents():
json_memory[actor.name] = list(agent.memory or [])
for character, agent in get_all_character_agents():
json_memory[character.name] = list(agent.memory or [])
return {
"world": json_world,

View File

@ -138,9 +138,9 @@ def update_logic(
) -> None:
for room in world.rooms:
update_attributes(room, rules=rules, triggers=triggers)
for actor in room.actors:
update_attributes(actor, rules=rules, triggers=triggers)
for item in actor.items:
for character in room.characters:
update_attributes(character, rules=rules, triggers=triggers)
for item in character.items:
update_attributes(item, rules=rules, triggers=triggers)
for item in room.items:
update_attributes(item, rules=rules, triggers=triggers)

View File

@ -8,7 +8,7 @@ from adventure.context import get_system_data
from adventure.game_system import GameSystem, SystemData
from adventure.models.base import Attributes, dataclass, uuid
from adventure.models.entity import (
Actor,
Character,
EntityReference,
Item,
Room,
@ -35,8 +35,8 @@ class QuestGoalContains:
Quest goal for any kind of fetch quest, including delivery and escort quests.
Valid combinations are:
- container: Room and items: List[Actor | Item]
- container: Actor and items: List[Item]
- container: Room and items: List[Character | Item]
- container: Character and items: List[Item]
"""
container: EntityReference
@ -98,7 +98,7 @@ def is_quest_complete(world: World, quest: Quest) -> bool:
if content.item:
if not find_item_in_room(container, content.item):
return False
elif isinstance(container, (Actor, Item)):
elif isinstance(container, (Character, Item)):
if content.item:
if not find_item_in_container(container, content.item):
return False
@ -122,41 +122,41 @@ def is_quest_complete(world: World, quest: Quest) -> bool:
# region state management
def get_quests_for_actor(quests: QuestData, actor: Actor) -> List[Quest]:
def get_quests_for_character(quests: QuestData, character: Character) -> List[Quest]:
"""
Get all quests for the given actor.
Get all quests for the given character.
"""
return quests.available.get(actor.name, [])
return quests.available.get(character.name, [])
def set_active_quest(quests: QuestData, actor: Actor, quest: Quest) -> None:
def set_active_quest(quests: QuestData, character: Character, quest: Quest) -> None:
"""
Set the active quest for the given actor.
Set the active quest for the given character.
"""
quests.active[actor.name] = quest
quests.active[character.name] = quest
def get_active_quest(quests: QuestData, actor: Actor) -> Quest | None:
def get_active_quest(quests: QuestData, character: Character) -> Quest | None:
"""
Get the active quest for the given actor.
Get the active quest for the given character.
"""
return quests.active.get(actor.name)
return quests.active.get(character.name)
def complete_quest(quests: QuestData, actor: Actor, quest: Quest) -> None:
def complete_quest(quests: QuestData, character: Character, quest: Quest) -> None:
"""
Complete the given quest for the given actor.
Complete the given quest for the given character.
"""
if quest in quests.available.get(actor.name, []):
quests.available[actor.name].remove(quest)
if quest in quests.available.get(character.name, []):
quests.available[character.name].remove(quest)
if quest == quests.active.get(actor.name, None):
del quests.active[actor.name]
if quest == quests.active.get(character.name, None):
del quests.active[character.name]
if actor.name not in quests.completed:
quests.completed[actor.name] = []
if character.name not in quests.completed:
quests.completed[character.name] = []
quests.completed[actor.name].append(quest)
quests.completed[character.name].append(quest)
# endregion
@ -180,8 +180,8 @@ def generate_quests(agent: Agent, theme: str, entity: WorldEntity) -> None:
if not quests:
raise ValueError("Quest data is required for quest generation")
if isinstance(entity, Actor):
available_quests = get_quests_for_actor(quests, entity)
if isinstance(entity, Character):
available_quests = get_quests_for_character(quests, entity)
if len(available_quests) == 0:
logger.info(f"generating new quest for {entity.name}")
# TODO: generate one new quest
@ -201,13 +201,17 @@ def simulate_quests(world: World, step: int, data: QuestData | None = None) -> N
raise ValueError("Quest data is required for simulation")
for room in world.rooms:
for actor in room.actors:
active_quest = get_active_quest(quests, actor)
for character in room.characters:
active_quest = get_active_quest(quests, character)
if active_quest:
logger.info(f"simulating quest for {actor.name}: {active_quest.name}")
logger.info(
f"simulating quest for {character.name}: {active_quest.name}"
)
if is_quest_complete(world, active_quest):
logger.info(f"quest complete for {actor.name}: {active_quest.name}")
complete_quest(quests, actor, active_quest)
logger.info(
f"quest complete for {character.name}: {active_quest.name}"
)
complete_quest(quests, character, active_quest)
def load_quest_data(file: str) -> QuestData:

View File

@ -31,19 +31,19 @@ def action_craft(item: str) -> str:
Args:
item: The name of the item to craft.
"""
with world_context() as (action_world, _, action_actor):
with world_context() as (action_world, _, action_character):
if item not in recipes:
return f"There is no recipe to craft a {item}."
recipe = recipes[item]
# Check if the actor has the required skill level
# Check if the character has the required skill level
skill = randint(1, 20)
if skill < recipe.difficulty:
return f"You need a crafting skill level of {recipe.difficulty} to craft {item}."
# Collect inventory items names
inventory_items = {item.name for item in action_actor.items}
inventory_items = {item.name for item in action_character.items}
# Check for sufficient ingredients
missing_items = [
@ -55,13 +55,14 @@ def action_craft(item: str) -> str:
# Deduct the ingredients from inventory
for ingredient in recipe.ingredients:
item_to_remove = next(
item for item in action_actor.items if item.name == ingredient
item for item in action_character.items if item.name == ingredient
)
action_actor.items.remove(item_to_remove)
action_character.items.remove(item_to_remove)
# Create and add the crafted item to inventory
result_item = next(
(item for item in action_actor.items if item.name == recipe.result), None
(item for item in action_character.items if item.name == recipe.result),
None,
)
if result_item:
new_item = Item(**vars(result_item)) # Copying the item
@ -72,7 +73,7 @@ def action_craft(item: str) -> str:
dungeon_master, action_world, systems
) # TODO: pass crafting recipe and generate from that
action_actor.items.append(new_item)
action_character.items.append(new_item)
broadcast(f"{action_actor.name} crafts a {item}.")
broadcast(f"{action_character.name} crafts a {item}.")
return f"You successfully craft a {item}."

View File

@ -1,5 +1,5 @@
from adventure.context import action_context, broadcast
from adventure.utils.search import find_item_in_actor
from adventure.utils.search import find_item_in_character
def action_read(item: str) -> str:
@ -9,13 +9,13 @@ def action_read(item: str) -> str:
Args:
item: The name of the item to read.
"""
with action_context() as (_, action_actor):
action_item = find_item_in_actor(action_actor, item)
with action_context() as (_, action_character):
action_item = find_item_in_character(action_character, item)
if not action_item:
return f"You do not have a {item} to read."
if "text" in action_item.attributes:
broadcast(f"{action_actor.name} reads {item}")
broadcast(f"{action_character.name} reads {item}")
return str(action_item.attributes["text"])
return f"The {item} has nothing to read."

View File

@ -1,7 +1,7 @@
from random import randint
from adventure.context import action_context, broadcast, get_dungeon_master
from adventure.utils.search import find_actor_in_room
from adventure.utils.search import find_character_in_room
def action_cast(spell: str, target: str) -> str:
@ -12,25 +12,30 @@ def action_cast(spell: str, target: str) -> str:
spell: The name of the spell to cast.
target: The target of the spell.
"""
with action_context() as (action_room, action_actor):
target_actor = find_actor_in_room(action_room, target)
with action_context() as (action_room, action_character):
target_character = find_character_in_room(action_room, target)
dungeon_master = get_dungeon_master()
# Check for spell availability and mana costs
if spell not in action_actor.attributes["spells"]:
if spell not in action_character.attributes["spells"]:
return f"You do not know the spell '{spell}'."
if action_actor.attributes["mana"] < action_actor.attributes["spells"][spell]:
if (
action_character.attributes["mana"]
< action_character.attributes["spells"][spell]
):
return "You do not have enough mana to cast this spell."
action_actor.attributes["mana"] -= action_actor.attributes["spells"][spell]
action_character.attributes["mana"] -= action_character.attributes["spells"][
spell
]
# Get flavor text from the dungeon master
flavor_text = dungeon_master(f"Describe the effects of {spell} on {target}.")
broadcast(f"{action_actor.name} casts {spell} on {target}. {flavor_text}")
broadcast(f"{action_character.name} casts {spell} on {target}. {flavor_text}")
# Apply effects based on the spell
if spell == "heal" and target_actor:
if spell == "heal" and target_character:
heal_amount = randint(10, 30)
target_actor.attributes["health"] += heal_amount
target_character.attributes["health"] += heal_amount
return f"{target} is healed for {heal_amount} points."
return f"{spell} was successfully cast on {target}."

View File

@ -11,7 +11,7 @@ def action_climb(target: str) -> str:
Args:
target: The object or feature to climb.
"""
with action_context() as (action_room, action_actor):
with action_context() as (action_room, action_character):
dungeon_master = get_dungeon_master()
# Assume 'climbable' is an attribute that marks climbable targets
climbable_feature = find_item_in_room(action_room, target)
@ -22,16 +22,16 @@ def action_climb(target: str) -> str:
# Get flavor text for the climb attempt
flavor_text = dungeon_master(
f"Describe {action_actor.name}'s attempt to climb {target}."
f"Describe {action_character.name}'s attempt to climb {target}."
)
if climb_roll > climb_difficulty:
broadcast(
f"{action_actor.name} successfully climbs the {target}. {flavor_text}"
f"{action_character.name} successfully climbs the {target}. {flavor_text}"
)
return f"You successfully climb the {target}."
else:
broadcast(
f"{action_actor.name} fails to climb the {target}. {flavor_text}"
f"{action_character.name} fails to climb the {target}. {flavor_text}"
)
return f"You fail to climb the {target}."
else:

View File

@ -1,10 +1,10 @@
from adventure.context import (
action_context,
broadcast,
get_agent_for_actor,
get_agent_for_character,
get_dungeon_master,
)
from adventure.utils.search import find_actor_in_room, find_item_in_room
from adventure.utils.search import find_character_in_room, find_item_in_room
from adventure.utils.world import describe_entity
@ -16,30 +16,32 @@ def action_attack(target: str) -> str:
target: The name of the character or item to attack.
"""
with action_context() as (action_room, action_actor):
with action_context() as (action_room, action_character):
# make sure the target is in the room
target_actor = find_actor_in_room(action_room, target)
target_character = find_character_in_room(action_room, target)
target_item = find_item_in_room(action_room, target)
dungeon_master = get_dungeon_master()
if target_actor:
target_agent = get_agent_for_actor(target_actor)
if target_character:
target_agent = get_agent_for_character(target_character)
if not target_agent:
raise ValueError(f"no agent found for actor {target_actor.name}")
raise ValueError(
f"no agent found for character {target_character.name}"
)
reaction = target_agent(
f"{action_actor.name} is attacking you in the {action_room.name}. How do you react?"
f"{action_character.name} is attacking you in the {action_room.name}. How do you react?"
"Respond with 'fighting', 'fleeing', or 'surrendering'."
)
outcome = dungeon_master(
f"{action_actor.name} attacks {target} in the {action_room.name}. {describe_entity(action_room)}."
f"{describe_entity(action_actor)}. {describe_entity(target_actor)}."
f"{action_character.name} attacks {target} in the {action_room.name}. {describe_entity(action_room)}."
f"{describe_entity(action_character)}. {describe_entity(target_character)}."
f"{target} reacts by {reaction}. What is the outcome of the attack? Describe the result in detail."
)
description = (
f"{action_actor.name} attacks the {target} in the {action_room.name}."
f"{action_character.name} attacks the {target} in the {action_room.name}."
f"{target} reacts by {reaction}. {outcome}"
)
broadcast(description)
@ -47,12 +49,12 @@ def action_attack(target: str) -> str:
if target_item:
outcome = dungeon_master(
f"{action_actor.name} attacks {target} in the {action_room.name}. {describe_entity(action_room)}."
f"{describe_entity(action_actor)}. {describe_entity(target_item)}."
f"{action_character.name} attacks {target} in the {action_room.name}. {describe_entity(action_room)}."
f"{describe_entity(action_character)}. {describe_entity(target_item)}."
f"What is the outcome of the attack? Describe the result in detail."
)
description = f"{action_actor.name} attacks the {target} in the {action_room.name}. {outcome}"
description = f"{action_character.name} attacks the {target} in the {action_room.name}. {outcome}"
broadcast(description)
return description
@ -68,21 +70,21 @@ def action_cast(target: str, spell: str) -> str:
spell: The name of the spell to cast.
"""
with action_context() as (action_room, action_actor):
with action_context() as (action_room, action_character):
# make sure the target is in the room
target_actor = find_actor_in_room(action_room, target)
target_character = find_character_in_room(action_room, target)
target_item = find_item_in_room(action_room, target)
if not target_actor and not target_item:
if not target_character and not target_item:
return f"{target} is not in the {action_room.name}."
dungeon_master = get_dungeon_master()
outcome = dungeon_master(
f"{action_actor.name} casts {spell} on {target} in the {action_room.name}. {describe_entity(action_room)}."
f"{describe_entity(action_actor)}. {describe_entity(target_actor) if target_actor else describe_entity(target_item)}."
f"{action_character.name} casts {spell} on {target} in the {action_room.name}. {describe_entity(action_room)}."
f"{describe_entity(action_character)}. {describe_entity(target_character) if target_character else describe_entity(target_item)}."
f"What is the outcome of the spell? Describe the result in detail."
)
description = f"{action_actor.name} casts {spell} on the {target} in the {action_room.name}. {outcome}"
description = f"{action_character.name} casts {spell} on the {target} in the {action_room.name}. {outcome}"
broadcast(description)
return description

View File

@ -2,7 +2,7 @@ rules:
# wet/dry logic
- group: environment-moisture
match:
type: actor
type: character
wet: true
chance: 0.1
set:
@ -10,7 +10,7 @@ rules:
- group: environment-moisture
match:
type: actor
type: character
wet: true
temperature: hot
chance: 0.2
@ -33,7 +33,7 @@ rules:
labels:
- match:
type: actor
type: character
wet: true
backstory: You are soaking wet.
description: They are soaking wet and dripping water.

View File

@ -3,21 +3,21 @@ from adventure.models.entity import Attributes, Room
def hot_room(room: Room, attributes: Attributes):
"""
If the room is hot, actors should get hotter.
If the room is hot, characters should get hotter.
"""
for actor in room.actors:
actor.attributes["hot"] = "hot"
for character in room.characters:
character.attributes["hot"] = "hot"
return attributes
def cold_room(room: Room, attributes: Attributes):
"""
If the room is cold, actors should get colder.
If the room is cold, characters should get colder.
"""
for actor in room.actors:
actor.attributes["cold"] = "cold"
for character in room.characters:
character.attributes["cold"] = "cold"
return attributes

View File

@ -1,5 +1,5 @@
from adventure.context import action_context
from adventure.utils.search import find_item_in_actor
from adventure.utils.search import find_item_in_character
def action_cook(item: str) -> str:
@ -9,8 +9,8 @@ def action_cook(item: str) -> str:
Args:
item: The name of the item to cook.
"""
with action_context() as (_, action_actor):
target_item = find_item_in_actor(action_actor, item)
with action_context() as (_, action_character):
target_item = find_item_in_character(action_character, item)
if target_item is None:
return "You don't have the item to cook."
@ -36,8 +36,8 @@ def action_eat(item: str) -> str:
Args:
item: The name of the item to eat.
"""
with action_context() as (_, action_actor):
target_item = find_item_in_actor(action_actor, item)
with action_context() as (_, action_character):
target_item = find_item_in_character(action_character, item)
if target_item is None:
return "You don't have the item to eat."
@ -56,12 +56,12 @@ def action_eat(item: str) -> str:
if spoiled:
return "You can't eat that item, it is rotten."
# Check if the actor is hungry
hunger = action_actor.attributes.get("hunger", None)
# Check if the character is hungry
hunger = action_character.attributes.get("hunger", None)
if hunger != "hungry":
return "You're not hungry."
# Eat the item
action_actor.items.remove(target_item)
action_actor.attributes["hunger"] = "full"
action_character.items.remove(target_item)
action_character.attributes["hunger"] = "full"
return f"You eat the {item}."

View File

@ -21,7 +21,7 @@ rules:
# hunger logic
- group: hunger
match:
type: actor
type: character
hunger: full
chance: 0.1
set:
@ -37,7 +37,7 @@ rules:
# thirst logic
- group: thirst
match:
type: actor
type: character
thirst: hydrated
chance: 0.1
set:
@ -77,27 +77,27 @@ labels:
backstory: You are rotten and inedible.
description: This item is rotten and inedible.
- match:
type: actor
type: character
spoiled: false
backstory: You are fresh and edible.
description: This item is fresh and edible.
- match:
type: actor
type: character
hunger: full
backstory: You are have eaten recently and are full.
description: ~
- match:
type: actor
type: character
hunger: hungry
backstory: You are hungry and need to eat.
description: They look hungry.
- match:
type: actor
type: character
thirst: hydrated
backstory: You are hydrated.
description: ~
- match:
type: actor
type: character
thirst: thirsty
backstory: You are thirsty and need to drink.
description: They look thirsty.

View File

@ -7,15 +7,15 @@ def action_wash(unused: bool) -> str:
Wash yourself.
"""
with action_context() as (action_room, action_actor):
hygiene = action_actor.attributes.get("hygiene", "clean")
with action_context() as (action_room, action_character):
hygiene = action_character.attributes.get("hygiene", "clean")
dungeon_master = get_dungeon_master()
outcome = dungeon_master(
f"{action_actor.name} washes themselves in the {action_room.name}. {describe_entity(action_room)}. {describe_entity(action_actor)}"
f"{action_actor.name} was {hygiene} to start with. How clean are they after washing? Respond with 'clean' or 'dirty'."
f"{action_character.name} washes themselves in the {action_room.name}. {describe_entity(action_room)}. {describe_entity(action_character)}"
f"{action_character.name} was {hygiene} to start with. How clean are they after washing? Respond with 'clean' or 'dirty'."
"If the room has a shower or running water, they should be cleaner. If the room is dirty, they should end up dirtier."
)
action_actor.attributes["clean"] = outcome.strip().lower()
action_character.attributes["clean"] = outcome.strip().lower()
return f"You wash yourself in the {action_room.name} and feel {outcome}"

View File

@ -1,13 +1,13 @@
rules:
- match:
type: actor
type: character
hygiene: clean
chance: 0.1
set:
hygiene: dirty
- match:
type: actor
type: character
hygiene: dirty
chance: 0.1
set:
@ -21,17 +21,17 @@ rules:
labels:
- match:
type: actor
type: character
hygiene: clean
backstory: You are clean and smell fresh.
description: They look freshly washed and smell clean.
- match:
type: actor
type: character
hygiene: dirty
backstory: You are dirty and smell bad.
description: They look dirty and smell bad.
- match:
type: actor
type: character
hygiene: filthy
backstory: You are filthy and smell terrible.
description: They look filthy and smell terrible.

View File

@ -2,7 +2,7 @@ rules:
# mood logic
- group: mood
match:
type: actor
type: character
mood: happy
chance: 0.1
set:
@ -10,7 +10,7 @@ rules:
- group: mood
match:
type: actor
type: character
mood: happy
chance: 0.1
set:
@ -18,7 +18,7 @@ rules:
- group: mood
match:
type: actor
type: character
mood: angry
chance: 0.1
set:
@ -26,7 +26,7 @@ rules:
- group: mood
match:
type: actor
type: character
mood: neutral
chance: 0.1
set:
@ -34,7 +34,7 @@ rules:
- group: mood
match:
type: actor
type: character
mood: neutral
chance: 0.1
set:
@ -42,7 +42,7 @@ rules:
- group: mood
match:
type: actor
type: character
mood: sad
chance: 0.1
set:
@ -50,7 +50,7 @@ rules:
- group: mood
match:
type: actor
type: character
mood: sad
chance: 0.1
set:
@ -59,7 +59,7 @@ rules:
# mood interactions with other systems
- group: mood
match:
type: actor
type: character
mood: sad
sleep: rested
chance: 0.2
@ -68,7 +68,7 @@ rules:
- group: mood
match:
type: actor
type: character
hunger: hungry
chance: 0.2
set:
@ -76,7 +76,7 @@ rules:
- group: mood
match:
type: actor
type: character
mood: angry
hunger: full
chance: 0.2
@ -85,7 +85,7 @@ rules:
- group: mood
match:
type: actor
type: character
mood: neutral
hunger: full
chance: 0.2
@ -94,7 +94,7 @@ rules:
- group: mood
match:
type: actor
type: character
mood: happy
hunger: hungry
chance: 0.2
@ -103,7 +103,7 @@ rules:
- group: mood
match:
type: actor
type: character
mood: neutral
sleep: tired
chance: 0.2
@ -119,17 +119,17 @@ rules:
labels:
- match:
type: actor
type: character
mood: happy
backstory: You are feeling happy.
description: They look happy.
- match:
type: actor
type: character
mood: sad
backstory: You are feeling sad.
description: They look sad.
- match:
type: actor
type: character
mood: angry
backstory: You are feeling angry.
description: They look angry.

View File

@ -7,12 +7,12 @@ def action_sleep(unused: bool) -> str:
Sleep until you are rested.
"""
with action_context() as (action_room, action_actor):
with action_context() as (action_room, action_character):
dungeon_master = get_dungeon_master()
outcome = dungeon_master(
f"{action_actor.name} sleeps in the {action_room.name}. {describe_entity(action_room)}. {describe_entity(action_actor)}"
f"{action_character.name} sleeps in the {action_room.name}. {describe_entity(action_room)}. {describe_entity(action_character)}"
"How rested are they? Respond with 'rested' or 'tired'."
)
action_actor.attributes["rested"] = outcome
action_character.attributes["rested"] = outcome
return f"You sleep in the {action_room.name} and wake up feeling {outcome}"

View File

@ -1,7 +1,7 @@
rules:
# sleeping logic
- match:
type: actor
type: character
sleep: rested
chance: 0.1
set:
@ -15,12 +15,12 @@ rules:
labels:
- match:
type: actor
type: character
sleep: rested
backstory: You are well-rested.
description: They look well-rested.
- match:
type: actor
type: character
sleep: tired
backstory: You are tired.
description: They look tired.

View File

@ -10,7 +10,7 @@ from packit.utils import could_be_json
from adventure.context import broadcast
from adventure.models.config import DEFAULT_CONFIG
from adventure.models.entity import Actor, Room
from adventure.models.entity import Character, Room
from adventure.models.event import ReplyEvent
from .string import normalize_name
@ -18,7 +18,7 @@ from .string import normalize_name
logger = getLogger(__name__)
actor_config = DEFAULT_CONFIG.world.actor
character_config = DEFAULT_CONFIG.world.character
def make_keyword_condition(end_message: str, keywords=["end", "stop"]):
@ -85,19 +85,23 @@ def or_list(items: List[str]) -> str:
return f"{', '.join(items[:-1])}, or {items[-1]}"
def summarize_room(room: Room, player: Actor) -> str:
def summarize_room(room: Room, player: Character) -> str:
"""
Summarize a room for the player.
"""
actor_names = and_list(
[actor.name for actor in room.actors if actor.name != player.name]
character_names = and_list(
[
character.name
for character in room.characters
if character.name != player.name
]
)
item_names = and_list([item.name for item in room.items])
inventory_names = and_list([item.name for item in player.items])
return (
f"You are in the {room.name} room with {actor_names}. "
f"You are in the {room.name} room with {character_names}. "
f"You see the {item_names} around the room. "
f"You are carrying the {inventory_names}."
)
@ -105,9 +109,9 @@ def summarize_room(room: Room, player: Actor) -> str:
def loop_conversation(
room: Room,
actors: List[Actor],
characters: List[Character],
agents: List[Agent],
first_actor: Actor,
first_character: Character,
first_prompt: str,
reply_prompt: str,
first_message: str,
@ -117,14 +121,14 @@ def loop_conversation(
max_length: int | None = None,
) -> str | None:
"""
Loop through a conversation between a series of agents, using metadata from their actors.
Loop through a conversation between a series of agents, using metadata from their characters.
"""
if max_length is None:
max_length = actor_config.conversation_limit
max_length = character_config.conversation_limit
if len(actors) != len(agents):
raise ValueError("The number of actors and agents must match.")
if len(characters) != len(agents):
raise ValueError("The number of characters and agents must match.")
# set up the keyword or length-limit compound condition
_, condition_end, parse_end = make_keyword_condition(end_message)
@ -145,34 +149,36 @@ def loop_conversation(
# prepare the loop state
i = 0
last_actor = first_actor
last_character = first_character
response = first_message
while not stop_condition(current=i):
if i == 0:
logger.debug(f"starting conversation with {first_actor.name}")
logger.debug(f"starting conversation with {first_character.name}")
prompt = first_prompt
else:
logger.debug(f"continuing conversation with {last_actor.name} on step {i}")
logger.debug(
f"continuing conversation with {last_character.name} on step {i}"
)
prompt = reply_prompt
# loop through the actors and agents
actor = actors[i % len(actors)]
# loop through the characters and agents
character = characters[i % len(characters)]
agent = agents[i % len(agents)]
# summarize the room and present the last response
summary = summarize_room(room, actor)
summary = summarize_room(room, character)
response = agent(
prompt, response=response, summary=summary, last_actor=last_actor
prompt, response=response, summary=summary, last_character=last_character
)
response = result_parser(response)
logger.info(f"{actor.name} responds: {response}")
reply_event = ReplyEvent.from_text(response, room, actor)
logger.info(f"{character.name} responds: {response}")
reply_event = ReplyEvent.from_text(response, room, character)
broadcast(reply_event)
# increment the step counter
i += 1
last_actor = actor
last_character = character
return response

View File

@ -13,7 +13,7 @@ from adventure.models.effect import (
StringEffectPattern,
StringEffectResult,
)
from adventure.models.entity import Actor, Attributes
from adventure.models.entity import Attributes, Character
from adventure.utils.attribute import (
add_value,
append_value,
@ -252,9 +252,9 @@ def apply_permanent_effects(
return apply_permanent_results(attributes, results)
def apply_effects(target: Actor, effects: List[EffectPattern]) -> None:
def apply_effects(target: Character, effects: List[EffectPattern]) -> None:
"""
Apply a set of effects to an actor and their attributes.
Apply a set of effects to a character and their attributes.
"""
permanent_effects = [
@ -270,9 +270,9 @@ def apply_effects(target: Actor, effects: List[EffectPattern]) -> None:
target.active_effects.extend(temporary_effects)
def expire_effects(target: Actor) -> None:
def expire_effects(target: Character) -> None:
"""
Decrement the duration of effects on an actor and remove any that have expired.
Decrement the duration of effects on a character and remove any that have expired.
"""
for effect in target.active_effects:

View File

@ -1,34 +1,36 @@
from adventure.models.entity import Actor
from adventure.models.entity import Character
def expire_events(actor: Actor, current_turn: int):
def expire_events(character: Character, current_turn: int):
"""
Expire events that have already happened.
"""
events = actor.planner.calendar.events
events = character.planner.calendar.events
expired_events = [event for event in events if event.turn < current_turn]
actor.planner.calendar.events[:] = [
character.planner.calendar.events[:] = [
event for event in events if event not in expired_events
]
return expired_events
def get_recent_notes(actor: Actor, count: int = 3):
def get_recent_notes(character: Character, count: int = 3):
"""
Get the most recent facts from your notes.
"""
return actor.planner.notes[-count:]
return character.planner.notes[-count:]
def get_upcoming_events(actor: Actor, current_turn: int, upcoming_turns: int = 3):
def get_upcoming_events(
character: Character, current_turn: int, upcoming_turns: int = 3
):
"""
Get a list of upcoming events within a certain number of turns.
"""
calendar = actor.planner.calendar
calendar = character.planner.calendar
# TODO: sort events by turn
return [
event

View File

@ -1,7 +1,7 @@
from typing import Any, Generator
from adventure.models.entity import (
Actor,
Character,
EntityReference,
Item,
Portal,
@ -30,19 +30,19 @@ def find_portal(world: World, portal_name: str) -> Portal | None:
return None
def find_actor(world: World, actor_name: str) -> Actor | None:
def find_character(world: World, character_name: str) -> Character | None:
for room in world.rooms:
actor = find_actor_in_room(room, actor_name)
if actor:
return actor
character = find_character_in_room(room, character_name)
if character:
return character
return None
def find_actor_in_room(room: Room, actor_name: str) -> Actor | None:
for actor in room.actors:
if normalize_name(actor.name) == normalize_name(actor_name):
return actor
def find_character_in_room(room: Room, character_name: str) -> Character | None:
for character in room.characters:
if normalize_name(character.name) == normalize_name(character_name):
return character
return None
@ -51,12 +51,12 @@ def find_actor_in_room(room: Room, actor_name: str) -> Actor | None:
def find_item(
world: World,
item_name: str,
include_actor_inventory=False,
include_character_inventory=False,
include_item_inventory=False,
) -> Item | None:
for room in world.rooms:
item = find_item_in_room(
room, item_name, include_actor_inventory, include_item_inventory
room, item_name, include_character_inventory, include_item_inventory
)
if item:
return item
@ -64,14 +64,14 @@ def find_item(
return None
def find_item_in_actor(
actor: Actor, item_name: str, include_item_inventory=False
def find_item_in_character(
character: Character, item_name: str, include_item_inventory=False
) -> Item | None:
return find_item_in_container(actor, item_name, include_item_inventory)
return find_item_in_container(character, item_name, include_item_inventory)
def find_item_in_container(
container: Actor | Item, item_name: str, include_item_inventory=False
container: Character | Item, item_name: str, include_item_inventory=False
) -> Item | None:
for item in container.items:
if normalize_name(item.name) == normalize_name(item_name):
@ -88,7 +88,7 @@ def find_item_in_container(
def find_item_in_room(
room: Room,
item_name: str,
include_actor_inventory=False,
include_character_inventory=False,
include_item_inventory=False,
) -> Item | None:
for item in room.items:
@ -100,30 +100,30 @@ def find_item_in_room(
if item:
return item
if include_actor_inventory:
for actor in room.actors:
item = find_item_in_actor(actor, item_name, include_item_inventory)
if include_character_inventory:
for character in room.characters:
item = find_item_in_character(character, item_name, include_item_inventory)
if item:
return item
return None
def find_room_with_actor(world: World, actor: Actor) -> Room | None:
def find_room_with_character(world: World, character: Character) -> Room | None:
for room in world.rooms:
for room_actor in room.actors:
if normalize_name(actor.name) == normalize_name(room_actor.name):
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 | Actor | Item) -> Room | None:
def find_containing_room(world: World, entity: Room | Character | Item) -> Room | None:
if isinstance(entity, Room):
return entity
for room in world.rooms:
if entity in room.actors or entity in room.items:
if entity in room.characters or entity in room.items:
return room
return None
@ -139,8 +139,8 @@ def find_entity_reference(
if reference.room:
return find_room(world, reference.room)
if reference.actor:
return find_actor(world, reference.actor)
if reference.character:
return find_character(world, reference.character)
if reference.item:
return find_item(world, reference.item)
@ -162,14 +162,14 @@ def list_portals(world: World) -> Generator[Portal, Any, None]:
yield portal
def list_actors(world: World) -> Generator[Actor, Any, None]:
def list_characters(world: World) -> Generator[Character, Any, None]:
for room in world.rooms:
for actor in room.actors:
yield actor
for character in room.characters:
yield character
def list_items(
world: World, include_actor_inventory=True, include_item_inventory=True
world: World, include_character_inventory=True, include_item_inventory=True
) -> Generator[Item, Any, None]:
for room in world.rooms:
@ -179,21 +179,21 @@ def list_items(
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:
if include_character_inventory:
for character in room.characters:
for item in character.items:
yield item
def list_actors_in_room(room: Room) -> Generator[Actor, Any, None]:
for actor in room.actors:
yield actor
def list_characters_in_room(room: Room) -> Generator[Character, Any, None]:
for character in room.characters:
yield character
def list_items_in_actor(
actor: Actor, include_item_inventory=True
def list_items_in_character(
character: Character, include_item_inventory=True
) -> Generator[Item, Any, None]:
for item in actor.items:
for item in character.items:
yield item
if include_item_inventory:
@ -212,7 +212,7 @@ def list_items_in_container(
def list_items_in_room(
room: Room,
include_actor_inventory=True,
include_character_inventory=True,
include_item_inventory=True,
) -> Generator[Item, Any, None]:
for item in room.items:
@ -221,7 +221,7 @@ def list_items_in_room(
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:
if include_character_inventory:
for character in room.characters:
for item in character.items:
yield item

View File

@ -2,25 +2,26 @@ from logging import getLogger
from adventure.context import get_game_systems
from adventure.game_system import FormatPerspective
from adventure.models.entity import Actor, WorldEntity
from adventure.models.entity import Character, WorldEntity
logger = getLogger(__name__)
def describe_actor(
actor: Actor, perspective: FormatPerspective = FormatPerspective.SECOND_PERSON
def describe_character(
character: Character,
perspective: FormatPerspective = FormatPerspective.SECOND_PERSON,
) -> str:
attribute_descriptions = format_attributes(actor, perspective=perspective)
logger.info("describing actor: %s, %s", actor, attribute_descriptions)
attribute_descriptions = format_attributes(character, perspective=perspective)
logger.info("describing character: %s, %s", character, attribute_descriptions)
if perspective == FormatPerspective.SECOND_PERSON:
actor_description = actor.backstory
character_description = character.backstory
elif perspective == FormatPerspective.THIRD_PERSON:
actor_description = actor.description
character_description = character.description
else:
raise ValueError(f"Perspective {perspective} is not implemented")
return f"{actor_description} {attribute_descriptions}"
return f"{character_description} {attribute_descriptions}"
def describe_static(entity: WorldEntity) -> str:
@ -32,8 +33,8 @@ def describe_entity(
entity: WorldEntity,
perspective: FormatPerspective = FormatPerspective.SECOND_PERSON,
) -> str:
if isinstance(entity, Actor):
return describe_actor(entity, perspective)
if isinstance(entity, Character):
return describe_character(entity, perspective)
return describe_static(entity)

View File

@ -13,7 +13,7 @@ import useWebSocketModule from 'react-use-websocket';
import { useStore } from 'zustand';
import { HistoryPanel } from './history.js';
import { Actor } from './models.js';
import { Character } from './models.js';
import { PlayerPanel } from './player.js';
import { Statusbar } from './status.js';
import { StoreState, store } from './store.js';
@ -52,15 +52,15 @@ export function App(props: AppProps) {
sendMessage(JSON.stringify({ type: 'render', event }));
}
function setPlayer(actor: Maybe<Actor>) {
function setPlayer(character: Maybe<Character>) {
// do not call setCharacter until the server confirms the player change
if (doesExist(actor)) {
sendMessage(JSON.stringify({ type: 'player', become: actor.name }));
if (doesExist(character)) {
sendMessage(JSON.stringify({ type: 'player', become: character.name }));
}
}
function sendInput(input: string) {
const { character, setActiveTurn } = store.getState();
const { playerCharacter: character, setActiveTurn } = store.getState();
if (doesExist(character)) {
sendMessage(JSON.stringify({ type: 'input', input }));
setActiveTurn(false);
@ -80,7 +80,7 @@ export function App(props: AppProps) {
});
useEffect(() => {
const { setClientId, setActiveTurn, setPlayers, appendEvent, setWorld, world, clientId, setCharacter } = store.getState();
const { setClientId, setActiveTurn, setPlayers, appendEvent, setWorld, world, clientId, setPlayerCharacter: setCharacter } = store.getState();
if (doesExist(lastMessage)) {
const event = JSON.parse(lastMessage.data);
@ -98,8 +98,8 @@ export function App(props: AppProps) {
case 'player':
if (event.status === 'join' && doesExist(world) && event.client === clientId) {
const { character: characterName } = event;
const actor = world.rooms.flatMap((room) => room.actors).find((a) => a.name === characterName);
setCharacter(actor);
const character = world.rooms.flatMap((room) => room.characters).find((a) => a.name === characterName);
setCharacter(character);
}
break;
case 'players':

View File

@ -21,11 +21,11 @@ import {
import { instance as graphviz } from '@viz-js/viz';
import React, { Fragment, useEffect } from 'react';
import { useStore } from 'zustand';
import { Actor, Attributes, Item, Portal, Room, World } from './models';
import { Character, Attributes, Item, Portal, Room, World } from './models';
import { StoreState, store } from './store';
export interface EntityDetailsProps {
entity: Maybe<Item | Actor | Portal | Room>;
entity: Maybe<Item | Character | Portal | Room>;
onClose: () => void;
onRender: (type: string, entity: string) => void;
}
@ -43,10 +43,10 @@ export function EntityDetails(props: EntityDetailsProps) {
let attributes: Attributes = {};
let planner;
if (type === 'actor') {
const actor = entity as Actor;
attributes = actor.attributes;
planner = actor.planner;
if (type === 'character') {
const character = entity as Character;
attributes = character.attributes;
planner = character.planner;
}
if (type === 'item') {
@ -155,7 +155,7 @@ export function DetailDialog(props: DetailDialogProps) {
>{details}</Dialog>;
}
export function isWorld(entity: Maybe<Item | Actor | Portal | Room | World>): entity is World {
export function isWorld(entity: Maybe<Item | Character | Portal | Room | World>): entity is World {
return doesExist(entity) && doesExist(Object.getOwnPropertyDescriptor(entity, 'theme'));
}

View File

@ -5,7 +5,7 @@ import { Maybe, doesExist } from '@apextoaster/js-utils';
import { Camera, Settings } from '@mui/icons-material';
import { useStore } from 'zustand';
import { formatters } from './format.js';
import { Actor } from './models.js';
import { Character } from './models.js';
import { StoreState, store } from './store.js';
export function openImage(image: string) {
@ -32,11 +32,11 @@ export interface EventItemProps {
export function characterSelector(state: StoreState) {
return {
character: state.character,
playerCharacter: state.playerCharacter,
};
}
export function sameCharacter(a: Maybe<Actor>, b: Maybe<Actor>): boolean {
export function sameCharacter(a: Maybe<Character>, b: Maybe<Character>): boolean {
if (doesExist(a) && doesExist(b)) {
return a.name === b.name;
}
@ -46,13 +46,13 @@ export function sameCharacter(a: Maybe<Actor>, b: Maybe<Actor>): boolean {
export function ActionEventItem(props: EventItemProps) {
const { event, renderEvent } = props;
const { id, actor, room, type } = event;
const { id, character, room, type } = event;
const content = formatters[type](event);
const state = useStore(store, characterSelector);
const { character } = state;
const { playerCharacter } = state;
const playerAction = sameCharacter(actor, character);
const playerAction = sameCharacter(character, playerCharacter);
const typographyProps = {
color: playerAction ? 'success.text' : 'primary.text',
};
@ -81,7 +81,7 @@ export function ActionEventItem(props: EventItemProps) {
variant="body2"
color="text.primary"
>
{actor.name}
{character.name}
</Typography>
{content}
</React.Fragment>
@ -220,7 +220,7 @@ export function PromptEventItem(props: EventItemProps) {
const { character, prompt } = event;
const state = useStore(store, characterSelector);
const { character: playerCharacter } = state;
const { playerCharacter: playerCharacter } = state;
const playerPrompt = sameCharacter(playerCharacter, character);
const typographyProps = {

View File

@ -17,8 +17,8 @@ export interface Item {
attributes: Attributes;
}
export interface Actor {
type: 'actor';
export interface Character {
type: 'character';
name: string;
backstory: string;
description: string;
@ -38,7 +38,7 @@ export interface Room {
type: 'room';
name: string;
description: string;
actors: Array<Actor>;
characters: Array<Character>;
items: Array<Item>;
portals: Array<Portal>;
attributes: Attributes;

View File

@ -10,7 +10,7 @@ export interface PlayerPanelProps {
export function playerStateSelector(s: StoreState) {
return {
character: s.character,
character: s.playerCharacter,
activeTurn: s.activeTurn,
};
}

View File

@ -4,7 +4,7 @@ import { createStore, StateCreator } from 'zustand';
import { doesExist, Maybe } from '@apextoaster/js-utils';
import { PaletteMode } from '@mui/material';
import { ReadyState } from 'react-use-websocket';
import { Actor, GameEvent, Item, Portal, Room, World } from './models';
import { Character, GameEvent, Item, Portal, Room, World } from './models';
export type LayoutMode = 'horizontal' | 'vertical';
@ -12,7 +12,7 @@ export interface ClientState {
autoScroll: boolean;
clientId: string;
clientName: string;
detailEntity: Maybe<Item | Actor | Portal | Room | World>;
detailEntity: Maybe<Item | Character | Portal | Room | World>;
eventHistory: Array<GameEvent>;
layoutMode: LayoutMode;
readyState: ReadyState;
@ -22,7 +22,7 @@ export interface ClientState {
setAutoScroll: (autoScroll: boolean) => void;
setClientId: (clientId: string) => void;
setClientName: (name: string) => void;
setDetailEntity: (entity: Maybe<Item | Actor | Portal | Room | World>) => void;
setDetailEntity: (entity: Maybe<Item | Character | Portal | Room | World>) => void;
setLayoutMode: (mode: LayoutMode) => void;
setReadyState: (state: ReadyState) => void;
setThemeMode: (mode: PaletteMode) => void;
@ -44,11 +44,11 @@ export interface WorldState {
export interface PlayerState {
activeTurn: boolean;
character: Maybe<Actor>;
playerCharacter: Maybe<Character>;
// setters
setActiveTurn: (activeTurn: boolean) => void;
setCharacter: (character: Maybe<Actor>) => void;
setPlayerCharacter: (character: Maybe<Character>) => void;
// misc helpers
isPlaying: () => boolean;
@ -114,11 +114,11 @@ export function createWorldStore(): StateCreator<WorldState> {
export function createPlayerStore(): StateCreator<PlayerState> {
return (set) => ({
activeTurn: false,
character: undefined,
playerCharacter: undefined,
setActiveTurn: (activeTurn: boolean) => set({ activeTurn }),
setCharacter: (character: Maybe<Actor>) => set({ character }),
setPlayerCharacter: (character: Maybe<Character>) => set({ playerCharacter: character }),
isPlaying() {
return doesExist(this.character);
return doesExist(this.playerCharacter);
},
});
}

View File

@ -6,10 +6,10 @@ import React from 'react';
import { useStore } from 'zustand';
import { StoreState, store } from './store';
import { Actor, Item, Portal, Room } from './models';
import { Character, Item, Portal, Room } from './models';
export type SetDetails = (entity: Maybe<Item | Actor | Room>) => void;
export type SetPlayer = (actor: Maybe<Actor>) => void;
export type SetDetails = (entity: Maybe<Item | Character | Room>) => void;
export type SetPlayer = (character: Maybe<Character>) => void;
export interface BaseEntityItemProps {
setPlayer: SetPlayer;
@ -25,14 +25,14 @@ export function formatLabel(name: string, active = false): string {
export function itemStateSelector(s: StoreState) {
return {
character: s.character,
playerCharacter: s.playerCharacter,
setDetailEntity: s.setDetailEntity,
};
}
export function actorStateSelector(s: StoreState) {
export function characterStateSelector(s: StoreState) {
return {
character: s.character,
playerCharacter: s.playerCharacter,
players: s.players,
setDetailEntity: s.setDetailEntity,
};
@ -65,33 +65,33 @@ export function ItemItem(props: { item: Item } & BaseEntityItemProps) {
</TreeItem>;
}
export function ActorItem(props: { actor: Actor } & BaseEntityItemProps) {
const { actor, setPlayer } = props;
const state = useStore(store, actorStateSelector);
const { character, players, setDetailEntity } = state;
export function CharacterItem(props: { character: Character } & BaseEntityItemProps) {
const { character, setPlayer } = props;
const state = useStore(store, characterStateSelector);
const { playerCharacter, players, setDetailEntity } = state;
const activeSelf = doesExist(character) && actor.name === character.name;
const activeOther = Object.values(players).some((it) => it === actor.name); // TODO: are these the keys or the values?
const label = formatLabel(actor.name, activeSelf);
const activeSelf = doesExist(playerCharacter) && character.name === playerCharacter.name;
const activeOther = Object.values(players).some((it) => it === character.name); // TODO: are these the keys or the values?
const label = formatLabel(character.name, activeSelf);
let playButton;
if (activeSelf) {
playButton = <TreeItem itemId={`${actor.name}-stop`} label="Stop playing" onClick={() => setPlayer(undefined)} />;
playButton = <TreeItem itemId={`${character.name}-stop`} label="Stop playing" onClick={() => setPlayer(undefined)} />;
} else {
if (activeOther) {
// eslint-disable-next-line no-restricted-syntax
const player = Object.entries(players).find((it) => it[1] === actor.name)?.[0];
playButton = <TreeItem itemId={`${actor.name}-taken`} label={`Played by ${player}`} />;
const player = Object.entries(players).find((it) => it[1] === character.name)?.[0];
playButton = <TreeItem itemId={`${character.name}-taken`} label={`Played by ${player}`} />;
} else {
playButton = <TreeItem itemId={`${actor.name}-play`} label="Play!" onClick={() => setPlayer(actor)} />;
playButton = <TreeItem itemId={`${character.name}-play`} label="Play!" onClick={() => setPlayer(character)} />;
}
}
return <TreeItem itemId={actor.name} label={label}>
return <TreeItem itemId={character.name} label={label}>
{playButton}
<TreeItem itemId={`${actor.name}-details`} label="Details" onClick={() => setDetailEntity(actor)} />
<TreeItem itemId={`${actor.name}-items`} label="Items">
{actor.items.map((item) => <ItemItem key={item.name} item={item} setPlayer={setPlayer} />)}
<TreeItem itemId={`${character.name}-details`} label="Details" onClick={() => setDetailEntity(character)} />
<TreeItem itemId={`${character.name}-items`} label="Items">
{character.items.map((item) => <ItemItem key={item.name} item={item} setPlayer={setPlayer} />)}
</TreeItem>
</TreeItem>;
}
@ -99,15 +99,15 @@ export function ActorItem(props: { actor: Actor } & BaseEntityItemProps) {
export function RoomItem(props: { room: Room } & BaseEntityItemProps) {
const { room, setPlayer } = props;
const state = useStore(store, itemStateSelector);
const { character, setDetailEntity } = state;
const { playerCharacter, setDetailEntity } = state;
const active = doesExist(character) && room.actors.some((it) => it.name === character.name);
const active = doesExist(playerCharacter) && room.characters.some((it) => it.name === playerCharacter.name);
const label = formatLabel(room.name, active);
return <TreeItem itemId={room.name} label={label}>
<TreeItem itemId={`${room.name}-details`} label="Details" onClick={() => setDetailEntity(room)} />
<TreeItem itemId={`${room.name}-actors`} label="Actors">
{room.actors.map((actor) => <ActorItem key={actor.name} actor={actor} setPlayer={setPlayer} />)}
<TreeItem itemId={`${room.name}-characters`} label="Characters">
{room.characters.map((character) => <CharacterItem key={character.name} character={character} setPlayer={setPlayer} />)}
</TreeItem>
<TreeItem itemId={`${room.name}-items`} label="Items">
{room.items.map((item) => <ItemItem key={item.name} item={item} setPlayer={setPlayer} />)}

View File

@ -28,7 +28,7 @@ server:
port: 8001
world:
size:
actor_items:
character_items:
min: 0
max: 3
item_effects:
@ -40,7 +40,7 @@ world:
rooms:
min: 3
max: 6
room_actors:
room_characters:
min: 1
max: 3
room_items:

View File

@ -45,5 +45,5 @@ additional configuration from a YAML or JSON file.
- figure out the human input syntax for actions
- make an admin panel in web UI
- store long-term memory for actors in a vector DB (RAG and all that)
- store long-term memory for characters in a vector DB (RAG and all that)
- generate and simulate should probably be async

View File

@ -30,10 +30,10 @@ mechanics to suit different types of adventures and narrative styles.
### What kinds of entities exist in the world?
In the immersive world of TaleWeave AI, entities are categorized into Rooms, Actors, and Items, each playing a vital
In the immersive world of TaleWeave AI, entities are categorized into Rooms, Characters, and Items, each playing a vital
role in crafting the narrative and gameplay experience. Rooms serve as the fundamental spatial units where the story
unfolds, each containing various Actors and potentially multiple Items. Actors, representing characters in the game,
possess inventories that hold Items, which are objects that can be interacted with or utilized by the Actors. Currently,
unfolds, each containing various Characters and potentially multiple Items. Characters, representing characters in the game,
possess inventories that hold Items, which are objects that can be interacted with or utilized by the Characters. Currently,
TaleWeave AI does not support Containers—Items that can hold other Items—but the structure is designed to support
complex interactions within and between these entity types, laying the groundwork for a deeply interactive environment.
@ -41,15 +41,15 @@ complex interactions within and between these entity types, laying the groundwor
Actions in TaleWeave AI are defined as Python functions that enable both human players and AI-driven characters to
interact with the game world. These actions, which include behaviors like taking an item or moving between rooms, are
integral to advancing the gameplay and affecting the state of the world. Each actor is permitted one action per round,
which can significantly alter the attributes of entities, reposition entities between rooms or actors, or modify the
integral to advancing the gameplay and affecting the state of the world. Each character is permitted one action per round,
which can significantly alter the attributes of entities, reposition entities between rooms or characters, or modify the
game world by adding or removing entities. This framework ensures that every turn is meaningful and that players'
decisions have direct consequences on the game's progression and outcome.
### What are attributes?
Attributes in TaleWeave AI are key-value pairs that define the properties of an entity. These attributes can be of
various types—boolean, number, or string—such as an actors mood being "happy," their health being quantified as 10, or
various types—boolean, number, or string—such as a character's mood being "happy," their health being quantified as 10, or
an item's quality described as "broken" or quantified with a remaining usage of 3. Attributes play a crucial role in the
game's logic system by influencing how entities react under different conditions. They are actively used to trigger
specific rules within the game, and their labels are included in prompts to guide language model players in making

View File

@ -77,7 +77,7 @@ added to the world.
```yaml
type: "generate"
name: string
entity: Room | Actor | Item | None
entity: Room | Character | Item | None
```
Two `generate` events will be fired for each entity. The first event will *not* have an `entity` set, only the `name`.
@ -88,14 +88,14 @@ more frequent progress updates when generating with slow models.
### Action Events
The action event is fired after player or actor input has been processed and any JSON function calls have been parsed.
The action event is fired after player or character input has been processed and any JSON function calls have been parsed.
```yaml
type: "action"
action: string
parameters: dict
room: Room
actor: Actor
character: Character
item: Item | None
```
@ -107,7 +107,7 @@ The prompt event is fired when a character's turn starts and their input is need
type: "prompt"
prompt: string
room: Room
actor: Actor
character: Character
```
### Reply Events
@ -118,7 +118,7 @@ The reply event is fired when a character has been asked a question or told a me
type: "reply"
text: string
room: Room
actor: Actor
character: Character
```
### Result Events
@ -129,10 +129,10 @@ The result event is fired after a character has taken an action and contains the
type: "result"
result: string
room: Room
actor: Actor
character: Character
```
The result is related to the most recent action for the same actor, although not every action will have a result - they
The result is related to the most recent action for the same character, although not every action will have a result - they
may have a reply or error instead.
### Status Events
@ -143,7 +143,7 @@ The status event is fired for general events in the world and messages about oth
type: "status"
text: string
room: Room | None
actor: Actor | None
character: Character | None
```
### Snapshot Events

View File

@ -24,6 +24,8 @@
- [Player leaves the game during their turn](#player-leaves-the-game-during-their-turn)
- [Spectator renders a recent event](#spectator-renders-a-recent-event)
- [User Stories for Developers](#user-stories-for-developers)
- [ML Engineer collects prompts and actions to fine-tune a character model](#ml-engineer-collects-prompts-and-actions-to-fine-tune-a-character-model)
- [ML Engineer collects character movement and conversations to chart emergent behavior](#ml-engineer-collects-character-movement-and-conversations-to-chart-emergent-behavior)
- [Mod Developer creates a new system for the game](#mod-developer-creates-a-new-system-for-the-game)
- [Project Contributor fixes a bug in the engine](#project-contributor-fixes-a-bug-in-the-engine)
@ -174,9 +176,20 @@ Skills:
### User Stories for Developers
#### ML Engineer collects prompts and actions to fine-tune a character model
> As an ML engineer, I want to collect data from the game history, especially prompts, actions, and their results, so
> that I can fine-tune an LLM to be a better model for characters.
#### ML Engineer collects character movement and conversations to chart emergent behavior
> As an ML engineer, I want to collect data from the game history, especially character movement, interactions, and
> conversations, so that I can graph their movements and discover any emergent behavior in the game world.
#### Mod Developer creates a new system for the game
> As a Mod Developer, I want to write a Python module with a new game system and load it into a test world, so that I can develop custom features.
> As a Mod Developer, I want to write a Python module with a new game system and load it into a test world, so that I
> can develop custom features.
#### Project Contributor fixes a bug in the engine