diff --git a/adventure/actions/planning.py b/adventure/actions/planning.py index 277f90f..805bd17 100644 --- a/adventure/actions/planning.py +++ b/adventure/actions/planning.py @@ -1,4 +1,4 @@ -from adventure.context import action_context, get_agent_for_character, get_current_step +from adventure.context import action_context, get_agent_for_character, get_current_turn from adventure.errors import ActionError from adventure.models.config import DEFAULT_CONFIG from adventure.models.planning import CalendarEvent @@ -148,7 +148,7 @@ def check_calendar(count: int): """ count = min(count, character_config.event_limit) - current_turn = get_current_step() + current_turn = get_current_turn() with action_context() as (_, action_character): if len(action_character.planner.calendar.events) == 0: diff --git a/adventure/context.py b/adventure/context.py index 0c38cfc..96cf509 100644 --- a/adventure/context.py +++ b/adventure/context.py @@ -25,7 +25,7 @@ from adventure.utils.string import normalize_name logger = getLogger(__name__) # world context -current_step = 0 +current_turn = 0 current_world: World | None = None current_room: Room | None = None current_character: Character | None = None @@ -140,8 +140,8 @@ def get_current_character() -> Character | None: return current_character -def get_current_step() -> int: - return current_step +def get_current_turn() -> int: + return current_turn def get_dungeon_master() -> Agent: @@ -180,9 +180,9 @@ def set_current_character(character: Character | None): current_character = character -def set_current_step(step: int): - global current_step - current_step = step +def set_current_turn(turn: int): + global current_turn + current_turn = turn def set_character_agent(name, character, agent): diff --git a/adventure/game_system.py b/adventure/game_system.py index 9e38ee7..3d34b79 100644 --- a/adventure/game_system.py +++ b/adventure/game_system.py @@ -39,9 +39,9 @@ class SystemInitialize(Protocol): class SystemSimulate(Protocol): - def __call__(self, world: World, step: int, data: Any | None = None) -> None: + def __call__(self, world: World, turn: int, data: Any | None = None) -> None: """ - Simulate the world for the given step. + Simulate the world for the given turn. If this system has stored data, it will be passed in. """ ... diff --git a/adventure/main.py b/adventure/main.py index a4f0dbc..953c96c 100644 --- a/adventure/main.py +++ b/adventure/main.py @@ -32,7 +32,7 @@ load_dotenv(environ.get("ADVENTURE_ENV", ".env"), override=True) if True: from adventure.context import ( get_system_data, - set_current_step, + set_current_turn, set_dungeon_master, set_system_data, subscribe, @@ -142,10 +142,10 @@ def parse_args(): help="The file to save the world state to. Defaults to $world.state.json, if not set", ) parser.add_argument( - "--steps", + "--turns", type=int_or_inf, default=10, - help="The number of simulation steps to run", + help="The number of simulation turns to run", ) parser.add_argument( "--systems", @@ -223,7 +223,7 @@ def load_or_generate_world( add_rooms = args.add_rooms memory = {} - step = 0 + turn = 0 # prepare an agent for the world builder llm = agent_easy_connect() @@ -241,11 +241,11 @@ def load_or_generate_world( with open(world_state_file, "r") as f: state = WorldState(**load_yaml(f)) - set_current_step(state.step) + set_current_turn(state.turn) load_or_initialize_system_data(args, systems, state.world) memory = state.memory - step = state.step + turn = state.turn world = state.world elif path.exists(world_file): logger.info(f"loading world from {world_file}") @@ -276,7 +276,7 @@ def load_or_generate_world( link_rooms(world_builder, world, systems, new_rooms) create_agents(world, memory=memory, players=players) - return (world, world_state_file, step) + return (world, world_state_file, turn) def main(): @@ -356,20 +356,20 @@ def main(): # load or generate the world world_prompt = get_world_prompt(args) - world, world_state_file, world_step = load_or_generate_world( + world, world_state_file, world_turn = load_or_generate_world( args, players, extra_systems, world_prompt=world_prompt ) # make sure the snapshot system runs last - def snapshot_system(world: World, step: int, data: None = None) -> None: + def snapshot_system(world: World, turn: int, data: None = None) -> None: logger.info("taking snapshot of world state") - save_world_state(world, step, world_state_file) + save_world_state(world, turn, world_state_file) extra_systems.append(GameSystem(name="snapshot", simulate=snapshot_system)) # hack: send a snapshot to the websocket server if args.server: - server_system(world, world_step) + server_system(world, world_turn) # create the DM llm = agent_easy_connect() @@ -390,7 +390,7 @@ def main(): logger.debug("simulating world: %s", world) simulate_world( world, - steps=args.steps, + turns=args.turns, actions=extra_actions, systems=extra_systems, ) diff --git a/adventure/models/config.py b/adventure/models/config.py index 044d832..68041f4 100644 --- a/adventure/models/config.py +++ b/adventure/models/config.py @@ -58,7 +58,7 @@ class WorldSizeConfig: @dataclass -class WorldStepConfig: +class WorldTurnConfig: action_retries: int planning_steps: int planning_retries: int @@ -68,7 +68,7 @@ class WorldStepConfig: class WorldConfig: character: WorldCharacterConfig size: WorldSizeConfig - step: WorldStepConfig + turn: WorldTurnConfig @dataclass @@ -109,7 +109,7 @@ DEFAULT_CONFIG = Config( room_characters=IntRange(min=1, max=3), room_items=IntRange(min=1, max=3), ), - step=WorldStepConfig( + turn=WorldTurnConfig( action_retries=5, planning_steps=3, planning_retries=3, diff --git a/adventure/models/entity.py b/adventure/models/entity.py index fd8ce8d..ed2ced5 100644 --- a/adventure/models/entity.py +++ b/adventure/models/entity.py @@ -74,7 +74,7 @@ class World(BaseModel): @dataclass class WorldState(BaseModel): memory: Dict[str, List[str | Dict[str, str]]] - step: int + turn: int world: World id: str = Field(default_factory=uuid) type: Literal["world_state"] = "world_state" diff --git a/adventure/models/event.py b/adventure/models/event.py index c84bbc3..6d2da41 100644 --- a/adventure/models/event.py +++ b/adventure/models/event.py @@ -125,7 +125,7 @@ class SnapshotEvent(BaseModel): world: Dict[str, Any] memory: Dict[str, List[Any]] - step: int + turn: int id: str = Field(default_factory=uuid) type: Literal["snapshot"] = "snapshot" diff --git a/adventure/server/websocket.py b/adventure/server/websocket.py index 59c0dd9..436a6b6 100644 --- a/adventure/server/websocket.py +++ b/adventure/server/websocket.py @@ -301,11 +301,11 @@ async def server_main(): await asyncio.Future() # run forever -def server_system(world: World, step: int, data: Any | None = None): +def server_system(world: World, turn: int, data: Any | None = None): global last_snapshot id = uuid4().hex # TODO: should a server be allowed to generate event IDs? json_state = { - **snapshot_world(world, step), + **snapshot_world(world, turn), "id": id, "type": "snapshot", } diff --git a/adventure/simulate.py b/adventure/simulate.py index b17021f..883c3ef 100644 --- a/adventure/simulate.py +++ b/adventure/simulate.py @@ -34,11 +34,11 @@ from adventure.context import ( broadcast, get_character_agent_for_name, get_character_for_agent, - get_current_step, + get_current_turn, get_current_world, set_current_character, set_current_room, - set_current_step, + set_current_turn, set_current_world, set_game_systems, ) @@ -55,7 +55,7 @@ from adventure.utils.world import describe_entity, format_attributes logger = getLogger(__name__) -step_config = DEFAULT_CONFIG.world.step +turn_config = DEFAULT_CONFIG.world.turn def world_result_parser(value, agent, **kwargs): @@ -154,7 +154,7 @@ def prompt_character_action( toolbox=action_toolbox, ) - logger.debug(f"{character.name} step result: {result}") + logger.debug(f"{character.name} action result: {result}") if agent.memory: # TODO: make sure this is not duplicating memories and wasting space agent.memory.append(result) @@ -173,9 +173,9 @@ def get_notes_events(character: Character, current_turn: int): notes_prompt = "You have no recent notes.\n" if len(upcoming_events) > 0: - current_step = get_current_step() + current_turn = get_current_turn() events = [ - f"{event.name} in {event.turn - current_step} turns" + f"{event.name} in {event.turn - current_turn} turns" for event in upcoming_events ] events = "\n".join(events) @@ -194,7 +194,7 @@ def prompt_character_think( current_turn: int, max_steps: int | None = None, ) -> str: - max_steps = max_steps or step_config.planning_steps + max_steps = max_steps or turn_config.planning_steps notes_prompt, events_prompt = get_notes_events(character, current_turn) @@ -242,7 +242,7 @@ def prompt_character_think( def simulate_world( world: World, - steps: float | int = inf, + turns: float | int = inf, actions: Sequence[Callable[..., str]] = [], systems: Sequence[GameSystem] = [], ): @@ -279,8 +279,8 @@ def simulate_world( # simulate each character for i in count(): - current_step = get_current_step() - logger.info(f"simulating step {i} of {steps} (world step {current_step})") + current_turn = get_current_turn() + logger.info(f"simulating turn {i} of {turns} (world turn {current_turn})") for character_name in world.order: character, agent = get_character_agent_for_name(character_name) @@ -299,13 +299,13 @@ def simulate_world( # decrement effects on the character and remove any that have expired expire_effects(character) - expire_events(character, current_step) + expire_events(character, current_turn) # give the character a chance to think and check their planner if agent.memory and len(agent.memory) > 0: try: thoughts = prompt_character_think( - room, character, agent, planner_toolbox, current_step + room, character, agent, planner_toolbox, current_turn ) logger.debug(f"{character.name} thinks: {thoughts}") except Exception: @@ -313,17 +313,22 @@ def simulate_world( f"error during planning for character {character.name}" ) - result = prompt_character_action( - room, character, agent, action_names, action_tools, current_step - ) - result_event = ResultEvent(result=result, room=room, character=character) - broadcast(result_event) + try: + result = prompt_character_action( + room, character, agent, action_names, action_tools, current_turn + ) + result_event = ResultEvent( + result=result, room=room, character=character + ) + broadcast(result_event) + except Exception: + logger.exception(f"error during action for character {character.name}") for system in systems: if system.simulate: - system.simulate(world, current_step) + system.simulate(world, current_turn) - set_current_step(current_step + 1) - if i >= steps: - logger.info("reached step limit at world step %s", current_step + 1) + set_current_turn(current_turn + 1) + if i >= turns: + logger.info("reached turn limit at world turn %s", current_turn + 1) break diff --git a/adventure/state.py b/adventure/state.py index e7f2a88..5b4e7b0 100644 --- a/adventure/state.py +++ b/adventure/state.py @@ -34,10 +34,10 @@ def create_agents( set_character_agent(character.name, character, agent) -def graph_world(world: World, step: int): +def graph_world(world: World, turn: int): import graphviz - graph_name = f"{path.basename(world.name)}-{step}" + graph_name = f"{path.basename(world.name)}-{turn}" graph = graphviz.Digraph(graph_name, format="png") for room in world.rooms: characters = [character.name for character in room.characters] @@ -50,8 +50,8 @@ def graph_world(world: World, step: int): graph.render(directory=graph_path) -def snapshot_world(world: World, step: int): - # save the world itself, along with the step number of the memory of each agent +def snapshot_world(world: World, turn: int): + # save the world itself, along with the turn number and the memory of each agent json_world = RootModel[World](world).model_dump() json_memory = {} @@ -62,7 +62,7 @@ def snapshot_world(world: World, step: int): return { "world": json_world, "memory": json_memory, - "step": step, + "turn": turn, } @@ -94,9 +94,9 @@ def save_world(world, filename): f.write(json_world) -def save_world_state(world, step, filename): - graph_world(world, step) - json_state = snapshot_world(world, step) +def save_world_state(world, turn, filename): + graph_world(world, turn) + json_state = snapshot_world(world, turn) with open(filename, "w") as f: dump(json_state, f, default=world_json, indent=2) diff --git a/adventure/systems/logic.py b/adventure/systems/logic.py index fcea916..70478f6 100644 --- a/adventure/systems/logic.py +++ b/adventure/systems/logic.py @@ -130,7 +130,7 @@ def update_attributes( def update_logic( world: World, - step: int, + turn: int, data: Any | None = None, *, rules: LogicTable, @@ -190,7 +190,7 @@ def load_logic(filename: str): logger.info("initialized logic system") system_format = wraps(format_logic)(partial(format_logic, rules=logic_rules)) system_initialize = wraps(update_logic)( - partial(update_logic, step=0, rules=logic_rules, triggers=logic_triggers) + partial(update_logic, turn=0, rules=logic_rules, triggers=logic_triggers) ) system_simulate = wraps(update_logic)( partial(update_logic, rules=logic_rules, triggers=logic_triggers) diff --git a/adventure/systems/quest.py b/adventure/systems/quest.py index c55ca10..1c532d8 100644 --- a/adventure/systems/quest.py +++ b/adventure/systems/quest.py @@ -187,7 +187,7 @@ def generate_quests(agent: Agent, theme: str, entity: WorldEntity) -> None: # TODO: generate one new quest -def simulate_quests(world: World, step: int, data: QuestData | None = None) -> None: +def simulate_quests(world: World, turn: int, data: QuestData | None = None) -> None: """ 1. Check for any completed quests. 2. Update any active quests. diff --git a/client/src/app.tsx b/client/src/app.tsx index f6cf6f7..cf15c11 100644 --- a/client/src/app.tsx +++ b/client/src/app.tsx @@ -80,7 +80,7 @@ export function App(props: AppProps) { }); useEffect(() => { - const { setClientId, setActiveTurn, setPlayers, appendEvent, setWorld, world, clientId, setPlayerCharacter: setCharacter } = store.getState(); + const { setClientId, setActiveTurn, setPlayers, appendEvent, setTurn, setWorld, world, clientId, setPlayerCharacter: setCharacter } = store.getState(); if (doesExist(lastMessage)) { const event = JSON.parse(lastMessage.data); @@ -107,6 +107,7 @@ export function App(props: AppProps) { return; case 'snapshot': setWorld(event.world); + setTurn(event.turn); break; default: // this is not concerning, other events are kept in history and displayed diff --git a/client/src/events.tsx b/client/src/events.tsx index 38bddfb..05cc99a 100644 --- a/client/src/events.tsx +++ b/client/src/events.tsx @@ -92,12 +92,12 @@ export function ActionEventItem(props: EventItemProps) { export function SnapshotEventItem(props: EventItemProps) { const { event } = props; - const { step, world } = event; + const { turn, world } = event; const { name, theme } = world; return - {step} + {turn} - Step: {step} + Turn: {turn} World Theme: {theme} diff --git a/client/src/format.ts b/client/src/format.ts index 35c755d..6d4c39d 100644 --- a/client/src/format.ts +++ b/client/src/format.ts @@ -22,7 +22,7 @@ export function formatResult(data: any) { } export function formatWorld(data: any) { - return `${data.world.theme} - ${data.step}`; + return `${data.world.theme} - ${data.turn}`; } export const formatters: Record = { diff --git a/client/src/models.ts b/client/src/models.ts index 02c419d..4d40236 100644 --- a/client/src/models.ts +++ b/client/src/models.ts @@ -50,6 +50,7 @@ export interface World { order: Array; rooms: Array; theme: string; + turn: number; } // TODO: copy event types from server diff --git a/client/src/store.ts b/client/src/store.ts index 9b947a3..c3f4972 100644 --- a/client/src/store.ts +++ b/client/src/store.ts @@ -35,10 +35,12 @@ export interface ClientState { export interface WorldState { players: Record; + turn: Maybe; world: Maybe; // setters setPlayers: (players: Record) => void; + setTurn: (turn: Maybe) => void; setWorld: (world: Maybe) => void; } @@ -105,8 +107,10 @@ export function createClientStore(): StateCreator { export function createWorldStore(): StateCreator { return (set) => ({ players: {}, + turn: undefined, world: undefined, setPlayers: (players) => set({ players }), + setTurn: (turn) => set({ turn }), setWorld: (world) => set({ world }), }); } diff --git a/client/src/world.tsx b/client/src/world.tsx index ed46188..c34e0b7 100644 --- a/client/src/world.tsx +++ b/client/src/world.tsx @@ -1,5 +1,5 @@ import { Maybe, doesExist } from '@apextoaster/js-utils'; -import { Card, CardContent, Typography } from '@mui/material'; +import { Card, CardContent, Divider, Stack, Typography } from '@mui/material'; import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView'; import { TreeItem } from '@mui/x-tree-view/TreeItem'; import React from 'react'; @@ -40,6 +40,7 @@ export function characterStateSelector(s: StoreState) { export function worldStateSelector(s: StoreState) { return { + turn: s.turn, world: s.world, setDetailEntity: s.setDetailEntity, }; @@ -121,7 +122,7 @@ export function RoomItem(props: { room: Room } & BaseEntityItemProps) { export function WorldPanel(props: BaseEntityItemProps) { const { setPlayer } = props; const state = useStore(store, worldStateSelector); - const { world, setDetailEntity } = state; + const { turn, world, setDetailEntity } = state; // eslint-disable-next-line no-restricted-syntax if (!doesExist(world)) { @@ -136,14 +137,20 @@ export function WorldPanel(props: BaseEntityItemProps) { return - {world.name} - - Theme: {world.theme} - - - setDetailEntity(world)} /> - {world.rooms.map((room) => )} - + + {world.name} + + Theme: {world.theme} + + + Turn: {turn} + + + + setDetailEntity(world)} /> + {world.rooms.map((room) => )} + + ; } diff --git a/docs/events.md b/docs/events.md index 9723137..c67dc53 100644 --- a/docs/events.md +++ b/docs/events.md @@ -154,7 +154,7 @@ The snapshot event is fired at the end of each turn and contains a complete snap type: "snapshot" world: Dict memory: Dict -step: int +turn: int ``` This is primarily used to save the world state, but can also be used to sync clients and populate the world menu.