diff --git a/tasks/DailyTrifles/script_task.py b/tasks/DailyTrifles/script_task.py index e2994c027f..bdea4e2fb9 100644 --- a/tasks/DailyTrifles/script_task.py +++ b/tasks/DailyTrifles/script_task.py @@ -216,7 +216,7 @@ def run_store(self): if self.config.daily_trifles.trifles_config.buy_sushi_count > 0: self.run_buy_sushi() - self.ui_click(self.I_UI_BACK_YELLOW, self.I_CHECK_MALL, interval=2.4, timeout=8) + self.goto_page(page_mall, skip_first_screenshot=False, timeout=8) self.goto_page(page_main) def run_store_sign(self): diff --git a/tasks/GameUi/assets.py b/tasks/GameUi/assets.py index c5211c12f3..44138964ae 100644 --- a/tasks/GameUi/assets.py +++ b/tasks/GameUi/assets.py @@ -145,6 +145,8 @@ class GameUiAssets: I_CHECK_DAILY = RuleImage(roi_front=(28,515,58,62), roi_back=(1,487,154,145), threshold=0.8, method="Template matching", file="./tasks/GameUi/page/page_check_daily.png") # 商店页面标志 I_CHECK_MALL = RuleImage(roi_front=(231,455,131,78), roi_back=(201,432,185,121), threshold=0.8, method="Template matching", file="./tasks/GameUi/page/page_check_mall.png") + # 商店页面标志(紫色招牌) + I_CHECK_MALL_PURPLE = RuleImage(roi_front=(231,455,131,78), roi_back=(201,432,185,121), threshold=0.8, method="Template matching", file="./tasks/GameUi/page/page_check_mall_purple.png") # 商店推荐页面标志 I_CHECK_MALL_RECOMMEND = RuleImage(roi_front=(930,12,42,37), roi_back=(905,0,100,100), threshold=0.8, method="Template matching", file="./tasks/GameUi/page/page_check_mall_recommend.png") # 阴阳寮页面标志 diff --git a/tasks/GameUi/default_pages.py b/tasks/GameUi/default_pages.py index 2860d86581..b812840cec 100644 --- a/tasks/GameUi/default_pages.py +++ b/tasks/GameUi/default_pages.py @@ -95,7 +95,7 @@ def handle_login_page(task) -> bool: page_daily.connect(page_main, GlobalGameAssets.I_UI_BACK_YELLOW, key="page_daily->page_main") page_main.connect(page_daily, GameUiAssets.I_MAIN_GOTO_DAILY, key="page_main->page_daily") -page_mall = Page(GameUiAssets.I_CHECK_MALL, category="global") +page_mall = Page(any_of(GameUiAssets.I_CHECK_MALL, GameUiAssets.I_CHECK_MALL_PURPLE), category="global") page_mall.add_enter_failure_hooks(conditional_action(condition=GameUiAssets.I_CHECK_MAIN, action=RestartAssets.C_LOGIN_SCROLL_CLOSE_AREA)) page_mall.add_enter_success_hooks(GameUiAssets.I_AD_CLOSE_RED, GlobalGameAssets.I_UI_BACK_RED, GlobalGameAssets.I_UI_CANCEL_SAMLL) diff --git a/tasks/GameUi/page/image_main.json b/tasks/GameUi/page/image_main.json index ece4396bb8..935d51f9fa 100644 --- a/tasks/GameUi/page/image_main.json +++ b/tasks/GameUi/page/image_main.json @@ -170,6 +170,15 @@ "threshold": 0.8, "description": "商店页面标志" }, + { + "itemName": "check_mall_purple", + "imageName": "page_check_mall_purple.png", + "roiFront": "231,455,131,78", + "roiBack": "201,432,185,121", + "method": "Template matching", + "threshold": 0.8, + "description": "商店页面标志(紫色招牌)" + }, { "itemName": "check_mall_recommend", "imageName": "page_check_mall_recommend.png", diff --git a/tasks/GameUi/page/page_check_mall_purple.png b/tasks/GameUi/page/page_check_mall_purple.png new file mode 100644 index 0000000000..f6766029c2 Binary files /dev/null and b/tasks/GameUi/page/page_check_mall_purple.png differ diff --git a/tasks/RichMan/mall/navbar.py b/tasks/RichMan/mall/navbar.py index 713c9f69e4..aa725e54bc 100644 --- a/tasks/RichMan/mall/navbar.py +++ b/tasks/RichMan/mall/navbar.py @@ -6,7 +6,7 @@ from module.logger import logger -from tasks.GameUi.page import page_main, page_guild +from tasks.GameUi.page import page_main, page_guild, page_mall from tasks.GameUi.game_ui import GameUi from tasks.Component.Buy.buy import Buy from tasks.RichMan.assets import RichManAssets @@ -89,7 +89,7 @@ def back_mall(self): 返回商城 :return: """ - self.ui_click(self.I_UI_BACK_YELLOW, self.I_CHECK_MALL, interval=3, timeout=6) + self.goto_page(page_mall, skip_first_screenshot=False, timeout=6) def mall_resource(self, index: int) -> int: diff --git a/tests/test_mall_appearance.py b/tests/test_mall_appearance.py new file mode 100644 index 0000000000..f91bc90e0a --- /dev/null +++ b/tests/test_mall_appearance.py @@ -0,0 +1,150 @@ +"""Check both mall appearances and return paths without a device or OCR service.""" + +import unittest +from types import SimpleNamespace +from unittest.mock import patch + +import numpy as np + +from module.exception import GamePageUnknownError +from tasks.DailyTrifles.assets import DailyTriflesAssets +from tasks.DailyTrifles.page import page_store_gift_room +from tasks.DailyTrifles.script_task import ScriptTask +from tasks.GameUi.assets import GameUiAssets +from tasks.GameUi.game_ui import GameUi +from tasks.GameUi.page import page_main, page_mall, page_mall_recommend +from tasks.GameUi.session import NavigatorSession +from tasks.GlobalGame.assets import GlobalGameAssets +from tasks.RichMan.mall.navbar import MallNavbar + + +def frame_with(*rules): + """Build a synthetic frame using the unchanged asset positions.""" + frame = np.zeros((720, 1280, 3), dtype=np.uint8) + for rule in rules: + template = rule.image + x, y = rule.roi_front[:2] + height, width = template.shape[:2] + frame[y:y + height, x:x + width] = template + return frame + + +class OfflineMallUi(GameUi): + def __init__(self, mall_rule, state="mall"): + # Skip real Config/Device setup; keep the production matcher and navigator. + back = GlobalGameAssets.I_UI_BACK_YELLOW + self.frames = { + "mall": frame_with(mall_rule, back), + "main": frame_with(GameUiAssets.I_CHECK_MAIN, GameUiAssets.I_MAIN_GOTO_MALL), + "recommend": frame_with(GameUiAssets.I_CHECK_MALL_RECOMMEND, back), + "gift": frame_with(DailyTriflesAssets.I_GIFT_RECOMMEND, back), + "room": frame_with(back), + "blank": frame_with(), + } + self.state = state + self.after_sign = "gift" + self.clicks = [] + self.screenshot_count = 0 + self.interval_timer = {} + self.config = SimpleNamespace( + daily_trifles=SimpleNamespace( + trifles_config=SimpleNamespace(store_sign=True, buy_sushi_count=-1), + ), + script=SimpleNamespace(device=SimpleNamespace(control_method="minitouch")), + ) + self.device = SimpleNamespace( + image=self.frames[state], + app_lost_login=lambda: False, + app_is_running=lambda: True, + get_orientation=lambda: None, + ) + self.navigator = NavigatorSession(task_category="daily_trifles") + self.navigator.bootstrap([page_main, page_mall, page_mall_recommend, page_store_gift_room]) + + def screenshot(self): + self.screenshot_count += 1 + if self.screenshot_count > 100: + raise AssertionError("Mall navigation kept retrying") + self.device.image = self.frames[self.state] + return self.device.image + + def appear_then_click(self, target, **kwargs): + if not self.appear(target): + return False + self.clicks.append(target) + if target is self.I_UI_BACK_YELLOW: + self.state = "main" if self.state == "mall" else "mall" + elif target is self.I_MAIN_GOTO_MALL: + self.state = "recommend" + else: + raise AssertionError(f"Unexpected click: {target}") + return True + + def run_store_sign(self): + # Leave the cached mall screenshot stale to check that return takes a new one. + self.state = self.after_sign + + +class MallAppearanceTests(unittest.TestCase): + variants = (GameUiAssets.I_CHECK_MALL, GameUiAssets.I_CHECK_MALL_PURPLE) + + def setUp(self): + for target in ("tasks.GameUi.navigator.Timer.wait", "tasks.GameUi.navigator.time.sleep"): + patcher = patch(target) + patcher.start() + self.addCleanup(patcher.stop) + + def test_both_appearances_are_recognized_but_non_mall_is_not(self): + for rule in self.variants: + with self.subTest(rule=rule.name): + task = OfflineMallUi(rule) + self.assertTrue(task.confirm_page(page_mall)) + task.state = "main" + self.assertFalse(task.confirm_page(page_mall, skip_first_screenshot=False)) + + def test_entering_from_main_stops_at_either_mall_appearance(self): + for rule in self.variants: + with self.subTest(rule=rule.name): + task = OfflineMallUi(rule, state="main") + self.assertTrue(task.goto_page(page_mall)) + self.assertEqual(task.state, "mall") + self.assertEqual(task.clicks, [task.I_MAIN_GOTO_MALL, task.I_UI_BACK_YELLOW]) + + def test_return_does_not_click_back_when_already_in_mall(self): + for rule in self.variants: + with self.subTest(rule=rule.name): + task = OfflineMallUi(rule) + MallNavbar.back_mall(task) + self.assertEqual(task.state, "mall") + self.assertEqual(task.clicks, []) + + def test_daily_trifles_returns_from_gift_room_then_exits_to_main(self): + for rule in self.variants: + with self.subTest(rule=rule.name): + task = OfflineMallUi(rule) + ScriptTask.run_store(task) + self.assertEqual(task.state, "main") + self.assertEqual(task.clicks, [task.I_UI_BACK_YELLOW] * 2) + + def test_rich_man_returns_from_an_unregistered_shop_room(self): + for rule in self.variants: + with self.subTest(rule=rule.name): + task = OfflineMallUi(rule, state="room") + MallNavbar.back_mall(task) + self.assertEqual(task.state, "mall") + self.assertEqual(task.clicks, [task.I_UI_BACK_YELLOW]) + + def test_unrecognized_return_page_raises_instead_of_finishing(self): + for caller in (ScriptTask.run_store, MallNavbar.back_mall): + with self.subTest(caller=caller.__qualname__): + task = OfflineMallUi(self.variants[1], state="mall" if caller is ScriptTask.run_store else "blank") + task.after_sign = "blank" + with patch("tasks.GameUi.navigator.Timer") as timer: + timer.return_value.start.return_value.reached.return_value = True + with self.assertRaises(GamePageUnknownError): + caller(task) + self.assertEqual(task.clicks, []) + + +if __name__ == "__main__": + unittest.main()