1
0
Fork 0

add limited uses to effects, show entity description in web client during generation
Run Docker Build / build (push) Failing after 11s Details
Run Python Build / build (push) Failing after 17s Details

This commit is contained in:
Sean Sube 2024-05-26 21:33:44 -05:00
parent c81d2ae3f2
commit 1fea9e9aa4
Signed by: ssube
GPG Key ID: 3EED7B957D362AF1
10 changed files with 124 additions and 41 deletions

View File

@ -134,6 +134,7 @@ def action_use(item: str, target: str) -> str:
return f"The {target} character is not in the room."
effect_names = [effect.name for effect in action_item.effects]
# TODO: should use a retry loop and enum result parser
chosen_name = dungeon_master(
f"{action_character.name} uses {item} on {target}. "
f"{item} has the following effects: {effect_names}. "
@ -151,9 +152,17 @@ def action_use(item: str, target: str) -> str:
None,
)
if not chosen_effect:
# TODO: should retry the question if the effect is not found
raise ValueError(f"The {chosen_name} effect is not available to apply.")
if chosen_effect.uses is None:
pass
elif chosen_effect.uses == 0:
raise ActionError(
f"The {chosen_name} effect of {item} has no uses remaining."
)
elif chosen_effect.uses > 0:
chosen_effect.uses -= 1
try:
apply_effects(target_character, [chosen_effect])
except Exception:

View File

@ -130,21 +130,33 @@ def schedule_event(name: str, turns: int):
turns: The number of turns until the event happens.
"""
# TODO: check for existing events with the same name
# TODO: limit the number of events that can be scheduled
with action_context() as (_, action_character):
# TODO: check for existing events with the same name
event = CalendarEvent(name, turns)
action_character.planner.calendar.events.append(event)
return f"{name} is scheduled to happen in {turns} turns."
def check_calendar(unused: bool, count: int = 10):
def check_calendar(count: int):
"""
Read your calendar to see upcoming events that you have scheduled.
Args:
count: The number of upcoming events to read. 5 is usually a good number.
"""
count = min(count, character_config.event_limit)
current_turn = get_current_step()
with action_context() as (_, action_character):
if len(action_character.planner.calendar.events) == 0:
return (
"You have no upcoming events scheduled. You can plan events with other characters or on your own. "
"Make sure to inform others about events that involve them."
)
events = action_character.planner.calendar.events[:count]
return "\n".join(
[

View File

@ -207,7 +207,7 @@ def set_system_data(system: str, data: Any):
# region search functions
def get_character_for_agent(agent):
def get_character_for_agent(agent: Agent) -> Character | None:
return next(
(
inner_character
@ -218,7 +218,7 @@ def get_character_for_agent(agent):
)
def get_agent_for_character(character):
def get_agent_for_character(character: Character) -> Agent | None:
return next(
(
inner_agent
@ -229,7 +229,9 @@ def get_agent_for_character(character):
)
def get_character_agent_for_name(name):
def get_character_agent_for_name(
name: str,
) -> Tuple[Character, Agent] | Tuple[None, None]:
return next(
(
(character, agent)

View File

@ -284,6 +284,8 @@ def generate_character(
world: World,
systems: List[GameSystem],
dest_room: Room,
additional_prompt: str = "",
detail_prompt: str = "",
) -> Character:
existing_characters = [character.name for character in list_characters(world)] + [
character.name for character in list_characters_in_room(dest_room)
@ -291,13 +293,14 @@ def generate_character(
name = loop_retry(
agent,
"Generate one person or creature that would make sense in the world of {world_theme}. "
"The character will be placed in the {dest_room} room. "
"Generate a new character that would make sense in the world of {world_theme}. Characters can be a person, creature, or some other intelligent entity."
"The character will be placed in the {dest_room} room. {additional_prompt}. "
"Only respond with the character 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. '
"Do not include the name of the room. Do not give characters any duplicate names."
"Do not create any duplicate characters. The existing characters are: {existing_characters}",
context={
"additional_prompt": additional_prompt,
"dest_room": dest_room.name,
"existing_characters": existing_characters,
"world_theme": world.theme,
@ -308,14 +311,18 @@ def generate_character(
broadcast_generated(message=f"Generating character: {name}")
description = agent(
"Generate a detailed description of the {name} character. What do they look like? What are they wearing? "
"Generate a detailed description of the {name} character. {additional_prompt}. {detail_prompt}. What do they look like? What are they wearing? "
"What are they doing? Describe their appearance from the perspective of an outside observer."
"Do not include the room or any other characters in the description, because they will move around.",
additional_prompt=additional_prompt,
detail_prompt=detail_prompt,
name=name,
)
backstory = agent(
"Generate a backstory for the {name} character. Where are they from? What are they doing here? What are their "
"Generate a backstory for the {name} character. {additional_prompt}. {detail_prompt}. Where are they from? What are they doing here? What are their "
'goals? Make sure to phrase the backstory in the second person, starting with "you are" and speaking directly to {name}.',
additional_prompt=additional_prompt,
detail_prompt=detail_prompt,
name=name,
)
@ -374,6 +381,31 @@ def generate_effect(agent: Agent, world: World, entity: Item) -> EffectPattern:
name=name,
)
cooldown = loop_retry(
agent,
f"How many turns should the {name} effect wait before it can be used again? Enter a positive number to set a cooldown, or 0 for no cooldown. "
"Do not include any other text. Do not use JSON.",
context={
"name": name,
},
result_parser=int_result,
toolbox=None,
)
uses = loop_retry(
agent,
f"How many times can the {name} effect be used before it is exhausted? Enter a positive number to set a limit, or -1 for unlimited uses. "
"Do not include any other text. Do not use JSON.",
context={
"name": name,
},
result_parser=int_result,
toolbox=None,
)
if uses == -1:
uses = None
attribute_names = agent(
"Generate a short list of attributes that the {name} effect modifies. Include 1 to 3 attributes. "
"For example, 'heal' increases the target's 'health' attribute, while 'poison' decreases it. "
@ -453,8 +485,10 @@ def generate_effect(agent: Agent, world: World, entity: Item) -> EffectPattern:
name,
description,
application,
duration=duration,
attributes=attributes,
cooldown=cooldown,
duration=duration,
uses=uses,
)

View File

@ -20,6 +20,11 @@ class BotConfig:
discord: DiscordBotConfig
@dataclass
class PromptConfig:
prompts: Dict[str, str]
@dataclass
class RenderConfig:
cfg: int | IntRange
@ -74,6 +79,7 @@ class WorldConfig:
@dataclass
class Config:
bot: BotConfig
prompt: PromptConfig
render: RenderConfig
server: ServerConfig
world: WorldConfig
@ -81,6 +87,9 @@ class Config:
DEFAULT_CONFIG = Config(
bot=BotConfig(discord=DiscordBotConfig(channels=["adventure"])),
prompt=PromptConfig(
prompts={},
),
render=RenderConfig(
cfg=IntRange(min=5, max=8),
checkpoints=[

View File

@ -50,8 +50,10 @@ class EffectPattern:
name: str
description: str
application: Literal["permanent", "temporary"]
duration: int | IntRange | None = None
attributes: List[AttributeEffectPattern] = Field(default_factory=list)
cooldown: int | None = None
duration: int | IntRange | None = None
uses: int | None = None
id: str = Field(default_factory=uuid)
type: Literal["effect_pattern"] = "effect_pattern"
@ -96,7 +98,7 @@ AttributeEffectResult = (
class EffectResult:
name: str
description: str
duration: int | None = None
attributes: List[AttributeEffectResult] = Field(default_factory=list)
duration: int | None = None
id: str = Field(default_factory=uuid)
type: Literal["effect_result"] = "effect_result"

View File

@ -35,6 +35,18 @@ def prepend_value(value: AttributeValue, prefix: str) -> str:
return prefix + str(value)
def value_contains(value: AttributeValue, substring: str) -> bool:
"""
Check if a string contains a substring.
"""
if not isinstance(value, str):
raise ValueError(
f"Cannot check for a substring in a non-string attribute: {value}"
)
return substring in value
def add_attribute(attributes: Attributes, name: str, value: int | float) -> Attributes:
"""
Add an attribute to a set of attributes.

View File

@ -13,7 +13,7 @@ from adventure.models.config import DEFAULT_CONFIG
from adventure.models.entity import Character, Room
from adventure.models.event import ReplyEvent
from .string import normalize_name
from .string import and_list, normalize_name
logger = getLogger(__name__)
@ -59,32 +59,6 @@ def make_keyword_condition(end_message: str, keywords=["end", "stop"]):
return set_end, condition_end, result_parser
def and_list(items: List[str]) -> str:
"""
Convert a list of items into a human-readable list.
"""
if not items:
return "nothing"
if len(items) == 1:
return items[0]
return f"{', '.join(items[:-1])}, and {items[-1]}"
def or_list(items: List[str]) -> str:
"""
Convert a list of items into a human-readable list.
"""
if not items:
return "nothing"
if len(items) == 1:
return items[0]
return f"{', '.join(items[:-1])}, or {items[-1]}"
def summarize_room(room: Room, player: Character) -> str:
"""
Summarize a room for the player.

View File

@ -1,4 +1,5 @@
from functools import lru_cache
from typing import List
@lru_cache(maxsize=1024)
@ -6,3 +7,29 @@ def normalize_name(name: str) -> str:
name = name.lower().strip()
name = name.strip('"').strip("'")
return name.removesuffix(".")
def and_list(items: List[str]) -> str:
"""
Convert a list of items into a human-readable list.
"""
if not items:
return "nothing"
if len(items) == 1:
return items[0]
return f"{', '.join(items[:-1])}, and {items[-1]}"
def or_list(items: List[str]) -> str:
"""
Convert a list of items into a human-readable list.
"""
if not items:
return "nothing"
if len(items) == 1:
return items[0]
return f"{', '.join(items[:-1])}, or {items[-1]}"

View File

@ -253,8 +253,10 @@ export function GenerateEventItem(props: EventItemProps) {
const { event, renderEntity } = props;
const { entity, name } = event;
let description = name;
let renderButton;
if (doesExist(entity)) {
description = `Finished generating ${entity.type} ${entity.name}: ${entity.description}`;
renderButton = <IconButton edge="end" aria-label="render" onClick={() => renderEntity(entity.type, entity.name)}>
<Camera />
</IconButton>;
@ -277,7 +279,7 @@ export function GenerateEventItem(props: EventItemProps) {
variant="body2"
color="text.primary"
>
{name}
{description}
</Typography>
}
/>