add thought stage with planning, add quest system
This commit is contained in:
parent
2aaf531454
commit
a3cb7c3e4b
|
@ -84,7 +84,9 @@ def action_move(direction: str) -> str:
|
|||
if not destination_room:
|
||||
return f"The {portal.destination} room does not exist."
|
||||
|
||||
broadcast(f"{action_actor.name} moves {direction} to {destination_room.name}")
|
||||
broadcast(
|
||||
f"{action_actor.name} moves through {direction} to {destination_room.name}"
|
||||
)
|
||||
action_room.actors.remove(action_actor)
|
||||
destination_room.actors.append(action_actor)
|
||||
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
from adventure.context import action_context, get_current_step
|
||||
from adventure.models.planning import CalendarEvent
|
||||
|
||||
|
||||
def take_note(fact: str):
|
||||
"""
|
||||
Remember a fact by recording it in your notes. Facts are critical information about yourself and others that you
|
||||
have learned during your adventures. You can review your notes at any time to help you make decisions.
|
||||
|
||||
Args:
|
||||
fact: The fact to remember.
|
||||
"""
|
||||
|
||||
with action_context() as (_, action_actor):
|
||||
if fact in action_actor.planner.notes:
|
||||
return "You already know that."
|
||||
|
||||
action_actor.planner.notes.append(fact)
|
||||
return "You make a note of that."
|
||||
|
||||
|
||||
def read_notes(unused: bool, count: int = 10):
|
||||
"""
|
||||
Read your notes to review the facts that you have learned during your adventures.
|
||||
|
||||
Args:
|
||||
count: The number of recent notes to read. 10 is usually a good number.
|
||||
"""
|
||||
|
||||
facts = get_recent_notes(count=count)
|
||||
return "\n".join(facts)
|
||||
|
||||
|
||||
def erase_notes(prefix: str) -> str:
|
||||
"""
|
||||
Erase notes that start with a specific prefix.
|
||||
|
||||
Args:
|
||||
prefix: The prefix to match notes against.
|
||||
"""
|
||||
|
||||
with action_context() as (_, action_actor):
|
||||
matches = [
|
||||
note for note in action_actor.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
|
||||
]
|
||||
return f"Erased {len(matches)} notes."
|
||||
|
||||
|
||||
def replace_note(old: str, new: str) -> str:
|
||||
"""
|
||||
Replace a note with a new note.
|
||||
|
||||
Args:
|
||||
old: The old note to replace.
|
||||
new: The new note to replace it with.
|
||||
"""
|
||||
|
||||
with action_context() as (_, action_actor):
|
||||
if old not in action_actor.planner.notes:
|
||||
return "Note not found."
|
||||
|
||||
action_actor.planner.notes[:] = [
|
||||
new if note == old else note for note in action_actor.planner.notes
|
||||
]
|
||||
return "Note replaced."
|
||||
|
||||
|
||||
def schedule_event(name: str, turns: int):
|
||||
"""
|
||||
Schedule an event to happen at a specific turn. Events are important occurrences that can affect the world in
|
||||
significant ways. You will be notified about upcoming events so you can plan accordingly.
|
||||
|
||||
Args:
|
||||
name: The name of the event.
|
||||
turns: The number of turns until the event happens.
|
||||
"""
|
||||
|
||||
with action_context() as (_, action_actor):
|
||||
# TODO: check for existing events with the same name
|
||||
event = CalendarEvent(name, turns)
|
||||
action_actor.planner.calendar.events.append(event)
|
||||
return f"{name} is scheduled to happen in {turns} turns."
|
||||
|
||||
|
||||
def read_calendar(unused: bool, count: int = 10):
|
||||
"""
|
||||
Read your calendar to see upcoming events that you have scheduled.
|
||||
"""
|
||||
|
||||
current_turn = get_current_step()
|
||||
|
||||
with action_context() as (_, action_actor):
|
||||
events = action_actor.planner.calendar.events[:count]
|
||||
return "\n".join(
|
||||
[
|
||||
f"{event.name} will happen in {event.turn - current_turn} turns"
|
||||
for event in events
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def get_upcoming_events(turns: int = 3):
|
||||
"""
|
||||
Get a list of upcoming events within a certain number of turns.
|
||||
|
||||
Args:
|
||||
turns: The number of turns to look ahead for events.
|
||||
"""
|
||||
|
||||
current_turn = get_current_step()
|
||||
|
||||
with action_context() as (_, action_actor):
|
||||
calendar = action_actor.planner.calendar
|
||||
# TODO: sort events by turn
|
||||
return [
|
||||
event for event in calendar.events if event.turn - current_turn <= turns
|
||||
]
|
||||
|
||||
|
||||
def get_recent_notes(count: int = 3):
|
||||
"""
|
||||
Get the most recent facts from your notes.
|
||||
|
||||
Args:
|
||||
history: The number of recent facts to retrieve.
|
||||
"""
|
||||
|
||||
with action_context() as (_, action_actor):
|
||||
return action_actor.planner.notes[-count:]
|
|
@ -0,0 +1,58 @@
|
|||
from adventure.context import action_context, get_system_data
|
||||
from adventure.systems.quest import (
|
||||
QUEST_SYSTEM,
|
||||
complete_quest,
|
||||
get_active_quest,
|
||||
get_quests_for_actor,
|
||||
set_active_quest,
|
||||
)
|
||||
from adventure.utils.search import find_actor_in_room
|
||||
|
||||
|
||||
def accept_quest(actor: str, quest: str) -> str:
|
||||
"""
|
||||
Accept and start a quest being given by another character.
|
||||
"""
|
||||
|
||||
with action_context() as (action_room, action_actor):
|
||||
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."
|
||||
|
||||
available_quests = get_quests_for_actor(quests, target_actor)
|
||||
|
||||
for available_quest in available_quests:
|
||||
if available_quest.name == quest:
|
||||
set_active_quest(quests, action_actor, available_quest)
|
||||
return f"You have accepted the quest: {quest}"
|
||||
|
||||
return f"{actor} does not have the quest: {quest}"
|
||||
|
||||
|
||||
def submit_quest(actor: 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):
|
||||
quests = get_system_data(QUEST_SYSTEM)
|
||||
if not quests:
|
||||
return "No quests available."
|
||||
|
||||
active_quest = get_active_quest(quests, action_actor)
|
||||
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."
|
||||
|
||||
if active_quest.giver.actor == target_actor.name:
|
||||
complete_quest(quests, action_actor, active_quest)
|
||||
return f"You have completed the quest: {active_quest.name}"
|
||||
|
||||
return f"{actor} is not the quest giver for your active quest."
|
|
@ -2,6 +2,7 @@ from contextlib import contextmanager
|
|||
from logging import getLogger
|
||||
from types import UnionType
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
List,
|
||||
|
@ -33,6 +34,7 @@ dungeon_master: Agent | None = None
|
|||
# game context
|
||||
event_emitter = EventEmitter()
|
||||
game_systems: List[GameSystem] = []
|
||||
system_data: Dict[str, Any] = {}
|
||||
|
||||
|
||||
# TODO: where should this one go?
|
||||
|
@ -155,6 +157,10 @@ def get_game_systems() -> List[GameSystem]:
|
|||
return game_systems
|
||||
|
||||
|
||||
def get_system_data(system: str) -> Any | None:
|
||||
return system_data.get(system)
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
|
@ -193,6 +199,10 @@ def set_game_systems(systems: Sequence[GameSystem]):
|
|||
game_systems = list(systems)
|
||||
|
||||
|
||||
def set_system_data(system: str, data: Any):
|
||||
system_data[system] = data
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
from enum import Enum
|
||||
from typing import Protocol
|
||||
from typing import Any, Callable, Protocol
|
||||
|
||||
from packit.agent import Agent
|
||||
|
||||
from adventure.models.entity import World, WorldEntity
|
||||
from adventure.utils import format_callable
|
||||
|
||||
|
||||
class FormatPerspective(Enum):
|
||||
|
@ -31,32 +30,59 @@ class SystemGenerate(Protocol):
|
|||
...
|
||||
|
||||
|
||||
class SystemInitialize(Protocol):
|
||||
def __call__(self, world: World) -> Any:
|
||||
"""
|
||||
Initialize the system for the given world.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class SystemSimulate(Protocol):
|
||||
def __call__(self, world: World, step: int) -> None:
|
||||
def __call__(self, world: World, step: int, data: Any | None = None) -> None:
|
||||
"""
|
||||
Simulate the world for the given step.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class SystemData:
|
||||
load: Callable[[str], Any]
|
||||
save: Callable[[str, Any], None]
|
||||
|
||||
def __init__(self, load: Callable[[str], Any], save: Callable[[str, Any], None]):
|
||||
self.load = load
|
||||
self.save = save
|
||||
|
||||
|
||||
class GameSystem:
|
||||
name: str
|
||||
data: SystemData | None = None
|
||||
format: SystemFormat | None = None
|
||||
generate: SystemGenerate | None = None
|
||||
initialize: SystemInitialize | None = None
|
||||
simulate: SystemSimulate | None = None
|
||||
# render: TODO
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
*,
|
||||
data: SystemData | None = None,
|
||||
format: SystemFormat | None = None,
|
||||
generate: SystemGenerate | None = None,
|
||||
initialize: SystemInitialize | None = None,
|
||||
simulate: SystemSimulate | None = None,
|
||||
):
|
||||
self.name = name
|
||||
self.data = data
|
||||
self.format = format
|
||||
self.generate = generate
|
||||
self.initialize = initialize
|
||||
self.simulate = simulate
|
||||
|
||||
def __str__(self):
|
||||
return f"GameSystem(format={format_callable(self.format)}, generate={format_callable(self.generate)}, simulate={format_callable(self.simulate)})"
|
||||
return f"GameSystem({self.name})"
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
|
|
|
@ -7,7 +7,7 @@ from packit.loops import loop_retry
|
|||
from packit.results import enum_result, int_result
|
||||
from packit.utils import could_be_json
|
||||
|
||||
from adventure.context import broadcast, set_current_world
|
||||
from adventure.context import broadcast, set_current_world, set_system_data
|
||||
from adventure.game_system import GameSystem
|
||||
from adventure.models.config import DEFAULT_CONFIG, WorldConfig
|
||||
from adventure.models.effect import (
|
||||
|
@ -74,6 +74,7 @@ def generate_system_attributes(
|
|||
) -> None:
|
||||
for system in systems:
|
||||
if system.generate:
|
||||
# TODO: pass the whole world
|
||||
system.generate(agent, world.theme, entity)
|
||||
|
||||
|
||||
|
@ -423,6 +424,7 @@ def generate_effect(agent: Agent, world: World, entity: Item) -> EffectPattern:
|
|||
"name": name,
|
||||
},
|
||||
result_parser=int_result,
|
||||
toolbox=None,
|
||||
)
|
||||
|
||||
def parse_application(value: str, **kwargs) -> str:
|
||||
|
@ -447,6 +449,7 @@ def generate_effect(agent: Agent, world: World, entity: Item) -> EffectPattern:
|
|||
"name": name,
|
||||
},
|
||||
result_parser=parse_application,
|
||||
toolbox=None,
|
||||
)
|
||||
|
||||
return EffectPattern(
|
||||
|
@ -522,6 +525,12 @@ def generate_world(
|
|||
world = World(name=name, rooms=[], theme=theme, order=[])
|
||||
set_current_world(world)
|
||||
|
||||
# initialize the systems
|
||||
for system in systems:
|
||||
if system.initialize:
|
||||
data = system.initialize(world)
|
||||
set_system_data(system.name, data)
|
||||
|
||||
# generate the rooms
|
||||
for _ in range(room_count):
|
||||
try:
|
||||
|
|
|
@ -6,14 +6,9 @@ from typing import List
|
|||
from dotenv import load_dotenv
|
||||
from packit.agent import Agent, agent_easy_connect
|
||||
from packit.utils import logger_with_colors
|
||||
from yaml import Loader, load
|
||||
|
||||
from adventure.context import subscribe
|
||||
|
||||
|
||||
def load_yaml(file):
|
||||
return load(file, Loader=Loader)
|
||||
|
||||
from adventure.context import get_system_data, set_system_data
|
||||
from adventure.utils.file import load_yaml
|
||||
|
||||
# configure logging
|
||||
LOG_PATH = "logging.json"
|
||||
|
@ -34,7 +29,7 @@ logger = logger_with_colors(__name__) # , level="DEBUG")
|
|||
load_dotenv(environ.get("ADVENTURE_ENV", ".env"), override=True)
|
||||
|
||||
if True:
|
||||
from adventure.context import set_current_step, set_dungeon_master
|
||||
from adventure.context import set_current_step, set_dungeon_master, subscribe
|
||||
from adventure.game_system import GameSystem
|
||||
from adventure.generate import generate_world
|
||||
from adventure.models.config import DEFAULT_CONFIG, Config
|
||||
|
@ -176,7 +171,34 @@ def get_world_prompt(args) -> WorldPrompt:
|
|||
)
|
||||
|
||||
|
||||
def load_or_generate_world(args, players, systems, world_prompt: WorldPrompt):
|
||||
def load_or_initialize_system_data(args, systems: List[GameSystem], world: World):
|
||||
for system in systems:
|
||||
if system.data:
|
||||
system_data_file = f"{args.world}.{system.name}.json"
|
||||
|
||||
data = None
|
||||
if path.exists(system_data_file):
|
||||
logger.info(f"loading system data from {system_data_file}")
|
||||
data = system.data.load(system_data_file)
|
||||
else:
|
||||
logger.info(f"no system data found at {system_data_file}")
|
||||
if system.initialize:
|
||||
data = system.initialize(world)
|
||||
|
||||
set_system_data(system.name, data)
|
||||
|
||||
|
||||
def save_system_data(args, systems: List[GameSystem]):
|
||||
for system in systems:
|
||||
if system.data:
|
||||
system_data_file = f"{args.world}.{system.name}.json"
|
||||
logger.info(f"saving system data to {system_data_file}")
|
||||
system.data.save(system_data_file, get_system_data(system.name))
|
||||
|
||||
|
||||
def load_or_generate_world(
|
||||
args, players, systems: List[GameSystem], world_prompt: WorldPrompt
|
||||
):
|
||||
world_file = args.world + ".json"
|
||||
world_state_file = args.state or (args.world + ".state.json")
|
||||
|
||||
|
@ -187,6 +209,7 @@ def load_or_generate_world(args, players, systems, world_prompt: WorldPrompt):
|
|||
state = WorldState(**load_yaml(f))
|
||||
|
||||
set_current_step(state.step)
|
||||
load_or_initialize_system_data(args, systems, state.world)
|
||||
|
||||
memory = state.memory
|
||||
world = state.world
|
||||
|
@ -194,6 +217,8 @@ def load_or_generate_world(args, players, systems, world_prompt: WorldPrompt):
|
|||
logger.info(f"loading world from {world_file}")
|
||||
with open(world_file, "r") as f:
|
||||
world = World(**load_yaml(f))
|
||||
|
||||
load_or_initialize_system_data(args, systems, world)
|
||||
else:
|
||||
logger.info(f"generating a new world using theme: {world_prompt.theme}")
|
||||
llm = agent_easy_connect()
|
||||
|
@ -213,11 +238,7 @@ def load_or_generate_world(args, players, systems, world_prompt: WorldPrompt):
|
|||
room_count=args.rooms,
|
||||
)
|
||||
save_world(world, world_file)
|
||||
|
||||
# run the systems once to initialize everything
|
||||
for system in systems:
|
||||
if system.simulate:
|
||||
system.simulate(world, 0)
|
||||
save_system_data(args, systems)
|
||||
|
||||
create_agents(world, memory=memory, players=players)
|
||||
return (world, world_state_file)
|
||||
|
@ -296,7 +317,7 @@ def main():
|
|||
if args.server:
|
||||
from adventure.server.websocket import server_system
|
||||
|
||||
extra_systems.append(GameSystem(simulate=server_system))
|
||||
extra_systems.append(GameSystem(name="server", simulate=server_system))
|
||||
|
||||
# load or generate the world
|
||||
world_prompt = get_world_prompt(args)
|
||||
|
@ -305,11 +326,11 @@ def main():
|
|||
)
|
||||
|
||||
# make sure the snapshot system runs last
|
||||
def snapshot_system(world: World, step: int) -> None:
|
||||
def snapshot_system(world: World, step: int, data: None = None) -> None:
|
||||
logger.info("taking snapshot of world state")
|
||||
save_world_state(world, step, world_state_file)
|
||||
|
||||
extra_systems.append(GameSystem(simulate=snapshot_system))
|
||||
extra_systems.append(GameSystem(name="snapshot", simulate=snapshot_system))
|
||||
|
||||
# hack: send a snapshot to the websocket server
|
||||
if args.server:
|
||||
|
|
|
@ -4,6 +4,7 @@ from pydantic import Field
|
|||
|
||||
from .base import Attributes, BaseModel, dataclass, uuid
|
||||
from .effect import EffectPattern, EffectResult
|
||||
from .planning import Planner
|
||||
|
||||
Actions = Dict[str, Callable]
|
||||
|
||||
|
@ -26,6 +27,7 @@ class Actor(BaseModel):
|
|||
name: str
|
||||
backstory: str
|
||||
description: str
|
||||
planner: Planner = Field(default_factory=Planner)
|
||||
actions: Actions = Field(default_factory=dict)
|
||||
active_effects: List[EffectResult] = Field(default_factory=list)
|
||||
attributes: Attributes = Field(default_factory=dict)
|
||||
|
@ -79,3 +81,11 @@ class WorldState(BaseModel):
|
|||
|
||||
|
||||
WorldEntity = Room | Actor | Item | Portal
|
||||
|
||||
|
||||
@dataclass
|
||||
class EntityReference:
|
||||
actor: str | None = None
|
||||
item: str | None = None
|
||||
portal: str | None = None
|
||||
room: str | None = None
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
from typing import List
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from adventure.models.base import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class CalendarEvent:
|
||||
name: str
|
||||
turn: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class Calendar:
|
||||
events: List[CalendarEvent] = Field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Planner:
|
||||
calendar: Calendar = Field(default_factory=Calendar)
|
||||
notes: List[str] = Field(default_factory=list)
|
|
@ -296,7 +296,7 @@ async def server_main():
|
|||
await asyncio.Future() # run forever
|
||||
|
||||
|
||||
def server_system(world: World, step: int):
|
||||
def server_system(world: World, step: int, data: Any | None = None):
|
||||
global last_snapshot
|
||||
id = uuid4().hex # TODO: should a server be allowed to generate event IDs?
|
||||
json_state = {
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
from functools import partial
|
||||
from itertools import count
|
||||
from logging import getLogger
|
||||
from math import inf
|
||||
from typing import Callable, Sequence
|
||||
|
||||
from packit.loops import loop_retry
|
||||
from packit.agent import Agent
|
||||
from packit.conditions import condition_or, condition_threshold, make_flag_condition
|
||||
from packit.loops import loop_reduce, loop_retry
|
||||
from packit.results import multi_function_or_str_result
|
||||
from packit.toolbox import Toolbox
|
||||
from packit.utils import could_be_json
|
||||
|
@ -16,6 +19,16 @@ from adventure.actions.base import (
|
|||
action_take,
|
||||
action_tell,
|
||||
)
|
||||
from adventure.actions.planning import (
|
||||
erase_notes,
|
||||
get_recent_notes,
|
||||
get_upcoming_events,
|
||||
read_calendar,
|
||||
read_notes,
|
||||
replace_note,
|
||||
schedule_event,
|
||||
take_note,
|
||||
)
|
||||
from adventure.context import (
|
||||
broadcast,
|
||||
get_actor_agent_for_name,
|
||||
|
@ -29,10 +42,11 @@ from adventure.context import (
|
|||
set_game_systems,
|
||||
)
|
||||
from adventure.game_system import GameSystem
|
||||
from adventure.models.entity import World
|
||||
from adventure.models.entity import Actor, Room, World
|
||||
from adventure.models.event import ActionEvent, ReplyEvent, ResultEvent
|
||||
from adventure.utils.effect import is_active_effect
|
||||
from adventure.utils.effect import expire_effects
|
||||
from adventure.utils.search import find_room_with_actor
|
||||
from adventure.utils.string import normalize_name
|
||||
from adventure.utils.world import describe_entity, format_attributes
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
@ -58,13 +72,136 @@ def world_result_parser(value, agent, **kwargs):
|
|||
return multi_function_or_str_result(value, agent=agent, **kwargs)
|
||||
|
||||
|
||||
def prompt_actor_action(room, actor, agent, action_names, action_toolbox) -> str:
|
||||
# collect data for the prompt
|
||||
room_actors = [actor.name for actor in room.actors]
|
||||
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]
|
||||
|
||||
# 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 could_be_json(value):
|
||||
event = ActionEvent.from_json(value, room, actor)
|
||||
else:
|
||||
event = ReplyEvent.from_text(value, room, actor)
|
||||
|
||||
broadcast(event)
|
||||
|
||||
return world_result_parser(value, agent, **kwargs)
|
||||
|
||||
# prompt and act
|
||||
logger.info("starting turn for actor: %s", actor.name)
|
||||
result = loop_retry(
|
||||
agent,
|
||||
(
|
||||
"You are currently in {room_name}. {room_description}. {attributes}. "
|
||||
"The room contains the following characters: {visible_actors}. "
|
||||
"The room contains the following items: {visible_items}. "
|
||||
"Your inventory contains the following items: {actor_items}."
|
||||
"You can take the following actions: {actions}. "
|
||||
"You can move in the following directions: {directions}. "
|
||||
"What will you do next? Reply with a JSON function call, calling one of the actions."
|
||||
"You can only perform one action per turn. What is your next action?"
|
||||
),
|
||||
context={
|
||||
"actions": action_names,
|
||||
"actor_items": actor_items,
|
||||
"attributes": actor_attributes,
|
||||
"directions": room_directions,
|
||||
"room_name": room.name,
|
||||
"room_description": describe_entity(room),
|
||||
"visible_actors": room_actors,
|
||||
"visible_items": room_items,
|
||||
},
|
||||
result_parser=result_parser,
|
||||
toolbox=action_toolbox,
|
||||
)
|
||||
|
||||
logger.debug(f"{actor.name} step result: {result}")
|
||||
if agent.memory:
|
||||
# TODO: make sure this is not duplicating memories and wasting space
|
||||
agent.memory.append(result)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def prompt_actor_think(
|
||||
room: Room, actor: Actor, agent: Agent, planner_toolbox: Toolbox
|
||||
) -> str:
|
||||
recent_notes = get_recent_notes()
|
||||
upcoming_events = get_upcoming_events()
|
||||
|
||||
if len(recent_notes) > 0:
|
||||
notes = "\n".join(recent_notes)
|
||||
notes_prompt = f"Your recent notes are: {notes}\n"
|
||||
else:
|
||||
notes_prompt = "You have no recent notes.\n"
|
||||
|
||||
if len(upcoming_events) > 0:
|
||||
current_step = get_current_step()
|
||||
events = [
|
||||
f"{event.name} in {event.turn - current_step} turns"
|
||||
for event in upcoming_events
|
||||
]
|
||||
events = "\n".join(events)
|
||||
events_prompt = f"Upcoming events are: {events}\n"
|
||||
else:
|
||||
events_prompt = "You have no upcoming events.\n"
|
||||
|
||||
event_count = len(actor.planner.calendar.events)
|
||||
note_count = len(actor.planner.notes)
|
||||
|
||||
logger.info("starting planning for actor: %s", actor.name)
|
||||
set_end, condition_end = make_flag_condition()
|
||||
|
||||
def result_parser(value, **kwargs):
|
||||
if normalize_name(value) == "end":
|
||||
set_end()
|
||||
|
||||
return multi_function_or_str_result(value, **kwargs)
|
||||
|
||||
stop_condition = condition_or(condition_end, partial(condition_threshold, max=3))
|
||||
|
||||
result = loop_reduce(
|
||||
agent,
|
||||
"You are about to take your turn. Plan your next action carefully. "
|
||||
"You can check your notes for important facts or check your calendar for upcoming events. You have {note_count} notes. "
|
||||
"Try to keep your notes accurate. Replace or erase old notes if they are no longer accurate or useful. "
|
||||
"If you have upcoming events with other characters, schedule them on your calendar. You have {event_count} calendar events. "
|
||||
"Think about your goals and any quests that you are working on, and plan your next action accordingly. "
|
||||
"You can perform up to 3 planning actions in a single turn. When you are done planning, reply with 'END'."
|
||||
"{notes_prompt} {events_prompt}",
|
||||
context={
|
||||
"event_count": event_count,
|
||||
"events_prompt": events_prompt,
|
||||
"note_count": note_count,
|
||||
"notes_prompt": notes_prompt,
|
||||
},
|
||||
result_parser=result_parser,
|
||||
stop_condition=stop_condition,
|
||||
toolbox=planner_toolbox,
|
||||
)
|
||||
|
||||
if agent.memory:
|
||||
agent.memory.append(result)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def simulate_world(
|
||||
world: World,
|
||||
steps: float | int = inf,
|
||||
actions: Sequence[Callable[..., str]] = [],
|
||||
systems: Sequence[GameSystem] = [],
|
||||
):
|
||||
logger.info("Simulating the world")
|
||||
logger.info("simulating the world")
|
||||
set_current_world(world)
|
||||
set_game_systems(systems)
|
||||
|
||||
|
@ -82,6 +219,18 @@ def simulate_world(
|
|||
)
|
||||
action_names = action_tools.list_tools()
|
||||
|
||||
# build a toolbox for the planners
|
||||
planner_toolbox = Toolbox(
|
||||
[
|
||||
take_note,
|
||||
read_notes,
|
||||
replace_note,
|
||||
erase_notes,
|
||||
schedule_event,
|
||||
read_calendar,
|
||||
]
|
||||
)
|
||||
|
||||
# simulate each actor
|
||||
for i in count():
|
||||
current_step = get_current_step()
|
||||
|
@ -90,79 +239,31 @@ def simulate_world(
|
|||
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}")
|
||||
logger.error(f"agent or actor not found for name {actor_name}")
|
||||
continue
|
||||
|
||||
room = find_room_with_actor(world, actor)
|
||||
if not room:
|
||||
logger.error(f"Actor {actor_name} is not in a room")
|
||||
logger.error(f"actor {actor_name} is not in a room")
|
||||
continue
|
||||
|
||||
# prep context
|
||||
set_current_room(room)
|
||||
set_current_actor(actor)
|
||||
|
||||
# decrement effects on the actor and remove any that have expired
|
||||
for effect in actor.active_effects:
|
||||
if effect.duration is not None:
|
||||
effect.duration -= 1
|
||||
expire_effects(actor)
|
||||
# TODO: expire calendar events
|
||||
|
||||
actor.active_effects[:] = [
|
||||
effect for effect in actor.active_effects if is_active_effect(effect)
|
||||
]
|
||||
|
||||
# collect data for the prompt
|
||||
room_actors = [actor.name for actor in room.actors]
|
||||
room_items = [item.name for item in room.items]
|
||||
room_directions = [portal.name for portal in room.portals]
|
||||
|
||||
actor_attributes = format_attributes(actor)
|
||||
actor_items = [item.name for item in actor.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 could_be_json(value):
|
||||
event = ActionEvent.from_json(value, room, actor)
|
||||
else:
|
||||
event = ReplyEvent.from_text(value, room, actor)
|
||||
|
||||
broadcast(event)
|
||||
|
||||
return world_result_parser(value, agent, **kwargs)
|
||||
|
||||
# prompt and act
|
||||
logger.info("starting turn for actor: %s", actor_name)
|
||||
result = loop_retry(
|
||||
agent,
|
||||
(
|
||||
"You are currently in {room_name}. {room_description}. {attributes}. "
|
||||
"The room contains the following characters: {visible_actors}. "
|
||||
"The room contains the following items: {visible_items}. "
|
||||
"Your inventory contains the following items: {actor_items}."
|
||||
"You can take the following actions: {actions}. "
|
||||
"You can move in the following directions: {directions}. "
|
||||
"What will you do next? Reply with a JSON function call, calling one of the actions."
|
||||
"You can only perform one action per turn. What is your next action?"
|
||||
),
|
||||
context={
|
||||
"actions": action_names,
|
||||
"actor_items": actor_items,
|
||||
"attributes": actor_attributes,
|
||||
"directions": room_directions,
|
||||
"room_name": room.name,
|
||||
"room_description": describe_entity(room),
|
||||
"visible_actors": room_actors,
|
||||
"visible_items": room_items,
|
||||
},
|
||||
result_parser=result_parser,
|
||||
toolbox=action_tools,
|
||||
)
|
||||
|
||||
logger.debug(f"{actor.name} step result: {result}")
|
||||
if agent.memory:
|
||||
agent.memory.append(result)
|
||||
# give the actor 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)
|
||||
logger.debug(f"{actor.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)
|
||||
result_event = ResultEvent(result=result, room=room, actor=actor)
|
||||
broadcast(result_event)
|
||||
|
||||
|
@ -171,6 +272,6 @@ def simulate_world(
|
|||
system.simulate(world, current_step)
|
||||
|
||||
set_current_step(current_step + 1)
|
||||
if i > steps:
|
||||
if i >= steps:
|
||||
logger.info("reached step limit at world step %s", current_step + 1)
|
||||
break
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
from functools import partial, wraps
|
||||
from logging import getLogger
|
||||
from os import path
|
||||
from random import random
|
||||
from typing import Dict, List, Optional, Protocol
|
||||
from typing import Any, Dict, List, Optional, Protocol
|
||||
|
||||
from pydantic import Field
|
||||
from rule_engine import Rule
|
||||
|
@ -128,7 +129,12 @@ def update_attributes(
|
|||
|
||||
|
||||
def update_logic(
|
||||
world: World, step: int, rules: LogicTable, triggers: TriggerTable
|
||||
world: World,
|
||||
step: int,
|
||||
data: Any | None = None,
|
||||
*,
|
||||
rules: LogicTable,
|
||||
triggers: TriggerTable
|
||||
) -> None:
|
||||
for room in world.rooms:
|
||||
update_attributes(room, rules=rules, triggers=triggers)
|
||||
|
@ -165,7 +171,9 @@ def format_logic(
|
|||
|
||||
|
||||
def load_logic(filename: str):
|
||||
logger.info("loading logic from file: %s", filename)
|
||||
system_name = "logic-" + path.splitext(path.basename(filename))[0]
|
||||
logger.info("loading logic from file %s as system %s", filename, system_name)
|
||||
|
||||
with open(filename) as file:
|
||||
logic_rules = LogicTable(**load(file, Loader=Loader))
|
||||
logic_triggers = {}
|
||||
|
@ -179,12 +187,17 @@ def load_logic(filename: str):
|
|||
logic_triggers[trigger] = get_plugin_function(function_name)
|
||||
|
||||
logger.info("initialized logic system")
|
||||
system_format = wraps(format_logic)(partial(format_logic, rules=logic_rules))
|
||||
system_initialize = wraps(load_logic)(
|
||||
partial(update_logic, step=0, rules=logic_rules, triggers=logic_triggers)
|
||||
)
|
||||
system_simulate = wraps(update_logic)(
|
||||
partial(update_logic, rules=logic_rules, triggers=logic_triggers)
|
||||
)
|
||||
system_format = wraps(format_logic)(partial(format_logic, rules=logic_rules))
|
||||
|
||||
return GameSystem(
|
||||
name=system_name,
|
||||
format=system_format,
|
||||
initialize=system_initialize,
|
||||
simulate=system_simulate,
|
||||
)
|
|
@ -0,0 +1,233 @@
|
|||
from logging import getLogger
|
||||
from typing import Dict, List, Literal, Optional
|
||||
|
||||
from packit.agent import Agent
|
||||
from pydantic import Field
|
||||
|
||||
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,
|
||||
EntityReference,
|
||||
Item,
|
||||
Room,
|
||||
World,
|
||||
WorldEntity,
|
||||
)
|
||||
from adventure.systems.logic import match_logic
|
||||
from adventure.utils.search import (
|
||||
find_entity_reference,
|
||||
find_item_in_container,
|
||||
find_item_in_room,
|
||||
)
|
||||
from adventure.utils.systems import load_system_data, save_system_data
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
QUEST_SYSTEM = "quest"
|
||||
|
||||
|
||||
@dataclass
|
||||
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: EntityReference
|
||||
contents: List[EntityReference] = Field(default_factory=list)
|
||||
type: Literal["contains"] = "contains"
|
||||
|
||||
|
||||
@dataclass
|
||||
class QuestGoalAttributes:
|
||||
"""
|
||||
Quest goal for any kind of attribute quest, including spell casting and item usage.
|
||||
"""
|
||||
|
||||
target: EntityReference
|
||||
match: Optional[Attributes] = None
|
||||
rule: Optional[str] = None
|
||||
type: Literal["attributes"] = "attributes"
|
||||
|
||||
|
||||
QuestGoal = QuestGoalAttributes | QuestGoalContains
|
||||
|
||||
|
||||
@dataclass
|
||||
class QuestReward:
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Quest:
|
||||
name: str
|
||||
description: str
|
||||
giver: EntityReference
|
||||
goal: QuestGoal
|
||||
reward: QuestReward
|
||||
type: Literal["quest"] = "quest"
|
||||
id: str = Field(default_factory=uuid)
|
||||
|
||||
|
||||
@dataclass
|
||||
class QuestData:
|
||||
active: Dict[str, Quest]
|
||||
available: Dict[str, List[Quest]]
|
||||
completed: Dict[str, List[Quest]]
|
||||
|
||||
|
||||
# region quest completion
|
||||
def is_quest_complete(world: World, quest: Quest) -> bool:
|
||||
"""
|
||||
Check if the given quest is complete.
|
||||
"""
|
||||
|
||||
if quest.goal.type == "contains":
|
||||
container = find_entity_reference(world, quest.goal.container)
|
||||
if not container:
|
||||
raise ValueError(f"quest container not found: {quest.goal.container}")
|
||||
|
||||
for content in quest.goal.contents:
|
||||
if isinstance(container, Room):
|
||||
if content.item:
|
||||
if not find_item_in_room(container, content.item):
|
||||
return False
|
||||
elif isinstance(container, (Actor, Item)):
|
||||
if content.item:
|
||||
if not find_item_in_container(container, content.item):
|
||||
return False
|
||||
else:
|
||||
logger.warning(f"unsupported container type: {container}")
|
||||
return False
|
||||
|
||||
return True
|
||||
elif quest.goal.type == "attributes":
|
||||
target = find_entity_reference(world, quest.goal.target)
|
||||
if not target:
|
||||
raise ValueError(f"quest target not found: {quest.goal.target}")
|
||||
|
||||
if match_logic(target, quest.goal):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
# region state management
|
||||
def get_quests_for_actor(quests: QuestData, actor: Actor) -> List[Quest]:
|
||||
"""
|
||||
Get all quests for the given actor.
|
||||
"""
|
||||
return quests.available.get(actor.name, [])
|
||||
|
||||
|
||||
def set_active_quest(quests: QuestData, actor: Actor, quest: Quest) -> None:
|
||||
"""
|
||||
Set the active quest for the given actor.
|
||||
"""
|
||||
quests.active[actor.name] = quest
|
||||
|
||||
|
||||
def get_active_quest(quests: QuestData, actor: Actor) -> Quest | None:
|
||||
"""
|
||||
Get the active quest for the given actor.
|
||||
"""
|
||||
return quests.active.get(actor.name)
|
||||
|
||||
|
||||
def complete_quest(quests: QuestData, actor: Actor, quest: Quest) -> None:
|
||||
"""
|
||||
Complete the given quest for the given actor.
|
||||
"""
|
||||
if quest in quests.available.get(actor.name, []):
|
||||
quests.available[actor.name].remove(quest)
|
||||
|
||||
if quest == quests.active.get(actor.name, None):
|
||||
del quests.active[actor.name]
|
||||
|
||||
if actor.name not in quests.completed:
|
||||
quests.completed[actor.name] = []
|
||||
|
||||
quests.completed[actor.name].append(quest)
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
def initialize_quests(world: World) -> QuestData:
|
||||
"""
|
||||
Initialize quests for the world.
|
||||
"""
|
||||
|
||||
logger.info("initializing quest data for world %s", world.name)
|
||||
return QuestData(active={}, available={}, completed={})
|
||||
|
||||
|
||||
def generate_quests(agent: Agent, theme: str, entity: WorldEntity) -> None:
|
||||
"""
|
||||
Generate new quests for the world.
|
||||
"""
|
||||
|
||||
quests: QuestData | None = get_system_data(QUEST_SYSTEM)
|
||||
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 len(available_quests) == 0:
|
||||
logger.info(f"generating new quest for {entity.name}")
|
||||
# TODO: generate one new quest
|
||||
|
||||
|
||||
def simulate_quests(world: World, step: int, data: QuestData | None = None) -> None:
|
||||
"""
|
||||
1. Check for any completed quests.
|
||||
2. Update any active quests.
|
||||
3. Generate any new quests.
|
||||
"""
|
||||
|
||||
if not data:
|
||||
# TODO: initialize quest data for worlds that don't have it
|
||||
raise ValueError("Quest data is required for simulation")
|
||||
|
||||
for room in world.rooms:
|
||||
for actor in room.actors:
|
||||
active_quest = get_active_quest(data, actor)
|
||||
if active_quest:
|
||||
logger.info(f"simulating quest for {actor.name}: {active_quest.name}")
|
||||
if is_quest_complete(world, active_quest):
|
||||
logger.info(f"quest complete for {actor.name}: {active_quest.name}")
|
||||
complete_quest(data, actor, active_quest)
|
||||
|
||||
|
||||
def load_quest_data(file: str) -> QuestData:
|
||||
logger.info(f"loading quest data from {file}")
|
||||
return load_system_data(QuestData, file)
|
||||
|
||||
|
||||
def save_quest_data(file: str, data: QuestData) -> None:
|
||||
logger.info(f"saving quest data to {file}")
|
||||
return save_system_data(QuestData, file, data)
|
||||
|
||||
|
||||
def init() -> List[GameSystem]:
|
||||
return [
|
||||
GameSystem(
|
||||
QUEST_SYSTEM,
|
||||
data=SystemData(
|
||||
load=load_quest_data,
|
||||
save=save_quest_data,
|
||||
),
|
||||
generate=generate_quests,
|
||||
initialize=initialize_quests,
|
||||
simulate=simulate_quests,
|
||||
)
|
||||
]
|
|
@ -3,7 +3,7 @@ from .language_actions import action_read
|
|||
from .magic_actions import action_cast
|
||||
from .movement_actions import action_climb
|
||||
|
||||
from adventure.logic import load_logic
|
||||
from adventure.systems.logic import load_logic
|
||||
|
||||
LOGIC_FILES = [
|
||||
"./adventure/systems/rpg/weather_logic.yaml",
|
||||
|
|
|
@ -2,7 +2,7 @@ from .hunger_actions import action_cook, action_eat
|
|||
from .hygiene_actions import action_wash
|
||||
from .sleeping_actions import action_sleep
|
||||
|
||||
from adventure.logic import load_logic
|
||||
from adventure.systems.logic import load_logic
|
||||
|
||||
LOGIC_FILES = [
|
||||
"./adventure/systems/sim/environment_logic.yaml",
|
||||
|
|
|
@ -298,7 +298,7 @@ def apply_permanent_effects(
|
|||
|
||||
def apply_effects(target: Actor, effects: List[EffectPattern]) -> None:
|
||||
"""
|
||||
Apply a set of effects to a set of attributes.
|
||||
Apply a set of effects to an actor and their attributes.
|
||||
"""
|
||||
|
||||
permanent_effects = [
|
||||
|
@ -312,3 +312,17 @@ def apply_effects(target: Actor, effects: List[EffectPattern]) -> None:
|
|||
]
|
||||
temporary_effects = resolve_effects(temporary_effects)
|
||||
target.active_effects.extend(temporary_effects)
|
||||
|
||||
|
||||
def expire_effects(target: Actor) -> None:
|
||||
"""
|
||||
Decrement the duration of effects on an actor and remove any that have expired.
|
||||
"""
|
||||
|
||||
for effect in target.active_effects:
|
||||
if effect.duration is not None:
|
||||
effect.duration -= 1
|
||||
|
||||
target.active_effects[:] = [
|
||||
effect for effect in target.active_effects if is_active_effect(effect)
|
||||
]
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
from yaml import Loader, dump, load
|
||||
|
||||
|
||||
def load_yaml(file):
|
||||
return load(file, Loader=Loader)
|
||||
|
||||
|
||||
def save_yaml(file, data):
|
||||
return dump(data, file)
|
|
@ -1,6 +1,14 @@
|
|||
from typing import Any, Generator
|
||||
|
||||
from adventure.models.entity import Actor, Item, Portal, Room, World
|
||||
from adventure.models.entity import (
|
||||
Actor,
|
||||
EntityReference,
|
||||
Item,
|
||||
Portal,
|
||||
Room,
|
||||
World,
|
||||
WorldEntity,
|
||||
)
|
||||
|
||||
from .string import normalize_name
|
||||
|
||||
|
@ -59,20 +67,11 @@ def find_item(
|
|||
def find_item_in_actor(
|
||||
actor: Actor, item_name: str, include_item_inventory=False
|
||||
) -> Item | None:
|
||||
for item in actor.items:
|
||||
if normalize_name(item.name) == normalize_name(item_name):
|
||||
return item
|
||||
|
||||
if include_item_inventory:
|
||||
item = find_item_in_container(item, item_name, include_item_inventory)
|
||||
if item:
|
||||
return item
|
||||
|
||||
return None
|
||||
return find_item_in_container(actor, item_name, include_item_inventory)
|
||||
|
||||
|
||||
def find_item_in_container(
|
||||
container: Item, item_name: str, include_item_inventory=False
|
||||
container: Actor | Item, item_name: str, include_item_inventory=False
|
||||
) -> Item | None:
|
||||
for item in container.items:
|
||||
if normalize_name(item.name) == normalize_name(item_name):
|
||||
|
@ -130,6 +129,28 @@ def find_containing_room(world: World, entity: Room | Actor | Item) -> Room | No
|
|||
return None
|
||||
|
||||
|
||||
def find_entity_reference(
|
||||
world: World, reference: EntityReference
|
||||
) -> WorldEntity | None:
|
||||
"""
|
||||
Resolve an entity reference to an entity in the world.
|
||||
"""
|
||||
|
||||
if reference.room:
|
||||
return find_room(world, reference.room)
|
||||
|
||||
if reference.actor:
|
||||
return find_actor(world, reference.actor)
|
||||
|
||||
if reference.item:
|
||||
return find_item(world, reference.item)
|
||||
|
||||
if reference.portal:
|
||||
return find_portal(world, reference.portal)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def list_rooms(world: World) -> Generator[Room, Any, None]:
|
||||
for room in world.rooms:
|
||||
yield room
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
from pydantic import RootModel
|
||||
|
||||
from adventure.utils.file import load_yaml, save_yaml
|
||||
|
||||
|
||||
def load_system_data(cls, file):
|
||||
with load_yaml(file) as data:
|
||||
return cls(**data)
|
||||
|
||||
|
||||
def save_system_data(cls, file, model):
|
||||
data = RootModel[cls](model).model_dump()
|
||||
with open(file, "w") as f:
|
||||
save_yaml(f, data)
|
|
@ -1,13 +1,31 @@
|
|||
import { Maybe, doesExist } from '@apextoaster/js-utils';
|
||||
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Typography } from '@mui/material';
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Paper,
|
||||
Stack,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { instance as graphviz } from '@viz-js/viz';
|
||||
import React, { Fragment, useEffect } from 'react';
|
||||
import { useStore } from 'zustand';
|
||||
import { Actor, Item, Room, World } from './models';
|
||||
import { Actor, Attributes, Item, Portal, Room, World } from './models';
|
||||
import { StoreState, store } from './store';
|
||||
|
||||
export interface EntityDetailsProps {
|
||||
entity: Maybe<Item | Actor | Room>;
|
||||
entity: Maybe<Item | Actor | Portal | Room>;
|
||||
onClose: () => void;
|
||||
onRender: (type: string, entity: string) => void;
|
||||
}
|
||||
|
@ -20,15 +38,58 @@ export function EntityDetails(props: EntityDetailsProps) {
|
|||
return <Fragment />;
|
||||
}
|
||||
|
||||
const { description, name, type } = entity;
|
||||
|
||||
let attributes: Attributes = {};
|
||||
let planner;
|
||||
|
||||
if (type === 'actor') {
|
||||
const actor = entity as Actor;
|
||||
attributes = actor.attributes;
|
||||
planner = actor.planner;
|
||||
}
|
||||
|
||||
if (type === 'item') {
|
||||
const item = entity as Item;
|
||||
attributes = item.attributes;
|
||||
}
|
||||
|
||||
return <Fragment>
|
||||
<DialogTitle>{entity.name}</DialogTitle>
|
||||
<DialogTitle>{name}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Typography>
|
||||
{entity.description}
|
||||
</Typography>
|
||||
<Stack direction='column' spacing={2}>
|
||||
<Typography>
|
||||
{description}
|
||||
</Typography>
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Attribute</TableCell>
|
||||
<TableCell>Value</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{Object.entries(attributes).map(([key, value]) => (
|
||||
<TableRow key={key}>
|
||||
<TableCell>{key}</TableCell>
|
||||
<TableCell>{value}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{doesExist(planner) && <List>
|
||||
{planner.notes.map((note: string) => (
|
||||
<ListItem>
|
||||
<ListItemText primary={note} />
|
||||
</ListItem>
|
||||
))}
|
||||
</List>}
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => onRender(entity.type, entity.name)}>Render</Button>
|
||||
<Button onClick={() => onRender(type, name)}>Render</Button>
|
||||
<Button onClick={onClose}>Close</Button>
|
||||
</DialogActions>
|
||||
</Fragment>;
|
||||
|
@ -94,7 +155,7 @@ export function DetailDialog(props: DetailDialogProps) {
|
|||
>{details}</Dialog>;
|
||||
}
|
||||
|
||||
export function isWorld(entity: Maybe<Item | Actor | Room | World>): entity is World {
|
||||
export function isWorld(entity: Maybe<Item | Actor | Portal | Room | World>): entity is World {
|
||||
return doesExist(entity) && doesExist(Object.getOwnPropertyDescriptor(entity, 'theme'));
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,20 @@
|
|||
export type Attributes = Record<string, boolean | number | string>;
|
||||
|
||||
export interface CalendarEvent {
|
||||
name: string;
|
||||
turn: number;
|
||||
}
|
||||
|
||||
export interface Planner {
|
||||
calendar: Array<CalendarEvent>;
|
||||
notes: Array<string>;
|
||||
}
|
||||
|
||||
export interface Item {
|
||||
type: 'item';
|
||||
name: string;
|
||||
description: string;
|
||||
attributes: Attributes;
|
||||
}
|
||||
|
||||
export interface Actor {
|
||||
|
@ -10,6 +23,8 @@ export interface Actor {
|
|||
backstory: string;
|
||||
description: string;
|
||||
items: Array<Item>;
|
||||
attributes: Attributes;
|
||||
planner: Planner;
|
||||
}
|
||||
|
||||
export interface Portal {
|
||||
|
@ -26,6 +41,7 @@ export interface Room {
|
|||
actors: Array<Actor>;
|
||||
items: Array<Item>;
|
||||
portals: Array<Portal>;
|
||||
attributes: Attributes;
|
||||
}
|
||||
|
||||
export interface World {
|
||||
|
|
Loading…
Reference in New Issue