add server and logic systems
This commit is contained in:
parent
65f8c50199
commit
ebf4ccf1c4
|
@ -49,7 +49,8 @@ def generate_item(
|
||||||
|
|
||||||
name = agent(
|
name = agent(
|
||||||
"Generate one item or object that would make sense in the world of {world_theme}. {dest_note}. "
|
"Generate one item or object that would make sense in the world of {world_theme}. {dest_note}. "
|
||||||
'Only respond with the item name, do not include a description or any other text. Do not prefix the name with "the", do not wrap it in quotes. '
|
"Only respond with the item name, do not include a description or any other text. Do not prefix the "
|
||||||
|
'name with "the", do not wrap it in quotes. Do not include the name of the room. '
|
||||||
"Do not create any duplicate items in the same room. Do not give characters any duplicate items. The existing items are: {existing_items}",
|
"Do not create any duplicate items in the same room. Do not give characters any duplicate items. The existing items are: {existing_items}",
|
||||||
dest_note=dest_note,
|
dest_note=dest_note,
|
||||||
existing_items=existing_items,
|
existing_items=existing_items,
|
||||||
|
@ -72,7 +73,8 @@ def generate_actor(
|
||||||
name = agent(
|
name = agent(
|
||||||
"Generate one person or creature that would make sense in the world of {world_theme}. The character will be placed in the {dest_room} room. "
|
"Generate one person or creature that would make sense in the world of {world_theme}. The character will be placed in the {dest_room} room. "
|
||||||
'Only respond with the character name, do not include a description or any other text. Do not prefix the name with "the", do not wrap it in quotes. '
|
'Only respond with the character name, do not include a description or any other text. Do not prefix the name with "the", do not wrap it in quotes. '
|
||||||
"Do not create any duplicate characters in the same room. The existing characters are: {existing_actors}",
|
"Do not include the name of the room. Do not give characters any duplicate names."
|
||||||
|
"Do not create any duplicate characters. The existing characters are: {existing_actors}",
|
||||||
dest_room=dest_room,
|
dest_room=dest_room,
|
||||||
existing_actors=existing_actors,
|
existing_actors=existing_actors,
|
||||||
world_theme=world_theme,
|
world_theme=world_theme,
|
||||||
|
@ -90,22 +92,22 @@ def generate_actor(
|
||||||
name=name,
|
name=name,
|
||||||
)
|
)
|
||||||
|
|
||||||
health = 100
|
|
||||||
actions = {}
|
|
||||||
|
|
||||||
return Actor(
|
return Actor(
|
||||||
name=name,
|
name=name,
|
||||||
backstory=backstory,
|
backstory=backstory,
|
||||||
description=description,
|
description=description,
|
||||||
health=health,
|
actions={},
|
||||||
actions=actions,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def generate_world(
|
def generate_world(
|
||||||
agent: Agent, name: str, theme: str, rooms: int | None = None, max_rooms: int = 5
|
agent: Agent,
|
||||||
|
name: str,
|
||||||
|
theme: str,
|
||||||
|
room_count: int | None = None,
|
||||||
|
max_rooms: int = 5,
|
||||||
) -> World:
|
) -> World:
|
||||||
room_count = rooms or randint(3, max_rooms)
|
room_count = room_count or randint(3, max_rooms)
|
||||||
logger.info(f"Generating a {theme} with {room_count} rooms")
|
logger.info(f"Generating a {theme} with {room_count} rooms")
|
||||||
|
|
||||||
existing_actors: List[str] = []
|
existing_actors: List[str] = []
|
||||||
|
@ -150,7 +152,7 @@ def generate_world(
|
||||||
"west": "east",
|
"west": "east",
|
||||||
}
|
}
|
||||||
|
|
||||||
# TODO: generate portals to link the rooms together
|
# generate portals to link the rooms together
|
||||||
for room in rooms:
|
for room in rooms:
|
||||||
directions = ["north", "south", "east", "west"]
|
directions = ["north", "south", "east", "west"]
|
||||||
for direction in directions:
|
for direction in directions:
|
||||||
|
@ -180,4 +182,6 @@ def generate_world(
|
||||||
room.portals[direction] = dest_room.name
|
room.portals[direction] = dest_room.name
|
||||||
dest_room.portals[opposite_directions[direction]] = room.name
|
dest_room.portals[opposite_directions[direction]] = room.name
|
||||||
|
|
||||||
return World(name=name, rooms=rooms, theme=theme)
|
# ensure actors act in a stable order
|
||||||
|
order = [actor.name for room in rooms for actor in room.actors]
|
||||||
|
return World(name=name, rooms=rooms, theme=theme, order=order)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
from json import load
|
from json import load
|
||||||
from os import environ, path
|
from os import environ, path
|
||||||
|
from typing import Callable, Dict, Sequence, Tuple
|
||||||
|
|
||||||
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
|
||||||
|
@ -18,8 +19,8 @@ from adventure.actions import (
|
||||||
action_tell,
|
action_tell,
|
||||||
)
|
)
|
||||||
from adventure.context import (
|
from adventure.context import (
|
||||||
|
get_actor_agent_for_name,
|
||||||
get_actor_for_agent,
|
get_actor_for_agent,
|
||||||
get_agent_for_actor,
|
|
||||||
get_current_world,
|
get_current_world,
|
||||||
get_step,
|
get_step,
|
||||||
set_current_actor,
|
set_current_actor,
|
||||||
|
@ -28,7 +29,7 @@ from adventure.context import (
|
||||||
set_step,
|
set_step,
|
||||||
)
|
)
|
||||||
from adventure.generate import generate_world
|
from adventure.generate import generate_world
|
||||||
from adventure.models import World, WorldState
|
from adventure.models import Actor, Room, World, WorldState
|
||||||
from adventure.state import create_agents, save_world, save_world_state
|
from adventure.state import create_agents, save_world, save_world_state
|
||||||
|
|
||||||
logger = logger_with_colors(__name__)
|
logger = logger_with_colors(__name__)
|
||||||
|
@ -57,13 +58,20 @@ 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 simulate_world(world: World, steps: int = 10, callback=None, extra_actions=[]):
|
def simulate_world(
|
||||||
|
world: World,
|
||||||
|
steps: int = 10,
|
||||||
|
actions: Sequence[Callable[..., str]] = [],
|
||||||
|
systems: Sequence[
|
||||||
|
Tuple[Callable[[World, int], None], Callable[[Dict[str, str]], str] | None]
|
||||||
|
] = [],
|
||||||
|
input_callbacks: Sequence[Callable[[Room, Actor, str], None]] = [],
|
||||||
|
result_callbacks: Sequence[Callable[[Room, Actor, str], None]] = [],
|
||||||
|
):
|
||||||
logger.info("Simulating the world")
|
logger.info("Simulating the world")
|
||||||
|
set_current_world(world)
|
||||||
|
|
||||||
# collect actors, so they are only processed once
|
# build a toolbox for the actions
|
||||||
all_actors = [actor for room in world.rooms for actor in room.actors]
|
|
||||||
|
|
||||||
# TODO: add actions for: drop, use, attack, cast, jump, climb, swim, fly, etc.
|
|
||||||
action_tools = Toolbox(
|
action_tools = Toolbox(
|
||||||
[
|
[
|
||||||
action_ask,
|
action_ask,
|
||||||
|
@ -72,40 +80,51 @@ def simulate_world(world: World, steps: int = 10, callback=None, extra_actions=[
|
||||||
action_move,
|
action_move,
|
||||||
action_take,
|
action_take,
|
||||||
action_tell,
|
action_tell,
|
||||||
*extra_actions,
|
*actions,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
action_names = action_tools.list_tools()
|
action_names = action_tools.list_tools()
|
||||||
|
|
||||||
# create a result parser that will memorize the actor and room
|
|
||||||
set_current_world(world)
|
|
||||||
|
|
||||||
# simulate each actor
|
# simulate each actor
|
||||||
for i in range(steps):
|
for i in range(steps):
|
||||||
current_step = get_step()
|
current_step = get_step()
|
||||||
logger.info(f"Simulating step {current_step}")
|
logger.info(f"Simulating step {current_step}")
|
||||||
for actor in all_actors:
|
for actor_name in world.order:
|
||||||
agent = get_agent_for_actor(actor)
|
actor, agent = get_actor_agent_for_name(actor_name)
|
||||||
if not agent:
|
if not agent or not actor:
|
||||||
logger.error(f"Agent not found for actor {actor.name}")
|
logger.error(f"Agent or actor not found for name {actor_name}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
room = next((room for room in world.rooms if actor in room.actors), None)
|
room = next((room for room in world.rooms if actor in room.actors), None)
|
||||||
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
|
||||||
|
|
||||||
room_actors = [actor.name for actor in room.actors]
|
room_actors = [actor.name for actor in room.actors]
|
||||||
room_items = [item.name for item in room.items]
|
room_items = [item.name for item in room.items]
|
||||||
room_directions = list(room.portals.keys())
|
room_directions = list(room.portals.keys())
|
||||||
|
|
||||||
logger.info("starting actor %s turn", actor.name)
|
actor_attributes = " ".join(
|
||||||
|
system_format(actor.attributes)
|
||||||
|
for _, system_format in systems
|
||||||
|
if system_format
|
||||||
|
)
|
||||||
|
actor_items = [item.name for item in actor.items]
|
||||||
|
|
||||||
|
def result_parser(value, agent, **kwargs):
|
||||||
|
for callback in input_callbacks:
|
||||||
|
callback(room, actor, value)
|
||||||
|
|
||||||
|
return world_result_parser(value, agent, **kwargs)
|
||||||
|
|
||||||
|
logger.info("starting turn for actor: %s", actor_name)
|
||||||
result = loop_retry(
|
result = loop_retry(
|
||||||
agent,
|
agent,
|
||||||
(
|
(
|
||||||
"You are currently in {room_name}. {room_description}. "
|
"You are currently in {room_name}. {room_description}. {attributes}. "
|
||||||
"The room contains the following characters: {actors}. "
|
"The room contains the following characters: {visible_actors}. "
|
||||||
"The room contains the following items: {items}. "
|
"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 take the following actions: {actions}. "
|
||||||
"You can move in the following directions: {directions}. "
|
"You can move in the following directions: {directions}. "
|
||||||
"What will you do next? Reply with a JSON function call, calling one of the actions."
|
"What will you do next? Reply with a JSON function call, calling one of the actions."
|
||||||
|
@ -114,21 +133,26 @@ def simulate_world(world: World, steps: int = 10, callback=None, extra_actions=[
|
||||||
),
|
),
|
||||||
context={
|
context={
|
||||||
"actions": action_names,
|
"actions": action_names,
|
||||||
"actors": room_actors,
|
"actor_items": actor_items,
|
||||||
|
"attributes": actor_attributes,
|
||||||
"directions": room_directions,
|
"directions": room_directions,
|
||||||
"items": room_items,
|
|
||||||
"room_name": room.name,
|
"room_name": room.name,
|
||||||
"room_description": room.description,
|
"room_description": room.description,
|
||||||
|
"visible_actors": room_actors,
|
||||||
|
"visible_items": room_items,
|
||||||
},
|
},
|
||||||
result_parser=world_result_parser,
|
result_parser=result_parser,
|
||||||
toolbox=action_tools,
|
toolbox=action_tools,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug(f"{actor.name} step result: {result}")
|
logger.debug(f"{actor.name} step result: {result}")
|
||||||
agent.memory.append(result)
|
agent.memory.append(result)
|
||||||
|
|
||||||
if callback:
|
for callback in result_callbacks:
|
||||||
callback(world, current_step)
|
callback(room, actor, result)
|
||||||
|
|
||||||
|
for system_update, _ in systems:
|
||||||
|
system_update(world, current_step)
|
||||||
|
|
||||||
set_step(current_step + 1)
|
set_step(current_step + 1)
|
||||||
|
|
||||||
|
@ -138,10 +162,13 @@ def parse_args():
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Generate and simulate a fantasy world"
|
description="Generate and simulate a text adventure world"
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--actions", type=str, help="Extra actions to include in the simulation"
|
"--actions",
|
||||||
|
type=str,
|
||||||
|
nargs="*",
|
||||||
|
help="Extra actions to include in the simulation",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--flavor", type=str, help="Some additional flavor text for the generated world"
|
"--flavor", type=str, help="Some additional flavor text for the generated world"
|
||||||
|
@ -155,6 +182,9 @@ def parse_args():
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--max-rooms", type=int, help="The maximum number of rooms to generate"
|
"--max-rooms", type=int, help="The maximum number of rooms to generate"
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--server", type=str, help="The address on which to run the server"
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--state",
|
"--state",
|
||||||
type=str,
|
type=str,
|
||||||
|
@ -164,6 +194,12 @@ def parse_args():
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--steps", type=int, default=10, help="The number of simulation steps to run"
|
"--steps", type=int, default=10, help="The number of simulation steps to run"
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--systems",
|
||||||
|
type=str,
|
||||||
|
nargs="*",
|
||||||
|
help="Extra logic systems to run in the simulation",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--theme", type=str, default="fantasy", help="The theme of the generated world"
|
"--theme", type=str, default="fantasy", help="The theme of the generated world"
|
||||||
)
|
)
|
||||||
|
@ -211,7 +247,11 @@ def main():
|
||||||
llm,
|
llm,
|
||||||
)
|
)
|
||||||
world = generate_world(
|
world = generate_world(
|
||||||
agent, args.world, args.theme, rooms=args.rooms, max_rooms=args.max_rooms
|
agent,
|
||||||
|
args.world,
|
||||||
|
args.theme,
|
||||||
|
room_count=args.rooms,
|
||||||
|
max_rooms=args.max_rooms,
|
||||||
)
|
)
|
||||||
save_world(world, world_file)
|
save_world(world, world_file)
|
||||||
|
|
||||||
|
@ -219,25 +259,63 @@ def main():
|
||||||
|
|
||||||
# load extra actions
|
# load extra actions
|
||||||
extra_actions = []
|
extra_actions = []
|
||||||
if args.actions:
|
for action_name in args.actions:
|
||||||
logger.info(f"Loading extra actions from {args.actions}")
|
logger.info(f"Loading extra actions from {action_name}")
|
||||||
action_module, action_function = args.actions.rsplit(":", 1)
|
module_actions = load_plugin(action_name)
|
||||||
action_module = import_module(action_module)
|
|
||||||
action_function = getattr(action_module, action_function)
|
|
||||||
module_actions = action_function()
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Loaded extra actions: {[action.__name__ for action in module_actions]}"
|
f"Loaded extra actions: {[action.__name__ for action in module_actions]}"
|
||||||
)
|
)
|
||||||
extra_actions.extend(module_actions)
|
extra_actions.extend(module_actions)
|
||||||
|
|
||||||
|
# load extra systems
|
||||||
|
def snapshot_system(world: World, step: int) -> None:
|
||||||
|
logger.debug("Snapshotting world state")
|
||||||
|
save_world_state(world, step, world_state_file)
|
||||||
|
|
||||||
|
extra_systems = [(snapshot_system, None)]
|
||||||
|
for system_name in args.systems:
|
||||||
|
logger.info(f"Loading extra systems from {system_name}")
|
||||||
|
module_systems = load_plugin(system_name)
|
||||||
|
logger.info(
|
||||||
|
f"Loaded extra systems: {[system.__name__ for system in module_systems]}"
|
||||||
|
)
|
||||||
|
extra_systems.append(module_systems)
|
||||||
|
|
||||||
|
# make sure the server system is last
|
||||||
|
input_callbacks = []
|
||||||
|
result_callbacks = []
|
||||||
|
|
||||||
|
if args.server:
|
||||||
|
from adventure.server import (
|
||||||
|
launch_server,
|
||||||
|
server_input,
|
||||||
|
server_result,
|
||||||
|
server_system,
|
||||||
|
)
|
||||||
|
|
||||||
|
launch_server()
|
||||||
|
extra_systems.append((server_system, None))
|
||||||
|
input_callbacks.append(server_input)
|
||||||
|
result_callbacks.append(server_result)
|
||||||
|
|
||||||
|
# start the sim
|
||||||
logger.debug("Simulating world: %s", world)
|
logger.debug("Simulating world: %s", world)
|
||||||
simulate_world(
|
simulate_world(
|
||||||
world,
|
world,
|
||||||
steps=args.steps,
|
steps=args.steps,
|
||||||
callback=lambda w, s: save_world_state(w, s, world_state_file),
|
actions=extra_actions,
|
||||||
extra_actions=extra_actions,
|
systems=extra_systems,
|
||||||
|
input_callbacks=input_callbacks,
|
||||||
|
result_callbacks=result_callbacks,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def load_plugin(name):
|
||||||
|
module_name, function_name = name.rsplit(":", 1)
|
||||||
|
plugin_module = import_module(module_name)
|
||||||
|
plugin_entry = getattr(plugin_module, function_name)
|
||||||
|
return plugin_entry()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
|
@ -16,6 +16,7 @@ class Item:
|
||||||
name: str
|
name: str
|
||||||
description: str
|
description: str
|
||||||
actions: Actions = Field(default_factory=dict)
|
actions: Actions = Field(default_factory=dict)
|
||||||
|
attributes: Dict[str, str] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -23,9 +24,9 @@ class Actor:
|
||||||
name: str
|
name: str
|
||||||
backstory: str
|
backstory: str
|
||||||
description: str
|
description: str
|
||||||
health: int
|
|
||||||
actions: Actions = Field(default_factory=dict)
|
actions: Actions = Field(default_factory=dict)
|
||||||
items: List[Item] = Field(default_factory=list)
|
items: List[Item] = Field(default_factory=list)
|
||||||
|
attributes: Dict[str, str] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -36,17 +37,19 @@ class Room:
|
||||||
items: List[Item] = Field(default_factory=list)
|
items: List[Item] = Field(default_factory=list)
|
||||||
actors: List[Actor] = Field(default_factory=list)
|
actors: List[Actor] = Field(default_factory=list)
|
||||||
actions: Actions = Field(default_factory=dict)
|
actions: Actions = Field(default_factory=dict)
|
||||||
|
attributes: Dict[str, str] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class World:
|
class World:
|
||||||
name: str
|
name: str
|
||||||
|
order: List[str]
|
||||||
rooms: List[Room]
|
rooms: List[Room]
|
||||||
theme: str
|
theme: str
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class WorldState:
|
class WorldState:
|
||||||
world: World
|
|
||||||
memory: Dict[str, List[str | Dict[str, str]]]
|
memory: Dict[str, List[str | Dict[str, str]]]
|
||||||
step: int
|
step: int
|
||||||
|
world: World
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
import asyncio
|
||||||
|
from json import dumps
|
||||||
|
from logging import getLogger
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
import websockets
|
||||||
|
from flask import Flask, send_from_directory
|
||||||
|
|
||||||
|
from adventure.models import Actor, Room, World
|
||||||
|
from adventure.state import snapshot_world, world_json
|
||||||
|
|
||||||
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
connected = set()
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/<path:page>")
|
||||||
|
def send_report(page: str):
|
||||||
|
print(f"Sending {page}")
|
||||||
|
return send_from_directory(
|
||||||
|
"/home/ssube/code/github/ssube/llm-adventure/web-ui", page
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def handler(websocket):
|
||||||
|
connected.add(websocket)
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
# await websocket.wait_closed()
|
||||||
|
message = await websocket.recv()
|
||||||
|
print(message)
|
||||||
|
except websockets.ConnectionClosedOK:
|
||||||
|
break
|
||||||
|
|
||||||
|
connected.remove(websocket)
|
||||||
|
|
||||||
|
|
||||||
|
socket_thread = None
|
||||||
|
static_thread = None
|
||||||
|
|
||||||
|
|
||||||
|
def launch_server():
|
||||||
|
global socket_thread, static_thread
|
||||||
|
|
||||||
|
def run_sockets():
|
||||||
|
asyncio.run(server_main())
|
||||||
|
|
||||||
|
def run_static():
|
||||||
|
app.run(port=8000)
|
||||||
|
|
||||||
|
socket_thread = Thread(target=run_sockets)
|
||||||
|
socket_thread.start()
|
||||||
|
|
||||||
|
static_thread = Thread(target=run_static)
|
||||||
|
static_thread.start()
|
||||||
|
|
||||||
|
|
||||||
|
async def server_main():
|
||||||
|
async with websockets.serve(handler, "", 8001):
|
||||||
|
logger.info("Server started")
|
||||||
|
await asyncio.Future() # run forever
|
||||||
|
|
||||||
|
|
||||||
|
def server_system(world: World, step: int):
|
||||||
|
json_state = {
|
||||||
|
**snapshot_world(world, step),
|
||||||
|
"type": "world",
|
||||||
|
}
|
||||||
|
websockets.broadcast(connected, dumps(json_state, default=world_json))
|
||||||
|
|
||||||
|
|
||||||
|
def server_result(room: Room, actor: Actor, action: str):
|
||||||
|
json_action = {
|
||||||
|
"actor": actor.name,
|
||||||
|
"result": action,
|
||||||
|
"room": room.name,
|
||||||
|
"type": "result",
|
||||||
|
}
|
||||||
|
websockets.broadcast(connected, dumps(json_action))
|
||||||
|
|
||||||
|
|
||||||
|
def server_input(room: Room, actor: Actor, message: str):
|
||||||
|
json_input = {
|
||||||
|
"actor": actor.name,
|
||||||
|
"input": message,
|
||||||
|
"room": room.name,
|
||||||
|
"type": "input",
|
||||||
|
}
|
||||||
|
websockets.broadcast(connected, dumps(json_input))
|
|
@ -95,14 +95,14 @@ def save_world_state(world, step, filename):
|
||||||
graph_world(world, step)
|
graph_world(world, step)
|
||||||
json_state = snapshot_world(world, step)
|
json_state = snapshot_world(world, step)
|
||||||
with open(filename, "w") as f:
|
with open(filename, "w") as f:
|
||||||
|
dump(json_state, f, default=world_json, indent=2)
|
||||||
|
|
||||||
def dumper(obj):
|
|
||||||
if isinstance(obj, BaseMessage):
|
|
||||||
return {
|
|
||||||
"content": obj.content,
|
|
||||||
"type": obj.type,
|
|
||||||
}
|
|
||||||
|
|
||||||
raise ValueError(f"Cannot serialize {obj}")
|
def world_json(obj):
|
||||||
|
if isinstance(obj, BaseMessage):
|
||||||
|
return {
|
||||||
|
"content": obj.content,
|
||||||
|
"type": obj.type,
|
||||||
|
}
|
||||||
|
|
||||||
dump(json_state, f, default=dumper, indent=2)
|
raise ValueError(f"Cannot serialize {obj}")
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
from logging import getLogger
|
||||||
|
from random import random
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
from pydantic import Field
|
||||||
|
from yaml import Loader, load
|
||||||
|
|
||||||
|
from adventure.models import World, dataclass
|
||||||
|
|
||||||
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LogicLabel:
|
||||||
|
backstory: str
|
||||||
|
description: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LogicRule:
|
||||||
|
match: Dict[str, str]
|
||||||
|
chance: float = 1.0
|
||||||
|
remove: Optional[List[str]] = None
|
||||||
|
set: Optional[Dict[str, str]] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LogicTable:
|
||||||
|
rules: List[LogicRule]
|
||||||
|
labels: Dict[str, Dict[str, LogicLabel]] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
with open("./worlds/logic.yaml") as file:
|
||||||
|
logic_rules = LogicTable(**load(file, Loader=Loader))
|
||||||
|
|
||||||
|
|
||||||
|
def update_attributes(
|
||||||
|
attributes: Dict[str, str], dataset: LogicTable
|
||||||
|
) -> Dict[str, str]:
|
||||||
|
for rule in dataset.rules:
|
||||||
|
if rule.match.items() <= attributes.items():
|
||||||
|
logger.info("matched logic: %s", rule.match)
|
||||||
|
if rule.chance < 1:
|
||||||
|
if random() > rule.chance:
|
||||||
|
logger.info("logic skipped by chance: %s", rule.chance)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if rule.set:
|
||||||
|
attributes.update(rule.set)
|
||||||
|
logger.info("logic set state: %s", rule.set)
|
||||||
|
|
||||||
|
for key in rule.remove or []:
|
||||||
|
attributes.pop(key, None)
|
||||||
|
|
||||||
|
return attributes
|
||||||
|
|
||||||
|
|
||||||
|
def update_logic(world: World, step: int) -> None:
|
||||||
|
for room in world.rooms:
|
||||||
|
room.attributes = update_attributes(room.attributes, logic_rules)
|
||||||
|
for actor in room.actors:
|
||||||
|
actor.attributes = update_attributes(actor.attributes, logic_rules)
|
||||||
|
for item in actor.items:
|
||||||
|
item.attributes = update_attributes(item.attributes, logic_rules)
|
||||||
|
for item in room.items:
|
||||||
|
item.attributes = update_attributes(item.attributes, logic_rules)
|
||||||
|
|
||||||
|
logger.info("updated world attributes")
|
||||||
|
|
||||||
|
|
||||||
|
def format_logic(attributes: Dict[str, str], self=True) -> str:
|
||||||
|
labels = []
|
||||||
|
|
||||||
|
for attribute, value in attributes.items():
|
||||||
|
if attribute in logic_rules.labels and value in logic_rules.labels[attribute]:
|
||||||
|
label = logic_rules.labels[attribute][value]
|
||||||
|
if self:
|
||||||
|
labels.append(label.backstory)
|
||||||
|
else:
|
||||||
|
labels.append(label.description)
|
||||||
|
|
||||||
|
if len(labels) > 0:
|
||||||
|
logger.info("adding labels: %s", labels)
|
||||||
|
|
||||||
|
return " ".join(labels)
|
||||||
|
|
||||||
|
|
||||||
|
def init():
|
||||||
|
logger.info("initialized logic system")
|
||||||
|
return (update_logic, format_logic)
|
Loading…
Reference in New Issue