fix a bug

This commit is contained in:
bridge
2026-01-06 20:42:53 +08:00
parent fa909e5a2a
commit 3f980d4593
7 changed files with 91 additions and 28 deletions

View File

@@ -206,7 +206,7 @@ You can also join the QQ group for discussion: 1071821688. Verification answer i
### 🏛️ World Lore
- ✅ Inject basic world knowledge
- [ ] Dynamic worldview generation
- [ ] Dynamic history generation
- [ ] Dynamic generation of techniques, equipment, sects, and maps based on user input history
### Specials
- ✅ Fortuitous encounters
@@ -225,7 +225,6 @@ You can also join the QQ group for discussion: 1071821688. Verification answer i
- [ ] Become a Legend of Later Ages
### 🔭 Long-term
- [ ] ECS parallel toolkit
- [ ] Novelization/imagery/video for history and events
- [ ] Avatar calling MCP tools on their own

View File

@@ -206,7 +206,7 @@
### 🏛️ 世界背景系统
- ✅ 注入基础世界知识
- [ ] 动态世界观生成
- [ ] 动态历史生成
- [ ] 基于用户输入历史的动态功法、装备、宗门、地图生成
### 特殊
- ✅ 奇遇
@@ -225,7 +225,6 @@
- [ ] 成为后世传奇
### 🔭 远期展望
- [ ] ECS并行
- [ ] 历史/事件的小说化&图片化&视频化
- [ ] MCP agent化修士自行调用工具

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Dict, List, TYPE_CHECKING
from typing import Dict, List, TYPE_CHECKING, Iterable
import itertools
if TYPE_CHECKING:
from src.classes.avatar import Avatar
@@ -9,22 +10,39 @@ from src.classes.observe import get_observable_avatars
@dataclass
class AvatarManager:
# 仅存储存活的角色,用于主循环遍历
avatars: Dict[str, "Avatar"] = field(default_factory=dict)
# 存储已死亡的角色(归档)
dead_avatars: Dict[str, "Avatar"] = field(default_factory=dict)
def get_avatar(self, avatar_id: str) -> "Avatar | None":
"""
根据 ID 获取角色对象
根据 ID 获取角色对象,优先查找活人,再查找死者
"""
return self.avatars.get(str(avatar_id))
aid = str(avatar_id)
return self.avatars.get(aid) or self.dead_avatars.get(aid)
def handle_death(self, avatar_id: str) -> None:
"""
处理角色死亡:将角色从活跃列表移动到墓地
"""
aid = str(avatar_id)
if aid in self.avatars:
avatar = self.avatars.pop(aid)
self.dead_avatars[aid] = avatar
# 断开地图连接,确保不出现在地图网格上
if hasattr(avatar, "tile"):
avatar.tile = None
def get_avatars_in_same_region(self, avatar: "Avatar") -> List["Avatar"]:
"""
返回与给定 avatar 处于同一区域的其他角色列表(不含自己)。
返回与给定 avatar 处于同一区域的其他【存活】角色列表(不含自己)。
"""
if avatar is None or getattr(avatar, "tile", None) is None or avatar.tile.region is None:
return []
region = avatar.tile.region
same_region: list["Avatar"] = []
# 只遍历活人
for other in self.avatars.values():
if other is avatar or getattr(other, "tile", None) is None:
continue
@@ -35,35 +53,46 @@ class AvatarManager:
def get_living_avatars(self) -> List["Avatar"]:
"""
返回所有存活的角色列表。
由于 avatars 现在只存活人,直接返回 values 即可。
"""
return [avatar for avatar in self.avatars.values() if not avatar.is_dead]
return list(self.avatars.values())
def get_observable_avatars(self, avatar: "Avatar") -> List["Avatar"]:
"""
返回处于 avatar 交互范围内的其他角色列表(不含自己)。
基于曼哈顿距离与境界映射的感知半径过滤。
返回处于 avatar 交互范围内的其他【存活】角色列表(不含自己)。
"""
return get_observable_avatars(avatar, self.avatars.values())
def _iter_all_avatars(self) -> Iterable["Avatar"]:
"""辅助方法:遍历所有角色(活人+死者)"""
return itertools.chain(self.avatars.values(), self.dead_avatars.values())
def remove_avatar(self, avatar_id: str) -> None:
"""
从管理器中删除一个 avatar并清理所有与其相关的双向关系。
从管理器中彻底删除一个 avatar(无论是死是活),并清理所有与其相关的双向关系。
此操作不可逆。
"""
avatar = self.avatars.get(avatar_id)
aid = str(avatar_id)
avatar = self.get_avatar(aid)
if avatar is None:
return
# 先清理与其直接记录的关系(会保持对称)
# 1. 清理与其直接记录的关系
related = list(getattr(avatar, "relations", {}).keys())
for other in related:
avatar.clear_relation(other)
# 再次扫一遍所有 avatar确保不存在残留引用
for other in list(self.avatars.values()):
# 2. 扫一遍所有角色(含死者),确保清除反向引用
for other in self._iter_all_avatars():
if other is avatar:
continue
if getattr(other, "relations", None) is not None and avatar in other.relations:
other.clear_relation(avatar)
# 最后移除自身
self.avatars.pop(avatar_id, None)
# 3. 移除自身
self.avatars.pop(aid, None)
self.dead_avatars.pop(aid, None)
def remove_avatars(self, avatar_ids: List[str]) -> None:
"""
@@ -71,5 +100,3 @@ class AvatarManager:
"""
for aid in list(avatar_ids):
self.remove_avatar(aid)

View File

@@ -402,8 +402,8 @@ async def game_loop():
# 2. 发送刚死角色的状态更新
for aid in newly_dead_ids:
# 注意:死人可能不在 get_living_avatars() 里,但还在 avatars 里
a = world.avatar_manager.avatars.get(aid)
# 使用 get_avatar 以兼容死者查询
a = world.avatar_manager.get_avatar(aid)
if a:
avatar_updates.append({
"id": str(a.id),
@@ -411,6 +411,8 @@ async def game_loop():
"is_dead": True,
"action": "已故"
})
# 将死者归档到墓地,从活跃列表移除
world.avatar_manager.handle_death(aid)
# 3. 常规位置更新(暂时只发前 50 个旧角色,减少数据量)
limit = 50
@@ -723,7 +725,7 @@ def get_hover_info(
target = None
if target_type == "avatar":
target = world.avatar_manager.avatars.get(target_id)
target = world.avatar_manager.get_avatar(target_id)
elif target_type == "region":
if world.map and hasattr(world.map, "regions"):
regions = world.map.regions
@@ -762,7 +764,7 @@ def get_detail_info(
target = None
if target_type == "avatar":
target = world.avatar_manager.avatars.get(target_id)
target = world.avatar_manager.get_avatar(target_id)
elif target_type == "region":
if world.map and hasattr(world.map, "regions"):
regions = world.map.regions

View File

@@ -123,7 +123,12 @@ def load_game(save_path: Optional[Path] = None) -> Tuple["World", "Simulator", L
avatar.relations[other_avatar] = relation
# 将所有avatar添加到world
world.avatar_manager.avatars = all_avatars
# 根据生死状态分流
for avatar in all_avatars.values():
if avatar.is_dead:
world.avatar_manager.dead_avatars[avatar.id] = avatar
else:
world.avatar_manager.avatars[avatar.id] = avatar
# 恢复洞府主人关系
cultivate_regions_hosts = world_data.get("cultivate_regions_hosts", {})

View File

@@ -100,8 +100,9 @@ def save_game(
}
# 保存所有Avatar第一阶段不含relations
# 需要保存活人和死者
avatars_data = []
for avatar in world.avatar_manager.avatars.values():
for avatar in world.avatar_manager._iter_all_avatars():
avatars_data.append(avatar.to_save_dict())
# 保存事件历史(限制数量)

View File

@@ -81,6 +81,26 @@ def test_relation_display_with_death(base_world, dummy_avatar):
strs_after = get_relations_strs(dummy_avatar)
assert "朋友Friend(已故:重伤不治身亡)" in strs_after
def test_avatar_manager_archive_death(base_world, dummy_avatar):
"""测试 AvatarManager 的死亡归档逻辑"""
manager = base_world.avatar_manager
manager.avatars[dummy_avatar.id] = dummy_avatar
# 确保初始在活人表
assert dummy_avatar.id in manager.avatars
assert dummy_avatar.id not in manager.dead_avatars
# 执行归档
manager.handle_death(dummy_avatar.id)
# 验证位置转移
assert dummy_avatar.id not in manager.avatars
assert dummy_avatar.id in manager.dead_avatars
# 验证 get_avatar 依然能查到
assert manager.get_avatar(dummy_avatar.id) == dummy_avatar
@pytest.mark.asyncio
async def test_simulator_resolve_death(base_world, dummy_avatar):
"""测试模拟器的死亡结算阶段"""
@@ -96,6 +116,10 @@ async def test_simulator_resolve_death(base_world, dummy_avatar):
assert dummy_avatar.death_info["reason"] == "重伤不治身亡"
assert len(events) > 0
assert "重伤不治身亡" in str(events[0])
# 注意:在 Simulator 的 phase 中,角色只是被标记死亡
# 真正的归档发生在 main.py 循环中,或者我们可以手动触发
# 这里我们只验证标记逻辑
@pytest.mark.asyncio
async def test_simulator_evolve_relations_filter_dead(base_world, dummy_avatar, mock_llm_managers):
@@ -130,19 +154,25 @@ async def test_simulator_evolve_relations_filter_dead(base_world, dummy_avatar,
# 设置交互状态达到阈值
dummy_avatar.relation_interaction_states[target.id]["count"] = 100
# 让 Target 死亡
# 让 Target 死亡并归档(模拟真实流程)
target.set_dead("测试死亡", base_world.month_stamp)
base_world.avatar_manager.handle_death(target.id)
# 获取 mock_rr 用于验证调用
mock_run = mock_llm_managers["rr"]
await sim._phase_evolve_relations()
# 验证:因为 target 已死,应该不会调用 run_batch
# 验证:因为 target 已死且归档get_living_avatars 不会返回它target 也不在活人列表里
# 即使 get_avatar 能查到它,逻辑中应该有防守检查
mock_run.assert_not_called()
# 如果 Target 活着,应该会调用
target.is_dead = False
# 复活:手动移回活人表
if target.id in base_world.avatar_manager.dead_avatars:
base_world.avatar_manager.avatars[target.id] = base_world.avatar_manager.dead_avatars.pop(target.id)
mock_run.reset_mock() # 重置 mock 调用记录
mock_run.return_value = [] # AsyncMock 会自动将其 wrap 进 awaitable