fix a bug
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -206,7 +206,7 @@
|
||||
### 🏛️ 世界背景系统
|
||||
- ✅ 注入基础世界知识
|
||||
- [ ] 动态世界观生成
|
||||
- [ ] 动态历史生成
|
||||
- [ ] 基于用户输入历史的动态功法、装备、宗门、地图生成
|
||||
|
||||
### 特殊
|
||||
- ✅ 奇遇
|
||||
@@ -225,7 +225,6 @@
|
||||
- [ ] 成为后世传奇
|
||||
|
||||
### 🔭 远期展望
|
||||
- [ ] ECS并行
|
||||
- [ ] 历史/事件的小说化&图片化&视频化
|
||||
- [ ] MCP agent化,修士自行调用工具
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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", {})
|
||||
|
||||
@@ -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())
|
||||
|
||||
# 保存事件历史(限制数量)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user