support for temporary and immediate/permanent effects
This commit is contained in:
parent
0859bca2fd
commit
2aaf531454
|
@ -14,8 +14,9 @@ from adventure.context import (
|
|||
world_context,
|
||||
)
|
||||
from adventure.generate import generate_item, generate_room, link_rooms
|
||||
from adventure.utils.effect import apply_effect
|
||||
from adventure.utils.effect import apply_effects
|
||||
from adventure.utils.search import find_actor_in_room
|
||||
from adventure.utils.string import normalize_name
|
||||
from adventure.utils.world import describe_actor, describe_entity
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
@ -134,13 +135,13 @@ def action_use(item: str, target: str) -> str:
|
|||
"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_name = normalize_name(chosen_name)
|
||||
|
||||
chosen_effect = next(
|
||||
(
|
||||
search_effect
|
||||
for search_effect in action_item.effects
|
||||
if search_effect.name == chosen_name
|
||||
if normalize_name(search_effect.name) == chosen_name
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
@ -148,7 +149,11 @@ def action_use(item: str, target: str) -> str:
|
|||
# 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)
|
||||
try:
|
||||
apply_effects(target_actor, [chosen_effect])
|
||||
except Exception:
|
||||
logger.exception("error applying effect: %s", chosen_effect)
|
||||
return f"There was a problem applying the {chosen_name} effect."
|
||||
|
||||
broadcast(
|
||||
f"{action_actor.name} uses the {chosen_name} effect of {item} on {target}"
|
||||
|
|
|
@ -4,22 +4,19 @@ from typing import List, Tuple
|
|||
|
||||
from packit.agent import Agent
|
||||
from packit.loops import loop_retry
|
||||
from packit.results import enum_result, int_result
|
||||
from packit.utils import could_be_json
|
||||
|
||||
from adventure.context import broadcast, set_current_world
|
||||
from adventure.game_system import GameSystem
|
||||
from adventure.models.config import DEFAULT_CONFIG, WorldConfig
|
||||
from adventure.models.entity import (
|
||||
Actor,
|
||||
Effect,
|
||||
Item,
|
||||
NumberAttributeEffect,
|
||||
Portal,
|
||||
Room,
|
||||
StringAttributeEffect,
|
||||
World,
|
||||
WorldEntity,
|
||||
from adventure.models.effect import (
|
||||
EffectPattern,
|
||||
FloatEffectPattern,
|
||||
IntEffectPattern,
|
||||
StringEffectPattern,
|
||||
)
|
||||
from adventure.models.entity import Actor, Item, Portal, Room, World, WorldEntity
|
||||
from adventure.models.event import GenerateEvent
|
||||
from adventure.utils import try_parse_float, try_parse_int
|
||||
from adventure.utils.search import (
|
||||
|
@ -36,24 +33,6 @@ logger = getLogger(__name__)
|
|||
|
||||
world_config: WorldConfig = DEFAULT_CONFIG.world
|
||||
|
||||
PROMPT_TYPE_FRAGMENTS = {
|
||||
"both": "Enter a positive or negative number, or a string value",
|
||||
"number": "Enter a positive or negative number",
|
||||
"string": "Enter a string value",
|
||||
}
|
||||
|
||||
PROMPT_OPERATION_TYPES = {
|
||||
"set": "both",
|
||||
"add": "number",
|
||||
"subtract": "number",
|
||||
"multiply": "number",
|
||||
"divide": "number",
|
||||
"append": "string",
|
||||
"prepend": "string",
|
||||
}
|
||||
|
||||
OPERATIONS = list(PROMPT_OPERATION_TYPES.keys())
|
||||
|
||||
|
||||
def duplicate_name_parser(existing_names: List[str]):
|
||||
def name_parser(value: str, **kwargs):
|
||||
|
@ -369,7 +348,7 @@ def generate_actor(
|
|||
return actor
|
||||
|
||||
|
||||
def generate_effect(agent: Agent, world: World, entity: Item) -> Effect:
|
||||
def generate_effect(agent: Agent, world: World, entity: Item) -> EffectPattern:
|
||||
entity_type = entity.type
|
||||
existing_effects = [effect.name for effect in entity.effects]
|
||||
|
||||
|
@ -405,65 +384,78 @@ def generate_effect(agent: Agent, world: World, entity: Item) -> Effect:
|
|||
name=name,
|
||||
)
|
||||
|
||||
def operation_parser(value: str, **kwargs):
|
||||
if value not in OPERATIONS:
|
||||
raise ValueError(
|
||||
f'"{value}" is not a valid operation. Choose from: {OPERATIONS}'
|
||||
)
|
||||
|
||||
return value
|
||||
|
||||
attributes = []
|
||||
for attribute_name in attribute_names.split(","):
|
||||
attribute_name = normalize_name(attribute_name)
|
||||
if attribute_name:
|
||||
operation = loop_retry(
|
||||
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."
|
||||
"Reply with the operation only, without any other text. Respond with a single word for the list of operations."
|
||||
"Choose from the following operations: {operations}",
|
||||
context={
|
||||
"name": name,
|
||||
"attribute_name": attribute_name,
|
||||
"operations": OPERATIONS,
|
||||
},
|
||||
result_parser=operation_parser,
|
||||
toolbox=None,
|
||||
)
|
||||
|
||||
operation_type = PROMPT_OPERATION_TYPES[operation]
|
||||
operation_prompt = PROMPT_TYPE_FRAGMENTS[operation_type]
|
||||
|
||||
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."
|
||||
f"{operation_prompt}. Do not include any other text. Do not use JSON.",
|
||||
"For example, heal might add 10 to the health attribute, while poison might remove -5 from it."
|
||||
"Enter a positive number to increase the attribute or a negative number to decrease it. "
|
||||
"Do not include any other text. Do not use JSON.",
|
||||
name=name,
|
||||
attribute_name=attribute_name,
|
||||
)
|
||||
value = value.strip()
|
||||
|
||||
# TODO: support more than just set: offset and multiply
|
||||
int_value = try_parse_int(value)
|
||||
if int_value is not None:
|
||||
attribute_effect = NumberAttributeEffect(
|
||||
name=attribute_name, operation=operation, value=int_value
|
||||
)
|
||||
attribute_effect = IntEffectPattern(name=attribute_name, set=int_value)
|
||||
else:
|
||||
float_value = try_parse_float(value)
|
||||
if float_value is not None:
|
||||
attribute_effect = NumberAttributeEffect(
|
||||
name=attribute_name, operation=operation, value=float_value
|
||||
attribute_effect = FloatEffectPattern(
|
||||
name=attribute_name, set=float_value
|
||||
)
|
||||
else:
|
||||
attribute_effect = StringAttributeEffect(
|
||||
name=attribute_name, operation=operation, value=value
|
||||
attribute_effect = StringEffectPattern(
|
||||
name=attribute_name, set=value
|
||||
)
|
||||
|
||||
attributes.append(attribute_effect)
|
||||
|
||||
return Effect(name=name, description=description, attributes=attributes)
|
||||
duration = loop_retry(
|
||||
agent,
|
||||
f"How many turns does the {name} effect last? Enter a positive number to set a duration, or 0 for an instant effect. "
|
||||
"Do not include any other text. Do not use JSON.",
|
||||
context={
|
||||
"name": name,
|
||||
},
|
||||
result_parser=int_result,
|
||||
)
|
||||
|
||||
def parse_application(value: str, **kwargs) -> str:
|
||||
value = enum_result(value, ["temporary", "permanent"])
|
||||
if value:
|
||||
return value
|
||||
|
||||
raise ValueError("The application must be 'temporary' or 'permanent'.")
|
||||
|
||||
application = loop_retry(
|
||||
agent,
|
||||
(
|
||||
f"How should the {name} effect be applied? Respond with 'temporary' for a temporary effect that lasts for a duration, "
|
||||
"or 'permanent' for a permanent effect that immediately modifies the target. "
|
||||
"For example, a healing potion would be a permanent effect that increases health every turn, "
|
||||
"while bleeding would be a temporary effect that decreases health every turn. "
|
||||
"A haste potion would be a temporary effect that increases speed for a duration, "
|
||||
"while a slow spell would be a temporary effect that decreases speed for a duration. "
|
||||
"Do not include any other text. Do not use JSON."
|
||||
),
|
||||
context={
|
||||
"name": name,
|
||||
},
|
||||
result_parser=parse_application,
|
||||
)
|
||||
|
||||
return EffectPattern(
|
||||
name,
|
||||
description,
|
||||
application,
|
||||
duration=duration,
|
||||
attributes=attributes,
|
||||
)
|
||||
|
||||
|
||||
def link_rooms(
|
||||
|
|
|
@ -173,7 +173,10 @@ def load_logic(filename: str):
|
|||
for rule in logic_rules.rules:
|
||||
if rule.trigger:
|
||||
for trigger in rule.trigger:
|
||||
logic_triggers[trigger] = get_plugin_function(trigger)
|
||||
function_name = (
|
||||
trigger if isinstance(trigger, str) else trigger.function
|
||||
)
|
||||
logic_triggers[trigger] = get_plugin_function(function_name)
|
||||
|
||||
logger.info("initialized logic system")
|
||||
system_simulate = wraps(update_logic)(
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Dict
|
||||
from uuid import uuid4
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -7,6 +7,10 @@ else:
|
|||
from pydantic.dataclasses import dataclass as dataclass # noqa
|
||||
|
||||
|
||||
AttributeValue = bool | float | int | str
|
||||
Attributes = Dict[str, AttributeValue]
|
||||
|
||||
|
||||
class BaseModel:
|
||||
type: str
|
||||
id: str
|
||||
|
@ -14,3 +18,17 @@ class BaseModel:
|
|||
|
||||
def uuid() -> str:
|
||||
return uuid4().hex
|
||||
|
||||
|
||||
@dataclass
|
||||
class FloatRange:
|
||||
min: float
|
||||
max: float
|
||||
interval: float = 1.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class IntRange:
|
||||
min: int
|
||||
max: int
|
||||
interval: int = 1
|
||||
|
|
|
@ -1,13 +1,6 @@
|
|||
from typing import Dict, List
|
||||
|
||||
from .base import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Range:
|
||||
min: int
|
||||
max: int
|
||||
interval: int = 1
|
||||
from .base import IntRange, dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -29,11 +22,11 @@ class BotConfig:
|
|||
|
||||
@dataclass
|
||||
class RenderConfig:
|
||||
cfg: Range
|
||||
cfg: IntRange
|
||||
checkpoints: List[str]
|
||||
path: str
|
||||
sizes: Dict[str, Size]
|
||||
steps: Range
|
||||
steps: IntRange
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -49,12 +42,12 @@ class ServerConfig:
|
|||
|
||||
@dataclass
|
||||
class WorldSizeConfig:
|
||||
actor_items: Range
|
||||
item_effects: Range
|
||||
portals: Range
|
||||
room_actors: Range
|
||||
room_items: Range
|
||||
rooms: Range
|
||||
actor_items: IntRange
|
||||
item_effects: IntRange
|
||||
portals: IntRange
|
||||
room_actors: IntRange
|
||||
room_items: IntRange
|
||||
rooms: IntRange
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -73,7 +66,7 @@ class Config:
|
|||
DEFAULT_CONFIG = Config(
|
||||
bot=BotConfig(discord=DiscordBotConfig(channels=["adventure"])),
|
||||
render=RenderConfig(
|
||||
cfg=Range(min=5, max=8),
|
||||
cfg=IntRange(min=5, max=8),
|
||||
checkpoints=[
|
||||
"diffusion-sdxl-dynavision-0-5-5-7.safetensors",
|
||||
],
|
||||
|
@ -83,17 +76,17 @@ DEFAULT_CONFIG = Config(
|
|||
"portrait": Size(width=768, height=1024),
|
||||
"square": Size(width=768, height=768),
|
||||
},
|
||||
steps=Range(min=30, max=30),
|
||||
steps=IntRange(min=30, max=30),
|
||||
),
|
||||
server=ServerConfig(websocket=WebsocketServerConfig(host="localhost", port=8001)),
|
||||
world=WorldConfig(
|
||||
size=WorldSizeConfig(
|
||||
actor_items=Range(min=0, max=2),
|
||||
item_effects=Range(min=1, max=2),
|
||||
portals=Range(min=1, max=3),
|
||||
rooms=Range(min=3, max=6),
|
||||
room_actors=Range(min=1, max=3),
|
||||
room_items=Range(min=1, max=3),
|
||||
actor_items=IntRange(min=0, max=2),
|
||||
item_effects=IntRange(min=1, max=2),
|
||||
portals=IntRange(min=1, max=3),
|
||||
rooms=IntRange(min=3, max=6),
|
||||
room_actors=IntRange(min=1, max=3),
|
||||
room_items=IntRange(min=1, max=3),
|
||||
)
|
||||
),
|
||||
)
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
from typing import List, Literal
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from .base import FloatRange, IntRange, dataclass, uuid
|
||||
|
||||
|
||||
@dataclass
|
||||
class StringEffectPattern:
|
||||
name: str
|
||||
append: str | List[str] | None = None
|
||||
prepend: str | List[str] | None = None
|
||||
set: str | List[str] | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class FloatEffectPattern:
|
||||
name: str
|
||||
set: float | FloatRange | None = None
|
||||
offset: float | FloatRange | None = None
|
||||
multiply: float | FloatRange | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class IntEffectPattern:
|
||||
name: str
|
||||
set: int | IntRange | None = None
|
||||
offset: int | IntRange | None = None
|
||||
multiply: float | FloatRange | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class BooleanEffectPattern:
|
||||
name: str
|
||||
set: bool | None = None
|
||||
toggle: bool | None = None
|
||||
|
||||
|
||||
AttributeEffectPattern = (
|
||||
StringEffectPattern | FloatEffectPattern | IntEffectPattern | BooleanEffectPattern
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class EffectPattern:
|
||||
"""
|
||||
TODO: should this be an EffectTemplate?
|
||||
"""
|
||||
|
||||
name: str
|
||||
description: str
|
||||
application: Literal["permanent", "temporary"]
|
||||
duration: int | IntRange | None = None
|
||||
attributes: List[AttributeEffectPattern] = Field(default_factory=list)
|
||||
id: str = Field(default_factory=uuid)
|
||||
type: Literal["effect_pattern"] = "effect_pattern"
|
||||
|
||||
|
||||
@dataclass
|
||||
class BooleanEffectResult:
|
||||
name: str
|
||||
set: bool | None = None
|
||||
toggle: bool | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class FloatEffectResult:
|
||||
name: str
|
||||
set: float | None = None
|
||||
offset: float | None = None
|
||||
multiply: float | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class IntEffectResult:
|
||||
name: str
|
||||
set: int | None = None
|
||||
offset: int | None = None
|
||||
multiply: float | None = None # still needs to be a float for decimals/division
|
||||
|
||||
|
||||
@dataclass
|
||||
class StringEffectResult:
|
||||
name: str
|
||||
append: str | None = None
|
||||
prepend: str | None = None
|
||||
set: str | None = None
|
||||
|
||||
|
||||
AttributeEffectResult = (
|
||||
BooleanEffectResult | FloatEffectResult | IntEffectResult | StringEffectResult
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class EffectResult:
|
||||
name: str
|
||||
description: str
|
||||
duration: int | None = None
|
||||
attributes: List[AttributeEffectResult] = Field(default_factory=list)
|
||||
id: str = Field(default_factory=uuid)
|
||||
type: Literal["effect_result"] = "effect_result"
|
|
@ -2,45 +2,10 @@ from typing import Callable, Dict, List, Literal
|
|||
|
||||
from pydantic import Field
|
||||
|
||||
from .base import BaseModel, dataclass, uuid
|
||||
from .base import Attributes, BaseModel, dataclass, uuid
|
||||
from .effect import EffectPattern, EffectResult
|
||||
|
||||
Actions = Dict[str, Callable]
|
||||
AttributeValue = bool | float | int | str
|
||||
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
|
||||
|
@ -48,8 +13,9 @@ class Item(BaseModel):
|
|||
name: str
|
||||
description: str
|
||||
actions: Actions = Field(default_factory=dict)
|
||||
active_effects: List[EffectResult] = Field(default_factory=list)
|
||||
attributes: Attributes = Field(default_factory=dict)
|
||||
effects: List[Effect] = Field(default_factory=list)
|
||||
effects: List[EffectPattern] = Field(default_factory=list)
|
||||
items: List["Item"] = Field(default_factory=list)
|
||||
id: str = Field(default_factory=uuid)
|
||||
type: Literal["item"] = "item"
|
||||
|
@ -61,6 +27,7 @@ class Actor(BaseModel):
|
|||
backstory: str
|
||||
description: str
|
||||
actions: Actions = Field(default_factory=dict)
|
||||
active_effects: List[EffectResult] = Field(default_factory=list)
|
||||
attributes: Attributes = Field(default_factory=dict)
|
||||
items: List[Item] = Field(default_factory=list)
|
||||
id: str = Field(default_factory=uuid)
|
||||
|
@ -84,6 +51,7 @@ class Room(BaseModel):
|
|||
description: str
|
||||
actors: List[Actor] = Field(default_factory=list)
|
||||
actions: Actions = Field(default_factory=dict)
|
||||
active_effects: List[EffectResult] = Field(default_factory=list)
|
||||
attributes: Attributes = Field(default_factory=dict)
|
||||
items: List[Item] = Field(default_factory=list)
|
||||
portals: List[Portal] = Field(default_factory=list)
|
||||
|
|
|
@ -117,7 +117,7 @@ def scene_from_event(event: GameEvent) -> str | None:
|
|||
def scene_from_entity(entity: WorldEntity) -> str:
|
||||
logger.debug("generating scene from entity: %s", entity)
|
||||
|
||||
return f"Describe the {entity.type} called {entity.name}. {describe_entity(entity)}"
|
||||
return f"Describe the {entity.type} named {entity.name} in vivid, visual terms. {describe_entity(entity)}"
|
||||
|
||||
|
||||
def make_example_prompts(keywords: List[str], k=5, q=10) -> List[str]:
|
||||
|
@ -162,6 +162,7 @@ def generate_prompt_from_scene(scene: str, example_prompts: List[str]) -> str:
|
|||
"Reply with a comma-separated list of keywords that summarize the visual details of the scene."
|
||||
"Make sure you describe the location, all of the characters, and any items present using keywords and phrases. "
|
||||
"Be creative with the details. Avoid using proper nouns or character names. Describe any actions being taken. "
|
||||
"Describe the characters first, then the location, then the other visual details and general atmosphere. "
|
||||
"Do not include the question or any JSON. Only include the list of keywords on a single line.",
|
||||
examples=example_prompts,
|
||||
scene=scene,
|
||||
|
|
|
@ -31,6 +31,7 @@ from adventure.context import (
|
|||
from adventure.game_system import GameSystem
|
||||
from adventure.models.entity import World
|
||||
from adventure.models.event import ActionEvent, ReplyEvent, ResultEvent
|
||||
from adventure.utils.effect import is_active_effect
|
||||
from adventure.utils.search import find_room_with_actor
|
||||
from adventure.utils.world import describe_entity, format_attributes
|
||||
|
||||
|
@ -97,6 +98,16 @@ def simulate_world(
|
|||
logger.error(f"Actor {actor_name} is not in a room")
|
||||
continue
|
||||
|
||||
# decrement effects on the actor and remove any that have expired
|
||||
for effect in actor.active_effects:
|
||||
if effect.duration is not None:
|
||||
effect.duration -= 1
|
||||
|
||||
actor.active_effects[:] = [
|
||||
effect for effect in actor.active_effects if is_active_effect(effect)
|
||||
]
|
||||
|
||||
# collect data for the prompt
|
||||
room_actors = [actor.name for actor in room.actors]
|
||||
room_items = [item.name for item in room.items]
|
||||
room_directions = [portal.name for portal in room.portals]
|
||||
|
@ -104,6 +115,7 @@ def simulate_world(
|
|||
actor_attributes = format_attributes(actor)
|
||||
actor_items = [item.name for item in actor.items]
|
||||
|
||||
# set up a result parser for the agent
|
||||
def result_parser(value, agent, **kwargs):
|
||||
if not room or not actor:
|
||||
raise ValueError(
|
||||
|
@ -119,6 +131,7 @@ def simulate_world(
|
|||
|
||||
return world_result_parser(value, agent, **kwargs)
|
||||
|
||||
# prompt and act
|
||||
logger.info("starting turn for actor: %s", actor_name)
|
||||
result = loop_retry(
|
||||
agent,
|
||||
|
|
|
@ -22,14 +22,14 @@ rules:
|
|||
type: room
|
||||
temperature: hot
|
||||
chance: 0.2
|
||||
trigger: [adventure.sim_systems.environment_triggers:hot_room]
|
||||
trigger: [adventure.systems.sim.environment_triggers:hot_room]
|
||||
|
||||
- group: environment-temperature
|
||||
match:
|
||||
type: room
|
||||
temperature: cold
|
||||
chance: 0.2
|
||||
trigger: [adventure.sim_systems.environment_triggers:cold_room]
|
||||
trigger: [adventure.systems.sim.environment_triggers:cold_room]
|
||||
|
||||
labels:
|
||||
- match:
|
||||
|
|
|
@ -1,4 +1,38 @@
|
|||
from adventure.models.entity import Attributes, AttributeValue
|
||||
from adventure.models.base import Attributes, AttributeValue
|
||||
|
||||
|
||||
def add_value(value: AttributeValue, offset: int | float) -> AttributeValue:
|
||||
"""
|
||||
Add an offset to a value.
|
||||
"""
|
||||
if isinstance(value, str):
|
||||
raise ValueError(f"Cannot add a number to a string attribute: {value}")
|
||||
|
||||
return value + offset
|
||||
|
||||
|
||||
def multiply_value(value: AttributeValue, factor: int | float) -> AttributeValue:
|
||||
"""
|
||||
Multiply a value by a factor.
|
||||
"""
|
||||
if isinstance(value, str):
|
||||
raise ValueError(f"Cannot multiply a string attribute: {value}")
|
||||
|
||||
return value * factor
|
||||
|
||||
|
||||
def append_value(value: AttributeValue, suffix: str) -> str:
|
||||
"""
|
||||
Append a suffix to a string.
|
||||
"""
|
||||
return str(value) + suffix
|
||||
|
||||
|
||||
def prepend_value(value: AttributeValue, prefix: str) -> str:
|
||||
"""
|
||||
Prepend a prefix to a string.
|
||||
"""
|
||||
return prefix + str(value)
|
||||
|
||||
|
||||
def add_attribute(attributes: Attributes, name: str, value: int | float) -> Attributes:
|
||||
|
@ -7,10 +41,7 @@ def add_attribute(attributes: Attributes, name: str, value: int | float) -> Attr
|
|||
"""
|
||||
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
|
||||
attributes[name] = add_value(previous_value, value)
|
||||
else:
|
||||
attributes[name] = value
|
||||
|
||||
|
|
|
@ -1,47 +1,314 @@
|
|||
import random
|
||||
from logging import getLogger
|
||||
from typing import List
|
||||
|
||||
from adventure.models.entity import Attributes, Effect
|
||||
from adventure.models.base import FloatRange, IntRange
|
||||
from adventure.models.effect import (
|
||||
BooleanEffectPattern,
|
||||
BooleanEffectResult,
|
||||
EffectPattern,
|
||||
EffectResult,
|
||||
FloatEffectPattern,
|
||||
FloatEffectResult,
|
||||
IntEffectPattern,
|
||||
IntEffectResult,
|
||||
StringEffectPattern,
|
||||
StringEffectResult,
|
||||
)
|
||||
from adventure.models.entity import Actor, Attributes
|
||||
from adventure.utils.attribute import (
|
||||
add_attribute,
|
||||
append_attribute,
|
||||
divide_attribute,
|
||||
multiply_attribute,
|
||||
prepend_attribute,
|
||||
set_attribute,
|
||||
subtract_attribute,
|
||||
add_value,
|
||||
append_value,
|
||||
multiply_value,
|
||||
prepend_value,
|
||||
)
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
def apply_effect(effect: Effect, attributes: Attributes) -> Attributes:
|
||||
|
||||
def effective_boolean(attributes: Attributes, effect: BooleanEffectResult) -> bool:
|
||||
"""
|
||||
Apply a boolean effect to a value.
|
||||
"""
|
||||
|
||||
if effect.set is not None:
|
||||
return effect.set
|
||||
|
||||
value = attributes.get(effect.name, False)
|
||||
|
||||
if effect.toggle is not None:
|
||||
return not value
|
||||
|
||||
return bool(value)
|
||||
|
||||
|
||||
def effective_float(attributes: Attributes, effect: FloatEffectResult) -> float:
|
||||
"""
|
||||
Apply a float effect to a value.
|
||||
"""
|
||||
|
||||
if effect.set is not None:
|
||||
return effect.set
|
||||
|
||||
value = attributes.get(effect.name, 0.0)
|
||||
|
||||
if effect.offset is not None:
|
||||
value = add_value(value, effect.offset)
|
||||
|
||||
if effect.multiply is not None:
|
||||
value = multiply_value(value, effect.multiply)
|
||||
|
||||
return float(value)
|
||||
|
||||
|
||||
def effective_int(attributes: Attributes, effect: IntEffectResult) -> int:
|
||||
"""
|
||||
Apply an integer effect to a value.
|
||||
"""
|
||||
|
||||
if effect.set is not None:
|
||||
return effect.set
|
||||
|
||||
value = attributes.get(effect.name, 0)
|
||||
|
||||
if effect.offset is not None:
|
||||
value = add_value(value, effect.offset)
|
||||
|
||||
if effect.multiply is not None:
|
||||
value = multiply_value(value, effect.multiply)
|
||||
|
||||
return int(value)
|
||||
|
||||
|
||||
def effective_string(attributes: Attributes, effect: StringEffectResult) -> str:
|
||||
"""
|
||||
Apply a string effect to a value.
|
||||
"""
|
||||
|
||||
if effect.set:
|
||||
return effect.set
|
||||
|
||||
value = attributes.get(effect.name, "")
|
||||
|
||||
if effect.append:
|
||||
value = append_value(value, effect.append)
|
||||
|
||||
if effect.prepend:
|
||||
value = prepend_value(value, effect.prepend)
|
||||
|
||||
return str(value)
|
||||
|
||||
|
||||
def effective_attributes(
|
||||
effects: List[EffectResult], base_attributes: Attributes
|
||||
) -> Attributes:
|
||||
"""
|
||||
Apply an effect to a set of attributes.
|
||||
"""
|
||||
|
||||
attributes = base_attributes.copy()
|
||||
|
||||
for effect in effects:
|
||||
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)
|
||||
if isinstance(attribute, BooleanEffectResult):
|
||||
attributes[attribute.name] = effective_boolean(attributes, attribute)
|
||||
elif isinstance(attribute, FloatEffectResult):
|
||||
attributes[attribute.name] = effective_float(attributes, attribute)
|
||||
elif isinstance(attribute, IntEffectResult):
|
||||
attributes[attribute.name] = effective_int(attributes, attribute)
|
||||
elif isinstance(attribute, StringEffectResult):
|
||||
attributes[attribute.name] = effective_string(attributes, attribute)
|
||||
else:
|
||||
raise ValueError(f"Invalid operation: {attribute.operation}")
|
||||
|
||||
return attributes
|
||||
|
||||
|
||||
def apply_effects(effects: List[Effect], attributes: Attributes) -> Attributes:
|
||||
def resolve_float_range(range: float | FloatRange | None) -> float | None:
|
||||
"""
|
||||
Apply a list of effects to a set of attributes.
|
||||
Resolve a float range to a single value.
|
||||
"""
|
||||
|
||||
if range is None:
|
||||
return None
|
||||
|
||||
if isinstance(
|
||||
range, (float, int)
|
||||
): # int is not really necessary here, but mypy complains without it
|
||||
return range
|
||||
|
||||
return random.uniform(range.min, range.max)
|
||||
|
||||
|
||||
def resolve_int_range(range: int | IntRange | None) -> int | None:
|
||||
"""
|
||||
Resolve an integer range to a single value.
|
||||
"""
|
||||
|
||||
if range is None:
|
||||
return None
|
||||
|
||||
if isinstance(range, int):
|
||||
return range
|
||||
|
||||
return random.randint(range.min, range.max)
|
||||
|
||||
|
||||
def resolve_string_list(result: str | List[str] | None) -> str | None:
|
||||
"""
|
||||
Resolve a string result to a single value.
|
||||
"""
|
||||
|
||||
if result is None:
|
||||
return None
|
||||
|
||||
if isinstance(result, str):
|
||||
return result
|
||||
|
||||
return random.choice(result)
|
||||
|
||||
|
||||
def resolve_boolean_effect(effect: BooleanEffectPattern) -> BooleanEffectResult:
|
||||
"""
|
||||
Apply a boolean effect pattern to a set of attributes.
|
||||
"""
|
||||
|
||||
return BooleanEffectResult(
|
||||
name=effect.name,
|
||||
set=effect.set,
|
||||
toggle=effect.toggle,
|
||||
)
|
||||
|
||||
|
||||
def resolve_float_effect(effect: FloatEffectPattern) -> FloatEffectResult:
|
||||
"""
|
||||
Apply a float effect pattern to a set of attributes.
|
||||
"""
|
||||
|
||||
return FloatEffectResult(
|
||||
name=effect.name,
|
||||
set=resolve_float_range(effect.set),
|
||||
offset=resolve_float_range(effect.offset),
|
||||
multiply=resolve_float_range(effect.multiply),
|
||||
)
|
||||
|
||||
|
||||
def resolve_int_effect(effect: IntEffectPattern) -> IntEffectResult:
|
||||
"""
|
||||
Apply an integer effect pattern to a set of attributes.
|
||||
"""
|
||||
|
||||
return IntEffectResult(
|
||||
name=effect.name,
|
||||
set=resolve_int_range(effect.set),
|
||||
offset=resolve_int_range(effect.offset),
|
||||
multiply=resolve_float_range(effect.multiply),
|
||||
)
|
||||
|
||||
|
||||
def resolve_string_effect(effect: StringEffectPattern) -> StringEffectResult:
|
||||
"""
|
||||
Apply a string effect pattern to a set of attributes.
|
||||
"""
|
||||
|
||||
return StringEffectResult(
|
||||
name=effect.name,
|
||||
set=resolve_string_list(effect.set),
|
||||
append=resolve_string_list(effect.append),
|
||||
prepend=resolve_string_list(effect.prepend),
|
||||
)
|
||||
|
||||
|
||||
def resolve_effects(effects: List[EffectPattern]) -> List[EffectResult]:
|
||||
"""
|
||||
Generate results for a set of effect patterns, rolling all of the random values.
|
||||
"""
|
||||
|
||||
results = []
|
||||
|
||||
for effect in effects:
|
||||
attributes = apply_effect(effect, attributes)
|
||||
attributes = []
|
||||
for attribute in effect.attributes:
|
||||
if isinstance(attribute, BooleanEffectPattern):
|
||||
result = resolve_boolean_effect(attribute)
|
||||
elif isinstance(attribute, FloatEffectPattern):
|
||||
result = resolve_float_effect(attribute)
|
||||
elif isinstance(attribute, IntEffectPattern):
|
||||
result = resolve_int_effect(attribute)
|
||||
elif isinstance(attribute, StringEffectPattern):
|
||||
result = resolve_string_effect(attribute)
|
||||
else:
|
||||
raise ValueError(f"Invalid operation: {attribute.operation}")
|
||||
attributes.append(result)
|
||||
|
||||
duration = resolve_int_range(effect.duration)
|
||||
|
||||
result = EffectResult(
|
||||
name=effect.name,
|
||||
description=effect.description,
|
||||
duration=duration,
|
||||
attributes=attributes,
|
||||
)
|
||||
results.append(result)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def is_active_effect(effect: EffectResult) -> bool:
|
||||
"""
|
||||
Determine if an effect is active.
|
||||
"""
|
||||
|
||||
return effect.duration is None or effect.duration > 0
|
||||
|
||||
|
||||
def apply_permanent_results(
|
||||
attributes: Attributes, effects: List[EffectResult]
|
||||
) -> Attributes:
|
||||
"""
|
||||
Permanently apply a set of effects to a set of attributes.
|
||||
"""
|
||||
|
||||
for effect in effects:
|
||||
for attribute in effect.attributes:
|
||||
if isinstance(attribute, BooleanEffectResult):
|
||||
attributes[attribute.name] = effective_boolean(attributes, attribute)
|
||||
elif isinstance(attribute, FloatEffectResult):
|
||||
attributes[attribute.name] = effective_float(attributes, attribute)
|
||||
elif isinstance(attribute, IntEffectResult):
|
||||
attributes[attribute.name] = effective_int(attributes, attribute)
|
||||
elif isinstance(attribute, StringEffectResult):
|
||||
attributes[attribute.name] = effective_string(attributes, attribute)
|
||||
else:
|
||||
raise ValueError(f"Invalid operation: {attribute.operation}")
|
||||
|
||||
return attributes
|
||||
|
||||
|
||||
def apply_permanent_effects(
|
||||
attributes: Attributes, effects: List[EffectPattern]
|
||||
) -> Attributes:
|
||||
"""
|
||||
Permanently apply a set of effects to a set of attributes.
|
||||
"""
|
||||
|
||||
results = resolve_effects(effects)
|
||||
return apply_permanent_results(attributes, results)
|
||||
|
||||
|
||||
def apply_effects(target: Actor, effects: List[EffectPattern]) -> None:
|
||||
"""
|
||||
Apply a set of effects to a set of attributes.
|
||||
"""
|
||||
|
||||
permanent_effects = [
|
||||
effect for effect in effects if effect.application == "permanent"
|
||||
]
|
||||
permanent_effects = resolve_effects(permanent_effects)
|
||||
target.attributes = apply_permanent_results(target.attributes, permanent_effects)
|
||||
|
||||
temporary_effects = [
|
||||
effect for effect in effects if effect.application == "temporary"
|
||||
]
|
||||
temporary_effects = resolve_effects(temporary_effects)
|
||||
target.active_effects.extend(temporary_effects)
|
||||
|
|
Loading…
Reference in New Issue