From e17c9e35a9958b3e6d615a97ad4b56854c0fc50a Mon Sep 17 00:00:00 2001 From: bridge Date: Sat, 4 Oct 2025 22:08:10 +0800 Subject: [PATCH] add observe range --- src/classes/action/talk.py | 37 +++++---------- src/classes/action/targeting_mixin.py | 4 ++ src/classes/ai.py | 6 +-- src/classes/avatar.py | 4 +- src/classes/avatar_manager.py | 8 ++++ src/classes/mutual_action/mutual_action.py | 8 ++-- src/classes/observe.py | 55 ++++++++++++++++++++++ src/classes/world.py | 5 +- 8 files changed, 93 insertions(+), 34 deletions(-) create mode 100644 src/classes/observe.py diff --git a/src/classes/action/talk.py b/src/classes/action/talk.py index dafa34c..2cbd84d 100644 --- a/src/classes/action/talk.py +++ b/src/classes/action/talk.py @@ -8,48 +8,37 @@ from src.classes.event import Event class Talk(InstantAction): """ - 攀谈:尝试与同区域内的某个NPC进行交谈。 - - can_start:同区域内存在其他NPC - - 发起后:随机寻找“同一tile”的NPC,若不存在则本次无法攀谈 - - 若找到,则进入 MutualAction: Conversation(允许建立关系) + 攀谈:尝试与感知范围内的某个NPC进行交谈。 + - can_start:感知范围内存在其他NPC + - 发起后:从感知范围内随机选择一个目标,进入 MutualAction: Conversation(允许建立关系) """ - COMMENT = "与同区域内的NPC发起攀谈,若同一tile有人则进入交谈" - DOABLES_REQUIREMENTS = "同区域内存在其他NPC" + COMMENT = "与感知范围内的NPC发起攀谈" + 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_observed_others(self) -> list["Avatar"]: + return self.world.avatar_manager.get_observable_avatars(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 + # 不再限定必须同一 tile,由感知范围统一约束 def _execute(self) -> None: # Talk 本身不做长期效果,主要在 step 中驱动 Conversation return def can_start(self) -> bool: - # 是否同区域存在其他NPC(用于展示在动作空间) - return len(self._get_same_region_others()) > 0 + # 感知范围内是否存在其他NPC(用于展示在动作空间) + return len(self._get_observed_others()) > 0 def start(self) -> Event: - self.same_region_others = self._get_same_region_others() + self.observed_others = self._get_observed_others() # 记录开始事件 - return Event(self.world.month_stamp, f"{self.avatar.name} 尝试与同区域的他人攀谈") + return Event(self.world.month_stamp, f"{self.avatar.name} 尝试与感知范围内的他人攀谈") def step(self) -> ActionResult: import random - target = random.choice(self.same_region_others) + target = random.choice(self.observed_others) # 进入交谈:由概率决定本次是否允许建立关系 from src.classes.mutual_action import Conversation diff --git a/src/classes/action/targeting_mixin.py b/src/classes/action/targeting_mixin.py index ec3af4c..e7d26db 100644 --- a/src/classes/action/targeting_mixin.py +++ b/src/classes/action/targeting_mixin.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import Optional, Iterable from src.classes.tile import get_avatar_distance +from src.classes.observe import get_observable_avatars class TargetingMixin: @@ -35,6 +36,9 @@ class TargetingMixin: def distance_between(self, a: "Avatar", b: "Avatar") -> int: return get_avatar_distance(a, b) + def avatars_in_observation_range(self, avatar: "Avatar") -> list["Avatar"]: + return self.world.avatar_manager.get_observable_avatars(avatar) + def preempt_avatar(self, avatar: "Avatar") -> None: """抢占目标:清空其计划并中断当前动作。""" avatar.clear_plans() diff --git a/src/classes/ai.py b/src/classes/ai.py index 3f46fec..6bcaa7e 100644 --- a/src/classes/ai.py +++ b/src/classes/ai.py @@ -119,11 +119,11 @@ class LLMAI(AI): 异步决策逻辑:通过LLM决定执行什么动作和参数 """ global_info = world.get_info() - # 在提示中包含与该角色处于同一区域的其他角色 + # 在提示中包含处于角色观测范围内的其他角色 avatar_infos = {} for avatar in avatars_to_decide: - co_region = world.get_avatars_in_same_region(avatar) - avatar_infos[avatar.name] = avatar.get_prompt_info(co_region) + observed = world.get_observable_avatars(avatar) + avatar_infos[avatar.name] = avatar.get_prompt_info(observed) general_action_infos = ACTION_INFOS_STR info = { "avatar_infos": avatar_infos, diff --git a/src/classes/avatar.py b/src/classes/avatar.py index e41c6fb..5609368 100644 --- a/src/classes/avatar.py +++ b/src/classes/avatar.py @@ -379,13 +379,13 @@ class Avatar: else: items_info = "物品持有情况:无" - # 同区域角色(可选) + # 观测范围内角色(沿用参数名保持兼容) co_region_info = "" if co_region_avatars: entries: list[str] = [] for other in co_region_avatars[:8]: entries.append(f"{other.name}(境界:{other.cultivation_progress.get_simple_info()})") - co_region_info = "\n同区域角色:" + (",".join(entries) if entries else "无") + co_region_info = "\n观测范围内角色:" + (",".join(entries) if entries else "无") # 关系摘要 relations_summary = self._get_relations_summary_str() diff --git a/src/classes/avatar_manager.py b/src/classes/avatar_manager.py index 3fa5eae..a0663fa 100644 --- a/src/classes/avatar_manager.py +++ b/src/classes/avatar_manager.py @@ -5,6 +5,7 @@ from typing import Dict, List, TYPE_CHECKING if TYPE_CHECKING: from src.classes.avatar import Avatar +from src.classes.observe import get_observable_avatars @dataclass class AvatarManager: @@ -25,6 +26,13 @@ class AvatarManager: same_region.append(other) return same_region + def get_observable_avatars(self, avatar: "Avatar") -> List["Avatar"]: + """ + 返回处于 avatar 感知范围内的其他角色列表(不含自己)。 + 基于曼哈顿距离与境界映射的感知半径过滤。 + """ + return get_observable_avatars(avatar, self.avatars.values()) + def remove_avatar(self, avatar_id: str) -> None: """ 从管理器中删除一个 avatar,并清理所有与其相关的双向关系。 diff --git a/src/classes/mutual_action/mutual_action.py b/src/classes/mutual_action/mutual_action.py index a9095e9..bbf64ae 100644 --- a/src/classes/mutual_action/mutual_action.py +++ b/src/classes/mutual_action/mutual_action.py @@ -30,7 +30,7 @@ class MutualAction(DefineAction, LLMAction, TargetingMixin): ACTION_NAME: str = "MutualAction" COMMENT: str = "" - DOABLES_REQUIREMENTS: str = "同区域内可互动" + DOABLES_REQUIREMENTS: str = "感知范围内可互动" PARAMS: dict = {"target_avatar": "Avatar"} FEEDBACK_ACTIONS: list[str] = [] @@ -137,15 +137,15 @@ class MutualAction(DefineAction, LLMAction, TargetingMixin): # 实现 ActualActionMixin 接口 def can_start(self, target_avatar: "Avatar|str|None" = None) -> bool: """ - 检查互动动作能否启动:两个角色距离必须小于等于2 + 检查互动动作能否启动:目标需在发起者的感知范围内。 """ if target_avatar is None: return False target = self._get_target_avatar(target_avatar) if target is None: return False - distance = get_avatar_distance(self.avatar, target) - return distance <= 3 + from src.classes.observe import is_within_observation + return is_within_observation(self.avatar, target) def start(self, target_avatar: "Avatar|str") -> Event: """ diff --git a/src/classes/observe.py b/src/classes/observe.py new file mode 100644 index 0000000..bcf3e1f --- /dev/null +++ b/src/classes/observe.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +from typing import Iterable, List, TYPE_CHECKING + +from src.classes.cultivation import Realm +from src.classes.tile import get_avatar_distance + +if TYPE_CHECKING: + from src.classes.avatar import Avatar + + +_OBSERVATION_RADIUS_BY_REALM: dict[Realm, int] = { + Realm.Qi_Refinement: 2, # 练气 + Realm.Foundation_Establishment: 3, # 筑基 + Realm.Core_Formation: 4, # 金丹 + Realm.Nascent_Soul: 5, # 元婴 +} + + +def get_observation_radius_by_realm(realm: Realm) -> int: + """ + 根据境界返回感知半径(基于曼哈顿距离)。 + """ + return _OBSERVATION_RADIUS_BY_REALM.get(realm, 2) + + +def get_avatar_observation_radius(avatar: "Avatar") -> int: + """ + 获取角色的感知半径。 + """ + return get_observation_radius_by_realm(avatar.cultivation_progress.realm) + + +def is_within_observation(initiator: "Avatar", other: "Avatar") -> bool: + """ + 判断 other 是否处于 initiator 的感知范围内: + 汉明距离(曼哈顿距离) <= initiator 的感知半径。 + """ + return get_avatar_distance(initiator, other) <= get_avatar_observation_radius(initiator) + + +def get_observable_avatars(initiator: "Avatar", avatars: Iterable["Avatar"]) -> List["Avatar"]: + """ + 从给定集合中过滤出处于 initiator 感知范围内的角色(不包含 initiator 本人)。 + 算法:线性扫描 O(N),与现有管理器遍历复杂度一致。 + """ + result: list["Avatar"] = [] + for v in avatars: + if v is initiator: + continue + if is_within_observation(initiator, v): + result.append(v) + return result + + diff --git a/src/classes/world.py b/src/classes/world.py index 13c8cb4..ad03657 100644 --- a/src/classes/world.py +++ b/src/classes/world.py @@ -22,4 +22,7 @@ class World(): return info def get_avatars_in_same_region(self, avatar: "Avatar"): - return self.avatar_manager.get_avatars_in_same_region(avatar) \ No newline at end of file + return self.avatar_manager.get_avatars_in_same_region(avatar) + + def get_observable_avatars(self, avatar: "Avatar"): + return self.avatar_manager.get_observable_avatars(avatar) \ No newline at end of file