refactor death
This commit is contained in:
@@ -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)
|
||||
|
||||
# 可以在这里触发其他逻辑,比如检查是否有继承人等
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -108,3 +108,4 @@ def test_cp_serialization():
|
||||
assert cp_new.exp == 123
|
||||
assert cp_new.realm == Realm.Qi_Refinement
|
||||
|
||||
|
||||
|
||||
150
tests/test_death.py
Normal file
150
tests/test_death.py
Normal file
@@ -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()
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user