From b60481c99c88fbbeb00f8ff52b17f9b1347df71d Mon Sep 17 00:00:00 2001 From: bridge Date: Tue, 6 Jan 2026 21:23:06 +0800 Subject: [PATCH] fix death bug --- src/classes/age.py | 16 +++--- src/classes/avatar_manager.py | 30 ++++++++++ src/classes/death.py | 3 + src/server/main.py | 15 ++--- src/sim/simulator.py | 4 +- tests/test_birth.py | 82 ++++++++++++++++++++++++++++ tests/test_death.py | 100 ++++++++++------------------------ 7 files changed, 157 insertions(+), 93 deletions(-) create mode 100644 tests/test_birth.py diff --git a/src/classes/age.py b/src/classes/age.py index e70c328..e3fe66e 100644 --- a/src/classes/age.py +++ b/src/classes/age.py @@ -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 diff --git a/src/classes/avatar_manager.py b/src/classes/avatar_manager.py index 2b062bc..7a6fdfe 100644 --- a/src/classes/avatar_manager.py +++ b/src/classes/avatar_manager.py @@ -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"]: """ diff --git a/src/classes/death.py b/src/classes/death.py index 1053d4b..53ae144 100644 --- a/src/classes/death.py +++ b/src/classes/death.py @@ -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) + # 可以在这里触发其他逻辑,比如检查是否有继承人等 diff --git a/src/server/main.py b/src/server/main.py index f877907..2abf16e 100644 --- a/src/server/main.py +++ b/src/server/main.py @@ -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", diff --git a/src/sim/simulator.py b/src/sim/simulator.py index 09a623e..b05a7a3 100644 --- a/src/sim/simulator.py +++ b/src/sim/simulator.py @@ -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 diff --git a/tests/test_birth.py b/tests/test_birth.py new file mode 100644 index 0000000..7840aa9 --- /dev/null +++ b/tests/test_birth.py @@ -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 # 确保事件也生成了 + diff --git a/tests/test_death.py b/tests/test_death.py index 04c6a7d..3b9212b 100644 --- a/tests/test_death.py +++ b/tests/test_death.py @@ -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()