From c81d2ae3f2a1f27dcbf0e04f5c6178b69845400f Mon Sep 17 00:00:00 2001 From: Sean Sube Date: Sun, 26 May 2024 20:32:03 -0500 Subject: [PATCH] rename actor to character, add step limits to config --- adventure/actions/base.py | 122 ++++++------ adventure/actions/optional.py | 38 ++-- adventure/actions/planning.py | 54 +++--- adventure/actions/quest.py | 38 ++-- adventure/bot/discord.py | 32 ++-- adventure/context.py | 68 +++---- adventure/generate.py | 78 ++++---- adventure/models/config.py | 27 ++- adventure/models/entity.py | 10 +- adventure/models/event.py | 30 +-- adventure/player.py | 4 +- adventure/render/comfy.py | 8 +- adventure/render/prompt.py | 40 ++-- adventure/server/websocket.py | 51 ++--- adventure/simulate.py | 180 ++++++++++-------- adventure/state.py | 26 +-- adventure/systems/logic.py | 6 +- adventure/systems/quest.py | 62 +++--- adventure/systems/rpg/crafting_actions.py | 17 +- adventure/systems/rpg/language_actions.py | 8 +- adventure/systems/rpg/magic_actions.py | 23 ++- adventure/systems/rpg/movement_actions.py | 8 +- adventure/systems/sim/combat_actions.py | 42 ++-- adventure/systems/sim/environment_logic.yaml | 6 +- adventure/systems/sim/environment_triggers.py | 12 +- adventure/systems/sim/hunger_actions.py | 18 +- adventure/systems/sim/hunger_logic.yaml | 14 +- adventure/systems/sim/hygiene_actions.py | 10 +- adventure/systems/sim/hygiene_logic.yaml | 10 +- adventure/systems/sim/mood_logic.yaml | 32 ++-- adventure/systems/sim/sleeping_actions.py | 6 +- adventure/systems/sim/sleeping_logic.yaml | 6 +- adventure/utils/conversation.py | 50 ++--- adventure/utils/effect.py | 10 +- adventure/utils/planning.py | 18 +- adventure/utils/search.py | 86 ++++----- adventure/utils/world.py | 21 +- client/src/app.tsx | 16 +- client/src/details.tsx | 14 +- client/src/events.tsx | 16 +- client/src/models.ts | 6 +- client/src/player.tsx | 2 +- client/src/store.ts | 16 +- client/src/world.tsx | 50 ++--- config.yml | 4 +- docs/dev.md | 2 +- docs/engine.md | 12 +- docs/events.md | 16 +- docs/testing.md | 15 +- 49 files changed, 761 insertions(+), 679 deletions(-) diff --git a/adventure/actions/base.py b/adventure/actions/base.py index cde29d7..60cc473 100644 --- a/adventure/actions/base.py +++ b/adventure/actions/base.py @@ -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." diff --git a/adventure/actions/optional.py b/adventure/actions/optional.py index 97acad1..2f09dfb 100644 --- a/adventure/actions/optional.py +++ b/adventure/actions/optional.py @@ -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) diff --git a/adventure/actions/planning.py b/adventure/actions/planning.py index 30a7b3c..d31759e 100644 --- a/adventure/actions/planning.py +++ b/adventure/actions/planning.py @@ -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" diff --git a/adventure/actions/quest.py b/adventure/actions/quest.py index 8ea9d2f..499b34d 100644 --- a/adventure/actions/quest.py +++ b/adventure/actions/quest.py @@ -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." diff --git a/adventure/bot/discord.py b/adventure/bot/discord.py index 72406d5..49b990c 100644 --- a/adventure/bot/discord.py +++ b/adventure/bot/discord.py @@ -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 diff --git a/adventure/context.py b/adventure/context.py index 66719b1..3821cde 100644 --- a/adventure/context.py +++ b/adventure/context.py @@ -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 diff --git a/adventure/generate.py b/adventure/generate.py index c1bf52a..cc031d6 100644 --- a/adventure/generate.py +++ b/adventure/generate.py @@ -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 diff --git a/adventure/models/config.py b/adventure/models/config.py index c586a9c..044d832 100644 --- a/adventure/models/config.py +++ b/adventure/models/config.py @@ -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, + ), ), ) diff --git a/adventure/models/entity.py b/adventure/models/entity.py index 36dc47f..fd8ce8d 100644 --- a/adventure/models/entity.py +++ b/adventure/models/entity.py @@ -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 diff --git a/adventure/models/event.py b/adventure/models/event.py index fce3e77..c84bbc3 100644 --- a/adventure/models/event.py +++ b/adventure/models/event.py @@ -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] diff --git a/adventure/player.py b/adventure/player.py index 853ebff..6147b47 100644 --- a/adventure/player.py +++ b/adventure/player.py @@ -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: diff --git a/adventure/render/comfy.py b/adventure/render/comfy.py index 42c6310..30a7817 100644 --- a/adventure/render/comfy.py +++ b/adventure/render/comfy.py @@ -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): diff --git a/adventure/render/prompt.py b/adventure/render/prompt.py index 118ec03..678fb98 100644 --- a/adventure/render/prompt.py +++ b/adventure/render/prompt.py @@ -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)}." diff --git a/adventure/server/websocket.py b/adventure/server/websocket.py index 3a109c4..59c0dd9 100644 --- a/adventure/server/websocket.py +++ b/adventure/server/websocket.py @@ -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) diff --git a/adventure/simulate.py b/adventure/simulate.py index cb6a7e1..b17021f 100644 --- a/adventure/simulate.py +++ b/adventure/simulate.py @@ -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,43 +186,56 @@ 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)) - - result = loop_reduce( - 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. " - "If you have plans with other characters, schedule them on your calendar. You have {event_count} events on your calendar. " - "{room_summary}" - "Think about your goals and any quests that you are working on, and plan your next action accordingly. " - "Try to keep your notes accurate and up-to-date. Replace or erase old notes when they are no longer accurate or useful. " - "Do not keeps notes about upcoming events, use your calendar for that. " - "You can perform up to 3 planning actions in a single turn. When you are done planning, reply with 'END'." - "{notes_prompt} {events_prompt}", - context={ - "event_count": event_count, - "events_prompt": events_prompt, - "note_count": note_count, - "notes_prompt": notes_prompt, - "room_summary": summarize_room(room, actor), - }, - result_parser=result_parser, - stop_condition=stop_condition, - toolbox=planner_toolbox, + stop_condition = condition_or( + condition_end, partial(condition_threshold, max=max_steps) ) - if agent.memory: - agent.memory.append(result) + 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. " + "If you have plans with other characters, schedule them on your calendar. You have {event_count} events on your calendar. " + "{room_summary}" + "Think about your goals and any quests that you are working on, and plan your next action accordingly. " + "Try to keep your notes accurate and up-to-date. Replace or erase old notes when they are no longer accurate or useful. " + "Do not keeps notes about upcoming events, use your calendar for that. " + "You can perform up to 3 planning actions in a single turn. When you are done planning, reply with 'END'." + "{notes_prompt} {events_prompt}", + context={ + "event_count": event_count, + "events_prompt": events_prompt, + "note_count": note_count, + "notes_prompt": notes_prompt, + "room_summary": summarize_room(room, character), + }, + result_parser=result_parser, + stop_condition=stop_condition, + toolbox=planner_toolbox, + ) + + 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}") + logger.exception( + f"error during planning for character {character.name}" + ) - result = prompt_actor_action( - room, actor, agent, action_names, action_tools, current_step + result = prompt_character_action( + room, character, agent, action_names, action_tools, current_step ) - result_event = ResultEvent(result=result, room=room, actor=actor) + result_event = ResultEvent(result=result, room=room, character=character) broadcast(result_event) for system in systems: diff --git a/adventure/state.py b/adventure/state.py index 70181f9..e77a7a4 100644 --- a/adventure/state.py +++ b/adventure/state.py @@ -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, diff --git a/adventure/systems/logic.py b/adventure/systems/logic.py index 9755048..fcea916 100644 --- a/adventure/systems/logic.py +++ b/adventure/systems/logic.py @@ -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) diff --git a/adventure/systems/quest.py b/adventure/systems/quest.py index 776bb37..c55ca10 100644 --- a/adventure/systems/quest.py +++ b/adventure/systems/quest.py @@ -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: diff --git a/adventure/systems/rpg/crafting_actions.py b/adventure/systems/rpg/crafting_actions.py index b75f0f2..ed12d4b 100644 --- a/adventure/systems/rpg/crafting_actions.py +++ b/adventure/systems/rpg/crafting_actions.py @@ -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}." diff --git a/adventure/systems/rpg/language_actions.py b/adventure/systems/rpg/language_actions.py index fd32a24..721e4e5 100644 --- a/adventure/systems/rpg/language_actions.py +++ b/adventure/systems/rpg/language_actions.py @@ -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." diff --git a/adventure/systems/rpg/magic_actions.py b/adventure/systems/rpg/magic_actions.py index f945a57..c591a09 100644 --- a/adventure/systems/rpg/magic_actions.py +++ b/adventure/systems/rpg/magic_actions.py @@ -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}." diff --git a/adventure/systems/rpg/movement_actions.py b/adventure/systems/rpg/movement_actions.py index bccd231..7e16b97 100644 --- a/adventure/systems/rpg/movement_actions.py +++ b/adventure/systems/rpg/movement_actions.py @@ -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: diff --git a/adventure/systems/sim/combat_actions.py b/adventure/systems/sim/combat_actions.py index 1c46e0e..caf51c8 100644 --- a/adventure/systems/sim/combat_actions.py +++ b/adventure/systems/sim/combat_actions.py @@ -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 diff --git a/adventure/systems/sim/environment_logic.yaml b/adventure/systems/sim/environment_logic.yaml index 8f12868..e69be1a 100644 --- a/adventure/systems/sim/environment_logic.yaml +++ b/adventure/systems/sim/environment_logic.yaml @@ -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. diff --git a/adventure/systems/sim/environment_triggers.py b/adventure/systems/sim/environment_triggers.py index 82ff522..b61d0b9 100644 --- a/adventure/systems/sim/environment_triggers.py +++ b/adventure/systems/sim/environment_triggers.py @@ -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 diff --git a/adventure/systems/sim/hunger_actions.py b/adventure/systems/sim/hunger_actions.py index 289aeef..6b87a91 100644 --- a/adventure/systems/sim/hunger_actions.py +++ b/adventure/systems/sim/hunger_actions.py @@ -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}." diff --git a/adventure/systems/sim/hunger_logic.yaml b/adventure/systems/sim/hunger_logic.yaml index c50e43b..e9bbabe 100644 --- a/adventure/systems/sim/hunger_logic.yaml +++ b/adventure/systems/sim/hunger_logic.yaml @@ -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. \ No newline at end of file diff --git a/adventure/systems/sim/hygiene_actions.py b/adventure/systems/sim/hygiene_actions.py index 08657e7..d906f8d 100644 --- a/adventure/systems/sim/hygiene_actions.py +++ b/adventure/systems/sim/hygiene_actions.py @@ -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}" diff --git a/adventure/systems/sim/hygiene_logic.yaml b/adventure/systems/sim/hygiene_logic.yaml index 4ba9949..f675251 100644 --- a/adventure/systems/sim/hygiene_logic.yaml +++ b/adventure/systems/sim/hygiene_logic.yaml @@ -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. diff --git a/adventure/systems/sim/mood_logic.yaml b/adventure/systems/sim/mood_logic.yaml index 807dd8b..da9633b 100644 --- a/adventure/systems/sim/mood_logic.yaml +++ b/adventure/systems/sim/mood_logic.yaml @@ -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. diff --git a/adventure/systems/sim/sleeping_actions.py b/adventure/systems/sim/sleeping_actions.py index ec3a300..9cf37a1 100644 --- a/adventure/systems/sim/sleeping_actions.py +++ b/adventure/systems/sim/sleeping_actions.py @@ -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}" diff --git a/adventure/systems/sim/sleeping_logic.yaml b/adventure/systems/sim/sleeping_logic.yaml index c99357d..49f2550 100644 --- a/adventure/systems/sim/sleeping_logic.yaml +++ b/adventure/systems/sim/sleeping_logic.yaml @@ -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. diff --git a/adventure/utils/conversation.py b/adventure/utils/conversation.py index a2d96e1..1df673f 100644 --- a/adventure/utils/conversation.py +++ b/adventure/utils/conversation.py @@ -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 diff --git a/adventure/utils/effect.py b/adventure/utils/effect.py index 5a25690..db16a62 100644 --- a/adventure/utils/effect.py +++ b/adventure/utils/effect.py @@ -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: diff --git a/adventure/utils/planning.py b/adventure/utils/planning.py index f336a9e..06b8345 100644 --- a/adventure/utils/planning.py +++ b/adventure/utils/planning.py @@ -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 diff --git a/adventure/utils/search.py b/adventure/utils/search.py index 37d7d40..a7f2c90 100644 --- a/adventure/utils/search.py +++ b/adventure/utils/search.py @@ -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 diff --git a/adventure/utils/world.py b/adventure/utils/world.py index 5831bb0..2b6921c 100644 --- a/adventure/utils/world.py +++ b/adventure/utils/world.py @@ -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) diff --git a/client/src/app.tsx b/client/src/app.tsx index 368376b..f6cf6f7 100644 --- a/client/src/app.tsx +++ b/client/src/app.tsx @@ -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) { + function setPlayer(character: Maybe) { // 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': diff --git a/client/src/details.tsx b/client/src/details.tsx index 7ac5e64..85ea5f3 100644 --- a/client/src/details.tsx +++ b/client/src/details.tsx @@ -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; + entity: Maybe; 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}; } -export function isWorld(entity: Maybe): entity is World { +export function isWorld(entity: Maybe): entity is World { return doesExist(entity) && doesExist(Object.getOwnPropertyDescriptor(entity, 'theme')); } diff --git a/client/src/events.tsx b/client/src/events.tsx index 2080c4c..e3410ec 100644 --- a/client/src/events.tsx +++ b/client/src/events.tsx @@ -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, b: Maybe): boolean { +export function sameCharacter(a: Maybe, b: Maybe): boolean { if (doesExist(a) && doesExist(b)) { return a.name === b.name; } @@ -46,13 +46,13 @@ export function sameCharacter(a: Maybe, b: Maybe): 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} {content} @@ -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 = { diff --git a/client/src/models.ts b/client/src/models.ts index 3c6bd81..02c419d 100644 --- a/client/src/models.ts +++ b/client/src/models.ts @@ -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; + characters: Array; items: Array; portals: Array; attributes: Attributes; diff --git a/client/src/player.tsx b/client/src/player.tsx index b6c0242..ce7ba93 100644 --- a/client/src/player.tsx +++ b/client/src/player.tsx @@ -10,7 +10,7 @@ export interface PlayerPanelProps { export function playerStateSelector(s: StoreState) { return { - character: s.character, + character: s.playerCharacter, activeTurn: s.activeTurn, }; } diff --git a/client/src/store.ts b/client/src/store.ts index 8930c85..9b947a3 100644 --- a/client/src/store.ts +++ b/client/src/store.ts @@ -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; + detailEntity: Maybe; eventHistory: Array; 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) => void; + setDetailEntity: (entity: Maybe) => 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; + playerCharacter: Maybe; // setters setActiveTurn: (activeTurn: boolean) => void; - setCharacter: (character: Maybe) => void; + setPlayerCharacter: (character: Maybe) => void; // misc helpers isPlaying: () => boolean; @@ -114,11 +114,11 @@ export function createWorldStore(): StateCreator { export function createPlayerStore(): StateCreator { return (set) => ({ activeTurn: false, - character: undefined, + playerCharacter: undefined, setActiveTurn: (activeTurn: boolean) => set({ activeTurn }), - setCharacter: (character: Maybe) => set({ character }), + setPlayerCharacter: (character: Maybe) => set({ playerCharacter: character }), isPlaying() { - return doesExist(this.character); + return doesExist(this.playerCharacter); }, }); } diff --git a/client/src/world.tsx b/client/src/world.tsx index 5068fc6..ed46188 100644 --- a/client/src/world.tsx +++ b/client/src/world.tsx @@ -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) => void; -export type SetPlayer = (actor: Maybe) => void; +export type SetDetails = (entity: Maybe) => void; +export type SetPlayer = (character: Maybe) => 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) { ; } -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 = setPlayer(undefined)} />; + playButton = 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 = ; + const player = Object.entries(players).find((it) => it[1] === character.name)?.[0]; + playButton = ; } else { - playButton = setPlayer(actor)} />; + playButton = setPlayer(character)} />; } } - return + return {playButton} - setDetailEntity(actor)} /> - - {actor.items.map((item) => )} + setDetailEntity(character)} /> + + {character.items.map((item) => )} ; } @@ -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 setDetailEntity(room)} /> - - {room.actors.map((actor) => )} + + {room.characters.map((character) => )} {room.items.map((item) => )} diff --git a/config.yml b/config.yml index 5c87e85..256c9f8 100644 --- a/config.yml +++ b/config.yml @@ -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: diff --git a/docs/dev.md b/docs/dev.md index 8309fc3..0518dd9 100644 --- a/docs/dev.md +++ b/docs/dev.md @@ -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 diff --git a/docs/engine.md b/docs/engine.md index ecbfd5e..a93c9bd 100644 --- a/docs/engine.md +++ b/docs/engine.md @@ -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 actor’s 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 diff --git a/docs/events.md b/docs/events.md index 74ca5af..9723137 100644 --- a/docs/events.md +++ b/docs/events.md @@ -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 diff --git a/docs/testing.md b/docs/testing.md index a00241a..94d3251 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -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