fix logging, make config optional, fix event attributes
This commit is contained in:
parent
fee406e607
commit
1453038f6d
|
@ -11,7 +11,7 @@
|
|||
"program": "${file}",
|
||||
"args": [
|
||||
"--world",
|
||||
"worlds/test-1.json",
|
||||
"worlds/test-3.json",
|
||||
"--rooms",
|
||||
"2",
|
||||
"--server",
|
||||
|
|
|
@ -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] = {}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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()
|
||||
|
||||
if args.config:
|
||||
with open(args.config, "r") as f:
|
||||
config = Config(**load_yaml(f))
|
||||
else:
|
||||
config = DEFAULT_CONFIG
|
||||
|
||||
players = []
|
||||
if args.player:
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}."
|
|
@ -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."
|
|
@ -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}."
|
|
@ -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."
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue