diff --git a/src/classes/action/__init__.py b/src/classes/action/__init__.py index 0b5512f..2984e9f 100644 --- a/src/classes/action/__init__.py +++ b/src/classes/action/__init__.py @@ -29,7 +29,6 @@ from .sold import SellItems from .battle import Battle from .plunder_mortals import PlunderMortals from .help_mortals import HelpMortals -from .talk import Talk from .devour_mortals import DevourMortals from .self_heal import SelfHeal from .catch import Catch @@ -58,10 +57,10 @@ register_action(actual=True)(SellItems) register_action(actual=False)(Battle) register_action(actual=True)(PlunderMortals) register_action(actual=True)(HelpMortals) -register_action(actual=True)(Talk) register_action(actual=True)(DevourMortals) register_action(actual=True)(SelfHeal) register_action(actual=True)(Catch) +# Talk 已移动到 mutual_action 模块,在那里注册 __all__ = [ # 基类 @@ -89,10 +88,10 @@ __all__ = [ "Battle", "PlunderMortals", "HelpMortals", - "Talk", "DevourMortals", "SelfHeal", "Catch", + # Talk 已移动到 mutual_action 模块 ] diff --git a/src/classes/action/talk.py b/src/classes/action/talk.py deleted file mode 100644 index e18e6b1..0000000 --- a/src/classes/action/talk.py +++ /dev/null @@ -1,59 +0,0 @@ -from __future__ import annotations - -from src.classes.action import InstantAction -from src.classes.action_runtime import ActionResult, ActionStatus -from src.utils.config import CONFIG -from src.classes.event import Event - - -class Talk(InstantAction): - """ - 攀谈:尝试与交互范围内的某个NPC进行交谈。 - - can_start:交互范围内存在其他NPC - - 发起后:从交互范围内随机选择一个目标,进入 MutualAction: Conversation(允许建立关系) - """ - - COMMENT = "与交互范围内的NPC发起攀谈" - DOABLES_REQUIREMENTS = "交互范围内存在其他NPC" - PARAMS = {} - - def _get_observed_others(self) -> list["Avatar"]: - return self.world.avatar_manager.get_observable_avatars(self.avatar) - - # 不再限定必须同一 tile,由交互范围统一约束 - - def _execute(self) -> None: - # Talk 本身不做长期效果,主要在 step 中驱动 Conversation - return - - def can_start(self, **kwargs) -> tuple[bool, str]: - # 交互范围内是否存在其他NPC(用于展示在动作空间) - ok = len(self._get_observed_others()) > 0 - return (ok, "" if ok else "交互范围内没有可交谈对象") - - def start(self) -> Event: - self.observed_others = self._get_observed_others() - # 记录开始事件 - return Event(self.world.month_stamp, f"{self.avatar.name} 尝试与交互范围内的他人攀谈", related_avatars=[self.avatar.id]) - - def step(self) -> ActionResult: - import random - - target = random.choice(self.observed_others) - - # 进入交谈:由概率决定本次是否允许建立关系 - from src.classes.mutual_action import Conversation - # 由配置决定本次是否有“有机会进入关系”标记 - prob = CONFIG.social.talk_into_relation_probability - can_into_relation = random.random() < prob - - conv = Conversation(self.avatar, self.world) - # 启动事件写入历史,不入侧边栏 - conv.start(target_avatar=target) - conv.step(target_avatar=target, can_into_relation=can_into_relation) - return ActionResult(status=ActionStatus.COMPLETED, events=[]) - - def finish(self) -> list[Event]: - return [] - - diff --git a/src/classes/avatar.py b/src/classes/avatar.py index 0f0b10b..9bdc595 100644 --- a/src/classes/avatar.py +++ b/src/classes/avatar.py @@ -590,23 +590,26 @@ class Avatar: def set_relation(self, other: "Avatar", relation: Relation) -> None: """ 设置与另一个角色的关系。 - - 对称关系(如 FRIEND/ENEMY/LOVERS/SIBLING/KIN)会在对方处写入相同的关系。 - - 有向关系(如 MASTER、APPRENTICE、PARENT、CHILD)会在对方处写入对偶关系。 + 委托给 relations.py 中的函数。 """ - if other is self: - return - self.relations[other] = relation - # 写入对方的对偶关系(对称关系会得到同一枚举值) - if getattr(other, "relations", None) is not None: - other.relations[self] = get_reciprocal(relation) + from src.classes.relations import set_relation + set_relation(self, other, relation) def get_relation(self, other: "Avatar") -> Optional[Relation]: - return self.relations.get(other) + """ + 获取与另一个角色的关系。 + 委托给 relations.py 中的函数。 + """ + from src.classes.relations import get_relation + return get_relation(self, other) def clear_relation(self, other: "Avatar") -> None: - self.relations.pop(other, None) - if getattr(other, "relations", None) is not None: - other.relations.pop(self, None) + """ + 清除与另一个角色的关系。 + 委托给 relations.py 中的函数。 + """ + from src.classes.relations import clear_relation + clear_relation(self, other) def _get_relations_summary_str(self, max_count: int = 8) -> str: entries: list[str] = [] diff --git a/src/classes/mutual_action/__init__.py b/src/classes/mutual_action/__init__.py index 3174c43..5baa3db 100644 --- a/src/classes/mutual_action/__init__.py +++ b/src/classes/mutual_action/__init__.py @@ -5,6 +5,7 @@ from .drive_away import DriveAway from .attack import Attack from .conversation import Conversation from .dual_cultivation import DualCultivation +from .talk import Talk from src.classes.action.registry import register_action __all__ = [ @@ -13,6 +14,7 @@ __all__ = [ "Attack", "Conversation", "DualCultivation", + "Talk", ] # 注册 mutual actions(均为实际动作) @@ -20,5 +22,6 @@ register_action(actual=True)(DriveAway) register_action(actual=True)(Attack) register_action(actual=True)(Conversation) register_action(actual=True)(DualCultivation) +register_action(actual=True)(Talk) diff --git a/src/classes/mutual_action/conversation.py b/src/classes/mutual_action/conversation.py index 719be0a..cb52127 100644 --- a/src/classes/mutual_action/conversation.py +++ b/src/classes/mutual_action/conversation.py @@ -4,7 +4,13 @@ from pathlib import Path from typing import TYPE_CHECKING from .mutual_action import MutualAction -from src.classes.relation import relation_display_names, Relation, get_possible_post_relations +from src.classes.relation import relation_display_names, Relation +from src.classes.relations import ( + get_possible_new_relations, + get_possible_cancel_relations, + set_relation, + cancel_relation, +) from src.classes.event import Event from src.utils.config import CONFIG from src.classes.action_runtime import ActionResult, ActionStatus @@ -17,9 +23,9 @@ if TYPE_CHECKING: class Conversation(MutualAction): """交谈:两名角色在同一区域进行交流。 - - 可由“攀谈”触发,或直接发起 + - 可由"攀谈"触发,或直接发起 - 仅当双方处于同一 Region 时可启动 - - 当 can_into_relation=True 且 LLM 决策返回 into_relation 时,根据返回建立关系 + - LLM 可决策是否进入新关系或取消旧关系 - 会将对话内容写入事件系统 """ @@ -27,14 +33,14 @@ class Conversation(MutualAction): COMMENT = "与对方进行一段交流对话" DOABLES_REQUIREMENTS = "目标在交互范围内" PARAMS = {"target_avatar": "AvatarName"} - FEEDBACK_ACTIONS: list[str] = ["Talk", "Reject"] + FEEDBACK_ACTIONS: list[str] = [] # Conversation 自动触发,不需要对方决策 STORY_PROMPT: str = "" def _get_template_path(self) -> Path: - # 使用 talk.txt 模板,以获取是否接受与对话内容 - return CONFIG.paths.templates / "talk.txt" + # 使用专门的 conversation.txt 模板 + return CONFIG.paths.templates / "conversation.txt" - def _build_prompt_infos(self, target_avatar: "Avatar", *, can_into_relation: bool) -> dict: + def _build_prompt_infos(self, target_avatar: "Avatar") -> dict: avatar_name_1 = self.avatar.name avatar_name_2 = target_avatar.name # 交谈:使用详细信息,便于生成更丰富对话 @@ -43,8 +49,12 @@ class Conversation(MutualAction): avatar_name_2: target_avatar.get_info(detailed=True), } # 可能的后天关系(转中文名,给模板阅读) - possible_relations = [relation_display_names[r] for r in get_possible_post_relations(self.avatar, target_avatar)] - # 历史上下文:仅双方共同经历的最近事件(与 MutualAction 对齐) + # 注意:这里计算的是 target 相对于 avatar 的可能关系 + possible_new_relations = [relation_display_names[r] for r in get_possible_new_relations(self.avatar, target_avatar)] + # 可能取消的关系 + possible_cancel_relations = [relation_display_names[r] for r in get_possible_cancel_relations(target_avatar, self.avatar)] + + # 历史上下文:仅双方共同经历的最近事件 n = CONFIG.social.event_context_num em = self.world.event_manager pair_recent_events = [str(e) for e in em.get_events_between(self.avatar.id, target_avatar.id, limit=n)] @@ -52,11 +62,12 @@ class Conversation(MutualAction): "avatar_infos": avatar_infos, "avatar_name_1": avatar_name_1, "avatar_name_2": avatar_name_2, - "can_into_relation": bool(can_into_relation), - "possible_relations": possible_relations, + "possible_new_relations": possible_new_relations, + "possible_cancal_relations": possible_cancel_relations, # 保持模板中的拼写 "recent_events": pair_recent_events, } + # 覆盖 can_start:Conversation 不需要检查观察范围,只需要在有效区域即可 def can_start(self, target_avatar: "Avatar|str|None" = None, **kwargs) -> tuple[bool, str]: if target_avatar is None: return False, "缺少参数 target_avatar" @@ -65,11 +76,9 @@ class Conversation(MutualAction): return False, "目标不存在" if target.tile is None or self.avatar.tile is None: return False, "目标未处于有效区域" - # 先不限定同一区域,之后再限制 - # if target.tile.region != self.avatar.tile.region: - # return False, "目标不在同一区域" return True, "" + # 覆盖 start:自定义事件消息 def start(self, target_avatar: "Avatar|str", **kwargs) -> Event: target = self._get_target_avatar(target_avatar) target_name = target.name if target is not None else str(target_avatar) @@ -77,49 +86,84 @@ class Conversation(MutualAction): if target is not None: rel_ids.append(target.id) event = Event(self.world.month_stamp, f"{self.avatar.name} 与 {target_name} 开始交谈", related_avatars=rel_ids) - # 写入历史即可,内容事件稍后生成 self.avatar.add_event(event, to_sidebar=False) if target is not None: target.add_event(event, to_sidebar=False) return event - def step(self, target_avatar: "Avatar|str", can_into_relation: bool = False) -> ActionResult: + def _handle_feedback_result(self, target: "Avatar", result: dict) -> ActionResult: + """ + 处理 LLM 返回的对话结果,包括对话内容和关系变化。 + Conversation 不需要反馈(FEEDBACK_ACTIONS 为空),直接生成内容。 + """ + conversation_content = str(result.get("conversation_content", "")).strip() + new_relation_str = str(result.get("new_relation", "")).strip() + cancel_relation_str = str(result.get("cancal_relation", "")).strip() # 保持模板中的拼写 + + # 记录对话内容 + if conversation_content: + content_event = Event( + self.world.month_stamp, + f"{self.avatar.name} 与 {target.name} 的交谈:{conversation_content}", + related_avatars=[self.avatar.id, target.id] + ) + EventHelper.push_pair(content_event, initiator=self.avatar, target=target, to_sidebar_once=True) + + # 处理进入新关系 + if new_relation_str: + rel = Relation.from_chinese(new_relation_str) + if rel is not None: + set_relation(target, self.avatar, rel) + set_event = Event( + self.world.month_stamp, + f"{target.name} 与 {self.avatar.name} 的关系变为:{relation_display_names.get(rel, str(rel))}", + related_avatars=[self.avatar.id, target.id] + ) + EventHelper.push_pair(set_event, initiator=self.avatar, target=target, to_sidebar_once=True) + + # 处理取消关系 + if cancel_relation_str: + rel = Relation.from_chinese(cancel_relation_str) + if rel is not None: + success = cancel_relation(target, self.avatar, rel) + if success: + cancel_event = Event( + self.world.month_stamp, + f"{target.name} 与 {self.avatar.name} 取消了关系:{relation_display_names.get(rel, str(rel))}", + related_avatars=[self.avatar.id, target.id] + ) + EventHelper.push_pair(cancel_event, initiator=self.avatar, target=target, to_sidebar_once=True) + + return ActionResult(status=ActionStatus.COMPLETED, events=[]) + + def step(self, target_avatar: "Avatar|str", **kwargs) -> ActionResult: + """调用通用异步 step 逻辑""" target = self._get_target_avatar(target_avatar) if target is None: - return ActionResult(status=ActionStatus.COMPLETED, events=[]) + return ActionResult(status=ActionStatus.FAILED, events=[]) - infos = self._build_prompt_infos(target, can_into_relation=can_into_relation) - res = self._call_llm_feedback(infos) - r = res.get(infos["avatar_name_2"], {}) - thinking = r.get("thinking", "") - feedback = str(r.get("feedback", "")).strip() - talk_content = str(r.get("talk_content", "")).strip() - into_relation_str = str(r.get("into_relation", "")).strip() + # 若无任务,创建异步任务 + if self._feedback_task is None and self._feedback_cached is None: + infos = self._build_prompt_infos(target) + import asyncio + try: + loop = asyncio.get_running_loop() + self._feedback_task = loop.create_task(self._call_llm_feedback_async(infos)) + except RuntimeError: + self._feedback_cached = self._call_llm_feedback(infos) - target.thinking = thinking - - fb = feedback.strip() - # 仅当明确接受时才记录对话与关系;其余一律视为拒绝 - if fb == "Talk": - if talk_content: - content_event = Event(self.world.month_stamp, f"{self.avatar.name} 与 {target.name} 的交谈:{talk_content}", related_avatars=[self.avatar.id, target.id]) - # 进入侧栏一次,并写入双方历史 - EventHelper.push_pair(content_event, initiator=self.avatar, target=target, to_sidebar_once=True) - - if can_into_relation and into_relation_str: - rel = Relation.from_chinese(into_relation_str) - if rel is not None: - self.avatar.set_relation(target, rel) - set_event = Event(self.world.month_stamp, f"{self.avatar.name} 与 {target.name} 的关系变为:{relation_display_names.get(rel, str(rel))}", related_avatars=[self.avatar.id, target.id]) - EventHelper.push_pair(set_event, initiator=self.avatar, target=target, to_sidebar_once=True) - - return ActionResult(status=ActionStatus.COMPLETED, events=[]) - else: - feedback_event = Event(self.world.month_stamp, f"{target.name} 拒绝与 {self.avatar.name} 交谈", related_avatars=[self.avatar.id, target.id]) - EventHelper.push_pair(feedback_event, initiator=self.avatar, target=target, to_sidebar_once=True) - return ActionResult(status=ActionStatus.COMPLETED, events=[]) - - def finish(self, target_avatar: "Avatar|str", **kwargs) -> list[Event]: - return [] + # 若任务已完成,消费结果 + if self._feedback_task is not None and self._feedback_task.done(): + self._feedback_cached = self._feedback_task.result() + self._feedback_task = None + if self._feedback_cached is not None: + res = self._feedback_cached + self._feedback_cached = None + r = res.get(target.name, {}) + thinking = r.get("thinking", "") + target.thinking = thinking + + return self._handle_feedback_result(target, r) + return ActionResult(status=ActionStatus.RUNNING, events=[]) diff --git a/src/classes/mutual_action/mutual_action.py b/src/classes/mutual_action/mutual_action.py index 5c3933a..cfa4202 100644 --- a/src/classes/mutual_action/mutual_action.py +++ b/src/classes/mutual_action/mutual_action.py @@ -4,12 +4,13 @@ from pathlib import Path from typing import TYPE_CHECKING import asyncio -from src.classes.action import DefineAction, ActualActionMixin, LLMAction +from src.classes.action.action import DefineAction, ActualActionMixin, LLMAction from src.classes.tile import get_avatar_distance from src.classes.event import Event from src.utils.llm import get_prompt_and_call_llm, get_prompt_and_call_llm_async from src.utils.config import CONFIG -from src.classes.relation import relation_display_names, Relation, get_possible_post_relations +from src.classes.relation import relation_display_names, Relation +from src.classes.relations import get_possible_new_relations from src.classes.action_runtime import ActionResult, ActionStatus from src.classes.action.event_helper import EventHelper from src.classes.action.targeting_mixin import TargetingMixin diff --git a/src/classes/mutual_action/talk.py b/src/classes/mutual_action/talk.py new file mode 100644 index 0000000..af0b024 --- /dev/null +++ b/src/classes/mutual_action/talk.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from src.classes.action_runtime import ActionResult, ActionStatus +from src.classes.event import Event +from src.classes.action.event_helper import EventHelper + +if TYPE_CHECKING: + from src.classes.avatar import Avatar + +from .mutual_action import MutualAction + + +class Talk(MutualAction): + """ + 攀谈:向交互范围内的某个NPC发起攀谈。 + - 接受后自动进入 Conversation + """ + + ACTION_NAME = "攀谈" + COMMENT = "向对方发起攀谈" + DOABLES_REQUIREMENTS = "目标在交互范围内" + PARAMS = {"target_avatar": "AvatarName"} + FEEDBACK_ACTIONS: list[str] = ["Talk", "Reject"] + + # 复用父类的所有方法: + # - _get_template_path() -> mutual_action.txt + # - _build_prompt_infos() -> 标准的双方信息和历史事件 + # - can_start() -> 检查目标在交互范围内 + # - start() -> 生成开始事件 + # - finish() -> 返回空列表(已在父类实现) + + def _handle_feedback_result(self, target: "Avatar", result: dict) -> ActionResult: + """ + 处理 LLM 返回的反馈结果。 + 子类可覆盖此方法来定义自己的反馈处理逻辑。 + """ + feedback = str(result.get("feedback", "")).strip() + + # 处理反馈 + if feedback == "Talk": + # 接受攀谈,自动进入 Conversation + accept_event = Event( + self.world.month_stamp, + f"{target.name} 接受了 {self.avatar.name} 的攀谈", + related_avatars=[self.avatar.id, target.id] + ) + EventHelper.push_pair(accept_event, initiator=self.avatar, target=target, to_sidebar_once=True) + + # 立即启动 Conversation + from .conversation import Conversation + conv = Conversation(self.avatar, self.world) + conv.start(target_avatar=target) + # 直接执行一次 step,启动异步调用 + conv.step(target_avatar=target) + else: + # 拒绝攀谈 + reject_event = Event( + self.world.month_stamp, + f"{target.name} 拒绝了 {self.avatar.name} 的攀谈", + related_avatars=[self.avatar.id, target.id] + ) + EventHelper.push_pair(reject_event, initiator=self.avatar, target=target, to_sidebar_once=True) + + return ActionResult(status=ActionStatus.COMPLETED, events=[]) + + def step(self, target_avatar: "Avatar|str", **kwargs) -> ActionResult: + """调用父类的通用异步 step 逻辑""" + target = self._get_target_avatar(target_avatar) + if target is None: + return ActionResult(status=ActionStatus.FAILED, events=[]) + + # 若无任务,创建异步任务 + if self._feedback_task is None and self._feedback_cached is None: + infos = self._build_prompt_infos(target) + import asyncio + try: + loop = asyncio.get_running_loop() + self._feedback_task = loop.create_task(self._call_llm_feedback_async(infos)) + except RuntimeError: + self._feedback_cached = self._call_llm_feedback(infos) + + # 若任务已完成,消费结果 + if self._feedback_task is not None and self._feedback_task.done(): + self._feedback_cached = self._feedback_task.result() + self._feedback_task = None + + if self._feedback_cached is not None: + res = self._feedback_cached + self._feedback_cached = None + r = res.get(target.name, {}) + thinking = r.get("thinking", "") + target.thinking = thinking + + return self._handle_feedback_result(target, r) + + return ActionResult(status=ActionStatus.RUNNING, events=[]) \ No newline at end of file diff --git a/src/classes/relation.py b/src/classes/relation.py index 11f03fe..2355e8d 100644 --- a/src/classes/relation.py +++ b/src/classes/relation.py @@ -91,50 +91,6 @@ if TYPE_CHECKING: from src.classes.avatar import Avatar -def get_possible_post_relations(from_avatar: "Avatar", to_avatar: "Avatar") -> List[Relation]: - """ - 评估“to_avatar 相对于 from_avatar”可能新增的后天关系集合(方向性明确)。 - - 清晰规则: - - LOVERS(道侣):要求男女异性;若已存在 to->from 的相同关系则不重复 - - MASTER(师傅):要求 to.level >= from.level + 20 - - APPRENTICE(徒弟):要求 to.level <= from.level - 20 - - FRIEND(朋友):始终可能(若未已存在) - - ENEMY(仇人):始终可能(若未已存在) - - 说明:本函数只判断“是否可能”,不做概率与人格相关控制;概率留给上层逻辑。 - 返回的是 Relation 列表,均为 to_avatar 相对于 from_avatar 的候选。 - """ - # 方向相关:检查 to->from 已有关系,避免重复推荐 - existing_to_from = to_avatar.get_relation(from_avatar) - - candidates: list[Relation] = [] - - # 基础信息(Avatar 定义确保存在) - level_from = from_avatar.cultivation_progress.level - level_to = to_avatar.cultivation_progress.level - - # - FRIEND - if existing_to_from != Relation.FRIEND: - candidates.append(Relation.FRIEND) - - # - ENEMY - if existing_to_from != Relation.ENEMY: - candidates.append(Relation.ENEMY) - - # - LOVERS:异性(Avatar 定义确保性别存在) - if from_avatar.gender != to_avatar.gender and existing_to_from != Relation.LOVERS: - candidates.append(Relation.LOVERS) - - # - 师徒(方向性): - # MASTER:to 是 from 的师傅 → to.level >= from.level + 20 - # APPRENTICE:to 是 from 的徒弟 → to.level <= from.level - 20 - if level_to >= level_from + 20 and existing_to_from != Relation.MASTER: - candidates.append(Relation.MASTER) - if level_to <= level_from - 20 and existing_to_from != Relation.APPRENTICE: - candidates.append(Relation.APPRENTICE) - - return candidates # ——— 显示层:性别化称谓映射与标签工具 ——— @@ -189,4 +145,3 @@ def get_relations_strs(avatar: "Avatar", max_lines: int = 6) -> list[str]: def relations_to_str(avatar: "Avatar", sep: str = ";", max_lines: int = 6) -> str: lines = get_relations_strs(avatar, max_lines=max_lines) return sep.join(lines) if lines else "无" - diff --git a/src/classes/relations.py b/src/classes/relations.py new file mode 100644 index 0000000..cc3a336 --- /dev/null +++ b/src/classes/relations.py @@ -0,0 +1,126 @@ +""" +两个角色之间的关系操作函数 +""" +from __future__ import annotations + +from typing import TYPE_CHECKING, List + +from src.classes.relation import Relation, INNATE_RELATIONS, get_reciprocal, is_innate + +if TYPE_CHECKING: + from src.classes.avatar import Avatar + + +def get_possible_new_relations(from_avatar: "Avatar", to_avatar: "Avatar") -> List[Relation]: + """ + 评估"to_avatar 相对于 from_avatar"可能新增的后天关系集合(方向性明确)。 + + 清晰规则: + - LOVERS(道侣):要求男女异性;若已存在 to->from 的相同关系则不重复 + - MASTER(师傅):要求 to.level >= from.level + 20 + - APPRENTICE(徒弟):要求 to.level <= from.level - 20 + - FRIEND(朋友):始终可能(若未已存在) + - ENEMY(仇人):始终可能(若未已存在) + + 说明:本函数只判断"是否可能",不做概率与人格相关控制;概率留给上层逻辑。 + 返回的是 Relation 列表,均为 to_avatar 相对于 from_avatar 的候选。 + """ + # 方向相关:检查 to->from 已有关系,避免重复推荐 + existing_to_from = to_avatar.get_relation(from_avatar) + + candidates: list[Relation] = [] + + # 基础信息(Avatar 定义确保存在) + level_from = from_avatar.cultivation_progress.level + level_to = to_avatar.cultivation_progress.level + + # - FRIEND + if existing_to_from != Relation.FRIEND: + candidates.append(Relation.FRIEND) + + # - ENEMY + if existing_to_from != Relation.ENEMY: + candidates.append(Relation.ENEMY) + + # - LOVERS:异性(Avatar 定义确保性别存在) + if from_avatar.gender != to_avatar.gender and existing_to_from != Relation.LOVERS: + candidates.append(Relation.LOVERS) + + # - 师徒(方向性): + # MASTER:to 是 from 的师傅 → to.level >= from.level + 20 + # APPRENTICE:to 是 from 的徒弟 → to.level <= from.level - 20 + if level_to >= level_from + 20 and existing_to_from != Relation.MASTER: + candidates.append(Relation.MASTER) + if level_to <= level_from - 20 and existing_to_from != Relation.APPRENTICE: + candidates.append(Relation.APPRENTICE) + + return candidates + + +def set_relation(from_avatar: "Avatar", to_avatar: "Avatar", relation: Relation) -> None: + """ + 设置 from_avatar 对 to_avatar 的关系。 + - 对称关系(如 FRIEND/ENEMY/LOVERS/SIBLING/KIN)会在对方处写入相同的关系。 + - 有向关系(如 MASTER、APPRENTICE、PARENT、CHILD)会在对方处写入对偶关系。 + """ + if to_avatar is from_avatar: + return + from_avatar.relations[to_avatar] = relation + # 写入对方的对偶关系(对称关系会得到同一枚举值) + to_avatar.relations[from_avatar] = get_reciprocal(relation) + + +def get_relation(from_avatar: "Avatar", to_avatar: "Avatar") -> Relation | None: + """ + 获取 from_avatar 对 to_avatar 的关系。 + """ + return from_avatar.relations.get(to_avatar) + + +def clear_relation(from_avatar: "Avatar", to_avatar: "Avatar") -> None: + """ + 清除 from_avatar 和 to_avatar 之间的关系(双向清除)。 + """ + from_avatar.relations.pop(to_avatar, None) + to_avatar.relations.pop(from_avatar, None) + + +def cancel_relation(from_avatar: "Avatar", to_avatar: "Avatar", relation: Relation) -> bool: + """ + 取消指定的后天关系。 + - 只能取消后天关系(INNATE_RELATIONS 不可取消) + - 检查该关系是否存在且匹配 + - 双向清除 + + 返回:是否成功取消 + """ + # 先天关系不可取消 + if is_innate(relation): + return False + + # 检查关系是否存在且匹配 + existing = get_relation(from_avatar, to_avatar) + if existing != relation: + return False + + # 清除关系 + clear_relation(from_avatar, to_avatar) + return True + + +def get_possible_cancel_relations(from_avatar: "Avatar", to_avatar: "Avatar") -> List[Relation]: + """ + 获取可能取消的关系列表(仅后天关系)。 + + 返回:from_avatar 对 to_avatar 的可取消关系列表 + """ + existing = get_relation(from_avatar, to_avatar) + if existing is None: + return [] + + # 只有后天关系可以取消 + if is_innate(existing): + return [] + + return [existing] + diff --git a/static/config.yml b/static/config.yml index 48ad404..a5b4fec 100644 --- a/static/config.yml +++ b/static/config.yml @@ -26,8 +26,7 @@ avatar: persona_num: 3 social: - talk_into_relation_probability: 0.1 - event_context_num: 6 + event_context_num: 8 # defined_avatar: # surname: 丰川 diff --git a/static/templates/conversation.txt b/static/templates/conversation.txt new file mode 100644 index 0000000..aeca067 --- /dev/null +++ b/static/templates/conversation.txt @@ -0,0 +1,23 @@ +你是一个决策者,这是一个仙侠世界,你负责来生成两个NPC间的对话内容。 + +你需要进行决策的NPC的dict[AvatarName, info]为 +{avatar_infos} +正在进行的动作为:{avatar_name_1}和{avatar_name_2}正在对话。这个对话可能是善意的,也可能是恶意的,也可能是闲聊。内容和性质取决于NPC性格、正邪、关系等因素。 + +两者可能进入的关系:{possible_new_relations} +两者可能取消的关系:{possible_cancal_relations} +注意:进入/取消关系不是必须的,完全由你根据对话情况、双方性格、历史事件等判断决定。 + +最近事件: +{recent_events} + +注意,只返回json格式的结果。 +格式为: +{{ + "{avatar_name_2}": {{ + "thinking": ..., // 简单思考对话的情况 + "conversation_content": ... // 对话双方均为第三人称视角的,对话的主题和情况概括,约100字。注意不是对话的口语内容,仙侠语言风格。 + "new_relation": ... // 可选,如果你认为可以让两者产生某种身份关系,则返回关系的中文名。注意这是{avatar_name_2}相对于{avatar_name_1}的身份。 + "cancal_relation": ... // 可选,如果你认为可以让两者取消某种身份关系,则返回关系的中文名。注意这是{avatar_name_2}相对于{avatar_name_1}的身份。 + }} +}} \ No newline at end of file diff --git a/static/templates/talk.txt b/static/templates/talk.txt deleted file mode 100644 index 9fcf829..0000000 --- a/static/templates/talk.txt +++ /dev/null @@ -1,23 +0,0 @@ -你是一个决策者,这是一个仙侠世界,你负责来决定一个NPC对另一个NPC的攀谈行为。 - -你需要进行决策的NPC的dict[AvatarName, info]为 -{avatar_infos} -正在进行的动作为:{avatar_name_1}向{avatar_name_2}发起了攀谈。这代表{avatar_name_1}希望与{avatar_name_2}进行对话(他们目前是陌生人)。这个对话可能是善意的,也可能是恶意的,也可能是闲聊。取决于NPC性格、正邪等因素。 -{avatar_name_2}可以进行的选择为: -["Talk", "Reject"] -两者是否可能进入某种关系:{can_into_relation}。注意,如果为True,也不代表一定要进入某种关系。这都由你来判断。 -{avatar_name_2}可能相对于{avatar_name_1}的身份为: {possible_relations} - -最近事件: -{recent_events} - -注意,只返回json格式的结果。 -只返回{avatar_name_2}的行动,格式为: -{{ - {avatar_name_2}: {{ - "thinking": ..., // 简单思考应该怎么决策 - "feedback": ... // 面对{avatar_name_1}的行为的合法feedback action name - "talk_content": ... // 对话双方均为第三人称视角的,对话的主题和情况概括,约100字。注意不是对话的口语内容,仙侠语言风格。 - "into_relation": ... // 如果你认为可以让两者产生某种身份关系,则返回。注意这是{avatar_name_2}相对于{avatar_name_1}的身份。 - }} -}} \ No newline at end of file