From 8b705c080698e7035845b972e04f2ac6b22eb8c1 Mon Sep 17 00:00:00 2001 From: bridge Date: Tue, 16 Sep 2025 23:50:41 +0800 Subject: [PATCH] add personas --- src/classes/avatar.py | 25 +++++++++++---- src/classes/persona.py | 57 +++++++++++++++++++++++++++++++-- src/front/rendering.py | 3 +- static/config.yml | 5 ++- static/game_configs/persona.csv | 24 +++++++------- 5 files changed, 91 insertions(+), 23 deletions(-) diff --git a/src/classes/avatar.py b/src/classes/avatar.py index 5a7a88a..279d557 100644 --- a/src/classes/avatar.py +++ b/src/classes/avatar.py @@ -1,7 +1,7 @@ import random from dataclasses import dataclass, field from enum import Enum -from typing import Optional +from typing import Optional, List import json from src.classes.calendar import MonthStamp @@ -15,13 +15,15 @@ from src.classes.age import Age from src.classes.event import NULL_EVENT, Event from src.classes.typings import ACTION_NAME, ACTION_PARAMS, ACTION_PAIR, ACTION_NAME_PARAMS_PAIRS, ACTION_NAME_PARAMS_PAIR -from src.classes.persona import Persona, personas_by_id +from src.classes.persona import Persona, personas_by_id, get_random_compatible_personas from src.classes.item import Item from src.classes.magic_stone import MagicStone from src.classes.hp_and_mp import HP, MP, HP_MAX_BY_REALM, MP_MAX_BY_REALM from src.utils.id_generator import get_avatar_id from src.utils.config import CONFIG +persona_num = CONFIG.avatar.persona_num + class Gender(Enum): MALE = "male" FEMALE = "female" @@ -55,7 +57,7 @@ class Avatar: tile: Optional[Tile] = None root: Root = field(default_factory=lambda: random.choice(list(Root))) - persona: Persona = field(default_factory=lambda: random.choice(list(personas_by_id.values()))) + personas: List[Persona] = field(default_factory=list) cur_action_pair: Optional[ACTION_PAIR] = None history_action_pairs: list[ACTION_PAIR] = field(default_factory=list) next_actions: ACTION_NAME_PARAMS_PAIRS = field(default_factory=list) @@ -77,6 +79,10 @@ class Avatar: max_mp = MP_MAX_BY_REALM.get(self.cultivation_progress.realm, 100) self.hp = HP(max_hp, max_hp) self.mp = MP(max_mp, max_mp) + + # 如果personas列表为空,则随机分配两个不互斥的persona + if not self.personas: + self.personas = get_random_compatible_personas(persona_num) def __hash__(self) -> int: return hash(self.id) @@ -86,7 +92,8 @@ class Avatar: 获取avatar的详细信息 尽量多打一些,因为会用来给LLM进行决策 """ - return f"Avatar(id={self.id}, 性别={self.gender}, 年龄={self.age}, name={self.name}, 区域={self.tile.region.name}, 灵根={self.root.value}, 境界={self.cultivation_progress}, HP={self.hp}, MP={self.mp})" + personas_str = ", ".join([persona.name for persona in self.personas]) + return f"Avatar(id={self.id}, 性别={self.gender}, 年龄={self.age}, name={self.name}, 区域={self.tile.region.name}, 灵根={self.root.value}, 境界={self.cultivation_progress}, HP={self.hp}, MP={self.mp}, 个性={personas_str})" def __str__(self) -> str: return self.get_info() @@ -345,9 +352,14 @@ class Avatar: 获取角色提示词信息 """ info = self.get_info() - persona = self.persona.prompt action_space = self.get_action_space_str() + # 构建personas的提示词信息 + personas_prompts = [] + for i, persona in enumerate(self.personas, 1): + personas_prompts.append(f"其个性{i}:{persona.prompt}") + personas_info = "\n".join(personas_prompts) + # 添加灵石信息 magic_stone_info = f"灵石持有情况:{str(self.magic_stone)}" @@ -357,7 +369,8 @@ class Avatar: else: items_info = "物品持有情况:无" - return f"{info}\n其个性为:{persona}\n{magic_stone_info}\n{items_info}\n决策时需参考这个角色的个性。\n该角色的目前暂时的合法动作为:{action_space}" + personas_count = len(self.personas) + return f"{info}\n{personas_info}\n{magic_stone_info}\n{items_info}\n决策时需参考这个角色的{personas_count}个个性特点。\n该角色的目前暂时的合法动作为:{action_space}" @property def move_step_length(self) -> int: diff --git a/src/classes/persona.py b/src/classes/persona.py index bb40f0c..9b90433 100644 --- a/src/classes/persona.py +++ b/src/classes/persona.py @@ -1,6 +1,11 @@ +import random from dataclasses import dataclass +from typing import List from src.utils.df import game_configs +from src.utils.config import CONFIG + +ids_separator = CONFIG.df.ids_separator @dataclass class Persona: @@ -9,7 +14,8 @@ class Persona: """ id: int name: str - prompt: str + prompt: str + exclusion_ids: List[int] def _load_personas() -> tuple[dict[int, Persona], dict[str, Persona]]: """从配表加载persona数据""" @@ -18,10 +24,17 @@ def _load_personas() -> tuple[dict[int, Persona], dict[str, Persona]]: persona_df = game_configs["persona"] for _, row in persona_df.iterrows(): + # 解析exclusion_ids字符串,转换为int列表 + exclusion_ids_str = str(row["exclusion_ids"]) if str(row["exclusion_ids"]) != "nan" else "" + exclusion_ids = [] + if exclusion_ids_str: + exclusion_ids = [int(x.strip()) for x in exclusion_ids_str.split(ids_separator) if x.strip()] + persona = Persona( id=int(row["id"]), name=str(row["name"]), - prompt=str(row["prompt"]) + prompt=str(row["prompt"]), + exclusion_ids=exclusion_ids ) personas_by_id[persona.id] = persona personas_by_name[persona.name] = persona @@ -31,6 +44,44 @@ 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]: + """ + 随机选择指定数量的互相不冲突的persona + + Args: + num_personas: 需要选择的persona数量,默认为2 + + Returns: + List[Persona]: 互相不冲突的persona列表 + + Raises: + ValueError: 如果无法找到足够数量的兼容persona + """ + all_persona_ids = set(personas_by_id.keys()) - + selected_personas = [] + available_ids = all_persona_ids.copy() + + for i in range(num_personas): + if not available_ids: + raise ValueError(f"只能找到{i}个兼容的persona,无法满足需要的{num_personas}个") + + # 从可用列表中随机选择一个 + selected_id = random.choice(list(available_ids)) + selected_persona = personas_by_id[selected_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) + + return selected_personas diff --git a/src/front/rendering.py b/src/front/rendering.py index 50ad848..6b8df61 100644 --- a/src/front/rendering.py +++ b/src/front/rendering.py @@ -129,9 +129,10 @@ def draw_tooltip_for_avatar(pygame_mod, screen, colors, font, avatar: Avatar): f"HP: {avatar.hp}", f"MP: {avatar.mp}", f"灵根: {avatar.root.value}", - f"个性: {avatar.persona.name}", + f"个性: {', '.join([persona.name for persona in avatar.personas])}", f"位置: ({avatar.pos_x}, {avatar.pos_y})", ] + lines.append(f"灵石: {str(avatar.magic_stone)}") if avatar.items: lines.append("物品:") diff --git a/static/config.yml b/static/config.yml index 4be7596..bd21cce 100644 --- a/static/config.yml +++ b/static/config.yml @@ -16,4 +16,7 @@ game: npc_birth_rate_per_month: 0.001 df: - ids_separator: "," \ No newline at end of file + ids_separator: ";" + +avatar: + persona_num: 2 \ No newline at end of file diff --git a/static/game_configs/persona.csv b/static/game_configs/persona.csv index 6313348..a78d346 100644 --- a/static/game_configs/persona.csv +++ b/static/game_configs/persona.csv @@ -1,12 +1,12 @@ -id,name,prompt -,, -1,理性,你是一个理性的人,你总是会用逻辑来思考问题,做事会谋定而后动。 -2,无常,你是一个无常的人,目标飘忽不定,不会长期坚持一个目标。 -3,怠惰,你是一个怠惰的人,你总是会拖延,不想努力,更热衷于享受人生。 -4,冒险,你是一个冒险的人,你总是会冒险,喜欢刺激,总想放手一搏。 -5,随性,你是一个随性的人,你总是会随机应变,性子到哪里了就是哪里,没有一定之规。 -6,贪财,你是一个贪财的人,你对灵石和财富有着强烈的渴望。 -7,采药,你是一个热爱采集的人,喜欢在山林中寻找各种奇花异草和灵药,对植物有着敏锐的直觉和深厚的兴趣。你认为大自然的恩赐需要用心去发现和珍惜。 -8,猎者,你是一个热爱狩猎的人,享受在野外追踪猎物的刺激感,对各种动物的习性了如指掌。你相信通过狩猎能够磨练自己的意志和技能,获得更强大的力量。 -9,爱财,你嗜财如命,对灵石和财富有着强烈的渴望。 -10,沉思,你是一个沉思的人,你总是会深思熟虑,思考问题比较有哲理。 +id,name,exclusion_ids,prompt +,,和本persona互斥的persona的id,输入给LLM的prompt +1,理性,2;5,你是一个理性的人,你总是会用逻辑来思考问题,做事会谋定而后动。 +2,无常,1;9,你是一个无常的人,目标飘忽不定,不会长期坚持一个目标。 +3,怠惰,4,你是一个怠惰的人,你总是会拖延,不想努力,更热衷于享受人生。 +4,冒险,3;10,你是一个冒险的人,你总是会冒险,喜欢刺激,总想放手一搏。 +5,随性,1,你是一个随性的人,你总是会随机应变,性子到哪里了就是哪里,没有一定之规。 +6,贪财,,你是一个贪财的人,你对灵石和财富有着强烈的渴望。 +7,采药,,你是一个热爱采集的人,喜欢在山林中寻找各种奇花异草和灵药,对植物有着敏锐的直觉和深厚的兴趣。你认为大自然的恩赐需要用心去发现和珍惜。 +8,猎者,,你是一个热爱狩猎的人,享受在野外追踪猎物的刺激感,对各种动物的习性了如指掌。你相信通过狩猎能够磨练自己的意志和技能,获得更强大的力量。 +9,沉思,2,你是一个沉思的人,你总是会深思熟虑,思考问题比较有哲理。 +10,惜命,4,你是一个惜命的人,你总是会珍惜自己的生命,不会轻易冒险。