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."
|
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:
|
||||||
|
|
|
@ -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.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
with action_context() as (_, action_character):
|
|
||||||
# TODO: check for existing events with the same name
|
# 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):
|
||||||
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(
|
||||||
[
|
[
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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=[
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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]}"
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
Loading…
Reference in New Issue