use the same events on the react client
This commit is contained in:
parent
94e02ebfe1
commit
90912d2bfe
15
README.md
15
README.md
|
@ -125,19 +125,20 @@ python3 -m adventure.main \
|
||||||
# --systems adventure.custom_systems:init_logic
|
# --systems adventure.custom_systems:init_logic
|
||||||
```
|
```
|
||||||
|
|
||||||
This will generate a relatively small world with 3 rooms or areas, run for 30 steps, then shut down. The world will be
|
This will generate a relatively small world with 3 rooms or areas, run for 30 steps, then shut down.
|
||||||
saved to a file named `worlds/outback-animals-1.json` and the state will be saved after each step to another file named
|
|
||||||
`worlds/outback-animals-1.state.json`. The world can be stopped at any time by pressing Ctrl-C, although the step in
|
The world will be saved to a file named `worlds/outback-animals-1.json` and the state will be saved after each step to
|
||||||
progress will be lost. The saved state can be resumed and played for any number of additional steps.
|
another file named `worlds/outback-animals-1.state.json`. The world can be stopped at any time by pressing Ctrl-C,
|
||||||
|
although the step in progress will be lost. The saved state can be resumed and played for any number of additional
|
||||||
|
steps by running the server again with the same arguments.
|
||||||
|
|
||||||
> Note: `module.name:function_name` and `path/filename.yml:key` are patterns you will see repeated throughout TaleWeave AI.
|
> Note: `module.name:function_name` and `path/filename.yml:key` are patterns you will see repeated throughout TaleWeave AI.
|
||||||
> They indicate a Python module and function within it, or a data file and key within it, respectively.
|
> They indicate a Python module and function within it, or a data file and key within it, respectively.
|
||||||
|
|
||||||
The `sim_systems` provide many mechanics from popular life simulations, including hunger, thirst, exhaustion, and mood.
|
The `sim_systems` provide many mechanics from popular life simulations, including hunger, thirst, exhaustion, and mood.
|
||||||
Custom actions and systems can be used to provide any other mechanics that are desired for your setting. The logic
|
Custom actions and systems can be used to provide any other mechanics that are desired for your setting. The logic
|
||||||
system uses a combination of Python and YAML to build complex systems that add and modify the attributes on rooms,
|
system uses a combination of Python and YAML to modify the prompts connected to rooms, characters, and items in the
|
||||||
characters, and items. Attributes can become sentences and fragments in the character prompt and entity description,
|
world, influencing the behavior of the language models.
|
||||||
allowing the logic to influence the language models.
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,9 @@ from typing import Callable, Dict, Tuple
|
||||||
from packit.agent import Agent
|
from packit.agent import Agent
|
||||||
|
|
||||||
from adventure.models.entity import Actor, Room, World
|
from adventure.models.entity import Actor, Room, World
|
||||||
|
from adventure.models.event import GameEvent
|
||||||
|
|
||||||
current_broadcast: Callable[[str], None] | None = None
|
current_broadcast: Callable[[str | GameEvent], None] | None = None
|
||||||
current_world: World | None = None
|
current_world: World | None = None
|
||||||
current_room: Room | None = None
|
current_room: Room | None = None
|
||||||
current_actor: Actor | None = None
|
current_actor: Actor | None = None
|
||||||
|
@ -16,7 +17,7 @@ dungeon_master: Agent | None = None
|
||||||
actor_agents: Dict[str, Tuple[Actor, Agent]] = {}
|
actor_agents: Dict[str, Tuple[Actor, Agent]] = {}
|
||||||
|
|
||||||
|
|
||||||
def broadcast(message: str):
|
def broadcast(message: str | GameEvent):
|
||||||
if current_broadcast:
|
if current_broadcast:
|
||||||
current_broadcast(message)
|
current_broadcast(message)
|
||||||
|
|
||||||
|
|
|
@ -3,11 +3,11 @@ from os import environ
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
from re import sub
|
from re import sub
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
from discord import Client, Embed, File, Intents
|
from discord import Client, Embed, File, Intents
|
||||||
|
|
||||||
from adventure.context import (
|
from adventure.context import (
|
||||||
|
broadcast,
|
||||||
get_actor_agent_for_name,
|
get_actor_agent_for_name,
|
||||||
get_current_world,
|
get_current_world,
|
||||||
set_actor_agent,
|
set_actor_agent,
|
||||||
|
@ -16,6 +16,7 @@ from adventure.models.event import (
|
||||||
ActionEvent,
|
ActionEvent,
|
||||||
GameEvent,
|
GameEvent,
|
||||||
GenerateEvent,
|
GenerateEvent,
|
||||||
|
PlayerEvent,
|
||||||
PromptEvent,
|
PromptEvent,
|
||||||
ReplyEvent,
|
ReplyEvent,
|
||||||
ResultEvent,
|
ResultEvent,
|
||||||
|
@ -28,7 +29,7 @@ logger = getLogger(__name__)
|
||||||
client = None
|
client = None
|
||||||
|
|
||||||
active_tasks = set()
|
active_tasks = set()
|
||||||
prompt_queue: Queue[Tuple[GameEvent, Embed | str]] = Queue()
|
event_queue: Queue[GameEvent] = Queue()
|
||||||
|
|
||||||
|
|
||||||
def remove_tags(text: str) -> str:
|
def remove_tags(text: str) -> str:
|
||||||
|
@ -156,8 +157,7 @@ class AdventureClient(Client):
|
||||||
event.prompt,
|
event.prompt,
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: build an embed from the prompt
|
event_queue.put(event)
|
||||||
prompt_queue.put((event, event.prompt))
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
player = RemotePlayer(
|
player = RemotePlayer(
|
||||||
|
@ -167,25 +167,25 @@ class AdventureClient(Client):
|
||||||
set_player(user_name, player)
|
set_player(user_name, player)
|
||||||
|
|
||||||
logger.info(f"{user_name} has joined the game as {actor.name}!")
|
logger.info(f"{user_name} has joined the game as {actor.name}!")
|
||||||
await message.channel.send(
|
join_event = PlayerEvent("join", character_name, user_name)
|
||||||
f"{user_name} has joined the game as {actor.name}!"
|
return broadcast(join_event)
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
if message.content.startswith("!leave"):
|
|
||||||
# TODO: revert to LLM agent
|
|
||||||
logger.info(f"{user_name} has left the game!")
|
|
||||||
await message.channel.send(f"{user_name} has left the game!")
|
|
||||||
return
|
|
||||||
|
|
||||||
player = get_player(user_name)
|
player = get_player(user_name)
|
||||||
if player and isinstance(player, RemotePlayer):
|
if player:
|
||||||
content = remove_tags(message.content)
|
if message.content.startswith("!leave"):
|
||||||
player.input_queue.put(content)
|
# TODO: check if player is playing
|
||||||
logger.info(
|
# TODO: revert to LLM agent
|
||||||
f"Received message from {user_name} for {player.name}: {content}"
|
logger.info(f"{user_name} has left the game!")
|
||||||
)
|
leave_event = PlayerEvent("leave", player.name, user_name)
|
||||||
return
|
return broadcast(leave_event)
|
||||||
|
|
||||||
|
if isinstance(player, RemotePlayer):
|
||||||
|
content = remove_tags(message.content)
|
||||||
|
player.input_queue.put(content)
|
||||||
|
logger.info(
|
||||||
|
f"Received message from {user_name} for {player.name}: {content}"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
await message.channel.send(
|
await message.channel.send(
|
||||||
"You are not currently playing Adventure! Type `!join` to start playing!"
|
"You are not currently playing Adventure! Type `!join` to start playing!"
|
||||||
|
@ -194,41 +194,49 @@ class AdventureClient(Client):
|
||||||
|
|
||||||
|
|
||||||
def launch_bot():
|
def launch_bot():
|
||||||
|
global client
|
||||||
|
|
||||||
|
intents = Intents.default()
|
||||||
|
# intents.message_content = True
|
||||||
|
|
||||||
|
client = AdventureClient(intents=intents)
|
||||||
|
|
||||||
def bot_main():
|
def bot_main():
|
||||||
global client
|
if not client:
|
||||||
|
raise ValueError("No Discord client available")
|
||||||
|
|
||||||
intents = Intents.default()
|
|
||||||
# intents.message_content = True
|
|
||||||
|
|
||||||
client = AdventureClient(intents=intents)
|
|
||||||
client.run(environ["DISCORD_TOKEN"])
|
client.run(environ["DISCORD_TOKEN"])
|
||||||
|
|
||||||
def prompt_main():
|
def send_main():
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
sleep(0.1)
|
sleep(0.1)
|
||||||
if prompt_queue.empty():
|
if event_queue.empty():
|
||||||
|
# logger.debug("no events to prompt")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if len(active_tasks) > 0:
|
if len(active_tasks) > 0:
|
||||||
|
logger.debug("waiting for active tasks to complete")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
event, prompt = prompt_queue.get()
|
event = event_queue.get()
|
||||||
logger.info("Prompting for event %s: %s", event, prompt)
|
logger.info("broadcasting event %s", event.type)
|
||||||
|
|
||||||
if client:
|
if client:
|
||||||
prompt_task = client.loop.create_task(broadcast_event(prompt))
|
event_task = client.loop.create_task(broadcast_event(event))
|
||||||
active_tasks.add(prompt_task)
|
active_tasks.add(event_task)
|
||||||
prompt_task.add_done_callback(active_tasks.discard)
|
event_task.add_done_callback(active_tasks.discard)
|
||||||
|
else:
|
||||||
|
logger.warning("no Discord client available")
|
||||||
|
|
||||||
bot_thread = Thread(target=bot_main, daemon=True)
|
bot_thread = Thread(target=bot_main, daemon=True)
|
||||||
bot_thread.start()
|
bot_thread.start()
|
||||||
|
|
||||||
prompt_thread = Thread(target=prompt_main, daemon=True)
|
send_thread = Thread(target=send_main, daemon=True)
|
||||||
prompt_thread.start()
|
send_thread.start()
|
||||||
|
|
||||||
return [bot_thread, prompt_thread]
|
return [bot_thread, send_thread]
|
||||||
|
|
||||||
|
|
||||||
def stop_bot():
|
def stop_bot():
|
||||||
|
@ -253,72 +261,99 @@ def get_active_channels():
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
async def broadcast_event(message: str | Embed):
|
def bot_event(event: GameEvent):
|
||||||
|
event_queue.put(event)
|
||||||
|
|
||||||
|
|
||||||
|
async def broadcast_event(message: str | GameEvent):
|
||||||
if not client:
|
if not client:
|
||||||
logger.warning("No Discord client available")
|
logger.warning("no Discord client available")
|
||||||
return
|
return
|
||||||
|
|
||||||
active_channels = get_active_channels()
|
active_channels = get_active_channels()
|
||||||
if not active_channels:
|
if not active_channels:
|
||||||
logger.warning("No active channels")
|
logger.warning("no active channels")
|
||||||
return
|
return
|
||||||
|
|
||||||
for channel in active_channels:
|
for channel in active_channels:
|
||||||
if isinstance(message, str):
|
if isinstance(message, str):
|
||||||
logger.info("Broadcasting to channel %s: %s", channel, message)
|
logger.info("broadcasting to channel %s: %s", channel, message)
|
||||||
await channel.send(content=message)
|
await channel.send(content=message)
|
||||||
elif isinstance(message, Embed):
|
elif isinstance(message, GameEvent):
|
||||||
|
embed = embed_from_event(message)
|
||||||
logger.info(
|
logger.info(
|
||||||
"Broadcasting to channel %s: %s - %s",
|
"broadcasting to channel %s: %s - %s",
|
||||||
channel,
|
channel,
|
||||||
message.title,
|
embed.title,
|
||||||
message.description,
|
embed.description,
|
||||||
)
|
)
|
||||||
await channel.send(embed=message)
|
await channel.send(embed=embed)
|
||||||
|
|
||||||
|
|
||||||
def bot_event(event: GameEvent):
|
def embed_from_event(event: GameEvent) -> Embed:
|
||||||
if isinstance(event, GenerateEvent):
|
if isinstance(event, GenerateEvent):
|
||||||
bot_generate(event)
|
return embed_from_generate(event)
|
||||||
elif isinstance(event, ResultEvent):
|
elif isinstance(event, ResultEvent):
|
||||||
bot_result(event)
|
return embed_from_result(event)
|
||||||
elif isinstance(event, (ActionEvent, ReplyEvent)):
|
elif isinstance(event, (ActionEvent, ReplyEvent)):
|
||||||
bot_action(event)
|
return embed_from_action(event)
|
||||||
elif isinstance(event, StatusEvent):
|
elif isinstance(event, StatusEvent):
|
||||||
pass
|
return embed_from_status(event)
|
||||||
|
elif isinstance(event, PlayerEvent):
|
||||||
|
return embed_from_player(event)
|
||||||
else:
|
else:
|
||||||
logger.warning("Unknown event type: %s", event)
|
logger.warning("unknown event type: %s", event)
|
||||||
|
|
||||||
|
|
||||||
def bot_action(event: ActionEvent | ReplyEvent):
|
def embed_from_action(event: ActionEvent | ReplyEvent):
|
||||||
try:
|
action_embed = Embed(title=event.room.name, description=event.actor.name)
|
||||||
action_embed = Embed(title=event.room.name, description=event.actor.name)
|
|
||||||
|
|
||||||
if isinstance(event, ActionEvent):
|
if isinstance(event, ActionEvent):
|
||||||
action_name = event.action.replace("action_", "").title()
|
action_name = event.action.replace("action_", "").title()
|
||||||
action_parameters = event.parameters
|
action_parameters = event.parameters
|
||||||
|
|
||||||
action_embed.add_field(name="Action", value=action_name)
|
action_embed.add_field(name="Action", value=action_name)
|
||||||
|
|
||||||
for key, value in action_parameters.items():
|
for key, value in action_parameters.items():
|
||||||
action_embed.add_field(name=key.replace("_", " ").title(), value=value)
|
action_embed.add_field(name=key.replace("_", " ").title(), value=value)
|
||||||
else:
|
else:
|
||||||
action_embed.add_field(name="Message", value=event.text)
|
action_embed.add_field(name="Message", value=event.text)
|
||||||
|
|
||||||
prompt_queue.put((event, action_embed))
|
return action_embed
|
||||||
except Exception as e:
|
|
||||||
logger.error("Failed to broadcast action: %s", e)
|
|
||||||
|
|
||||||
|
|
||||||
def bot_generate(event: GenerateEvent):
|
def embed_from_generate(event: GenerateEvent) -> Embed:
|
||||||
prompt_queue.put((event, event.name))
|
generate_embed = Embed(title="Generating", description=event.name)
|
||||||
|
return generate_embed
|
||||||
|
|
||||||
|
|
||||||
def bot_result(event: ResultEvent):
|
def embed_from_result(event: ResultEvent):
|
||||||
text = event.result
|
text = event.result
|
||||||
if len(text) > 1000:
|
if len(text) > 1000:
|
||||||
text = text[:1000] + "..."
|
text = text[:1000] + "..."
|
||||||
|
|
||||||
result_embed = Embed(title=event.room.name, description=event.actor.name)
|
result_embed = Embed(title=event.room.name, description=event.actor.name)
|
||||||
result_embed.add_field(name="Result", value=text)
|
result_embed.add_field(name="Result", value=text)
|
||||||
prompt_queue.put((event, result_embed))
|
return result_embed
|
||||||
|
|
||||||
|
|
||||||
|
def embed_from_player(event: PlayerEvent):
|
||||||
|
if event.status == "join":
|
||||||
|
title = "New Player"
|
||||||
|
description = f"{event.client} is now playing as {event.character}"
|
||||||
|
else:
|
||||||
|
title = "Player Left"
|
||||||
|
description = f"{event.client} has left the game, {event.character} will be played by the AI"
|
||||||
|
|
||||||
|
player_embed = Embed(title=title, description=description)
|
||||||
|
return player_embed
|
||||||
|
|
||||||
|
|
||||||
|
def embed_from_status(event: StatusEvent):
|
||||||
|
# TODO: add room and actor
|
||||||
|
status_embed = Embed(
|
||||||
|
title=event.room.name if event.room else "",
|
||||||
|
description=event.actor.name if event.actor else "",
|
||||||
|
)
|
||||||
|
status_embed.add_field(name="Status", value=event.text)
|
||||||
|
return status_embed
|
||||||
|
|
|
@ -41,6 +41,15 @@ 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)
|
||||||
|
|
||||||
|
|
||||||
|
# start the debugger, if needed
|
||||||
|
if environ.get("DEBUG", "false").lower() == "true":
|
||||||
|
import debugpy
|
||||||
|
|
||||||
|
debugpy.listen(5679)
|
||||||
|
logger.info("waiting for debugger to attach...")
|
||||||
|
debugpy.wait_for_client()
|
||||||
|
|
||||||
|
|
||||||
# main
|
# main
|
||||||
def parse_args():
|
def parse_args():
|
||||||
import argparse
|
import argparse
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from json import loads
|
from json import loads
|
||||||
from typing import Callable, Dict, Literal
|
from typing import Any, Callable, Dict, List, Literal
|
||||||
|
|
||||||
from .base import dataclass
|
from .base import dataclass
|
||||||
from .entity import Actor, Item, Room, WorldEntity
|
from .entity import Actor, Item, Room, WorldEntity
|
||||||
|
@ -11,7 +11,7 @@ class BaseEvent:
|
||||||
A base event class.
|
A base event class.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
event: str
|
type: str
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -20,7 +20,7 @@ class GenerateEvent:
|
||||||
A new entity has been generated.
|
A new entity has been generated.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
event = "generate"
|
type = "generate"
|
||||||
name: str
|
name: str
|
||||||
entity: WorldEntity | None = None
|
entity: WorldEntity | None = None
|
||||||
|
|
||||||
|
@ -39,9 +39,9 @@ class ActionEvent:
|
||||||
An actor has taken an action.
|
An actor has taken an action.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
event = "action"
|
type = "action"
|
||||||
action: str
|
action: str
|
||||||
parameters: Dict[str, str]
|
parameters: Dict[str, bool | float | int | str]
|
||||||
|
|
||||||
room: Room
|
room: Room
|
||||||
actor: Actor
|
actor: Actor
|
||||||
|
@ -65,7 +65,7 @@ class PromptEvent:
|
||||||
A prompt for an actor to take an action.
|
A prompt for an actor to take an action.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
event = "prompt"
|
type = "prompt"
|
||||||
prompt: str
|
prompt: str
|
||||||
room: Room
|
room: Room
|
||||||
actor: Actor
|
actor: Actor
|
||||||
|
@ -83,7 +83,7 @@ class ReplyEvent:
|
||||||
This is the non-JSON version of an ActionEvent.
|
This is the non-JSON version of an ActionEvent.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
event = "text"
|
type = "reply"
|
||||||
text: str
|
text: str
|
||||||
room: Room
|
room: Room
|
||||||
actor: Actor
|
actor: Actor
|
||||||
|
@ -99,7 +99,7 @@ class ResultEvent:
|
||||||
A result of an action.
|
A result of an action.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
event = "result"
|
type = "result"
|
||||||
result: str
|
result: str
|
||||||
room: Room
|
room: Room
|
||||||
actor: Actor
|
actor: Actor
|
||||||
|
@ -111,19 +111,34 @@ class StatusEvent:
|
||||||
A status broadcast event with text.
|
A status broadcast event with text.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
event = "status"
|
type = "status"
|
||||||
text: str
|
text: str
|
||||||
room: Room | None = None
|
room: Room | None = None
|
||||||
actor: Actor | None = None
|
actor: Actor | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SnapshotEvent:
|
||||||
|
"""
|
||||||
|
A snapshot of the world state.
|
||||||
|
|
||||||
|
This one is slightly unusual, because the world has already been dumped to a JSON-compatible dictionary.
|
||||||
|
That is especially important for the memory, which is a dictionary of actor names to lists of messages.
|
||||||
|
"""
|
||||||
|
|
||||||
|
type = "snapshot"
|
||||||
|
world: Dict[str, Any]
|
||||||
|
memory: Dict[str, List[Any]]
|
||||||
|
step: int
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PlayerEvent:
|
class PlayerEvent:
|
||||||
"""
|
"""
|
||||||
A player joining or leaving the game.
|
A player joining or leaving the game.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
event = "player"
|
type = "player"
|
||||||
status: Literal["join", "leave"]
|
status: Literal["join", "leave"]
|
||||||
character: str
|
character: str
|
||||||
client: str
|
client: str
|
||||||
|
|
|
@ -30,7 +30,7 @@ if not has_dungeon_master():
|
||||||
|
|
||||||
def action_explore(direction: str) -> str:
|
def action_explore(direction: str) -> str:
|
||||||
"""
|
"""
|
||||||
Explore the room in a new direction.
|
Explore the room in a new direction. You can only explore directions that do not already have a portal.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
direction: The direction to explore: north, south, east, or west.
|
direction: The direction to explore: north, south, east, or west.
|
||||||
|
@ -44,7 +44,7 @@ def action_explore(direction: str) -> str:
|
||||||
|
|
||||||
if direction in current_room.portals:
|
if direction in current_room.portals:
|
||||||
dest_room = current_room.portals[direction]
|
dest_room = current_room.portals[direction]
|
||||||
return f"You cannot explore {direction} from here, that direction leads to {dest_room}."
|
return f"You cannot explore {direction} from here, that direction already leads to {dest_room}. Please use the move action to go there."
|
||||||
|
|
||||||
existing_rooms = [room.name for room in current_world.rooms]
|
existing_rooms = [room.name for room in current_world.rooms]
|
||||||
new_room = generate_room(
|
new_room = generate_room(
|
||||||
|
|
|
@ -7,18 +7,11 @@ from typing import Literal
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import websockets
|
import websockets
|
||||||
|
from pydantic import RootModel
|
||||||
|
|
||||||
from adventure.context import get_actor_agent_for_name, set_actor_agent
|
from adventure.context import broadcast, get_actor_agent_for_name, set_actor_agent
|
||||||
from adventure.models.entity import Actor, Room, World
|
from adventure.models.entity import Actor, Item, Room, World
|
||||||
from adventure.models.event import (
|
from adventure.models.event import GameEvent, PlayerEvent, PromptEvent
|
||||||
ActionEvent,
|
|
||||||
GameEvent,
|
|
||||||
GenerateEvent,
|
|
||||||
PromptEvent,
|
|
||||||
ReplyEvent,
|
|
||||||
ResultEvent,
|
|
||||||
StatusEvent,
|
|
||||||
)
|
|
||||||
from adventure.player import (
|
from adventure.player import (
|
||||||
RemotePlayer,
|
RemotePlayer,
|
||||||
get_player,
|
get_player,
|
||||||
|
@ -46,7 +39,7 @@ async def handler(websocket):
|
||||||
dumps(
|
dumps(
|
||||||
{
|
{
|
||||||
"type": "prompt",
|
"type": "prompt",
|
||||||
"id": id,
|
"client": id,
|
||||||
"character": character,
|
"character": character,
|
||||||
"prompt": prompt,
|
"prompt": prompt,
|
||||||
"actions": [],
|
"actions": [],
|
||||||
|
@ -153,10 +146,7 @@ static_thread = None
|
||||||
|
|
||||||
|
|
||||||
def server_json(obj):
|
def server_json(obj):
|
||||||
if isinstance(obj, Actor):
|
if isinstance(obj, (Actor, Item, Room)):
|
||||||
return obj.name
|
|
||||||
|
|
||||||
if isinstance(obj, Room):
|
|
||||||
return obj.name
|
return obj.name
|
||||||
|
|
||||||
return world_json(obj)
|
return world_json(obj)
|
||||||
|
@ -191,68 +181,26 @@ def server_system(world: World, step: int):
|
||||||
global last_snapshot
|
global last_snapshot
|
||||||
json_state = {
|
json_state = {
|
||||||
**snapshot_world(world, step),
|
**snapshot_world(world, step),
|
||||||
"type": "world",
|
"type": "snapshot",
|
||||||
}
|
}
|
||||||
last_snapshot = send_and_append(json_state)
|
last_snapshot = send_and_append(json_state)
|
||||||
|
|
||||||
|
|
||||||
def server_result(room: Room, actor: Actor, action: str):
|
|
||||||
json_action = {
|
|
||||||
"actor": actor,
|
|
||||||
"result": action,
|
|
||||||
"room": room,
|
|
||||||
"type": "result",
|
|
||||||
}
|
|
||||||
send_and_append(json_action)
|
|
||||||
|
|
||||||
|
|
||||||
def server_action(room: Room, actor: Actor, message: str):
|
|
||||||
json_input = {
|
|
||||||
"actor": actor,
|
|
||||||
"input": message,
|
|
||||||
"room": room,
|
|
||||||
"type": "action",
|
|
||||||
}
|
|
||||||
send_and_append(json_input)
|
|
||||||
|
|
||||||
|
|
||||||
def server_generate(event: GenerateEvent):
|
|
||||||
json_broadcast = {
|
|
||||||
"name": event.name,
|
|
||||||
"type": "generate",
|
|
||||||
}
|
|
||||||
send_and_append(json_broadcast)
|
|
||||||
|
|
||||||
|
|
||||||
def server_event(event: GameEvent):
|
def server_event(event: GameEvent):
|
||||||
if isinstance(event, GenerateEvent):
|
json_event = RootModel[event.__class__](event).model_dump()
|
||||||
return server_generate(event)
|
json_event["type"] = event.type
|
||||||
elif isinstance(event, ActionEvent):
|
send_and_append(json_event)
|
||||||
return server_action(event.room, event.actor, event.action)
|
|
||||||
elif isinstance(event, ReplyEvent):
|
|
||||||
return server_action(event.room, event.actor, event.text)
|
|
||||||
elif isinstance(event, ResultEvent):
|
|
||||||
return server_result(event.room, event.actor, event.result)
|
|
||||||
elif isinstance(event, StatusEvent):
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
logger.warning("Unknown event type: %s", event)
|
|
||||||
|
|
||||||
|
|
||||||
def player_event(character: str, id: str, event: Literal["join", "leave"]):
|
def player_event(character: str, client: str, status: Literal["join", "leave"]):
|
||||||
json_broadcast = {
|
event = PlayerEvent(status=status, character=character, client=client)
|
||||||
"type": "player",
|
broadcast(event)
|
||||||
"character": character,
|
|
||||||
"id": id,
|
|
||||||
"event": event,
|
|
||||||
}
|
|
||||||
send_and_append(json_broadcast)
|
|
||||||
|
|
||||||
|
|
||||||
def player_list():
|
def player_list():
|
||||||
players = {value: key for key, value in list_players()}
|
|
||||||
json_broadcast = {
|
json_broadcast = {
|
||||||
"type": "players",
|
"type": "players",
|
||||||
"players": players,
|
"players": list_players(),
|
||||||
}
|
}
|
||||||
|
# TODO: broadcast this
|
||||||
send_and_append(json_broadcast)
|
send_and_append(json_broadcast)
|
||||||
|
|
|
@ -29,6 +29,7 @@ from adventure.models.entity import Attributes, World
|
||||||
from adventure.models.event import (
|
from adventure.models.event import (
|
||||||
ActionEvent,
|
ActionEvent,
|
||||||
EventCallback,
|
EventCallback,
|
||||||
|
GameEvent,
|
||||||
ReplyEvent,
|
ReplyEvent,
|
||||||
ResultEvent,
|
ResultEvent,
|
||||||
StatusEvent,
|
StatusEvent,
|
||||||
|
@ -70,9 +71,13 @@ def simulate_world(
|
||||||
set_current_world(world)
|
set_current_world(world)
|
||||||
|
|
||||||
# set up a broadcast callback
|
# set up a broadcast callback
|
||||||
def broadcast_callback(message):
|
def broadcast_callback(message: str | GameEvent):
|
||||||
logger.info(message)
|
logger.info(message)
|
||||||
event = StatusEvent(text=message)
|
if isinstance(message, str):
|
||||||
|
event = StatusEvent(text=message)
|
||||||
|
else:
|
||||||
|
event = message
|
||||||
|
|
||||||
for callback in callbacks:
|
for callback in callbacks:
|
||||||
callback(event)
|
callback(event)
|
||||||
|
|
||||||
|
|
|
@ -119,7 +119,7 @@ export function App(props: AppProps) {
|
||||||
|
|
||||||
if (data.type === 'prompt') {
|
if (data.type === 'prompt') {
|
||||||
// prompts are broadcast to all players
|
// prompts are broadcast to all players
|
||||||
if (data.id === clientId) {
|
if (data.client === clientId) {
|
||||||
// only notify the active player
|
// only notify the active player
|
||||||
setActiveTurn(true);
|
setActiveTurn(true);
|
||||||
} else {
|
} else {
|
||||||
|
@ -137,7 +137,7 @@ export function App(props: AppProps) {
|
||||||
setHistory((prev) => prev.concat(data));
|
setHistory((prev) => prev.concat(data));
|
||||||
|
|
||||||
// if we get a world event, update the last world state
|
// if we get a world event, update the last world state
|
||||||
if (data.type === 'world') {
|
if (data.type === 'snapshot') {
|
||||||
setWorld(data.world);
|
setWorld(data.world);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,17 +10,17 @@ export interface EventItemProps {
|
||||||
focusRef?: MutableRefObject<any>;
|
focusRef?: MutableRefObject<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ActionItem(props: EventItemProps) {
|
export function ActionEventItem(props: EventItemProps) {
|
||||||
const { event } = props;
|
const { event } = props;
|
||||||
const { actor, room, type } = event;
|
const { actor, room, type } = event;
|
||||||
const content = formatters[type](event);
|
const content = formatters[type](event);
|
||||||
|
|
||||||
return <ListItem alignItems="flex-start" ref={props.focusRef}>
|
return <ListItem alignItems="flex-start" ref={props.focusRef}>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<Avatar alt={actor} src="/static/images/avatar/1.jpg" />
|
<Avatar alt={actor.name} src="/static/images/avatar/1.jpg" />
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={room}
|
primary={room.name}
|
||||||
secondary={
|
secondary={
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Typography
|
<Typography
|
||||||
|
@ -29,7 +29,7 @@ export function ActionItem(props: EventItemProps) {
|
||||||
variant="body2"
|
variant="body2"
|
||||||
color="text.primary"
|
color="text.primary"
|
||||||
>
|
>
|
||||||
{actor}
|
{actor.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
{content}
|
{content}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
@ -38,7 +38,7 @@ export function ActionItem(props: EventItemProps) {
|
||||||
</ListItem>;
|
</ListItem>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WorldItem(props: EventItemProps) {
|
export function SnapshotEventItem(props: EventItemProps) {
|
||||||
const { event } = props;
|
const { event } = props;
|
||||||
const { step, world } = event;
|
const { step, world } = event;
|
||||||
const { theme } = world;
|
const { theme } = world;
|
||||||
|
@ -63,9 +63,9 @@ export function WorldItem(props: EventItemProps) {
|
||||||
</ListItem>;
|
</ListItem>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MessageItem(props: EventItemProps) {
|
export function ReplyEventItem(props: EventItemProps) {
|
||||||
const { event } = props;
|
const { event } = props;
|
||||||
const { message } = event;
|
const { text } = event;
|
||||||
|
|
||||||
return <ListItem alignItems="flex-start" ref={props.focusRef}>
|
return <ListItem alignItems="flex-start" ref={props.focusRef}>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
|
@ -80,26 +80,26 @@ export function MessageItem(props: EventItemProps) {
|
||||||
variant="body2"
|
variant="body2"
|
||||||
color="text.primary"
|
color="text.primary"
|
||||||
>
|
>
|
||||||
{message}
|
{text}
|
||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</ListItem>;
|
</ListItem>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PlayerItem(props: EventItemProps) {
|
export function PlayerEventItem(props: EventItemProps) {
|
||||||
const { event } = props;
|
const { event } = props;
|
||||||
const { character, event: innerEvent, id } = event;
|
const { character, status, client } = event;
|
||||||
|
|
||||||
let primary = '';
|
let primary = '';
|
||||||
let secondary = '';
|
let secondary = '';
|
||||||
if (innerEvent === 'join') {
|
if (status === 'join') {
|
||||||
primary = 'New Player';
|
primary = 'New Player';
|
||||||
secondary = `${id} is now playing as ${character}`;
|
secondary = `${client} is now playing as ${character}`;
|
||||||
}
|
}
|
||||||
if (innerEvent === 'leave') {
|
if (status === 'leave') {
|
||||||
primary = 'Player Left';
|
primary = 'Player Left';
|
||||||
secondary = `${id} has left the game. ${character} is now controlled by an LLM`;
|
secondary = `${client} has left the game. ${character} is now controlled by an LLM`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ListItem alignItems="flex-start" ref={props.focusRef}>
|
return <ListItem alignItems="flex-start" ref={props.focusRef}>
|
||||||
|
@ -129,13 +129,13 @@ export function EventItem(props: EventItemProps) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'action':
|
case 'action':
|
||||||
case 'result':
|
case 'result':
|
||||||
return <ActionItem event={event} focusRef={props.focusRef} />;
|
return <ActionEventItem event={event} focusRef={props.focusRef} />;
|
||||||
case 'event':
|
case 'reply':
|
||||||
return <MessageItem event={event} focusRef={props.focusRef} />;
|
return <ReplyEventItem event={event} focusRef={props.focusRef} />;
|
||||||
case 'player':
|
case 'player':
|
||||||
return <PlayerItem event={event} focusRef={props.focusRef} />;
|
return <PlayerEventItem event={event} focusRef={props.focusRef} />;
|
||||||
case 'world':
|
case 'snapshot':
|
||||||
return <WorldItem event={event} focusRef={props.focusRef} />;
|
return <SnapshotEventItem event={event} focusRef={props.focusRef} />;
|
||||||
default:
|
default:
|
||||||
return <ListItem ref={props.focusRef}>
|
return <ListItem ref={props.focusRef}>
|
||||||
<ListItemText primary={`Unknown event type: ${type}`} />
|
<ListItemText primary={`Unknown event type: ${type}`} />
|
||||||
|
|
|
@ -6,19 +6,15 @@ export function formatActionName(name: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatAction(data: any) {
|
export function formatAction(data: any) {
|
||||||
const actionName = formatActionName(data.function);
|
const actionName = formatActionName(data.action);
|
||||||
const actionParameters = data.parameters;
|
const actionParameters = data.parameters;
|
||||||
|
|
||||||
return `Action: ${actionName} - ${Object.entries(actionParameters).map(([key, value]) => `${key}: ${value}`).join(', ')}`;
|
return `Action: ${actionName} - ${Object.entries(actionParameters).map(([key, value]) => `${key}: ${value}`).join(', ')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatInput(data: any) {
|
export function formatInput(data: any) {
|
||||||
try {
|
const action = formatAction(data);
|
||||||
const action = formatAction(JSON.parse(data.input));
|
return `Starting turn: ${action}`;
|
||||||
return `Starting turn: ${action}`;
|
|
||||||
} catch (err) {
|
|
||||||
return `Error parsing input: ${err}`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatResult(data: any) {
|
export function formatResult(data: any) {
|
||||||
|
|
Loading…
Reference in New Issue