Files
cultivation-world-simulator/src/classes/avatar/action_mixin.py
4thfever 84027fc1d7 Feat/retreat (#104)
* feat: add retreat
2026-01-26 23:18:11 +08:00

229 lines
9.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Avatar 动作管理 Mixin
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Optional, List
if TYPE_CHECKING:
from src.classes.avatar.core import Avatar
from src.classes.action import Action
from src.classes.action_runtime import ActionStatus, ActionResult, ActionPlan, ActionInstance
from src.classes.action.registry import ActionRegistry
from src.classes.event import Event
from src.classes.typings import ACTION_NAME, ACTION_NAME_PARAMS_PAIRS
from src.utils.params import filter_kwargs_for_callable
from src.run.log import get_logger
class ActionMixin:
"""动作管理相关方法"""
def create_action(self: "Avatar", action_name: ACTION_NAME) -> Action:
"""
根据动作名称创建新的action实例
Args:
action_name: 动作类的名称(如 'Cultivate', 'Breakthrough' 等)
Returns:
新创建的Action实例
Raises:
ValueError: 如果找不到对应的动作类
"""
action_cls = ActionRegistry.get(action_name)
return action_cls(self, self.world)
def load_decide_result_chain(
self: "Avatar",
action_name_params_pairs: ACTION_NAME_PARAMS_PAIRS,
avatar_thinking: str,
short_term_objective: str,
prepend: bool = False
):
"""
加载AI的决策结果动作链立即设置第一个为当前动作其余进入队列。
Args:
action_name_params_pairs: 动作名和参数对列表
avatar_thinking: 思考内容
short_term_objective: 短期目标
prepend: 是否插队到最前面默认False即追加到末尾
"""
if not action_name_params_pairs:
return
self.thinking = avatar_thinking
self.short_term_objective = short_term_objective
# 转为计划并入队(不立即提交,交由提交阶段统一触发开始事件)
plans: List[ActionPlan] = [ActionPlan(name, params) for name, params in action_name_params_pairs]
if prepend:
self.planned_actions[0:0] = plans
else:
self.planned_actions.extend(plans)
def clear_plans(self: "Avatar") -> None:
self.planned_actions.clear()
def has_plans(self: "Avatar") -> bool:
return len(self.planned_actions) > 0
def commit_next_plan(self: "Avatar") -> Optional[Event]:
"""
提交下一个可启动的计划为当前动作;返回开始事件(若有)。
"""
if self.current_action is not None:
return None
while self.planned_actions:
plan = self.planned_actions.pop(0)
try:
action = self.create_action(plan.action_name)
except Exception as e:
logger = get_logger().logger
logger.warning(
"非法动作: Avatar(name=%s,id=%s) 的动作 %s 参数=%s 无法启动,原因=%s",
self.name, self.id, plan.action_name, plan.params, e
)
continue
# 再验证
if not isinstance(plan.params, dict):
get_logger().logger.warning(
"非法参数: Avatar(name=%s) 动作 %s 参数类型错误: %s",
self.name, plan.action_name, type(plan.params)
)
continue
params_for_can_start = filter_kwargs_for_callable(action.can_start, plan.params)
try:
can_start, reason = action.can_start(**params_for_can_start)
except TypeError as e:
get_logger().logger.warning(
"动作启动失败: Avatar(name=%s) 动作 %s 参数校验异常: %s",
self.name, plan.action_name, e
)
continue
if not can_start:
# 记录不合法动作
logger = get_logger().logger
logger.warning(
"非法动作: Avatar(name=%s,id=%s) 的动作 %s 参数=%s 无法启动,原因=%s",
self.name, self.id, plan.action_name, plan.params, reason
)
continue
# 启动
params_for_start = filter_kwargs_for_callable(action.start, plan.params)
start_event = action.start(**params_for_start)
self.current_action = ActionInstance(action=action, params=plan.params, status="running")
# 标记为"本轮新设动作",用于本月补充执行
self._new_action_set_this_step = True
return start_event
return None
async def tick_action(self: "Avatar") -> List[Event]:
"""
推进当前动作一步;返回过程中由动作内部产生的事件(通过 add_event 收集)。
"""
if self.current_action is None:
return []
# 记录当前动作实例引用,用于检测执行过程中是否发生了"抢占/切换"
action_instance_before = self.current_action
action = action_instance_before.action
params = action_instance_before.params
params_for_step = filter_kwargs_for_callable(action.step, params)
result: ActionResult = action.step(**params_for_step)
if result.status == ActionStatus.COMPLETED:
params_for_finish = filter_kwargs_for_callable(action.finish, params)
finish_events = await action.finish(**params_for_finish)
# 仅当当前动作仍然是刚才执行的那个实例时才清空
# 若在 step() 内部通过"抢占"机制切换了动作(如 Escape 失败立即切到 Attack不要清空新动作
if self.current_action is action_instance_before:
self.current_action = None
if finish_events:
# 允许 finish 直接返回事件(极少用),统一并入 pending
for e in finish_events:
self._pending_events.append(e)
# 合并动作返回的事件(通常为空)
if result.events:
for e in result.events:
self.add_event(e)
events, self._pending_events = self._pending_events, []
# 本轮已执行过,清除"新设动作"标记
# 1. 动作结束 (None)
# 2. 动作继续执行且未发生切换 (is action_instance_before)
# 注意:如果动作发生了切换(如 Escape -> Attack则视为新动作不清除标记以便 Simulator 进行下一轮调度
if self.current_action is None or self.current_action is action_instance_before:
self._new_action_set_this_step = False
return events
def add_event(self: "Avatar", event: Event, *, to_sidebar: bool = True) -> None:
"""
添加事件:
- to_sidebar: 是否进入全局侧边栏(通过 Avatar._pending_events 暂存)
注意事件会先存入_pending_events统一由Simulator写入event_manager避免重复
"""
if to_sidebar:
self._pending_events.append(event)
def process_interaction_from_event(self: "Avatar", event: "Event") -> None:
"""
根据事件更新与其他角色的交互计数。
该方法由 Simulator 统一调用。
"""
if not event.related_avatars:
return
for aid in event.related_avatars:
if str(aid) == str(self.id):
continue
# self.id 与 aid 有交互
# relation_interaction_states 是 defaultdict会自动初始化新条目
self.relation_interaction_states[aid]["count"] += 1
def get_planned_actions_str(self: "Avatar") -> str:
"""
获取易读的计划动作列表字符串。
"""
from src.i18n import t
if not self.planned_actions:
return t("None")
lines = []
for i, plan in enumerate(self.planned_actions, 1):
try:
action_cls = ActionRegistry.get(plan.action_name)
# 优先取 ACTION_NAME否则用类名
display_name = getattr(action_cls, "ACTION_NAME", plan.action_name)
except Exception:
display_name = plan.action_name
# 简化参数显示,只保留基本类型
simple_params = {k: v for k, v in plan.params.items() if isinstance(v, (str, int, float, bool))}
info = f"{i}. {display_name}"
if simple_params:
info += f" {simple_params}"
lines.append(info)
return "\n".join(lines)
@property
def can_join_gathering(self: "Avatar") -> bool:
"""是否可以参加聚会"""
if self.current_action and self.current_action.action:
return getattr(self.current_action.action, 'ALLOW_GATHERING', True)
return True # 空闲状态默认可以
@property
def can_trigger_world_event(self: "Avatar") -> bool:
"""是否可以触发奇遇/霉运"""
if self.current_action and self.current_action.action:
return getattr(self.current_action.action, 'ALLOW_WORLD_EVENTS', True)
return True