From f109abbb080ae3a968c7ad79411719ff72f843d4 Mon Sep 17 00:00:00 2001 From: bridge Date: Sat, 4 Oct 2025 17:13:35 +0800 Subject: [PATCH] add persona conditions --- src/classes/alignment.py | 15 ++++++++ src/classes/avatar.py | 4 +-- src/classes/persona.py | 64 ++++++++++++++++++++++----------- src/run/run.py | 3 -- static/config.yml | 2 +- static/game_configs/persona.csv | 54 ++++++++++++++-------------- 6 files changed, 89 insertions(+), 53 deletions(-) diff --git a/src/classes/alignment.py b/src/classes/alignment.py index 6fc7a7e..4d5d26a 100644 --- a/src/classes/alignment.py +++ b/src/classes/alignment.py @@ -15,6 +15,21 @@ class Alignment(Enum): def get_info(self) -> str: return alignment_strs[self] + ": " + alignment_infos[self] + def __hash__(self) -> int: + return hash(self.value) + + def __eq__(self, other) -> bool: + """ + 允许与同类或字符串比较: + - Alignment: 恒等比较 + - str: 同时支持英文值(value)与中文显示(__str__) + """ + if isinstance(other, Alignment): + return self is other + if isinstance(other, str): + return other == self.value or other == str(self) + return False + alignment_strs = { Alignment.RIGHTEOUS: "正", diff --git a/src/classes/avatar.py b/src/classes/avatar.py index c1e0aa2..e41c6fb 100644 --- a/src/classes/avatar.py +++ b/src/classes/avatar.py @@ -93,9 +93,9 @@ class Avatar: # 最大寿元已在 Age 构造时基于境界初始化 - # 如果personas列表为空,则随机分配两个不互斥的persona + # 如果personas列表为空,则随机分配两个符合条件且不互斥的persona if not self.personas: - self.personas = get_random_compatible_personas(persona_num) + self.personas = get_random_compatible_personas(persona_num, avatar=self) def __hash__(self) -> int: return hash(self.id) diff --git a/src/classes/persona.py b/src/classes/persona.py index 6935520..190e083 100644 --- a/src/classes/persona.py +++ b/src/classes/persona.py @@ -1,12 +1,16 @@ import random from dataclasses import dataclass -from typing import List +from typing import List, Optional, TYPE_CHECKING from src.utils.df import game_configs from src.utils.config import CONFIG ids_separator = CONFIG.df.ids_separator +if TYPE_CHECKING: + # 仅用于类型检查,避免运行时循环导入 + from src.classes.avatar import Avatar + @dataclass class Persona: """ @@ -17,6 +21,7 @@ class Persona: prompt: str exclusion_ids: List[int] weight: float + condition: str def _load_personas() -> tuple[dict[int, Persona], dict[str, Persona]]: """从配表加载persona数据""" @@ -34,6 +39,9 @@ def _load_personas() -> tuple[dict[int, Persona], dict[str, Persona]]: weight_val = row.get("weight", 1) weight_str = str(weight_val).strip() weight = float(weight_str) if weight_str and weight_str.lower() != "nan" else 1.0 + # 条件:可为空 + condition_val = row.get("condition", "") + condition = "" if str(condition_val) == "nan" else str(condition_val).strip() persona = Persona( id=int(row["id"]), @@ -41,6 +49,7 @@ def _load_personas() -> tuple[dict[int, Persona], dict[str, Persona]]: prompt=str(row["prompt"]), exclusion_ids=exclusion_ids, weight=weight, + condition=condition, ) personas_by_id[persona.id] = persona personas_by_name[persona.name] = persona @@ -50,12 +59,32 @@ def _load_personas() -> tuple[dict[int, Persona], dict[str, Persona]]: # 从配表加载persona数据 personas_by_id, personas_by_name = _load_personas() -def get_random_compatible_personas(num_personas: int = 2) -> List[Persona]: +def _is_persona_allowed(persona_id: int, already_selected_ids: set[int], avatar: Optional["Avatar"]) -> bool: + """ + 统一判断:persona 是否允许被选择(条件 + 互斥)。 + - 条件:当存在 avatar 且配置了 condition 时,通过安全 eval 判断。 + - 互斥:与已选 persona 双向互斥。 + """ + persona = personas_by_id[persona_id] + # 条件判定 + if avatar is not None and persona.condition: + allowed = bool(eval(persona.condition, {"__builtins__": {}}, {"avatar": avatar})) + if not allowed: + return False + # 与已选互斥检查(双向) + for sid in already_selected_ids: + other = personas_by_id[sid] + if (persona_id in other.exclusion_ids) or (sid in persona.exclusion_ids): + return False + return True + +def get_random_compatible_personas(num_personas: int = 2, avatar: Optional["Avatar"] = None) -> List[Persona]: """ 随机选择指定数量的互相不冲突的persona Args: num_personas: 需要选择的persona数量,默认为2 + avatar: 可选,若提供则按 persona.condition 过滤 Returns: List[Persona]: 互相不冲突的persona列表 @@ -63,32 +92,25 @@ def get_random_compatible_personas(num_personas: int = 2) -> List[Persona]: Raises: ValueError: 如果无法找到足够数量的兼容persona """ - all_persona_ids = set(personas_by_id.keys()) + # 初始候选:若提供 avatar,则先按条件过滤;否则全量 + initial_ids = set(personas_by_id.keys()) + if avatar is not None: + initial_ids = {pid for pid in initial_ids if _is_persona_allowed(pid, set(), avatar)} selected_personas: List[Persona] = [] - available_ids = all_persona_ids.copy() - + selected_ids: set[int] = set() + for i in range(num_personas): + # 按当前已选进行二次过滤(互斥 + 条件) + available_ids = [pid for pid in initial_ids if pid not in selected_ids and _is_persona_allowed(pid, selected_ids, avatar)] if not available_ids: raise ValueError(f"只能找到{i}个兼容的persona,无法满足需要的{num_personas}个") - # 按权重从可用列表中选择一个 - candidates: List[Persona] = [personas_by_id[i] for i in available_ids] + + candidates: List[Persona] = [personas_by_id[pid] for pid in available_ids] weights: List[float] = [max(0.0, c.weight) for c in candidates] selected_persona = random.choices(candidates, weights=weights, k=1)[0] - selected_id = selected_persona.id selected_personas.append(selected_persona) - - # 更新可用列表:移除已选择的和与其互斥的 - available_ids.discard(selected_id) # 移除自己 - - # 移除所有与当前选择互斥的persona - for exclusion_id in selected_persona.exclusion_ids: - available_ids.discard(exclusion_id) - - # 移除所有将当前选择作为互斥对象的persona - for persona_id in list(available_ids): - if selected_id in personas_by_id[persona_id].exclusion_ids: - available_ids.discard(persona_id) - + selected_ids.add(selected_persona.id) + return selected_personas diff --git a/src/run/run.py b/src/run/run.py index d57ed77..6662484 100644 --- a/src/run/run.py +++ b/src/run/run.py @@ -24,7 +24,6 @@ from src.utils.id_generator import get_avatar_id from src.utils.config import CONFIG from src.run.log import get_logger from src.classes.relation import Relation -from src.classes.alignment import Alignment def clamp(value: int, lo: int, hi: int) -> int: @@ -86,8 +85,6 @@ def make_avatars(world: World, count: int = 12, current_month_stamp: MonthStamp root=random.choice(list(Root)), # 随机选择灵根 ) avatar.tile = world.map.get_tile(x, y) - # 随机分配阵营(正/邪) - avatar.alignment = random.choice(list(Alignment)) avatars[avatar.id] = avatar # # —— 为演示添加少量示例关系 —— avatar_list = list(avatars.values()) diff --git a/static/config.yml b/static/config.yml index 55f6b30..e5e8d39 100644 --- a/static/config.yml +++ b/static/config.yml @@ -13,7 +13,7 @@ ai: max_decide_num: 3 game: - init_npc_num: 3 + init_npc_num: 6 npc_birth_rate_per_month: 0.001 df: diff --git a/static/game_configs/persona.csv b/static/game_configs/persona.csv index 923b693..5a3bd8d 100644 --- a/static/game_configs/persona.csv +++ b/static/game_configs/persona.csv @@ -1,26 +1,28 @@ -id,name,exclusion_ids,prompt,weight -,,和本persona互斥的persona的id,输入给LLM的prompt,采样时不同权重被采样到的概率 -1,理性,2;5,你是一个理性的人,你总是会用逻辑来思考问题,做事会谋定而后动。,1 -2,无常,1;9;20,你是一个无常的人,目标飘忽不定,不会长期坚持一个目标。,1 -3,怠惰,4;20,你是一个怠惰的人,你总是会拖延,不想努力,更热衷于享受人生。,1 -4,冒险,3;10,你是一个冒险的人,你总是会冒险,喜欢刺激,总想放手一搏。,1 -5,随性,1;20,你是一个随性的人,你总是会随机应变,性子到哪里了就是哪里,没有一定之规。,1 -6,贪财,,你是一个贪财的人,你对灵石和财富有着强烈的渴望。,1 -7,采药,,你是一个热爱采集的人,喜欢在山林中寻找各种奇花异草和灵药,对植物有着敏锐的直觉和深厚的兴趣。你认为大自然的恩赐需要用心去发现和珍惜。,1 -8,猎者,,你是一个热爱狩猎的人,享受在野外追踪猎物的刺激感,对各种动物的习性了如指掌。你相信通过狩猎能够磨练自己的意志和技能,获得更强大的力量。,1 -9,沉思,2,你是一个沉思的人,你总是会深思熟虑,思考问题比较有哲理。,1 -10,惜命,4;20,你是一个惜命的人,你总是会珍惜自己的生命,不会轻易冒险。,1 -11,友爱,13;14;15;12;20,你是一个友爱的人,你重视同伴与和谐,乐于助人,倾向通过协作与沟通化解矛盾。,1 -12,复仇,11;14,你是一个复仇心强的人,你绝不轻易放下仇怨,为了复仇愿意付出代价与时间。,1 -13,孤僻,11,你是一个孤僻的人,你喜欢独处,避免与人深交,更信赖自己的判断与行动。,1 -14,淡漠,11;12;15;20,你是一个淡漠的人,你情感克制,对外界冷静疏离,不轻易被他人或事件影响。,1 -15,好斗,11;14;10;17,你是一个好斗的人,你直面冲突,偏好以力量与对抗解决问题,越挫越勇。,1 -16,鲁莽,1;9;10,你是一个鲁莽的人,你行事冲动、少考虑后果,常凭直觉立刻行动。,1 -17,胆小,4;15;12;20,你是一个胆小的人,你谨小慎微,容易畏惧风险,倾向回避正面冲突。,1 -18,霸道,11;17,你是一个霸道的人,你行事强势,不讲道理,习惯以自己的利益为先,倾向多吃多占、压人一步,对他人的反对不以为意。,1 -19,修行痴迷,2;3;5,你是一个对修行极度痴迷的人,你将绝大多数时间用于修炼,厌恶与修行无关的社交与享乐。,1 -20,极端,11;14;2;5;3;10;17,你是一个极端的人,你仇视对立阵营,如果你是正义阵营,那么你极度正义;如果你是邪恶阵营,那么你极度邪恶。,1 -21,内向,22,你是一个内向的人,你更享受独处与自我思考,倾向回避不必要的社交与长谈。,1 -22,外向,13;14;21,你是一个外向的人,你乐于与人交流,主动结识伙伴,倾向接受对话和合作。,1 -23,刻薄,11;24,你是一个刻薄的人,你在对话中倾向以讽刺、嘲弄、挖苦的方式表达自己,更容易引发冲突与对立;你对他人的弱点与错误毫不留情,偏向以攻击性语言试探或压制对方;你通常不愿迅速进入友好关系,更可能在不顺利的交流后滋生敌意或结仇。,1 -24,热情,13;14;23,你是一个热情的人,你待人友好、积极回应,倾向用鼓励与赞美拉近距离;你更愿意主动展开善意的交流,乐于合作与分享;在大多数情况下你避免激化矛盾,更倾向于促成友好关系并保持和气。,1 +id,name,exclusion_ids,prompt,weight,condition +,,和本persona互斥的persona的id,输入给LLM的prompt,采样时不同权重被采样到的概率,选取条件(可用avatar字段/Alignment中文或英文等) +1,理性,2;5,你是一个理性的人,你总是会用逻辑来思考问题,做事会谋定而后动。,1, +2,无常,1;25;26;9,你是一个无常的人,目标飘忽不定,不会长期坚持一个目标。,1, +3,怠惰,4;25;26,你是一个怠惰的人,你总是会拖延,不想努力,更热衷于享受人生。,1, +4,冒险,3;10,你是一个冒险的人,你总是会冒险,喜欢刺激,总想放手一搏。,1, +5,随性,1;25;26,你是一个随性的人,你总是会随机应变,性子到哪里了就是哪里,没有一定之规。,1, +6,贪财,,你是一个贪财的人,你对灵石和财富有着强烈的渴望。,1, +7,采药,,你是一个热爱采集的人,喜欢在山林中寻找各种奇花异草和灵药,对植物有着敏锐的直觉和深厚的兴趣。你认为大自然的恩赐需要用心去发现和珍惜。,1, +8,猎者,,你是一个热爱狩猎的人,享受在野外追踪猎物的刺激感,对各种动物的习性了如指掌。你相信通过狩猎能够磨练自己的意志和技能,获得更强大的力量。,1, +9,沉思,2,你是一个沉思的人,你总是会深思熟虑,思考问题比较有哲理。,1, +10,惜命,4;25;26,你是一个惜命的人,你总是会珍惜自己的生命,不会轻易冒险。,1, +11,友爱,13;14;15;12;25;26,你是一个友爱的人,你重视同伴与和谐,乐于助人,倾向通过协作与沟通化解矛盾。,1, +12,复仇,11;14,你是一个复仇心强的人,你绝不轻易放下仇怨,为了复仇愿意付出代价与时间。,1, +13,孤僻,11,你是一个孤僻的人,你喜欢独处,避免与人深交,更信赖自己的判断与行动。,1, +14,淡漠,11;12;15;25;26,你是一个淡漠的人,你情感克制,对外界冷静疏离,不轻易被他人或事件影响。,1, +15,好斗,11;14;10;17,你是一个好斗的人,你直面冲突,偏好以力量与对抗解决问题,越挫越勇。,1, +16,鲁莽,1;9;10,你是一个鲁莽的人,你行事冲动、少考虑后果,常凭直觉立刻行动。,1, +17,胆小,4;15;12;25;26,你是一个胆小的人,你谨小慎微,容易畏惧风险,倾向回避正面冲突。,1, +18,霸道,11;17,你是一个霸道的人,你行事强势,不讲道理,习惯以自己的利益为先,倾向多吃多占、压人一步,对他人的反对不以为意。,1, +19,修行痴迷,2;3;5,你是一个对修行极度痴迷的人,你将绝大多数时间用于修炼,厌恶与修行无关的社交与享乐。,1, +20,极端,25;26;11;14;2;5;3;10;17,你是一个极端的人,你仇视对立阵营,如果你是正义阵营,那么你极度正义;如果你是邪恶阵营,那么你极度邪恶。,0.0001, +21,内向,22,你是一个内向的人,你更享受独处与自我思考,倾向回避不必要的社交与长谈。,1, +22,外向,13;14;21,你是一个外向的人,你乐于与人交流,主动结识伙伴,倾向接受对话和合作。,1, +23,刻薄,11;24,你是一个刻薄的人,你在对话中倾向以讽刺、嘲弄、挖苦的方式表达自己,更容易引发冲突与对立;你对他人的弱点与错误毫不留情,偏向以攻击性语言试探或压制对方;你通常不愿迅速进入友好关系,更可能在不顺利的交流后滋生敌意或结仇。,1, +24,热情,13;14;23,你是一个热情的人,你待人友好、积极回应,倾向用鼓励与赞美拉近距离;你更愿意主动展开善意的交流,乐于合作与分享;在大多数情况下你避免激化矛盾,更倾向于促成友好关系并保持和气。,1, +25,极端正义,20;26,你是一个极端正义的人,你对邪恶深恶痛绝,对正义的理想抱有近乎偏执的追求。,1,avatar.alignment == "正" +26,极端邪恶,20;25,你是一个极端邪恶的人,你推崇权力与恐惧,为达目的不择手段,对善良嗤之以鼻。,1,avatar.alignment == "邪"