1
0
Fork 0

rename actor to character, add step limits to config

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

View File

@ -3,15 +3,15 @@ from logging import getLogger
from adventure.context import ( from adventure.context import (
action_context, action_context,
broadcast, broadcast,
get_actor_agent_for_name, get_agent_for_character,
get_agent_for_actor, get_character_agent_for_name,
world_context, world_context,
) )
from adventure.errors import ActionError from adventure.errors import ActionError
from adventure.utils.conversation import loop_conversation from adventure.utils.conversation import loop_conversation
from adventure.utils.search import ( from adventure.utils.search import (
find_actor_in_room, find_character_in_room,
find_item_in_actor, find_item_in_character,
find_item_in_room, find_item_in_room,
find_room, find_room,
) )
@ -31,31 +31,31 @@ def action_look(target: str) -> str:
target: The name of the target to look at. target: The name of the target to look at.
""" """
with action_context() as (action_room, action_actor): with action_context() as (action_room, action_character):
broadcast(f"{action_actor.name} looks at {target}") broadcast(f"{action_character.name} looks at {target}")
if normalize_name(target) == normalize_name(action_room.name): 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) return describe_entity(action_room)
target_actor = find_actor_in_room(action_room, target) target_character = find_character_in_room(action_room, target)
if target_actor: if target_character:
broadcast( 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) target_item = find_item_in_room(action_room, target)
if target_item: if target_item:
broadcast( 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) 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: if target_item:
broadcast( 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) return describe_entity(target_item)
@ -70,7 +70,7 @@ def action_move(direction: str) -> str:
direction: The direction to move in. 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( portal = next(
( (
p p
@ -87,10 +87,10 @@ def action_move(direction: str) -> str:
raise ActionError(f"The {portal.destination} room does not exist.") raise ActionError(f"The {portal.destination} room does not exist.")
broadcast( 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) action_room.characters.remove(action_character)
destination_room.actors.append(action_actor) destination_room.characters.append(action_character)
return ( return (
f"You move through the {direction} and arrive at {destination_room.name}." f"You move through the {direction} and arrive at {destination_room.name}."
@ -104,14 +104,14 @@ def action_take(item: str) -> str:
Args: Args:
item: The name of the item to take. 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) action_item = find_item_in_room(action_room, item)
if not action_item: if not action_item:
raise ActionError(f"The {item} item is not in the room.") 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_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." 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. character: The name of the character to ask. You cannot ask yourself questions.
question: The question to ask them. question: The question to ask 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 # sanity checks
question_actor, question_agent = get_actor_agent_for_name(character) question_character, question_agent = get_character_agent_for_name(character)
if question_actor == action_actor: if question_character == action_character:
raise ActionError( raise ActionError(
"You cannot ask yourself a question. Stop talking to yourself. Try another action." "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.") raise ActionError(f"The {character} character is not in the room.")
if not question_agent: if not question_agent:
raise ActionError(f"The {character} character does not exist.") 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 = ( 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. " "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 = ( 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. " "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( answer = loop_conversation(
action_room, action_room,
[question_actor, action_actor], [question_character, action_character],
[question_agent, action_agent], [question_agent, action_agent],
action_actor, action_character,
first_prompt, first_prompt,
reply_prompt, reply_prompt,
question, question,
@ -166,7 +166,7 @@ def action_ask(character: str, question: str) -> str:
) )
if answer: 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} responds: {answer}"
return f"{character} does not respond." 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. character: The name of the character to tell. You cannot talk to yourself.
message: The message to tell them. 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 # sanity checks
question_actor, question_agent = get_actor_agent_for_name(character) question_character, question_agent = get_character_agent_for_name(character)
if question_actor == action_actor: if question_character == action_character:
raise ActionError( raise ActionError(
"You cannot tell yourself a message. Stop talking to yourself. Try another action." "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.") raise ActionError(f"The {character} character is not in the room.")
if not question_agent: if not question_agent:
raise ActionError(f"The {character} character does not exist.") 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 = ( 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. " "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 = ( 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 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( answer = loop_conversation(
action_room, action_room,
[question_actor, action_actor], [question_character, action_character],
[question_agent, action_agent], [question_agent, action_agent],
action_actor, action_character,
first_prompt, first_prompt,
reply_prompt, reply_prompt,
message, message,
@ -224,7 +224,7 @@ def action_tell(character: str, message: str) -> str:
) )
if answer: 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} responds: {answer}"
return f"{character} does not respond." 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. character: The name of the character to give the item to.
item: The name of the item to give. item: The name of the item to give.
""" """
with action_context() as (action_room, action_actor): with action_context() as (action_room, action_character):
destination_actor = find_actor_in_room(action_room, character) destination_character = find_character_in_room(action_room, character)
if not destination_actor: if not destination_character:
raise ActionError(f"The {character} character is not in the room.") raise ActionError(f"The {character} character is not in the room.")
if destination_actor == action_actor: if destination_character == action_character:
raise ActionError( raise ActionError(
"You cannot give an item to yourself. Try another action." "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: if not action_item:
raise ActionError(f"You do not have the {item} item in your inventory.") raise ActionError(f"You do not have the {item} item in your inventory.")
broadcast(f"{action_actor.name} gives {character} the {item} item.") broadcast(f"{action_character.name} gives {character} the {item} item.")
action_actor.items.remove(action_item) action_character.items.remove(action_item)
destination_actor.items.append(action_item) destination_character.items.append(action_item)
return f"You give the {item} item to {character}." 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. item: The name of the item to drop.
""" """
with action_context() as (action_room, action_actor): with action_context() as (action_room, action_character):
action_item = find_item_in_actor(action_actor, item) action_item = find_item_in_character(action_character, item)
if not action_item: if not action_item:
raise ActionError(f"You do not have the {item} item in your inventory.") raise ActionError(f"You do not have the {item} item in your inventory.")
broadcast(f"{action_actor.name} drops the {item} item") broadcast(f"{action_character.name} drops the {item} item")
action_actor.items.remove(action_item) action_character.items.remove(action_item)
action_room.items.append(action_item) action_room.items.append(action_item)
return f"You drop the {item} item." return f"You drop the {item} item."

View File

@ -6,7 +6,7 @@ from packit.agent import Agent, agent_easy_connect
from adventure.context import ( from adventure.context import (
action_context, action_context,
broadcast, broadcast,
get_agent_for_actor, get_agent_for_character,
get_dungeon_master, get_dungeon_master,
get_game_systems, get_game_systems,
has_dungeon_master, has_dungeon_master,
@ -16,9 +16,9 @@ from adventure.context import (
from adventure.errors import ActionError from adventure.errors import ActionError
from adventure.generate import generate_item, generate_room, link_rooms from adventure.generate import generate_item, generate_room, link_rooms
from adventure.utils.effect import apply_effects 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.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__) 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. 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() dungeon_master = get_dungeon_master()
if direction in action_room.portals: 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]) link_rooms(dungeon_master, action_world, systems, [new_room])
broadcast( 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}" return f"You explore {direction} and find a new room: {new_room.name}"
except Exception: except Exception:
@ -75,7 +75,7 @@ def action_search(unused: bool) -> str:
Search the room for hidden items. 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() dungeon_master = get_dungeon_master()
if len(action_room.items) > 2: if len(action_room.items) > 2:
@ -94,7 +94,7 @@ def action_search(unused: bool) -> str:
action_room.items.append(new_item) action_room.items.append(new_item)
broadcast( 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}" return f"You search the room and find a new item: {new_item.name}"
except Exception: except Exception:
@ -110,13 +110,13 @@ def action_use(item: str, target: str) -> str:
item: The name of the item to use. 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. 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() dungeon_master = get_dungeon_master()
action_item = next( action_item = next(
( (
search_item 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 if search_item.name == item
), ),
None, None,
@ -125,17 +125,17 @@ def action_use(item: str, target: str) -> str:
raise ActionError(f"The {item} item is not available to use.") raise ActionError(f"The {item} item is not available to use.")
if target == "self": if target == "self":
target_actor = action_actor target_character = action_character
target = action_actor.name target = action_character.name
else: else:
# TODO: allow targeting the room itself and items in the room # TODO: allow targeting the room itself and items in the room
target_actor = find_actor_in_room(action_room, target) target_character = find_character_in_room(action_room, target)
if not target_actor: if not target_character:
return f"The {target} character is not in the room." return f"The {target} character is not in the room."
effect_names = [effect.name for effect in action_item.effects] effect_names = [effect.name for effect in action_item.effects]
chosen_name = dungeon_master( 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}. " f"{item} has the following effects: {effect_names}. "
"Which effect should be applied? Specify the name of the effect to apply." "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." "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.") raise ValueError(f"The {chosen_name} effect is not available to apply.")
try: try:
apply_effects(target_actor, [chosen_effect]) apply_effects(target_character, [chosen_effect])
except Exception: except Exception:
logger.exception("error applying effect: %s", chosen_effect) logger.exception("error applying effect: %s", chosen_effect)
raise ValueError( raise ValueError(
@ -163,11 +163,11 @@ def action_use(item: str, target: str) -> str:
) )
broadcast( 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( outcome = dungeon_master(
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}. "
f"{describe_actor(action_actor)}. {describe_actor(target_actor)}. {describe_entity(action_item)}. " 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." 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." "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." "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}") broadcast(f"The action resulted in: {outcome}")
# make sure both agents remember the 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: if target_agent and target_agent.memory:
target_agent.memory.append(outcome) target_agent.memory.append(outcome)

View File

@ -1,10 +1,10 @@
from adventure.context import action_context, get_agent_for_actor, get_current_step from adventure.context import action_context, get_agent_for_character, get_current_step
from adventure.errors import ActionError from adventure.errors import ActionError
from adventure.models.config import DEFAULT_CONFIG from adventure.models.config import DEFAULT_CONFIG
from adventure.models.planning import CalendarEvent from adventure.models.planning import CalendarEvent
from adventure.utils.planning import get_recent_notes 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): def take_note(fact: str):
@ -16,16 +16,16 @@ def take_note(fact: str):
fact: The fact to remember. fact: The fact to remember.
""" """
with action_context() as (_, action_actor): with action_context() as (_, action_character):
if fact in action_actor.planner.notes: if fact in action_character.planner.notes:
raise ActionError("You already have a note about that fact.") raise ActionError("You already have a note about that fact.")
if len(action_actor.planner.notes) >= actor_config.note_limit: if len(action_character.planner.notes) >= character_config.note_limit:
raise ActionError( raise ActionError(
"You have reached the limit of notes you can take. Please erase, replace, or summarize some notes." "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." 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. count: The number of recent notes to read. 10 is usually a good number.
""" """
with action_context() as (_, action_actor): with action_context() as (_, action_character):
facts = get_recent_notes(action_actor, count=count) facts = get_recent_notes(action_character, count=count)
return "\n".join(facts) return "\n".join(facts)
@ -51,15 +51,15 @@ def erase_notes(prefix: str) -> str:
prefix: The prefix to match notes against. prefix: The prefix to match notes against.
""" """
with action_context() as (_, action_actor): with action_context() as (_, action_character):
matches = [ 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: if not matches:
return "No notes found with that prefix." return "No notes found with that prefix."
action_actor.planner.notes[:] = [ action_character.planner.notes[:] = [
note for note in action_actor.planner.notes if note not in matches note for note in action_character.planner.notes if note not in matches
] ]
return f"Erased {len(matches)} notes." 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. new: The new note to replace it with.
""" """
with action_context() as (_, action_actor): with action_context() as (_, action_character):
if old not in action_actor.planner.notes: if old not in action_character.planner.notes:
return "Note not found." return "Note not found."
action_actor.planner.notes[:] = [ action_character.planner.notes[:] = [
new if note == old else note for note in action_actor.planner.notes new if note == old else note for note in action_character.planner.notes
] ]
return "Note replaced." return "Note replaced."
@ -91,12 +91,12 @@ def summarize_notes(limit: int) -> str:
limit: The maximum number of notes to keep. limit: The maximum number of notes to keep.
""" """
with action_context() as (_, action_actor): with action_context() as (_, action_character):
notes = action_actor.planner.notes notes = action_character.planner.notes
action_agent = get_agent_for_actor(action_actor) action_agent = get_agent_for_character(action_character)
if not action_agent: 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( summary = action_agent(
"Please summarize your notes. Remove any duplicates and combine similar notes. " "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()] 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( 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." 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. 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 # TODO: check for existing events with the same name
event = CalendarEvent(name, turns) 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." 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() current_turn = get_current_step()
with action_context() as (_, action_actor): with action_context() as (_, action_character):
events = action_actor.planner.calendar.events[:count] events = action_character.planner.calendar.events[:count]
return "\n".join( return "\n".join(
[ [
f"{event.name} will happen in {event.turn - current_turn} turns" f"{event.name} will happen in {event.turn - current_turn} turns"

View File

@ -3,56 +3,56 @@ from adventure.systems.quest import (
QUEST_SYSTEM, QUEST_SYSTEM,
complete_quest, complete_quest,
get_active_quest, get_active_quest,
get_quests_for_actor, get_quests_for_character,
set_active_quest, 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. 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) quests = get_system_data(QUEST_SYSTEM)
if not quests: if not quests:
return "No quests available." return "No quests available."
target_actor = find_actor_in_room(action_room, actor) target_character = find_character_in_room(action_room, character)
if not target_actor: if not target_character:
return f"{actor} is not in the room." 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: for available_quest in available_quests:
if available_quest.name == quest: 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"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. 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) quests = get_system_data(QUEST_SYSTEM)
if not quests: if not quests:
return "No quests available." return "No quests available."
active_quest = get_active_quest(quests, action_actor) active_quest = get_active_quest(quests, action_character)
if not active_quest: if not active_quest:
return "You do not have an active quest." return "You do not have an active quest."
target_actor = find_actor_in_room(action_room, actor) target_character = find_character_in_room(action_room, character)
if not target_actor: if not target_character:
return f"{actor} is not in the room." return f"{character} is not in the room."
if active_quest.giver.actor == target_actor.name: if active_quest.giver.character == target_character.name:
complete_quest(quests, action_actor, active_quest) complete_quest(quests, action_character, active_quest)
return f"You have completed the quest: {active_quest.name}" return f"You have completed the quest: {active_quest.name}"
return f"{actor} is not the quest giver for your active quest." return f"{character} is not the quest giver for your active quest."

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -178,9 +178,9 @@ class RemotePlayer(BasePlayer):
formatted_prompt = prompt.format(**kwargs) formatted_prompt = prompt.format(**kwargs)
self.memory.append(HumanMessage(content=formatted_prompt)) 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_event = PromptEvent(
prompt=formatted_prompt, room=current_room, actor=current_actor prompt=formatted_prompt, room=current_room, character=current_character
) )
try: try:

View File

@ -226,14 +226,16 @@ def fast_hash(text: str) -> str:
def get_image_prefix(event: GameEvent | WorldEntity) -> str: def get_image_prefix(event: GameEvent | WorldEntity) -> str:
if isinstance(event, ActionEvent): 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): 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): if isinstance(event, ResultEvent):
return sanitize_name( 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): if isinstance(event, StatusEvent):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -31,19 +31,19 @@ def action_craft(item: str) -> str:
Args: Args:
item: The name of the item to craft. 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: if item not in recipes:
return f"There is no recipe to craft a {item}." return f"There is no recipe to craft a {item}."
recipe = recipes[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) skill = randint(1, 20)
if skill < recipe.difficulty: if skill < recipe.difficulty:
return f"You need a crafting skill level of {recipe.difficulty} to craft {item}." return f"You need a crafting skill level of {recipe.difficulty} to craft {item}."
# Collect inventory items names # 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 # Check for sufficient ingredients
missing_items = [ missing_items = [
@ -55,13 +55,14 @@ def action_craft(item: str) -> str:
# Deduct the ingredients from inventory # Deduct the ingredients from inventory
for ingredient in recipe.ingredients: for ingredient in recipe.ingredients:
item_to_remove = next( 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 # Create and add the crafted item to inventory
result_item = next( 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: if result_item:
new_item = Item(**vars(result_item)) # Copying the 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 dungeon_master, action_world, systems
) # TODO: pass crafting recipe and generate from that ) # 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}." return f"You successfully craft a {item}."

View File

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

View File

@ -1,7 +1,7 @@
from random import randint from random import randint
from adventure.context import action_context, broadcast, get_dungeon_master 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: 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. spell: The name of the spell to cast.
target: The target of the spell. target: The target of the spell.
""" """
with action_context() as (action_room, action_actor): with action_context() as (action_room, action_character):
target_actor = find_actor_in_room(action_room, target) target_character = find_character_in_room(action_room, target)
dungeon_master = get_dungeon_master() dungeon_master = get_dungeon_master()
# Check for spell availability and mana costs # 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}'." 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." 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 # Get flavor text from the dungeon master
flavor_text = dungeon_master(f"Describe the effects of {spell} on {target}.") 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 # Apply effects based on the spell
if spell == "heal" and target_actor: if spell == "heal" and target_character:
heal_amount = randint(10, 30) 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"{target} is healed for {heal_amount} points."
return f"{spell} was successfully cast on {target}." return f"{spell} was successfully cast on {target}."

View File

@ -11,7 +11,7 @@ def action_climb(target: str) -> str:
Args: Args:
target: The object or feature to climb. 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() dungeon_master = get_dungeon_master()
# Assume 'climbable' is an attribute that marks climbable targets # Assume 'climbable' is an attribute that marks climbable targets
climbable_feature = find_item_in_room(action_room, target) 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 # Get flavor text for the climb attempt
flavor_text = dungeon_master( 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: if climb_roll > climb_difficulty:
broadcast( 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}." return f"You successfully climb the {target}."
else: else:
broadcast( 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}." return f"You fail to climb the {target}."
else: else:

View File

@ -1,10 +1,10 @@
from adventure.context import ( from adventure.context import (
action_context, action_context,
broadcast, broadcast,
get_agent_for_actor, get_agent_for_character,
get_dungeon_master, 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 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. 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 # 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) target_item = find_item_in_room(action_room, target)
dungeon_master = get_dungeon_master() dungeon_master = get_dungeon_master()
if target_actor: if target_character:
target_agent = get_agent_for_actor(target_actor) target_agent = get_agent_for_character(target_character)
if not target_agent: 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( 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'." "Respond with 'fighting', 'fleeing', or 'surrendering'."
) )
outcome = dungeon_master( outcome = dungeon_master(
f"{action_actor.name} attacks {target} in the {action_room.name}. {describe_entity(action_room)}." f"{action_character.name} attacks {target} in the {action_room.name}. {describe_entity(action_room)}."
f"{describe_entity(action_actor)}. {describe_entity(target_actor)}." 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." f"{target} reacts by {reaction}. What is the outcome of the attack? Describe the result in detail."
) )
description = ( 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}" f"{target} reacts by {reaction}. {outcome}"
) )
broadcast(description) broadcast(description)
@ -47,12 +49,12 @@ def action_attack(target: str) -> str:
if target_item: if target_item:
outcome = dungeon_master( outcome = dungeon_master(
f"{action_actor.name} attacks {target} in the {action_room.name}. {describe_entity(action_room)}." f"{action_character.name} attacks {target} in the {action_room.name}. {describe_entity(action_room)}."
f"{describe_entity(action_actor)}. {describe_entity(target_item)}." f"{describe_entity(action_character)}. {describe_entity(target_item)}."
f"What is the outcome of the attack? Describe the result in detail." 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) broadcast(description)
return description return description
@ -68,21 +70,21 @@ def action_cast(target: str, spell: str) -> str:
spell: The name of the spell to cast. 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 # 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) 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}." return f"{target} is not in the {action_room.name}."
dungeon_master = get_dungeon_master() dungeon_master = get_dungeon_master()
outcome = dungeon_master( outcome = dungeon_master(
f"{action_actor.name} casts {spell} on {target} in the {action_room.name}. {describe_entity(action_room)}." f"{action_character.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"{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." 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) broadcast(description)
return description return description

View File

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

View File

@ -3,21 +3,21 @@ from adventure.models.entity import Attributes, Room
def hot_room(room: Room, attributes: Attributes): 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: for character in room.characters:
actor.attributes["hot"] = "hot" character.attributes["hot"] = "hot"
return attributes return attributes
def cold_room(room: Room, attributes: 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: for character in room.characters:
actor.attributes["cold"] = "cold" character.attributes["cold"] = "cold"
return attributes return attributes

View File

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

View File

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

View File

@ -7,15 +7,15 @@ def action_wash(unused: bool) -> str:
Wash yourself. Wash yourself.
""" """
with action_context() as (action_room, action_actor): with action_context() as (action_room, action_character):
hygiene = action_actor.attributes.get("hygiene", "clean") hygiene = action_character.attributes.get("hygiene", "clean")
dungeon_master = get_dungeon_master() dungeon_master = get_dungeon_master()
outcome = 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_character.name} washes themselves in the {action_room.name}. {describe_entity(action_room)}. {describe_entity(action_character)}"
f"{action_actor.name} was {hygiene} to start with. How clean are they after washing? Respond with 'clean' or 'dirty'." 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." "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}" return f"You wash yourself in the {action_room.name} and feel {outcome}"

View File

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

View File

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

View File

@ -7,12 +7,12 @@ def action_sleep(unused: bool) -> str:
Sleep until you are rested. 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() dungeon_master = get_dungeon_master()
outcome = 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'." "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}" return f"You sleep in the {action_room.name} and wake up feeling {outcome}"

View File

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

View File

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

View File

@ -13,7 +13,7 @@ from adventure.models.effect import (
StringEffectPattern, StringEffectPattern,
StringEffectResult, StringEffectResult,
) )
from adventure.models.entity import Actor, Attributes from adventure.models.entity import Attributes, Character
from adventure.utils.attribute import ( from adventure.utils.attribute import (
add_value, add_value,
append_value, append_value,
@ -252,9 +252,9 @@ def apply_permanent_effects(
return apply_permanent_results(attributes, results) 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 = [ permanent_effects = [
@ -270,9 +270,9 @@ def apply_effects(target: Actor, effects: List[EffectPattern]) -> None:
target.active_effects.extend(temporary_effects) 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: for effect in target.active_effects:

View File

@ -1,34 +1,36 @@
from adventure.models.entity import Actor from adventure.models.entity import Character
def expire_events(actor: Actor, current_turn: int): def expire_events(character: Character, current_turn: int):
""" """
Expire events that have already happened. 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] 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 event for event in events if event not in expired_events
] ]
return 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. 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. 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 # TODO: sort events by turn
return [ return [
event event

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -45,5 +45,5 @@ additional configuration from a YAML or JSON file.
- figure out the human input syntax for actions - figure out the human input syntax for actions
- make an admin panel in web UI - 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 - generate and simulate should probably be async

View File

@ -30,10 +30,10 @@ mechanics to suit different types of adventures and narrative styles.
### What kinds of entities exist in the world? ### 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 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, 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 Actors. Currently, 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 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. 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 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 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, 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 actors, or modify the 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' 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. decisions have direct consequences on the game's progression and outcome.
### What are attributes? ### What are attributes?
Attributes in TaleWeave AI are key-value pairs that define the properties of an entity. These attributes can be of Attributes in TaleWeave AI are key-value pairs that define the properties of an entity. These attributes can be of
various types—boolean, number, or string—such as an actors mood being "happy," their health being quantified as 10, or various types—boolean, number, or string—such as a character's mood being "happy," their health being quantified as 10, or
an item's quality described as "broken" or quantified with a remaining usage of 3. Attributes play a crucial role in the 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 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 specific rules within the game, and their labels are included in prompts to guide language model players in making

View File

@ -77,7 +77,7 @@ added to the world.
```yaml ```yaml
type: "generate" type: "generate"
name: string 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`. 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 ### 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 ```yaml
type: "action" type: "action"
action: string action: string
parameters: dict parameters: dict
room: Room room: Room
actor: Actor character: Character
item: Item | None 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" type: "prompt"
prompt: string prompt: string
room: Room room: Room
actor: Actor character: Character
``` ```
### Reply Events ### 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" type: "reply"
text: string text: string
room: Room room: Room
actor: Actor character: Character
``` ```
### Result Events ### Result Events
@ -129,10 +129,10 @@ The result event is fired after a character has taken an action and contains the
type: "result" type: "result"
result: string result: string
room: Room 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. may have a reply or error instead.
### Status Events ### Status Events
@ -143,7 +143,7 @@ The status event is fired for general events in the world and messages about oth
type: "status" type: "status"
text: string text: string
room: Room | None room: Room | None
actor: Actor | None character: Character | None
``` ```
### Snapshot Events ### Snapshot Events

View File

@ -24,6 +24,8 @@
- [Player leaves the game during their turn](#player-leaves-the-game-during-their-turn) - [Player leaves the game during their turn](#player-leaves-the-game-during-their-turn)
- [Spectator renders a recent event](#spectator-renders-a-recent-event) - [Spectator renders a recent event](#spectator-renders-a-recent-event)
- [User Stories for Developers](#user-stories-for-developers) - [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) - [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) - [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 ### 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 #### 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 #### Project Contributor fixes a bug in the engine