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:
|
if not destination_room:
|
||||||
return f"The {portal.destination} room does not exist."
|
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)
|
action_room.actors.remove(action_actor)
|
||||||
destination_room.actors.append(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 logging import getLogger
|
||||||
from types import UnionType
|
from types import UnionType
|
||||||
from typing import (
|
from typing import (
|
||||||
|
Any,
|
||||||
Callable,
|
Callable,
|
||||||
Dict,
|
Dict,
|
||||||
List,
|
List,
|
||||||
|
@ -33,6 +34,7 @@ dungeon_master: Agent | None = None
|
||||||
# game context
|
# game context
|
||||||
event_emitter = EventEmitter()
|
event_emitter = EventEmitter()
|
||||||
game_systems: List[GameSystem] = []
|
game_systems: List[GameSystem] = []
|
||||||
|
system_data: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
|
||||||
# TODO: where should this one go?
|
# TODO: where should this one go?
|
||||||
|
@ -155,6 +157,10 @@ def get_game_systems() -> List[GameSystem]:
|
||||||
return game_systems
|
return game_systems
|
||||||
|
|
||||||
|
|
||||||
|
def get_system_data(system: str) -> Any | None:
|
||||||
|
return system_data.get(system)
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
|
@ -193,6 +199,10 @@ def set_game_systems(systems: Sequence[GameSystem]):
|
||||||
game_systems = list(systems)
|
game_systems = list(systems)
|
||||||
|
|
||||||
|
|
||||||
|
def set_system_data(system: str, data: Any):
|
||||||
|
system_data[system] = data
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Protocol
|
from typing import Any, Callable, Protocol
|
||||||
|
|
||||||
from packit.agent import Agent
|
from packit.agent import Agent
|
||||||
|
|
||||||
from adventure.models.entity import World, WorldEntity
|
from adventure.models.entity import World, WorldEntity
|
||||||
from adventure.utils import format_callable
|
|
||||||
|
|
||||||
|
|
||||||
class FormatPerspective(Enum):
|
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):
|
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.
|
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:
|
class GameSystem:
|
||||||
|
name: str
|
||||||
|
data: SystemData | None = None
|
||||||
format: SystemFormat | None = None
|
format: SystemFormat | None = None
|
||||||
generate: SystemGenerate | None = None
|
generate: SystemGenerate | None = None
|
||||||
|
initialize: SystemInitialize | None = None
|
||||||
simulate: SystemSimulate | None = None
|
simulate: SystemSimulate | None = None
|
||||||
# render: TODO
|
# render: TODO
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
name: str,
|
||||||
|
*,
|
||||||
|
data: SystemData | None = None,
|
||||||
format: SystemFormat | None = None,
|
format: SystemFormat | None = None,
|
||||||
generate: SystemGenerate | None = None,
|
generate: SystemGenerate | None = None,
|
||||||
|
initialize: SystemInitialize | None = None,
|
||||||
simulate: SystemSimulate | None = None,
|
simulate: SystemSimulate | None = None,
|
||||||
):
|
):
|
||||||
|
self.name = name
|
||||||
|
self.data = data
|
||||||
self.format = format
|
self.format = format
|
||||||
self.generate = generate
|
self.generate = generate
|
||||||
|
self.initialize = initialize
|
||||||
self.simulate = simulate
|
self.simulate = simulate
|
||||||
|
|
||||||
def __str__(self):
|
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):
|
def __repr__(self):
|
||||||
return str(self)
|
return str(self)
|
||||||
|
|
|
@ -7,7 +7,7 @@ from packit.loops import loop_retry
|
||||||
from packit.results import enum_result, int_result
|
from packit.results import enum_result, int_result
|
||||||
from packit.utils import could_be_json
|
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.game_system import GameSystem
|
||||||
from adventure.models.config import DEFAULT_CONFIG, WorldConfig
|
from adventure.models.config import DEFAULT_CONFIG, WorldConfig
|
||||||
from adventure.models.effect import (
|
from adventure.models.effect import (
|
||||||
|
@ -74,6 +74,7 @@ def generate_system_attributes(
|
||||||
) -> None:
|
) -> None:
|
||||||
for system in systems:
|
for system in systems:
|
||||||
if system.generate:
|
if system.generate:
|
||||||
|
# TODO: pass the whole world
|
||||||
system.generate(agent, world.theme, entity)
|
system.generate(agent, world.theme, entity)
|
||||||
|
|
||||||
|
|
||||||
|
@ -423,6 +424,7 @@ def generate_effect(agent: Agent, world: World, entity: Item) -> EffectPattern:
|
||||||
"name": name,
|
"name": name,
|
||||||
},
|
},
|
||||||
result_parser=int_result,
|
result_parser=int_result,
|
||||||
|
toolbox=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
def parse_application(value: str, **kwargs) -> str:
|
def parse_application(value: str, **kwargs) -> str:
|
||||||
|
@ -447,6 +449,7 @@ def generate_effect(agent: Agent, world: World, entity: Item) -> EffectPattern:
|
||||||
"name": name,
|
"name": name,
|
||||||
},
|
},
|
||||||
result_parser=parse_application,
|
result_parser=parse_application,
|
||||||
|
toolbox=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
return EffectPattern(
|
return EffectPattern(
|
||||||
|
@ -522,6 +525,12 @@ def generate_world(
|
||||||
world = World(name=name, rooms=[], theme=theme, order=[])
|
world = World(name=name, rooms=[], theme=theme, order=[])
|
||||||
set_current_world(world)
|
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
|
# generate the rooms
|
||||||
for _ in range(room_count):
|
for _ in range(room_count):
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -6,14 +6,9 @@ from typing import List
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from packit.agent import Agent, agent_easy_connect
|
from packit.agent import Agent, agent_easy_connect
|
||||||
from packit.utils import logger_with_colors
|
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
|
# configure logging
|
||||||
LOG_PATH = "logging.json"
|
LOG_PATH = "logging.json"
|
||||||
|
@ -34,7 +29,7 @@ logger = logger_with_colors(__name__) # , level="DEBUG")
|
||||||
load_dotenv(environ.get("ADVENTURE_ENV", ".env"), override=True)
|
load_dotenv(environ.get("ADVENTURE_ENV", ".env"), override=True)
|
||||||
|
|
||||||
if 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.game_system import GameSystem
|
||||||
from adventure.generate import generate_world
|
from adventure.generate import generate_world
|
||||||
from adventure.models.config import DEFAULT_CONFIG, Config
|
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_file = args.world + ".json"
|
||||||
world_state_file = args.state or (args.world + ".state.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))
|
state = WorldState(**load_yaml(f))
|
||||||
|
|
||||||
set_current_step(state.step)
|
set_current_step(state.step)
|
||||||
|
load_or_initialize_system_data(args, systems, state.world)
|
||||||
|
|
||||||
memory = state.memory
|
memory = state.memory
|
||||||
world = state.world
|
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}")
|
logger.info(f"loading world from {world_file}")
|
||||||
with open(world_file, "r") as f:
|
with open(world_file, "r") as f:
|
||||||
world = World(**load_yaml(f))
|
world = World(**load_yaml(f))
|
||||||
|
|
||||||
|
load_or_initialize_system_data(args, systems, world)
|
||||||
else:
|
else:
|
||||||
logger.info(f"generating a new world using theme: {world_prompt.theme}")
|
logger.info(f"generating a new world using theme: {world_prompt.theme}")
|
||||||
llm = agent_easy_connect()
|
llm = agent_easy_connect()
|
||||||
|
@ -213,11 +238,7 @@ def load_or_generate_world(args, players, systems, world_prompt: WorldPrompt):
|
||||||
room_count=args.rooms,
|
room_count=args.rooms,
|
||||||
)
|
)
|
||||||
save_world(world, world_file)
|
save_world(world, world_file)
|
||||||
|
save_system_data(args, systems)
|
||||||
# run the systems once to initialize everything
|
|
||||||
for system in systems:
|
|
||||||
if system.simulate:
|
|
||||||
system.simulate(world, 0)
|
|
||||||
|
|
||||||
create_agents(world, memory=memory, players=players)
|
create_agents(world, memory=memory, players=players)
|
||||||
return (world, world_state_file)
|
return (world, world_state_file)
|
||||||
|
@ -296,7 +317,7 @@ def main():
|
||||||
if args.server:
|
if args.server:
|
||||||
from adventure.server.websocket import server_system
|
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
|
# load or generate the world
|
||||||
world_prompt = get_world_prompt(args)
|
world_prompt = get_world_prompt(args)
|
||||||
|
@ -305,11 +326,11 @@ def main():
|
||||||
)
|
)
|
||||||
|
|
||||||
# make sure the snapshot system runs last
|
# 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")
|
logger.info("taking snapshot of world state")
|
||||||
save_world_state(world, step, world_state_file)
|
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
|
# hack: send a snapshot to the websocket server
|
||||||
if args.server:
|
if args.server:
|
||||||
|
|
|
@ -4,6 +4,7 @@ from pydantic import Field
|
||||||
|
|
||||||
from .base import Attributes, BaseModel, dataclass, uuid
|
from .base import Attributes, BaseModel, dataclass, uuid
|
||||||
from .effect import EffectPattern, EffectResult
|
from .effect import EffectPattern, EffectResult
|
||||||
|
from .planning import Planner
|
||||||
|
|
||||||
Actions = Dict[str, Callable]
|
Actions = Dict[str, Callable]
|
||||||
|
|
||||||
|
@ -26,6 +27,7 @@ class Actor(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
backstory: str
|
backstory: str
|
||||||
description: str
|
description: str
|
||||||
|
planner: Planner = Field(default_factory=Planner)
|
||||||
actions: Actions = Field(default_factory=dict)
|
actions: Actions = Field(default_factory=dict)
|
||||||
active_effects: List[EffectResult] = Field(default_factory=list)
|
active_effects: List[EffectResult] = Field(default_factory=list)
|
||||||
attributes: Attributes = Field(default_factory=dict)
|
attributes: Attributes = Field(default_factory=dict)
|
||||||
|
@ -79,3 +81,11 @@ class WorldState(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
WorldEntity = Room | Actor | Item | Portal
|
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
|
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
|
global last_snapshot
|
||||||
id = uuid4().hex # TODO: should a server be allowed to generate event IDs?
|
id = uuid4().hex # TODO: should a server be allowed to generate event IDs?
|
||||||
json_state = {
|
json_state = {
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
|
from functools import partial
|
||||||
from itertools import count
|
from itertools import count
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from math import inf
|
from math import inf
|
||||||
from typing import Callable, Sequence
|
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.results import multi_function_or_str_result
|
||||||
from packit.toolbox import Toolbox
|
from packit.toolbox import Toolbox
|
||||||
from packit.utils import could_be_json
|
from packit.utils import could_be_json
|
||||||
|
@ -16,6 +19,16 @@ from adventure.actions.base import (
|
||||||
action_take,
|
action_take,
|
||||||
action_tell,
|
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 (
|
from adventure.context import (
|
||||||
broadcast,
|
broadcast,
|
||||||
get_actor_agent_for_name,
|
get_actor_agent_for_name,
|
||||||
|
@ -29,10 +42,11 @@ from adventure.context import (
|
||||||
set_game_systems,
|
set_game_systems,
|
||||||
)
|
)
|
||||||
from adventure.game_system import GameSystem
|
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.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.search import find_room_with_actor
|
||||||
|
from adventure.utils.string import normalize_name
|
||||||
from adventure.utils.world import describe_entity, format_attributes
|
from adventure.utils.world import describe_entity, format_attributes
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
@ -58,13 +72,136 @@ def world_result_parser(value, agent, **kwargs):
|
||||||
return multi_function_or_str_result(value, agent=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(
|
def simulate_world(
|
||||||
world: World,
|
world: World,
|
||||||
steps: float | int = inf,
|
steps: float | int = inf,
|
||||||
actions: Sequence[Callable[..., str]] = [],
|
actions: Sequence[Callable[..., str]] = [],
|
||||||
systems: Sequence[GameSystem] = [],
|
systems: Sequence[GameSystem] = [],
|
||||||
):
|
):
|
||||||
logger.info("Simulating the world")
|
logger.info("simulating the world")
|
||||||
set_current_world(world)
|
set_current_world(world)
|
||||||
set_game_systems(systems)
|
set_game_systems(systems)
|
||||||
|
|
||||||
|
@ -82,6 +219,18 @@ def simulate_world(
|
||||||
)
|
)
|
||||||
action_names = action_tools.list_tools()
|
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
|
# simulate each actor
|
||||||
for i in count():
|
for i in count():
|
||||||
current_step = get_current_step()
|
current_step = get_current_step()
|
||||||
|
@ -90,79 +239,31 @@ def simulate_world(
|
||||||
for actor_name in world.order:
|
for actor_name in world.order:
|
||||||
actor, agent = get_actor_agent_for_name(actor_name)
|
actor, agent = get_actor_agent_for_name(actor_name)
|
||||||
if not agent or not actor:
|
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
|
continue
|
||||||
|
|
||||||
room = find_room_with_actor(world, actor)
|
room = find_room_with_actor(world, actor)
|
||||||
if not room:
|
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
|
continue
|
||||||
|
|
||||||
|
# prep context
|
||||||
|
set_current_room(room)
|
||||||
|
set_current_actor(actor)
|
||||||
|
|
||||||
# decrement effects on the actor and remove any that have expired
|
# decrement effects on the actor and remove any that have expired
|
||||||
for effect in actor.active_effects:
|
expire_effects(actor)
|
||||||
if effect.duration is not None:
|
# TODO: expire calendar events
|
||||||
effect.duration -= 1
|
|
||||||
|
|
||||||
actor.active_effects[:] = [
|
# give the actor a chance to think and check their planner
|
||||||
effect for effect in actor.active_effects if is_active_effect(effect)
|
if agent.memory and len(agent.memory) > 0:
|
||||||
]
|
try:
|
||||||
|
thoughts = prompt_actor_think(room, actor, agent, planner_toolbox)
|
||||||
# collect data for the prompt
|
logger.debug(f"{actor.name} thinks: {thoughts}")
|
||||||
room_actors = [actor.name for actor in room.actors]
|
except Exception:
|
||||||
room_items = [item.name for item in room.items]
|
logger.exception(f"error during planning for actor {actor.name}")
|
||||||
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)
|
|
||||||
|
|
||||||
|
result = prompt_actor_action(room, actor, agent, action_names, action_tools)
|
||||||
result_event = ResultEvent(result=result, room=room, actor=actor)
|
result_event = ResultEvent(result=result, room=room, actor=actor)
|
||||||
broadcast(result_event)
|
broadcast(result_event)
|
||||||
|
|
||||||
|
@ -171,6 +272,6 @@ def simulate_world(
|
||||||
system.simulate(world, current_step)
|
system.simulate(world, current_step)
|
||||||
|
|
||||||
set_current_step(current_step + 1)
|
set_current_step(current_step + 1)
|
||||||
if i > steps:
|
if i >= steps:
|
||||||
logger.info("reached step limit at world step %s", current_step + 1)
|
logger.info("reached step limit at world step %s", current_step + 1)
|
||||||
break
|
break
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
from functools import partial, wraps
|
from functools import partial, wraps
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
from os import path
|
||||||
from random import random
|
from random import random
|
||||||
from typing import Dict, List, Optional, Protocol
|
from typing import Any, Dict, List, Optional, Protocol
|
||||||
|
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
from rule_engine import Rule
|
from rule_engine import Rule
|
||||||
|
@ -128,7 +129,12 @@ def update_attributes(
|
||||||
|
|
||||||
|
|
||||||
def update_logic(
|
def update_logic(
|
||||||
world: World, step: int, rules: LogicTable, triggers: TriggerTable
|
world: World,
|
||||||
|
step: int,
|
||||||
|
data: Any | None = None,
|
||||||
|
*,
|
||||||
|
rules: LogicTable,
|
||||||
|
triggers: TriggerTable
|
||||||
) -> None:
|
) -> None:
|
||||||
for room in world.rooms:
|
for room in world.rooms:
|
||||||
update_attributes(room, rules=rules, triggers=triggers)
|
update_attributes(room, rules=rules, triggers=triggers)
|
||||||
|
@ -165,7 +171,9 @@ def format_logic(
|
||||||
|
|
||||||
|
|
||||||
def load_logic(filename: str):
|
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:
|
with open(filename) as file:
|
||||||
logic_rules = LogicTable(**load(file, Loader=Loader))
|
logic_rules = LogicTable(**load(file, Loader=Loader))
|
||||||
logic_triggers = {}
|
logic_triggers = {}
|
||||||
|
@ -179,12 +187,17 @@ def load_logic(filename: str):
|
||||||
logic_triggers[trigger] = get_plugin_function(function_name)
|
logic_triggers[trigger] = get_plugin_function(function_name)
|
||||||
|
|
||||||
logger.info("initialized logic system")
|
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)(
|
system_simulate = wraps(update_logic)(
|
||||||
partial(update_logic, rules=logic_rules, triggers=logic_triggers)
|
partial(update_logic, rules=logic_rules, triggers=logic_triggers)
|
||||||
)
|
)
|
||||||
system_format = wraps(format_logic)(partial(format_logic, rules=logic_rules))
|
|
||||||
|
|
||||||
return GameSystem(
|
return GameSystem(
|
||||||
|
name=system_name,
|
||||||
format=system_format,
|
format=system_format,
|
||||||
|
initialize=system_initialize,
|
||||||
simulate=system_simulate,
|
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 .magic_actions import action_cast
|
||||||
from .movement_actions import action_climb
|
from .movement_actions import action_climb
|
||||||
|
|
||||||
from adventure.logic import load_logic
|
from adventure.systems.logic import load_logic
|
||||||
|
|
||||||
LOGIC_FILES = [
|
LOGIC_FILES = [
|
||||||
"./adventure/systems/rpg/weather_logic.yaml",
|
"./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 .hygiene_actions import action_wash
|
||||||
from .sleeping_actions import action_sleep
|
from .sleeping_actions import action_sleep
|
||||||
|
|
||||||
from adventure.logic import load_logic
|
from adventure.systems.logic import load_logic
|
||||||
|
|
||||||
LOGIC_FILES = [
|
LOGIC_FILES = [
|
||||||
"./adventure/systems/sim/environment_logic.yaml",
|
"./adventure/systems/sim/environment_logic.yaml",
|
||||||
|
|
|
@ -298,7 +298,7 @@ def apply_permanent_effects(
|
||||||
|
|
||||||
def apply_effects(target: Actor, effects: List[EffectPattern]) -> None:
|
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 = [
|
permanent_effects = [
|
||||||
|
@ -312,3 +312,17 @@ def apply_effects(target: Actor, effects: List[EffectPattern]) -> None:
|
||||||
]
|
]
|
||||||
temporary_effects = resolve_effects(temporary_effects)
|
temporary_effects = resolve_effects(temporary_effects)
|
||||||
target.active_effects.extend(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 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
|
from .string import normalize_name
|
||||||
|
|
||||||
|
@ -59,20 +67,11 @@ def find_item(
|
||||||
def find_item_in_actor(
|
def find_item_in_actor(
|
||||||
actor: Actor, item_name: str, include_item_inventory=False
|
actor: Actor, item_name: str, include_item_inventory=False
|
||||||
) -> Item | None:
|
) -> Item | None:
|
||||||
for item in actor.items:
|
return find_item_in_container(actor, item_name, include_item_inventory)
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def find_item_in_container(
|
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:
|
) -> Item | None:
|
||||||
for item in container.items:
|
for item in container.items:
|
||||||
if normalize_name(item.name) == normalize_name(item_name):
|
if normalize_name(item.name) == normalize_name(item_name):
|
||||||
|
@ -130,6 +129,28 @@ def find_containing_room(world: World, entity: Room | Actor | Item) -> Room | No
|
||||||
return None
|
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]:
|
def list_rooms(world: World) -> Generator[Room, Any, None]:
|
||||||
for room in world.rooms:
|
for room in world.rooms:
|
||||||
yield room
|
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 { 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 { instance as graphviz } from '@viz-js/viz';
|
||||||
import React, { Fragment, useEffect } from 'react';
|
import React, { Fragment, useEffect } from 'react';
|
||||||
import { useStore } from 'zustand';
|
import { useStore } from 'zustand';
|
||||||
import { Actor, Item, Room, World } from './models';
|
import { Actor, Attributes, Item, Portal, Room, World } from './models';
|
||||||
import { StoreState, store } from './store';
|
import { StoreState, store } from './store';
|
||||||
|
|
||||||
export interface EntityDetailsProps {
|
export interface EntityDetailsProps {
|
||||||
entity: Maybe<Item | Actor | Room>;
|
entity: Maybe<Item | Actor | Portal | Room>;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onRender: (type: string, entity: string) => void;
|
onRender: (type: string, entity: string) => void;
|
||||||
}
|
}
|
||||||
|
@ -20,15 +38,58 @@ export function EntityDetails(props: EntityDetailsProps) {
|
||||||
return <Fragment />;
|
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>
|
return <Fragment>
|
||||||
<DialogTitle>{entity.name}</DialogTitle>
|
<DialogTitle>{name}</DialogTitle>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<Typography>
|
<Stack direction='column' spacing={2}>
|
||||||
{entity.description}
|
<Typography>
|
||||||
</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>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={() => onRender(entity.type, entity.name)}>Render</Button>
|
<Button onClick={() => onRender(type, name)}>Render</Button>
|
||||||
<Button onClick={onClose}>Close</Button>
|
<Button onClick={onClose}>Close</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Fragment>;
|
</Fragment>;
|
||||||
|
@ -94,7 +155,7 @@ export function DetailDialog(props: DetailDialogProps) {
|
||||||
>{details}</Dialog>;
|
>{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'));
|
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 {
|
export interface Item {
|
||||||
type: 'item';
|
type: 'item';
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
attributes: Attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Actor {
|
export interface Actor {
|
||||||
|
@ -10,6 +23,8 @@ export interface Actor {
|
||||||
backstory: string;
|
backstory: string;
|
||||||
description: string;
|
description: string;
|
||||||
items: Array<Item>;
|
items: Array<Item>;
|
||||||
|
attributes: Attributes;
|
||||||
|
planner: Planner;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Portal {
|
export interface Portal {
|
||||||
|
@ -26,6 +41,7 @@ export interface Room {
|
||||||
actors: Array<Actor>;
|
actors: Array<Actor>;
|
||||||
items: Array<Item>;
|
items: Array<Item>;
|
||||||
portals: Array<Portal>;
|
portals: Array<Portal>;
|
||||||
|
attributes: Attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface World {
|
export interface World {
|
||||||
|
|
Loading…
Reference in New Issue