From 6a4059280a7d2efbc1bdb0249a6d8ff4b2d30ec8 Mon Sep 17 00:00:00 2001 From: bridge Date: Mon, 29 Dec 2025 21:46:26 +0800 Subject: [PATCH] refactor death --- src/classes/death.py | 9 +- src/classes/death_reason.py | 21 ++++- src/classes/relation.py | 11 ++- src/sim/simulator.py | 18 ++-- tests/test_cultivation_logic.py | 1 + tests/test_death.py | 150 ++++++++++++++++++++++++++++++++ tests/test_utils_numerical.py | 1 + 7 files changed, 192 insertions(+), 19 deletions(-) create mode 100644 tests/test_death.py diff --git a/src/classes/death.py b/src/classes/death.py index 216c44c..1053d4b 100644 --- a/src/classes/death.py +++ b/src/classes/death.py @@ -6,7 +6,7 @@ if TYPE_CHECKING: from src.classes.world import World from src.classes.avatar import Avatar -def handle_death(world: World, avatar: Avatar, reason: Union[str, DeathReason] = DeathReason.UNKNOWN) -> None: +def handle_death(world: World, avatar: Avatar, reason: Union[str, DeathReason]) -> None: """ 处理角色死亡的统一入口。 负责将角色标记为死亡,清理行动队列,但保留角色数据。 @@ -14,14 +14,11 @@ def handle_death(world: World, avatar: Avatar, reason: Union[str, DeathReason] = Args: world: 世界对象 avatar: 死亡的角色 - reason: 死亡原因(DeathReason枚举或字符串) + reason: 死亡原因(DeathReason对象或字符串) """ - # 如果传入的是枚举,转为字符串值 - reason_str = reason.value if isinstance(reason, DeathReason) else str(reason) + reason_str = str(reason) # 标记为死亡(软删除) avatar.set_dead(reason_str, world.month_stamp) # 可以在这里触发其他逻辑,比如检查是否有继承人等 - - diff --git a/src/classes/death_reason.py b/src/classes/death_reason.py index 92fa9f1..7761da0 100644 --- a/src/classes/death_reason.py +++ b/src/classes/death_reason.py @@ -1,11 +1,24 @@ +from __future__ import annotations from enum import Enum +from dataclasses import dataclass +from typing import Optional -class DeathReason(Enum): +class DeathType(Enum): OLD_AGE = "老死" BATTLE = "战死" SERIOUS_INJURY = "重伤" - UNKNOWN = "未知" + +@dataclass +class DeathReason: + death_type: DeathType + killer_name: Optional[str] = None def __str__(self) -> str: - return self.value - + if self.death_type == DeathType.BATTLE: + killer = self.killer_name if self.killer_name else "未知角色" + return f"被{killer}杀害" + elif self.death_type == DeathType.SERIOUS_INJURY: + return "重伤不治身亡" + elif self.death_type == DeathType.OLD_AGE: + return "寿元耗尽而亡" + return self.death_type.value diff --git a/src/classes/relation.py b/src/classes/relation.py index 8c2ddbe..0996244 100644 --- a/src/classes/relation.py +++ b/src/classes/relation.py @@ -188,7 +188,16 @@ def get_relations_strs(avatar: "Avatar", max_lines: int = 12) -> list[str]: grouped: dict[str, list[str]] = defaultdict(list) for other, rel in relations.items(): label = get_relation_label(rel, avatar, other) - grouped[label].append(other.name) + + display_name = other.name + # 死亡标记 + if getattr(other, "is_dead", False): + # death_info 是一个可选的字典,其中 'reason' 已经被 handle_death 格式化好了 + d_info = getattr(other, "death_info", None) + reason = d_info["reason"] if d_info and "reason" in d_info else "未知原因" + display_name = f"{other.name}(已故:{reason})" + + grouped[label].append(display_name) lines: list[str] = [] processed_labels = set() diff --git a/src/sim/simulator.py b/src/sim/simulator.py index 3bbd417..aac6343 100644 --- a/src/sim/simulator.py +++ b/src/sim/simulator.py @@ -151,25 +151,24 @@ class Simulator: - 战斗死亡已在 Action 中结算 - 此时剩下的 avatars 都是存活的,只需检查非战斗因素(如老死、被动掉血) """ + from src.classes.death_reason import DeathReason, DeathType + events = [] for avatar in self.world.avatar_manager.get_living_avatars(): is_dead = False - reason_str = "" - death_reason = DeathReason.UNKNOWN + death_reason: DeathReason | None = None # 优先判定重伤(可能是被动效果导致) if avatar.hp.cur <= 0: # 注意:这里应该是 avatar.hp.cur 或者 avatar.hp <= 0 取决于 HP 类的实现,原代码是 avatar.hp <= 0 is_dead = True - reason_str = f"{avatar.name} 因重伤不治身亡" - death_reason = DeathReason.SERIOUS_INJURY + death_reason = DeathReason(DeathType.SERIOUS_INJURY) # 其次判定寿元 elif avatar.death_by_old_age(): is_dead = True - reason_str = f"{avatar.name} 老死了,时年{avatar.age.get_age()}岁" - death_reason = DeathReason.OLD_AGE + death_reason = DeathReason(DeathType.OLD_AGE) - if is_dead: - event = Event(self.world.month_stamp, reason_str, related_avatars=[avatar.id]) + if is_dead and death_reason: + event = Event(self.world.month_stamp, str(death_reason), related_avatars=[avatar.id]) events.append(event) handle_death(self.world, avatar, death_reason) @@ -318,6 +317,9 @@ class Simulator: state = avatar.relation_interaction_states[target_id] target = self.world.avatar_manager.get_avatar(target_id) + if target is None or target.is_dead: + continue + # 判定是否触发 count = state["count"] should_trigger = False diff --git a/tests/test_cultivation_logic.py b/tests/test_cultivation_logic.py index 9ed9ebf..5f837f3 100644 --- a/tests/test_cultivation_logic.py +++ b/tests/test_cultivation_logic.py @@ -108,3 +108,4 @@ def test_cp_serialization(): assert cp_new.exp == 123 assert cp_new.realm == Realm.Qi_Refinement + diff --git a/tests/test_death.py b/tests/test_death.py new file mode 100644 index 0000000..7faf56a --- /dev/null +++ b/tests/test_death.py @@ -0,0 +1,150 @@ +import pytest +from unittest.mock import MagicMock + +from src.classes.death_reason import DeathReason, DeathType +from src.classes.death import handle_death +from src.classes.relation import Relation, get_relations_strs +from src.classes.event import Event + +def test_death_reason_str(): + """测试死因的字符串格式化""" + # 战死 + reason_battle = DeathReason(DeathType.BATTLE, killer_name="张三") + assert str(reason_battle) == "被张三杀害" + + # 战死(未知凶手) + reason_battle_unknown = DeathReason(DeathType.BATTLE) + assert str(reason_battle_unknown) == "被未知角色杀害" + + # 重伤 + reason_injury = DeathReason(DeathType.SERIOUS_INJURY) + assert str(reason_injury) == "重伤不治身亡" + + # 老死 + reason_old = DeathReason(DeathType.OLD_AGE) + assert str(reason_old) == "寿元耗尽而亡" + +def test_handle_death(base_world, dummy_avatar): + """测试死亡处理函数""" + reason = DeathReason(DeathType.BATTLE, killer_name="李四") + + # 执行死亡处理 + handle_death(base_world, dummy_avatar, reason) + + # 验证状态 + 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 + +def test_relation_display_with_death(base_world, dummy_avatar): + """测试关系列表中的死亡显示""" + # 创建另一个角色作为朋友 + 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 + + friend = Avatar( + world=base_world, + name="Friend", + 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.WOOD, + alignment=Alignment.RIGHTEOUS + ) + + # 建立关系 + dummy_avatar.set_relation(friend, Relation.FRIEND) + + # 初始状态:显示正常名字 + strs_before = get_relations_strs(dummy_avatar) + assert "朋友:Friend" in strs_before + + # 朋友死亡(重伤) + reason = DeathReason(DeathType.SERIOUS_INJURY) + handle_death(base_world, friend, reason) + + # 死亡后:显示带死因的名字 + strs_after = get_relations_strs(dummy_avatar) + assert "朋友:Friend(已故:重伤不治身亡)" in strs_after + +@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 + + # 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]) + +@pytest.mark.asyncio +async def test_simulator_evolve_relations_filter_dead(base_world, dummy_avatar): + """测试关系演化阶段过滤死者""" + 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) + + # Mock RelationResolver 防止真正调用 LLM + from unittest.mock import patch + with patch('src.classes.relation_resolver.RelationResolver.run_batch') as mock_run: + await sim._phase_evolve_relations() + + # 验证:因为 target 已死,应该不会调用 run_batch + mock_run.assert_not_called() + + # 如果 Target 活着,应该会调用 + target.is_dead = False + with patch('src.classes.relation_resolver.RelationResolver.run_batch') as mock_run: + mock_run.return_value = [] + await sim._phase_evolve_relations() + mock_run.assert_called_once() + diff --git a/tests/test_utils_numerical.py b/tests/test_utils_numerical.py index b732976..db22909 100644 --- a/tests/test_utils_numerical.py +++ b/tests/test_utils_numerical.py @@ -116,3 +116,4 @@ def test_df_get_list_int(): assert get_list_int(row, "a", separator="|") == [1, 2, 3] assert get_list_int(row, "c", separator="|") == [1, 3] +