From a0261d62ac27df57a7190cfc4f3a771b49009f86 Mon Sep 17 00:00:00 2001 From: sui-feng-cb <2518179942@qq.com> Date: Wed, 15 Oct 2025 03:45:59 +0800 Subject: [PATCH] Refractor: island transport preparation --- module/island/assets.py | 7 + module/island/island.py | 388 +--------------------------------- module/island/project.py | 417 ++++++++++++++++++++++++++++++++++++- module/island/transport.py | 310 ++------------------------- module/island/ui.py | 13 -- 5 files changed, 442 insertions(+), 693 deletions(-) diff --git a/module/island/assets.py b/module/island/assets.py index 2d2a23aa7..d2cb08a3e 100644 --- a/module/island/assets.py +++ b/module/island/assets.py @@ -16,6 +16,8 @@ ISLAND_TRANSPORT = Button(area={'cn': (905, 335, 986, 358), 'en': (905, 335, 986 ISLAND_TRANSPORT_CHECK = Button(area={'cn': (264, 154, 317, 180), 'en': (264, 154, 317, 180), 'jp': (264, 154, 317, 180), 'tw': (264, 154, 317, 180)}, color={'cn': (142, 143, 143), 'en': (142, 143, 143), 'jp': (142, 143, 143), 'tw': (142, 143, 143)}, button={'cn': (264, 154, 317, 180), 'en': (264, 154, 317, 180), 'jp': (264, 154, 317, 180), 'tw': (264, 154, 317, 180)}, file={'cn': './assets/cn/island/ISLAND_TRANSPORT_CHECK.png', 'en': './assets/cn/island/ISLAND_TRANSPORT_CHECK.png', 'jp': './assets/cn/island/ISLAND_TRANSPORT_CHECK.png', 'tw': './assets/cn/island/ISLAND_TRANSPORT_CHECK.png'}) OCR_PRODUCTION_TIME = Button(area={'cn': (714, 611, 808, 631), 'en': (714, 611, 808, 631), 'jp': (714, 611, 808, 631), 'tw': (714, 611, 808, 631)}, color={'cn': (120, 210, 255), 'en': (120, 210, 255), 'jp': (120, 210, 255), 'tw': (120, 210, 255)}, button={'cn': (714, 611, 808, 631), 'en': (714, 611, 808, 631), 'jp': (714, 611, 808, 631), 'tw': (714, 611, 808, 631)}, file={'cn': './assets/cn/island/OCR_PRODUCTION_TIME.png', 'en': './assets/cn/island/OCR_PRODUCTION_TIME.png', 'jp': './assets/cn/island/OCR_PRODUCTION_TIME.png', 'tw': './assets/cn/island/OCR_PRODUCTION_TIME.png'}) OCR_PRODUCTION_TIME_REMAIN = Button(area={'cn': (621, 427, 666, 439), 'en': (621, 427, 666, 439), 'jp': (621, 427, 666, 439), 'tw': (621, 427, 666, 439)}, color={'cn': (196, 203, 205), 'en': (196, 203, 205), 'jp': (196, 203, 205), 'tw': (196, 203, 205)}, button={'cn': (621, 427, 666, 439), 'en': (621, 427, 666, 439), 'jp': (621, 427, 666, 439), 'tw': (621, 427, 666, 439)}, file={'cn': './assets/cn/island/OCR_PRODUCTION_TIME_REMAIN.png', 'en': './assets/cn/island/OCR_PRODUCTION_TIME_REMAIN.png', 'jp': './assets/cn/island/OCR_PRODUCTION_TIME_REMAIN.png', 'tw': './assets/cn/island/OCR_PRODUCTION_TIME_REMAIN.png'}) +OCR_TRANSPORT_TIME = Button(area={'cn': (611, 139, 687, 154), 'en': (611, 139, 687, 154), 'jp': (611, 139, 687, 154), 'tw': (611, 139, 687, 154)}, color={'cn': (119, 119, 120), 'en': (119, 119, 120), 'jp': (119, 119, 120), 'tw': (119, 119, 120)}, button={'cn': (611, 139, 687, 154), 'en': (611, 139, 687, 154), 'jp': (611, 139, 687, 154), 'tw': (611, 139, 687, 154)}, file={'cn': './assets/cn/island/OCR_TRANSPORT_TIME.png', 'en': './assets/cn/island/OCR_TRANSPORT_TIME.png', 'jp': './assets/cn/island/OCR_TRANSPORT_TIME.png', 'tw': './assets/cn/island/OCR_TRANSPORT_TIME.png'}) +OCR_TRANSPORT_TIME_REMAIN = Button(area={'cn': (753, 210, 842, 230), 'en': (753, 210, 842, 230), 'jp': (753, 210, 842, 230), 'tw': (753, 210, 842, 230)}, color={'cn': (252, 203, 127), 'en': (252, 203, 127), 'jp': (252, 203, 127), 'tw': (252, 203, 127)}, button={'cn': (753, 210, 842, 230), 'en': (753, 210, 842, 230), 'jp': (753, 210, 842, 230), 'tw': (753, 210, 842, 230)}, file={'cn': './assets/cn/island/OCR_TRANSPORT_TIME_REMAIN.png', 'en': './assets/cn/island/OCR_TRANSPORT_TIME_REMAIN.png', 'jp': './assets/cn/island/OCR_TRANSPORT_TIME_REMAIN.png', 'tw': './assets/cn/island/OCR_TRANSPORT_TIME_REMAIN.png'}) PROJECT_COMPLETE = Button(area={'cn': (576, 394, 704, 414), 'en': (576, 394, 704, 414), 'jp': (576, 394, 704, 414), 'tw': (576, 394, 704, 414)}, color={'cn': (210, 210, 209), 'en': (210, 210, 209), 'jp': (210, 210, 209), 'tw': (210, 210, 209)}, button={'cn': (580, 533, 762, 584), 'en': (580, 533, 762, 584), 'jp': (580, 533, 762, 584), 'tw': (580, 533, 762, 584)}, file={'cn': './assets/cn/island/PROJECT_COMPLETE.png', 'en': './assets/cn/island/PROJECT_COMPLETE.png', 'jp': './assets/cn/island/PROJECT_COMPLETE.png', 'tw': './assets/cn/island/PROJECT_COMPLETE.png'}) PROJECT_START = Button(area={'cn': (686, 610, 706, 630), 'en': (686, 610, 706, 630), 'jp': (686, 610, 706, 630), 'tw': (686, 610, 706, 630)}, color={'cn': (129, 213, 255), 'en': (129, 213, 255), 'jp': (129, 213, 255), 'tw': (129, 213, 255)}, button={'cn': (494, 599, 1087, 642), 'en': (494, 599, 1087, 642), 'jp': (494, 599, 1087, 642), 'tw': (494, 599, 1087, 642)}, file={'cn': './assets/cn/island/PROJECT_START.png', 'en': './assets/cn/island/PROJECT_START.png', 'jp': './assets/cn/island/PROJECT_START.png', 'tw': './assets/cn/island/PROJECT_START.png'}) ROLE_SELECT_CHECK = Button(area={'cn': (1162, 182, 1214, 205), 'en': (1162, 182, 1214, 205), 'jp': (1162, 182, 1214, 205), 'tw': (1162, 182, 1214, 205)}, color={'cn': (221, 192, 128), 'en': (221, 192, 128), 'jp': (221, 192, 128), 'tw': (221, 192, 128)}, button={'cn': (1162, 182, 1214, 205), 'en': (1162, 182, 1214, 205), 'jp': (1162, 182, 1214, 205), 'tw': (1162, 182, 1214, 205)}, file={'cn': './assets/cn/island/ROLE_SELECT_CHECK.png', 'en': './assets/cn/island/ROLE_SELECT_CHECK.png', 'jp': './assets/cn/island/ROLE_SELECT_CHECK.png', 'tw': './assets/cn/island/ROLE_SELECT_CHECK.png'}) @@ -25,3 +27,8 @@ TEMPLATE_ISLAND_MANJUU = Template(file={'cn': './assets/cn/island/TEMPLATE_ISLAN TEMPLATE_ISLAND_SWITCH = Template(file={'cn': './assets/cn/island/TEMPLATE_ISLAND_SWITCH.png', 'en': './assets/cn/island/TEMPLATE_ISLAND_SWITCH.png', 'jp': './assets/cn/island/TEMPLATE_ISLAND_SWITCH.png', 'tw': './assets/cn/island/TEMPLATE_ISLAND_SWITCH.png'}) TEMPLATE_PROJECT_LOCKED = Template(file={'cn': './assets/cn/island/TEMPLATE_PROJECT_LOCKED.png', 'en': './assets/cn/island/TEMPLATE_PROJECT_LOCKED.png', 'jp': './assets/cn/island/TEMPLATE_PROJECT_LOCKED.png', 'tw': './assets/cn/island/TEMPLATE_PROJECT_LOCKED.png'}) TEMPLATE_SLOT_LOCKED = Template(file={'cn': './assets/cn/island/TEMPLATE_SLOT_LOCKED.png', 'en': './assets/cn/island/TEMPLATE_SLOT_LOCKED.png', 'jp': './assets/cn/island/TEMPLATE_SLOT_LOCKED.png', 'tw': './assets/cn/island/TEMPLATE_SLOT_LOCKED.png'}) +TRANSPORT_LOCKED = Button(area={'cn': (659, 380, 683, 412), 'en': (659, 380, 683, 412), 'jp': (659, 380, 683, 412), 'tw': (659, 380, 683, 412)}, color={'cn': (192, 192, 190), 'en': (192, 192, 190), 'jp': (192, 192, 190), 'tw': (192, 192, 190)}, button={'cn': (659, 380, 683, 412), 'en': (659, 380, 683, 412), 'jp': (659, 380, 683, 412), 'tw': (659, 380, 683, 412)}, file={'cn': './assets/cn/island/TRANSPORT_LOCKED.png', 'en': './assets/cn/island/TRANSPORT_LOCKED.png', 'jp': './assets/cn/island/TRANSPORT_LOCKED.png', 'tw': './assets/cn/island/TRANSPORT_LOCKED.png'}) +TRANSPORT_RECEIVE = Button(area={'cn': (938, 206, 1065, 235), 'en': (938, 206, 1065, 235), 'jp': (938, 206, 1065, 235), 'tw': (938, 206, 1065, 235)}, color={'cn': (76, 195, 255), 'en': (76, 195, 255), 'jp': (76, 195, 255), 'tw': (76, 195, 255)}, button={'cn': (938, 206, 1065, 235), 'en': (938, 206, 1065, 235), 'jp': (938, 206, 1065, 235), 'tw': (938, 206, 1065, 235)}, file={'cn': './assets/cn/island/TRANSPORT_RECEIVE.png', 'en': './assets/cn/island/TRANSPORT_RECEIVE.png', 'jp': './assets/cn/island/TRANSPORT_RECEIVE.png', 'tw': './assets/cn/island/TRANSPORT_RECEIVE.png'}) +TRANSPORT_START = Button(area={'cn': (1142, 170, 1174, 281), 'en': (1142, 170, 1174, 281), 'jp': (1142, 170, 1174, 281), 'tw': (1142, 170, 1174, 281)}, color={'cn': (98, 202, 255), 'en': (98, 202, 255), 'jp': (98, 202, 255), 'tw': (98, 202, 255)}, button={'cn': (1142, 170, 1174, 281), 'en': (1142, 170, 1174, 281), 'jp': (1142, 170, 1174, 281), 'tw': (1142, 170, 1174, 281)}, file={'cn': './assets/cn/island/TRANSPORT_START.png', 'en': './assets/cn/island/TRANSPORT_START.png', 'jp': './assets/cn/island/TRANSPORT_START.png', 'tw': './assets/cn/island/TRANSPORT_START.png'}) +TRANSPORT_STATUS_PENDING = Button(area={'cn': (459, 138, 522, 156), 'en': (459, 138, 522, 156), 'jp': (459, 138, 522, 156), 'tw': (459, 138, 522, 156)}, color={'cn': (120, 121, 121), 'en': (120, 121, 121), 'jp': (120, 121, 121), 'tw': (120, 121, 121)}, button={'cn': (459, 138, 522, 156), 'en': (459, 138, 522, 156), 'jp': (459, 138, 522, 156), 'tw': (459, 138, 522, 156)}, file={'cn': './assets/cn/island/TRANSPORT_STATUS_PENDING.png', 'en': './assets/cn/island/TRANSPORT_STATUS_PENDING.png', 'jp': './assets/cn/island/TRANSPORT_STATUS_PENDING.png', 'tw': './assets/cn/island/TRANSPORT_STATUS_PENDING.png'}) +TRANSPORT_STATUS_RUNNING = Button(area={'cn': (459, 138, 522, 156), 'en': (459, 138, 522, 156), 'jp': (459, 138, 522, 156), 'tw': (459, 138, 522, 156)}, color={'cn': (137, 136, 133), 'en': (137, 136, 133), 'jp': (137, 136, 133), 'tw': (137, 136, 133)}, button={'cn': (459, 138, 522, 156), 'en': (459, 138, 522, 156), 'jp': (459, 138, 522, 156), 'tw': (459, 138, 522, 156)}, file={'cn': './assets/cn/island/TRANSPORT_STATUS_RUNNING.png', 'en': './assets/cn/island/TRANSPORT_STATUS_RUNNING.png', 'jp': './assets/cn/island/TRANSPORT_STATUS_RUNNING.png', 'tw': './assets/cn/island/TRANSPORT_STATUS_RUNNING.png'}) diff --git a/module/island/island.py b/module/island/island.py index 77d1fb5ca..3fb6a2bf5 100644 --- a/module/island/island.py +++ b/module/island/island.py @@ -1,389 +1,14 @@ -import cv2 -import numpy as np -from scipy import signal import module.config.server as server -from module.base.timer import Timer -from module.base.utils import color_similarity_2d, random_rectangle_vector, rgb2gray -from module.config.deep import deep_get from module.island.assets import * from module.island.project_data import * -from module.island.project import IslandItem, IslandProduct, IslandProject -from module.island.ui import IslandUI +from module.island.project import IslandProjectRun +from module.island.transport import IslandTransportRun from module.logger import logger -from module.map.map_grids import SelectedGrids from module.ui.page import page_dormmenu, page_island, page_island_phone, page_main -class Island(IslandUI): - project = SelectedGrids([]) - total = SelectedGrids([]) - - def project_detect(self, image): - """ - Get all projects from an image. - - Args: - image (np.ndarray): - - Returns: - SelectedGrids: - """ - image_gray = rgb2gray(image) - projects = SelectedGrids([IslandProject(image, image_gray, button) - for button in TEMPLATE_ISLAND_SWITCH.match_multi(image_gray)]) - return projects.select(valid=True) - - def project_receive(self, button, skip_first_screenshot=True): - """ - Receive and start a project. - - Args: - button (Button): project button to click - - Returns: - bool: if received. - """ - logger.hr('Island Project', level=2) - self.device.click_record_clear() - self.interval_clear([ISLAND_MANAGEMENT_CHECK, PROJECT_COMPLETE, - GET_ITEMS_ISLAND, ROLE_SELECT_ENTER]) - received = False - enter = True - timeout = Timer(3, count=6).start() - while 1: - if skip_first_screenshot: - skip_first_screenshot = False - else: - self.device.screenshot() - - if self.island_in_management(interval=5): - self.device.click(button) - timeout.reset() - continue - - if self.handle_info_bar(): - timeout.reset() - continue - - if enter and self.appear_then_click(ROLE_SELECT_ENTER, offset=(5, 5), interval=2): - received = True - self.interval_clear(GET_ITEMS_ISLAND) - timeout.reset() - continue - - if self.appear_then_click(PROJECT_COMPLETE, offset=(20, 20), interval=2): - received = True - enter = False - self.interval_clear(GET_ITEMS_ISLAND) - self.interval_reset(ROLE_SELECT_ENTER) - timeout.reset() - continue - - if self.appear_then_click(GET_ITEMS_ISLAND, offset=(20, 20), interval=2): - enter = True - self.interval_clear(ROLE_SELECT_ENTER) - timeout.reset() - continue - - if self.appear(ROLE_SELECT_CONFIRM, offset=(20, 20)): - break - if timeout.reached(): - break - - if not received: - product = IslandProduct(self.device.image) - if product.valid: - self.total = self.total.add_by_eq(SelectedGrids([product])) - self.device.click(ISLAND_CLICK_SAFE_AREA) - break - else: - self.interval_clear(ROLE_SELECT_ENTER) - - return received - - def island_select_manjuu(self, button): - """ - Args: - button (Button): role button to click - """ - self.interval_clear([ROLE_SELECT_CONFIRM, ISLAND_AMOUNT_MAX]) - for _ in self.loop(): - if self.appear(ROLE_SELECT_CHECK, offset=(20, 20)): - break - - if self.appear(ROLE_SELECT_CONFIRM, offset=(20, 20), interval=5): - self.device.click(button) - continue - - self.ui_click( - click_button=ROLE_SELECT_CONFIRM, - check_button=ISLAND_AMOUNT_MAX, - offset=(20, 20), - retry_wait=3, - skip_first_screenshot=True - ) - - def island_select_role(self, skip_first_screenshot=True): - """ - Select a role to produce. - - Args: - skip_first_screenshot (bool): - - Returns: - bool: if selected - """ - logger.info('Island select role') - timeout = Timer(1.5, count=3).start() - while 1: - if skip_first_screenshot: - skip_first_screenshot = False - else: - self.device.screenshot() - - if timeout.reached(): - self.island_product_quit() - return False - - image = self.image_crop((0, 0, 910, 1280), copy=False) - sim, button = TEMPLATE_ISLAND_MANJUU.match_result(image) - if sim > 0.9: - self.island_select_manjuu(button) - return True - else: - logger.info('No manjuu found') - continue - - def island_current_product(self): - """ - Get currently selected product on self.device.image. - - Returns: - IslandItem: currently selected item - """ - image = self.image_crop(ISLAND_PRODUCT_ITEMS, copy=False) - y_top = ISLAND_PRODUCT_ITEMS.area[1] - line = cv2.reduce(image, 1, cv2.REDUCE_AVG) - # blue line - line = color_similarity_2d(line, color=(57, 189, 255))[:, 0] - parameters = { - 'height': 200, - 'distance': 50, - } - peaks, _ = signal.find_peaks(line, **parameters) - peaks = np.array(peaks) + y_top - return IslandItem(self.device.image, peaks) - - def island_select_product(self, option, trial=2, skip_first_screenshot=True): - """ - Select a product in items list. - - Args: - option (str): option to select - trail (int): retry times - skip_first_screenshot (bool): - - Returns: - bool: if selected - """ - logger.hr('Island Select Product') - last = None - click_interval = Timer(1) - while 1: - if skip_first_screenshot: - skip_first_screenshot = False - else: - self.device.screenshot() - - current = self.island_current_product() - if trial > 0 and not len(current.items): - trial -= 1 - continue - if trial <= 0: - self.island_product_quit() - return False - - if option == current.name: - logger.info(f'Selected item {option}') - return True - - drag = True - for item in current.items: - if option == item.name: - if click_interval.reached(): - self.device.click(item.button) - self.device.sleep(0.2) - click_interval.reset() - drag = False - - if last == current.items[-1]: - logger.info(f'Reach the bottom of items, dit not match item {option}') - self.island_product_quit() - return False - - if drag: - last = current.items[-1] - self.device.click(last.button) - self.island_drag_next_page((0, -300), ISLAND_PRODUCT_ITEMS.area, 0.5) - - def island_product_confirm(self, skip_first_screenshot=True): - """ - Start the product after product selected. - - Args: - skip_first_screenshot (bool): - """ - logger.info('Island product confirm') - last = None - success = False - timeout = Timer(1.5, count=3).start() - while 1: - if skip_first_screenshot: - skip_first_screenshot = False - else: - self.device.screenshot() - - if timeout.reached(): - break - if self.image_color_count(PROJECT_START, color=(151, 155, 155), threshold=221, count=200): - self.island_product_quit() - break - - if not success: - if self.appear_then_click(ISLAND_AMOUNT_MAX, offset=(5, 5), interval=5): - timeout.reset() - continue - - product = IslandProduct(self.device.image, new=True) - if product == last: - success = True - self.total = self.total.add_by_eq(SelectedGrids([product])) - timeout.reset() - continue - last = product - else: - if self.appear_then_click(PROJECT_START, offset=(20,20), interval=2): - timeout.reset() - self.interval_reset(ISLAND_MANAGEMENT_CHECK) - continue - - if self.info_bar_count(): - self.island_product_quit() - break - if self.island_in_management(): - break - - def island_drag_next_page(self, vector, box, sleep=0.5): - """ - Drag to the next page. - - Args: - vector (tuple): - box (tuple): - sleep (float): - """ - logger.info('Island drag to next page') - p1, p2 = random_rectangle_vector(vector, box=box, random_range=(0, -5, 0, 5)) - self.device.drag(p1, p2, segments=2, shake=(0, 25), point_random=(0, 0, 0, 0), shake_random=(0, -5, 0, 5)) - self.device.sleep(sleep) - - def ensure_project(self, project: IslandProject, trial=7, skip_first_screenshot=True): - logger.hr('Project ensure') - for _ in range(trial): - if skip_first_screenshot: - skip_first_screenshot = False - else: - self.device.screenshot() - - projects = self.project_detect(self.device.image) - if not projects: - continue - if project.name in projects.get('name'): - logger.info(f'Ensured project: {project}') - break - - self.island_drag_next_page((0, -500), ISLAND_PROJECT_SWIPE.area, 0.6) - - def island_project_run(self, names, trial=2, skip_first_screenshot=True): - """ - Execute island run to receive and start project. - - Args: - names (list[str]): a list of name for island receive - trial (int): retry times - skip_first_screenshot (bool): - - Returns: - list[timedelta]: future finish timedelta - """ - logger.hr('Island Project Run', level=1) - end = False - timeout = Timer(3, count=3).start() - while 1: - if skip_first_screenshot: - skip_first_screenshot = False - else: - self.device.screenshot() - - if timeout.reached(): - break - - projects = self.project_detect(self.device.image) - if trial > 0 and not projects: - trial -= 1 - continue - projects: SelectedGrids = projects.filter( - lambda proj: proj.name in names and proj.name not in self.project.get('name')) - self.project = self.project.add_by_eq(projects) - - for proj in projects: - if proj.name == names[-1]: - end = True - proj_config = self.island_project_config(proj) - - for button, option, index in zip( - proj.slot_buttons.buttons, proj_config, range(len(proj_config))): - if option is None: - continue - if self.project_receive(button): - if self.island_select_role(): - if self.island_select_product(option): - self.island_product_confirm() - self.ui_ensure_management_page() - if not end or index != len(proj_config) - 1: - self.ensure_project(proj) - timeout.reset() - - if end: - break - self.island_drag_next_page((0, -500), ISLAND_PROJECT_SWIPE.area, 0.6) - - # task delay - future_finish = sorted([f for f in self.total.get('finish_time') if f is not None]) - logger.info(f'Project finish: {[str(f) for f in future_finish]}') - if not len(future_finish): - logger.info('No island project running') - return future_finish - - def island_project_config(self, project: IslandProject): - """ - Args: - project (IslandProject): - - Returns: - list[str]: a list of options for production - """ - slot_option = [] - proj_id = project.id - for proj_slot in range(1, project.slot + 1): - option = self.config.__getattribute__(f'Island{proj_id}_Option{proj_slot}') - if option == 0: - slot_option.append(None) - continue - slot_option.append(deep_get(items_data_cn, [proj_id, option])) - return slot_option - +class Island(IslandProjectRun, IslandTransportRun): @staticmethod def island_config_to_names(config): """ @@ -398,12 +23,10 @@ class Island(IslandUI): else: return [] - def island_transport_run(self): - - return [] - def island_run(self, transport=False, project=True, names=None): """ + Execute island routine. + Args: transport (bool): project (bool): @@ -425,6 +48,7 @@ class Island(IslandUI): self.config.Scheduler_Enable = False return False + # task delay if len(future_finish): self.config.task_delay(target=future_finish) else: diff --git a/module/island/project.py b/module/island/project.py index 7b88f9eeb..dddc8d050 100644 --- a/module/island/project.py +++ b/module/island/project.py @@ -1,13 +1,19 @@ from datetime import datetime, timedelta +import cv2 import re +import numpy as np +from scipy import signal from module.base.button import Button, ButtonGrid -from module.base.utils import crop -from module.config.deep import deep_values +from module.base.timer import Timer +from module.base.utils import color_similarity_2d, crop, random_rectangle_vector, rgb2gray +from module.config.deep import deep_get, deep_values from module.island.assets import * from module.island.project_data import * -from module.island.ui import OCR_PRODUCTION_TIME, OCR_PRODUCTION_TIME_REMAIN -from module.ocr.ocr import Ocr +from module.island.ui import IslandUI +from module.logger import logger +from module.map.map_grids import SelectedGrids +from module.ocr.ocr import Duration, Ocr class ProjectNameOcr(Ocr): @@ -18,11 +24,17 @@ class ProjectNameOcr(Ocr): class IslandProject: + # If success to parse project valid: bool + # OCR result name: str + # Project workplace id id: int + # max slot that the workplace has max_slot: int + # available slots that the workplace has slot: int + # buttons of all available slots slot_buttons: ButtonGrid def __init__(self, image, image_gray, button): @@ -109,15 +121,27 @@ class ItemNameOcr(Ocr): return result +class IslandProductionTime(Duration): + def after_process(self, result): + result = super().after_process(result) + if result == '0:40:00': + result = '01:40:00' + return result + + class IslandProduct: + # Duration to run this product duration: timedelta + # If success to parse product duration valid: bool def __init__(self, image, new=False): if new: - self.duration = OCR_PRODUCTION_TIME.ocr(image) + ocr = IslandProductionTime(OCR_PRODUCTION_TIME, lang='azur_lane_jp', name='OCR_PRODUCTION_TIME') + self.duration = ocr.ocr(image) else: - self.duration = OCR_PRODUCTION_TIME_REMAIN.ocr(image) + ocr = Duration(OCR_PRODUCTION_TIME_REMAIN, name='OCR_PRODUCTION_TIME_REMAIN') + self.duration = ocr.ocr(image) self.valid = True if not self.duration.total_seconds(): @@ -151,6 +175,15 @@ class IslandProduct: return True class IslandItem: + # OCR result + name: str + # If success to parse item name + valid: bool + # Button to click for the current item + button: Button + # All buttons on this page to click + item_buttons: ButtonGrid + def __init__(self, image, y, get_button=True): self.image = image self.y = y @@ -238,4 +271,374 @@ class IslandItem: if self.name != other.name: return False - return True \ No newline at end of file + return True + + +class IslandProjectRun(IslandUI): + project = SelectedGrids([]) + total = SelectedGrids([]) + + def project_detect(self, image): + """ + Get all projects from an image. + + Args: + image (np.ndarray): + + Returns: + SelectedGrids: + """ + image_gray = rgb2gray(image) + projects = SelectedGrids([IslandProject(image, image_gray, button) + for button in TEMPLATE_ISLAND_SWITCH.match_multi(image_gray)]) + return projects.select(valid=True) + + def project_receive(self, button, skip_first_screenshot=True): + """ + Receive and start a project. + + Args: + button (Button): project button to click + + Returns: + bool: if received. + """ + logger.hr('Island Project', level=2) + self.device.click_record_clear() + self.interval_clear([ISLAND_MANAGEMENT_CHECK, PROJECT_COMPLETE, + GET_ITEMS_ISLAND, ROLE_SELECT_ENTER]) + received = False + enter = True + timeout = Timer(3, count=6).start() + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + if self.island_in_management(interval=5): + self.device.click(button) + timeout.reset() + continue + + if self.handle_info_bar(): + timeout.reset() + continue + + if enter and self.appear_then_click(ROLE_SELECT_ENTER, offset=(5, 5), interval=2): + received = True + self.interval_clear(GET_ITEMS_ISLAND) + timeout.reset() + continue + + if self.appear_then_click(PROJECT_COMPLETE, offset=(20, 20), interval=2): + received = True + enter = False + self.interval_clear(GET_ITEMS_ISLAND) + self.interval_reset(ROLE_SELECT_ENTER) + timeout.reset() + continue + + if self.appear_then_click(GET_ITEMS_ISLAND, offset=(20, 20), interval=2): + enter = True + self.interval_clear(ROLE_SELECT_ENTER) + timeout.reset() + continue + + if self.appear(ROLE_SELECT_CONFIRM, offset=(20, 20)): + break + if timeout.reached(): + break + + if not received: + product = IslandProduct(self.device.image) + if product.valid: + self.total = self.total.add_by_eq(SelectedGrids([product])) + self.device.click(ISLAND_CLICK_SAFE_AREA) + break + else: + self.interval_clear(ROLE_SELECT_ENTER) + + return received + + def island_select_manjuu(self, button): + """ + Args: + button (Button): role button to click + """ + self.interval_clear([ROLE_SELECT_CONFIRM, ISLAND_AMOUNT_MAX]) + for _ in self.loop(): + if self.appear(ROLE_SELECT_CHECK, offset=(20, 20)): + break + + if self.appear(ROLE_SELECT_CONFIRM, offset=(20, 20), interval=5): + self.device.click(button) + continue + + self.ui_click( + click_button=ROLE_SELECT_CONFIRM, + check_button=ISLAND_AMOUNT_MAX, + offset=(20, 20), + retry_wait=3, + skip_first_screenshot=True + ) + + def island_select_role(self, skip_first_screenshot=True): + """ + Select a role to produce. + + Args: + skip_first_screenshot (bool): + + Returns: + bool: if selected + """ + logger.info('Island select role') + timeout = Timer(1.5, count=3).start() + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + if timeout.reached(): + self.island_product_quit() + return False + + image = self.image_crop((0, 0, 910, 1280), copy=False) + sim, button = TEMPLATE_ISLAND_MANJUU.match_result(image) + if sim > 0.9: + self.island_select_manjuu(button) + return True + else: + logger.info('No manjuu found') + continue + + def island_current_product(self): + """ + Get currently selected product on self.device.image. + + Returns: + IslandItem: currently selected item + """ + image = self.image_crop(ISLAND_PRODUCT_ITEMS, copy=False) + y_top = ISLAND_PRODUCT_ITEMS.area[1] + line = cv2.reduce(image, 1, cv2.REDUCE_AVG) + # blue line + line = color_similarity_2d(line, color=(57, 189, 255))[:, 0] + parameters = { + 'height': 200, + 'distance': 50, + } + peaks, _ = signal.find_peaks(line, **parameters) + peaks = np.array(peaks) + y_top + return IslandItem(self.device.image, peaks) + + def island_select_product(self, option, trial=2, skip_first_screenshot=True): + """ + Select a product in items list. + + Args: + option (str): option to select + trail (int): retry times + skip_first_screenshot (bool): + + Returns: + bool: if selected + """ + logger.hr('Island Select Product') + last = None + click_interval = Timer(1) + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + current = self.island_current_product() + if trial > 0 and not len(current.items): + trial -= 1 + continue + if trial <= 0: + self.island_product_quit() + return False + + if option == current.name: + logger.info(f'Selected item {option}') + return True + + drag = True + for item in current.items: + if option == item.name: + if click_interval.reached(): + self.device.click(item.button) + self.device.sleep(0.2) + click_interval.reset() + drag = False + + if last == current.items[-1]: + logger.info(f'Reach the bottom of items, dit not match item {option}') + self.island_product_quit() + return False + + if drag: + last = current.items[-1] + self.device.click(last.button) + self.island_drag_next_page((0, -300), ISLAND_PRODUCT_ITEMS.area, 0.5) + + def island_product_confirm(self, skip_first_screenshot=True): + """ + Start the product after product selected. + + Args: + skip_first_screenshot (bool): + """ + logger.info('Island product confirm') + last = None + success = False + timeout = Timer(1.5, count=3).start() + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + if timeout.reached(): + break + if self.image_color_count(PROJECT_START, color=(151, 155, 155), threshold=221, count=200): + self.island_product_quit() + break + + if not success: + if self.appear_then_click(ISLAND_AMOUNT_MAX, offset=(5, 5), interval=5): + timeout.reset() + continue + + product = IslandProduct(self.device.image, new=True) + if product == last: + success = True + self.total = self.total.add_by_eq(SelectedGrids([product])) + timeout.reset() + continue + last = product + else: + if self.appear_then_click(PROJECT_START, offset=(20,20), interval=2): + timeout.reset() + self.interval_reset(ISLAND_MANAGEMENT_CHECK) + continue + + if self.info_bar_count(): + self.island_product_quit() + break + if self.island_in_management(): + break + + def island_drag_next_page(self, vector, box, sleep=0.5): + """ + Drag to the next page. + + Args: + vector (tuple): + box (tuple): + sleep (float): + """ + logger.info('Island drag to next page') + p1, p2 = random_rectangle_vector(vector, box=box, random_range=(0, -5, 0, 5)) + self.device.drag(p1, p2, segments=2, shake=(0, 25), point_random=(0, 0, 0, 0), shake_random=(0, -5, 0, 5)) + self.device.sleep(sleep) + + def ensure_project(self, project: IslandProject, trial=7, skip_first_screenshot=True): + logger.hr('Project ensure') + for _ in range(trial): + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + projects = self.project_detect(self.device.image) + if not projects: + continue + if project.name in projects.get('name'): + logger.info(f'Ensured project: {project}') + break + + self.island_drag_next_page((0, -500), ISLAND_PROJECT_SWIPE.area, 0.6) + + def island_project_config(self, project: IslandProject): + """ + Args: + project (IslandProject): + + Returns: + list[str]: a list of options for production + """ + slot_option = [] + proj_id = project.id + for proj_slot in range(1, project.slot + 1): + option = self.config.__getattribute__(f'Island{proj_id}_Option{proj_slot}') + if option == 0: + slot_option.append(None) + continue + slot_option.append(deep_get(items_data_cn, [proj_id, option])) + return slot_option + + def island_project_run(self, names, trial=2, skip_first_screenshot=True): + """ + Execute island run to receive and start project. + + Args: + names (list[str]): a list of name for island receive + trial (int): retry times + skip_first_screenshot (bool): + + Returns: + list[timedelta]: future finish timedelta + """ + logger.hr('Island Project Run', level=1) + end = False + timeout = Timer(3, count=3).start() + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + if timeout.reached(): + break + + projects = self.project_detect(self.device.image) + if trial > 0 and not projects: + trial -= 1 + continue + projects: SelectedGrids = projects.filter( + lambda proj: proj.name in names and proj.name not in self.project.get('name')) + self.project = self.project.add_by_eq(projects) + + for proj in projects: + if proj.name == names[-1]: + end = True + proj_config = self.island_project_config(proj) + + for button, option, index in zip( + proj.slot_buttons.buttons, proj_config, range(len(proj_config))): + if option is None: + continue + if self.project_receive(button): + if self.island_select_role(): + if self.island_select_product(option): + self.island_product_confirm() + self.ui_ensure_management_page() + if not end or index != len(proj_config) - 1: + self.ensure_project(proj) + timeout.reset() + + if end: + break + self.island_drag_next_page((0, -500), ISLAND_PROJECT_SWIPE.area, 0.6) + + # task delay + future_finish = sorted([f for f in self.total.get('finish_time') if f is not None]) + logger.info(f'Project finish: {[str(f) for f in future_finish]}') + if not len(future_finish): + logger.info('No island project running') + return future_finish diff --git a/module/island/transport.py b/module/island/transport.py index 8af66c569..de326a3a7 100644 --- a/module/island/transport.py +++ b/module/island/transport.py @@ -1,18 +1,11 @@ from datetime import datetime, timedelta -import numpy as np -import re -from cached_property import cached_property - -from module.base.button import ButtonGrid -from module.base.timer import Timer -from module.base.utils import area_offset, crop, image_color_count, rgb2gray +from module.base.utils import area_offset from module.island.assets import * from module.island.ui import IslandUI from module.logger import logger from module.map.map_grids import SelectedGrids from module.ocr.ocr import Duration -from module.ui_white.assets import POPUP_CANCEL_WHITE, POPUP_CONFIRM_WHITE class IslandTransport: @@ -20,37 +13,20 @@ class IslandTransport: index: int # If success to parse transport commission valid: bool - # If the commission is locked - locked: bool # Duration to run this transport commission duration: timedelta # Status of transport commission - # Value: finished, running, pending, refreshing, unknown + # Value: finished, running, pending, unknown status: str - # If the transprt commission need to start - start: bool - # If the transprt commission need to refresh - refresh: bool - def __init__(self, main, index, blacklist): - """ - Args: - main: - index (int): - blacklist (list[Template]): a blacklist of templates of items to submit - """ + def __init__(self, main, index): self.index = index - self.blacklist = blacklist - self.image = main.device.image self.valid = True - self.locked = False self.duration = None - self.start = True - self.refresh = False - self.items = SelectedGrids([]) + self.can_start = True self.parse_transport(main) if not self.valid: - self.start = False + self.can_start = False self.create_time = datetime.now() def parse_transport(self, main): @@ -61,7 +37,7 @@ class IslandTransport: # commission locked lock_offset = area_offset(offset, (0, delta * (self.index - 1))) if self.index >= 1 and main.appear(TRANSPORT_LOCKED, lock_offset): - self.locked = True + self.valid = False return self.status = self.get_transport_status(main) @@ -71,42 +47,25 @@ class IslandTransport: elif self.status == 'pending': button = OCR_TRANSPORT_TIME.move((0, self.offset[1] + 20)) ocr = Duration(button, lang='cnocr', letter=(207, 207, 207), name='OCR_TRANSPORT_TIME') - self.duration = ocr.ocr(self.image) + self.duration = ocr.ocr(main.device.image) if not self.duration.total_seconds(): self.valid = False return - # items info - origin_y = 174 + delta * self.index - grids = ButtonGrid(origin=(481, origin_y), delta=(105, 0), - button_shape=(86, 86), grid_shape=(3, 1), name='ITEMS') - self.items = SelectedGrids([TransportItem(self.image, button, self.blacklist) - for button in grids.buttons]).select(valid=True) - self.start = self.items.select(load=True).count == self.items.count - self.refresh = main.appear(TRANSPORT_REFRESH, offset=self.offset) and \ - bool(self.items.select(refresh=True).count) - - # detect items first because we need to get refresh info if not main.match_template_color(TRANSPORT_START, offset=self.offset): - self.start = False + self.can_start = False + return elif self.status == 'running': - self.start = False + self.can_start = False + button = OCR_TRANSPORT_TIME_REMAIN.move((0, self.offset[1] + 20)) ocr = Duration(button, name='OCR_TRANSPORT_TIME') - self.duration = ocr.ocr(self.image) + self.duration = ocr.ocr(main.device.image) if not self.duration.total_seconds(): self.valid = False return elif self.status == 'finished': - self.start = False - elif self.status == 'refreshing': - self.start = False - button = OCR_TRANSPORT_REFRESH.move((0, self.offset[1] + 20)) - ocr = Duration(button, letter=(63, 64, 66), name='OCR_TRANSPORT_REFRESH') - self.duration = ocr.ocr(self.image) - if not self.duration.total_seconds(): - self.valid = False - return + self.can_start = False def get_transport_status(self, main): if main.appear(TRANSPORT_STATUS_PENDING, offset=self.offset): @@ -115,24 +74,12 @@ class IslandTransport: return 'running' elif main.appear(TRANSPORT_RECEIVE, offset=self.offset): return 'finished' - elif main.appear(TRANSPORT_REFRESH_CHECK, offset=self.offset): - return 'refreshing' else: return 'unknown' - def convert_to_refreshing(self): - if self.valid: - self.status = 'refreshing' - self.start = False - self.refresh = False - self.duration = timedelta(hours=0, minutes=30, seconds=0) - self.create_time = datetime.now() - def convert_to_running(self): if self.valid: self.status = 'running' - self.start = False - self.refresh = False self.create_time = datetime.now() @property @@ -145,87 +92,15 @@ class IslandTransport: def __str__(self): if not self.valid: return f'Index: {self.index} (Invalid)' - if self.locked: - return f'Index: {self.index} (Locked)' info = {'Index': self.index, 'Status': self.status} if self.duration: info['Duration'] = self.duration - info['Start'] = self.start - info['Refresh'] = self.refresh - info = ', '.join([f'{k}: {v}' for k, v in info.items()]) - return info - - -class TransportItem: - # If the item is enough to submit and not in blacklist - load: bool - # If the item is in blacklist - refresh: bool - - def __init__(self, image, button, blacklist): - """ - Args: - image: - button: - blacklist: - """ - self.image_raw = image - self.button = button - self.blacklist = blacklist - self.image = crop(image, button.area) - self.valid = self.predict_valid() - self.refresh = False - self.load = self.predict_load() - - def predict_valid(self): - # gray item means an empty item - mean = np.mean(np.max(self.image, axis=2) > 234) - # blue bar on top of the item means already loaded - blue_bar_check = image_color_count(self.image[:10, :, :], color=(90, 201, 255), threshold=221, count=500) - return mean > 0.3 and not blue_bar_check - - def predict_load(self): - if not self.valid: - return False - self.refresh = self.handle_blacklist_items() - if self.refresh: - return False - if not TEMPLATE_ITEM_SATISFIED.match(rgb2gray(self.image)): - return False - return True - - def handle_blacklist_items(self): - """ - Check if current item is a blacklist item. - - Returns: - bool: if any blacklist item - """ - for template in self.blacklist: - if template.match(self.image): - return True - return False - - def __str__(self): - if not self.valid: - return '(Invalid)' - info = {'Load': self.load, 'Refresh': self.refresh} + info['can_start'] = self.can_start info = ', '.join([f'{k}: {v}' for k, v in info.items()]) return info class IslandTransportRun(IslandUI): - @cached_property - def blacklist(self): - blacklist = [] - for k, v in self.config.cross_get(keys='Island.IslandTransport').items(): - if k.startswith('Submit') and not v: - item = k.split('Submit')[-1] - item = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', item) - item = re.sub('([a-z0-9])([A-Z])', r'\1_\2', item) - blacklist.append(globals()[f'TEMPLATE_{item.upper()}']) - return blacklist - def _transport_detect(self): """ Get all commissions from self.device.image. @@ -236,10 +111,8 @@ class IslandTransportRun(IslandUI): logger.hr('Transport Commission detect') commission = [] for index in range(3): - comm = IslandTransport(main=self, index=index, blacklist=self.blacklist) + comm = IslandTransport(main=self, index=index) logger.attr(f'Transport Commission', comm) - for item in comm.items: - logger.attr(item.button, item) commission.append(comm) return SelectedGrids(commission) @@ -262,11 +135,8 @@ class IslandTransportRun(IslandUI): self.device.screenshot() commissions = self._transport_detect() - if not commissions.count: - logger.warning('No commission detected, retry commission detect') - continue - elif commissions.select(valid=False).count: - logger.warning('Found 1 invalid commission at least, retry commission detect') + if commissions.count >= 2 and commissions.select(valid=False).count == 1: + logger.warning('Found 1 invalid commission, retry commission detect') continue else: return commissions.select(valid=True) @@ -274,154 +144,12 @@ class IslandTransportRun(IslandUI): logger.info('trials of transport commission detect exhausted, stop') return commissions.select(valid=True) - def transport_receive(self): - """ - Receive all transport missions from transport page. - - Returns: - bool: if received - """ - logger.hr('Island Transport', level=2) - self.device.click_record_clear() - self.interval_clear([GET_ITEMS_ISLAND, TRANSPORT_RECEIVE, POPUP_CANCEL_WHITE]) - success = True - click_timer = Timer(5, count=10).start() - confirm_timer = Timer(1, count=2).start() - for _ in self.loop(): - if self.handle_info_bar(): - click_timer.reset() - confirm_timer.reset() - continue - - if self.appear_then_click(TRANSPORT_RECEIVE, offset=(-20, -20, 20, 400), interval=2): - success = False - click_timer.reset() - confirm_timer.reset() - continue - - if self.handle_get_items(): - success = True - click_timer.reset() - confirm_timer.reset() - continue - - if self.handle_popup_cancel('REFRESH_CANCEL', offset=(30, 30)): - click_timer.reset() - confirm_timer.reset() - continue - - # handle island level up - if click_timer.reached(): - success = True - self.device.click(GET_ITEMS_ISLAND) - self.device.sleep(0.3) - click_timer.reset() - confirm_timer.reset() - continue - - if self.island_in_transport(): - if success and confirm_timer.reached(): - break - click_timer.reset() - else: - confirm_timer.reset() - - return success - - def transport_refresh(self, comm): - """ - Refresh the specific transport mission from transport page. - - Args: - comm (IslandTransport): the commission to refresh - - Returns: - bool: if success - """ - logger.info('Transport commission refresh') - self.interval_clear([TRANSPORT_REFRESH, POPUP_CONFIRM_WHITE]) - success = True - confirm_timer = Timer(1, count=2).start() - for _ in self.loop(): - if self.appear_then_click(TRANSPORT_REFRESH, offset=comm.offset, interval=2): - continue - - if self.handle_popup_confirm('REFRESH_CONFIRM', offset=(30, 30)): - success = True - confirm_timer.reset() - continue - - if self.island_in_transport(): - if success and confirm_timer.reached(): - break - else: - confirm_timer.reset() - return success - - def transport_start(self, comm): - """ - Start the specific transport mission from transport page. - - Args: - comm (IslandTransport): the commission to start - - Returns: - bool: if success - """ - logger.info('Transport commission start') - self.interval_clear([GET_ITEMS_ISLAND, TRANSPORT_START, POPUP_CANCEL_WHITE]) - success = True - confirm_timer = Timer(1, count=2).start() - for _ in self.loop(): - if self.appear_then_click(TRANSPORT_START, offset=comm.offset, interval=2): - success = False - confirm_timer.reset() - continue - - if self.handle_get_items(): - success = True - confirm_timer.reset() - continue - - if self.handle_popup_cancel('REFRESH_CANCEL', offset=(30, 30)): - confirm_timer.reset() - continue - - if self.island_in_transport(): - if success and confirm_timer.reached(): - break - else: - confirm_timer.reset() - return success - def island_transport_run(self): - """ - Execute island run to receive and start transport commissions. - - Returns: - list[timedelta]: future finish timedelta - """ logger.hr('Island Transport Run', level=1) future_finish = [] - self.transport_receive() - commissions = self.transport_detect(trial=5) + commissions = self.transport_detect(trial=2) - comm_refresh = commissions.select(status='pending', refresh=True) - comm_choose = commissions.select(status='pending', start=True) - for comm in comm_refresh: - if self.transport_refresh(comm): - comm.convert_to_refreshing() - for comm in comm_choose: - if self.transport_start(comm): - comm.convert_to_running() - - logger.hr('Showing transport commission', level=2) - for comm in commissions: - logger.attr(f'Transport Commission', comm) - - running_finish = [f for f in commissions.select(status='running').get('finish_time') if f is not None] - refreshing_finish = [f for f in commissions.select(status='refreshing').get('finish_time') if f is not None] - future_finish = sorted(running_finish + refreshing_finish) + future_finish = sorted([f for f in commissions.select(status='running').get('finish_time') if f is not None]) logger.info(f'Transport finish: {[str(f) for f in future_finish]}') if not len(future_finish): logger.info('No island transport running') diff --git a/module/island/ui.py b/module/island/ui.py index 2ac2539cf..0e7976177 100644 --- a/module/island/ui.py +++ b/module/island/ui.py @@ -1,24 +1,11 @@ from module.base.timer import Timer from module.island.assets import * from module.logger import logger -from module.ocr.ocr import Duration from module.ui.assets import SHOP_BACK_ARROW from module.ui.page import page_island_phone from module.ui.ui import UI -class IslandProductionTime(Duration): - def after_process(self, result): - result = super().after_process(result) - if result == '0:40:00': - result = '01:40:00' - return result - - -OCR_PRODUCTION_TIME = IslandProductionTime(OCR_PRODUCTION_TIME, lang='azur_lane_jp', name='OCR_PRODUCTION_TIME') -OCR_PRODUCTION_TIME_REMAIN = Duration(OCR_PRODUCTION_TIME_REMAIN, name='OCR_PRODUCTION_TIME_REMAIN') - - class IslandUI(UI): def island_in_management(self, interval=0): """