fix death bug
This commit is contained in:
@@ -19,8 +19,8 @@ class Age:
|
||||
|
||||
def __init__(self, age: int, realm: Realm):
|
||||
self.age = age
|
||||
# 基础最大寿元(年),不含effects加成,初始化为 max(境界基线, 当前年龄+1)
|
||||
self.base_max_lifespan: int = max(self.get_base_expected_lifespan(realm), self.age + 1)
|
||||
# 基础最大寿元(年),不含effects加成
|
||||
self.base_max_lifespan: int = self.get_base_expected_lifespan(realm)
|
||||
# 实际最大寿元(年),包含effects加成,初始值与基础值相同
|
||||
self.max_lifespan: int = self.base_max_lifespan
|
||||
|
||||
@@ -38,8 +38,7 @@ class Age:
|
||||
|
||||
def set_initial_max_lifespan(self, realm: Realm) -> None:
|
||||
"""构造时已设置最大寿元,此处保持与构造策略一致。"""
|
||||
base = self.get_base_expected_lifespan(realm)
|
||||
self.base_max_lifespan = max(base, self.age + 1)
|
||||
self.base_max_lifespan = self.get_base_expected_lifespan(realm)
|
||||
self.max_lifespan = self.base_max_lifespan
|
||||
|
||||
def update_realm(self, new_realm: Realm) -> None:
|
||||
@@ -48,11 +47,10 @@ class Age:
|
||||
self.ensure_max_lifespan_at_least_realm_base(new_realm)
|
||||
|
||||
def ensure_max_lifespan_at_least_realm_base(self, realm: Realm) -> None:
|
||||
"""确保基础最大寿元至少达到 max(该境界基线, 当前年龄+1)。"""
|
||||
"""确保基础最大寿元至少达到该境界基线。"""
|
||||
base = self.get_base_expected_lifespan(realm)
|
||||
floor_value = max(base, self.age + 1)
|
||||
if self.base_max_lifespan < floor_value:
|
||||
self.base_max_lifespan = floor_value
|
||||
if self.base_max_lifespan < base:
|
||||
self.base_max_lifespan = base
|
||||
self.max_lifespan = self.base_max_lifespan
|
||||
|
||||
def increase_max_lifespan(self, years: int) -> None:
|
||||
@@ -85,7 +83,7 @@ class Age:
|
||||
|
||||
# 基础概率:每超过1年增加0.01的概率
|
||||
prob_add = 0.01
|
||||
death_probability = min(years_over_lifespan * prob_add, 0.1)
|
||||
death_probability = min(years_over_lifespan * prob_add, 0.01)
|
||||
|
||||
return death_probability
|
||||
|
||||
|
||||
@@ -14,6 +14,33 @@ class AvatarManager:
|
||||
avatars: Dict[str, "Avatar"] = field(default_factory=dict)
|
||||
# 存储已死亡的角色(归档)
|
||||
dead_avatars: Dict[str, "Avatar"] = field(default_factory=dict)
|
||||
|
||||
# --- 变更缓冲区 (不参与序列化) ---
|
||||
_newly_dead_buffer: List[str] = field(default_factory=list, init=False)
|
||||
_newly_born_buffer: List[str] = field(default_factory=list, init=False)
|
||||
|
||||
def register_avatar(self, avatar: "Avatar", is_newly_born: bool = False) -> None:
|
||||
"""
|
||||
注册一个角色到管理器中。
|
||||
Args:
|
||||
avatar: 角色对象
|
||||
is_newly_born: 是否为新出生的角色(若是,则加入变更缓冲供前端同步)
|
||||
"""
|
||||
self.avatars[str(avatar.id)] = avatar
|
||||
if is_newly_born:
|
||||
self._newly_born_buffer.append(str(avatar.id))
|
||||
|
||||
def pop_newly_dead(self) -> List[str]:
|
||||
"""获取并清空本帧刚死亡的角色ID列表"""
|
||||
res = list(self._newly_dead_buffer)
|
||||
self._newly_dead_buffer.clear()
|
||||
return res
|
||||
|
||||
def pop_newly_born(self) -> List[str]:
|
||||
"""获取并清空本帧刚出生的角色ID列表"""
|
||||
res = list(self._newly_born_buffer)
|
||||
self._newly_born_buffer.clear()
|
||||
return res
|
||||
|
||||
def get_avatar(self, avatar_id: str) -> "Avatar | None":
|
||||
"""
|
||||
@@ -33,6 +60,9 @@ class AvatarManager:
|
||||
# 断开地图连接,确保不出现在地图网格上
|
||||
if hasattr(avatar, "tile"):
|
||||
avatar.tile = None
|
||||
|
||||
# 记录变更
|
||||
self._newly_dead_buffer.append(aid)
|
||||
|
||||
def get_avatars_in_same_region(self, avatar: "Avatar") -> List["Avatar"]:
|
||||
"""
|
||||
|
||||
@@ -21,4 +21,7 @@ def handle_death(world: World, avatar: Avatar, reason: Union[str, DeathReason])
|
||||
# 标记为死亡(软删除)
|
||||
avatar.set_dead(reason_str, world.month_stamp)
|
||||
|
||||
# 从管理器中归档(硬移动),并记录变更
|
||||
world.avatar_manager.handle_death(avatar.id)
|
||||
|
||||
# 可以在这里触发其他逻辑,比如检查是否有继承人等
|
||||
|
||||
@@ -369,14 +369,9 @@ async def game_loop():
|
||||
# 执行一步
|
||||
events = await sim.step()
|
||||
|
||||
# 找出新诞生的角色 ID 和 刚死亡的角色 ID
|
||||
newly_born_ids = set()
|
||||
newly_dead_ids = set()
|
||||
for e in events:
|
||||
if "晋升为修士" in e.content and e.related_avatars:
|
||||
newly_born_ids.update(e.related_avatars)
|
||||
if ("身亡" in e.content or "老死" in e.content) and e.related_avatars:
|
||||
newly_dead_ids.update(e.related_avatars)
|
||||
# 获取状态变更 (Source of Truth: AvatarManager)
|
||||
newly_born_ids = world.avatar_manager.pop_newly_born()
|
||||
newly_dead_ids = world.avatar_manager.pop_newly_dead()
|
||||
|
||||
avatar_updates = []
|
||||
|
||||
@@ -411,8 +406,6 @@ async def game_loop():
|
||||
"is_dead": True,
|
||||
"action": "已故"
|
||||
})
|
||||
# 将死者归档到墓地,从活跃列表移除
|
||||
world.avatar_manager.handle_death(aid)
|
||||
|
||||
# 3. 常规位置更新(暂时只发前 50 个旧角色,减少数据量)
|
||||
limit = 50
|
||||
@@ -1063,7 +1056,7 @@ def create_avatar(req: CreateAvatarRequest):
|
||||
avatar.alignment = Alignment.from_str(req.alignment)
|
||||
|
||||
# 注册到管理器
|
||||
world.avatar_manager.avatars[avatar.id] = avatar
|
||||
world.avatar_manager.register_avatar(avatar, is_newly_born=True)
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
|
||||
@@ -173,7 +173,7 @@ class Simulator:
|
||||
death_reason = DeathReason(DeathType.OLD_AGE)
|
||||
|
||||
if is_dead and death_reason:
|
||||
event = Event(self.world.month_stamp, str(death_reason), related_avatars=[avatar.id])
|
||||
event = Event(self.world.month_stamp, f"{avatar.name}{death_reason}", related_avatars=[avatar.id])
|
||||
events.append(event)
|
||||
handle_death(self.world, avatar, death_reason)
|
||||
|
||||
@@ -192,7 +192,7 @@ class Simulator:
|
||||
name = get_random_name(gender)
|
||||
# create_random_mortal 内部会获取 existing_avatars,需要确保它处理活人
|
||||
new_avatar = create_random_mortal(self.world, self.world.month_stamp, name, Age(age, Realm.Qi_Refinement))
|
||||
self.world.avatar_manager.avatars[new_avatar.id] = new_avatar
|
||||
self.world.avatar_manager.register_avatar(new_avatar, is_newly_born=True)
|
||||
event = Event(self.world.month_stamp, f"{new_avatar.name}晋升为修士了。", related_avatars=[new_avatar.id])
|
||||
events.append(event)
|
||||
return events
|
||||
|
||||
82
tests/test_birth.py
Normal file
82
tests/test_birth.py
Normal file
@@ -0,0 +1,82 @@
|
||||
import pytest
|
||||
from src.classes.avatar import Avatar, Gender
|
||||
from src.classes.age import Age
|
||||
from src.classes.cultivation import Realm, CultivationProgress
|
||||
from src.utils.id_generator import get_avatar_id
|
||||
from src.classes.root import Root
|
||||
from src.classes.calendar import create_month_stamp, Year, Month
|
||||
|
||||
def test_register_avatar_buffer(base_world):
|
||||
"""测试注册新角色时的缓冲区逻辑"""
|
||||
manager = base_world.avatar_manager
|
||||
|
||||
# 1. 注册普通角色(非新生,例如加载存档)
|
||||
a1 = Avatar(
|
||||
world=base_world,
|
||||
name="OldGuy",
|
||||
id=get_avatar_id(),
|
||||
birth_month_stamp=create_month_stamp(Year(100), Month.JANUARY),
|
||||
age=Age(20, Realm.Qi_Refinement),
|
||||
gender=Gender.MALE
|
||||
)
|
||||
manager.register_avatar(a1, is_newly_born=False)
|
||||
|
||||
assert a1.id in manager.avatars
|
||||
assert len(manager.pop_newly_born()) == 0
|
||||
|
||||
# 2. 注册新生角色
|
||||
a2 = Avatar(
|
||||
world=base_world,
|
||||
name="Baby",
|
||||
id=get_avatar_id(),
|
||||
birth_month_stamp=create_month_stamp(Year(200), Month.JANUARY),
|
||||
age=Age(1, Realm.Qi_Refinement),
|
||||
gender=Gender.FEMALE
|
||||
)
|
||||
manager.register_avatar(a2, is_newly_born=True)
|
||||
|
||||
assert a2.id in manager.avatars
|
||||
newly_born = manager.pop_newly_born()
|
||||
assert len(newly_born) == 1
|
||||
assert str(a2.id) in newly_born
|
||||
|
||||
# 3. 再次获取应为空
|
||||
assert len(manager.pop_newly_born()) == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_simulator_birth_logic(base_world):
|
||||
"""测试模拟器中的生子逻辑集成"""
|
||||
from src.sim.simulator import Simulator
|
||||
from unittest.mock import patch
|
||||
from src.classes.avatar import Avatar
|
||||
from src.classes.age import Age
|
||||
from src.classes.cultivation import Realm, CultivationProgress
|
||||
|
||||
# 构造一个简单的模拟返回值
|
||||
mock_avatar = Avatar(
|
||||
world=base_world,
|
||||
name="MockBaby",
|
||||
id="mock_id_123",
|
||||
birth_month_stamp=base_world.month_stamp,
|
||||
age=Age(1, Realm.Qi_Refinement),
|
||||
gender=Gender.MALE
|
||||
)
|
||||
|
||||
sim = Simulator(base_world)
|
||||
sim.birth_rate = 1.0 # 必生
|
||||
|
||||
# Patch 掉 create_random_mortal,避免依赖复杂的宗门/地图数据
|
||||
with patch('src.sim.simulator.create_random_mortal', return_value=mock_avatar):
|
||||
# 执行一次更新
|
||||
events = sim._phase_update_age_and_birth()
|
||||
|
||||
# 验证产生了一个新角色
|
||||
newly_born = base_world.avatar_manager.pop_newly_born()
|
||||
assert len(newly_born) == 1
|
||||
assert newly_born[0] == mock_avatar.id
|
||||
|
||||
# 验证新角色在管理器中
|
||||
avatar = base_world.avatar_manager.get_avatar(mock_avatar.id)
|
||||
assert avatar is mock_avatar
|
||||
assert avatar.name in events[0].content # 确保事件也生成了
|
||||
|
||||
@@ -25,23 +25,32 @@ def test_death_reason_str():
|
||||
assert str(reason_old) == "寿元耗尽而亡"
|
||||
|
||||
def test_handle_death(base_world, dummy_avatar):
|
||||
"""测试死亡处理函数"""
|
||||
"""测试死亡处理函数(集成测试:包含归档和缓冲)"""
|
||||
reason = DeathReason(DeathType.BATTLE, killer_name="李四")
|
||||
|
||||
# 确保角色在管理器中
|
||||
base_world.avatar_manager.register_avatar(dummy_avatar)
|
||||
assert dummy_avatar.id in base_world.avatar_manager.avatars
|
||||
|
||||
# 执行死亡处理
|
||||
handle_death(base_world, dummy_avatar, reason)
|
||||
|
||||
# 验证状态
|
||||
# 1. 验证对象状态
|
||||
assert dummy_avatar.is_dead is True
|
||||
assert dummy_avatar.death_info is not None
|
||||
assert dummy_avatar.death_info["reason"] == "被李四杀害"
|
||||
assert dummy_avatar.death_info["time"] == int(base_world.month_stamp)
|
||||
assert dummy_avatar.death_info["location"] == (dummy_avatar.pos_x, dummy_avatar.pos_y)
|
||||
|
||||
# 验证清理工作
|
||||
assert len(dummy_avatar.planned_actions) == 0
|
||||
assert dummy_avatar.current_action is None
|
||||
assert dummy_avatar.sect is None
|
||||
# 2. 验证管理器状态(已归档)
|
||||
assert dummy_avatar.id not in base_world.avatar_manager.avatars
|
||||
assert dummy_avatar.id in base_world.avatar_manager.dead_avatars
|
||||
|
||||
# 3. 验证缓冲区(用于前端推送)
|
||||
newly_dead = base_world.avatar_manager.pop_newly_dead()
|
||||
assert str(dummy_avatar.id) in newly_dead
|
||||
|
||||
# 4. 验证缓冲区清空
|
||||
assert len(base_world.avatar_manager.pop_newly_dead()) == 0
|
||||
|
||||
def test_relation_display_with_death(base_world, dummy_avatar):
|
||||
"""测试关系列表中的死亡显示"""
|
||||
@@ -65,6 +74,8 @@ def test_relation_display_with_death(base_world, dummy_avatar):
|
||||
root=Root.WOOD,
|
||||
alignment=Alignment.RIGHTEOUS
|
||||
)
|
||||
# 注册朋友
|
||||
base_world.avatar_manager.register_avatar(friend)
|
||||
|
||||
# 建立关系
|
||||
dummy_avatar.set_relation(friend, Relation.FRIEND)
|
||||
@@ -85,7 +96,7 @@ def test_relation_display_with_death(base_world, dummy_avatar):
|
||||
def test_avatar_manager_archive_death(base_world, dummy_avatar):
|
||||
"""测试 AvatarManager 的死亡归档逻辑"""
|
||||
manager = base_world.avatar_manager
|
||||
manager.avatars[dummy_avatar.id] = dummy_avatar
|
||||
manager.register_avatar(dummy_avatar)
|
||||
|
||||
# 确保初始在活人表
|
||||
assert dummy_avatar.id in manager.avatars
|
||||
@@ -100,82 +111,29 @@ def test_avatar_manager_archive_death(base_world, dummy_avatar):
|
||||
|
||||
# 验证 get_avatar 依然能查到
|
||||
assert manager.get_avatar(dummy_avatar.id) == dummy_avatar
|
||||
|
||||
# 验证 buffer
|
||||
assert str(dummy_avatar.id) in manager.pop_newly_dead()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_simulator_resolve_death(base_world, dummy_avatar):
|
||||
"""测试模拟器的死亡结算阶段"""
|
||||
from src.sim.simulator import Simulator
|
||||
sim = Simulator(base_world)
|
||||
base_world.avatar_manager.avatars[dummy_avatar.id] = dummy_avatar
|
||||
base_world.avatar_manager.register_avatar(dummy_avatar)
|
||||
|
||||
# Case 1: 重伤死亡
|
||||
dummy_avatar.hp.cur = -10
|
||||
|
||||
# 执行死亡结算
|
||||
events = sim._phase_resolve_death()
|
||||
|
||||
# 验证
|
||||
assert dummy_avatar.is_dead is True
|
||||
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):
|
||||
"""测试关系演化阶段过滤死者"""
|
||||
from src.sim.simulator import Simulator
|
||||
sim = Simulator(base_world)
|
||||
|
||||
# 创建对手
|
||||
from src.classes.avatar import Avatar, Gender
|
||||
from src.classes.age import Age
|
||||
from src.classes.cultivation import Realm
|
||||
from src.utils.id_generator import get_avatar_id
|
||||
from src.classes.root import Root
|
||||
from src.classes.alignment import Alignment
|
||||
from src.classes.calendar import create_month_stamp, Year, Month
|
||||
|
||||
target = Avatar(
|
||||
world=base_world,
|
||||
name="Target",
|
||||
id=get_avatar_id(),
|
||||
birth_month_stamp=create_month_stamp(Year(2000), Month.JANUARY),
|
||||
age=Age(20, Realm.Qi_Refinement),
|
||||
gender=Gender.MALE,
|
||||
pos_x=0, pos_y=0,
|
||||
root=Root.FIRE,
|
||||
alignment=Alignment.EVIL
|
||||
)
|
||||
|
||||
base_world.avatar_manager.avatars[dummy_avatar.id] = dummy_avatar
|
||||
base_world.avatar_manager.avatars[target.id] = target
|
||||
|
||||
# 设置交互状态达到阈值
|
||||
dummy_avatar.relation_interaction_states[target.id]["count"] = 100
|
||||
|
||||
# 让 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 已死且归档,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
|
||||
|
||||
await sim._phase_evolve_relations()
|
||||
mock_run.assert_called_once()
|
||||
|
||||
# 验证已被自动归档(因为 handle_death 现在会调用 manager.handle_death)
|
||||
assert dummy_avatar.id in base_world.avatar_manager.dead_avatars
|
||||
assert str(dummy_avatar.id) in base_world.avatar_manager.pop_newly_dead()
|
||||
|
||||
Reference in New Issue
Block a user