formalize game systems, start implementing effects
This commit is contained in:
parent
4d90cbef33
commit
fee406e607
|
@ -11,9 +11,11 @@
|
||||||
"program": "${file}",
|
"program": "${file}",
|
||||||
"args": [
|
"args": [
|
||||||
"--world",
|
"--world",
|
||||||
"worlds/cyber.json",
|
"worlds/test-1.json",
|
||||||
"--world-state",
|
"--rooms",
|
||||||
"worlds/cyber-state.json"
|
"2",
|
||||||
|
"--server",
|
||||||
|
"--optional-actions"
|
||||||
],
|
],
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceFolder}",
|
||||||
|
|
|
@ -10,6 +10,7 @@ from adventure.search import (
|
||||||
find_item_in_room,
|
find_item_in_room,
|
||||||
find_room,
|
find_room,
|
||||||
)
|
)
|
||||||
|
from adventure.utils.world import describe_entity
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
@ -26,28 +27,28 @@ def action_look(target: str) -> str:
|
||||||
|
|
||||||
if target.lower() == action_room.name.lower():
|
if target.lower() == action_room.name.lower():
|
||||||
broadcast(f"{action_actor.name} saw the {action_room.name} room")
|
broadcast(f"{action_actor.name} saw the {action_room.name} room")
|
||||||
return action_room.description
|
return describe_entity(action_room)
|
||||||
|
|
||||||
target_actor = find_actor_in_room(action_room, target)
|
target_actor = find_actor_in_room(action_room, target)
|
||||||
if target_actor:
|
if target_actor:
|
||||||
broadcast(
|
broadcast(
|
||||||
f"{action_actor.name} saw the {target_actor.name} actor in the {action_room.name} room"
|
f"{action_actor.name} saw the {target_actor.name} actor in the {action_room.name} room"
|
||||||
)
|
)
|
||||||
return target_actor.description
|
return describe_entity(target_actor)
|
||||||
|
|
||||||
target_item = find_item_in_room(action_room, target)
|
target_item = find_item_in_room(action_room, target)
|
||||||
if target_item:
|
if target_item:
|
||||||
broadcast(
|
broadcast(
|
||||||
f"{action_actor.name} saw the {target_item.name} item in the {action_room.name} room"
|
f"{action_actor.name} saw the {target_item.name} item in the {action_room.name} room"
|
||||||
)
|
)
|
||||||
return target_item.description
|
return describe_entity(target_item)
|
||||||
|
|
||||||
target_item = find_item_in_actor(action_actor, target)
|
target_item = find_item_in_actor(action_actor, target)
|
||||||
if target_item:
|
if target_item:
|
||||||
broadcast(
|
broadcast(
|
||||||
f"{action_actor.name} saw the {target_item.name} item in their inventory"
|
f"{action_actor.name} saw the {target_item.name} item in their inventory"
|
||||||
)
|
)
|
||||||
return target_item.description
|
return describe_entity(target_item)
|
||||||
|
|
||||||
return "You do not see that item or character in the room."
|
return "You do not see that item or character in the room."
|
||||||
|
|
||||||
|
@ -104,18 +105,13 @@ def action_ask(character: str, question: str) -> str:
|
||||||
question: The question to ask them.
|
question: The question to ask them.
|
||||||
"""
|
"""
|
||||||
# capture references to the current actor and room, because they will be overwritten
|
# capture references to the current actor and room, because they will be overwritten
|
||||||
_, action_room, action_actor = get_current_context()
|
_world, _room, action_actor = get_current_context()
|
||||||
|
|
||||||
if not action_actor or not action_room:
|
|
||||||
raise ValueError(
|
|
||||||
"The current actor and room must be set before calling action_ask"
|
|
||||||
)
|
|
||||||
|
|
||||||
# sanity checks
|
# sanity checks
|
||||||
if character == action_actor.name:
|
question_actor, question_agent = get_actor_agent_for_name(character)
|
||||||
|
if question_actor == action_actor:
|
||||||
return "You cannot ask yourself a question. Stop talking to yourself. Try another action."
|
return "You cannot ask yourself a question. Stop talking to yourself. Try another action."
|
||||||
|
|
||||||
question_actor, question_agent = get_actor_agent_for_name(character)
|
|
||||||
if not question_actor:
|
if not question_actor:
|
||||||
return f"The {character} character is not in the room."
|
return f"The {character} character is not in the room."
|
||||||
|
|
||||||
|
@ -147,18 +143,13 @@ def action_tell(character: str, message: str) -> str:
|
||||||
message: The message to tell them.
|
message: The message to tell them.
|
||||||
"""
|
"""
|
||||||
# capture references to the current actor and room, because they will be overwritten
|
# capture references to the current actor and room, because they will be overwritten
|
||||||
_, action_room, action_actor = get_current_context()
|
_world, _room, action_actor = get_current_context()
|
||||||
|
|
||||||
if not action_actor or not action_room:
|
|
||||||
raise ValueError(
|
|
||||||
"The current actor and room must be set before calling action_tell"
|
|
||||||
)
|
|
||||||
|
|
||||||
# sanity checks
|
# sanity checks
|
||||||
if character == action_actor.name:
|
question_actor, question_agent = get_actor_agent_for_name(character)
|
||||||
|
if question_actor == action_actor:
|
||||||
return "You cannot tell yourself a message. Stop talking to yourself. Try another action."
|
return "You cannot tell yourself a message. Stop talking to yourself. Try another action."
|
||||||
|
|
||||||
question_actor, question_agent = get_actor_agent_for_name(character)
|
|
||||||
if not question_actor:
|
if not question_actor:
|
||||||
return f"The {character} character is not in the room."
|
return f"The {character} character is not in the room."
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
from typing import Callable, Dict, Tuple
|
from typing import Callable, Dict, List, Sequence, Tuple
|
||||||
|
|
||||||
from packit.agent import Agent
|
from packit.agent import Agent
|
||||||
|
|
||||||
|
from adventure.game_system import GameSystem
|
||||||
from adventure.models.entity import Actor, Room, World
|
from adventure.models.entity import Actor, Room, World
|
||||||
from adventure.models.event import GameEvent
|
from adventure.models.event import GameEvent
|
||||||
|
|
||||||
|
@ -11,6 +12,7 @@ current_room: Room | None = None
|
||||||
current_actor: Actor | None = None
|
current_actor: Actor | None = None
|
||||||
current_step = 0
|
current_step = 0
|
||||||
dungeon_master: Agent | None = None
|
dungeon_master: Agent | None = None
|
||||||
|
game_systems: List[GameSystem] = []
|
||||||
|
|
||||||
|
|
||||||
# TODO: where should this one go?
|
# TODO: where should this one go?
|
||||||
|
@ -71,6 +73,10 @@ def get_dungeon_master() -> Agent:
|
||||||
return dungeon_master
|
return dungeon_master
|
||||||
|
|
||||||
|
|
||||||
|
def get_game_systems() -> List[GameSystem]:
|
||||||
|
return game_systems
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
|
@ -109,6 +115,11 @@ def set_dungeon_master(agent):
|
||||||
dungeon_master = agent
|
dungeon_master = agent
|
||||||
|
|
||||||
|
|
||||||
|
def set_game_systems(systems: Sequence[GameSystem]):
|
||||||
|
global game_systems
|
||||||
|
game_systems = list(systems)
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Callable, Protocol
|
||||||
|
|
||||||
|
from packit.agent import Agent
|
||||||
|
|
||||||
|
from adventure.models.entity import World, WorldEntity
|
||||||
|
|
||||||
|
|
||||||
|
class FormatPerspective(Enum):
|
||||||
|
FIRST_PERSON = "first-person"
|
||||||
|
SECOND_PERSON = "second-person"
|
||||||
|
THIRD_PERSON = "third-person"
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: remove the attributes parameter from all of these
|
||||||
|
class SystemFormat(Protocol):
|
||||||
|
def __call__(
|
||||||
|
self,
|
||||||
|
entity: WorldEntity,
|
||||||
|
perspective: FormatPerspective = FormatPerspective.SECOND_PERSON,
|
||||||
|
) -> str:
|
||||||
|
# TODO: should this return a list?
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class SystemGenerate(Protocol):
|
||||||
|
def __call__(self, agent: Agent, theme: str, entity: WorldEntity) -> None:
|
||||||
|
"""
|
||||||
|
Generate a new world entity based on the given theme and entity.
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class SystemSimulate(Protocol):
|
||||||
|
def __call__(self, world: World, step: int) -> None:
|
||||||
|
"""
|
||||||
|
Simulate the world for the given step.
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class GameSystem:
|
||||||
|
format: SystemFormat | None = None
|
||||||
|
generate: SystemGenerate | None = None
|
||||||
|
simulate: SystemSimulate | None = None
|
||||||
|
# render: TODO
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
format: SystemFormat | None = None,
|
||||||
|
generate: SystemGenerate | None = None,
|
||||||
|
simulate: SystemSimulate | None = None,
|
||||||
|
):
|
||||||
|
self.format = format
|
||||||
|
self.generate = generate
|
||||||
|
self.simulate = simulate
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"GameSystem(format={format_callable(self.format)}, generate={format_callable(self.generate)}, simulate={format_callable(self.simulate)})"
|
||||||
|
|
||||||
|
|
||||||
|
def format_callable(fn: Callable | None) -> str:
|
||||||
|
if fn:
|
||||||
|
return f"{fn.__module__}:{fn.__name__}"
|
||||||
|
|
||||||
|
return "None"
|
|
@ -4,8 +4,19 @@ from typing import List
|
||||||
|
|
||||||
from packit.agent import Agent
|
from packit.agent import Agent
|
||||||
from packit.loops import loop_retry
|
from packit.loops import loop_retry
|
||||||
|
from packit.utils import could_be_json
|
||||||
|
|
||||||
from adventure.models.entity import Actor, Item, Room, World, WorldEntity
|
from adventure.game_system import GameSystem
|
||||||
|
from adventure.models.entity import (
|
||||||
|
Actor,
|
||||||
|
Effect,
|
||||||
|
Item,
|
||||||
|
NumberAttributeEffect,
|
||||||
|
Room,
|
||||||
|
StringAttributeEffect,
|
||||||
|
World,
|
||||||
|
WorldEntity,
|
||||||
|
)
|
||||||
from adventure.models.event import EventCallback, GenerateEvent
|
from adventure.models.event import EventCallback, GenerateEvent
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
@ -19,19 +30,22 @@ OPPOSITE_DIRECTIONS = {
|
||||||
|
|
||||||
|
|
||||||
def duplicate_name_parser(existing_names: List[str]):
|
def duplicate_name_parser(existing_names: List[str]):
|
||||||
def name_parser(name: str, **kwargs):
|
def name_parser(value: str, **kwargs):
|
||||||
logger.debug(f"validating name: {name}")
|
print(f"validating generated name: {value}")
|
||||||
|
|
||||||
if name in existing_names:
|
if value in existing_names:
|
||||||
raise ValueError(f'"{name}" has already been used.')
|
raise ValueError(f'"{value}" has already been used.')
|
||||||
|
|
||||||
if '"' in name or ":" in name:
|
if could_be_json(value):
|
||||||
|
raise ValueError("The name cannot contain JSON or other commands.")
|
||||||
|
|
||||||
|
if '"' in value or ":" in value:
|
||||||
raise ValueError("The name cannot contain quotes or colons.")
|
raise ValueError("The name cannot contain quotes or colons.")
|
||||||
|
|
||||||
if len(name) > 50:
|
if len(value) > 50:
|
||||||
raise ValueError("The name cannot be longer than 50 characters.")
|
raise ValueError("The name cannot be longer than 50 characters.")
|
||||||
|
|
||||||
return name
|
return value
|
||||||
|
|
||||||
return name_parser
|
return name_parser
|
||||||
|
|
||||||
|
@ -123,8 +137,23 @@ def generate_item(
|
||||||
)
|
)
|
||||||
|
|
||||||
actions = {}
|
actions = {}
|
||||||
|
item = Item(name=name, description=desc, actions=actions)
|
||||||
|
|
||||||
return Item(name=name, description=desc, actions=actions)
|
effect_count = randint(1, 2)
|
||||||
|
callback_wrapper(
|
||||||
|
callback, message=f"Generating {effect_count} effects for item: {name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
effects = []
|
||||||
|
for i in range(effect_count):
|
||||||
|
try:
|
||||||
|
effect = generate_effect(agent, world_theme, entity=item, callback=callback)
|
||||||
|
effects.append(effect)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("error generating effect")
|
||||||
|
|
||||||
|
item.effects = effects
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
def generate_actor(
|
def generate_actor(
|
||||||
|
@ -171,6 +200,100 @@ def generate_actor(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_effect(
|
||||||
|
agent: Agent, theme: str, entity: Item, callback: EventCallback | None = None
|
||||||
|
) -> Effect:
|
||||||
|
entity_type = entity.type
|
||||||
|
|
||||||
|
existing_effects = [effect.name for effect in entity.effects]
|
||||||
|
|
||||||
|
name = loop_retry(
|
||||||
|
agent,
|
||||||
|
"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_type": entity_type,
|
||||||
|
"existing_effects": existing_effects,
|
||||||
|
"theme": theme,
|
||||||
|
},
|
||||||
|
result_parser=duplicate_name_parser(existing_effects),
|
||||||
|
)
|
||||||
|
callback_wrapper(callback, message=f"Generating effect: {name}")
|
||||||
|
|
||||||
|
description = agent(
|
||||||
|
"Generate a detailed description of the {name} effect. What does it look like? What does it do? "
|
||||||
|
"How does it affect the target? Describe the effect from the perspective of an outside observer.",
|
||||||
|
name=name,
|
||||||
|
)
|
||||||
|
|
||||||
|
attribute_names = agent(
|
||||||
|
"Generate a list of attributes that the {name} effect modifies. "
|
||||||
|
"For example, 'heal' increases the target's 'health' attribute, while 'poison' decreases it. "
|
||||||
|
"Use a comma-separated list of attribute names, such as 'health, strength, speed'. "
|
||||||
|
"Only include the attribute names, do not include the question or any JSON.",
|
||||||
|
name=name,
|
||||||
|
)
|
||||||
|
|
||||||
|
attributes = []
|
||||||
|
for attribute_name in attribute_names.split(","):
|
||||||
|
attribute_name = attribute_name.strip()
|
||||||
|
if attribute_name:
|
||||||
|
operation = agent(
|
||||||
|
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."
|
||||||
|
"Choose from the following operations: {operations}",
|
||||||
|
name=name,
|
||||||
|
attribute_name=attribute_name,
|
||||||
|
operations=[
|
||||||
|
"set",
|
||||||
|
"add",
|
||||||
|
"subtract",
|
||||||
|
"multiply",
|
||||||
|
"divide",
|
||||||
|
"append",
|
||||||
|
"prepend",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
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.",
|
||||||
|
name=name,
|
||||||
|
attribute_name=attribute_name,
|
||||||
|
)
|
||||||
|
value = value.strip()
|
||||||
|
if value.isdigit():
|
||||||
|
value = int(value)
|
||||||
|
attribute_effect = NumberAttributeEffect(
|
||||||
|
name=attribute_name, operation=operation, value=value
|
||||||
|
)
|
||||||
|
elif value.isdecimal():
|
||||||
|
value = float(value)
|
||||||
|
attribute_effect = NumberAttributeEffect(
|
||||||
|
name=attribute_name, operation=operation, value=value
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
attribute_effect = StringAttributeEffect(
|
||||||
|
name=attribute_name, operation=operation, value=value
|
||||||
|
)
|
||||||
|
|
||||||
|
attributes.append(attribute_effect)
|
||||||
|
|
||||||
|
return Effect(name=name, description=description, attributes=[])
|
||||||
|
|
||||||
|
|
||||||
|
def generate_system_attributes(
|
||||||
|
agent: Agent, theme: str, entity: WorldEntity, systems: List[GameSystem] = []
|
||||||
|
) -> None:
|
||||||
|
for system in systems:
|
||||||
|
if system.generate:
|
||||||
|
system.generate(agent, theme, entity)
|
||||||
|
|
||||||
|
|
||||||
def generate_world(
|
def generate_world(
|
||||||
agent: Agent,
|
agent: Agent,
|
||||||
name: str,
|
name: str,
|
||||||
|
@ -178,6 +301,7 @@ def generate_world(
|
||||||
room_count: int | None = None,
|
room_count: int | None = None,
|
||||||
max_rooms: int = 5,
|
max_rooms: int = 5,
|
||||||
callback: EventCallback | None = None,
|
callback: EventCallback | None = None,
|
||||||
|
systems: List[GameSystem] = [],
|
||||||
) -> World:
|
) -> World:
|
||||||
room_count = room_count or randint(3, max_rooms)
|
room_count = room_count or randint(3, max_rooms)
|
||||||
|
|
||||||
|
@ -194,6 +318,7 @@ def generate_world(
|
||||||
room = generate_room(
|
room = generate_room(
|
||||||
agent, theme, existing_rooms=existing_rooms, callback=callback
|
agent, theme, existing_rooms=existing_rooms, callback=callback
|
||||||
)
|
)
|
||||||
|
generate_system_attributes(agent, theme, room, systems)
|
||||||
callback_wrapper(callback, entity=room)
|
callback_wrapper(callback, entity=room)
|
||||||
rooms.append(room)
|
rooms.append(room)
|
||||||
existing_rooms.append(room.name)
|
existing_rooms.append(room.name)
|
||||||
|
@ -215,6 +340,7 @@ def generate_world(
|
||||||
existing_items=existing_items,
|
existing_items=existing_items,
|
||||||
callback=callback,
|
callback=callback,
|
||||||
)
|
)
|
||||||
|
generate_system_attributes(agent, theme, item, systems)
|
||||||
callback_wrapper(callback, entity=item)
|
callback_wrapper(callback, entity=item)
|
||||||
|
|
||||||
room.items.append(item)
|
room.items.append(item)
|
||||||
|
@ -236,6 +362,7 @@ def generate_world(
|
||||||
existing_actors=existing_actors,
|
existing_actors=existing_actors,
|
||||||
callback=callback,
|
callback=callback,
|
||||||
)
|
)
|
||||||
|
generate_system_attributes(agent, theme, actor, systems)
|
||||||
callback_wrapper(callback, entity=actor)
|
callback_wrapper(callback, entity=actor)
|
||||||
|
|
||||||
room.actors.append(actor)
|
room.actors.append(actor)
|
||||||
|
@ -259,6 +386,7 @@ def generate_world(
|
||||||
existing_items=existing_items,
|
existing_items=existing_items,
|
||||||
callback=callback,
|
callback=callback,
|
||||||
)
|
)
|
||||||
|
generate_system_attributes(agent, theme, item, systems)
|
||||||
callback_wrapper(callback, entity=item)
|
callback_wrapper(callback, entity=item)
|
||||||
|
|
||||||
actor.items.append(item)
|
actor.items.append(item)
|
||||||
|
|
|
@ -7,13 +7,12 @@ from pydantic import Field
|
||||||
from rule_engine import Rule
|
from rule_engine import Rule
|
||||||
from yaml import Loader, load
|
from yaml import Loader, load
|
||||||
|
|
||||||
|
from adventure.game_system import FormatPerspective, GameSystem
|
||||||
from adventure.models.entity import (
|
from adventure.models.entity import (
|
||||||
Actor,
|
|
||||||
Attributes,
|
Attributes,
|
||||||
AttributeValue,
|
AttributeValue,
|
||||||
Item,
|
|
||||||
Room,
|
|
||||||
World,
|
World,
|
||||||
|
WorldEntity,
|
||||||
dataclass,
|
dataclass,
|
||||||
)
|
)
|
||||||
from adventure.plugins import get_plugin_function
|
from adventure.plugins import get_plugin_function
|
||||||
|
@ -44,16 +43,15 @@ class LogicTable:
|
||||||
labels: Dict[str, Dict[AttributeValue, LogicLabel]] = Field(default_factory=dict)
|
labels: Dict[str, Dict[AttributeValue, LogicLabel]] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
LogicTrigger = Callable[[Room | Actor | Item, Attributes], Attributes]
|
LogicTrigger = Callable[[WorldEntity], None]
|
||||||
TriggerTable = Dict[str, LogicTrigger]
|
TriggerTable = Dict[str, LogicTrigger]
|
||||||
|
|
||||||
|
|
||||||
def update_attributes(
|
def update_attributes(
|
||||||
entity: Room | Actor | Item,
|
entity: WorldEntity,
|
||||||
attributes: Attributes,
|
|
||||||
rules: LogicTable,
|
rules: LogicTable,
|
||||||
triggers: TriggerTable,
|
triggers: TriggerTable,
|
||||||
) -> Attributes:
|
) -> None:
|
||||||
entity_type = entity.__class__.__name__.lower()
|
entity_type = entity.__class__.__name__.lower()
|
||||||
skip_groups = set()
|
skip_groups = set()
|
||||||
|
|
||||||
|
@ -64,7 +62,7 @@ def update_attributes(
|
||||||
continue
|
continue
|
||||||
|
|
||||||
typed_attributes = {
|
typed_attributes = {
|
||||||
**attributes,
|
**entity.attributes,
|
||||||
"type": entity_type,
|
"type": entity_type,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,52 +91,46 @@ def update_attributes(
|
||||||
skip_groups.add(rule.group)
|
skip_groups.add(rule.group)
|
||||||
|
|
||||||
for key in rule.remove or []:
|
for key in rule.remove or []:
|
||||||
attributes.pop(key, None)
|
entity.attributes.pop(key, None)
|
||||||
|
|
||||||
if rule.set:
|
if rule.set:
|
||||||
attributes.update(rule.set)
|
entity.attributes.update(rule.set)
|
||||||
logger.info("logic set state: %s", rule.set)
|
logger.info("logic set state: %s", rule.set)
|
||||||
|
|
||||||
if rule.trigger:
|
if rule.trigger:
|
||||||
for trigger in rule.trigger:
|
for trigger in rule.trigger:
|
||||||
if trigger in triggers:
|
if trigger in triggers:
|
||||||
attributes = triggers[trigger](entity, attributes)
|
triggers[trigger](entity)
|
||||||
|
|
||||||
return attributes
|
|
||||||
|
|
||||||
|
|
||||||
def update_logic(
|
def update_logic(
|
||||||
world: World, step: int, rules: LogicTable, triggers: TriggerTable
|
world: World, step: int, rules: LogicTable, triggers: TriggerTable
|
||||||
) -> None:
|
) -> None:
|
||||||
for room in world.rooms:
|
for room in world.rooms:
|
||||||
room.attributes = update_attributes(
|
update_attributes(room, rules=rules, triggers=triggers)
|
||||||
room, room.attributes, rules=rules, triggers=triggers
|
|
||||||
)
|
|
||||||
for actor in room.actors:
|
for actor in room.actors:
|
||||||
actor.attributes = update_attributes(
|
update_attributes(actor, rules=rules, triggers=triggers)
|
||||||
actor, actor.attributes, rules=rules, triggers=triggers
|
|
||||||
)
|
|
||||||
for item in actor.items:
|
for item in actor.items:
|
||||||
item.attributes = update_attributes(
|
update_attributes(item, rules=rules, triggers=triggers)
|
||||||
item, item.attributes, rules=rules, triggers=triggers
|
|
||||||
)
|
|
||||||
for item in room.items:
|
for item in room.items:
|
||||||
item.attributes = update_attributes(
|
update_attributes(item, rules=rules, triggers=triggers)
|
||||||
item, item.attributes, rules=rules, triggers=triggers
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info("updated world attributes")
|
logger.info("updated world attributes")
|
||||||
|
|
||||||
|
|
||||||
def format_logic(attributes: Attributes, rules: LogicTable, self=True) -> str:
|
def format_logic(
|
||||||
|
entity: WorldEntity,
|
||||||
|
rules: LogicTable,
|
||||||
|
perspective: FormatPerspective = FormatPerspective.SECOND_PERSON,
|
||||||
|
) -> str:
|
||||||
labels = []
|
labels = []
|
||||||
|
|
||||||
for attribute, value in attributes.items():
|
for attribute, value in entity.attributes.items():
|
||||||
if attribute in rules.labels and value in rules.labels[attribute]:
|
if attribute in rules.labels and value in rules.labels[attribute]:
|
||||||
label = rules.labels[attribute][value]
|
label = rules.labels[attribute][value]
|
||||||
if self:
|
if perspective == FormatPerspective.SECOND_PERSON:
|
||||||
labels.append(label.backstory)
|
labels.append(label.backstory)
|
||||||
elif label.description:
|
elif perspective == FormatPerspective.THIRD_PERSON and label.description:
|
||||||
labels.append(label.description)
|
labels.append(label.description)
|
||||||
else:
|
else:
|
||||||
logger.debug("label has no relevant description: %s", label)
|
logger.debug("label has no relevant description: %s", label)
|
||||||
|
@ -149,7 +141,7 @@ def format_logic(attributes: Attributes, rules: LogicTable, self=True) -> str:
|
||||||
return " ".join(labels)
|
return " ".join(labels)
|
||||||
|
|
||||||
|
|
||||||
def init_from_file(filename: str):
|
def load_logic(filename: str):
|
||||||
logger.info("loading logic from file: %s", filename)
|
logger.info("loading logic from file: %s", filename)
|
||||||
with open(filename) as file:
|
with open(filename) as file:
|
||||||
logic_rules = LogicTable(**load(file, Loader=Loader))
|
logic_rules = LogicTable(**load(file, Loader=Loader))
|
||||||
|
@ -161,9 +153,12 @@ def init_from_file(filename: str):
|
||||||
logic_triggers[trigger] = get_plugin_function(trigger)
|
logic_triggers[trigger] = get_plugin_function(trigger)
|
||||||
|
|
||||||
logger.info("initialized logic system")
|
logger.info("initialized logic system")
|
||||||
return (
|
system_simulate = wraps(update_logic)(
|
||||||
wraps(update_logic)(
|
|
||||||
partial(update_logic, rules=logic_rules, triggers=logic_triggers)
|
partial(update_logic, rules=logic_rules, triggers=logic_triggers)
|
||||||
),
|
)
|
||||||
wraps(format_logic)(partial(format_logic, rules=logic_rules)),
|
system_format = wraps(format_logic)(partial(format_logic, rules=logic_rules))
|
||||||
|
|
||||||
|
return GameSystem(
|
||||||
|
format=system_format,
|
||||||
|
simulate=system_simulate,
|
||||||
)
|
)
|
||||||
|
|
|
@ -9,6 +9,7 @@ from packit.utils import logger_with_colors
|
||||||
from yaml import Loader, load
|
from yaml import Loader, load
|
||||||
|
|
||||||
from adventure.context import set_current_step, set_dungeon_master
|
from adventure.context import set_current_step, set_dungeon_master
|
||||||
|
from adventure.game_system import GameSystem
|
||||||
from adventure.generate import generate_world
|
from adventure.generate import generate_world
|
||||||
from adventure.models.config import Config
|
from adventure.models.config import Config
|
||||||
from adventure.models.entity import World, WorldState
|
from adventure.models.entity import World, WorldState
|
||||||
|
@ -83,6 +84,7 @@ def parse_args():
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--max-rooms",
|
"--max-rooms",
|
||||||
|
default=6,
|
||||||
type=int,
|
type=int,
|
||||||
help="The maximum number of rooms to generate",
|
help="The maximum number of rooms to generate",
|
||||||
)
|
)
|
||||||
|
@ -113,7 +115,7 @@ def parse_args():
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--server",
|
"--server",
|
||||||
type=str,
|
action="store_true",
|
||||||
help="The address on which to run the server",
|
help="The address on which to run the server",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
|
@ -220,8 +222,9 @@ def load_or_generate_world(
|
||||||
save_world(world, world_file)
|
save_world(world, world_file)
|
||||||
|
|
||||||
# run the systems once to initialize everything
|
# run the systems once to initialize everything
|
||||||
for system_update, _ in systems:
|
for system in systems:
|
||||||
system_update(world, 0)
|
if system.simulate:
|
||||||
|
system.simulate(world, 0)
|
||||||
|
|
||||||
create_agents(world, memory=memory, players=players)
|
create_agents(world, memory=memory, players=players)
|
||||||
return (world, world_state_file)
|
return (world, world_state_file)
|
||||||
|
@ -299,18 +302,16 @@ def main():
|
||||||
extra_actions.extend(module_actions)
|
extra_actions.extend(module_actions)
|
||||||
|
|
||||||
# load extra systems from plugins
|
# load extra systems from plugins
|
||||||
extra_systems = []
|
extra_systems: List[GameSystem] = []
|
||||||
for system_name in args.systems or []:
|
for system_name in args.systems or []:
|
||||||
logger.info(f"loading extra systems from {system_name}")
|
logger.info(f"loading extra systems from {system_name}")
|
||||||
module_systems = load_plugin(system_name)
|
module_systems = load_plugin(system_name)
|
||||||
logger.info(
|
logger.info(f"loaded extra systems: {module_systems}")
|
||||||
f"loaded extra systems: {[component.__name__ for system in module_systems for component in system]}"
|
|
||||||
)
|
|
||||||
extra_systems.extend(module_systems)
|
extra_systems.extend(module_systems)
|
||||||
|
|
||||||
# make sure the server system runs after any updates
|
# make sure the server system runs after any updates
|
||||||
if args.server:
|
if args.server:
|
||||||
extra_systems.append((server_system, None))
|
extra_systems.append(GameSystem(simulate=server_system))
|
||||||
|
|
||||||
# load or generate the world
|
# load or generate the world
|
||||||
world_prompt = get_world_prompt(args)
|
world_prompt = get_world_prompt(args)
|
||||||
|
@ -323,7 +324,7 @@ def main():
|
||||||
logger.info("taking snapshot of world state")
|
logger.info("taking snapshot of world state")
|
||||||
save_world_state(world, step, world_state_file)
|
save_world_state(world, step, world_state_file)
|
||||||
|
|
||||||
extra_systems.append((snapshot_system, None))
|
extra_systems.append(GameSystem(simulate=snapshot_system))
|
||||||
|
|
||||||
# hack: send a snapshot to the websocket server
|
# hack: send a snapshot to the websocket server
|
||||||
if args.server:
|
if args.server:
|
||||||
|
|
|
@ -5,16 +5,51 @@ from pydantic import Field
|
||||||
from .base import BaseModel, dataclass, uuid
|
from .base import BaseModel, dataclass, uuid
|
||||||
|
|
||||||
Actions = Dict[str, Callable]
|
Actions = Dict[str, Callable]
|
||||||
AttributeValue = bool | int | str
|
AttributeValue = bool | float | int | str
|
||||||
Attributes = Dict[str, AttributeValue]
|
Attributes = Dict[str, AttributeValue]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class StringAttributeEffect:
|
||||||
|
name: str
|
||||||
|
operation: Literal["set", "append", "prepend"]
|
||||||
|
value: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class NumberAttributeEffect:
|
||||||
|
name: str
|
||||||
|
operation: Literal["set", "add", "subtract", "multiply", "divide"]
|
||||||
|
# TODO: make this a range
|
||||||
|
value: int | float
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BooleanAttributeEffect:
|
||||||
|
name: str
|
||||||
|
operation: Literal["set", "toggle"]
|
||||||
|
value: bool
|
||||||
|
|
||||||
|
|
||||||
|
AttributeEffect = StringAttributeEffect | NumberAttributeEffect | BooleanAttributeEffect
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Effect(BaseModel):
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
attributes: list[AttributeEffect] = Field(default_factory=list)
|
||||||
|
id: str = Field(default_factory=uuid)
|
||||||
|
type: Literal["effect"] = "effect"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Item(BaseModel):
|
class Item(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
description: str
|
description: str
|
||||||
actions: Actions = Field(default_factory=dict)
|
actions: Actions = Field(default_factory=dict)
|
||||||
attributes: Attributes = Field(default_factory=dict)
|
attributes: Attributes = Field(default_factory=dict)
|
||||||
|
effects: List[Effect] = Field(default_factory=list)
|
||||||
items: List["Item"] = Field(default_factory=list)
|
items: List["Item"] = Field(default_factory=list)
|
||||||
id: str = Field(default_factory=uuid)
|
id: str = Field(default_factory=uuid)
|
||||||
type: Literal["item"] = "item"
|
type: Literal["item"] = "item"
|
||||||
|
|
|
@ -13,6 +13,8 @@ from adventure.context import (
|
||||||
)
|
)
|
||||||
from adventure.generate import OPPOSITE_DIRECTIONS, generate_item, generate_room
|
from adventure.generate import OPPOSITE_DIRECTIONS, generate_item, generate_room
|
||||||
from adventure.search import find_actor_in_room
|
from adventure.search import find_actor_in_room
|
||||||
|
from adventure.utils.effect import apply_effect
|
||||||
|
from adventure.utils.world import describe_actor, describe_entity
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
@ -128,10 +130,35 @@ def action_use(item: str, target: str) -> str:
|
||||||
if not target_actor:
|
if not target_actor:
|
||||||
return f"The {target} character is not in the room."
|
return f"The {target} character is not in the room."
|
||||||
|
|
||||||
broadcast(f"{action_actor.name} uses {item} on {target}")
|
effect_names = [effect.name for effect in action_item.effects]
|
||||||
outcome = dungeon_master(
|
chosen_name = dungeon_master(
|
||||||
f"{action_actor.name} uses {item} on {target}. "
|
f"{action_actor.name} uses {item} on {target}. "
|
||||||
f"{action_actor.description}. {target_actor.description}. {action_item.description}. "
|
f"{item} has the following effects: {effect_names}. "
|
||||||
|
"Which effect should be applied? Specify the name of the effect to apply."
|
||||||
|
"Do not include the question or any JSON. Only include the name of the effect to apply."
|
||||||
|
)
|
||||||
|
chosen_name = chosen_name.strip()
|
||||||
|
|
||||||
|
chosen_effect = next(
|
||||||
|
(
|
||||||
|
search_effect
|
||||||
|
for search_effect in action_item.effects
|
||||||
|
if search_effect.name == chosen_name
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if not chosen_effect:
|
||||||
|
# TODO: should retry the question if the effect is not found
|
||||||
|
return f"The {chosen_name} effect is not available to apply."
|
||||||
|
|
||||||
|
apply_effect(chosen_effect, target_actor.attributes)
|
||||||
|
|
||||||
|
broadcast(
|
||||||
|
f"{action_actor.name} uses the {chosen_name} effect of {item} on {target}"
|
||||||
|
)
|
||||||
|
outcome = dungeon_master(
|
||||||
|
f"{action_actor.name} uses the {chosen_name} effect of {item} on {target}. "
|
||||||
|
f"{describe_actor(action_actor)}. {describe_actor(target_actor)}. {describe_entity(action_item)}. "
|
||||||
f"What happens? How does {target} react? Be creative with the results. The outcome can be good, bad, or neutral."
|
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."
|
"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."
|
"Specify the outcome of the action. Do not include the question or any JSON. Only include the outcome of the action."
|
||||||
|
|
|
@ -27,6 +27,7 @@ from adventure.models.event import (
|
||||||
ResultEvent,
|
ResultEvent,
|
||||||
StatusEvent,
|
StatusEvent,
|
||||||
)
|
)
|
||||||
|
from adventure.utils.world import describe_entity
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
@ -224,23 +225,26 @@ def generate_images(
|
||||||
def prompt_from_event(event: GameEvent) -> str | None:
|
def prompt_from_event(event: GameEvent) -> str | None:
|
||||||
if isinstance(event, ActionEvent):
|
if isinstance(event, ActionEvent):
|
||||||
if event.item:
|
if event.item:
|
||||||
return f"{event.actor.name} uses the {event.item.name}. {event.item.description}. {event.actor.description}. {event.room.description}."
|
return (
|
||||||
|
f"{event.actor.name} uses the {event.item.name}. {describe_entity(event.item)}. "
|
||||||
|
f"{describe_entity(event.actor)}. {describe_entity(event.room)}."
|
||||||
|
)
|
||||||
|
|
||||||
action_name = event.action.removeprefix("action_")
|
action_name = event.action.removeprefix("action_")
|
||||||
return f"{event.actor.name} uses {action_name}. {event.actor.description}. {event.room.description}."
|
return f"{event.actor.name} uses {action_name}. {describe_entity(event.actor)}. {describe_entity(event.room)}."
|
||||||
|
|
||||||
if isinstance(event, ReplyEvent):
|
if isinstance(event, ReplyEvent):
|
||||||
return event.text
|
return event.text
|
||||||
|
|
||||||
if isinstance(event, ResultEvent):
|
if isinstance(event, ResultEvent):
|
||||||
return f"{event.result}. {event.actor.description}. {event.room.description}."
|
return f"{event.result}. {describe_entity(event.actor)}. {describe_entity(event.room)}."
|
||||||
|
|
||||||
if isinstance(event, StatusEvent):
|
if isinstance(event, StatusEvent):
|
||||||
if event.room:
|
if event.room:
|
||||||
if event.actor:
|
if event.actor:
|
||||||
return f"{event.text}. {event.actor.description}. {event.room.description}."
|
return f"{event.text}. {describe_entity(event.actor)}. {describe_entity(event.room)}."
|
||||||
|
|
||||||
return f"{event.text}. {event.room.description}."
|
return f"{event.text}. {describe_entity(event.room)}."
|
||||||
|
|
||||||
return event.text
|
return event.text
|
||||||
|
|
||||||
|
@ -248,7 +252,7 @@ def prompt_from_event(event: GameEvent) -> str | None:
|
||||||
|
|
||||||
|
|
||||||
def prompt_from_entity(entity: WorldEntity) -> str:
|
def prompt_from_entity(entity: WorldEntity) -> str:
|
||||||
return entity.description
|
return describe_entity(entity)
|
||||||
|
|
||||||
|
|
||||||
def sanitize_name(name: str) -> str:
|
def sanitize_name(name: str) -> str:
|
||||||
|
|
|
@ -2,7 +2,7 @@ from .hunger_actions import action_cook, action_eat
|
||||||
from .hygiene_actions import action_wash
|
from .hygiene_actions import action_wash
|
||||||
from .sleeping_actions import action_sleep
|
from .sleeping_actions import action_sleep
|
||||||
|
|
||||||
from adventure.logic import init_from_file
|
from adventure.logic import load_logic
|
||||||
|
|
||||||
LOGIC_FILES = [
|
LOGIC_FILES = [
|
||||||
"./adventure/sim_systems/environment_logic.yaml",
|
"./adventure/sim_systems/environment_logic.yaml",
|
||||||
|
@ -26,4 +26,4 @@ def init_actions():
|
||||||
|
|
||||||
|
|
||||||
def init_logic():
|
def init_logic():
|
||||||
return [init_from_file(filename) for filename in LOGIC_FILES]
|
return [load_logic(filename) for filename in LOGIC_FILES]
|
||||||
|
|
|
@ -5,6 +5,7 @@ from adventure.context import (
|
||||||
get_dungeon_master,
|
get_dungeon_master,
|
||||||
)
|
)
|
||||||
from adventure.search import find_actor_in_room, find_item_in_room
|
from adventure.search import find_actor_in_room, find_item_in_room
|
||||||
|
from adventure.utils.world import describe_entity
|
||||||
|
|
||||||
|
|
||||||
def action_attack(target: str) -> str:
|
def action_attack(target: str) -> str:
|
||||||
|
@ -33,8 +34,8 @@ def action_attack(target: str) -> str:
|
||||||
)
|
)
|
||||||
|
|
||||||
outcome = dungeon_master(
|
outcome = dungeon_master(
|
||||||
f"{action_actor.name} attacks {target} in the {action_room.name}. {action_room.description}."
|
f"{action_actor.name} attacks {target} in the {action_room.name}. {describe_entity(action_room)}."
|
||||||
f"{action_actor.description}. {target_actor.description}."
|
f"{describe_entity(action_actor)}. {describe_entity(target_actor)}."
|
||||||
f"{target} reacts by {reaction}. What is the outcome of the attack? Describe the result in detail."
|
f"{target} reacts by {reaction}. What is the outcome of the attack? Describe the result in detail."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -46,8 +47,8 @@ def action_attack(target: str) -> str:
|
||||||
return description
|
return description
|
||||||
elif target_item:
|
elif target_item:
|
||||||
outcome = dungeon_master(
|
outcome = dungeon_master(
|
||||||
f"{action_actor.name} attacks {target} in the {action_room.name}. {action_room.description}."
|
f"{action_actor.name} attacks {target} in the {action_room.name}. {describe_entity(action_room)}."
|
||||||
f"{action_actor.description}. {target_item.description}."
|
f"{describe_entity(action_actor)}. {describe_entity(target_item)}."
|
||||||
f"What is the outcome of the attack? Describe the result in detail."
|
f"What is the outcome of the attack? Describe the result in detail."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -78,8 +79,8 @@ def action_cast(target: str, spell: str) -> str:
|
||||||
|
|
||||||
dungeon_master = get_dungeon_master()
|
dungeon_master = get_dungeon_master()
|
||||||
outcome = dungeon_master(
|
outcome = dungeon_master(
|
||||||
f"{action_actor.name} casts {spell} on {target} in the {action_room.name}. {action_room.description}."
|
f"{action_actor.name} casts {spell} on {target} in the {action_room.name}. {describe_entity(action_room)}."
|
||||||
f"{action_actor.description}. {target_actor.description if target_actor else target_item.description}."
|
f"{describe_entity(action_actor)}. {describe_entity(target_actor) if target_actor else describe_entity(target_item)}."
|
||||||
f"What is the outcome of the spell? Describe the result in detail."
|
f"What is the outcome of the spell? Describe the result in detail."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
rules:
|
rules:
|
||||||
# cooking logic
|
# cooking logic
|
||||||
- match:
|
- group: cooking
|
||||||
|
match:
|
||||||
type: item
|
type: item
|
||||||
edible: true
|
edible: true
|
||||||
cooked: false
|
cooked: false
|
||||||
|
@ -8,7 +9,8 @@ rules:
|
||||||
set:
|
set:
|
||||||
spoiled: true
|
spoiled: true
|
||||||
|
|
||||||
- match:
|
- group: cooking
|
||||||
|
match:
|
||||||
type: item
|
type: item
|
||||||
edible: true
|
edible: true
|
||||||
cooked: true
|
cooked: true
|
||||||
|
@ -17,7 +19,8 @@ rules:
|
||||||
spoiled: true
|
spoiled: true
|
||||||
|
|
||||||
# hunger logic
|
# hunger logic
|
||||||
- match:
|
- group: hunger
|
||||||
|
match:
|
||||||
type: actor
|
type: actor
|
||||||
hunger: full
|
hunger: full
|
||||||
chance: 0.1
|
chance: 0.1
|
||||||
|
@ -25,13 +28,15 @@ rules:
|
||||||
hunger: hungry
|
hunger: hungry
|
||||||
|
|
||||||
# hunger initialization
|
# hunger initialization
|
||||||
- rule: |
|
- group: hunger
|
||||||
|
rule: |
|
||||||
"hunger" not in attributes
|
"hunger" not in attributes
|
||||||
set:
|
set:
|
||||||
hunger: full
|
hunger: full
|
||||||
|
|
||||||
# thirst logic
|
# thirst logic
|
||||||
- match:
|
- group: thirst
|
||||||
|
match:
|
||||||
type: actor
|
type: actor
|
||||||
thirst: hydrated
|
thirst: hydrated
|
||||||
chance: 0.1
|
chance: 0.1
|
||||||
|
@ -39,7 +44,8 @@ rules:
|
||||||
thirst: thirsty
|
thirst: thirsty
|
||||||
|
|
||||||
# thirst initialization
|
# thirst initialization
|
||||||
- rule: |
|
- group: thirst
|
||||||
|
rule: |
|
||||||
"thirst" not in attributes
|
"thirst" not in attributes
|
||||||
set:
|
set:
|
||||||
thirst: hydrated
|
thirst: hydrated
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from adventure.context import get_current_context, get_dungeon_master
|
from adventure.context import get_current_context, get_dungeon_master
|
||||||
|
from adventure.utils.world import describe_entity
|
||||||
|
|
||||||
|
|
||||||
def action_wash(unused: bool) -> str:
|
def action_wash(unused: bool) -> str:
|
||||||
|
@ -11,7 +12,7 @@ def action_wash(unused: bool) -> str:
|
||||||
|
|
||||||
dungeon_master = get_dungeon_master()
|
dungeon_master = get_dungeon_master()
|
||||||
outcome = dungeon_master(
|
outcome = dungeon_master(
|
||||||
f"{action_actor.name} washes themselves in the {action_room.name}. {action_room.description}. {action_actor.description}"
|
f"{action_actor.name} washes themselves in the {action_room.name}. {describe_entity(action_room)}. {describe_entity(action_actor)}"
|
||||||
f"{action_actor.name} was {hygiene} to start with. How clean are they after washing? Respond with 'clean' or 'dirty'."
|
f"{action_actor.name} was {hygiene} to start with. How clean are they after washing? Respond with 'clean' or 'dirty'."
|
||||||
"If the room has a shower or running water, they should be cleaner. If the room is dirty, they should end up dirtier."
|
"If the room has a shower or running water, they should be cleaner. If the room is dirty, they should end up dirtier."
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from adventure.context import get_current_context, get_dungeon_master
|
from adventure.context import get_current_context, get_dungeon_master
|
||||||
|
from adventure.utils.world import describe_entity
|
||||||
|
|
||||||
|
|
||||||
def action_sleep(unused: bool) -> str:
|
def action_sleep(unused: bool) -> str:
|
||||||
|
@ -10,7 +11,7 @@ def action_sleep(unused: bool) -> str:
|
||||||
|
|
||||||
dungeon_master = get_dungeon_master()
|
dungeon_master = get_dungeon_master()
|
||||||
outcome = dungeon_master(
|
outcome = dungeon_master(
|
||||||
f"{action_actor.name} sleeps in the {action_room.name}. {action_room.description}. {action_actor.description}"
|
f"{action_actor.name} sleeps in the {action_room.name}. {describe_entity(action_room)}. {describe_entity(action_actor)}"
|
||||||
"How rested are they? Respond with 'rested' or 'tired'."
|
"How rested are they? Respond with 'rested' or 'tired'."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from typing import Callable, Sequence, Tuple
|
from typing import Callable, Sequence
|
||||||
|
|
||||||
from packit.loops import loop_retry
|
from packit.loops import loop_retry
|
||||||
from packit.results import multi_function_or_str_result
|
from packit.results import multi_function_or_str_result
|
||||||
|
@ -24,8 +24,10 @@ from adventure.context import (
|
||||||
set_current_room,
|
set_current_room,
|
||||||
set_current_step,
|
set_current_step,
|
||||||
set_current_world,
|
set_current_world,
|
||||||
|
set_game_systems,
|
||||||
)
|
)
|
||||||
from adventure.models.entity import Attributes, World
|
from adventure.game_system import GameSystem
|
||||||
|
from adventure.models.entity import World
|
||||||
from adventure.models.event import (
|
from adventure.models.event import (
|
||||||
ActionEvent,
|
ActionEvent,
|
||||||
EventCallback,
|
EventCallback,
|
||||||
|
@ -34,6 +36,7 @@ from adventure.models.event import (
|
||||||
ResultEvent,
|
ResultEvent,
|
||||||
StatusEvent,
|
StatusEvent,
|
||||||
)
|
)
|
||||||
|
from adventure.utils.world import describe_entity, format_attributes
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
@ -62,13 +65,12 @@ def simulate_world(
|
||||||
world: World,
|
world: World,
|
||||||
steps: int = 10,
|
steps: int = 10,
|
||||||
actions: Sequence[Callable[..., str]] = [],
|
actions: Sequence[Callable[..., str]] = [],
|
||||||
systems: Sequence[
|
|
||||||
Tuple[Callable[[World, int], None], Callable[[Attributes], str] | None]
|
|
||||||
] = [],
|
|
||||||
callbacks: Sequence[EventCallback] = [],
|
callbacks: Sequence[EventCallback] = [],
|
||||||
|
systems: Sequence[GameSystem] = [],
|
||||||
):
|
):
|
||||||
logger.info("Simulating the world")
|
logger.info("Simulating the world")
|
||||||
set_current_world(world)
|
set_current_world(world)
|
||||||
|
set_game_systems(systems)
|
||||||
|
|
||||||
# set up a broadcast callback
|
# set up a broadcast callback
|
||||||
def broadcast_callback(message: str | GameEvent):
|
def broadcast_callback(message: str | GameEvent):
|
||||||
|
@ -116,11 +118,7 @@ def simulate_world(
|
||||||
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())
|
||||||
|
|
||||||
actor_attributes = " ".join(
|
actor_attributes = format_attributes(actor)
|
||||||
system_format(actor.attributes)
|
|
||||||
for _, system_format in systems
|
|
||||||
if system_format
|
|
||||||
)
|
|
||||||
actor_items = [item.name for item in actor.items]
|
actor_items = [item.name for item in actor.items]
|
||||||
|
|
||||||
def result_parser(value, agent, **kwargs):
|
def result_parser(value, agent, **kwargs):
|
||||||
|
@ -161,7 +159,7 @@ def simulate_world(
|
||||||
"attributes": actor_attributes,
|
"attributes": actor_attributes,
|
||||||
"directions": room_directions,
|
"directions": room_directions,
|
||||||
"room_name": room.name,
|
"room_name": room.name,
|
||||||
"room_description": room.description,
|
"room_description": describe_entity(room),
|
||||||
"visible_actors": room_actors,
|
"visible_actors": room_actors,
|
||||||
"visible_items": room_items,
|
"visible_items": room_items,
|
||||||
},
|
},
|
||||||
|
@ -176,7 +174,8 @@ def simulate_world(
|
||||||
for callback in callbacks:
|
for callback in callbacks:
|
||||||
callback(result_event)
|
callback(result_event)
|
||||||
|
|
||||||
for system_update, _ in systems:
|
for system in systems:
|
||||||
system_update(world, current_step)
|
if system.simulate:
|
||||||
|
system.simulate(world, current_step)
|
||||||
|
|
||||||
set_current_step(current_step + 1)
|
set_current_step(current_step + 1)
|
||||||
|
|
|
@ -0,0 +1,120 @@
|
||||||
|
from adventure.models.entity import Attributes, AttributeValue
|
||||||
|
|
||||||
|
|
||||||
|
def add_attribute(attributes: Attributes, name: str, value: int | float) -> Attributes:
|
||||||
|
"""
|
||||||
|
Add an attribute to a set of attributes.
|
||||||
|
"""
|
||||||
|
if name in attributes:
|
||||||
|
previous_value = attributes[name]
|
||||||
|
if isinstance(previous_value, str):
|
||||||
|
raise ValueError(f"Cannot add a number to a string attribute: {name}")
|
||||||
|
|
||||||
|
attributes[name] = value + previous_value
|
||||||
|
else:
|
||||||
|
attributes[name] = value
|
||||||
|
|
||||||
|
return attributes
|
||||||
|
|
||||||
|
|
||||||
|
def subtract_attribute(
|
||||||
|
attributes: Attributes, name: str, value: int | float
|
||||||
|
) -> Attributes:
|
||||||
|
"""
|
||||||
|
Subtract an attribute from a set of attributes.
|
||||||
|
"""
|
||||||
|
if name in attributes:
|
||||||
|
previous_value = attributes[name]
|
||||||
|
if isinstance(previous_value, str):
|
||||||
|
raise ValueError(
|
||||||
|
f"Cannot subtract a number from a string attribute: {name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
attributes[name] = value - previous_value
|
||||||
|
else:
|
||||||
|
attributes[name] = -value
|
||||||
|
|
||||||
|
return attributes
|
||||||
|
|
||||||
|
|
||||||
|
def multiply_attribute(
|
||||||
|
attributes: Attributes, name: str, value: int | float
|
||||||
|
) -> Attributes:
|
||||||
|
"""
|
||||||
|
Multiply an attribute in a set of attributes.
|
||||||
|
"""
|
||||||
|
if name in attributes:
|
||||||
|
previous_value = attributes[name]
|
||||||
|
if isinstance(previous_value, str):
|
||||||
|
raise ValueError(f"Cannot multiply a string attribute: {name}")
|
||||||
|
|
||||||
|
attributes[name] = previous_value * value
|
||||||
|
else:
|
||||||
|
attributes[name] = 0
|
||||||
|
|
||||||
|
return attributes
|
||||||
|
|
||||||
|
|
||||||
|
def divide_attribute(
|
||||||
|
attributes: Attributes, name: str, value: int | float
|
||||||
|
) -> Attributes:
|
||||||
|
"""
|
||||||
|
Divide an attribute in a set of attributes.
|
||||||
|
"""
|
||||||
|
if name in attributes:
|
||||||
|
previous_value = attributes[name]
|
||||||
|
if isinstance(previous_value, str):
|
||||||
|
raise ValueError(f"Cannot divide a string attribute: {name}")
|
||||||
|
|
||||||
|
attributes[name] = previous_value / value
|
||||||
|
else:
|
||||||
|
attributes[name] = 0
|
||||||
|
|
||||||
|
return attributes
|
||||||
|
|
||||||
|
|
||||||
|
def set_attribute(
|
||||||
|
attributes: Attributes, name: str, value: AttributeValue
|
||||||
|
) -> Attributes:
|
||||||
|
"""
|
||||||
|
Set an attribute in a set of attributes.
|
||||||
|
"""
|
||||||
|
attributes[name] = value
|
||||||
|
|
||||||
|
return attributes
|
||||||
|
|
||||||
|
|
||||||
|
def append_attribute(attributes: Attributes, name: str, value: str) -> Attributes:
|
||||||
|
"""
|
||||||
|
Append a string to an attribute in a set of attributes.
|
||||||
|
"""
|
||||||
|
if name in attributes:
|
||||||
|
previous_value = attributes[name]
|
||||||
|
if isinstance(previous_value, str):
|
||||||
|
attributes[name] = previous_value + value
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"Cannot append a string to a non-string attribute: {name}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
attributes[name] = value
|
||||||
|
|
||||||
|
return attributes
|
||||||
|
|
||||||
|
|
||||||
|
def prepend_attribute(attributes: Attributes, name: str, value: str) -> Attributes:
|
||||||
|
"""
|
||||||
|
Prepend a string to an attribute in a set of attributes.
|
||||||
|
"""
|
||||||
|
if name in attributes:
|
||||||
|
previous_value = attributes[name]
|
||||||
|
if isinstance(previous_value, str):
|
||||||
|
attributes[name] = value + previous_value
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"Cannot prepend a string to a non-string attribute: {name}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
attributes[name] = value
|
||||||
|
|
||||||
|
return attributes
|
|
@ -0,0 +1,47 @@
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from adventure.models.entity import Attributes, Effect
|
||||||
|
from adventure.utils.attribute import (
|
||||||
|
add_attribute,
|
||||||
|
append_attribute,
|
||||||
|
divide_attribute,
|
||||||
|
multiply_attribute,
|
||||||
|
prepend_attribute,
|
||||||
|
set_attribute,
|
||||||
|
subtract_attribute,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_effect(effect: Effect, attributes: Attributes) -> Attributes:
|
||||||
|
"""
|
||||||
|
Apply an effect to a set of attributes.
|
||||||
|
"""
|
||||||
|
for attribute in effect.attributes:
|
||||||
|
if attribute.operation == "set":
|
||||||
|
set_attribute(attributes, attribute.name, attribute.value)
|
||||||
|
elif attribute.operation == "add":
|
||||||
|
add_attribute(attributes, attribute.name, attribute.value)
|
||||||
|
elif attribute.operation == "subtract":
|
||||||
|
subtract_attribute(attributes, attribute.name, attribute.value)
|
||||||
|
elif attribute.operation == "multiply":
|
||||||
|
multiply_attribute(attributes, attribute.name, attribute.value)
|
||||||
|
elif attribute.operation == "divide":
|
||||||
|
divide_attribute(attributes, attribute.name, attribute.value)
|
||||||
|
elif attribute.operation == "append":
|
||||||
|
append_attribute(attributes, attribute.name, attribute.value)
|
||||||
|
elif attribute.operation == "prepend":
|
||||||
|
prepend_attribute(attributes, attribute.name, attribute.value)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Invalid operation: {attribute.operation}")
|
||||||
|
|
||||||
|
return attributes
|
||||||
|
|
||||||
|
|
||||||
|
def apply_effects(effects: List[Effect], attributes: Attributes) -> Attributes:
|
||||||
|
"""
|
||||||
|
Apply a list of effects to a set of attributes.
|
||||||
|
"""
|
||||||
|
for effect in effects:
|
||||||
|
attributes = apply_effect(effect, attributes)
|
||||||
|
|
||||||
|
return attributes
|
|
@ -0,0 +1,52 @@
|
||||||
|
from logging import getLogger
|
||||||
|
|
||||||
|
from adventure.context import get_game_systems
|
||||||
|
from adventure.game_system import FormatPerspective
|
||||||
|
from adventure.models.entity import Actor, WorldEntity
|
||||||
|
|
||||||
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def describe_actor(
|
||||||
|
actor: Actor, perspective: FormatPerspective = FormatPerspective.SECOND_PERSON
|
||||||
|
) -> str:
|
||||||
|
attribute_descriptions = format_attributes(actor, perspective=perspective)
|
||||||
|
logger.info("describing actor: %s, %s", actor, attribute_descriptions)
|
||||||
|
|
||||||
|
if perspective == FormatPerspective.SECOND_PERSON:
|
||||||
|
actor_description = actor.backstory
|
||||||
|
elif perspective == FormatPerspective.THIRD_PERSON:
|
||||||
|
actor_description = actor.description
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Perspective {perspective} is not implemented")
|
||||||
|
|
||||||
|
return f"{actor_description} {attribute_descriptions}"
|
||||||
|
|
||||||
|
|
||||||
|
def describe_static(entity: WorldEntity) -> str:
|
||||||
|
# TODO: include attributes
|
||||||
|
return entity.description
|
||||||
|
|
||||||
|
|
||||||
|
def describe_entity(
|
||||||
|
entity: WorldEntity,
|
||||||
|
perspective: FormatPerspective = FormatPerspective.SECOND_PERSON,
|
||||||
|
) -> str:
|
||||||
|
if isinstance(entity, Actor):
|
||||||
|
return describe_actor(entity, perspective)
|
||||||
|
|
||||||
|
return describe_static(entity)
|
||||||
|
|
||||||
|
|
||||||
|
def format_attributes(
|
||||||
|
entity: WorldEntity,
|
||||||
|
perspective: FormatPerspective = FormatPerspective.SECOND_PERSON,
|
||||||
|
) -> str:
|
||||||
|
systems = get_game_systems()
|
||||||
|
attribute_descriptions = [
|
||||||
|
system.format(entity, perspective=perspective)
|
||||||
|
for system in systems
|
||||||
|
if system.format
|
||||||
|
]
|
||||||
|
|
||||||
|
return f"{'. '.join(attribute_descriptions)}"
|
|
@ -187,6 +187,12 @@ export function RenderEventItem(props: EventItemProps) {
|
||||||
const { images } = event;
|
const { images } = event;
|
||||||
|
|
||||||
return <ListItem alignItems="flex-start" ref={props.focusRef}>
|
return <ListItem alignItems="flex-start" ref={props.focusRef}>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar alt="Render">
|
||||||
|
<Camera />
|
||||||
|
</Avatar>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText primary="">Render</ListItemText>
|
||||||
<ImageList cols={3} rowHeight={256}>
|
<ImageList cols={3} rowHeight={256}>
|
||||||
{Object.entries(images).map(([name, image]) => <ImageListItem key={name}>
|
{Object.entries(images).map(([name, image]) => <ImageListItem key={name}>
|
||||||
<a href='#' onClick={() => openImage(image as string)}>
|
<a href='#' onClick={() => openImage(image as string)}>
|
||||||
|
|
Loading…
Reference in New Issue