Files
cultivation-world-simulator/tests/test_action_combat.py
Zihao Xu 8d985e0a2b fix: prevent actions on dead avatars
- Add validate_target_avatar() to TargetingMixin for unified validation.
- Update Attack and Assassinate to use the new validation method.
- Add comment to MutualAction.can_start() explaining why it uses inline check.
- Add tests for dead target validation.
2026-01-04 19:29:35 -08:00

183 lines
6.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import pytest
from unittest.mock import MagicMock, patch, AsyncMock
from src.classes.action.attack import Attack
from src.classes.action.assassinate import Assassinate
from src.classes.mutual_action.talk import Talk
from src.classes.action_runtime import ActionStatus
class TestActionCombat:
@pytest.fixture
def target_avatar(self, dummy_avatar):
"""创建一个靶子角色"""
target = MagicMock()
target.name = "TargetDummy"
target.id = "target_id"
target.hp = MagicMock()
target.hp.current = 100
target.hp.max = 100
target.increase_weapon_proficiency = MagicMock()
return target
@patch("src.classes.action.attack.decide_battle")
def test_attack_execution(self, mock_decide, dummy_avatar, target_avatar):
"""测试攻击执行:扣除 HP"""
# Mock decide_battle 返回 (winner, loser, loser_dmg, winner_dmg)
# 假设 dummy 赢了Target 掉了 10 点血dummy 掉了 2 点
mock_decide.return_value = (dummy_avatar, target_avatar, 10, 2)
# 注入 target 到 world
dummy_avatar.world.avatar_manager.avatars = {target_avatar.name: target_avatar}
# Mock HP 为 MagicMock 以便 assert_called
dummy_avatar.hp = MagicMock()
action = Attack(dummy_avatar, dummy_avatar.world)
action._execute(avatar_name="TargetDummy")
# 验证伤害应用
target_avatar.hp.reduce.assert_called_with(10)
dummy_avatar.hp.reduce.assert_called_with(2)
# 验证熟练度增加 (虽然是随机的,但 mock 了 uniform 就好了,或者只验证调用)
assert dummy_avatar.weapon.get_detailed_info.called or True # 只是确保流程跑通
@patch("src.classes.action.attack.handle_death") # 这个是在 death.py 里的
@patch("src.classes.battle.handle_battle_finish", new_callable=AsyncMock)
def test_attack_finish(self, mock_battle_finish, mock_handle_death, dummy_avatar, target_avatar):
"""测试战斗结束回调"""
# 注入 target
dummy_avatar.world.avatar_manager.avatars = {target_avatar.name: target_avatar}
action = Attack(dummy_avatar, dummy_avatar.world)
# 设置 _last_result (通常由 execute 设置)
action._last_result = (dummy_avatar, target_avatar, 10, 2)
action._start_event_content = "Start Battle"
# 运行 finish
import asyncio
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(action.finish(avatar_name="TargetDummy"))
# 验证调用了 handle_battle_finish
mock_battle_finish.assert_called_once()
args, kwargs = mock_battle_finish.call_args
assert args[1] == dummy_avatar # winner
assert args[2] == target_avatar # loser
def test_can_start_missing_target(self, dummy_avatar):
"""测试目标不存在"""
dummy_avatar.world.avatar_manager.avatars = {}
action = Attack(dummy_avatar, dummy_avatar.world)
ok, reason = action.can_start("Ghost")
assert ok is False
assert reason == "目标不存在"
def test_attack_can_start_dead_target(self, dummy_avatar, target_avatar):
"""测试不能攻击已死亡的目标"""
target_avatar.is_dead = True
dummy_avatar.world.avatar_manager.avatars = {target_avatar.name: target_avatar}
action = Attack(dummy_avatar, dummy_avatar.world)
ok, reason = action.can_start("TargetDummy")
assert ok is False
assert reason == "目标已死亡"
def test_attack_can_start_alive_target(self, dummy_avatar, target_avatar):
"""测试可以攻击存活的目标"""
target_avatar.is_dead = False
dummy_avatar.world.avatar_manager.avatars = {target_avatar.name: target_avatar}
action = Attack(dummy_avatar, dummy_avatar.world)
ok, reason = action.can_start("TargetDummy")
assert ok is True
assert reason == ""
class TestAssassinate:
"""暗杀动作测试"""
@pytest.fixture
def target_avatar(self):
"""创建一个靶子角色"""
target = MagicMock()
target.name = "TargetDummy"
target.id = "target_id"
target.is_dead = False
return target
def test_assassinate_can_start_dead_target(self, dummy_avatar, target_avatar):
"""测试不能暗杀已死亡的目标"""
target_avatar.is_dead = True
dummy_avatar.world.avatar_manager.avatars = {target_avatar.name: target_avatar}
action = Assassinate(dummy_avatar, dummy_avatar.world)
ok, reason = action.can_start(avatar_name="TargetDummy")
assert ok is False
assert reason == "目标已死亡"
def test_assassinate_can_start_alive_target(self, dummy_avatar, target_avatar):
"""测试可以暗杀存活的目标"""
target_avatar.is_dead = False
dummy_avatar.world.avatar_manager.avatars = {target_avatar.name: target_avatar}
action = Assassinate(dummy_avatar, dummy_avatar.world)
ok, reason = action.can_start(avatar_name="TargetDummy")
assert ok is True
assert reason == ""
def test_assassinate_can_start_missing_target(self, dummy_avatar):
"""测试目标不存在"""
dummy_avatar.world.avatar_manager.avatars = {}
action = Assassinate(dummy_avatar, dummy_avatar.world)
ok, reason = action.can_start(avatar_name="Ghost")
assert ok is False
assert reason == "目标不存在"
class TestMutualActionDeadTarget:
"""互动动作对死亡目标的测试"""
@pytest.fixture
def target_avatar(self):
"""创建一个靶子角色"""
target = MagicMock()
target.name = "TargetDummy"
target.id = "target_id"
target.is_dead = False
target.tile = MagicMock()
return target
def test_talk_can_start_dead_target(self, dummy_avatar, target_avatar):
"""测试不能对已死亡的目标发起攀谈"""
target_avatar.is_dead = True
dummy_avatar.world.avatar_manager.avatars = {target_avatar.name: target_avatar}
action = Talk(dummy_avatar, dummy_avatar.world)
ok, reason = action.can_start("TargetDummy")
assert ok is False
assert reason == "目标已死亡"
def test_talk_can_start_self(self, dummy_avatar):
"""测试不能对自己发起攀谈"""
dummy_avatar.is_dead = False
dummy_avatar.world.avatar_manager.avatars = {dummy_avatar.name: dummy_avatar}
action = Talk(dummy_avatar, dummy_avatar.world)
ok, reason = action.can_start(dummy_avatar.name)
assert ok is False
assert reason == "不能对自己发起互动"