fix death bug

This commit is contained in:
bridge
2026-01-06 21:23:06 +08:00
parent 3f980d4593
commit b60481c99c
7 changed files with 157 additions and 93 deletions

View File

@@ -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

View File

@@ -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"]:
"""

View File

@@ -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)
# 可以在这里触发其他逻辑,比如检查是否有继承人等

View File

@@ -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",

View File

@@ -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
View 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 # 确保事件也生成了

View File

@@ -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()