1
0
Fork 0

fix logging, make config optional, fix event attributes

This commit is contained in:
Sean Sube 2024-05-18 16:20:47 -05:00
parent fee406e607
commit 1453038f6d
Signed by: ssube
GPG Key ID: 3EED7B957D362AF1
12 changed files with 258 additions and 44 deletions

2
.vscode/launch.json vendored
View File

@ -11,7 +11,7 @@
"program": "${file}",
"args": [
"--world",
"worlds/test-1.json",
"worlds/test-3.json",
"--rooms",
"2",
"--server",

View File

@ -13,7 +13,7 @@ from adventure.context import (
get_current_world,
set_actor_agent,
)
from adventure.models.config import DiscordBotConfig
from adventure.models.config import DiscordBotConfig, DEFAULT_CONFIG
from adventure.models.event import (
ActionEvent,
GameEvent,
@ -36,7 +36,7 @@ from adventure.render_comfy import render_event
logger = getLogger(__name__)
client = None
bot_config: DiscordBotConfig = DiscordBotConfig(channels=["bots"])
bot_config: DiscordBotConfig = DEFAULT_CONFIG.bot.discord
active_tasks = set()
event_messages: Dict[str, str | GameEvent] = {}

View File

@ -1,17 +1,25 @@
from typing import Callable, Dict, List, Sequence, Tuple
from contextlib import contextmanager
from packit.agent import Agent
from pyee.base import EventEmitter
from adventure.game_system import GameSystem
from adventure.models.entity import Actor, Room, World
from adventure.models.event import GameEvent
# TODO: replace with event emitter and a context manager
current_broadcast: Callable[[str | GameEvent], None] | None = None
# world context
current_step = 0
current_world: World | None = None
current_room: Room | None = None
current_actor: Actor | None = None
current_step = 0
dungeon_master: Agent | None = None
# game context
event_emitter = EventEmitter()
game_systems: List[GameSystem] = []
@ -28,7 +36,23 @@ def has_dungeon_master():
return dungeon_master is not None
# region context manager
# TODO
# endregion
# region context getters
def get_action_context() -> Tuple[Room, Actor]:
if not current_room:
raise ValueError("The current room must be set before calling action functions")
if not current_actor:
raise ValueError(
"The current actor must be set before calling action functions"
)
return (current_room, current_actor)
def get_current_context() -> Tuple[World, Room, Actor]:
if not current_world:
raise ValueError(

View File

@ -209,12 +209,13 @@ def generate_effect(
name = loop_retry(
agent,
"Generate one effect for an {entity_type} named {entity.name} that would make sense in the world of {theme}. "
"Generate one effect for an {entity_type} named {entity_name} that would make sense in the world of {theme}. "
"Only respond with the effect name in title case, do not include a description or any other text. "
'Do not prefix the name with "the", do not wrap it in quotes. Use a unique name. '
"Do not create any duplicate effects on the same item. The existing effects are: {existing_effects}. "
"Some example effects are: 'fire', 'poison', 'frost', 'haste', 'slow', and 'heal'.",
context={
"entity_name": entity.name,
"entity_type": entity_type,
"existing_effects": existing_effects,
"theme": theme,
@ -245,6 +246,7 @@ def generate_effect(
f"How does the {name} effect modify the {attribute_name} attribute? "
"For example, 'heal' might 'add' to the 'health' attribute, while 'poison' might 'subtract' from it."
"Another example is 'writing' might 'set' the 'text' attribute, while 'break' might 'set' the 'condition' attribute."
"Reply with the operation only, without any other text. Give a single word."
"Choose from the following operations: {operations}",
name=name,
attribute_name=attribute_name,
@ -260,8 +262,8 @@ def generate_effect(
)
value = agent(
f"How much does the {name} effect modify the {attribute_name} attribute? "
"For example, 'heal' might 'add' 10 to the 'health' attribute, while 'poison' might 'subtract' 5 from it."
"Enter a positive or negative number, or a string value.",
"For example, heal might add '10' to the health attribute, while poison might subtract '5' from it."
"Enter a positive or negative number, or a string value. Do not include any other text. Do not use JSON.",
name=name,
attribute_name=attribute_name,
)
@ -283,7 +285,7 @@ def generate_effect(
attributes.append(attribute_effect)
return Effect(name=name, description=description, attributes=[])
return Effect(name=name, description=description, attributes=attributes)
def generate_system_attributes(

View File

@ -6,19 +6,9 @@ from typing import List
from dotenv import load_dotenv
from packit.agent import Agent, agent_easy_connect
from packit.utils import logger_with_colors
from pyee.base import EventEmitter
from yaml import Loader, load
from adventure.context import set_current_step, set_dungeon_master
from adventure.game_system import GameSystem
from adventure.generate import generate_world
from adventure.models.config import Config
from adventure.models.entity import World, WorldState
from adventure.models.event import EventCallback, GameEvent, GenerateEvent
from adventure.models.files import PromptFile, WorldPrompt
from adventure.plugins import load_plugin
from adventure.simulate import simulate_world
from adventure.state import create_agents, save_world, save_world_state
def load_yaml(file):
return load(file, Loader=Loader)
@ -42,6 +32,18 @@ logger = logger_with_colors(__name__) # , level="DEBUG")
load_dotenv(environ.get("ADVENTURE_ENV", ".env"), override=True)
if True:
from adventure.context import set_current_step, set_dungeon_master
from adventure.game_system import GameSystem
from adventure.generate import generate_world
from adventure.models.config import Config, DEFAULT_CONFIG
from adventure.models.entity import World, WorldState
from adventure.models.event import EventCallback, GameEvent, GenerateEvent
from adventure.models.files import PromptFile, WorldPrompt
from adventure.plugins import load_plugin
from adventure.simulate import simulate_world
from adventure.state import create_agents, save_world, save_world_state
# start the debugger, if needed
if environ.get("DEBUG", "false").lower() == "true":
@ -52,6 +54,13 @@ if environ.get("DEBUG", "false").lower() == "true":
debugpy.wait_for_client()
def int_or_inf(value: str) -> float | int:
if value == "inf":
return float("inf")
return int(value)
# main
def parse_args():
import argparse
@ -68,8 +77,7 @@ def parse_args():
parser.add_argument(
"--config",
type=str,
default="config.yml",
help="The file to load the configuration from",
help="The file to load additional configuration from",
)
parser.add_argument(
"--discord",
@ -91,7 +99,7 @@ def parse_args():
parser.add_argument(
"--optional-actions",
action="store_true",
help="Whether to include optional actions",
help="Whether to include optional actions in the simulation",
)
parser.add_argument(
"--player",
@ -101,7 +109,7 @@ def parse_args():
parser.add_argument(
"--render",
action="store_true",
help="Whether to render the simulation",
help="Whether to run the render thread",
)
parser.add_argument(
"--render-generated",
@ -116,7 +124,7 @@ def parse_args():
parser.add_argument(
"--server",
action="store_true",
help="The address on which to run the server",
help="Whether to run the websocket server",
)
parser.add_argument(
"--state",
@ -125,7 +133,7 @@ def parse_args():
)
parser.add_argument(
"--steps",
type=int,
type=int_or_inf,
default=10,
help="The number of simulation steps to run",
)
@ -233,8 +241,11 @@ def load_or_generate_world(
def main():
args = parse_args()
with open(args.config, "r") as f:
config = Config(**load_yaml(f))
if args.config:
with open(args.config, "r") as f:
config = Config(**load_yaml(f))
else:
config = DEFAULT_CONFIG
players = []
if args.player:

View File

@ -7,6 +7,7 @@ from .base import dataclass
class Range:
min: int
max: int
interval: int = 1
@dataclass
@ -39,3 +40,21 @@ class RenderConfig:
class Config:
bot: BotConfig
render: RenderConfig
DEFAULT_CONFIG = Config(
bot=BotConfig(discord=DiscordBotConfig(channels=["adventure"])),
render=RenderConfig(
cfg=Range(min=5, max=8),
checkpoints=[
"diffusion-sdxl-dynavision-0-5-5-7.safetensors",
],
path="/tmp/adventure-images",
sizes={
"landscape": Size(width=1024, height=768),
"portrait": Size(width=768, height=1024),
"square": Size(width=768, height=768),
},
steps=Range(min=30, max=30),
),
)

View File

@ -17,7 +17,7 @@ from jinja2 import Environment, FileSystemLoader, select_autoescape
from PIL import Image
from adventure.context import broadcast
from adventure.models.config import Range, RenderConfig, Size
from adventure.models.config import RenderConfig, DEFAULT_CONFIG
from adventure.models.entity import WorldEntity
from adventure.models.event import (
ActionEvent,
@ -33,19 +33,7 @@ logger = getLogger(__name__)
server_address = environ["COMFY_API"]
client_id = uuid4().hex
render_config: RenderConfig = RenderConfig(
cfg=Range(min=5, max=8),
checkpoints=[
"diffusion-sdxl-dynavision-0-5-5-7.safetensors",
],
path="/tmp/adventure-images",
sizes={
"landscape": Size(width=1024, height=768),
"portrait": Size(width=768, height=1024),
"square": Size(width=768, height=768),
},
steps=Range(min=30, max=30),
)
render_config: RenderConfig = DEFAULT_CONFIG.render
# requests to generate images for game events

View File

@ -0,0 +1,70 @@
from random import randint
from adventure.context import broadcast, get_current_context, get_dungeon_master
from adventure.generate import generate_item
from adventure.models.entity import Item
from adventure.models.base import dataclass
@dataclass
class Recipe:
ingredients: list[str]
result: str
difficulty: int
recipes = {
"potion": Recipe(["herb", "water"], "potion", 5),
"sword": Recipe(["metal", "wood"], "sword", 10),
}
def action_craft(item_name: str) -> str:
"""
Craft an item using available recipes and inventory items.
Args:
item_name: The name of the item to craft.
"""
action_world, _, action_actor = get_current_context()
if item_name not in recipes:
return f"There is no recipe to craft a {item_name}."
recipe = recipes[item_name]
# Check if the actor has the required skill level
skill = randint(1, 20)
if skill < recipe.difficulty:
return f"You need a crafting skill level of {recipe.difficulty} to craft {item_name}."
# Collect inventory items names
inventory_items = {item.name for item in action_actor.items}
# Check for sufficient ingredients
missing_items = [item for item in recipe.ingredients if item not in inventory_items]
if missing_items:
return f"You are missing {' and '.join(missing_items)} to craft {item_name}."
# Deduct the ingredients from inventory
for ingredient in recipe.ingredients:
item_to_remove = next(
item for item in action_actor.items if item.name == ingredient
)
action_actor.items.remove(item_to_remove)
# Create and add the crafted item to inventory
result_item = next(
(item for item in action_actor.items if item.name == recipe.result), None
)
if result_item:
new_item = Item(**vars(result_item)) # Copying the item
else:
dungeon_master = get_dungeon_master()
new_item = generate_item(
dungeon_master, action_world.theme
) # TODO: pass recipe item
action_actor.items.append(new_item)
broadcast(f"{action_actor.name} crafts a {item_name}.")
return f"You successfully craft a {item_name}."

View File

@ -0,0 +1,22 @@
from adventure.context import broadcast, get_current_context
from adventure.search import find_item_in_actor
def action_read(item_name: str) -> str:
"""
Read an item like a book or a sign.
Args:
item_name: The name of the item to read.
"""
_, _, action_actor = get_current_context()
item = find_item_in_actor(action_actor, item_name)
if not item:
return f"You do not have a {item_name} to read."
if "text" in item.attributes:
broadcast(f"{action_actor.name} reads {item_name}")
return str(item.attributes["text"])
else:
return f"The {item_name} has nothing to read."

View File

@ -0,0 +1,36 @@
from random import randint
from adventure.context import broadcast, get_current_context, get_dungeon_master
from adventure.search import find_actor_in_room
def action_cast(spell: str, target: str) -> str:
"""
Cast a spell on a target.
Args:
spell: The name of the spell to cast.
target: The target of the spell.
"""
_, action_room, action_actor = get_current_context()
target_actor = find_actor_in_room(action_room, target)
dungeon_master = get_dungeon_master()
# Check for spell availability and mana costs
if spell not in action_actor.attributes["spells"]:
return f"You do not know the spell '{spell}'."
if action_actor.attributes["mana"] < action_actor.attributes["spells"][spell]:
return "You do not have enough mana to cast this spell."
action_actor.attributes["mana"] -= action_actor.attributes["spells"][spell]
# Get flavor text from the dungeon master
flavor_text = dungeon_master(f"Describe the effects of {spell} on {target}.")
broadcast(f"{action_actor.name} casts {spell} on {target}. {flavor_text}")
# Apply effects based on the spell
if spell == "heal" and target_actor:
heal_amount = randint(10, 30)
target_actor.attributes["health"] += heal_amount
return f"{target} is healed for {heal_amount} points."
return f"{spell} was successfully cast on {target}."

View File

@ -0,0 +1,36 @@
from random import randint
from adventure.context import broadcast, get_current_context, get_dungeon_master
from adventure.search import find_item_in_room
def action_climb(target: str) -> str:
"""
Climb a structure or natural feature.
Args:
target: The object or feature to climb.
"""
_, action_room, action_actor = get_current_context()
dungeon_master = get_dungeon_master()
# Assume 'climbable' is an attribute that marks climbable targets
climbable_feature = find_item_in_room(action_room, target)
if climbable_feature and climbable_feature.attributes.get("climbable", False):
climb_difficulty = int(climbable_feature.attributes.get("difficulty", 5))
climb_roll = randint(1, 20)
# Get flavor text for the climb attempt
flavor_text = dungeon_master(
f"Describe {action_actor.name}'s attempt to climb {target}."
)
if climb_roll > climb_difficulty:
broadcast(
f"{action_actor.name} successfully climbs the {target}. {flavor_text}"
)
return f"You successfully climb the {target}."
else:
broadcast(f"{action_actor.name} fails to climb the {target}. {flavor_text}")
return f"You fail to climb the {target}."
else:
return f"The {target} is not climbable."

View File

@ -1,5 +1,7 @@
from itertools import count
from logging import getLogger
from typing import Callable, Sequence
from math import inf
from packit.loops import loop_retry
from packit.results import multi_function_or_str_result
@ -63,7 +65,7 @@ def world_result_parser(value, agent, **kwargs):
def simulate_world(
world: World,
steps: int = 10,
steps: float | int = inf,
actions: Sequence[Callable[..., str]] = [],
callbacks: Sequence[EventCallback] = [],
systems: Sequence[GameSystem] = [],
@ -100,9 +102,10 @@ def simulate_world(
action_names = action_tools.list_tools()
# simulate each actor
for i in range(steps):
for i in count():
current_step = get_current_step()
logger.info(f"Simulating step {current_step}")
logger.info(f"simulating step {i} of {steps} (world step {current_step})")
for actor_name in world.order:
actor, agent = get_actor_agent_for_name(actor_name)
if not agent or not actor:
@ -179,3 +182,6 @@ def simulate_world(
system.simulate(world, current_step)
set_current_step(current_step + 1)
if i > steps:
logger.info("reached step limit at world step %s", current_step + 1)
break