decide only one avatar each time
This commit is contained in:
@@ -66,22 +66,33 @@ class LLMAI(AI):
|
||||
async def _decide(self, world: World, avatars_to_decide: list[Avatar]) -> dict[Avatar, tuple[ACTION_NAME_PARAMS_PAIRS, str, str]]:
|
||||
"""
|
||||
异步决策逻辑:通过LLM决定执行什么动作和参数
|
||||
改动:支持每个角色仅获取其已知区域的世界信息,并发调用 LLM。
|
||||
"""
|
||||
world_info = world.get_info()
|
||||
# 在提示中包含处于角色观测范围内的其他角色
|
||||
avatar_infos = {}
|
||||
for avatar in avatars_to_decide:
|
||||
observed = world.get_observable_avatars(avatar)
|
||||
avatar_infos[avatar.name] = avatar.get_expanded_info(observed)
|
||||
general_action_infos = ACTION_INFOS_STR
|
||||
info = {
|
||||
"avatar_infos": avatar_infos,
|
||||
"world_info": world_info,
|
||||
"general_action_infos": general_action_infos,
|
||||
}
|
||||
res = await call_ai_action(info)
|
||||
async def decide_one(avatar: Avatar):
|
||||
# 获取基于该角色已知区域的世界信息
|
||||
world_info = world.get_info(known_region_ids=avatar.known_regions)
|
||||
|
||||
# 在提示中包含处于角色观测范围内的其他角色
|
||||
observed = world.get_observable_avatars(avatar)
|
||||
avatar_info = avatar.get_expanded_info(co_region_avatars=observed)
|
||||
|
||||
info = {
|
||||
"avatar_info": avatar_info,
|
||||
"world_info": world_info,
|
||||
"general_action_infos": general_action_infos,
|
||||
}
|
||||
res = await call_ai_action(info)
|
||||
return avatar, res
|
||||
|
||||
tasks = [decide_one(avatar) for avatar in avatars_to_decide]
|
||||
results_list = await asyncio.gather(*tasks)
|
||||
|
||||
results: dict[Avatar, tuple[ACTION_NAME_PARAMS_PAIRS, str, str]] = {}
|
||||
for avatar in avatars_to_decide:
|
||||
for avatar, res in results_list:
|
||||
if not res or avatar.name not in res:
|
||||
continue
|
||||
|
||||
r = res[avatar.name]
|
||||
# 仅接受 action_name_params_pairs,不再支持单个 action_name/action_params
|
||||
raw_pairs = r["action_name_params_pairs"]
|
||||
@@ -94,6 +105,7 @@ class LLMAI(AI):
|
||||
else:
|
||||
# 跳过无法解析的项
|
||||
continue
|
||||
|
||||
# 至少有一个
|
||||
if not pairs:
|
||||
raise ValueError(f"LLM未返回有效的action_name_params_pairs: {r}")
|
||||
@@ -101,6 +113,7 @@ class LLMAI(AI):
|
||||
avatar_thinking = r.get("avatar_thinking", r.get("thinking", ""))
|
||||
short_term_objective = r.get("short_term_objective", "")
|
||||
results[avatar] = (pairs, avatar_thinking, short_term_objective)
|
||||
|
||||
return results
|
||||
|
||||
llm_ai = LLMAI()
|
||||
@@ -125,6 +125,9 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin):
|
||||
# 动作冷却:记录动作类名 -> 上次完成月戳
|
||||
_action_cd_last_months: dict[str, int] = field(default_factory=dict)
|
||||
# 不缓存 effects;实时从宗门与功法合并
|
||||
|
||||
# 知道的区域 ID 集合
|
||||
known_regions: set[int] = field(default_factory=set)
|
||||
|
||||
def join_sect(self, sect: Sect, rank: "SectRank") -> None:
|
||||
"""加入宗门"""
|
||||
@@ -216,6 +219,24 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin):
|
||||
# 初始化时计算所有长期效果(HP等)
|
||||
self.recalc_effects()
|
||||
|
||||
# 初始化已知区域
|
||||
self._init_known_regions()
|
||||
|
||||
def _init_known_regions(self):
|
||||
"""初始化已知区域:当前位置 + 宗门驻地"""
|
||||
# 1. 当前位置
|
||||
if self.tile and self.tile.region:
|
||||
self.known_regions.add(self.tile.region.id)
|
||||
|
||||
# 2. 宗门驻地
|
||||
if self.sect:
|
||||
# 遍历地图寻找该宗门的驻地
|
||||
# map.sect_regions 是 {region_id: SectRegion}
|
||||
for r in self.world.map.sect_regions.values():
|
||||
if r.sect_id == self.sect.id:
|
||||
self.known_regions.add(r.id)
|
||||
break
|
||||
|
||||
@property
|
||||
def effects(self) -> dict[str, object]:
|
||||
merged: dict[str, object] = defaultdict(str)
|
||||
@@ -571,6 +592,11 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin):
|
||||
if self.current_action is None:
|
||||
# 当前无动作时才清除标记,避免清除新提交动作的标记
|
||||
self._new_action_set_this_step = False
|
||||
|
||||
# 每次执行动作后,更新已知区域
|
||||
if self.tile and self.tile.region:
|
||||
self.known_regions.add(self.tile.region.id)
|
||||
|
||||
return events
|
||||
|
||||
def update_cultivation(self, new_level: int):
|
||||
|
||||
@@ -78,8 +78,8 @@ async def generate_long_term_objective(avatar: "Avatar") -> Optional[LongTermObj
|
||||
Returns:
|
||||
生成的LongTermObjective对象,失败则返回None
|
||||
"""
|
||||
# 准备世界信息
|
||||
world_info = avatar.world.get_info()
|
||||
# 准备世界信息(仅获取已知区域)
|
||||
world_info = avatar.world.get_info(known_region_ids=avatar.known_regions)
|
||||
|
||||
# 获取 expanded_info(包含详细信息和事件历史)
|
||||
expanded_info = avatar.get_expanded_info(detailed=True)
|
||||
|
||||
@@ -80,9 +80,10 @@ class Map():
|
||||
"""
|
||||
return self.tiles[(x, y)].region
|
||||
|
||||
def get_info(self, detailed: bool = False) -> dict:
|
||||
def get_info(self, detailed: bool = False, known_region_ids: Optional[set[int]] = None) -> dict:
|
||||
"""
|
||||
返回地图信息(dict)。
|
||||
known_region_ids: 如果提供,仅返回这些ID对应的区域信息。
|
||||
"""
|
||||
# 动态分类(因为现在没有自动分类字典了)
|
||||
# 或者我们简单点,不分类返回,只返回总览?
|
||||
@@ -91,7 +92,10 @@ class Map():
|
||||
from src.classes.region import NormalRegion, CultivateRegion, CityRegion
|
||||
|
||||
def filter_regions(cls):
|
||||
return {rid: r for rid, r in self.regions.items() if isinstance(r, cls)}
|
||||
return {
|
||||
rid: r for rid, r in self.regions.items()
|
||||
if rid in known_region_ids
|
||||
}
|
||||
|
||||
def build_regions_info(regions_dict) -> list[str]:
|
||||
if detailed:
|
||||
|
||||
@@ -23,12 +23,13 @@ class World():
|
||||
# 天地灵机开始年份(用于计算持续时间)
|
||||
phenomenon_start_year: int = 0
|
||||
|
||||
def get_info(self, detailed: bool = False) -> dict:
|
||||
def get_info(self, detailed: bool = False, known_region_ids: Optional[set[int]] = None) -> dict:
|
||||
"""
|
||||
返回世界信息(dict),其中包含地图信息(dict)。
|
||||
如果指定了 known_region_ids,则只返回这些 ID 对应的区域信息。
|
||||
"""
|
||||
static_info = self.static_info
|
||||
map_info = self.map.get_info(detailed=detailed)
|
||||
map_info = self.map.get_info(detailed=detailed, known_region_ids=known_region_ids)
|
||||
world_info = {**map_info, **static_info}
|
||||
|
||||
if self.current_phenomenon:
|
||||
|
||||
@@ -157,6 +157,7 @@ class AvatarLoadMixin:
|
||||
avatar.thinking = data.get("thinking", "")
|
||||
avatar.short_term_objective = data.get("short_term_objective", "")
|
||||
avatar._action_cd_last_months = data.get("_action_cd_last_months", {})
|
||||
avatar.known_regions = set(data.get("known_regions", []))
|
||||
|
||||
# 加载长期目标
|
||||
long_term_objective_data = data.get("long_term_objective")
|
||||
|
||||
@@ -104,5 +104,6 @@ class AvatarSaveMixin:
|
||||
"set_year": self.long_term_objective.set_year
|
||||
} if self.long_term_objective else None,
|
||||
"_action_cd_last_months": self._action_cd_last_months,
|
||||
"known_regions": list(self.known_regions),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
你是一个决策者,这是一个仙侠世界,你负责来决定一些角色之后的动作行为。
|
||||
你是一个决策者,这是一个仙侠世界,你负责来决定某角色之后的动作行为。
|
||||
{world_info}
|
||||
全部可执行的动作有:
|
||||
{general_action_infos}
|
||||
你需要进行决策的NPC的dict[AvatarName, info]为
|
||||
{avatar_infos}
|
||||
你需要进行决策的NPC的info为
|
||||
{avatar_info}
|
||||
|
||||
|
||||
注意,只返回json格式结果。
|
||||
|
||||
@@ -77,7 +77,6 @@ function spawnCloud(initial: boolean = false) {
|
||||
const speedY = (Math.random() - 0.5) * 0.1 // Slight vertical drift
|
||||
|
||||
let x, y
|
||||
const margin = 200
|
||||
|
||||
if (initial) {
|
||||
// Random anywhere
|
||||
@@ -85,7 +84,17 @@ function spawnCloud(initial: boolean = false) {
|
||||
y = Math.random() * props.height
|
||||
} else {
|
||||
// Start from Left (off-screen)
|
||||
x = -margin - Math.random() * 100
|
||||
// We need to ensure the cloud is fully off-screen.
|
||||
// Shadow is offset to the right, so the rightmost point is shadow.x + halfWidth
|
||||
// shadow.x = x + offset
|
||||
// rightmost = x + offset + halfWidth
|
||||
// We want rightmost < 0 -> x < -(offset + halfWidth)
|
||||
|
||||
const shadowOffsetX = 40 * scale
|
||||
const halfWidth = (tex.width * scale) / 2
|
||||
const safeMargin = shadowOffsetX + halfWidth + 50
|
||||
|
||||
x = -safeMargin - Math.random() * 200
|
||||
y = Math.random() * props.height
|
||||
}
|
||||
|
||||
@@ -112,7 +121,7 @@ function spawnCloud(initial: boolean = false) {
|
||||
|
||||
function updateClouds(dt: number) {
|
||||
const bounds = { w: props.width, h: props.height }
|
||||
const margin = 300 // Wider margin for cleanup
|
||||
const verticalMargin = 300 // For Y-axis removal
|
||||
|
||||
for (let i = activeClouds.value.length - 1; i >= 0; i--) {
|
||||
const cloud = activeClouds.value[i]
|
||||
@@ -122,14 +131,24 @@ function updateClouds(dt: number) {
|
||||
cloud.sprite.y += cloud.speedY * dt
|
||||
|
||||
// Move Shadow (Keep offset)
|
||||
// Re-calculate offset based on scale to keep it simple, or just apply delta
|
||||
cloud.shadow.x += cloud.speedX * dt
|
||||
cloud.shadow.y += cloud.speedY * dt
|
||||
|
||||
// Boundary Check (Only check Right side since we move Right)
|
||||
if (cloud.sprite.x > bounds.w + margin ||
|
||||
cloud.sprite.y > bounds.h + margin ||
|
||||
cloud.sprite.y < -margin) {
|
||||
// Calculate removal boundary
|
||||
// Use sprite width to determine if fully off-screen
|
||||
// sprite.width is the scaled width. Anchor is 0.5.
|
||||
const halfWidth = cloud.sprite.width / 2
|
||||
|
||||
// Check if Cloud is fully off-screen to the right
|
||||
// We check the Leftmost edge of the cloud/shadow complex.
|
||||
// Since shadow is to the right, the leftmost edge is the cloud's left edge.
|
||||
// Left edge = cloud.sprite.x - halfWidth
|
||||
const isOffScreenRight = (cloud.sprite.x - halfWidth) > bounds.w
|
||||
|
||||
// Vertical check (standard margin)
|
||||
const isOffScreenVertical = cloud.sprite.y > bounds.h + verticalMargin || cloud.sprite.y < -verticalMargin
|
||||
|
||||
if (isOffScreenRight || isOffScreenVertical) {
|
||||
|
||||
// Remove
|
||||
if (container.value) {
|
||||
@@ -204,3 +223,4 @@ onUnmounted(() => {
|
||||
<!-- z-index 300 should be above entities (usually < 100) and map -->
|
||||
<container ref="container" :z-index="300" event-mode="none" />
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user