add talk and conversation
This commit is contained in:
@@ -12,6 +12,7 @@ from src.classes.item import Item, items_by_name
|
||||
from src.classes.prices import prices
|
||||
from src.classes.hp_and_mp import HP_MAX_BY_REALM, MP_MAX_BY_REALM
|
||||
from src.classes.battle import decide_battle
|
||||
from src.utils.config import CONFIG
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.classes.avatar import Avatar
|
||||
@@ -753,7 +754,7 @@ class HelpMortals(DefineAction, ActualActionMixin):
|
||||
if self.avatar.alignment != Alignment.RIGHTEOUS:
|
||||
return False
|
||||
cost = self.COST
|
||||
return getattr(self.avatar.magic_stone, "value", 0) >= cost
|
||||
return self.avatar.magic_stone >= cost
|
||||
|
||||
def start(self) -> Event:
|
||||
return Event(self.world.month_stamp, f"{self.avatar.name} 在城镇开始帮助凡人")
|
||||
@@ -764,3 +765,70 @@ class HelpMortals(DefineAction, ActualActionMixin):
|
||||
|
||||
def finish(self) -> list[Event]:
|
||||
return []
|
||||
|
||||
|
||||
class Talk(DefineAction, ActualActionMixin):
|
||||
"""
|
||||
攀谈:尝试与同区域内的某个NPC进行交谈。
|
||||
- can_start:同区域内存在其他NPC
|
||||
- 发起后:随机寻找“同一tile”的NPC,若不存在则本次无法攀谈
|
||||
- 若找到,则进入 MutualAction: Conversation(允许建立关系)
|
||||
"""
|
||||
|
||||
COMMENT = "与同区域内的NPC发起攀谈,若同一tile有人则进入交谈"
|
||||
DOABLES_REQUIREMENTS = "同区域内存在其他NPC"
|
||||
PARAMS = {}
|
||||
|
||||
def _get_same_region_others(self) -> list["Avatar"]:
|
||||
return self.world.avatar_manager.get_avatars_in_same_region(self.avatar)
|
||||
|
||||
def _get_same_tile_others(self) -> list["Avatar"]:
|
||||
same_tile: list["Avatar"] = []
|
||||
my_tile = self.avatar.tile
|
||||
if my_tile is None:
|
||||
return []
|
||||
for v in self.world.avatar_manager.avatars.values():
|
||||
if v is self.avatar or v.tile is None:
|
||||
continue
|
||||
if v.tile == my_tile:
|
||||
same_tile.append(v)
|
||||
return same_tile
|
||||
|
||||
def _execute(self) -> None:
|
||||
# Talk 本身不做长期效果,主要在 step 中驱动 Conversation
|
||||
return
|
||||
|
||||
def can_start(self) -> bool:
|
||||
# 是否同区域存在其他NPC(用于展示在动作空间)
|
||||
return len(self._get_same_region_others()) > 0
|
||||
|
||||
def start(self) -> Event:
|
||||
# 记录开始事件
|
||||
return Event(self.world.month_stamp, f"{self.avatar.name} 尝试与同区域的他人攀谈")
|
||||
|
||||
def step(self) -> tuple[StepStatus, list[Event]]:
|
||||
# 先找同tile对象
|
||||
same_tile_others = self._get_same_tile_others()
|
||||
if not same_tile_others:
|
||||
# 无同tile对象,本次作罢
|
||||
fail_event = Event(self.world.month_stamp, f"{self.avatar.name} 未在同一位置找到可攀谈之人")
|
||||
self.avatar.add_event(fail_event)
|
||||
return StepStatus.COMPLETED, []
|
||||
|
||||
import random
|
||||
target = random.choice(same_tile_others)
|
||||
|
||||
# 进入交谈:由概率决定本次是否允许建立关系
|
||||
from src.classes.mutual_action import Conversation
|
||||
# 由配置决定本次是否有“有机会进入关系”标记
|
||||
prob = float(getattr(CONFIG.social, "talk_into_relation_probability", 0.0))
|
||||
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 StepStatus.COMPLETED, []
|
||||
|
||||
def finish(self) -> list[Event]:
|
||||
return []
|
||||
|
||||
@@ -15,12 +15,14 @@ from src.classes.action import (
|
||||
Battle,
|
||||
PlunderMortals,
|
||||
HelpMortals,
|
||||
Talk,
|
||||
)
|
||||
from src.classes.mutual_action import (
|
||||
DriveAway,
|
||||
Attack,
|
||||
MoveAwayFromAvatar,
|
||||
MoveAwayFromRegion,
|
||||
Conversation,
|
||||
)
|
||||
|
||||
|
||||
@@ -37,11 +39,13 @@ ALL_ACTION_CLASSES = [
|
||||
Sold,
|
||||
PlunderMortals,
|
||||
HelpMortals,
|
||||
Talk,
|
||||
# 互动相关动作(实际执行的反馈动作也纳入)
|
||||
DriveAway,
|
||||
Attack,
|
||||
MoveAwayFromAvatar,
|
||||
MoveAwayFromRegion,
|
||||
Conversation,
|
||||
]
|
||||
|
||||
ALL_ACTUAL_ACTION_CLASSES = [
|
||||
@@ -55,8 +59,10 @@ ALL_ACTUAL_ACTION_CLASSES = [
|
||||
Sold,
|
||||
PlunderMortals,
|
||||
HelpMortals,
|
||||
Talk,
|
||||
DriveAway,
|
||||
Attack,
|
||||
Conversation,
|
||||
]
|
||||
|
||||
ALL_ACTION_NAMES = [action.__name__ for action in ALL_ACTION_CLASSES]
|
||||
|
||||
@@ -11,6 +11,7 @@ from src.classes.event import Event
|
||||
from src.utils.llm import get_prompt_and_call_llm
|
||||
from src.utils.config import CONFIG
|
||||
from src.classes.action import long_action
|
||||
from src.classes.relation import relation_display_names, Relation, get_possible_post_relations
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.classes.avatar import Avatar
|
||||
@@ -43,14 +44,16 @@ class MutualAction(DefineAction, LLMAction):
|
||||
avatar_name_1: self.avatar.cultivation_progress.get_simple_info(), # avatar1只放境界信息
|
||||
avatar_name_2: target_avatar.get_prompt_info([]), # avatar2放全量信息
|
||||
}
|
||||
feedback_actions = getattr(self, "FEEDBACK_ACTIONS", [])
|
||||
feedback_actions = self.FEEDBACK_ACTIONS
|
||||
comment = self.COMMENT
|
||||
action_name = self.ACTION_NAME
|
||||
return {
|
||||
"avatar_infos": avatar_infos,
|
||||
"avatar_name_1": avatar_name_1,
|
||||
"avatar_name_2": avatar_name_2,
|
||||
"action_name": getattr(self, "ACTION_NAME", self.name),
|
||||
"action_info": getattr(self, "COMMENT", ""),
|
||||
"FEEDBACK_ACTIONS": feedback_actions,
|
||||
"action_name": action_name,
|
||||
"action_info": comment,
|
||||
"feedback_actions": feedback_actions,
|
||||
}
|
||||
|
||||
def _call_llm_feedback(self, infos: dict) -> dict:
|
||||
@@ -256,4 +259,102 @@ class MoveAwayFromRegion(DefineAction, ActualActionMixin):
|
||||
if self.world.map.is_in_bounds(nx, ny):
|
||||
self.avatar.pos_x = nx
|
||||
self.avatar.pos_y = ny
|
||||
self.avatar.tile = self.world.map.get_tile(nx, ny)
|
||||
self.avatar.tile = self.world.map.get_tile(nx, ny)
|
||||
|
||||
|
||||
class Conversation(MutualAction, ActualActionMixin):
|
||||
"""交谈:两名角色在同一区域进行交流。
|
||||
|
||||
- 可由“攀谈”触发,或直接发起
|
||||
- 仅当双方处于同一 Region 时可启动
|
||||
- 当 can_into_relation=True 且 LLM 决策返回 into_relation 时,根据返回建立关系
|
||||
- 会将对话内容写入事件系统
|
||||
"""
|
||||
|
||||
ACTION_NAME = "交谈"
|
||||
COMMENT = "两人需在同一地区,进行一段交流对话"
|
||||
DOABLES_REQUIREMENTS = "与目标处于同一区域"
|
||||
PARAMS = {"target_avatar": "AvatarName"}
|
||||
FEEDBACK_ACTIONS: list[str] = ["Talk", "Reject"]
|
||||
|
||||
def _get_template_path(self) -> Path:
|
||||
# 使用 talk.txt 模板,以获取是否接受与对话内容
|
||||
return CONFIG.paths.templates / "talk.txt"
|
||||
|
||||
def _build_prompt_infos(self, target_avatar: "Avatar", *, can_into_relation: bool) -> dict:
|
||||
avatar_name_1 = self.avatar.name
|
||||
avatar_name_2 = target_avatar.name
|
||||
# 目标的 get_prompt_info 已含 personas、关系等,信息更充分
|
||||
avatar_infos = {
|
||||
avatar_name_1: self.avatar.get_prompt_info([]),
|
||||
avatar_name_2: target_avatar.get_prompt_info([]),
|
||||
}
|
||||
# 可能的后天关系(转中文名,给模板阅读)
|
||||
possible_relations = [relation_display_names[r] for r in get_possible_post_relations(self.avatar, target_avatar)]
|
||||
return {
|
||||
"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,
|
||||
}
|
||||
|
||||
# 关系解析由 Relation 提供类方法,仅接受中文关系名,无法解析则跳过
|
||||
|
||||
def can_start(self, target_avatar: "Avatar|str|None" = None, **kwargs) -> bool:
|
||||
if target_avatar is None:
|
||||
return False
|
||||
target = self._get_target_avatar(target_avatar)
|
||||
if target is None or target.tile is None or self.avatar.tile is None:
|
||||
return False
|
||||
return target.tile.region == self.avatar.tile.region
|
||||
|
||||
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)
|
||||
event = Event(self.world.month_stamp, f"{self.avatar.name} 与 {target_name} 开始交谈")
|
||||
# 写入历史即可,内容事件稍后生成
|
||||
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) -> tuple[StepStatus, list[Event]]:
|
||||
target = self._get_target_avatar(target_avatar)
|
||||
if target is None:
|
||||
return StepStatus.COMPLETED, []
|
||||
|
||||
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()
|
||||
|
||||
target.thinking = thinking
|
||||
|
||||
# 拒绝则只记录反馈
|
||||
if feedback and feedback != "Talk":
|
||||
feedback_event = Event(self.world.month_stamp, f"{target.name} 拒绝与 {self.avatar.name} 交谈")
|
||||
self._add_event_pair(feedback_event, initiator=self.avatar, target=target)
|
||||
return StepStatus.COMPLETED, []
|
||||
|
||||
# 接受并记录对话内容
|
||||
if talk_content:
|
||||
content_event = Event(self.world.month_stamp, f"{self.avatar.name} 与 {target.name} 的交谈:{talk_content}")
|
||||
# 进入侧栏一次,并写入双方历史
|
||||
self._add_event_pair(content_event, initiator=self.avatar, target=target)
|
||||
|
||||
# 仅当 can_into_relation=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))}")
|
||||
self._add_event_pair(set_event, initiator=self.avatar, target=target)
|
||||
|
||||
return StepStatus.COMPLETED, []
|
||||
|
||||
def finish(self, target_avatar: "Avatar|str", **kwargs) -> list[Event]:
|
||||
return []
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, List
|
||||
|
||||
|
||||
class Relation(Enum):
|
||||
@@ -20,6 +21,19 @@ class Relation(Enum):
|
||||
def __str__(self) -> str:
|
||||
return relation_display_names.get(self, self.value)
|
||||
|
||||
@classmethod
|
||||
def from_chinese(cls, name_cn: str) -> "Relation|None":
|
||||
"""
|
||||
依据中文显示名解析关系;无法解析返回 None。
|
||||
"""
|
||||
if not name_cn:
|
||||
return None
|
||||
s = str(name_cn).strip()
|
||||
for rel, cn in relation_display_names.items():
|
||||
if s == cn:
|
||||
return rel
|
||||
return None
|
||||
|
||||
|
||||
relation_display_names = {
|
||||
# 血缘(先天)
|
||||
@@ -70,3 +84,54 @@ def get_reciprocal(relation: Relation) -> Relation:
|
||||
"""
|
||||
return RECIPROCAL_RELATION.get(relation, relation)
|
||||
|
||||
|
||||
# ——— 新增:评估两名角色可能新增的后天关系 ———
|
||||
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
|
||||
|
||||
|
||||
@@ -13,11 +13,16 @@ ai:
|
||||
max_decide_num: 3
|
||||
|
||||
game:
|
||||
init_npc_num: 2
|
||||
init_npc_num: 6
|
||||
npc_birth_rate_per_month: 0.001
|
||||
|
||||
df:
|
||||
ids_separator: ";"
|
||||
|
||||
avatar:
|
||||
persona_num: 3
|
||||
persona_num: 3
|
||||
|
||||
# 社交相关配置
|
||||
social:
|
||||
# 攀谈触发关系的基础概率(仅“攀谈”驱动的交谈会尝试;直接“交谈”不会)
|
||||
talk_into_relation_probability: 0.1
|
||||
@@ -20,3 +20,5 @@ id,name,exclusion_ids,prompt
|
||||
18,霸道,11;17,你是一个霸道的人,你行事强势,不讲道理,习惯以自己的利益为先,倾向多吃多占、压人一步,对他人的反对不以为意。
|
||||
19,修行痴迷,2;3;5,你是一个对修行极度痴迷的人,你将绝大多数时间用于修炼,厌恶与修行无关的社交与享乐。
|
||||
20,极端,11;14;2;5;3;10;17,你是一个极端的人,你仇视对立阵营,如果你是正义阵营,那么你极度正义;如果你是邪恶阵营,那么你极度邪恶。
|
||||
21,外向,13;14;22,你是一个外向的人,你乐于与人交流,主动结识伙伴,倾向接受对话和合作。
|
||||
22,内向,21,你是一个内向的人,你更享受独处与自我思考,倾向回避不必要的社交与长谈。
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
你是一个决策者,这是一个修仙的仙侠世界,你负责来决定一些NPC的下一步行为。
|
||||
你是一个决策者,这是一个仙侠世界,你负责来决定一些NPC的下一步行为。
|
||||
{global_info}
|
||||
你需要进行决策的NPC的dict[AvatarName, info]为
|
||||
{avatar_infos}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
你是一个决策者,这是一个修仙的仙侠世界,你负责来决定两个NPC的下一步行为。
|
||||
你是一个决策者,这是一个仙侠世界,你负责来决定两个NPC的下一步行为。
|
||||
|
||||
你需要进行决策的NPC的dict[AvatarName, info]为
|
||||
{avatar_infos}
|
||||
|
||||
20
static/templates/talk.txt
Normal file
20
static/templates/talk.txt
Normal file
@@ -0,0 +1,20 @@
|
||||
你是一个决策者,这是一个仙侠世界,你负责来决定一个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}
|
||||
|
||||
注意,只返回json格式的结果。
|
||||
只返回{avatar_name_2}的行动,格式为:
|
||||
{{
|
||||
{avatar_name_2}: {{
|
||||
"thinking": ..., // 简单思考应该怎么决策
|
||||
"feedback": ... // 面对{avatar_name_1}的行为的合法feedback action name
|
||||
"talk_content": ... // 如果返回的action为Talk,则输出对话的大概内容。为Reject则返回空str。
|
||||
"into_relation": ... // 如果你认为可以让两者产生某种身份关系,则返回。注意这是{avatar_name_2}相对于{avatar_name_1}的身份。
|
||||
}}
|
||||
}}
|
||||
Reference in New Issue
Block a user