add limited uses to effects, show entity description in web client during generation
This commit is contained in:
parent
c81d2ae3f2
commit
1fea9e9aa4
|
@ -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:
|
||||
|
|
|
@ -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(
|
||||
[
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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=[
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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]}"
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
/>
|
||||
|
|
Loading…
Reference in New Issue