1
0
Fork 0

set up a proper dungeon master

This commit is contained in:
Sean Sube 2024-05-07 20:40:53 -05:00
parent 469ae79a9c
commit 5117db7150
Signed by: ssube
GPG Key ID: 3EED7B957D362AF1
3 changed files with 86 additions and 40 deletions

View File

@ -1,4 +1,4 @@
from functools import partial
from functools import partial, wraps
from logging import getLogger
from random import random
from typing import Callable, Dict, List, Optional
@ -45,7 +45,7 @@ class LogicTable:
LogicTrigger = Callable[[Room | Actor | Item, Attributes], Attributes]
TriggerTable = Dict[LogicRule, List[LogicTrigger]]
TriggerTable = Dict[str, LogicTrigger]
def update_attributes(
@ -99,9 +99,10 @@ def update_attributes(
attributes.update(rule.set)
logger.info("logic set state: %s", rule.set)
if rule in triggers:
for trigger in triggers[rule]:
attributes = trigger(entity, attributes)
if rule.trigger:
for trigger in rule.trigger:
if trigger in triggers:
attributes = triggers[trigger](entity, attributes)
return attributes
@ -143,7 +144,7 @@ def format_logic(attributes: Attributes, rules: LogicTable, self=True) -> str:
logger.debug("label has no relevant description: %s", label)
if len(labels) > 0:
logger.info("adding labels: %s", labels)
logger.info("adding attribute labels: %s", labels)
return " ".join(labels)
@ -152,14 +153,17 @@ def init_from_file(filename: str):
logger.info("loading logic from file: %s", filename)
with open(filename) as file:
logic_rules = LogicTable(**load(file, Loader=Loader))
logic_triggers = {
rule: [get_plugin_function(trigger) for trigger in rule.trigger]
for rule in logic_rules.rules
if rule.trigger
}
logic_triggers = {}
for rule in logic_rules.rules:
if rule.trigger:
for trigger in rule.trigger:
logic_triggers[trigger] = get_plugin_function(trigger)
logger.info("initialized logic system")
return (
partial(update_logic, rules=logic_rules, triggers=logic_triggers),
partial(format_logic, rules=logic_rules),
wraps(update_logic)(
partial(update_logic, rules=logic_rules, triggers=logic_triggers)
),
wraps(format_logic)(partial(format_logic, rules=logic_rules)),
)

View File

@ -1,7 +1,7 @@
from json import load
from logging.config import dictConfig
from os import environ, path
from typing import Callable, Dict, Sequence, Tuple
from typing import Callable, Sequence, Tuple
from dotenv import load_dotenv
from packit.agent import Agent, agent_easy_connect
@ -10,7 +10,8 @@ from packit.results import multi_function_or_str_result
from packit.toolbox import Toolbox
from packit.utils import logger_with_colors
from adventure.context import set_current_broadcast
from adventure.context import set_current_broadcast, set_dungeon_master
from adventure.models import Attributes
from adventure.plugins import load_plugin
# Configure logging
@ -50,7 +51,7 @@ if True:
from adventure.models import Actor, Room, World, WorldState
from adventure.state import create_agents, save_world, save_world_state
logger = logger_with_colors(__name__, level="INFO")
logger = logger_with_colors(__name__, level="DEBUG")
load_dotenv(environ.get("ADVENTURE_ENV", ".env"), override=True)
@ -81,7 +82,7 @@ def simulate_world(
steps: int = 10,
actions: Sequence[Callable[..., str]] = [],
systems: Sequence[
Tuple[Callable[[World, int], None], Callable[[Dict[str, str]], str] | None]
Tuple[Callable[[World, int], None], Callable[[Attributes], str] | None]
] = [],
event_callbacks: Sequence[Callable[[str], None]] = [],
input_callbacks: Sequence[Callable[[Room, Actor, str], None]] = [],
@ -140,6 +141,9 @@ def simulate_world(
def result_parser(value, agent, **kwargs):
for callback in input_callbacks:
logger.info(
f"calling input callback for {actor_name}: {callback.__name__}"
)
callback(room, actor, value)
return world_result_parser(value, agent, **kwargs)
@ -198,7 +202,13 @@ def parse_args():
help="Extra actions to include in the simulation",
)
parser.add_argument(
"--flavor", type=str, help="Some additional flavor text for the generated world"
"--discord", type=bool, help="Whether to run the simulation in a Discord bot"
)
parser.add_argument(
"--flavor",
type=str,
default="",
help="Some additional flavor text for the generated world",
)
parser.add_argument(
"--player", type=str, help="The name of the character to play as"
@ -289,7 +299,7 @@ def main():
else:
logger.info(f"Generating a new {args.theme} world")
llm = agent_easy_connect()
agent = Agent(
world_builder = Agent(
"World Builder",
f"You are an experienced game master creating a visually detailed {args.theme} world for a new adventure. {args.flavor}",
{},
@ -306,7 +316,7 @@ def main():
server_system(world, 0)
world = generate_world(
agent,
world_builder,
args.world,
args.theme,
room_count=args.rooms,
@ -349,14 +359,28 @@ def main():
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]}"
f"Loaded extra systems: {[component.__name__ for system in module_systems for component in system]}"
)
extra_systems.append(module_systems)
extra_systems.extend(module_systems)
# make sure the server system runs after any updates
if args.server:
extra_systems.append((server_system, None))
# create the DM
llm = agent_easy_connect()
world_builder = Agent(
"dungeon master",
(
f"You are the dungeon master in charge of a {world.theme} world. Be creative and original, and come up with "
f"interesting events that will keep players interested. {args.flavor}"
"Do not to repeat yourself unless you are given the same prompt with the same characters and actions."
),
{},
llm,
)
set_dungeon_master(world_builder)
# start the sim
logger.debug("Simulating world: %s", world)
simulate_world(

View File

@ -3,21 +3,29 @@ from typing import Callable, List
from packit.agent import Agent, agent_easy_connect
from adventure.context import broadcast, get_agent_for_actor, get_current_context
from adventure.context import (
broadcast,
get_agent_for_actor,
get_current_context,
get_dungeon_master,
has_dungeon_master,
set_dungeon_master,
)
from adventure.generate import OPPOSITE_DIRECTIONS, generate_item, generate_room
logger = getLogger(__name__)
llm = agent_easy_connect()
# TODO: provide dungeon master with the world theme
dungeon_master = Agent(
# this is the fallback dungeon master if none is set
if not has_dungeon_master():
llm = agent_easy_connect()
set_dungeon_master(
Agent(
"dungeon master",
"You are the dungeon master in charge of a fantasy world.",
{},
llm,
)
)
)
def action_explore(direction: str) -> str:
@ -29,6 +37,7 @@ def action_explore(direction: str) -> str:
"""
current_world, current_room, current_actor = get_current_context()
dungeon_master = get_dungeon_master()
if not current_world:
raise ValueError("No world found")
@ -53,12 +62,13 @@ def action_explore(direction: str) -> str:
return f"You explore {direction} and find a new room: {new_room.name}"
def action_search() -> str:
def action_search(unused: bool) -> str:
"""
Search the room for hidden items.
"""
action_world, action_room, action_actor = get_current_context()
dungeon_master = get_dungeon_master()
if len(action_room.items) > 2:
return "You find nothing hidden in the room."
@ -88,12 +98,17 @@ def action_use(item: str, target: str) -> str:
target: The name of the character to use the item on, or "self" to use the item on yourself.
"""
_, action_room, action_actor = get_current_context()
dungeon_master = get_dungeon_master()
available_items = [item.name for item in action_actor.items] + [
item.name for item in action_room.items
]
if item not in available_items:
action_item = next(
(
search_item
for search_item in (action_actor.items + action_room.items)
if search_item.name == item
),
None,
)
if not action_item:
return f"The {item} item is not available to use."
if target == "self":
@ -108,7 +123,10 @@ def action_use(item: str, target: str) -> str:
broadcast(f"{action_actor.name} uses {item} on {target}")
outcome = dungeon_master(
f"{action_actor.name} uses {item} on {target}. {action_actor.description}. {target_actor.description}. What happens? How does {target} react? "
f"{action_actor.name} uses {item} on {target}. "
f"{action_actor.description}. {target_actor.description}. {action_item.description}. "
f"What happens? How does {target} react? Be creative with the results. The outcome can be good, bad, or neutral."
"Decide based on the characters involved and the item being used."
"Specify the outcome of the action. Do not include the question or any JSON. Only include the outcome of the action."
)
broadcast(f"The action resulted in: {outcome}")