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." return f"The {target} character is not in the room."
effect_names = [effect.name for effect in action_item.effects] effect_names = [effect.name for effect in action_item.effects]
# TODO: should use a retry loop and enum result parser
chosen_name = dungeon_master( chosen_name = dungeon_master(
f"{action_character.name} uses {item} on {target}. " f"{action_character.name} uses {item} on {target}. "
f"{item} has the following effects: {effect_names}. " f"{item} has the following effects: {effect_names}. "
@ -151,9 +152,17 @@ def action_use(item: str, target: str) -> str:
None, None,
) )
if not chosen_effect: 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.") 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: try:
apply_effects(target_character, [chosen_effect]) apply_effects(target_character, [chosen_effect])
except Exception: except Exception:

View File

@ -130,21 +130,33 @@ def schedule_event(name: str, turns: int):
turns: The number of turns until the event happens. 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): with action_context() as (_, action_character):
# TODO: check for existing events with the same name
event = CalendarEvent(name, turns) event = CalendarEvent(name, turns)
action_character.planner.calendar.events.append(event) action_character.planner.calendar.events.append(event)
return f"{name} is scheduled to happen in {turns} turns." 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. 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() current_turn = get_current_step()
with action_context() as (_, action_character): 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] events = action_character.planner.calendar.events[:count]
return "\n".join( return "\n".join(
[ [

View File

@ -207,7 +207,7 @@ def set_system_data(system: str, data: Any):
# region search functions # region search functions
def get_character_for_agent(agent): def get_character_for_agent(agent: Agent) -> Character | None:
return next( return next(
( (
inner_character 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( return next(
( (
inner_agent 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( return next(
( (
(character, agent) (character, agent)

View File

@ -284,6 +284,8 @@ def generate_character(
world: World, world: World,
systems: List[GameSystem], systems: List[GameSystem],
dest_room: Room, dest_room: Room,
additional_prompt: str = "",
detail_prompt: str = "",
) -> Character: ) -> Character:
existing_characters = [character.name for character in list_characters(world)] + [ existing_characters = [character.name for character in list_characters(world)] + [
character.name for character in list_characters_in_room(dest_room) character.name for character in list_characters_in_room(dest_room)
@ -291,13 +293,14 @@ def generate_character(
name = loop_retry( name = loop_retry(
agent, agent,
"Generate one person or creature that would make sense in the world of {world_theme}. " "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. " "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. " "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 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 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}", "Do not create any duplicate characters. The existing characters are: {existing_characters}",
context={ context={
"additional_prompt": additional_prompt,
"dest_room": dest_room.name, "dest_room": dest_room.name,
"existing_characters": existing_characters, "existing_characters": existing_characters,
"world_theme": world.theme, "world_theme": world.theme,
@ -308,14 +311,18 @@ def generate_character(
broadcast_generated(message=f"Generating character: {name}") broadcast_generated(message=f"Generating character: {name}")
description = agent( 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." "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.", "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, name=name,
) )
backstory = agent( 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}.', '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, name=name,
) )
@ -374,6 +381,31 @@ def generate_effect(agent: Agent, world: World, entity: Item) -> EffectPattern:
name=name, 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( attribute_names = agent(
"Generate a short list of attributes that the {name} effect modifies. Include 1 to 3 attributes. " "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. " "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, name,
description, description,
application, application,
duration=duration,
attributes=attributes, attributes=attributes,
cooldown=cooldown,
duration=duration,
uses=uses,
) )

View File

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

View File

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

View File

@ -35,6 +35,18 @@ def prepend_value(value: AttributeValue, prefix: str) -> str:
return prefix + str(value) 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: def add_attribute(attributes: Attributes, name: str, value: int | float) -> Attributes:
""" """
Add an attribute to a set of 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.entity import Character, Room
from adventure.models.event import ReplyEvent from adventure.models.event import ReplyEvent
from .string import normalize_name from .string import and_list, normalize_name
logger = getLogger(__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 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: def summarize_room(room: Room, player: Character) -> str:
""" """
Summarize a room for the player. Summarize a room for the player.

View File

@ -1,4 +1,5 @@
from functools import lru_cache from functools import lru_cache
from typing import List
@lru_cache(maxsize=1024) @lru_cache(maxsize=1024)
@ -6,3 +7,29 @@ def normalize_name(name: str) -> str:
name = name.lower().strip() name = name.lower().strip()
name = name.strip('"').strip("'") name = name.strip('"').strip("'")
return name.removesuffix(".") 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 { event, renderEntity } = props;
const { entity, name } = event; const { entity, name } = event;
let description = name;
let renderButton; let renderButton;
if (doesExist(entity)) { 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)}> renderButton = <IconButton edge="end" aria-label="render" onClick={() => renderEntity(entity.type, entity.name)}>
<Camera /> <Camera />
</IconButton>; </IconButton>;
@ -277,7 +279,7 @@ export function GenerateEventItem(props: EventItemProps) {
variant="body2" variant="body2"
color="text.primary" color="text.primary"
> >
{name} {description}
</Typography> </Typography>
} }
/> />