diff --git a/autowsgr/ui/battle/fleet_change/_selection.py b/autowsgr/ui/battle/fleet_change/_selection.py index 150896d3..8fdc446a 100644 --- a/autowsgr/ui/battle/fleet_change/_selection.py +++ b/autowsgr/ui/battle/fleet_change/_selection.py @@ -6,12 +6,13 @@ from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, replace from typing import TYPE_CHECKING from autowsgr.combat.fleet import ShipSelector from autowsgr.infra.logger import get_logger from autowsgr.ui.battle.constants import CLICK_BACK +from autowsgr.vision.ocr_rules import get_user_ship_name_aliases from ._planning import FleetPlanningMixin @@ -39,6 +40,15 @@ class _ShipSelection: class FleetSelectionMixin(FleetPlanningMixin): """提供船池页面的进入、退出、选择和移除操作。""" + def _search_options(self, option: ShipSelector) -> tuple[ShipSelector, ...]: + """按固定顺序生成自定义舰名和标准舰名搜索规则。""" + if not self._use_search or option.search_name is not None: + return (option,) + + aliases = get_user_ship_name_aliases(option.name) + search_names = aliases if option.name in aliases else (*aliases, option.name) + return tuple(replace(option, search_name=name) for name in search_names) + def _open_choose_page(self, slot: int) -> ChooseShipPage: """打开指定物理槽位的选船页面。""" from autowsgr.ui.choose_ship_page import ChooseShipPage @@ -76,14 +86,16 @@ def _try_select_option( if self._ctx.ocr is None: raise RuntimeError('智能换船需要 OCR 引擎') - choose_page = self._open_choose_page(slot) - selected = choose_page.change_single_ship( - option, - use_search=self._use_search, - ) - if selected is None: + for search_option in self._search_options(option): + choose_page = self._open_choose_page(slot) + selected = choose_page.change_single_ship( + search_option, + use_search=self._use_search, + ) + if selected is not None: + return _ShipSelection(name=selected, option=option) self._cancel_choose_page() - return _ShipSelection(name=selected, option=option) + return _ShipSelection(name=None, option=option) # 打开指定槽位的选船页面,完成单艘舰船的选择或移除。 def _change_single_ship( diff --git a/autowsgr/vision/ocr_rules.py b/autowsgr/vision/ocr_rules.py index be155eed..c4510e47 100644 --- a/autowsgr/vision/ocr_rules.py +++ b/autowsgr/vision/ocr_rules.py @@ -34,7 +34,11 @@ from autowsgr.constants import ( expand_ship_name_candidates as expand_group_candidates, ) -from autowsgr.constants import get_ship_name_group_id, set_ship_name_aliases +from autowsgr.constants import ( + get_ship_name_group_id, + normalize_ship_name, + set_ship_name_aliases, +) from autowsgr.infra.logger import get_logger from autowsgr.types import ShipType @@ -211,6 +215,24 @@ def set_user_ship_name_aliases(aliases: Mapping[str, str]) -> int: return len(loaded) +def get_user_ship_name_aliases(ship_name: str) -> tuple[str, ...]: + """返回标准舰名对应的全部游戏内自定义名,结果不依赖配置顺序。""" + name = ship_name.strip() + if not name: + return () + if name in _USER_SHIP_NAME_ALIASES: + return (name,) + + identity = normalize_ship_name(name) + return tuple( + sorted( + alias + for alias, standard_name in _USER_SHIP_NAME_ALIASES.items() + if normalize_ship_name(standard_name) == identity + ) + ) + + def expand_ship_name_candidates(candidates: list[str]) -> list[str]: """将当前舰名候选扩展为同组全部名称。""" return expand_group_candidates(candidates) diff --git a/testing/ui/battle_preparation/test_unit.py b/testing/ui/battle_preparation/test_unit.py index e9403239..a29d3a96 100644 --- a/testing/ui/battle_preparation/test_unit.py +++ b/testing/ui/battle_preparation/test_unit.py @@ -1373,6 +1373,93 @@ def test_decisive_uses_new_flow_when_enabled(self): new_change.assert_called_once_with(None, exact_fleet_rules(['A'])) +class TestFleetSelection: + @pytest.mark.parametrize( + 'aliases', + [ + {'别名甲': '85工程', '别名乙': '85工程'}, + {'别名乙': '85工程', '别名甲': '85工程'}, + ], + ) + def test_try_select_option_retries_all_aliases_in_stable_order( + self, + aliases: dict[str, str], + ): + page = BattlePreparationPage( + _make_ctx(MagicMock(spec=AndroidController), MagicMock()), + ) + set_user_ship_name_aliases(aliases) + option = ShipSelector( + name='85工程', + ship_types=(ShipType.CV,), + min_level=100, + ) + first_page = MagicMock() + first_page.change_single_ship.return_value = None + second_page = MagicMock() + second_page.change_single_ship.return_value = '85工程' + + with ( + patch.object( + page, + '_open_choose_page', + side_effect=[first_page, second_page], + ) as open_page, + patch.object(page, '_cancel_choose_page') as cancel_page, + ): + selected = page._try_select_option(2, option) + + attempted = [ + first_page.change_single_ship.call_args.args[0], + second_page.change_single_ship.call_args.args[0], + ] + assert selected == _ShipSelection(name='85工程', option=option) + assert [item.search_name for item in attempted] == sorted(aliases) + assert all(item.ship_types == (ShipType.CV,) for item in attempted) + assert all(item.min_level == 100 for item in attempted) + assert open_page.call_args_list == [call(2), call(2)] + cancel_page.assert_called_once_with() + + def test_try_select_option_falls_back_to_standard_name(self): + page = BattlePreparationPage( + _make_ctx(MagicMock(spec=AndroidController), MagicMock()), + ) + aliases = {'别名甲': '85工程', '别名乙': '85工程'} + set_user_ship_name_aliases(aliases) + option = ShipSelector(name='85工程') + choose_pages = [MagicMock(), MagicMock(), MagicMock()] + for choose_page in choose_pages[:-1]: + choose_page.change_single_ship.return_value = None + choose_pages[-1].change_single_ship.return_value = '85工程' + + with ( + patch.object( + page, + '_open_choose_page', + side_effect=choose_pages, + ), + patch.object(page, '_cancel_choose_page') as cancel_page, + ): + selected = page._try_select_option(0, option) + + attempted = [ + choose_page.change_single_ship.call_args.args[0].search_name + for choose_page in choose_pages + ] + assert selected == _ShipSelection(name='85工程', option=option) + assert attempted == [*sorted(aliases), '85工程'] + assert cancel_page.call_count == 2 + + def test_explicit_search_name_is_not_expanded(self): + page = BattlePreparationPage( + _make_ctx(MagicMock(spec=AndroidController), MagicMock()), + ) + set_user_ship_name_aliases({'别名甲': '85工程', '别名乙': '85工程'}) + option = ShipSelector(name='85工程', search_name='别名乙') + + assert page._search_options(option) == (option,) + + # ───────────────────────────────────────────── # 智能换船 # ───────────────────────────────────────────── diff --git a/testing/ui/test_choose_ship_page.py b/testing/ui/test_choose_ship_page.py index 31c0b865..7da46ad8 100644 --- a/testing/ui/test_choose_ship_page.py +++ b/testing/ui/test_choose_ship_page.py @@ -37,13 +37,16 @@ def test_custom_name_rejected_above_confidence(self): def test_bidirectional_prefix_ambiguity_is_rejected(self): assert not ChooseShipPage._matches_ship_name('安东尼奥', '安东尼') - def test_user_alias_is_used_for_search_and_matching(self): + def test_user_alias_is_used_for_matching(self): set_user_ship_name_aliases({'契卡洛夫': '85工程'}) assert ChooseShipPage._normalize_search_keyword('契卡洛夫') == '契卡洛夫' assert ChooseShipPage._matches_ship_name('契卡洛夫', '85工程') assert ChooseShipPage._matches_ship_name('85工程', '契卡洛夫') + def test_standard_name_is_used_when_no_user_alias_exists(self): + assert ChooseShipPage._normalize_search_keyword(' 岛风 ') == '岛风' + class TestShipTypeProbeRoutes: @staticmethod diff --git a/testing/vision/test_ocr.py b/testing/vision/test_ocr.py index 60ee01e4..f00d051a 100644 --- a/testing/vision/test_ocr.py +++ b/testing/vision/test_ocr.py @@ -30,6 +30,7 @@ FastOCRProfile, get_easyocr_params, get_fastocr_params, + get_user_ship_name_aliases, normalize_level_digits, set_user_ship_name_aliases, set_user_ship_name_corrections, @@ -569,6 +570,23 @@ def test_user_ship_name_aliases_map_display_names_to_standard_names(self): assert _fuzzy_match(apply_ship_patches('U-47·狼群'), SHIPNAMES) == 'U-47' assert _fuzzy_match(apply_ship_patches('巴尔的摩:英魂'), SHIPNAMES) == '巴尔的摩' + @pytest.mark.parametrize( + 'aliases', + [ + {'别名甲': '85工程', '别名乙': '85工程'}, + {'别名乙': '85工程', '别名甲': '85工程'}, + ], + ) + def test_reverse_alias_lookup_returns_all_aliases_in_stable_order( + self, + aliases: dict[str, str], + ): + set_user_ship_name_aliases(aliases) + + expected = tuple(sorted(aliases)) + assert get_user_ship_name_aliases('85工程') == expected + assert get_user_ship_name_aliases(expected[0]) == (expected[0],) + def test_user_ship_name_is_added_to_the_same_ship_group(self): set_user_ship_name_aliases({'契卡洛夫': '85工程'})