Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added screens/通用-出战/选人面板-初始.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added screens/通用-出战/选人面板-选中UP.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
"""LostVoidApp.check_predefined_team + get_target_team_idx_by_priority 测试。
"""LostVoidApp.check_predefined_team 测试。

按 testing methodology 动作一:
- ``check_predefined_team``:按 ``mission_name`` / ``choose_team_by_priority`` /
``complete_task_force_with_up`` / ``predefined_team_idx`` / priority 匹配结果,
路由到「需选择预备编队」/「无需选择预备编队」,并设置 ``use_priority_agent`` +
``ctx.lost_void.predefined_team_idx``。
- ``get_target_team_idx_by_priority``:遍历 ``team_config.team_list`` ×
``priority_agent_list`` → 匹配数最多的 idx;都不匹配返 ``predefined_team_idx``。
``complete_task_force_with_up`` / ``up_team_auto_compose`` / ``predefined_team_idx`` /
priority 匹配结果,路由到「需选择预备编队」/「无需选择预备编队」,
并设置 ``use_priority_agent`` + ``ctx.lost_void.predefined_team_idx``。
- UP匹配失败(编队没配 / 编队里没UP)时按固定编队出战,
**不设置 use_priority_agent**(否则通关后误标记本周已用UP)。
- ``up_team_auto_compose`` 开启时先走 ``LostVoidComposeUpTeam`` 自动配队,
成功 → 无需选择预备编队;失败 → 回退预备编队匹配。

纯配置/mock 测试,无画面依赖:mock ``team_config.team_list`` 构造可控配队,
``priority_agent_list`` 用 ``AgentEnum.X.value`` 构造。
``priority_agent_list`` 用 ``AgentEnum.X.value`` 构造,自动配队 op 用 mock 替身

``LostVoidApp`` 实例化可行(同 test_app_bounty_commission)。
"""
from types import SimpleNamespace
from unittest.mock import patch

from test.conftest import TestContext
Expand All @@ -30,6 +33,7 @@ def _setup_op(test_context: TestContext) -> LostVoidApp:
test_context.lost_void.load_artifact_data()
test_context.lost_void.load_challenge_config()
test_context.lost_void.challenge_config.choose_team_by_priority = False
test_context.lost_void.challenge_config.up_team_auto_compose = False
test_context.lost_void.challenge_config.predefined_team_idx = -1
test_context.lost_void.predefined_team_idx = -1
return LostVoidApp(
Expand All @@ -44,70 +48,50 @@ def _make_team(idx: int, name: str, agent_ids: list[str]) -> PredefinedTeamInfo:
return PredefinedTeamInfo(idx, name, '全配队通用', agent_ids)


# ===== get_target_team_idx_by_priority 测试 =====
# ===== 预备编队匹配 =====


def test_priority_match_returns_best_team_idx(test_context: TestContext) -> None:
"""priority_agent_list 命中 team[1] 最多代理人 → 返回 idx=1。"""
def test_special_investigation_priority_match(test_context: TestContext) -> None:
"""特遣调查 + choose_by_priority=True + 未用过 UP + priority 命中 → 需选择,use_priority=True。"""
op = _setup_op(test_context)
op.config.mission_name = '特遣调查'
test_context.lost_void.challenge_config.choose_team_by_priority = True
op.run_record.complete_task_force_with_up = False
mock_teams = [
_make_team(0, '编队A', ['anby', 'billy', 'unknown']),
_make_team(1, '编队B', ['ellen', 'anby', 'unknown']),
_make_team(2, '编队C', ['grace', 'unknown', 'unknown']),
]
op.priority_agent_list = [AgentEnum.ELLEN.value, AgentEnum.ANBY.value]
with patch.object(type(test_context.team_config), 'team_list',
new=mock_teams):
idx = op.get_target_team_idx_by_priority()
assert idx == 1, 'team[1] 命中 ellen + anby(2 个),应返回 1'


def test_no_priority_match_returns_predefined_idx(test_context: TestContext) -> None:
"""priority_agent_list 都不匹配 → 返回 challenge_config.predefined_team_idx。"""
op = _setup_op(test_context)
test_context.lost_void.challenge_config.predefined_team_idx = 2
mock_teams = [
_make_team(0, '编队A', ['anby', 'billy', 'unknown']),
_make_team(1, '编队B', ['grace', 'unknown', 'unknown']),
]
op.priority_agent_list = [AgentEnum.ELLEN.value] # ellen 不在任何 team
with patch.object(type(test_context.team_config), 'team_list',
new=mock_teams):
idx = op.get_target_team_idx_by_priority()
assert idx == 2, '无匹配应返回 predefined_team_idx=2'


def test_priority_default_predefined_idx_is_minus_one(test_context: TestContext) -> None:
"""无匹配且 predefined_team_idx=-1 → 返回 -1。"""
op = _setup_op(test_context)
mock_teams = [_make_team(0, '编队A', ['anby', 'billy', 'unknown'])]
op.priority_agent_list = [AgentEnum.ELLEN.value]
with patch.object(type(test_context.team_config), 'team_list',
new=mock_teams):
idx = op.get_target_team_idx_by_priority()
assert idx == -1

result = op.check_predefined_team()
assert result.status == '需选择预备编队'
assert op.use_priority_agent is True
assert test_context.lost_void.predefined_team_idx == 1

# ===== check_predefined_team 测试 =====

def test_no_priority_match_falls_back_without_flag(test_context: TestContext) -> None:
"""priority 无匹配 + 固定编队=2 → 需选择固定编队,但 use_priority 必须为 False。

def test_special_investigation_priority_match(test_context: TestContext) -> None:
"""特遣调查 + choose_by_priority=True + 未用过 UP + priority 命中 → 需选择,use_priority=True。"""
原实现无匹配时也置 use_priority=True,通关后误标记「本周已用UP完成」,
本周内配好编队也不再尝试。
"""
op = _setup_op(test_context)
op.config.mission_name = '特遣调查'
test_context.lost_void.challenge_config.choose_team_by_priority = True
test_context.lost_void.challenge_config.predefined_team_idx = 2
op.run_record.complete_task_force_with_up = False
mock_teams = [
_make_team(0, '编队A', ['anby', 'billy', 'unknown']),
_make_team(1, '编队B', ['ellen', 'anby', 'unknown']),
_make_team(1, '编队B', ['grace', 'unknown', 'unknown']),
]
op.priority_agent_list = [AgentEnum.ELLEN.value, AgentEnum.ANBY.value]
op.priority_agent_list = [AgentEnum.ELLEN.value] # ellen 不在任何 team
with patch.object(type(test_context.team_config), 'team_list',
new=mock_teams):
result = op.check_predefined_team()
assert result.status == '需选择预备编队'
assert op.use_priority_agent is True
assert test_context.lost_void.predefined_team_idx == 1
assert test_context.lost_void.predefined_team_idx == 2
assert op.use_priority_agent is False, '无UP匹配时不能标记使用了UP'


def test_special_investigation_no_priority_match(test_context: TestContext) -> None:
Expand All @@ -116,7 +100,6 @@ def test_special_investigation_no_priority_match(test_context: TestContext) -> N
op.config.mission_name = '特遣调查'
test_context.lost_void.challenge_config.choose_team_by_priority = True
op.run_record.complete_task_force_with_up = False
# predefined_team_idx 已经是 -1,get_target_team_idx_by_priority 无匹配也返 -1
mock_teams = [_make_team(0, '编队A', ['anby', 'billy', 'unknown'])]
op.priority_agent_list = [AgentEnum.ELLEN.value] # 无匹配
with patch.object(type(test_context.team_config), 'team_list',
Expand Down Expand Up @@ -169,3 +152,45 @@ def test_choose_by_priority_false_skips_priority_branch(test_context: TestContex
result = op.check_predefined_team()
assert result.status == '无需选择预备编队'
assert op.use_priority_agent is False


# ===== UP自动配队分支 =====


def test_auto_compose_success_skips_team_select(test_context: TestContext) -> None:
"""up_team_auto_compose=True + 配队 op 成功 → 无需选择预备编队 + use_priority=True。"""
op = _setup_op(test_context)
op.config.mission_name = '特遣调查'
test_context.lost_void.challenge_config.choose_team_by_priority = True
test_context.lost_void.challenge_config.up_team_auto_compose = True
op.run_record.complete_task_force_with_up = False
mock_teams = [_make_team(0, '编队A', ['ellen', 'anby', 'unknown'])]
op.priority_agent_list = [AgentEnum.ELLEN.value]
with (patch.object(type(test_context.team_config), 'team_list', new=mock_teams),
patch('zzz_od.application.hollow_zero.lost_void.lost_void_app.LostVoidComposeUpTeam') as mock_op_cls):
mock_op_cls.return_value.execute.return_value = SimpleNamespace(success=True, status='配队完成')
result = op.check_predefined_team()
assert result.status == '无需选择预备编队'
assert op.use_priority_agent is True
assert mock_op_cls.called


def test_auto_compose_fail_falls_back_to_team_match(test_context: TestContext) -> None:
"""up_team_auto_compose=True + 配队 op 失败 → 回退预备编队匹配(命中编队B)。"""
op = _setup_op(test_context)
op.config.mission_name = '特遣调查'
test_context.lost_void.challenge_config.choose_team_by_priority = True
test_context.lost_void.challenge_config.up_team_auto_compose = True
op.run_record.complete_task_force_with_up = False
mock_teams = [
_make_team(0, '编队A', ['anby', 'billy', 'unknown']),
_make_team(1, '编队B', ['ellen', 'unknown', 'unknown']),
]
op.priority_agent_list = [AgentEnum.ELLEN.value]
with (patch.object(type(test_context.team_config), 'team_list', new=mock_teams),
patch('zzz_od.application.hollow_zero.lost_void.lost_void_app.LostVoidComposeUpTeam') as mock_op_cls):
mock_op_cls.return_value.execute.return_value = SimpleNamespace(success=False, status='配队校验失败')
result = op.check_predefined_team()
assert result.status == '需选择预备编队'
assert test_context.lost_void.predefined_team_idx == 1
assert op.use_priority_agent is True
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
"""迷失之地 UP自动配队 lost_void_up_team 测试。

- ``compute_up_lineup``:配队规则(已拥有UP全上 / 同队搭档补位 / 无UP试用保底)。纯数据。
- ``recognize_matrix_up`` / ``find_up_in_agent_grid``:真实 fixture 识别
(矩阵入口UP头像行 / 编队选择网格UP徽章;2026-08-21 实拍,当期UP:
特遣=蕾米埃尔/维琳娜/爱丽丝,矩阵主战=蕾米埃尔+维琳娜(试用)、协战=妮可)。
- ``LostVoidComposeUpTeam``:出战画面槽位名字OCR读队 + 换人计划。真实 fixture。

fixture 不存在时 skip(采到后自动恢复,见 conftest.has_screen)。
"""
import pytest

from test.conftest import TestContext

from one_dragon.base.geometry.rectangle import Rect
from zzz_od.application.hollow_zero.lost_void.operation.lost_void_up_team import (
LostVoidComposeUpTeam,
compute_up_lineup,
find_up_in_agent_grid,
recognize_matrix_up,
)
from zzz_od.config.team_config import PredefinedTeamInfo
from zzz_od.game_data.agent import AgentEnum

# 编队选择画面「代理人列表」区域(与 screen_info 一致)
MATRIX_AGENT_GRID_RECT = Rect(185, 96, 966, 1059)


def _team(agent_ids: list[str]) -> PredefinedTeamInfo:
return PredefinedTeamInfo(0, '编队', '全配队通用', agent_ids)


def _agents(*names: str) -> list:
by_name = {e.value.agent_name: e.value for e in AgentEnum}
return [by_name[n] for n in names]


# ===== compute_up_lineup =====


def test_lineup_owned_up_plus_co_partner() -> None:
"""已拥有UP全上,搭档按同队次数补(爱丽丝+蕾米埃尔 → 补共同队友薇薇安)。"""
up = _agents('蕾米埃尔', '维琳娜', '爱丽丝')
teams = [
_team(['yuzuha', 'alice', 'vivian']),
_team(['promeia', 'vivian', 'remielle']),
_team(['nicole', 'billy', 'unknown']),
]
lineup = compute_up_lineup(up, teams, 3)
assert {a.agent_name for a in lineup} == {'蕾米埃尔', '爱丽丝', '薇薇安'}


def test_lineup_no_owned_up_uses_first_up() -> None:
"""一个UP都没拥有:用第一个UP保底 + 高频队友补位。"""
up = _agents('维琳娜')
teams = [
_team(['nicole', 'billy', 'unknown']),
_team(['nicole', 'ellen', 'unknown']),
]
lineup = compute_up_lineup(up, teams, 3)
names = [a.agent_name for a in lineup]
assert names[0] == '维琳娜'
assert '妮可' in names and len(lineup) == 3


def test_lineup_excludes_unowned_up_from_partners() -> None:
"""未拥有的UP不能被当成搭档补进来。"""
up = _agents('蕾米埃尔', '维琳娜')
teams = [_team(['remielle', 'vivian', 'unknown'])]
lineup = compute_up_lineup(up, teams, 3)
names = [a.agent_name for a in lineup]
assert '维琳娜' not in names
assert '蕾米埃尔' in names and '薇薇安' in names


# ===== 真实 fixture 识别 =====


def test_recognize_matrix_up_entry_panel(test_context: TestContext) -> None:
"""矩阵入口面板UP头像行:协战识别为妮可(带协战标签/最右),主战含蕾米埃尔。"""
if not test_context.has_screen('迷失之地-矩阵行动', '入口面板'):
pytest.skip('缺 fixture: 迷失之地-矩阵行动/入口面板')
screen = test_context.load_screen('迷失之地-矩阵行动', '入口面板')
mains, support = recognize_matrix_up(test_context, screen)
assert '妮可' in [a.agent_name for a in support]
assert '蕾米埃尔' in [a.agent_name for a in mains]


def test_find_up_in_agent_grid(test_context: TestContext) -> None:
"""编队选择网格UP徽章:关联到维琳娜(2026-08-21 实拍徽章唯一OCR命中)。"""
if not test_context.has_screen('迷失之地-矩阵行动-编队选择', '自由编队-初始'):
pytest.skip('缺 fixture: 迷失之地-矩阵行动-编队选择/自由编队-初始')
screen = test_context.load_screen('迷失之地-矩阵行动-编队选择', '自由编队-初始')
up_list = find_up_in_agent_grid(test_context, screen, MATRIX_AGENT_GRID_RECT)
assert '维琳娜' in [a.agent_name for a in up_list]


# ===== LostVoidComposeUpTeam 读队/计划 =====


def _make_compose_op(test_context: TestContext, state: str) -> LostVoidComposeUpTeam:
lineup = _agents('蕾米埃尔', '爱丽丝', '薇薇安')
up_ids = {a.agent_id for a in _agents('蕾米埃尔', '维琳娜', '爱丽丝')}
op = LostVoidComposeUpTeam(test_context, lineup, up_ids)
test_context.mock_screen('通用-出战', state)
op.screenshot()
return op


def test_read_current_member_ids_full_team(test_context: TestContext) -> None:
"""出战画面槽位名字OCR:妮可/比利/爱丽丝。"""
if not test_context.has_screen('通用-出战', '特遣调查-编队-满员含UP'):
pytest.skip('缺 fixture: 通用-出战/特遣调查-编队-满员含UP')
op = _make_compose_op(test_context, '特遣调查-编队-满员含UP')
assert op._read_current_member_ids() == ['nicole', 'billy', 'alice']


def test_plan_with_empty_slot(test_context: TestContext) -> None:
"""当前 妮可/比利/空 vs 目标 蕾米埃尔/爱丽丝/薇薇安 → 三个槽位都要换。"""
if not test_context.has_screen('通用-出战', '特遣调查-编队-有空位'):
pytest.skip('缺 fixture: 通用-出战/特遣调查-编队-有空位')
op = _make_compose_op(test_context, '特遣调查-编队-有空位')
result = op.check_current_team()
assert result.status == '需调整'
assert len(op.plan) == 3
assert [slot for slot, _ in op.plan] == [0, 1, 2]


# ===== 拿不到UP时提前收手 =====


def _make_plain_op(test_context: TestContext, lineup_names: list[str],
up_names: list[str]) -> LostVoidComposeUpTeam:
"""不依赖画面 只测 _can_still_get_up 的判断。"""
return LostVoidComposeUpTeam(
test_context, _agents(*lineup_names), {a.agent_id for a in _agents(*up_names)})


def test_can_still_get_up_when_up_left_in_plan(test_context: TestContext) -> None:
"""计划里还有没试过的UP → 继续换下去。"""
op = _make_plain_op(test_context, ['蕾米埃尔', '爱丽丝', '薇薇安'], ['蕾米埃尔', '爱丽丝'])
op.plan = list(zip([0, 1, 2], op.lineup, strict=False))
op.plan_idx = 1 # 槽位0的蕾米埃尔没找到 但爱丽丝还没试
assert op._can_still_get_up() is True


def test_can_not_get_up_after_all_up_missed(test_context: TestContext) -> None:
"""UP都试过且都没找到 原编队也没有UP → 不再动剩余槽位。"""
op = _make_plain_op(test_context, ['蕾米埃尔', '爱丽丝', '薇薇安'], ['蕾米埃尔', '爱丽丝'])
op.plan = list(zip([0, 1, 2], op.lineup, strict=False))
op.plan_idx = 2 # 只剩搭档薇薇安
assert op._can_still_get_up() is False


def test_can_still_get_up_when_original_team_has_up(test_context: TestContext) -> None:
"""原编队自带UP(没被换掉) → 即使目标UP没找到也还算数。"""
op = _make_plain_op(test_context, ['蕾米埃尔', '爱丽丝', '薇薇安'], ['蕾米埃尔', '爱丽丝'])
op.plan = list(zip([0, 1, 2], op.lineup, strict=False))
op.plan_idx = 2
op.kept_up = True
assert op._can_still_get_up() is True


def test_can_still_get_up_after_placed(test_context: TestContext) -> None:
"""已经换入过UP → 后面搭档找不到也不影响。"""
op = _make_plain_op(test_context, ['蕾米埃尔', '爱丽丝', '薇薇安'], ['蕾米埃尔', '爱丽丝'])
op.plan = list(zip([0, 1, 2], op.lineup, strict=False))
op.plan_idx = 2
op.placed = _agents('蕾米埃尔')
assert op._can_still_get_up() is True


# ===== 换人计划: 搭档不顶掉当前持有UP的槽位 =====


def _make_planned_op(test_context: TestContext, current_names: list[str | None],
lineup_names: list[str], up_names: list[str]) -> LostVoidComposeUpTeam:
"""打桩槽位读取 只测 check_current_team 的计划生成。"""
op = LostVoidComposeUpTeam(
test_context, _agents(*lineup_names), {a.agent_id for a in _agents(*up_names)})
current_ids = [(_agents(n)[0].agent_id if n is not None else None) for n in current_names]
op._read_current_member_ids = lambda: current_ids
return op


def test_plan_partner_never_replaces_up_slot(test_context: TestContext) -> None:
"""队里已有不在目标配队里的UP(试用维琳娜) → 搭档只配到非UP槽位 维琳娜留下 多出的搭档放弃。
否则UP换入失败时 kept_up 依据的维琳娜会被换搭档顶掉 队里反而没UP。"""
op = _make_planned_op(test_context, ['妮可', '维琳娜', '比利'],
['蕾米埃尔', '薇薇安', '普罗米娅'], ['蕾米埃尔', '维琳娜', '爱丽丝'])
result = op.check_current_team()
assert result.status == '需调整'
assert op.kept_up is True
assert [(slot, agent.agent_name) for slot, agent in op.plan] == [(0, '蕾米埃尔'), (2, '薇薇安')]


def test_plan_up_may_replace_up_slot(test_context: TestContext) -> None:
"""非UP槽位不够时 多出来的目标UP才允许换掉队里的UP(艾莲保留 蕾米埃尔换妮可 爱丽丝换维琳娜)。"""
op = _make_planned_op(test_context, ['维琳娜', '妮可', '艾莲'],
['蕾米埃尔', '爱丽丝', '艾莲'], ['蕾米埃尔', '维琳娜', '爱丽丝'])
result = op.check_current_team()
assert result.status == '需调整'
assert [(slot, agent.agent_name) for slot, agent in op.plan] == [(1, '蕾米埃尔'), (0, '爱丽丝')]
Loading