diff --git a/src/classes/avatar.py b/src/classes/avatar.py index cda016f..fb66247 100644 --- a/src/classes/avatar.py +++ b/src/classes/avatar.py @@ -108,6 +108,8 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin): auxiliary: Optional[Auxiliary] = None # 灵兽:最多一个;若再次捕捉则覆盖 spirit_animal: Optional[SpiritAnimal] = None + # 绰号:江湖中对该角色的称谓,满足条件后生成,永久不变 + nickname: Optional[str] = None # 当月/当步新设动作标记:在 commit_next_plan 设为 True,首次 tick_action 后清为 False _new_action_set_this_step: bool = False # 动作冷却:记录动作类名 -> 上次完成月戳 @@ -260,6 +262,9 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin): "兵器": weapon_info, "辅助装备": auxiliary_info, } + # 绰号:仅在存在时显示 + if self.nickname is not None: + info_dict["绰号"] = self.nickname # 灵兽:仅在存在时显示 if self.spirit_animal is not None: info_dict["灵兽"] = spirit_animal_info @@ -574,7 +579,10 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin): lines: list[str] = [] # 基础信息 - lines.append(f"{self.name}") + if self.nickname: + lines.append(f"{self.name}「{self.nickname}」") + else: + lines.append(f"{self.name}") add_kv(lines, "性别", self.gender) add_kv(lines, "年龄", self.age) add_kv(lines, "外貌", self.appearance.get_info()) diff --git a/src/classes/nickname.py b/src/classes/nickname.py new file mode 100644 index 0000000..98ff903 --- /dev/null +++ b/src/classes/nickname.py @@ -0,0 +1,136 @@ +""" +绰号生成模块 +为满足条件的角色生成江湖绰号 +""" +from typing import Optional, List, TYPE_CHECKING + +if TYPE_CHECKING: + from src.classes.avatar import Avatar + from src.classes.world import World + +from src.classes.event import Event +from src.utils.config import CONFIG +from src.utils.llm import get_prompt_and_call_llm_async +from src.run.log import get_logger + +logger = get_logger().logger + + +def can_get_nickname(avatar: "Avatar") -> bool: + """ + 检查角色是否满足获得绰号的条件 + + 条件: + 1. 尚未拥有绰号(nickname为None) + 2. 长期事件数量 >= major_event_threshold + 3. 短期事件数量 >= minor_event_threshold + + Args: + avatar: 要检查的角色 + + Returns: + 是否满足条件 + """ + # 已有绰号,不再生成 + if avatar.nickname is not None: + return False + + # 检查事件数量 + em = avatar.world.event_manager + major_threshold = CONFIG.nickname.major_event_threshold + minor_threshold = CONFIG.nickname.minor_event_threshold + + major_events = em.get_major_events_by_avatar(avatar.id) + minor_events = em.get_minor_events_by_avatar(avatar.id) + + major_count = len(major_events) + minor_count = len(minor_events) + + # AND逻辑:两个条件都要满足 + return major_count >= major_threshold and minor_count >= minor_threshold + + +async def generate_nickname(avatar: "Avatar") -> Optional[str]: + """ + 为角色生成绰号 + + 调用LLM基于角色信息和事件历史生成合适的绰号 + + Args: + avatar: 要生成绰号的角色 + + Returns: + 生成的绰号,失败则返回None + """ + try: + # 准备角色信息 + avatar_info = avatar.get_info(detailed=True) + avatar_info_str = "\n".join([f"{k}: {v}" for k, v in avatar_info.items()]) + + # 获取事件历史(根据配置的阈值获取对应数量的事件) + em = avatar.world.event_manager + major_threshold = CONFIG.nickname.major_event_threshold + minor_threshold = CONFIG.nickname.minor_event_threshold + major_events = em.get_major_events_by_avatar(avatar.id, limit=major_threshold) + minor_events = em.get_minor_events_by_avatar(avatar.id, limit=minor_threshold) + + major_events_str = "\n".join([f"- {str(e)}" for e in major_events]) if major_events else "无" + minor_events_str = "\n".join([f"- {str(e)}" for e in minor_events]) if minor_events else "无" + + # 准备模板参数 + template_path = CONFIG.paths.templates / "nickname.txt" + infos = { + "avatar_info": avatar_info_str, + "major_events": major_events_str, + "minor_events": minor_events_str + } + + # 调用LLM并自动解析JSON + response_data = await get_prompt_and_call_llm_async(template_path, infos, mode="normal") + + nickname = response_data.get("nickname", "").strip() + thinking = response_data.get("thinking", "") + + if not nickname: + logger.warning(f"为角色 {avatar.name} 生成绰号失败:返回空绰号") + return None + + logger.info(f"为角色 {avatar.name} 生成绰号:{nickname}") + logger.debug(f"绰号生成思考过程:{thinking}") + + return nickname + + except Exception as e: + logger.error(f"生成绰号时出错:{e}") + return None + + +async def process_avatar_nickname(avatar: "Avatar") -> Optional[Event]: + """ + 处理单个角色的绰号生成 + + 检查角色是否满足条件,满足则生成绰号并返回对应事件 + + Args: + avatar: 要处理的角色 + + Returns: + 生成的事件,如果不满足条件或生成失败则返回None + """ + if not can_get_nickname(avatar): + return None + + nickname = await generate_nickname(avatar) + if not nickname: + return None + + avatar.nickname = nickname + # 生成事件:角色获得绰号 + event = Event( + avatar.world.month_stamp, + f"{avatar.name}在江湖中闯出名号,被人称为'{nickname}'。", + related_avatars=[avatar.id], + is_major=True + ) + return event + diff --git a/src/sim/simulator.py b/src/sim/simulator.py index 0a2f878..f3cd6cd 100644 --- a/src/sim/simulator.py +++ b/src/sim/simulator.py @@ -122,6 +122,19 @@ class Simulator: events.extend(fortune_events) return events + async def _phase_nickname_generation(self): + """ + 绰号生成阶段 + """ + from src.classes.nickname import process_avatar_nickname + + events = [] + for avatar in list(self.world.avatar_manager.avatars.values()): + event = await process_avatar_nickname(avatar) + if event: + events.append(event) + return events + def _phase_update_celestial_phenomenon(self): """ 更新天地灵机: @@ -214,10 +227,13 @@ class Simulator: # 6. 被动结算(时间效果+奇遇) events.extend(await self._phase_passive_effects()) - # 7. 更新天地灵机 + # 7. 绰号生成 + events.extend(await self._phase_nickname_generation()) + + # 8. 更新天地灵机 events.extend(self._phase_update_celestial_phenomenon()) - # 8. 日志 + # 9. 日志 # 统一写入事件管理器 if hasattr(self.world, "event_manager") and self.world.event_manager is not None: for e in events: diff --git a/static/config.yml b/static/config.yml index 3511bf6..0992042 100644 --- a/static/config.yml +++ b/static/config.yml @@ -33,6 +33,10 @@ social: major_event_context_num: 10 # 大事(长期记忆)展示数量 minor_event_context_num: 10 # 小事(短期记忆)展示数量 +nickname: + major_event_threshold: 10 # 获得绰号需要的长期事件数量 + minor_event_threshold: 50 # 获得绰号需要的短期事件数量 + save: max_events_to_save: 1000 diff --git a/static/templates/nickname.txt b/static/templates/nickname.txt new file mode 100644 index 0000000..0366e53 --- /dev/null +++ b/static/templates/nickname.txt @@ -0,0 +1,30 @@ +你是一个仙侠世界的故事家,负责为修仙界人物起绰号。 + +绰号是修仙界中对一个人物的评价和称谓,要求: +1. 符合修仙世界观,具有仙侠风格 +2. 体现角色的特点、行为、性格或事迹 +3. 简洁有力,朗朗上口(2-4个字为佳) +4. 要帅气、有意境 + +角色信息: +{avatar_info} + +角色的长期事件(重大事迹): +{major_events} + +角色的短期事件(近期经历): +{minor_events} + +基于以上信息,为该角色起一个合适的修仙界绰号。 + +返回JSON格式: +{{ + "thinking": "分析角色特点、主要事迹、性格特质,思考什么绰号最能体现这个人物...", + "nickname": "绰号" +}} + +注意: +- thinking是你的思考过程,要详细分析 +- nickname只返回绰号本身,不要加引号或其他符号 +- 绰号要符合修仙世界的风格 +