add observe range

This commit is contained in:
bridge
2025-10-04 22:08:10 +08:00
parent d50f3ac70d
commit e17c9e35a9
8 changed files with 93 additions and 34 deletions

View File

@@ -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

View File

@@ -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()

View File

@@ -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,

View File

@@ -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()

View File

@@ -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并清理所有与其相关的双向关系。

View File

@@ -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:
"""

55
src/classes/observe.py Normal file
View File

@@ -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

View File

@@ -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)
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)