rename actor to character, add step limits to config
This commit is contained in:
parent
fc07ccd9df
commit
c81d2ae3f2
|
@ -3,15 +3,15 @@ from logging import getLogger
|
|||
from adventure.context import (
|
||||
action_context,
|
||||
broadcast,
|
||||
get_actor_agent_for_name,
|
||||
get_agent_for_actor,
|
||||
get_agent_for_character,
|
||||
get_character_agent_for_name,
|
||||
world_context,
|
||||
)
|
||||
from adventure.errors import ActionError
|
||||
from adventure.utils.conversation import loop_conversation
|
||||
from adventure.utils.search import (
|
||||
find_actor_in_room,
|
||||
find_item_in_actor,
|
||||
find_character_in_room,
|
||||
find_item_in_character,
|
||||
find_item_in_room,
|
||||
find_room,
|
||||
)
|
||||
|
@ -31,31 +31,31 @@ def action_look(target: str) -> str:
|
|||
target: The name of the target to look at.
|
||||
"""
|
||||
|
||||
with action_context() as (action_room, action_actor):
|
||||
broadcast(f"{action_actor.name} looks at {target}")
|
||||
with action_context() as (action_room, action_character):
|
||||
broadcast(f"{action_character.name} looks at {target}")
|
||||
|
||||
if normalize_name(target) == normalize_name(action_room.name):
|
||||
broadcast(f"{action_actor.name} saw the {action_room.name} room")
|
||||
broadcast(f"{action_character.name} saw the {action_room.name} room")
|
||||
return describe_entity(action_room)
|
||||
|
||||
target_actor = find_actor_in_room(action_room, target)
|
||||
if target_actor:
|
||||
target_character = find_character_in_room(action_room, target)
|
||||
if target_character:
|
||||
broadcast(
|
||||
f"{action_actor.name} saw the {target_actor.name} actor in the {action_room.name} room"
|
||||
f"{action_character.name} saw the {target_character.name} character in the {action_room.name} room"
|
||||
)
|
||||
return describe_entity(target_actor)
|
||||
return describe_entity(target_character)
|
||||
|
||||
target_item = find_item_in_room(action_room, target)
|
||||
if target_item:
|
||||
broadcast(
|
||||
f"{action_actor.name} saw the {target_item.name} item in the {action_room.name} room"
|
||||
f"{action_character.name} saw the {target_item.name} item in the {action_room.name} room"
|
||||
)
|
||||
return describe_entity(target_item)
|
||||
|
||||
target_item = find_item_in_actor(action_actor, target)
|
||||
target_item = find_item_in_character(action_character, target)
|
||||
if target_item:
|
||||
broadcast(
|
||||
f"{action_actor.name} saw the {target_item.name} item in their inventory"
|
||||
f"{action_character.name} saw the {target_item.name} item in their inventory"
|
||||
)
|
||||
return describe_entity(target_item)
|
||||
|
||||
|
@ -70,7 +70,7 @@ def action_move(direction: str) -> str:
|
|||
direction: The direction to move in.
|
||||
"""
|
||||
|
||||
with world_context() as (action_world, action_room, action_actor):
|
||||
with world_context() as (action_world, action_room, action_character):
|
||||
portal = next(
|
||||
(
|
||||
p
|
||||
|
@ -87,10 +87,10 @@ def action_move(direction: str) -> str:
|
|||
raise ActionError(f"The {portal.destination} room does not exist.")
|
||||
|
||||
broadcast(
|
||||
f"{action_actor.name} moves through {direction} to {destination_room.name}"
|
||||
f"{action_character.name} moves through {direction} to {destination_room.name}"
|
||||
)
|
||||
action_room.actors.remove(action_actor)
|
||||
destination_room.actors.append(action_actor)
|
||||
action_room.characters.remove(action_character)
|
||||
destination_room.characters.append(action_character)
|
||||
|
||||
return (
|
||||
f"You move through the {direction} and arrive at {destination_room.name}."
|
||||
|
@ -104,14 +104,14 @@ def action_take(item: str) -> str:
|
|||
Args:
|
||||
item: The name of the item to take.
|
||||
"""
|
||||
with action_context() as (action_room, action_actor):
|
||||
with action_context() as (action_room, action_character):
|
||||
action_item = find_item_in_room(action_room, item)
|
||||
if not action_item:
|
||||
raise ActionError(f"The {item} item is not in the room.")
|
||||
|
||||
broadcast(f"{action_actor.name} takes the {item} item")
|
||||
broadcast(f"{action_character.name} takes the {item} item")
|
||||
action_room.items.remove(action_item)
|
||||
action_actor.items.append(action_item)
|
||||
action_character.items.append(action_item)
|
||||
return f"You take the {item} item and put it in your inventory."
|
||||
|
||||
|
||||
|
@ -123,39 +123,39 @@ def action_ask(character: str, question: str) -> str:
|
|||
character: The name of the character to ask. You cannot ask yourself questions.
|
||||
question: The question to ask them.
|
||||
"""
|
||||
# capture references to the current actor and room, because they will be overwritten
|
||||
with action_context() as (action_room, action_actor):
|
||||
# capture references to the current character and room, because they will be overwritten
|
||||
with action_context() as (action_room, action_character):
|
||||
# sanity checks
|
||||
question_actor, question_agent = get_actor_agent_for_name(character)
|
||||
if question_actor == action_actor:
|
||||
question_character, question_agent = get_character_agent_for_name(character)
|
||||
if question_character == action_character:
|
||||
raise ActionError(
|
||||
"You cannot ask yourself a question. Stop talking to yourself. Try another action."
|
||||
)
|
||||
|
||||
if not question_actor:
|
||||
if not question_character:
|
||||
raise ActionError(f"The {character} character is not in the room.")
|
||||
|
||||
if not question_agent:
|
||||
raise ActionError(f"The {character} character does not exist.")
|
||||
|
||||
broadcast(f"{action_actor.name} asks {character}: {question}")
|
||||
broadcast(f"{action_character.name} asks {character}: {question}")
|
||||
first_prompt = (
|
||||
"{last_actor.name} asks you: {response}\n"
|
||||
"{last_character.name} asks you: {response}\n"
|
||||
"Reply with your response to them. Reply with 'END' to end the conversation. "
|
||||
"Do not include the question or any JSON. Only include your answer for {last_actor.name}."
|
||||
"Do not include the question or any JSON. Only include your answer for {last_character.name}."
|
||||
)
|
||||
reply_prompt = (
|
||||
"{last_actor.name} continues the conversation with you. They reply: {response}\n"
|
||||
"{last_character.name} continues the conversation with you. They reply: {response}\n"
|
||||
"Reply with your response to them. Reply with 'END' to end the conversation. "
|
||||
"Do not include the question or any JSON. Only include your answer for {last_actor.name}."
|
||||
"Do not include the question or any JSON. Only include your answer for {last_character.name}."
|
||||
)
|
||||
|
||||
action_agent = get_agent_for_actor(action_actor)
|
||||
action_agent = get_agent_for_character(action_character)
|
||||
answer = loop_conversation(
|
||||
action_room,
|
||||
[question_actor, action_actor],
|
||||
[question_character, action_character],
|
||||
[question_agent, action_agent],
|
||||
action_actor,
|
||||
action_character,
|
||||
first_prompt,
|
||||
reply_prompt,
|
||||
question,
|
||||
|
@ -166,7 +166,7 @@ def action_ask(character: str, question: str) -> str:
|
|||
)
|
||||
|
||||
if answer:
|
||||
broadcast(f"{character} responds to {action_actor.name}: {answer}")
|
||||
broadcast(f"{character} responds to {action_character.name}: {answer}")
|
||||
return f"{character} responds: {answer}"
|
||||
|
||||
return f"{character} does not respond."
|
||||
|
@ -180,40 +180,40 @@ def action_tell(character: str, message: str) -> str:
|
|||
character: The name of the character to tell. You cannot talk to yourself.
|
||||
message: The message to tell them.
|
||||
"""
|
||||
# capture references to the current actor and room, because they will be overwritten
|
||||
# capture references to the current character and room, because they will be overwritten
|
||||
|
||||
with action_context() as (action_room, action_actor):
|
||||
with action_context() as (action_room, action_character):
|
||||
# sanity checks
|
||||
question_actor, question_agent = get_actor_agent_for_name(character)
|
||||
if question_actor == action_actor:
|
||||
question_character, question_agent = get_character_agent_for_name(character)
|
||||
if question_character == action_character:
|
||||
raise ActionError(
|
||||
"You cannot tell yourself a message. Stop talking to yourself. Try another action."
|
||||
)
|
||||
|
||||
if not question_actor:
|
||||
if not question_character:
|
||||
raise ActionError(f"The {character} character is not in the room.")
|
||||
|
||||
if not question_agent:
|
||||
raise ActionError(f"The {character} character does not exist.")
|
||||
|
||||
broadcast(f"{action_actor.name} tells {character}: {message}")
|
||||
broadcast(f"{action_character.name} tells {character}: {message}")
|
||||
first_prompt = (
|
||||
"{last_actor.name} starts a conversation with you. They say: {response}\n"
|
||||
"{last_character.name} starts a conversation with you. They say: {response}\n"
|
||||
"Reply with your response to them. "
|
||||
"Do not include the message or any JSON. Only include your reply to {last_actor.name}."
|
||||
"Do not include the message or any JSON. Only include your reply to {last_character.name}."
|
||||
)
|
||||
reply_prompt = (
|
||||
"{last_actor.name} continues the conversation with you. They reply: {response}\n"
|
||||
"{last_character.name} continues the conversation with you. They reply: {response}\n"
|
||||
"Reply with your response to them. "
|
||||
"Do not include the message or any JSON. Only include your reply to {last_actor.name}."
|
||||
"Do not include the message or any JSON. Only include your reply to {last_character.name}."
|
||||
)
|
||||
|
||||
action_agent = get_agent_for_actor(action_actor)
|
||||
action_agent = get_agent_for_character(action_character)
|
||||
answer = loop_conversation(
|
||||
action_room,
|
||||
[question_actor, action_actor],
|
||||
[question_character, action_character],
|
||||
[question_agent, action_agent],
|
||||
action_actor,
|
||||
action_character,
|
||||
first_prompt,
|
||||
reply_prompt,
|
||||
message,
|
||||
|
@ -224,7 +224,7 @@ def action_tell(character: str, message: str) -> str:
|
|||
)
|
||||
|
||||
if answer:
|
||||
broadcast(f"{character} responds to {action_actor.name}: {answer}")
|
||||
broadcast(f"{character} responds to {action_character.name}: {answer}")
|
||||
return f"{character} responds: {answer}"
|
||||
|
||||
return f"{character} does not respond."
|
||||
|
@ -238,23 +238,23 @@ def action_give(character: str, item: str) -> str:
|
|||
character: The name of the character to give the item to.
|
||||
item: The name of the item to give.
|
||||
"""
|
||||
with action_context() as (action_room, action_actor):
|
||||
destination_actor = find_actor_in_room(action_room, character)
|
||||
if not destination_actor:
|
||||
with action_context() as (action_room, action_character):
|
||||
destination_character = find_character_in_room(action_room, character)
|
||||
if not destination_character:
|
||||
raise ActionError(f"The {character} character is not in the room.")
|
||||
|
||||
if destination_actor == action_actor:
|
||||
if destination_character == action_character:
|
||||
raise ActionError(
|
||||
"You cannot give an item to yourself. Try another action."
|
||||
)
|
||||
|
||||
action_item = find_item_in_actor(action_actor, item)
|
||||
action_item = find_item_in_character(action_character, item)
|
||||
if not action_item:
|
||||
raise ActionError(f"You do not have the {item} item in your inventory.")
|
||||
|
||||
broadcast(f"{action_actor.name} gives {character} the {item} item.")
|
||||
action_actor.items.remove(action_item)
|
||||
destination_actor.items.append(action_item)
|
||||
broadcast(f"{action_character.name} gives {character} the {item} item.")
|
||||
action_character.items.remove(action_item)
|
||||
destination_character.items.append(action_item)
|
||||
|
||||
return f"You give the {item} item to {character}."
|
||||
|
||||
|
@ -267,13 +267,13 @@ def action_drop(item: str) -> str:
|
|||
item: The name of the item to drop.
|
||||
"""
|
||||
|
||||
with action_context() as (action_room, action_actor):
|
||||
action_item = find_item_in_actor(action_actor, item)
|
||||
with action_context() as (action_room, action_character):
|
||||
action_item = find_item_in_character(action_character, item)
|
||||
if not action_item:
|
||||
raise ActionError(f"You do not have the {item} item in your inventory.")
|
||||
|
||||
broadcast(f"{action_actor.name} drops the {item} item")
|
||||
action_actor.items.remove(action_item)
|
||||
broadcast(f"{action_character.name} drops the {item} item")
|
||||
action_character.items.remove(action_item)
|
||||
action_room.items.append(action_item)
|
||||
|
||||
return f"You drop the {item} item."
|
||||
|
|
|
@ -6,7 +6,7 @@ from packit.agent import Agent, agent_easy_connect
|
|||
from adventure.context import (
|
||||
action_context,
|
||||
broadcast,
|
||||
get_agent_for_actor,
|
||||
get_agent_for_character,
|
||||
get_dungeon_master,
|
||||
get_game_systems,
|
||||
has_dungeon_master,
|
||||
|
@ -16,9 +16,9 @@ from adventure.context import (
|
|||
from adventure.errors import ActionError
|
||||
from adventure.generate import generate_item, generate_room, link_rooms
|
||||
from adventure.utils.effect import apply_effects
|
||||
from adventure.utils.search import find_actor_in_room
|
||||
from adventure.utils.search import find_character_in_room
|
||||
from adventure.utils.string import normalize_name
|
||||
from adventure.utils.world import describe_actor, describe_entity
|
||||
from adventure.utils.world import describe_character, describe_entity
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
@ -43,7 +43,7 @@ def action_explore(direction: str) -> str:
|
|||
direction: The direction to explore. For example: inside, outside, upstairs, downstairs, trapdoor, portal, etc.
|
||||
"""
|
||||
|
||||
with world_context() as (action_world, action_room, action_actor):
|
||||
with world_context() as (action_world, action_room, action_character):
|
||||
dungeon_master = get_dungeon_master()
|
||||
|
||||
if direction in action_room.portals:
|
||||
|
@ -62,7 +62,7 @@ def action_explore(direction: str) -> str:
|
|||
link_rooms(dungeon_master, action_world, systems, [new_room])
|
||||
|
||||
broadcast(
|
||||
f"{action_actor.name} explores {direction} of {action_room.name} and finds a new room: {new_room.name}"
|
||||
f"{action_character.name} explores {direction} of {action_room.name} and finds a new room: {new_room.name}"
|
||||
)
|
||||
return f"You explore {direction} and find a new room: {new_room.name}"
|
||||
except Exception:
|
||||
|
@ -75,7 +75,7 @@ def action_search(unused: bool) -> str:
|
|||
Search the room for hidden items.
|
||||
"""
|
||||
|
||||
with world_context() as (action_world, action_room, action_actor):
|
||||
with world_context() as (action_world, action_room, action_character):
|
||||
dungeon_master = get_dungeon_master()
|
||||
|
||||
if len(action_room.items) > 2:
|
||||
|
@ -94,7 +94,7 @@ def action_search(unused: bool) -> str:
|
|||
action_room.items.append(new_item)
|
||||
|
||||
broadcast(
|
||||
f"{action_actor.name} searches {action_room.name} and finds a new item: {new_item.name}"
|
||||
f"{action_character.name} searches {action_room.name} and finds a new item: {new_item.name}"
|
||||
)
|
||||
return f"You search the room and find a new item: {new_item.name}"
|
||||
except Exception:
|
||||
|
@ -110,13 +110,13 @@ def action_use(item: str, target: str) -> str:
|
|||
item: The name of the item to use.
|
||||
target: The name of the character to use the item on, or "self" to use the item on yourself.
|
||||
"""
|
||||
with action_context() as (action_room, action_actor):
|
||||
with action_context() as (action_room, action_character):
|
||||
dungeon_master = get_dungeon_master()
|
||||
|
||||
action_item = next(
|
||||
(
|
||||
search_item
|
||||
for search_item in (action_actor.items + action_room.items)
|
||||
for search_item in (action_character.items + action_room.items)
|
||||
if search_item.name == item
|
||||
),
|
||||
None,
|
||||
|
@ -125,17 +125,17 @@ def action_use(item: str, target: str) -> str:
|
|||
raise ActionError(f"The {item} item is not available to use.")
|
||||
|
||||
if target == "self":
|
||||
target_actor = action_actor
|
||||
target = action_actor.name
|
||||
target_character = action_character
|
||||
target = action_character.name
|
||||
else:
|
||||
# TODO: allow targeting the room itself and items in the room
|
||||
target_actor = find_actor_in_room(action_room, target)
|
||||
if not target_actor:
|
||||
target_character = find_character_in_room(action_room, target)
|
||||
if not target_character:
|
||||
return f"The {target} character is not in the room."
|
||||
|
||||
effect_names = [effect.name for effect in action_item.effects]
|
||||
chosen_name = dungeon_master(
|
||||
f"{action_actor.name} uses {item} on {target}. "
|
||||
f"{action_character.name} uses {item} on {target}. "
|
||||
f"{item} has the following effects: {effect_names}. "
|
||||
"Which effect should be applied? Specify the name of the effect to apply."
|
||||
"Do not include the question or any JSON. Only include the name of the effect to apply."
|
||||
|
@ -155,7 +155,7 @@ def action_use(item: str, target: str) -> str:
|
|||
raise ValueError(f"The {chosen_name} effect is not available to apply.")
|
||||
|
||||
try:
|
||||
apply_effects(target_actor, [chosen_effect])
|
||||
apply_effects(target_character, [chosen_effect])
|
||||
except Exception:
|
||||
logger.exception("error applying effect: %s", chosen_effect)
|
||||
raise ValueError(
|
||||
|
@ -163,11 +163,11 @@ def action_use(item: str, target: str) -> str:
|
|||
)
|
||||
|
||||
broadcast(
|
||||
f"{action_actor.name} uses the {chosen_name} effect of {item} on {target}"
|
||||
f"{action_character.name} uses the {chosen_name} effect of {item} on {target}"
|
||||
)
|
||||
outcome = dungeon_master(
|
||||
f"{action_actor.name} uses the {chosen_name} effect of {item} on {target}. "
|
||||
f"{describe_actor(action_actor)}. {describe_actor(target_actor)}. {describe_entity(action_item)}. "
|
||||
f"{action_character.name} uses the {chosen_name} effect of {item} on {target}. "
|
||||
f"{describe_character(action_character)}. {describe_character(target_character)}. {describe_entity(action_item)}. "
|
||||
f"What happens? How does {target} react? Be creative with the results. The outcome can be good, bad, or neutral."
|
||||
"Decide based on the characters involved and the item being used."
|
||||
"Specify the outcome of the action. Do not include the question or any JSON. Only include the outcome of the action."
|
||||
|
@ -175,7 +175,7 @@ def action_use(item: str, target: str) -> str:
|
|||
broadcast(f"The action resulted in: {outcome}")
|
||||
|
||||
# make sure both agents remember the outcome
|
||||
target_agent = get_agent_for_actor(target_actor)
|
||||
target_agent = get_agent_for_character(target_character)
|
||||
if target_agent and target_agent.memory:
|
||||
target_agent.memory.append(outcome)
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
from adventure.context import action_context, get_agent_for_actor, get_current_step
|
||||
from adventure.context import action_context, get_agent_for_character, get_current_step
|
||||
from adventure.errors import ActionError
|
||||
from adventure.models.config import DEFAULT_CONFIG
|
||||
from adventure.models.planning import CalendarEvent
|
||||
from adventure.utils.planning import get_recent_notes
|
||||
|
||||
actor_config = DEFAULT_CONFIG.world.actor
|
||||
character_config = DEFAULT_CONFIG.world.character
|
||||
|
||||
|
||||
def take_note(fact: str):
|
||||
|
@ -16,16 +16,16 @@ def take_note(fact: str):
|
|||
fact: The fact to remember.
|
||||
"""
|
||||
|
||||
with action_context() as (_, action_actor):
|
||||
if fact in action_actor.planner.notes:
|
||||
with action_context() as (_, action_character):
|
||||
if fact in action_character.planner.notes:
|
||||
raise ActionError("You already have a note about that fact.")
|
||||
|
||||
if len(action_actor.planner.notes) >= actor_config.note_limit:
|
||||
if len(action_character.planner.notes) >= character_config.note_limit:
|
||||
raise ActionError(
|
||||
"You have reached the limit of notes you can take. Please erase, replace, or summarize some notes."
|
||||
)
|
||||
|
||||
action_actor.planner.notes.append(fact)
|
||||
action_character.planner.notes.append(fact)
|
||||
|
||||
return "You make a note of that fact."
|
||||
|
||||
|
@ -38,8 +38,8 @@ def read_notes(unused: bool, count: int = 10):
|
|||
count: The number of recent notes to read. 10 is usually a good number.
|
||||
"""
|
||||
|
||||
with action_context() as (_, action_actor):
|
||||
facts = get_recent_notes(action_actor, count=count)
|
||||
with action_context() as (_, action_character):
|
||||
facts = get_recent_notes(action_character, count=count)
|
||||
return "\n".join(facts)
|
||||
|
||||
|
||||
|
@ -51,15 +51,15 @@ def erase_notes(prefix: str) -> str:
|
|||
prefix: The prefix to match notes against.
|
||||
"""
|
||||
|
||||
with action_context() as (_, action_actor):
|
||||
with action_context() as (_, action_character):
|
||||
matches = [
|
||||
note for note in action_actor.planner.notes if note.startswith(prefix)
|
||||
note for note in action_character.planner.notes if note.startswith(prefix)
|
||||
]
|
||||
if not matches:
|
||||
return "No notes found with that prefix."
|
||||
|
||||
action_actor.planner.notes[:] = [
|
||||
note for note in action_actor.planner.notes if note not in matches
|
||||
action_character.planner.notes[:] = [
|
||||
note for note in action_character.planner.notes if note not in matches
|
||||
]
|
||||
return f"Erased {len(matches)} notes."
|
||||
|
||||
|
@ -73,12 +73,12 @@ def replace_note(old: str, new: str) -> str:
|
|||
new: The new note to replace it with.
|
||||
"""
|
||||
|
||||
with action_context() as (_, action_actor):
|
||||
if old not in action_actor.planner.notes:
|
||||
with action_context() as (_, action_character):
|
||||
if old not in action_character.planner.notes:
|
||||
return "Note not found."
|
||||
|
||||
action_actor.planner.notes[:] = [
|
||||
new if note == old else note for note in action_actor.planner.notes
|
||||
action_character.planner.notes[:] = [
|
||||
new if note == old else note for note in action_character.planner.notes
|
||||
]
|
||||
return "Note replaced."
|
||||
|
||||
|
@ -91,12 +91,12 @@ def summarize_notes(limit: int) -> str:
|
|||
limit: The maximum number of notes to keep.
|
||||
"""
|
||||
|
||||
with action_context() as (_, action_actor):
|
||||
notes = action_actor.planner.notes
|
||||
action_agent = get_agent_for_actor(action_actor)
|
||||
with action_context() as (_, action_character):
|
||||
notes = action_character.planner.notes
|
||||
action_agent = get_agent_for_character(action_character)
|
||||
|
||||
if not action_agent:
|
||||
raise ActionError("Agent missing for actor {action_actor.name}")
|
||||
raise ActionError("Agent missing for character {action_character.name}")
|
||||
|
||||
summary = action_agent(
|
||||
"Please summarize your notes. Remove any duplicates and combine similar notes. "
|
||||
|
@ -110,12 +110,12 @@ def summarize_notes(limit: int) -> str:
|
|||
)
|
||||
|
||||
new_notes = [note.strip() for note in summary.split("\n") if note.strip()]
|
||||
if len(new_notes) > actor_config.note_limit:
|
||||
if len(new_notes) > character_config.note_limit:
|
||||
raise ActionError(
|
||||
f"Too many notes. You can only have up to {actor_config.note_limit} notes."
|
||||
f"Too many notes. You can only have up to {character_config.note_limit} notes."
|
||||
)
|
||||
|
||||
action_actor.planner.notes[:] = new_notes
|
||||
action_character.planner.notes[:] = new_notes
|
||||
return "Notes were summarized successfully."
|
||||
|
||||
|
||||
|
@ -130,10 +130,10 @@ def schedule_event(name: str, turns: int):
|
|||
turns: The number of turns until the event happens.
|
||||
"""
|
||||
|
||||
with action_context() as (_, action_actor):
|
||||
with action_context() as (_, action_character):
|
||||
# TODO: check for existing events with the same name
|
||||
event = CalendarEvent(name, turns)
|
||||
action_actor.planner.calendar.events.append(event)
|
||||
action_character.planner.calendar.events.append(event)
|
||||
return f"{name} is scheduled to happen in {turns} turns."
|
||||
|
||||
|
||||
|
@ -144,8 +144,8 @@ def check_calendar(unused: bool, count: int = 10):
|
|||
|
||||
current_turn = get_current_step()
|
||||
|
||||
with action_context() as (_, action_actor):
|
||||
events = action_actor.planner.calendar.events[:count]
|
||||
with action_context() as (_, action_character):
|
||||
events = action_character.planner.calendar.events[:count]
|
||||
return "\n".join(
|
||||
[
|
||||
f"{event.name} will happen in {event.turn - current_turn} turns"
|
||||
|
|
|
@ -3,56 +3,56 @@ from adventure.systems.quest import (
|
|||
QUEST_SYSTEM,
|
||||
complete_quest,
|
||||
get_active_quest,
|
||||
get_quests_for_actor,
|
||||
get_quests_for_character,
|
||||
set_active_quest,
|
||||
)
|
||||
from adventure.utils.search import find_actor_in_room
|
||||
from adventure.utils.search import find_character_in_room
|
||||
|
||||
|
||||
def accept_quest(actor: str, quest: str) -> str:
|
||||
def accept_quest(character: str, quest: str) -> str:
|
||||
"""
|
||||
Accept and start a quest being given by another character.
|
||||
"""
|
||||
|
||||
with action_context() as (action_room, action_actor):
|
||||
with action_context() as (action_room, action_character):
|
||||
quests = get_system_data(QUEST_SYSTEM)
|
||||
if not quests:
|
||||
return "No quests available."
|
||||
|
||||
target_actor = find_actor_in_room(action_room, actor)
|
||||
if not target_actor:
|
||||
return f"{actor} is not in the room."
|
||||
target_character = find_character_in_room(action_room, character)
|
||||
if not target_character:
|
||||
return f"{character} is not in the room."
|
||||
|
||||
available_quests = get_quests_for_actor(quests, target_actor)
|
||||
available_quests = get_quests_for_character(quests, target_character)
|
||||
|
||||
for available_quest in available_quests:
|
||||
if available_quest.name == quest:
|
||||
set_active_quest(quests, action_actor, available_quest)
|
||||
set_active_quest(quests, action_character, available_quest)
|
||||
return f"You have accepted the quest: {quest}"
|
||||
|
||||
return f"{actor} does not have the quest: {quest}"
|
||||
return f"{character} does not have the quest: {quest}"
|
||||
|
||||
|
||||
def submit_quest(actor: str) -> str:
|
||||
def submit_quest(character: str) -> str:
|
||||
"""
|
||||
Submit your active quest to the quest giver. If you have completed the quest, you will be rewarded.
|
||||
"""
|
||||
|
||||
with action_context() as (action_room, action_actor):
|
||||
with action_context() as (action_room, action_character):
|
||||
quests = get_system_data(QUEST_SYSTEM)
|
||||
if not quests:
|
||||
return "No quests available."
|
||||
|
||||
active_quest = get_active_quest(quests, action_actor)
|
||||
active_quest = get_active_quest(quests, action_character)
|
||||
if not active_quest:
|
||||
return "You do not have an active quest."
|
||||
|
||||
target_actor = find_actor_in_room(action_room, actor)
|
||||
if not target_actor:
|
||||
return f"{actor} is not in the room."
|
||||
target_character = find_character_in_room(action_room, character)
|
||||
if not target_character:
|
||||
return f"{character} is not in the room."
|
||||
|
||||
if active_quest.giver.actor == target_actor.name:
|
||||
complete_quest(quests, action_actor, active_quest)
|
||||
if active_quest.giver.character == target_character.name:
|
||||
complete_quest(quests, action_character, active_quest)
|
||||
return f"You have completed the quest: {active_quest.name}"
|
||||
|
||||
return f"{actor} is not the quest giver for your active quest."
|
||||
return f"{character} is not the quest giver for your active quest."
|
||||
|
|
|
@ -9,9 +9,9 @@ from discord import Client, Embed, File, Intents
|
|||
|
||||
from adventure.context import (
|
||||
broadcast,
|
||||
get_actor_agent_for_name,
|
||||
get_character_agent_for_name,
|
||||
get_current_world,
|
||||
set_actor_agent,
|
||||
set_character_agent,
|
||||
subscribe,
|
||||
)
|
||||
from adventure.models.config import DEFAULT_CONFIG, DiscordBotConfig
|
||||
|
@ -101,15 +101,15 @@ class AdventureClient(Client):
|
|||
await channel.send(f"{character_name} has already been taken!")
|
||||
return
|
||||
|
||||
actor, agent = get_actor_agent_for_name(character_name)
|
||||
if not actor:
|
||||
character, agent = get_character_agent_for_name(character_name)
|
||||
if not character:
|
||||
await channel.send(f"Character `{character_name}` not found!")
|
||||
return
|
||||
|
||||
def prompt_player(event: PromptEvent):
|
||||
logger.info(
|
||||
"append prompt for character %s (user %s) to queue: %s",
|
||||
event.actor.name,
|
||||
event.character.name,
|
||||
user_name,
|
||||
event.prompt,
|
||||
)
|
||||
|
@ -118,12 +118,12 @@ class AdventureClient(Client):
|
|||
return True
|
||||
|
||||
player = RemotePlayer(
|
||||
actor.name, actor.backstory, prompt_player, fallback_agent=agent
|
||||
character.name, character.backstory, prompt_player, fallback_agent=agent
|
||||
)
|
||||
set_actor_agent(character_name, actor, player)
|
||||
set_character_agent(character_name, character, player)
|
||||
set_player(user_name, player)
|
||||
|
||||
logger.info(f"{user_name} has joined the game as {actor.name}!")
|
||||
logger.info(f"{user_name} has joined the game as {character.name}!")
|
||||
join_event = PlayerEvent("join", character_name, user_name)
|
||||
return broadcast(join_event)
|
||||
|
||||
|
@ -133,10 +133,12 @@ class AdventureClient(Client):
|
|||
remove_player(user_name)
|
||||
|
||||
# revert to LLM agent
|
||||
actor, _ = get_actor_agent_for_name(player.name)
|
||||
if actor and player.fallback_agent:
|
||||
character, _ = get_character_agent_for_name(player.name)
|
||||
if character and player.fallback_agent:
|
||||
logger.info("restoring LLM agent for %s", player.name)
|
||||
set_actor_agent(actor.name, actor, player.fallback_agent)
|
||||
set_character_agent(
|
||||
character.name, character, player.fallback_agent
|
||||
)
|
||||
|
||||
# broadcast leave event
|
||||
logger.info("disconnecting player %s from %s", user_name, player.name)
|
||||
|
@ -324,7 +326,7 @@ def embed_from_event(event: GameEvent) -> Embed | None:
|
|||
|
||||
|
||||
def embed_from_action(event: ActionEvent | ReplyEvent):
|
||||
action_embed = Embed(title=event.room.name, description=event.actor.name)
|
||||
action_embed = Embed(title=event.room.name, description=event.character.name)
|
||||
|
||||
if isinstance(event, ActionEvent):
|
||||
action_name = event.action.replace("action_", "").title()
|
||||
|
@ -350,7 +352,7 @@ def embed_from_result(event: ResultEvent):
|
|||
if len(text) > 1000:
|
||||
text = text[:1000] + "..."
|
||||
|
||||
result_embed = Embed(title=event.room.name, description=event.actor.name)
|
||||
result_embed = Embed(title=event.room.name, description=event.character.name)
|
||||
result_embed.add_field(name="Result", value=text)
|
||||
return result_embed
|
||||
|
||||
|
@ -369,7 +371,7 @@ def embed_from_player(event: PlayerEvent):
|
|||
|
||||
def embed_from_prompt(event: PromptEvent):
|
||||
# TODO: ping the player
|
||||
prompt_embed = Embed(title=event.room.name, description=event.actor.name)
|
||||
prompt_embed = Embed(title=event.room.name, description=event.character.name)
|
||||
prompt_embed.add_field(name="Prompt", value=event.prompt)
|
||||
return prompt_embed
|
||||
|
||||
|
@ -377,7 +379,7 @@ def embed_from_prompt(event: PromptEvent):
|
|||
def embed_from_status(event: StatusEvent):
|
||||
status_embed = Embed(
|
||||
title=event.room.name if event.room else "",
|
||||
description=event.actor.name if event.actor else "",
|
||||
description=event.character.name if event.character else "",
|
||||
)
|
||||
status_embed.add_field(name="Status", value=event.text)
|
||||
return status_embed
|
||||
|
|
|
@ -18,7 +18,7 @@ from packit.agent import Agent
|
|||
from pyee.base import EventEmitter
|
||||
|
||||
from adventure.game_system import GameSystem
|
||||
from adventure.models.entity import Actor, Room, World
|
||||
from adventure.models.entity import Character, Room, World
|
||||
from adventure.models.event import GameEvent
|
||||
from adventure.utils.string import normalize_name
|
||||
|
||||
|
@ -28,7 +28,7 @@ logger = getLogger(__name__)
|
|||
current_step = 0
|
||||
current_world: World | None = None
|
||||
current_room: Room | None = None
|
||||
current_actor: Actor | None = None
|
||||
current_character: Character | None = None
|
||||
dungeon_master: Agent | None = None
|
||||
|
||||
# game context
|
||||
|
@ -38,7 +38,7 @@ system_data: Dict[str, Any] = {}
|
|||
|
||||
|
||||
# TODO: where should this one go?
|
||||
actor_agents: Dict[str, Tuple[Actor, Agent]] = {}
|
||||
character_agents: Dict[str, Tuple[Character, Agent]] = {}
|
||||
|
||||
STRING_EVENT_TYPE = "message"
|
||||
|
||||
|
@ -88,44 +88,44 @@ def has_dungeon_master():
|
|||
# region context manager
|
||||
@contextmanager
|
||||
def action_context():
|
||||
room, actor = get_action_context()
|
||||
yield room, actor
|
||||
room, character = get_action_context()
|
||||
yield room, character
|
||||
|
||||
|
||||
@contextmanager
|
||||
def world_context():
|
||||
world, room, actor = get_world_context()
|
||||
yield world, room, actor
|
||||
world, room, character = get_world_context()
|
||||
yield world, room, character
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
# region context getters
|
||||
def get_action_context() -> Tuple[Room, Actor]:
|
||||
def get_action_context() -> Tuple[Room, Character]:
|
||||
if not current_room:
|
||||
raise ValueError("The current room must be set before calling action functions")
|
||||
if not current_actor:
|
||||
if not current_character:
|
||||
raise ValueError(
|
||||
"The current actor must be set before calling action functions"
|
||||
"The current character must be set before calling action functions"
|
||||
)
|
||||
|
||||
return (current_room, current_actor)
|
||||
return (current_room, current_character)
|
||||
|
||||
|
||||
def get_world_context() -> Tuple[World, Room, Actor]:
|
||||
def get_world_context() -> Tuple[World, Room, Character]:
|
||||
if not current_world:
|
||||
raise ValueError(
|
||||
"The current world must be set before calling action functions"
|
||||
)
|
||||
if not current_room:
|
||||
raise ValueError("The current room must be set before calling action functions")
|
||||
if not current_actor:
|
||||
if not current_character:
|
||||
raise ValueError(
|
||||
"The current actor must be set before calling action functions"
|
||||
"The current character must be set before calling action functions"
|
||||
)
|
||||
|
||||
return (current_world, current_room, current_actor)
|
||||
return (current_world, current_room, current_character)
|
||||
|
||||
|
||||
def get_current_world() -> World | None:
|
||||
|
@ -136,8 +136,8 @@ def get_current_room() -> Room | None:
|
|||
return current_room
|
||||
|
||||
|
||||
def get_current_actor() -> Actor | None:
|
||||
return current_actor
|
||||
def get_current_character() -> Character | None:
|
||||
return current_character
|
||||
|
||||
|
||||
def get_current_step() -> int:
|
||||
|
@ -175,9 +175,9 @@ def set_current_room(room: Room | None):
|
|||
current_room = room
|
||||
|
||||
|
||||
def set_current_actor(actor: Actor | None):
|
||||
global current_actor
|
||||
current_actor = actor
|
||||
def set_current_character(character: Character | None):
|
||||
global current_character
|
||||
current_character = character
|
||||
|
||||
|
||||
def set_current_step(step: int):
|
||||
|
@ -185,8 +185,8 @@ def set_current_step(step: int):
|
|||
current_step = step
|
||||
|
||||
|
||||
def set_actor_agent(name, actor, agent):
|
||||
actor_agents[name] = (actor, agent)
|
||||
def set_character_agent(name, character, agent):
|
||||
character_agents[name] = (character, agent)
|
||||
|
||||
|
||||
def set_dungeon_master(agent):
|
||||
|
@ -207,41 +207,41 @@ def set_system_data(system: str, data: Any):
|
|||
|
||||
|
||||
# region search functions
|
||||
def get_actor_for_agent(agent):
|
||||
def get_character_for_agent(agent):
|
||||
return next(
|
||||
(
|
||||
inner_actor
|
||||
for inner_actor, inner_agent in actor_agents.values()
|
||||
inner_character
|
||||
for inner_character, inner_agent in character_agents.values()
|
||||
if inner_agent == agent
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
def get_agent_for_actor(actor):
|
||||
def get_agent_for_character(character):
|
||||
return next(
|
||||
(
|
||||
inner_agent
|
||||
for inner_actor, inner_agent in actor_agents.values()
|
||||
if inner_actor == actor
|
||||
for inner_character, inner_agent in character_agents.values()
|
||||
if inner_character == character
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
def get_actor_agent_for_name(name):
|
||||
def get_character_agent_for_name(name):
|
||||
return next(
|
||||
(
|
||||
(actor, agent)
|
||||
for actor, agent in actor_agents.values()
|
||||
if normalize_name(actor.name) == normalize_name(name)
|
||||
(character, agent)
|
||||
for character, agent in character_agents.values()
|
||||
if normalize_name(character.name) == normalize_name(name)
|
||||
),
|
||||
(None, None),
|
||||
)
|
||||
|
||||
|
||||
def get_all_actor_agents():
|
||||
return list(actor_agents.values())
|
||||
def get_all_character_agents():
|
||||
return list(character_agents.values())
|
||||
|
||||
|
||||
# endregion
|
||||
|
|
|
@ -16,15 +16,15 @@ from adventure.models.effect import (
|
|||
IntEffectPattern,
|
||||
StringEffectPattern,
|
||||
)
|
||||
from adventure.models.entity import Actor, Item, Portal, Room, World, WorldEntity
|
||||
from adventure.models.entity import Character, Item, Portal, Room, World, WorldEntity
|
||||
from adventure.models.event import GenerateEvent
|
||||
from adventure.utils import try_parse_float, try_parse_int
|
||||
from adventure.utils.effect import resolve_int_range
|
||||
from adventure.utils.search import (
|
||||
list_actors,
|
||||
list_actors_in_room,
|
||||
list_characters,
|
||||
list_characters_in_room,
|
||||
list_items,
|
||||
list_items_in_actor,
|
||||
list_items_in_character,
|
||||
list_items_in_room,
|
||||
list_rooms,
|
||||
)
|
||||
|
@ -107,7 +107,7 @@ def generate_room(
|
|||
)
|
||||
|
||||
actions = {}
|
||||
room = Room(name=name, description=desc, items=[], actors=[], actions=actions)
|
||||
room = Room(name=name, description=desc, items=[], characters=[], actions=actions)
|
||||
|
||||
item_count = resolve_int_range(world_config.size.room_items) or 0
|
||||
broadcast_generated(f"Generating {item_count} items for room: {name}")
|
||||
|
@ -126,22 +126,24 @@ def generate_room(
|
|||
except Exception:
|
||||
logger.exception("error generating item")
|
||||
|
||||
actor_count = resolve_int_range(world_config.size.room_actors) or 0
|
||||
broadcast_generated(message=f"Generating {actor_count} actors for room: {name}")
|
||||
character_count = resolve_int_range(world_config.size.room_characters) or 0
|
||||
broadcast_generated(
|
||||
message=f"Generating {character_count} characters for room: {name}"
|
||||
)
|
||||
|
||||
for _ in range(actor_count):
|
||||
for _ in range(character_count):
|
||||
try:
|
||||
actor = generate_actor(
|
||||
character = generate_character(
|
||||
agent,
|
||||
world,
|
||||
systems=systems,
|
||||
dest_room=room,
|
||||
)
|
||||
broadcast_generated(entity=actor)
|
||||
broadcast_generated(entity=character)
|
||||
|
||||
room.actors.append(actor)
|
||||
room.characters.append(character)
|
||||
except Exception:
|
||||
logger.exception("error generating actor")
|
||||
logger.exception("error generating character")
|
||||
continue
|
||||
|
||||
return room
|
||||
|
@ -218,18 +220,20 @@ def generate_item(
|
|||
world: World,
|
||||
systems: List[GameSystem],
|
||||
dest_room: Room | None = None,
|
||||
dest_actor: Actor | None = None,
|
||||
dest_character: Character | None = None,
|
||||
) -> Item:
|
||||
existing_items = [
|
||||
item.name
|
||||
for item in list_items(
|
||||
world, include_actor_inventory=True, include_item_inventory=True
|
||||
world, include_character_inventory=True, include_item_inventory=True
|
||||
)
|
||||
]
|
||||
|
||||
if dest_actor:
|
||||
dest_note = f"The item will be held by the {dest_actor.name} character"
|
||||
existing_items += [item.name for item in list_items_in_actor(dest_actor)]
|
||||
if dest_character:
|
||||
dest_note = f"The item will be held by the {dest_character.name} character"
|
||||
existing_items += [
|
||||
item.name for item in list_items_in_character(dest_character)
|
||||
]
|
||||
elif dest_room:
|
||||
dest_note = f"The item will be placed in the {dest_room.name} room"
|
||||
existing_items += [item.name for item in list_items_in_room(dest_room)]
|
||||
|
@ -275,14 +279,14 @@ def generate_item(
|
|||
return item
|
||||
|
||||
|
||||
def generate_actor(
|
||||
def generate_character(
|
||||
agent: Agent,
|
||||
world: World,
|
||||
systems: List[GameSystem],
|
||||
dest_room: Room,
|
||||
) -> Actor:
|
||||
existing_actors = [actor.name for actor in list_actors(world)] + [
|
||||
actor.name for actor in list_actors_in_room(dest_room)
|
||||
) -> Character:
|
||||
existing_characters = [character.name for character in list_characters(world)] + [
|
||||
character.name for character in list_characters_in_room(dest_room)
|
||||
]
|
||||
|
||||
name = loop_retry(
|
||||
|
@ -292,17 +296,17 @@ def generate_actor(
|
|||
"Only respond with the character name in title case, do not include a description or any other text. "
|
||||
'Do not prefix the name with "the", do not wrap it in quotes. '
|
||||
"Do not include the name of the room. Do not give characters any duplicate names."
|
||||
"Do not create any duplicate characters. The existing characters are: {existing_actors}",
|
||||
"Do not create any duplicate characters. The existing characters are: {existing_characters}",
|
||||
context={
|
||||
"dest_room": dest_room.name,
|
||||
"existing_actors": existing_actors,
|
||||
"existing_characters": existing_characters,
|
||||
"world_theme": world.theme,
|
||||
},
|
||||
result_parser=duplicate_name_parser(existing_actors),
|
||||
result_parser=duplicate_name_parser(existing_characters),
|
||||
toolbox=None,
|
||||
)
|
||||
|
||||
broadcast_generated(message=f"Generating actor: {name}")
|
||||
broadcast_generated(message=f"Generating character: {name}")
|
||||
description = agent(
|
||||
"Generate a detailed description of the {name} character. What do they look like? What are they wearing? "
|
||||
"What are they doing? Describe their appearance from the perspective of an outside observer."
|
||||
|
@ -310,19 +314,19 @@ def generate_actor(
|
|||
name=name,
|
||||
)
|
||||
backstory = agent(
|
||||
"Generate a backstory for the {name} actor. Where are they from? What are they doing here? What are their "
|
||||
"Generate a backstory for the {name} character. Where are they from? What are they doing here? What are their "
|
||||
'goals? Make sure to phrase the backstory in the second person, starting with "you are" and speaking directly to {name}.',
|
||||
name=name,
|
||||
)
|
||||
|
||||
actor = Actor(
|
||||
character = Character(
|
||||
name=name, backstory=backstory, description=description, actions={}, items=[]
|
||||
)
|
||||
generate_system_attributes(agent, world, actor, systems)
|
||||
generate_system_attributes(agent, world, character, systems)
|
||||
|
||||
# generate the actor's inventory
|
||||
item_count = resolve_int_range(world_config.size.actor_items) or 0
|
||||
broadcast_generated(f"Generating {item_count} items for actor {name}")
|
||||
# generate the character's inventory
|
||||
item_count = resolve_int_range(world_config.size.character_items) or 0
|
||||
broadcast_generated(f"Generating {item_count} items for character {name}")
|
||||
|
||||
for k in range(item_count):
|
||||
try:
|
||||
|
@ -330,16 +334,16 @@ def generate_actor(
|
|||
agent,
|
||||
world,
|
||||
systems,
|
||||
dest_actor=actor,
|
||||
dest_character=character,
|
||||
)
|
||||
generate_system_attributes(agent, world, item, systems)
|
||||
broadcast_generated(entity=item)
|
||||
|
||||
actor.items.append(item)
|
||||
character.items.append(item)
|
||||
except Exception:
|
||||
logger.exception("error generating item")
|
||||
|
||||
return actor
|
||||
return character
|
||||
|
||||
|
||||
def generate_effect(agent: Agent, world: World, entity: Item) -> EffectPattern:
|
||||
|
@ -534,6 +538,8 @@ def generate_world(
|
|||
# generate portals to link the rooms together
|
||||
link_rooms(agent, world, systems)
|
||||
|
||||
# ensure actors act in a stable order
|
||||
world.order = [actor.name for room in world.rooms for actor in room.actors]
|
||||
# ensure characters act in a stable order
|
||||
world.order = [
|
||||
character.name for room in world.rooms for character in room.characters
|
||||
]
|
||||
return world
|
||||
|
|
|
@ -41,7 +41,7 @@ class ServerConfig:
|
|||
|
||||
|
||||
@dataclass
|
||||
class WorldActorConfig:
|
||||
class WorldCharacterConfig:
|
||||
conversation_limit: int
|
||||
event_limit: int
|
||||
note_limit: int
|
||||
|
@ -49,18 +49,26 @@ class WorldActorConfig:
|
|||
|
||||
@dataclass
|
||||
class WorldSizeConfig:
|
||||
actor_items: int | IntRange
|
||||
character_items: int | IntRange
|
||||
item_effects: int | IntRange
|
||||
portals: int | IntRange
|
||||
room_actors: int | IntRange
|
||||
room_characters: int | IntRange
|
||||
room_items: int | IntRange
|
||||
rooms: int | IntRange
|
||||
|
||||
|
||||
@dataclass
|
||||
class WorldStepConfig:
|
||||
action_retries: int
|
||||
planning_steps: int
|
||||
planning_retries: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class WorldConfig:
|
||||
actor: WorldActorConfig
|
||||
character: WorldCharacterConfig
|
||||
size: WorldSizeConfig
|
||||
step: WorldStepConfig
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -88,18 +96,23 @@ DEFAULT_CONFIG = Config(
|
|||
),
|
||||
server=ServerConfig(websocket=WebsocketServerConfig(host="localhost", port=8001)),
|
||||
world=WorldConfig(
|
||||
actor=WorldActorConfig(
|
||||
character=WorldCharacterConfig(
|
||||
conversation_limit=2,
|
||||
event_limit=5,
|
||||
note_limit=10,
|
||||
),
|
||||
size=WorldSizeConfig(
|
||||
actor_items=IntRange(min=0, max=2),
|
||||
character_items=IntRange(min=0, max=2),
|
||||
item_effects=IntRange(min=1, max=1),
|
||||
portals=IntRange(min=1, max=3),
|
||||
rooms=IntRange(min=3, max=6),
|
||||
room_actors=IntRange(min=1, max=3),
|
||||
room_characters=IntRange(min=1, max=3),
|
||||
room_items=IntRange(min=1, max=3),
|
||||
),
|
||||
step=WorldStepConfig(
|
||||
action_retries=5,
|
||||
planning_steps=3,
|
||||
planning_retries=3,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
@ -23,7 +23,7 @@ class Item(BaseModel):
|
|||
|
||||
|
||||
@dataclass
|
||||
class Actor(BaseModel):
|
||||
class Character(BaseModel):
|
||||
name: str
|
||||
backstory: str
|
||||
description: str
|
||||
|
@ -33,7 +33,7 @@ class Actor(BaseModel):
|
|||
attributes: Attributes = Field(default_factory=dict)
|
||||
items: List[Item] = Field(default_factory=list)
|
||||
id: str = Field(default_factory=uuid)
|
||||
type: Literal["actor"] = "actor"
|
||||
type: Literal["character"] = "character"
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -51,7 +51,7 @@ class Portal(BaseModel):
|
|||
class Room(BaseModel):
|
||||
name: str
|
||||
description: str
|
||||
actors: List[Actor] = Field(default_factory=list)
|
||||
characters: List[Character] = Field(default_factory=list)
|
||||
actions: Actions = Field(default_factory=dict)
|
||||
active_effects: List[EffectResult] = Field(default_factory=list)
|
||||
attributes: Attributes = Field(default_factory=dict)
|
||||
|
@ -80,12 +80,12 @@ class WorldState(BaseModel):
|
|||
type: Literal["world_state"] = "world_state"
|
||||
|
||||
|
||||
WorldEntity = Room | Actor | Item | Portal
|
||||
WorldEntity = Room | Character | Item | Portal
|
||||
|
||||
|
||||
@dataclass
|
||||
class EntityReference:
|
||||
actor: str | None = None
|
||||
character: str | None = None
|
||||
item: str | None = None
|
||||
portal: str | None = None
|
||||
room: str | None = None
|
||||
|
|
|
@ -4,7 +4,7 @@ from typing import Any, Callable, Dict, List, Literal, Union
|
|||
from pydantic import Field
|
||||
|
||||
from .base import BaseModel, dataclass, uuid
|
||||
from .entity import Actor, Item, Room, WorldEntity
|
||||
from .entity import Character, Item, Room, WorldEntity
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -30,26 +30,26 @@ class GenerateEvent(BaseModel):
|
|||
@dataclass
|
||||
class ActionEvent(BaseModel):
|
||||
"""
|
||||
An actor has taken an action.
|
||||
A character has taken an action.
|
||||
"""
|
||||
|
||||
action: str
|
||||
parameters: Dict[str, bool | float | int | str]
|
||||
|
||||
room: Room
|
||||
actor: Actor
|
||||
character: Character
|
||||
item: Item | None = None
|
||||
id: str = Field(default_factory=uuid)
|
||||
type: Literal["action"] = "action"
|
||||
|
||||
@staticmethod
|
||||
def from_json(json: str, room: Room, actor: Actor) -> "ActionEvent":
|
||||
def from_json(json: str, room: Room, character: Character) -> "ActionEvent":
|
||||
openai_json = loads(json)
|
||||
return ActionEvent(
|
||||
action=openai_json["function"],
|
||||
parameters=openai_json["parameters"],
|
||||
room=room,
|
||||
actor=actor,
|
||||
character=character,
|
||||
item=None,
|
||||
)
|
||||
|
||||
|
@ -57,12 +57,12 @@ class ActionEvent(BaseModel):
|
|||
@dataclass
|
||||
class PromptEvent(BaseModel):
|
||||
"""
|
||||
A prompt for an actor to take an action.
|
||||
A prompt for a character to take an action.
|
||||
"""
|
||||
|
||||
prompt: str
|
||||
room: Room
|
||||
actor: Actor
|
||||
character: Character
|
||||
id: str = Field(default_factory=uuid)
|
||||
type: Literal["prompt"] = "prompt"
|
||||
|
||||
|
@ -70,22 +70,22 @@ class PromptEvent(BaseModel):
|
|||
@dataclass
|
||||
class ReplyEvent(BaseModel):
|
||||
"""
|
||||
An actor has replied with text.
|
||||
A character has replied with text.
|
||||
|
||||
This is the non-JSON version of an ActionEvent.
|
||||
|
||||
TODO: add the actor being replied to.
|
||||
TODO: add the character being replied to.
|
||||
"""
|
||||
|
||||
text: str
|
||||
room: Room
|
||||
actor: Actor
|
||||
character: Character
|
||||
id: str = Field(default_factory=uuid)
|
||||
type: Literal["reply"] = "reply"
|
||||
|
||||
@staticmethod
|
||||
def from_text(text: str, room: Room, actor: Actor) -> "ReplyEvent":
|
||||
return ReplyEvent(text=text, room=room, actor=actor)
|
||||
def from_text(text: str, room: Room, character: Character) -> "ReplyEvent":
|
||||
return ReplyEvent(text=text, room=room, character=character)
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -96,7 +96,7 @@ class ResultEvent(BaseModel):
|
|||
|
||||
result: str
|
||||
room: Room
|
||||
actor: Actor
|
||||
character: Character
|
||||
id: str = Field(default_factory=uuid)
|
||||
type: Literal["result"] = "result"
|
||||
|
||||
|
@ -109,7 +109,7 @@ class StatusEvent(BaseModel):
|
|||
|
||||
text: str
|
||||
room: Room | None = None
|
||||
actor: Actor | None = None
|
||||
character: Character | None = None
|
||||
id: str = Field(default_factory=uuid)
|
||||
type: Literal["status"] = "status"
|
||||
|
||||
|
@ -120,7 +120,7 @@ class SnapshotEvent(BaseModel):
|
|||
A snapshot of the world state.
|
||||
|
||||
This one is slightly unusual, because the world has already been dumped to a JSON-compatible dictionary.
|
||||
That is especially important for the memory, which is a dictionary of actor names to lists of messages.
|
||||
That is especially important for the memory, which is a dictionary of character names to lists of messages.
|
||||
"""
|
||||
|
||||
world: Dict[str, Any]
|
||||
|
|
|
@ -178,9 +178,9 @@ class RemotePlayer(BasePlayer):
|
|||
formatted_prompt = prompt.format(**kwargs)
|
||||
self.memory.append(HumanMessage(content=formatted_prompt))
|
||||
|
||||
with action_context() as (current_room, current_actor):
|
||||
with action_context() as (current_room, current_character):
|
||||
prompt_event = PromptEvent(
|
||||
prompt=formatted_prompt, room=current_room, actor=current_actor
|
||||
prompt=formatted_prompt, room=current_room, character=current_character
|
||||
)
|
||||
|
||||
try:
|
||||
|
|
|
@ -226,14 +226,16 @@ def fast_hash(text: str) -> str:
|
|||
|
||||
def get_image_prefix(event: GameEvent | WorldEntity) -> str:
|
||||
if isinstance(event, ActionEvent):
|
||||
return sanitize_name(f"event-action-{event.actor.name}-{event.action}")
|
||||
return sanitize_name(f"event-action-{event.character.name}-{event.action}")
|
||||
|
||||
if isinstance(event, ReplyEvent):
|
||||
return sanitize_name(f"event-reply-{event.actor.name}-{fast_hash(event.text)}")
|
||||
return sanitize_name(
|
||||
f"event-reply-{event.character.name}-{fast_hash(event.text)}"
|
||||
)
|
||||
|
||||
if isinstance(event, ResultEvent):
|
||||
return sanitize_name(
|
||||
f"event-result-{event.actor.name}-{fast_hash(event.result)}"
|
||||
f"event-result-{event.character.name}-{fast_hash(event.result)}"
|
||||
)
|
||||
|
||||
if isinstance(event, StatusEvent):
|
||||
|
|
|
@ -12,7 +12,7 @@ from adventure.models.event import (
|
|||
ResultEvent,
|
||||
StatusEvent,
|
||||
)
|
||||
from adventure.utils.search import find_actor_in_room, find_item_in_room, find_room
|
||||
from adventure.utils.search import find_character_in_room, find_item_in_room, find_room
|
||||
from adventure.utils.world import describe_entity
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
@ -28,11 +28,11 @@ def prompt_from_parameters(
|
|||
# look up the character
|
||||
character_name = str(parameters["character"])
|
||||
logger.debug("searching for parameter character: %s", character_name)
|
||||
target_actor = find_actor_in_room(action_room, character_name)
|
||||
if target_actor:
|
||||
logger.debug("adding actor to prompt: %s", target_actor.name)
|
||||
pre.append(f"with {target_actor.name}")
|
||||
post.append(describe_entity(target_actor))
|
||||
target_character = find_character_in_room(action_room, character_name)
|
||||
if target_character:
|
||||
logger.debug("adding character to prompt: %s", target_character.name)
|
||||
pre.append(f"with {target_character.name}")
|
||||
post.append(describe_entity(target_character))
|
||||
|
||||
if "item" in parameters:
|
||||
# look up the item
|
||||
|
@ -41,7 +41,7 @@ def prompt_from_parameters(
|
|||
target_item = find_item_in_room(
|
||||
action_room,
|
||||
item_name,
|
||||
include_actor_inventory=True,
|
||||
include_character_inventory=True,
|
||||
include_item_inventory=True,
|
||||
)
|
||||
if target_item:
|
||||
|
@ -50,7 +50,7 @@ def prompt_from_parameters(
|
|||
post.append(describe_entity(target_item))
|
||||
|
||||
if "target" in parameters:
|
||||
# could be a room, actor, or item
|
||||
# could be a room, character, or item
|
||||
target_name = str(parameters["target"])
|
||||
logger.debug("searching for parameter target: %s", target_name)
|
||||
|
||||
|
@ -62,16 +62,16 @@ def prompt_from_parameters(
|
|||
pre.append(f"in the {target_room.name}")
|
||||
post.append(describe_entity(target_room))
|
||||
|
||||
target_actor = find_actor_in_room(action_room, target_name)
|
||||
if target_actor:
|
||||
logger.debug("adding actor to prompt: %s", target_actor.name)
|
||||
pre.append(f"with {target_actor.name}")
|
||||
post.append(describe_entity(target_actor))
|
||||
target_character = find_character_in_room(action_room, target_name)
|
||||
if target_character:
|
||||
logger.debug("adding character to prompt: %s", target_character.name)
|
||||
pre.append(f"with {target_character.name}")
|
||||
post.append(describe_entity(target_character))
|
||||
|
||||
target_item = find_item_in_room(
|
||||
action_room,
|
||||
target_name,
|
||||
include_actor_inventory=True,
|
||||
include_character_inventory=True,
|
||||
include_item_inventory=True,
|
||||
)
|
||||
if target_item:
|
||||
|
@ -92,20 +92,20 @@ def scene_from_event(event: GameEvent) -> str | None:
|
|||
)
|
||||
|
||||
return (
|
||||
f"{event.actor.name} uses the {action_name} action {parameter_pre}. "
|
||||
"{describe_entity(event.actor)}. {describe_entity(event.room)}. {parameter_post}."
|
||||
f"{event.character.name} uses the {action_name} action {parameter_pre}. "
|
||||
"{describe_entity(event.character)}. {describe_entity(event.room)}. {parameter_post}."
|
||||
)
|
||||
|
||||
if isinstance(event, ReplyEvent):
|
||||
return f"{event.actor.name} replies: {event.text}. {describe_entity(event.actor)}. {describe_entity(event.room)}."
|
||||
return f"{event.character.name} replies: {event.text}. {describe_entity(event.character)}. {describe_entity(event.room)}."
|
||||
|
||||
if isinstance(event, ResultEvent):
|
||||
return f"{event.result}. {describe_entity(event.actor)}. {describe_entity(event.room)}."
|
||||
return f"{event.result}. {describe_entity(event.character)}. {describe_entity(event.room)}."
|
||||
|
||||
if isinstance(event, StatusEvent):
|
||||
if event.room:
|
||||
if event.actor:
|
||||
return f"{event.text}. {describe_entity(event.actor)}. {describe_entity(event.room)}."
|
||||
if event.character:
|
||||
return f"{event.text}. {describe_entity(event.character)}. {describe_entity(event.room)}."
|
||||
|
||||
return f"{event.text}. {describe_entity(event.room)}."
|
||||
|
||||
|
|
|
@ -14,13 +14,13 @@ from pydantic import RootModel
|
|||
|
||||
from adventure.context import (
|
||||
broadcast,
|
||||
get_actor_agent_for_name,
|
||||
get_character_agent_for_name,
|
||||
get_current_world,
|
||||
set_actor_agent,
|
||||
set_character_agent,
|
||||
subscribe,
|
||||
)
|
||||
from adventure.models.config import DEFAULT_CONFIG, WebsocketServerConfig
|
||||
from adventure.models.entity import Actor, Item, Room, World
|
||||
from adventure.models.entity import Character, Item, Room, World
|
||||
from adventure.models.event import (
|
||||
GameEvent,
|
||||
PlayerEvent,
|
||||
|
@ -38,7 +38,7 @@ from adventure.player import (
|
|||
)
|
||||
from adventure.render.comfy import render_entity, render_event
|
||||
from adventure.state import snapshot_world, world_json
|
||||
from adventure.utils.search import find_actor, find_item, find_portal, find_room
|
||||
from adventure.utils.search import find_character, find_item, find_portal, find_room
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
@ -76,8 +76,8 @@ async def handler(websocket):
|
|||
def sync_turn(event: PromptEvent) -> bool:
|
||||
# TODO: nothing about this is good
|
||||
player = get_player(id)
|
||||
if player and player.name == event.actor.name:
|
||||
asyncio.run(next_turn(event.actor.name, event.prompt))
|
||||
if player and player.name == event.character.name:
|
||||
asyncio.run(next_turn(event.character.name, event.prompt))
|
||||
return True
|
||||
|
||||
return False
|
||||
|
@ -137,9 +137,11 @@ async def handler(websocket):
|
|||
# TODO: should this always remove?
|
||||
remove_player(id)
|
||||
|
||||
actor, llm_agent = get_actor_agent_for_name(character_name)
|
||||
if not actor:
|
||||
logger.error(f"Failed to find actor {character_name}")
|
||||
character, llm_agent = get_character_agent_for_name(
|
||||
character_name
|
||||
)
|
||||
if not character:
|
||||
logger.error(f"Failed to find character {character_name}")
|
||||
continue
|
||||
|
||||
# prevent any recursive fallback bugs
|
||||
|
@ -150,8 +152,8 @@ async def handler(websocket):
|
|||
llm_agent = llm_agent.fallback_agent
|
||||
|
||||
player = RemotePlayer(
|
||||
actor.name,
|
||||
actor.backstory,
|
||||
character.name,
|
||||
character.backstory,
|
||||
sync_turn,
|
||||
fallback_agent=llm_agent,
|
||||
)
|
||||
|
@ -161,7 +163,7 @@ async def handler(websocket):
|
|||
)
|
||||
|
||||
# swap out the LLM agent
|
||||
set_actor_agent(actor.name, actor, player)
|
||||
set_character_agent(character.name, character, player)
|
||||
|
||||
# notify all clients that this character is now active
|
||||
broadcast_player_event(character_name, player_name, "join")
|
||||
|
@ -195,10 +197,10 @@ async def handler(websocket):
|
|||
broadcast_player_event(player.name, player_name, "leave")
|
||||
broadcast_player_list()
|
||||
|
||||
actor, _ = get_actor_agent_for_name(player.name)
|
||||
if actor and player.fallback_agent:
|
||||
character, _ = get_character_agent_for_name(player.name)
|
||||
if character and player.fallback_agent:
|
||||
logger.info("restoring LLM agent for %s", player.name)
|
||||
set_actor_agent(player.name, actor, player.fallback_agent)
|
||||
set_character_agent(player.name, character, player.fallback_agent)
|
||||
|
||||
logger.info("client disconnected: %s", id)
|
||||
|
||||
|
@ -220,17 +222,20 @@ def render_input(data):
|
|||
render_event(event)
|
||||
else:
|
||||
logger.error(f"failed to find event {event_id}")
|
||||
elif "actor" in data:
|
||||
actor_name = data["actor"]
|
||||
actor = find_actor(world, actor_name)
|
||||
if actor:
|
||||
render_entity(actor)
|
||||
elif "character" in data:
|
||||
character_name = data["character"]
|
||||
character = find_character(world, character_name)
|
||||
if character:
|
||||
render_entity(character)
|
||||
else:
|
||||
logger.error(f"failed to find actor {actor_name}")
|
||||
logger.error(f"failed to find character {character_name}")
|
||||
elif "item" in data:
|
||||
item_name = data["item"]
|
||||
item = find_item(
|
||||
world, item_name, include_actor_inventory=True, include_item_inventory=True
|
||||
world,
|
||||
item_name,
|
||||
include_character_inventory=True,
|
||||
include_item_inventory=True,
|
||||
)
|
||||
if item:
|
||||
render_entity(item)
|
||||
|
@ -258,7 +263,7 @@ socket_thread = None
|
|||
|
||||
|
||||
def server_json(obj):
|
||||
if isinstance(obj, (Actor, Item, Room)):
|
||||
if isinstance(obj, (Character, Item, Room)):
|
||||
return obj.name
|
||||
|
||||
return world_json(obj)
|
||||
|
|
|
@ -7,7 +7,7 @@ from typing import Callable, Sequence
|
|||
|
||||
from packit.agent import Agent
|
||||
from packit.conditions import condition_or, condition_threshold
|
||||
from packit.loops import loop_reduce, loop_retry
|
||||
from packit.loops import loop_retry
|
||||
from packit.results import multi_function_or_str_result
|
||||
from packit.toolbox import Toolbox
|
||||
from packit.utils import could_be_json
|
||||
|
@ -32,28 +32,32 @@ from adventure.actions.planning import (
|
|||
)
|
||||
from adventure.context import (
|
||||
broadcast,
|
||||
get_actor_agent_for_name,
|
||||
get_actor_for_agent,
|
||||
get_character_agent_for_name,
|
||||
get_character_for_agent,
|
||||
get_current_step,
|
||||
get_current_world,
|
||||
set_current_actor,
|
||||
set_current_character,
|
||||
set_current_room,
|
||||
set_current_step,
|
||||
set_current_world,
|
||||
set_game_systems,
|
||||
)
|
||||
from adventure.game_system import GameSystem
|
||||
from adventure.models.entity import Actor, Room, World
|
||||
from adventure.models.config import DEFAULT_CONFIG
|
||||
from adventure.models.entity import Character, Room, World
|
||||
from adventure.models.event import ActionEvent, ReplyEvent, ResultEvent
|
||||
from adventure.utils.conversation import make_keyword_condition, summarize_room
|
||||
from adventure.utils.effect import expire_effects
|
||||
from adventure.utils.planning import expire_events, get_upcoming_events
|
||||
from adventure.utils.search import find_room_with_actor
|
||||
from adventure.utils.search import find_room_with_character
|
||||
from adventure.utils.world import describe_entity, format_attributes
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
step_config = DEFAULT_CONFIG.world.step
|
||||
|
||||
|
||||
def world_result_parser(value, agent, **kwargs):
|
||||
current_world = get_current_world()
|
||||
if not current_world:
|
||||
|
@ -63,35 +67,36 @@ def world_result_parser(value, agent, **kwargs):
|
|||
|
||||
logger.debug(f"parsing action for {agent.name}: {value}")
|
||||
|
||||
current_actor = get_actor_for_agent(agent)
|
||||
current_character = get_character_for_agent(agent)
|
||||
current_room = next(
|
||||
(room for room in current_world.rooms if current_actor in room.actors), None
|
||||
(room for room in current_world.rooms if current_character in room.characters),
|
||||
None,
|
||||
)
|
||||
|
||||
set_current_room(current_room)
|
||||
set_current_actor(current_actor)
|
||||
set_current_character(current_character)
|
||||
|
||||
return multi_function_or_str_result(value, agent=agent, **kwargs)
|
||||
|
||||
|
||||
def prompt_actor_action(
|
||||
room, actor, agent, action_names, action_toolbox, current_turn
|
||||
def prompt_character_action(
|
||||
room, character, agent, action_names, action_toolbox, current_turn
|
||||
) -> str:
|
||||
# collect data for the prompt
|
||||
notes_prompt, events_prompt = get_notes_events(actor, current_turn)
|
||||
notes_prompt, events_prompt = get_notes_events(character, current_turn)
|
||||
|
||||
room_actors = [actor.name for actor in room.actors]
|
||||
room_characters = [character.name for character in room.characters]
|
||||
room_items = [item.name for item in room.items]
|
||||
room_directions = [portal.name for portal in room.portals]
|
||||
|
||||
actor_attributes = format_attributes(actor)
|
||||
# actor_effects = [effect.name for effect in actor.active_effects]
|
||||
actor_items = [item.name for item in actor.items]
|
||||
character_attributes = format_attributes(character)
|
||||
# character_effects = [effect.name for effect in character.active_effects]
|
||||
character_items = [item.name for item in character.items]
|
||||
|
||||
# set up a result parser for the agent
|
||||
def result_parser(value, agent, **kwargs):
|
||||
if not room or not actor:
|
||||
raise ValueError("Room and actor must be set before parsing results")
|
||||
if not room or not character:
|
||||
raise ValueError("Room and character must be set before parsing results")
|
||||
|
||||
# trim suffixes that are used elsewhere
|
||||
value = value.removesuffix("END").strip()
|
||||
|
@ -110,23 +115,23 @@ def prompt_actor_action(
|
|||
pass
|
||||
|
||||
if could_be_json(value):
|
||||
event = ActionEvent.from_json(value, room, actor)
|
||||
event = ActionEvent.from_json(value, room, character)
|
||||
else:
|
||||
event = ReplyEvent.from_text(value, room, actor)
|
||||
event = ReplyEvent.from_text(value, room, character)
|
||||
|
||||
broadcast(event)
|
||||
|
||||
return world_result_parser(value, agent, **kwargs)
|
||||
|
||||
# prompt and act
|
||||
logger.info("starting turn for actor: %s", actor.name)
|
||||
logger.info("starting turn for character: %s", character.name)
|
||||
result = loop_retry(
|
||||
agent,
|
||||
(
|
||||
"You are currently in the {room_name} room. {room_description}. {attributes}. "
|
||||
"The room contains the following characters: {visible_actors}. "
|
||||
"The room contains the following characters: {visible_characters}. "
|
||||
"The room contains the following items: {visible_items}. "
|
||||
"Your inventory contains the following items: {actor_items}."
|
||||
"Your inventory contains the following items: {character_items}."
|
||||
"You can take the following actions: {actions}. "
|
||||
"You can move in the following directions: {directions}. "
|
||||
"{notes_prompt} {events_prompt}"
|
||||
|
@ -135,12 +140,12 @@ def prompt_actor_action(
|
|||
),
|
||||
context={
|
||||
"actions": action_names,
|
||||
"actor_items": actor_items,
|
||||
"attributes": actor_attributes,
|
||||
"character_items": character_items,
|
||||
"attributes": character_attributes,
|
||||
"directions": room_directions,
|
||||
"room_name": room.name,
|
||||
"room_description": describe_entity(room),
|
||||
"visible_actors": room_actors,
|
||||
"visible_characters": room_characters,
|
||||
"visible_items": room_items,
|
||||
"notes_prompt": notes_prompt,
|
||||
"events_prompt": events_prompt,
|
||||
|
@ -149,7 +154,7 @@ def prompt_actor_action(
|
|||
toolbox=action_toolbox,
|
||||
)
|
||||
|
||||
logger.debug(f"{actor.name} step result: {result}")
|
||||
logger.debug(f"{character.name} step result: {result}")
|
||||
if agent.memory:
|
||||
# TODO: make sure this is not duplicating memories and wasting space
|
||||
agent.memory.append(result)
|
||||
|
@ -157,9 +162,9 @@ def prompt_actor_action(
|
|||
return result
|
||||
|
||||
|
||||
def get_notes_events(actor: Actor, current_turn: int):
|
||||
recent_notes = get_recent_notes(actor)
|
||||
upcoming_events = get_upcoming_events(actor, current_turn)
|
||||
def get_notes_events(character: Character, current_turn: int):
|
||||
recent_notes = get_recent_notes(character)
|
||||
upcoming_events = get_upcoming_events(character, current_turn)
|
||||
|
||||
if len(recent_notes) > 0:
|
||||
notes = "\n".join(recent_notes)
|
||||
|
@ -181,19 +186,30 @@ def get_notes_events(actor: Actor, current_turn: int):
|
|||
return notes_prompt, events_prompt
|
||||
|
||||
|
||||
def prompt_actor_think(
|
||||
room: Room, actor: Actor, agent: Agent, planner_toolbox: Toolbox, current_turn: int
|
||||
def prompt_character_think(
|
||||
room: Room,
|
||||
character: Character,
|
||||
agent: Agent,
|
||||
planner_toolbox: Toolbox,
|
||||
current_turn: int,
|
||||
max_steps: int | None = None,
|
||||
) -> str:
|
||||
notes_prompt, events_prompt = get_notes_events(actor, current_turn)
|
||||
max_steps = max_steps or step_config.planning_steps
|
||||
|
||||
event_count = len(actor.planner.calendar.events)
|
||||
note_count = len(actor.planner.notes)
|
||||
notes_prompt, events_prompt = get_notes_events(character, current_turn)
|
||||
|
||||
logger.info("starting planning for actor: %s", actor.name)
|
||||
event_count = len(character.planner.calendar.events)
|
||||
note_count = len(character.planner.notes)
|
||||
|
||||
logger.info("starting planning for character: %s", character.name)
|
||||
_, condition_end, result_parser = make_keyword_condition("You are done planning.")
|
||||
stop_condition = condition_or(condition_end, partial(condition_threshold, max=3))
|
||||
stop_condition = condition_or(
|
||||
condition_end, partial(condition_threshold, max=max_steps)
|
||||
)
|
||||
|
||||
result = loop_reduce(
|
||||
i = 0
|
||||
while not stop_condition(current=i):
|
||||
result = loop_retry(
|
||||
agent,
|
||||
"You are about to start your turn. Plan your next action carefully. Take notes and schedule events to help keep track of your goals. "
|
||||
"You can check your notes for important facts or check your calendar for upcoming events. You have {note_count} notes. "
|
||||
|
@ -209,7 +225,7 @@ def prompt_actor_think(
|
|||
"events_prompt": events_prompt,
|
||||
"note_count": note_count,
|
||||
"notes_prompt": notes_prompt,
|
||||
"room_summary": summarize_room(room, actor),
|
||||
"room_summary": summarize_room(room, character),
|
||||
},
|
||||
result_parser=result_parser,
|
||||
stop_condition=stop_condition,
|
||||
|
@ -219,6 +235,8 @@ def prompt_actor_think(
|
|||
if agent.memory:
|
||||
agent.memory.append(result)
|
||||
|
||||
i += 1
|
||||
|
||||
return result
|
||||
|
||||
|
||||
|
@ -259,44 +277,46 @@ def simulate_world(
|
|||
]
|
||||
)
|
||||
|
||||
# simulate each actor
|
||||
# simulate each character
|
||||
for i in count():
|
||||
current_step = get_current_step()
|
||||
logger.info(f"simulating step {i} of {steps} (world step {current_step})")
|
||||
|
||||
for actor_name in world.order:
|
||||
actor, agent = get_actor_agent_for_name(actor_name)
|
||||
if not agent or not actor:
|
||||
logger.error(f"agent or actor not found for name {actor_name}")
|
||||
for character_name in world.order:
|
||||
character, agent = get_character_agent_for_name(character_name)
|
||||
if not agent or not character:
|
||||
logger.error(f"agent or character not found for name {character_name}")
|
||||
continue
|
||||
|
||||
room = find_room_with_actor(world, actor)
|
||||
room = find_room_with_character(world, character)
|
||||
if not room:
|
||||
logger.error(f"actor {actor_name} is not in a room")
|
||||
logger.error(f"character {character_name} is not in a room")
|
||||
continue
|
||||
|
||||
# prep context
|
||||
set_current_room(room)
|
||||
set_current_actor(actor)
|
||||
set_current_character(character)
|
||||
|
||||
# decrement effects on the actor and remove any that have expired
|
||||
expire_effects(actor)
|
||||
expire_events(actor, current_step)
|
||||
# decrement effects on the character and remove any that have expired
|
||||
expire_effects(character)
|
||||
expire_events(character, current_step)
|
||||
|
||||
# give the actor a chance to think and check their planner
|
||||
# give the character a chance to think and check their planner
|
||||
if agent.memory and len(agent.memory) > 0:
|
||||
try:
|
||||
thoughts = prompt_actor_think(
|
||||
room, actor, agent, planner_toolbox, current_step
|
||||
thoughts = prompt_character_think(
|
||||
room, character, agent, planner_toolbox, current_step
|
||||
)
|
||||
logger.debug(f"{actor.name} thinks: {thoughts}")
|
||||
logger.debug(f"{character.name} thinks: {thoughts}")
|
||||
except Exception:
|
||||
logger.exception(f"error during planning for actor {actor.name}")
|
||||
|
||||
result = prompt_actor_action(
|
||||
room, actor, agent, action_names, action_tools, current_step
|
||||
logger.exception(
|
||||
f"error during planning for character {character.name}"
|
||||
)
|
||||
result_event = ResultEvent(result=result, room=room, actor=actor)
|
||||
|
||||
result = prompt_character_action(
|
||||
room, character, agent, action_names, action_tools, current_step
|
||||
)
|
||||
result_event = ResultEvent(result=result, room=room, character=character)
|
||||
broadcast(result_event)
|
||||
|
||||
for system in systems:
|
||||
|
|
|
@ -7,7 +7,7 @@ from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, System
|
|||
from packit.agent import Agent, agent_easy_connect
|
||||
from pydantic import RootModel
|
||||
|
||||
from adventure.context import get_all_actor_agents, set_actor_agent
|
||||
from adventure.context import get_all_character_agents, set_character_agent
|
||||
from adventure.models.entity import World
|
||||
from adventure.player import LocalPlayer
|
||||
|
||||
|
@ -17,19 +17,19 @@ def create_agents(
|
|||
memory: Dict[str, List[str | Dict[str, str]]] = {},
|
||||
players: List[str] = [],
|
||||
):
|
||||
# set up agents for each actor
|
||||
# set up agents for each character
|
||||
llm = agent_easy_connect()
|
||||
|
||||
for room in world.rooms:
|
||||
for actor in room.actors:
|
||||
if actor.name in players:
|
||||
agent = LocalPlayer(actor.name, actor.backstory)
|
||||
agent_memory = restore_memory(memory.get(actor.name, []))
|
||||
for character in room.characters:
|
||||
if character.name in players:
|
||||
agent = LocalPlayer(character.name, character.backstory)
|
||||
agent_memory = restore_memory(memory.get(character.name, []))
|
||||
agent.load_history(agent_memory)
|
||||
else:
|
||||
agent = Agent(actor.name, actor.backstory, {}, llm)
|
||||
agent.memory = restore_memory(memory.get(actor.name, []))
|
||||
set_actor_agent(actor.name, actor, agent)
|
||||
agent = Agent(character.name, character.backstory, {}, llm)
|
||||
agent.memory = restore_memory(memory.get(character.name, []))
|
||||
set_character_agent(character.name, character, agent)
|
||||
|
||||
|
||||
def graph_world(world: World, step: int):
|
||||
|
@ -38,8 +38,8 @@ def graph_world(world: World, step: int):
|
|||
graph_name = f"{path.basename(world.name)}-{step}"
|
||||
graph = graphviz.Digraph(graph_name, format="png")
|
||||
for room in world.rooms:
|
||||
actors = [actor.name for actor in room.actors]
|
||||
room_label = "\n".join([room.name, *actors])
|
||||
characters = [character.name for character in room.characters]
|
||||
room_label = "\n".join([room.name, *characters])
|
||||
graph.node(room.name, room_label)
|
||||
for portal in room.portals:
|
||||
graph.edge(room.name, portal.destination, label=portal.name)
|
||||
|
@ -54,8 +54,8 @@ def snapshot_world(world: World, step: int):
|
|||
|
||||
json_memory = {}
|
||||
|
||||
for actor, agent in get_all_actor_agents():
|
||||
json_memory[actor.name] = list(agent.memory or [])
|
||||
for character, agent in get_all_character_agents():
|
||||
json_memory[character.name] = list(agent.memory or [])
|
||||
|
||||
return {
|
||||
"world": json_world,
|
||||
|
|
|
@ -138,9 +138,9 @@ def update_logic(
|
|||
) -> None:
|
||||
for room in world.rooms:
|
||||
update_attributes(room, rules=rules, triggers=triggers)
|
||||
for actor in room.actors:
|
||||
update_attributes(actor, rules=rules, triggers=triggers)
|
||||
for item in actor.items:
|
||||
for character in room.characters:
|
||||
update_attributes(character, rules=rules, triggers=triggers)
|
||||
for item in character.items:
|
||||
update_attributes(item, rules=rules, triggers=triggers)
|
||||
for item in room.items:
|
||||
update_attributes(item, rules=rules, triggers=triggers)
|
||||
|
|
|
@ -8,7 +8,7 @@ from adventure.context import get_system_data
|
|||
from adventure.game_system import GameSystem, SystemData
|
||||
from adventure.models.base import Attributes, dataclass, uuid
|
||||
from adventure.models.entity import (
|
||||
Actor,
|
||||
Character,
|
||||
EntityReference,
|
||||
Item,
|
||||
Room,
|
||||
|
@ -35,8 +35,8 @@ class QuestGoalContains:
|
|||
Quest goal for any kind of fetch quest, including delivery and escort quests.
|
||||
|
||||
Valid combinations are:
|
||||
- container: Room and items: List[Actor | Item]
|
||||
- container: Actor and items: List[Item]
|
||||
- container: Room and items: List[Character | Item]
|
||||
- container: Character and items: List[Item]
|
||||
"""
|
||||
|
||||
container: EntityReference
|
||||
|
@ -98,7 +98,7 @@ def is_quest_complete(world: World, quest: Quest) -> bool:
|
|||
if content.item:
|
||||
if not find_item_in_room(container, content.item):
|
||||
return False
|
||||
elif isinstance(container, (Actor, Item)):
|
||||
elif isinstance(container, (Character, Item)):
|
||||
if content.item:
|
||||
if not find_item_in_container(container, content.item):
|
||||
return False
|
||||
|
@ -122,41 +122,41 @@ def is_quest_complete(world: World, quest: Quest) -> bool:
|
|||
|
||||
|
||||
# region state management
|
||||
def get_quests_for_actor(quests: QuestData, actor: Actor) -> List[Quest]:
|
||||
def get_quests_for_character(quests: QuestData, character: Character) -> List[Quest]:
|
||||
"""
|
||||
Get all quests for the given actor.
|
||||
Get all quests for the given character.
|
||||
"""
|
||||
return quests.available.get(actor.name, [])
|
||||
return quests.available.get(character.name, [])
|
||||
|
||||
|
||||
def set_active_quest(quests: QuestData, actor: Actor, quest: Quest) -> None:
|
||||
def set_active_quest(quests: QuestData, character: Character, quest: Quest) -> None:
|
||||
"""
|
||||
Set the active quest for the given actor.
|
||||
Set the active quest for the given character.
|
||||
"""
|
||||
quests.active[actor.name] = quest
|
||||
quests.active[character.name] = quest
|
||||
|
||||
|
||||
def get_active_quest(quests: QuestData, actor: Actor) -> Quest | None:
|
||||
def get_active_quest(quests: QuestData, character: Character) -> Quest | None:
|
||||
"""
|
||||
Get the active quest for the given actor.
|
||||
Get the active quest for the given character.
|
||||
"""
|
||||
return quests.active.get(actor.name)
|
||||
return quests.active.get(character.name)
|
||||
|
||||
|
||||
def complete_quest(quests: QuestData, actor: Actor, quest: Quest) -> None:
|
||||
def complete_quest(quests: QuestData, character: Character, quest: Quest) -> None:
|
||||
"""
|
||||
Complete the given quest for the given actor.
|
||||
Complete the given quest for the given character.
|
||||
"""
|
||||
if quest in quests.available.get(actor.name, []):
|
||||
quests.available[actor.name].remove(quest)
|
||||
if quest in quests.available.get(character.name, []):
|
||||
quests.available[character.name].remove(quest)
|
||||
|
||||
if quest == quests.active.get(actor.name, None):
|
||||
del quests.active[actor.name]
|
||||
if quest == quests.active.get(character.name, None):
|
||||
del quests.active[character.name]
|
||||
|
||||
if actor.name not in quests.completed:
|
||||
quests.completed[actor.name] = []
|
||||
if character.name not in quests.completed:
|
||||
quests.completed[character.name] = []
|
||||
|
||||
quests.completed[actor.name].append(quest)
|
||||
quests.completed[character.name].append(quest)
|
||||
|
||||
|
||||
# endregion
|
||||
|
@ -180,8 +180,8 @@ def generate_quests(agent: Agent, theme: str, entity: WorldEntity) -> None:
|
|||
if not quests:
|
||||
raise ValueError("Quest data is required for quest generation")
|
||||
|
||||
if isinstance(entity, Actor):
|
||||
available_quests = get_quests_for_actor(quests, entity)
|
||||
if isinstance(entity, Character):
|
||||
available_quests = get_quests_for_character(quests, entity)
|
||||
if len(available_quests) == 0:
|
||||
logger.info(f"generating new quest for {entity.name}")
|
||||
# TODO: generate one new quest
|
||||
|
@ -201,13 +201,17 @@ def simulate_quests(world: World, step: int, data: QuestData | None = None) -> N
|
|||
raise ValueError("Quest data is required for simulation")
|
||||
|
||||
for room in world.rooms:
|
||||
for actor in room.actors:
|
||||
active_quest = get_active_quest(quests, actor)
|
||||
for character in room.characters:
|
||||
active_quest = get_active_quest(quests, character)
|
||||
if active_quest:
|
||||
logger.info(f"simulating quest for {actor.name}: {active_quest.name}")
|
||||
logger.info(
|
||||
f"simulating quest for {character.name}: {active_quest.name}"
|
||||
)
|
||||
if is_quest_complete(world, active_quest):
|
||||
logger.info(f"quest complete for {actor.name}: {active_quest.name}")
|
||||
complete_quest(quests, actor, active_quest)
|
||||
logger.info(
|
||||
f"quest complete for {character.name}: {active_quest.name}"
|
||||
)
|
||||
complete_quest(quests, character, active_quest)
|
||||
|
||||
|
||||
def load_quest_data(file: str) -> QuestData:
|
||||
|
|
|
@ -31,19 +31,19 @@ def action_craft(item: str) -> str:
|
|||
Args:
|
||||
item: The name of the item to craft.
|
||||
"""
|
||||
with world_context() as (action_world, _, action_actor):
|
||||
with world_context() as (action_world, _, action_character):
|
||||
if item not in recipes:
|
||||
return f"There is no recipe to craft a {item}."
|
||||
|
||||
recipe = recipes[item]
|
||||
|
||||
# Check if the actor has the required skill level
|
||||
# Check if the character has the required skill level
|
||||
skill = randint(1, 20)
|
||||
if skill < recipe.difficulty:
|
||||
return f"You need a crafting skill level of {recipe.difficulty} to craft {item}."
|
||||
|
||||
# Collect inventory items names
|
||||
inventory_items = {item.name for item in action_actor.items}
|
||||
inventory_items = {item.name for item in action_character.items}
|
||||
|
||||
# Check for sufficient ingredients
|
||||
missing_items = [
|
||||
|
@ -55,13 +55,14 @@ def action_craft(item: str) -> str:
|
|||
# Deduct the ingredients from inventory
|
||||
for ingredient in recipe.ingredients:
|
||||
item_to_remove = next(
|
||||
item for item in action_actor.items if item.name == ingredient
|
||||
item for item in action_character.items if item.name == ingredient
|
||||
)
|
||||
action_actor.items.remove(item_to_remove)
|
||||
action_character.items.remove(item_to_remove)
|
||||
|
||||
# Create and add the crafted item to inventory
|
||||
result_item = next(
|
||||
(item for item in action_actor.items if item.name == recipe.result), None
|
||||
(item for item in action_character.items if item.name == recipe.result),
|
||||
None,
|
||||
)
|
||||
if result_item:
|
||||
new_item = Item(**vars(result_item)) # Copying the item
|
||||
|
@ -72,7 +73,7 @@ def action_craft(item: str) -> str:
|
|||
dungeon_master, action_world, systems
|
||||
) # TODO: pass crafting recipe and generate from that
|
||||
|
||||
action_actor.items.append(new_item)
|
||||
action_character.items.append(new_item)
|
||||
|
||||
broadcast(f"{action_actor.name} crafts a {item}.")
|
||||
broadcast(f"{action_character.name} crafts a {item}.")
|
||||
return f"You successfully craft a {item}."
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from adventure.context import action_context, broadcast
|
||||
from adventure.utils.search import find_item_in_actor
|
||||
from adventure.utils.search import find_item_in_character
|
||||
|
||||
|
||||
def action_read(item: str) -> str:
|
||||
|
@ -9,13 +9,13 @@ def action_read(item: str) -> str:
|
|||
Args:
|
||||
item: The name of the item to read.
|
||||
"""
|
||||
with action_context() as (_, action_actor):
|
||||
action_item = find_item_in_actor(action_actor, item)
|
||||
with action_context() as (_, action_character):
|
||||
action_item = find_item_in_character(action_character, item)
|
||||
if not action_item:
|
||||
return f"You do not have a {item} to read."
|
||||
|
||||
if "text" in action_item.attributes:
|
||||
broadcast(f"{action_actor.name} reads {item}")
|
||||
broadcast(f"{action_character.name} reads {item}")
|
||||
return str(action_item.attributes["text"])
|
||||
|
||||
return f"The {item} has nothing to read."
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from random import randint
|
||||
|
||||
from adventure.context import action_context, broadcast, get_dungeon_master
|
||||
from adventure.utils.search import find_actor_in_room
|
||||
from adventure.utils.search import find_character_in_room
|
||||
|
||||
|
||||
def action_cast(spell: str, target: str) -> str:
|
||||
|
@ -12,25 +12,30 @@ def action_cast(spell: str, target: str) -> str:
|
|||
spell: The name of the spell to cast.
|
||||
target: The target of the spell.
|
||||
"""
|
||||
with action_context() as (action_room, action_actor):
|
||||
target_actor = find_actor_in_room(action_room, target)
|
||||
with action_context() as (action_room, action_character):
|
||||
target_character = find_character_in_room(action_room, target)
|
||||
dungeon_master = get_dungeon_master()
|
||||
|
||||
# Check for spell availability and mana costs
|
||||
if spell not in action_actor.attributes["spells"]:
|
||||
if spell not in action_character.attributes["spells"]:
|
||||
return f"You do not know the spell '{spell}'."
|
||||
if action_actor.attributes["mana"] < action_actor.attributes["spells"][spell]:
|
||||
if (
|
||||
action_character.attributes["mana"]
|
||||
< action_character.attributes["spells"][spell]
|
||||
):
|
||||
return "You do not have enough mana to cast this spell."
|
||||
|
||||
action_actor.attributes["mana"] -= action_actor.attributes["spells"][spell]
|
||||
action_character.attributes["mana"] -= action_character.attributes["spells"][
|
||||
spell
|
||||
]
|
||||
# Get flavor text from the dungeon master
|
||||
flavor_text = dungeon_master(f"Describe the effects of {spell} on {target}.")
|
||||
broadcast(f"{action_actor.name} casts {spell} on {target}. {flavor_text}")
|
||||
broadcast(f"{action_character.name} casts {spell} on {target}. {flavor_text}")
|
||||
|
||||
# Apply effects based on the spell
|
||||
if spell == "heal" and target_actor:
|
||||
if spell == "heal" and target_character:
|
||||
heal_amount = randint(10, 30)
|
||||
target_actor.attributes["health"] += heal_amount
|
||||
target_character.attributes["health"] += heal_amount
|
||||
return f"{target} is healed for {heal_amount} points."
|
||||
|
||||
return f"{spell} was successfully cast on {target}."
|
||||
|
|
|
@ -11,7 +11,7 @@ def action_climb(target: str) -> str:
|
|||
Args:
|
||||
target: The object or feature to climb.
|
||||
"""
|
||||
with action_context() as (action_room, action_actor):
|
||||
with action_context() as (action_room, action_character):
|
||||
dungeon_master = get_dungeon_master()
|
||||
# Assume 'climbable' is an attribute that marks climbable targets
|
||||
climbable_feature = find_item_in_room(action_room, target)
|
||||
|
@ -22,16 +22,16 @@ def action_climb(target: str) -> str:
|
|||
|
||||
# Get flavor text for the climb attempt
|
||||
flavor_text = dungeon_master(
|
||||
f"Describe {action_actor.name}'s attempt to climb {target}."
|
||||
f"Describe {action_character.name}'s attempt to climb {target}."
|
||||
)
|
||||
if climb_roll > climb_difficulty:
|
||||
broadcast(
|
||||
f"{action_actor.name} successfully climbs the {target}. {flavor_text}"
|
||||
f"{action_character.name} successfully climbs the {target}. {flavor_text}"
|
||||
)
|
||||
return f"You successfully climb the {target}."
|
||||
else:
|
||||
broadcast(
|
||||
f"{action_actor.name} fails to climb the {target}. {flavor_text}"
|
||||
f"{action_character.name} fails to climb the {target}. {flavor_text}"
|
||||
)
|
||||
return f"You fail to climb the {target}."
|
||||
else:
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
from adventure.context import (
|
||||
action_context,
|
||||
broadcast,
|
||||
get_agent_for_actor,
|
||||
get_agent_for_character,
|
||||
get_dungeon_master,
|
||||
)
|
||||
from adventure.utils.search import find_actor_in_room, find_item_in_room
|
||||
from adventure.utils.search import find_character_in_room, find_item_in_room
|
||||
from adventure.utils.world import describe_entity
|
||||
|
||||
|
||||
|
@ -16,30 +16,32 @@ def action_attack(target: str) -> str:
|
|||
target: The name of the character or item to attack.
|
||||
"""
|
||||
|
||||
with action_context() as (action_room, action_actor):
|
||||
with action_context() as (action_room, action_character):
|
||||
# make sure the target is in the room
|
||||
target_actor = find_actor_in_room(action_room, target)
|
||||
target_character = find_character_in_room(action_room, target)
|
||||
target_item = find_item_in_room(action_room, target)
|
||||
|
||||
dungeon_master = get_dungeon_master()
|
||||
if target_actor:
|
||||
target_agent = get_agent_for_actor(target_actor)
|
||||
if target_character:
|
||||
target_agent = get_agent_for_character(target_character)
|
||||
if not target_agent:
|
||||
raise ValueError(f"no agent found for actor {target_actor.name}")
|
||||
raise ValueError(
|
||||
f"no agent found for character {target_character.name}"
|
||||
)
|
||||
|
||||
reaction = target_agent(
|
||||
f"{action_actor.name} is attacking you in the {action_room.name}. How do you react?"
|
||||
f"{action_character.name} is attacking you in the {action_room.name}. How do you react?"
|
||||
"Respond with 'fighting', 'fleeing', or 'surrendering'."
|
||||
)
|
||||
|
||||
outcome = dungeon_master(
|
||||
f"{action_actor.name} attacks {target} in the {action_room.name}. {describe_entity(action_room)}."
|
||||
f"{describe_entity(action_actor)}. {describe_entity(target_actor)}."
|
||||
f"{action_character.name} attacks {target} in the {action_room.name}. {describe_entity(action_room)}."
|
||||
f"{describe_entity(action_character)}. {describe_entity(target_character)}."
|
||||
f"{target} reacts by {reaction}. What is the outcome of the attack? Describe the result in detail."
|
||||
)
|
||||
|
||||
description = (
|
||||
f"{action_actor.name} attacks the {target} in the {action_room.name}."
|
||||
f"{action_character.name} attacks the {target} in the {action_room.name}."
|
||||
f"{target} reacts by {reaction}. {outcome}"
|
||||
)
|
||||
broadcast(description)
|
||||
|
@ -47,12 +49,12 @@ def action_attack(target: str) -> str:
|
|||
|
||||
if target_item:
|
||||
outcome = dungeon_master(
|
||||
f"{action_actor.name} attacks {target} in the {action_room.name}. {describe_entity(action_room)}."
|
||||
f"{describe_entity(action_actor)}. {describe_entity(target_item)}."
|
||||
f"{action_character.name} attacks {target} in the {action_room.name}. {describe_entity(action_room)}."
|
||||
f"{describe_entity(action_character)}. {describe_entity(target_item)}."
|
||||
f"What is the outcome of the attack? Describe the result in detail."
|
||||
)
|
||||
|
||||
description = f"{action_actor.name} attacks the {target} in the {action_room.name}. {outcome}"
|
||||
description = f"{action_character.name} attacks the {target} in the {action_room.name}. {outcome}"
|
||||
broadcast(description)
|
||||
return description
|
||||
|
||||
|
@ -68,21 +70,21 @@ def action_cast(target: str, spell: str) -> str:
|
|||
spell: The name of the spell to cast.
|
||||
"""
|
||||
|
||||
with action_context() as (action_room, action_actor):
|
||||
with action_context() as (action_room, action_character):
|
||||
# make sure the target is in the room
|
||||
target_actor = find_actor_in_room(action_room, target)
|
||||
target_character = find_character_in_room(action_room, target)
|
||||
target_item = find_item_in_room(action_room, target)
|
||||
|
||||
if not target_actor and not target_item:
|
||||
if not target_character and not target_item:
|
||||
return f"{target} is not in the {action_room.name}."
|
||||
|
||||
dungeon_master = get_dungeon_master()
|
||||
outcome = dungeon_master(
|
||||
f"{action_actor.name} casts {spell} on {target} in the {action_room.name}. {describe_entity(action_room)}."
|
||||
f"{describe_entity(action_actor)}. {describe_entity(target_actor) if target_actor else describe_entity(target_item)}."
|
||||
f"{action_character.name} casts {spell} on {target} in the {action_room.name}. {describe_entity(action_room)}."
|
||||
f"{describe_entity(action_character)}. {describe_entity(target_character) if target_character else describe_entity(target_item)}."
|
||||
f"What is the outcome of the spell? Describe the result in detail."
|
||||
)
|
||||
|
||||
description = f"{action_actor.name} casts {spell} on the {target} in the {action_room.name}. {outcome}"
|
||||
description = f"{action_character.name} casts {spell} on the {target} in the {action_room.name}. {outcome}"
|
||||
broadcast(description)
|
||||
return description
|
||||
|
|
|
@ -2,7 +2,7 @@ rules:
|
|||
# wet/dry logic
|
||||
- group: environment-moisture
|
||||
match:
|
||||
type: actor
|
||||
type: character
|
||||
wet: true
|
||||
chance: 0.1
|
||||
set:
|
||||
|
@ -10,7 +10,7 @@ rules:
|
|||
|
||||
- group: environment-moisture
|
||||
match:
|
||||
type: actor
|
||||
type: character
|
||||
wet: true
|
||||
temperature: hot
|
||||
chance: 0.2
|
||||
|
@ -33,7 +33,7 @@ rules:
|
|||
|
||||
labels:
|
||||
- match:
|
||||
type: actor
|
||||
type: character
|
||||
wet: true
|
||||
backstory: You are soaking wet.
|
||||
description: They are soaking wet and dripping water.
|
||||
|
|
|
@ -3,21 +3,21 @@ from adventure.models.entity import Attributes, Room
|
|||
|
||||
def hot_room(room: Room, attributes: Attributes):
|
||||
"""
|
||||
If the room is hot, actors should get hotter.
|
||||
If the room is hot, characters should get hotter.
|
||||
"""
|
||||
|
||||
for actor in room.actors:
|
||||
actor.attributes["hot"] = "hot"
|
||||
for character in room.characters:
|
||||
character.attributes["hot"] = "hot"
|
||||
|
||||
return attributes
|
||||
|
||||
|
||||
def cold_room(room: Room, attributes: Attributes):
|
||||
"""
|
||||
If the room is cold, actors should get colder.
|
||||
If the room is cold, characters should get colder.
|
||||
"""
|
||||
|
||||
for actor in room.actors:
|
||||
actor.attributes["cold"] = "cold"
|
||||
for character in room.characters:
|
||||
character.attributes["cold"] = "cold"
|
||||
|
||||
return attributes
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from adventure.context import action_context
|
||||
from adventure.utils.search import find_item_in_actor
|
||||
from adventure.utils.search import find_item_in_character
|
||||
|
||||
|
||||
def action_cook(item: str) -> str:
|
||||
|
@ -9,8 +9,8 @@ def action_cook(item: str) -> str:
|
|||
Args:
|
||||
item: The name of the item to cook.
|
||||
"""
|
||||
with action_context() as (_, action_actor):
|
||||
target_item = find_item_in_actor(action_actor, item)
|
||||
with action_context() as (_, action_character):
|
||||
target_item = find_item_in_character(action_character, item)
|
||||
if target_item is None:
|
||||
return "You don't have the item to cook."
|
||||
|
||||
|
@ -36,8 +36,8 @@ def action_eat(item: str) -> str:
|
|||
Args:
|
||||
item: The name of the item to eat.
|
||||
"""
|
||||
with action_context() as (_, action_actor):
|
||||
target_item = find_item_in_actor(action_actor, item)
|
||||
with action_context() as (_, action_character):
|
||||
target_item = find_item_in_character(action_character, item)
|
||||
if target_item is None:
|
||||
return "You don't have the item to eat."
|
||||
|
||||
|
@ -56,12 +56,12 @@ def action_eat(item: str) -> str:
|
|||
if spoiled:
|
||||
return "You can't eat that item, it is rotten."
|
||||
|
||||
# Check if the actor is hungry
|
||||
hunger = action_actor.attributes.get("hunger", None)
|
||||
# Check if the character is hungry
|
||||
hunger = action_character.attributes.get("hunger", None)
|
||||
if hunger != "hungry":
|
||||
return "You're not hungry."
|
||||
|
||||
# Eat the item
|
||||
action_actor.items.remove(target_item)
|
||||
action_actor.attributes["hunger"] = "full"
|
||||
action_character.items.remove(target_item)
|
||||
action_character.attributes["hunger"] = "full"
|
||||
return f"You eat the {item}."
|
||||
|
|
|
@ -21,7 +21,7 @@ rules:
|
|||
# hunger logic
|
||||
- group: hunger
|
||||
match:
|
||||
type: actor
|
||||
type: character
|
||||
hunger: full
|
||||
chance: 0.1
|
||||
set:
|
||||
|
@ -37,7 +37,7 @@ rules:
|
|||
# thirst logic
|
||||
- group: thirst
|
||||
match:
|
||||
type: actor
|
||||
type: character
|
||||
thirst: hydrated
|
||||
chance: 0.1
|
||||
set:
|
||||
|
@ -77,27 +77,27 @@ labels:
|
|||
backstory: You are rotten and inedible.
|
||||
description: This item is rotten and inedible.
|
||||
- match:
|
||||
type: actor
|
||||
type: character
|
||||
spoiled: false
|
||||
backstory: You are fresh and edible.
|
||||
description: This item is fresh and edible.
|
||||
- match:
|
||||
type: actor
|
||||
type: character
|
||||
hunger: full
|
||||
backstory: You are have eaten recently and are full.
|
||||
description: ~
|
||||
- match:
|
||||
type: actor
|
||||
type: character
|
||||
hunger: hungry
|
||||
backstory: You are hungry and need to eat.
|
||||
description: They look hungry.
|
||||
- match:
|
||||
type: actor
|
||||
type: character
|
||||
thirst: hydrated
|
||||
backstory: You are hydrated.
|
||||
description: ~
|
||||
- match:
|
||||
type: actor
|
||||
type: character
|
||||
thirst: thirsty
|
||||
backstory: You are thirsty and need to drink.
|
||||
description: They look thirsty.
|
|
@ -7,15 +7,15 @@ def action_wash(unused: bool) -> str:
|
|||
Wash yourself.
|
||||
"""
|
||||
|
||||
with action_context() as (action_room, action_actor):
|
||||
hygiene = action_actor.attributes.get("hygiene", "clean")
|
||||
with action_context() as (action_room, action_character):
|
||||
hygiene = action_character.attributes.get("hygiene", "clean")
|
||||
|
||||
dungeon_master = get_dungeon_master()
|
||||
outcome = dungeon_master(
|
||||
f"{action_actor.name} washes themselves in the {action_room.name}. {describe_entity(action_room)}. {describe_entity(action_actor)}"
|
||||
f"{action_actor.name} was {hygiene} to start with. How clean are they after washing? Respond with 'clean' or 'dirty'."
|
||||
f"{action_character.name} washes themselves in the {action_room.name}. {describe_entity(action_room)}. {describe_entity(action_character)}"
|
||||
f"{action_character.name} was {hygiene} to start with. How clean are they after washing? Respond with 'clean' or 'dirty'."
|
||||
"If the room has a shower or running water, they should be cleaner. If the room is dirty, they should end up dirtier."
|
||||
)
|
||||
|
||||
action_actor.attributes["clean"] = outcome.strip().lower()
|
||||
action_character.attributes["clean"] = outcome.strip().lower()
|
||||
return f"You wash yourself in the {action_room.name} and feel {outcome}"
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
rules:
|
||||
- match:
|
||||
type: actor
|
||||
type: character
|
||||
hygiene: clean
|
||||
chance: 0.1
|
||||
set:
|
||||
hygiene: dirty
|
||||
|
||||
- match:
|
||||
type: actor
|
||||
type: character
|
||||
hygiene: dirty
|
||||
chance: 0.1
|
||||
set:
|
||||
|
@ -21,17 +21,17 @@ rules:
|
|||
|
||||
labels:
|
||||
- match:
|
||||
type: actor
|
||||
type: character
|
||||
hygiene: clean
|
||||
backstory: You are clean and smell fresh.
|
||||
description: They look freshly washed and smell clean.
|
||||
- match:
|
||||
type: actor
|
||||
type: character
|
||||
hygiene: dirty
|
||||
backstory: You are dirty and smell bad.
|
||||
description: They look dirty and smell bad.
|
||||
- match:
|
||||
type: actor
|
||||
type: character
|
||||
hygiene: filthy
|
||||
backstory: You are filthy and smell terrible.
|
||||
description: They look filthy and smell terrible.
|
||||
|
|
|
@ -2,7 +2,7 @@ rules:
|
|||
# mood logic
|
||||
- group: mood
|
||||
match:
|
||||
type: actor
|
||||
type: character
|
||||
mood: happy
|
||||
chance: 0.1
|
||||
set:
|
||||
|
@ -10,7 +10,7 @@ rules:
|
|||
|
||||
- group: mood
|
||||
match:
|
||||
type: actor
|
||||
type: character
|
||||
mood: happy
|
||||
chance: 0.1
|
||||
set:
|
||||
|
@ -18,7 +18,7 @@ rules:
|
|||
|
||||
- group: mood
|
||||
match:
|
||||
type: actor
|
||||
type: character
|
||||
mood: angry
|
||||
chance: 0.1
|
||||
set:
|
||||
|
@ -26,7 +26,7 @@ rules:
|
|||
|
||||
- group: mood
|
||||
match:
|
||||
type: actor
|
||||
type: character
|
||||
mood: neutral
|
||||
chance: 0.1
|
||||
set:
|
||||
|
@ -34,7 +34,7 @@ rules:
|
|||
|
||||
- group: mood
|
||||
match:
|
||||
type: actor
|
||||
type: character
|
||||
mood: neutral
|
||||
chance: 0.1
|
||||
set:
|
||||
|
@ -42,7 +42,7 @@ rules:
|
|||
|
||||
- group: mood
|
||||
match:
|
||||
type: actor
|
||||
type: character
|
||||
mood: sad
|
||||
chance: 0.1
|
||||
set:
|
||||
|
@ -50,7 +50,7 @@ rules:
|
|||
|
||||
- group: mood
|
||||
match:
|
||||
type: actor
|
||||
type: character
|
||||
mood: sad
|
||||
chance: 0.1
|
||||
set:
|
||||
|
@ -59,7 +59,7 @@ rules:
|
|||
# mood interactions with other systems
|
||||
- group: mood
|
||||
match:
|
||||
type: actor
|
||||
type: character
|
||||
mood: sad
|
||||
sleep: rested
|
||||
chance: 0.2
|
||||
|
@ -68,7 +68,7 @@ rules:
|
|||
|
||||
- group: mood
|
||||
match:
|
||||
type: actor
|
||||
type: character
|
||||
hunger: hungry
|
||||
chance: 0.2
|
||||
set:
|
||||
|
@ -76,7 +76,7 @@ rules:
|
|||
|
||||
- group: mood
|
||||
match:
|
||||
type: actor
|
||||
type: character
|
||||
mood: angry
|
||||
hunger: full
|
||||
chance: 0.2
|
||||
|
@ -85,7 +85,7 @@ rules:
|
|||
|
||||
- group: mood
|
||||
match:
|
||||
type: actor
|
||||
type: character
|
||||
mood: neutral
|
||||
hunger: full
|
||||
chance: 0.2
|
||||
|
@ -94,7 +94,7 @@ rules:
|
|||
|
||||
- group: mood
|
||||
match:
|
||||
type: actor
|
||||
type: character
|
||||
mood: happy
|
||||
hunger: hungry
|
||||
chance: 0.2
|
||||
|
@ -103,7 +103,7 @@ rules:
|
|||
|
||||
- group: mood
|
||||
match:
|
||||
type: actor
|
||||
type: character
|
||||
mood: neutral
|
||||
sleep: tired
|
||||
chance: 0.2
|
||||
|
@ -119,17 +119,17 @@ rules:
|
|||
|
||||
labels:
|
||||
- match:
|
||||
type: actor
|
||||
type: character
|
||||
mood: happy
|
||||
backstory: You are feeling happy.
|
||||
description: They look happy.
|
||||
- match:
|
||||
type: actor
|
||||
type: character
|
||||
mood: sad
|
||||
backstory: You are feeling sad.
|
||||
description: They look sad.
|
||||
- match:
|
||||
type: actor
|
||||
type: character
|
||||
mood: angry
|
||||
backstory: You are feeling angry.
|
||||
description: They look angry.
|
||||
|
|
|
@ -7,12 +7,12 @@ def action_sleep(unused: bool) -> str:
|
|||
Sleep until you are rested.
|
||||
"""
|
||||
|
||||
with action_context() as (action_room, action_actor):
|
||||
with action_context() as (action_room, action_character):
|
||||
dungeon_master = get_dungeon_master()
|
||||
outcome = dungeon_master(
|
||||
f"{action_actor.name} sleeps in the {action_room.name}. {describe_entity(action_room)}. {describe_entity(action_actor)}"
|
||||
f"{action_character.name} sleeps in the {action_room.name}. {describe_entity(action_room)}. {describe_entity(action_character)}"
|
||||
"How rested are they? Respond with 'rested' or 'tired'."
|
||||
)
|
||||
|
||||
action_actor.attributes["rested"] = outcome
|
||||
action_character.attributes["rested"] = outcome
|
||||
return f"You sleep in the {action_room.name} and wake up feeling {outcome}"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
rules:
|
||||
# sleeping logic
|
||||
- match:
|
||||
type: actor
|
||||
type: character
|
||||
sleep: rested
|
||||
chance: 0.1
|
||||
set:
|
||||
|
@ -15,12 +15,12 @@ rules:
|
|||
|
||||
labels:
|
||||
- match:
|
||||
type: actor
|
||||
type: character
|
||||
sleep: rested
|
||||
backstory: You are well-rested.
|
||||
description: They look well-rested.
|
||||
- match:
|
||||
type: actor
|
||||
type: character
|
||||
sleep: tired
|
||||
backstory: You are tired.
|
||||
description: They look tired.
|
||||
|
|
|
@ -10,7 +10,7 @@ from packit.utils import could_be_json
|
|||
|
||||
from adventure.context import broadcast
|
||||
from adventure.models.config import DEFAULT_CONFIG
|
||||
from adventure.models.entity import Actor, Room
|
||||
from adventure.models.entity import Character, Room
|
||||
from adventure.models.event import ReplyEvent
|
||||
|
||||
from .string import normalize_name
|
||||
|
@ -18,7 +18,7 @@ from .string import normalize_name
|
|||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
actor_config = DEFAULT_CONFIG.world.actor
|
||||
character_config = DEFAULT_CONFIG.world.character
|
||||
|
||||
|
||||
def make_keyword_condition(end_message: str, keywords=["end", "stop"]):
|
||||
|
@ -85,19 +85,23 @@ def or_list(items: List[str]) -> str:
|
|||
return f"{', '.join(items[:-1])}, or {items[-1]}"
|
||||
|
||||
|
||||
def summarize_room(room: Room, player: Actor) -> str:
|
||||
def summarize_room(room: Room, player: Character) -> str:
|
||||
"""
|
||||
Summarize a room for the player.
|
||||
"""
|
||||
|
||||
actor_names = and_list(
|
||||
[actor.name for actor in room.actors if actor.name != player.name]
|
||||
character_names = and_list(
|
||||
[
|
||||
character.name
|
||||
for character in room.characters
|
||||
if character.name != player.name
|
||||
]
|
||||
)
|
||||
item_names = and_list([item.name for item in room.items])
|
||||
inventory_names = and_list([item.name for item in player.items])
|
||||
|
||||
return (
|
||||
f"You are in the {room.name} room with {actor_names}. "
|
||||
f"You are in the {room.name} room with {character_names}. "
|
||||
f"You see the {item_names} around the room. "
|
||||
f"You are carrying the {inventory_names}."
|
||||
)
|
||||
|
@ -105,9 +109,9 @@ def summarize_room(room: Room, player: Actor) -> str:
|
|||
|
||||
def loop_conversation(
|
||||
room: Room,
|
||||
actors: List[Actor],
|
||||
characters: List[Character],
|
||||
agents: List[Agent],
|
||||
first_actor: Actor,
|
||||
first_character: Character,
|
||||
first_prompt: str,
|
||||
reply_prompt: str,
|
||||
first_message: str,
|
||||
|
@ -117,14 +121,14 @@ def loop_conversation(
|
|||
max_length: int | None = None,
|
||||
) -> str | None:
|
||||
"""
|
||||
Loop through a conversation between a series of agents, using metadata from their actors.
|
||||
Loop through a conversation between a series of agents, using metadata from their characters.
|
||||
"""
|
||||
|
||||
if max_length is None:
|
||||
max_length = actor_config.conversation_limit
|
||||
max_length = character_config.conversation_limit
|
||||
|
||||
if len(actors) != len(agents):
|
||||
raise ValueError("The number of actors and agents must match.")
|
||||
if len(characters) != len(agents):
|
||||
raise ValueError("The number of characters and agents must match.")
|
||||
|
||||
# set up the keyword or length-limit compound condition
|
||||
_, condition_end, parse_end = make_keyword_condition(end_message)
|
||||
|
@ -145,34 +149,36 @@ def loop_conversation(
|
|||
|
||||
# prepare the loop state
|
||||
i = 0
|
||||
last_actor = first_actor
|
||||
last_character = first_character
|
||||
response = first_message
|
||||
|
||||
while not stop_condition(current=i):
|
||||
if i == 0:
|
||||
logger.debug(f"starting conversation with {first_actor.name}")
|
||||
logger.debug(f"starting conversation with {first_character.name}")
|
||||
prompt = first_prompt
|
||||
else:
|
||||
logger.debug(f"continuing conversation with {last_actor.name} on step {i}")
|
||||
logger.debug(
|
||||
f"continuing conversation with {last_character.name} on step {i}"
|
||||
)
|
||||
prompt = reply_prompt
|
||||
|
||||
# loop through the actors and agents
|
||||
actor = actors[i % len(actors)]
|
||||
# loop through the characters and agents
|
||||
character = characters[i % len(characters)]
|
||||
agent = agents[i % len(agents)]
|
||||
|
||||
# summarize the room and present the last response
|
||||
summary = summarize_room(room, actor)
|
||||
summary = summarize_room(room, character)
|
||||
response = agent(
|
||||
prompt, response=response, summary=summary, last_actor=last_actor
|
||||
prompt, response=response, summary=summary, last_character=last_character
|
||||
)
|
||||
response = result_parser(response)
|
||||
|
||||
logger.info(f"{actor.name} responds: {response}")
|
||||
reply_event = ReplyEvent.from_text(response, room, actor)
|
||||
logger.info(f"{character.name} responds: {response}")
|
||||
reply_event = ReplyEvent.from_text(response, room, character)
|
||||
broadcast(reply_event)
|
||||
|
||||
# increment the step counter
|
||||
i += 1
|
||||
last_actor = actor
|
||||
last_character = character
|
||||
|
||||
return response
|
||||
|
|
|
@ -13,7 +13,7 @@ from adventure.models.effect import (
|
|||
StringEffectPattern,
|
||||
StringEffectResult,
|
||||
)
|
||||
from adventure.models.entity import Actor, Attributes
|
||||
from adventure.models.entity import Attributes, Character
|
||||
from adventure.utils.attribute import (
|
||||
add_value,
|
||||
append_value,
|
||||
|
@ -252,9 +252,9 @@ def apply_permanent_effects(
|
|||
return apply_permanent_results(attributes, results)
|
||||
|
||||
|
||||
def apply_effects(target: Actor, effects: List[EffectPattern]) -> None:
|
||||
def apply_effects(target: Character, effects: List[EffectPattern]) -> None:
|
||||
"""
|
||||
Apply a set of effects to an actor and their attributes.
|
||||
Apply a set of effects to a character and their attributes.
|
||||
"""
|
||||
|
||||
permanent_effects = [
|
||||
|
@ -270,9 +270,9 @@ def apply_effects(target: Actor, effects: List[EffectPattern]) -> None:
|
|||
target.active_effects.extend(temporary_effects)
|
||||
|
||||
|
||||
def expire_effects(target: Actor) -> None:
|
||||
def expire_effects(target: Character) -> None:
|
||||
"""
|
||||
Decrement the duration of effects on an actor and remove any that have expired.
|
||||
Decrement the duration of effects on a character and remove any that have expired.
|
||||
"""
|
||||
|
||||
for effect in target.active_effects:
|
||||
|
|
|
@ -1,34 +1,36 @@
|
|||
from adventure.models.entity import Actor
|
||||
from adventure.models.entity import Character
|
||||
|
||||
|
||||
def expire_events(actor: Actor, current_turn: int):
|
||||
def expire_events(character: Character, current_turn: int):
|
||||
"""
|
||||
Expire events that have already happened.
|
||||
"""
|
||||
|
||||
events = actor.planner.calendar.events
|
||||
events = character.planner.calendar.events
|
||||
expired_events = [event for event in events if event.turn < current_turn]
|
||||
actor.planner.calendar.events[:] = [
|
||||
character.planner.calendar.events[:] = [
|
||||
event for event in events if event not in expired_events
|
||||
]
|
||||
|
||||
return expired_events
|
||||
|
||||
|
||||
def get_recent_notes(actor: Actor, count: int = 3):
|
||||
def get_recent_notes(character: Character, count: int = 3):
|
||||
"""
|
||||
Get the most recent facts from your notes.
|
||||
"""
|
||||
|
||||
return actor.planner.notes[-count:]
|
||||
return character.planner.notes[-count:]
|
||||
|
||||
|
||||
def get_upcoming_events(actor: Actor, current_turn: int, upcoming_turns: int = 3):
|
||||
def get_upcoming_events(
|
||||
character: Character, current_turn: int, upcoming_turns: int = 3
|
||||
):
|
||||
"""
|
||||
Get a list of upcoming events within a certain number of turns.
|
||||
"""
|
||||
|
||||
calendar = actor.planner.calendar
|
||||
calendar = character.planner.calendar
|
||||
# TODO: sort events by turn
|
||||
return [
|
||||
event
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from typing import Any, Generator
|
||||
|
||||
from adventure.models.entity import (
|
||||
Actor,
|
||||
Character,
|
||||
EntityReference,
|
||||
Item,
|
||||
Portal,
|
||||
|
@ -30,19 +30,19 @@ def find_portal(world: World, portal_name: str) -> Portal | None:
|
|||
return None
|
||||
|
||||
|
||||
def find_actor(world: World, actor_name: str) -> Actor | None:
|
||||
def find_character(world: World, character_name: str) -> Character | None:
|
||||
for room in world.rooms:
|
||||
actor = find_actor_in_room(room, actor_name)
|
||||
if actor:
|
||||
return actor
|
||||
character = find_character_in_room(room, character_name)
|
||||
if character:
|
||||
return character
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def find_actor_in_room(room: Room, actor_name: str) -> Actor | None:
|
||||
for actor in room.actors:
|
||||
if normalize_name(actor.name) == normalize_name(actor_name):
|
||||
return actor
|
||||
def find_character_in_room(room: Room, character_name: str) -> Character | None:
|
||||
for character in room.characters:
|
||||
if normalize_name(character.name) == normalize_name(character_name):
|
||||
return character
|
||||
|
||||
return None
|
||||
|
||||
|
@ -51,12 +51,12 @@ def find_actor_in_room(room: Room, actor_name: str) -> Actor | None:
|
|||
def find_item(
|
||||
world: World,
|
||||
item_name: str,
|
||||
include_actor_inventory=False,
|
||||
include_character_inventory=False,
|
||||
include_item_inventory=False,
|
||||
) -> Item | None:
|
||||
for room in world.rooms:
|
||||
item = find_item_in_room(
|
||||
room, item_name, include_actor_inventory, include_item_inventory
|
||||
room, item_name, include_character_inventory, include_item_inventory
|
||||
)
|
||||
if item:
|
||||
return item
|
||||
|
@ -64,14 +64,14 @@ def find_item(
|
|||
return None
|
||||
|
||||
|
||||
def find_item_in_actor(
|
||||
actor: Actor, item_name: str, include_item_inventory=False
|
||||
def find_item_in_character(
|
||||
character: Character, item_name: str, include_item_inventory=False
|
||||
) -> Item | None:
|
||||
return find_item_in_container(actor, item_name, include_item_inventory)
|
||||
return find_item_in_container(character, item_name, include_item_inventory)
|
||||
|
||||
|
||||
def find_item_in_container(
|
||||
container: Actor | Item, item_name: str, include_item_inventory=False
|
||||
container: Character | Item, item_name: str, include_item_inventory=False
|
||||
) -> Item | None:
|
||||
for item in container.items:
|
||||
if normalize_name(item.name) == normalize_name(item_name):
|
||||
|
@ -88,7 +88,7 @@ def find_item_in_container(
|
|||
def find_item_in_room(
|
||||
room: Room,
|
||||
item_name: str,
|
||||
include_actor_inventory=False,
|
||||
include_character_inventory=False,
|
||||
include_item_inventory=False,
|
||||
) -> Item | None:
|
||||
for item in room.items:
|
||||
|
@ -100,30 +100,30 @@ def find_item_in_room(
|
|||
if item:
|
||||
return item
|
||||
|
||||
if include_actor_inventory:
|
||||
for actor in room.actors:
|
||||
item = find_item_in_actor(actor, item_name, include_item_inventory)
|
||||
if include_character_inventory:
|
||||
for character in room.characters:
|
||||
item = find_item_in_character(character, item_name, include_item_inventory)
|
||||
if item:
|
||||
return item
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def find_room_with_actor(world: World, actor: Actor) -> Room | None:
|
||||
def find_room_with_character(world: World, character: Character) -> Room | None:
|
||||
for room in world.rooms:
|
||||
for room_actor in room.actors:
|
||||
if normalize_name(actor.name) == normalize_name(room_actor.name):
|
||||
for room_character in room.characters:
|
||||
if normalize_name(character.name) == normalize_name(room_character.name):
|
||||
return room
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def find_containing_room(world: World, entity: Room | Actor | Item) -> Room | None:
|
||||
def find_containing_room(world: World, entity: Room | Character | Item) -> Room | None:
|
||||
if isinstance(entity, Room):
|
||||
return entity
|
||||
|
||||
for room in world.rooms:
|
||||
if entity in room.actors or entity in room.items:
|
||||
if entity in room.characters or entity in room.items:
|
||||
return room
|
||||
|
||||
return None
|
||||
|
@ -139,8 +139,8 @@ def find_entity_reference(
|
|||
if reference.room:
|
||||
return find_room(world, reference.room)
|
||||
|
||||
if reference.actor:
|
||||
return find_actor(world, reference.actor)
|
||||
if reference.character:
|
||||
return find_character(world, reference.character)
|
||||
|
||||
if reference.item:
|
||||
return find_item(world, reference.item)
|
||||
|
@ -162,14 +162,14 @@ def list_portals(world: World) -> Generator[Portal, Any, None]:
|
|||
yield portal
|
||||
|
||||
|
||||
def list_actors(world: World) -> Generator[Actor, Any, None]:
|
||||
def list_characters(world: World) -> Generator[Character, Any, None]:
|
||||
for room in world.rooms:
|
||||
for actor in room.actors:
|
||||
yield actor
|
||||
for character in room.characters:
|
||||
yield character
|
||||
|
||||
|
||||
def list_items(
|
||||
world: World, include_actor_inventory=True, include_item_inventory=True
|
||||
world: World, include_character_inventory=True, include_item_inventory=True
|
||||
) -> Generator[Item, Any, None]:
|
||||
|
||||
for room in world.rooms:
|
||||
|
@ -179,21 +179,21 @@ def list_items(
|
|||
if include_item_inventory:
|
||||
yield from list_items_in_container(item)
|
||||
|
||||
if include_actor_inventory:
|
||||
for actor in room.actors:
|
||||
for item in actor.items:
|
||||
if include_character_inventory:
|
||||
for character in room.characters:
|
||||
for item in character.items:
|
||||
yield item
|
||||
|
||||
|
||||
def list_actors_in_room(room: Room) -> Generator[Actor, Any, None]:
|
||||
for actor in room.actors:
|
||||
yield actor
|
||||
def list_characters_in_room(room: Room) -> Generator[Character, Any, None]:
|
||||
for character in room.characters:
|
||||
yield character
|
||||
|
||||
|
||||
def list_items_in_actor(
|
||||
actor: Actor, include_item_inventory=True
|
||||
def list_items_in_character(
|
||||
character: Character, include_item_inventory=True
|
||||
) -> Generator[Item, Any, None]:
|
||||
for item in actor.items:
|
||||
for item in character.items:
|
||||
yield item
|
||||
|
||||
if include_item_inventory:
|
||||
|
@ -212,7 +212,7 @@ def list_items_in_container(
|
|||
|
||||
def list_items_in_room(
|
||||
room: Room,
|
||||
include_actor_inventory=True,
|
||||
include_character_inventory=True,
|
||||
include_item_inventory=True,
|
||||
) -> Generator[Item, Any, None]:
|
||||
for item in room.items:
|
||||
|
@ -221,7 +221,7 @@ def list_items_in_room(
|
|||
if include_item_inventory:
|
||||
yield from list_items_in_container(item)
|
||||
|
||||
if include_actor_inventory:
|
||||
for actor in room.actors:
|
||||
for item in actor.items:
|
||||
if include_character_inventory:
|
||||
for character in room.characters:
|
||||
for item in character.items:
|
||||
yield item
|
||||
|
|
|
@ -2,25 +2,26 @@ from logging import getLogger
|
|||
|
||||
from adventure.context import get_game_systems
|
||||
from adventure.game_system import FormatPerspective
|
||||
from adventure.models.entity import Actor, WorldEntity
|
||||
from adventure.models.entity import Character, WorldEntity
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
def describe_actor(
|
||||
actor: Actor, perspective: FormatPerspective = FormatPerspective.SECOND_PERSON
|
||||
def describe_character(
|
||||
character: Character,
|
||||
perspective: FormatPerspective = FormatPerspective.SECOND_PERSON,
|
||||
) -> str:
|
||||
attribute_descriptions = format_attributes(actor, perspective=perspective)
|
||||
logger.info("describing actor: %s, %s", actor, attribute_descriptions)
|
||||
attribute_descriptions = format_attributes(character, perspective=perspective)
|
||||
logger.info("describing character: %s, %s", character, attribute_descriptions)
|
||||
|
||||
if perspective == FormatPerspective.SECOND_PERSON:
|
||||
actor_description = actor.backstory
|
||||
character_description = character.backstory
|
||||
elif perspective == FormatPerspective.THIRD_PERSON:
|
||||
actor_description = actor.description
|
||||
character_description = character.description
|
||||
else:
|
||||
raise ValueError(f"Perspective {perspective} is not implemented")
|
||||
|
||||
return f"{actor_description} {attribute_descriptions}"
|
||||
return f"{character_description} {attribute_descriptions}"
|
||||
|
||||
|
||||
def describe_static(entity: WorldEntity) -> str:
|
||||
|
@ -32,8 +33,8 @@ def describe_entity(
|
|||
entity: WorldEntity,
|
||||
perspective: FormatPerspective = FormatPerspective.SECOND_PERSON,
|
||||
) -> str:
|
||||
if isinstance(entity, Actor):
|
||||
return describe_actor(entity, perspective)
|
||||
if isinstance(entity, Character):
|
||||
return describe_character(entity, perspective)
|
||||
|
||||
return describe_static(entity)
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ import useWebSocketModule from 'react-use-websocket';
|
|||
import { useStore } from 'zustand';
|
||||
|
||||
import { HistoryPanel } from './history.js';
|
||||
import { Actor } from './models.js';
|
||||
import { Character } from './models.js';
|
||||
import { PlayerPanel } from './player.js';
|
||||
import { Statusbar } from './status.js';
|
||||
import { StoreState, store } from './store.js';
|
||||
|
@ -52,15 +52,15 @@ export function App(props: AppProps) {
|
|||
sendMessage(JSON.stringify({ type: 'render', event }));
|
||||
}
|
||||
|
||||
function setPlayer(actor: Maybe<Actor>) {
|
||||
function setPlayer(character: Maybe<Character>) {
|
||||
// do not call setCharacter until the server confirms the player change
|
||||
if (doesExist(actor)) {
|
||||
sendMessage(JSON.stringify({ type: 'player', become: actor.name }));
|
||||
if (doesExist(character)) {
|
||||
sendMessage(JSON.stringify({ type: 'player', become: character.name }));
|
||||
}
|
||||
}
|
||||
|
||||
function sendInput(input: string) {
|
||||
const { character, setActiveTurn } = store.getState();
|
||||
const { playerCharacter: character, setActiveTurn } = store.getState();
|
||||
if (doesExist(character)) {
|
||||
sendMessage(JSON.stringify({ type: 'input', input }));
|
||||
setActiveTurn(false);
|
||||
|
@ -80,7 +80,7 @@ export function App(props: AppProps) {
|
|||
});
|
||||
|
||||
useEffect(() => {
|
||||
const { setClientId, setActiveTurn, setPlayers, appendEvent, setWorld, world, clientId, setCharacter } = store.getState();
|
||||
const { setClientId, setActiveTurn, setPlayers, appendEvent, setWorld, world, clientId, setPlayerCharacter: setCharacter } = store.getState();
|
||||
if (doesExist(lastMessage)) {
|
||||
const event = JSON.parse(lastMessage.data);
|
||||
|
||||
|
@ -98,8 +98,8 @@ export function App(props: AppProps) {
|
|||
case 'player':
|
||||
if (event.status === 'join' && doesExist(world) && event.client === clientId) {
|
||||
const { character: characterName } = event;
|
||||
const actor = world.rooms.flatMap((room) => room.actors).find((a) => a.name === characterName);
|
||||
setCharacter(actor);
|
||||
const character = world.rooms.flatMap((room) => room.characters).find((a) => a.name === characterName);
|
||||
setCharacter(character);
|
||||
}
|
||||
break;
|
||||
case 'players':
|
||||
|
|
|
@ -21,11 +21,11 @@ import {
|
|||
import { instance as graphviz } from '@viz-js/viz';
|
||||
import React, { Fragment, useEffect } from 'react';
|
||||
import { useStore } from 'zustand';
|
||||
import { Actor, Attributes, Item, Portal, Room, World } from './models';
|
||||
import { Character, Attributes, Item, Portal, Room, World } from './models';
|
||||
import { StoreState, store } from './store';
|
||||
|
||||
export interface EntityDetailsProps {
|
||||
entity: Maybe<Item | Actor | Portal | Room>;
|
||||
entity: Maybe<Item | Character | Portal | Room>;
|
||||
onClose: () => void;
|
||||
onRender: (type: string, entity: string) => void;
|
||||
}
|
||||
|
@ -43,10 +43,10 @@ export function EntityDetails(props: EntityDetailsProps) {
|
|||
let attributes: Attributes = {};
|
||||
let planner;
|
||||
|
||||
if (type === 'actor') {
|
||||
const actor = entity as Actor;
|
||||
attributes = actor.attributes;
|
||||
planner = actor.planner;
|
||||
if (type === 'character') {
|
||||
const character = entity as Character;
|
||||
attributes = character.attributes;
|
||||
planner = character.planner;
|
||||
}
|
||||
|
||||
if (type === 'item') {
|
||||
|
@ -155,7 +155,7 @@ export function DetailDialog(props: DetailDialogProps) {
|
|||
>{details}</Dialog>;
|
||||
}
|
||||
|
||||
export function isWorld(entity: Maybe<Item | Actor | Portal | Room | World>): entity is World {
|
||||
export function isWorld(entity: Maybe<Item | Character | Portal | Room | World>): entity is World {
|
||||
return doesExist(entity) && doesExist(Object.getOwnPropertyDescriptor(entity, 'theme'));
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import { Maybe, doesExist } from '@apextoaster/js-utils';
|
|||
import { Camera, Settings } from '@mui/icons-material';
|
||||
import { useStore } from 'zustand';
|
||||
import { formatters } from './format.js';
|
||||
import { Actor } from './models.js';
|
||||
import { Character } from './models.js';
|
||||
import { StoreState, store } from './store.js';
|
||||
|
||||
export function openImage(image: string) {
|
||||
|
@ -32,11 +32,11 @@ export interface EventItemProps {
|
|||
|
||||
export function characterSelector(state: StoreState) {
|
||||
return {
|
||||
character: state.character,
|
||||
playerCharacter: state.playerCharacter,
|
||||
};
|
||||
}
|
||||
|
||||
export function sameCharacter(a: Maybe<Actor>, b: Maybe<Actor>): boolean {
|
||||
export function sameCharacter(a: Maybe<Character>, b: Maybe<Character>): boolean {
|
||||
if (doesExist(a) && doesExist(b)) {
|
||||
return a.name === b.name;
|
||||
}
|
||||
|
@ -46,13 +46,13 @@ export function sameCharacter(a: Maybe<Actor>, b: Maybe<Actor>): boolean {
|
|||
|
||||
export function ActionEventItem(props: EventItemProps) {
|
||||
const { event, renderEvent } = props;
|
||||
const { id, actor, room, type } = event;
|
||||
const { id, character, room, type } = event;
|
||||
const content = formatters[type](event);
|
||||
|
||||
const state = useStore(store, characterSelector);
|
||||
const { character } = state;
|
||||
const { playerCharacter } = state;
|
||||
|
||||
const playerAction = sameCharacter(actor, character);
|
||||
const playerAction = sameCharacter(character, playerCharacter);
|
||||
const typographyProps = {
|
||||
color: playerAction ? 'success.text' : 'primary.text',
|
||||
};
|
||||
|
@ -81,7 +81,7 @@ export function ActionEventItem(props: EventItemProps) {
|
|||
variant="body2"
|
||||
color="text.primary"
|
||||
>
|
||||
{actor.name}
|
||||
{character.name}
|
||||
</Typography>
|
||||
{content}
|
||||
</React.Fragment>
|
||||
|
@ -220,7 +220,7 @@ export function PromptEventItem(props: EventItemProps) {
|
|||
const { character, prompt } = event;
|
||||
|
||||
const state = useStore(store, characterSelector);
|
||||
const { character: playerCharacter } = state;
|
||||
const { playerCharacter: playerCharacter } = state;
|
||||
|
||||
const playerPrompt = sameCharacter(playerCharacter, character);
|
||||
const typographyProps = {
|
||||
|
|
|
@ -17,8 +17,8 @@ export interface Item {
|
|||
attributes: Attributes;
|
||||
}
|
||||
|
||||
export interface Actor {
|
||||
type: 'actor';
|
||||
export interface Character {
|
||||
type: 'character';
|
||||
name: string;
|
||||
backstory: string;
|
||||
description: string;
|
||||
|
@ -38,7 +38,7 @@ export interface Room {
|
|||
type: 'room';
|
||||
name: string;
|
||||
description: string;
|
||||
actors: Array<Actor>;
|
||||
characters: Array<Character>;
|
||||
items: Array<Item>;
|
||||
portals: Array<Portal>;
|
||||
attributes: Attributes;
|
||||
|
|
|
@ -10,7 +10,7 @@ export interface PlayerPanelProps {
|
|||
|
||||
export function playerStateSelector(s: StoreState) {
|
||||
return {
|
||||
character: s.character,
|
||||
character: s.playerCharacter,
|
||||
activeTurn: s.activeTurn,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { createStore, StateCreator } from 'zustand';
|
|||
import { doesExist, Maybe } from '@apextoaster/js-utils';
|
||||
import { PaletteMode } from '@mui/material';
|
||||
import { ReadyState } from 'react-use-websocket';
|
||||
import { Actor, GameEvent, Item, Portal, Room, World } from './models';
|
||||
import { Character, GameEvent, Item, Portal, Room, World } from './models';
|
||||
|
||||
export type LayoutMode = 'horizontal' | 'vertical';
|
||||
|
||||
|
@ -12,7 +12,7 @@ export interface ClientState {
|
|||
autoScroll: boolean;
|
||||
clientId: string;
|
||||
clientName: string;
|
||||
detailEntity: Maybe<Item | Actor | Portal | Room | World>;
|
||||
detailEntity: Maybe<Item | Character | Portal | Room | World>;
|
||||
eventHistory: Array<GameEvent>;
|
||||
layoutMode: LayoutMode;
|
||||
readyState: ReadyState;
|
||||
|
@ -22,7 +22,7 @@ export interface ClientState {
|
|||
setAutoScroll: (autoScroll: boolean) => void;
|
||||
setClientId: (clientId: string) => void;
|
||||
setClientName: (name: string) => void;
|
||||
setDetailEntity: (entity: Maybe<Item | Actor | Portal | Room | World>) => void;
|
||||
setDetailEntity: (entity: Maybe<Item | Character | Portal | Room | World>) => void;
|
||||
setLayoutMode: (mode: LayoutMode) => void;
|
||||
setReadyState: (state: ReadyState) => void;
|
||||
setThemeMode: (mode: PaletteMode) => void;
|
||||
|
@ -44,11 +44,11 @@ export interface WorldState {
|
|||
|
||||
export interface PlayerState {
|
||||
activeTurn: boolean;
|
||||
character: Maybe<Actor>;
|
||||
playerCharacter: Maybe<Character>;
|
||||
|
||||
// setters
|
||||
setActiveTurn: (activeTurn: boolean) => void;
|
||||
setCharacter: (character: Maybe<Actor>) => void;
|
||||
setPlayerCharacter: (character: Maybe<Character>) => void;
|
||||
|
||||
// misc helpers
|
||||
isPlaying: () => boolean;
|
||||
|
@ -114,11 +114,11 @@ export function createWorldStore(): StateCreator<WorldState> {
|
|||
export function createPlayerStore(): StateCreator<PlayerState> {
|
||||
return (set) => ({
|
||||
activeTurn: false,
|
||||
character: undefined,
|
||||
playerCharacter: undefined,
|
||||
setActiveTurn: (activeTurn: boolean) => set({ activeTurn }),
|
||||
setCharacter: (character: Maybe<Actor>) => set({ character }),
|
||||
setPlayerCharacter: (character: Maybe<Character>) => set({ playerCharacter: character }),
|
||||
isPlaying() {
|
||||
return doesExist(this.character);
|
||||
return doesExist(this.playerCharacter);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -6,10 +6,10 @@ import React from 'react';
|
|||
|
||||
import { useStore } from 'zustand';
|
||||
import { StoreState, store } from './store';
|
||||
import { Actor, Item, Portal, Room } from './models';
|
||||
import { Character, Item, Portal, Room } from './models';
|
||||
|
||||
export type SetDetails = (entity: Maybe<Item | Actor | Room>) => void;
|
||||
export type SetPlayer = (actor: Maybe<Actor>) => void;
|
||||
export type SetDetails = (entity: Maybe<Item | Character | Room>) => void;
|
||||
export type SetPlayer = (character: Maybe<Character>) => void;
|
||||
|
||||
export interface BaseEntityItemProps {
|
||||
setPlayer: SetPlayer;
|
||||
|
@ -25,14 +25,14 @@ export function formatLabel(name: string, active = false): string {
|
|||
|
||||
export function itemStateSelector(s: StoreState) {
|
||||
return {
|
||||
character: s.character,
|
||||
playerCharacter: s.playerCharacter,
|
||||
setDetailEntity: s.setDetailEntity,
|
||||
};
|
||||
}
|
||||
|
||||
export function actorStateSelector(s: StoreState) {
|
||||
export function characterStateSelector(s: StoreState) {
|
||||
return {
|
||||
character: s.character,
|
||||
playerCharacter: s.playerCharacter,
|
||||
players: s.players,
|
||||
setDetailEntity: s.setDetailEntity,
|
||||
};
|
||||
|
@ -65,33 +65,33 @@ export function ItemItem(props: { item: Item } & BaseEntityItemProps) {
|
|||
</TreeItem>;
|
||||
}
|
||||
|
||||
export function ActorItem(props: { actor: Actor } & BaseEntityItemProps) {
|
||||
const { actor, setPlayer } = props;
|
||||
const state = useStore(store, actorStateSelector);
|
||||
const { character, players, setDetailEntity } = state;
|
||||
export function CharacterItem(props: { character: Character } & BaseEntityItemProps) {
|
||||
const { character, setPlayer } = props;
|
||||
const state = useStore(store, characterStateSelector);
|
||||
const { playerCharacter, players, setDetailEntity } = state;
|
||||
|
||||
const activeSelf = doesExist(character) && actor.name === character.name;
|
||||
const activeOther = Object.values(players).some((it) => it === actor.name); // TODO: are these the keys or the values?
|
||||
const label = formatLabel(actor.name, activeSelf);
|
||||
const activeSelf = doesExist(playerCharacter) && character.name === playerCharacter.name;
|
||||
const activeOther = Object.values(players).some((it) => it === character.name); // TODO: are these the keys or the values?
|
||||
const label = formatLabel(character.name, activeSelf);
|
||||
|
||||
let playButton;
|
||||
if (activeSelf) {
|
||||
playButton = <TreeItem itemId={`${actor.name}-stop`} label="Stop playing" onClick={() => setPlayer(undefined)} />;
|
||||
playButton = <TreeItem itemId={`${character.name}-stop`} label="Stop playing" onClick={() => setPlayer(undefined)} />;
|
||||
} else {
|
||||
if (activeOther) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const player = Object.entries(players).find((it) => it[1] === actor.name)?.[0];
|
||||
playButton = <TreeItem itemId={`${actor.name}-taken`} label={`Played by ${player}`} />;
|
||||
const player = Object.entries(players).find((it) => it[1] === character.name)?.[0];
|
||||
playButton = <TreeItem itemId={`${character.name}-taken`} label={`Played by ${player}`} />;
|
||||
} else {
|
||||
playButton = <TreeItem itemId={`${actor.name}-play`} label="Play!" onClick={() => setPlayer(actor)} />;
|
||||
playButton = <TreeItem itemId={`${character.name}-play`} label="Play!" onClick={() => setPlayer(character)} />;
|
||||
}
|
||||
}
|
||||
|
||||
return <TreeItem itemId={actor.name} label={label}>
|
||||
return <TreeItem itemId={character.name} label={label}>
|
||||
{playButton}
|
||||
<TreeItem itemId={`${actor.name}-details`} label="Details" onClick={() => setDetailEntity(actor)} />
|
||||
<TreeItem itemId={`${actor.name}-items`} label="Items">
|
||||
{actor.items.map((item) => <ItemItem key={item.name} item={item} setPlayer={setPlayer} />)}
|
||||
<TreeItem itemId={`${character.name}-details`} label="Details" onClick={() => setDetailEntity(character)} />
|
||||
<TreeItem itemId={`${character.name}-items`} label="Items">
|
||||
{character.items.map((item) => <ItemItem key={item.name} item={item} setPlayer={setPlayer} />)}
|
||||
</TreeItem>
|
||||
</TreeItem>;
|
||||
}
|
||||
|
@ -99,15 +99,15 @@ export function ActorItem(props: { actor: Actor } & BaseEntityItemProps) {
|
|||
export function RoomItem(props: { room: Room } & BaseEntityItemProps) {
|
||||
const { room, setPlayer } = props;
|
||||
const state = useStore(store, itemStateSelector);
|
||||
const { character, setDetailEntity } = state;
|
||||
const { playerCharacter, setDetailEntity } = state;
|
||||
|
||||
const active = doesExist(character) && room.actors.some((it) => it.name === character.name);
|
||||
const active = doesExist(playerCharacter) && room.characters.some((it) => it.name === playerCharacter.name);
|
||||
const label = formatLabel(room.name, active);
|
||||
|
||||
return <TreeItem itemId={room.name} label={label}>
|
||||
<TreeItem itemId={`${room.name}-details`} label="Details" onClick={() => setDetailEntity(room)} />
|
||||
<TreeItem itemId={`${room.name}-actors`} label="Actors">
|
||||
{room.actors.map((actor) => <ActorItem key={actor.name} actor={actor} setPlayer={setPlayer} />)}
|
||||
<TreeItem itemId={`${room.name}-characters`} label="Characters">
|
||||
{room.characters.map((character) => <CharacterItem key={character.name} character={character} setPlayer={setPlayer} />)}
|
||||
</TreeItem>
|
||||
<TreeItem itemId={`${room.name}-items`} label="Items">
|
||||
{room.items.map((item) => <ItemItem key={item.name} item={item} setPlayer={setPlayer} />)}
|
||||
|
|
|
@ -28,7 +28,7 @@ server:
|
|||
port: 8001
|
||||
world:
|
||||
size:
|
||||
actor_items:
|
||||
character_items:
|
||||
min: 0
|
||||
max: 3
|
||||
item_effects:
|
||||
|
@ -40,7 +40,7 @@ world:
|
|||
rooms:
|
||||
min: 3
|
||||
max: 6
|
||||
room_actors:
|
||||
room_characters:
|
||||
min: 1
|
||||
max: 3
|
||||
room_items:
|
||||
|
|
|
@ -45,5 +45,5 @@ additional configuration from a YAML or JSON file.
|
|||
|
||||
- figure out the human input syntax for actions
|
||||
- make an admin panel in web UI
|
||||
- store long-term memory for actors in a vector DB (RAG and all that)
|
||||
- store long-term memory for characters in a vector DB (RAG and all that)
|
||||
- generate and simulate should probably be async
|
||||
|
|
|
@ -30,10 +30,10 @@ mechanics to suit different types of adventures and narrative styles.
|
|||
|
||||
### What kinds of entities exist in the world?
|
||||
|
||||
In the immersive world of TaleWeave AI, entities are categorized into Rooms, Actors, and Items, each playing a vital
|
||||
In the immersive world of TaleWeave AI, entities are categorized into Rooms, Characters, and Items, each playing a vital
|
||||
role in crafting the narrative and gameplay experience. Rooms serve as the fundamental spatial units where the story
|
||||
unfolds, each containing various Actors and potentially multiple Items. Actors, representing characters in the game,
|
||||
possess inventories that hold Items, which are objects that can be interacted with or utilized by the Actors. Currently,
|
||||
unfolds, each containing various Characters and potentially multiple Items. Characters, representing characters in the game,
|
||||
possess inventories that hold Items, which are objects that can be interacted with or utilized by the Characters. Currently,
|
||||
TaleWeave AI does not support Containers—Items that can hold other Items—but the structure is designed to support
|
||||
complex interactions within and between these entity types, laying the groundwork for a deeply interactive environment.
|
||||
|
||||
|
@ -41,15 +41,15 @@ complex interactions within and between these entity types, laying the groundwor
|
|||
|
||||
Actions in TaleWeave AI are defined as Python functions that enable both human players and AI-driven characters to
|
||||
interact with the game world. These actions, which include behaviors like taking an item or moving between rooms, are
|
||||
integral to advancing the gameplay and affecting the state of the world. Each actor is permitted one action per round,
|
||||
which can significantly alter the attributes of entities, reposition entities between rooms or actors, or modify the
|
||||
integral to advancing the gameplay and affecting the state of the world. Each character is permitted one action per round,
|
||||
which can significantly alter the attributes of entities, reposition entities between rooms or characters, or modify the
|
||||
game world by adding or removing entities. This framework ensures that every turn is meaningful and that players'
|
||||
decisions have direct consequences on the game's progression and outcome.
|
||||
|
||||
### What are attributes?
|
||||
|
||||
Attributes in TaleWeave AI are key-value pairs that define the properties of an entity. These attributes can be of
|
||||
various types—boolean, number, or string—such as an actor’s mood being "happy," their health being quantified as 10, or
|
||||
various types—boolean, number, or string—such as a character's mood being "happy," their health being quantified as 10, or
|
||||
an item's quality described as "broken" or quantified with a remaining usage of 3. Attributes play a crucial role in the
|
||||
game's logic system by influencing how entities react under different conditions. They are actively used to trigger
|
||||
specific rules within the game, and their labels are included in prompts to guide language model players in making
|
||||
|
|
|
@ -77,7 +77,7 @@ added to the world.
|
|||
```yaml
|
||||
type: "generate"
|
||||
name: string
|
||||
entity: Room | Actor | Item | None
|
||||
entity: Room | Character | Item | None
|
||||
```
|
||||
|
||||
Two `generate` events will be fired for each entity. The first event will *not* have an `entity` set, only the `name`.
|
||||
|
@ -88,14 +88,14 @@ more frequent progress updates when generating with slow models.
|
|||
|
||||
### Action Events
|
||||
|
||||
The action event is fired after player or actor input has been processed and any JSON function calls have been parsed.
|
||||
The action event is fired after player or character input has been processed and any JSON function calls have been parsed.
|
||||
|
||||
```yaml
|
||||
type: "action"
|
||||
action: string
|
||||
parameters: dict
|
||||
room: Room
|
||||
actor: Actor
|
||||
character: Character
|
||||
item: Item | None
|
||||
```
|
||||
|
||||
|
@ -107,7 +107,7 @@ The prompt event is fired when a character's turn starts and their input is need
|
|||
type: "prompt"
|
||||
prompt: string
|
||||
room: Room
|
||||
actor: Actor
|
||||
character: Character
|
||||
```
|
||||
|
||||
### Reply Events
|
||||
|
@ -118,7 +118,7 @@ The reply event is fired when a character has been asked a question or told a me
|
|||
type: "reply"
|
||||
text: string
|
||||
room: Room
|
||||
actor: Actor
|
||||
character: Character
|
||||
```
|
||||
|
||||
### Result Events
|
||||
|
@ -129,10 +129,10 @@ The result event is fired after a character has taken an action and contains the
|
|||
type: "result"
|
||||
result: string
|
||||
room: Room
|
||||
actor: Actor
|
||||
character: Character
|
||||
```
|
||||
|
||||
The result is related to the most recent action for the same actor, although not every action will have a result - they
|
||||
The result is related to the most recent action for the same character, although not every action will have a result - they
|
||||
may have a reply or error instead.
|
||||
|
||||
### Status Events
|
||||
|
@ -143,7 +143,7 @@ The status event is fired for general events in the world and messages about oth
|
|||
type: "status"
|
||||
text: string
|
||||
room: Room | None
|
||||
actor: Actor | None
|
||||
character: Character | None
|
||||
```
|
||||
|
||||
### Snapshot Events
|
||||
|
|
|
@ -24,6 +24,8 @@
|
|||
- [Player leaves the game during their turn](#player-leaves-the-game-during-their-turn)
|
||||
- [Spectator renders a recent event](#spectator-renders-a-recent-event)
|
||||
- [User Stories for Developers](#user-stories-for-developers)
|
||||
- [ML Engineer collects prompts and actions to fine-tune a character model](#ml-engineer-collects-prompts-and-actions-to-fine-tune-a-character-model)
|
||||
- [ML Engineer collects character movement and conversations to chart emergent behavior](#ml-engineer-collects-character-movement-and-conversations-to-chart-emergent-behavior)
|
||||
- [Mod Developer creates a new system for the game](#mod-developer-creates-a-new-system-for-the-game)
|
||||
- [Project Contributor fixes a bug in the engine](#project-contributor-fixes-a-bug-in-the-engine)
|
||||
|
||||
|
@ -174,9 +176,20 @@ Skills:
|
|||
|
||||
### User Stories for Developers
|
||||
|
||||
#### ML Engineer collects prompts and actions to fine-tune a character model
|
||||
|
||||
> As an ML engineer, I want to collect data from the game history, especially prompts, actions, and their results, so
|
||||
> that I can fine-tune an LLM to be a better model for characters.
|
||||
|
||||
#### ML Engineer collects character movement and conversations to chart emergent behavior
|
||||
|
||||
> As an ML engineer, I want to collect data from the game history, especially character movement, interactions, and
|
||||
> conversations, so that I can graph their movements and discover any emergent behavior in the game world.
|
||||
|
||||
#### Mod Developer creates a new system for the game
|
||||
|
||||
> As a Mod Developer, I want to write a Python module with a new game system and load it into a test world, so that I can develop custom features.
|
||||
> As a Mod Developer, I want to write a Python module with a new game system and load it into a test world, so that I
|
||||
> can develop custom features.
|
||||
|
||||
#### Project Contributor fixes a bug in the engine
|
||||
|
||||
|
|
Loading…
Reference in New Issue