diff --git a/tasks/auto_mission/auto_mission.py b/tasks/auto_mission/auto_mission.py index de57298..296df8b 100644 --- a/tasks/auto_mission/auto_mission.py +++ b/tasks/auto_mission/auto_mission.py @@ -1,5 +1,6 @@ from tasks.mission.mission import Mission from tasks.mission.ui import SWITCH_NORMAL, SWITCH_HARD +from tasks.auto_mission.stage import Stage from tasks.auto_mission.ui import AutoMissionUI from enum import Enum @@ -12,29 +13,33 @@ import re class AutoMissionStatus(Enum): AP = 0 # Calculate AP and decide to terminate Auto-Mission module or not - STAGES_DATA = 1 + STAGES_DATA = 1 # Retrieve stages data for the area and resolve conflicts for type_to_preset NAVIGATE = 2 # Navigate to the area and select mode ENTER = 3 # Enter the first stage in the stage list CHECK = 4 # Check stages and find a stage that requires to be completed START = 5 # Start the stage FORMATION = 6 # Select units based on the types required by the stage FIGHT = 7 # Fight the stage - END = 8 + END = 8 # Update task FINISH = -1 # Indicate termination of Auto-Mission module class AutoMission(AutoMissionUI, Mission): def __init__(self, config, device): super().__init__(config, device) - self.task = None - self.previous_mode = None - self.previous_area = None - self._stage = None - self.stages_data = None - self.default_type_to_preset = self.get_default_type_to_preset() - self.current_type_to_preset = None + self.task: list[str, list[int], bool] = None + self.previous_mode: str = None + self.previous_area: int = None + self._stage: Stage = None + self.stages_data: dict = None + self.default_type_to_preset: dict = self.get_default_type_to_preset() + self.current_type_to_preset: dict = None - def get_default_type_to_preset(self): - type_to_preset = { + def get_default_type_to_preset(self) -> dict[str, list[int, int]]: + """ + Validate preset settings and returs a dictionary + mapping each type to its preset e.g {burst1: [1, 1]} + """ + type_to_preset: dict[str, str] = { "burst1": self.config.Formation_burst1, "burst2": self.config.Formation_burst2, "pierce1": self.config.Formation_pierce1, @@ -43,7 +48,6 @@ class AutoMission(AutoMissionUI, Mission): "mystic2": self.config.Formation_mystic2 } valid = True - for type, preset in type_to_preset.items(): preset_list = [] if isinstance(preset, str): @@ -64,8 +68,11 @@ class AutoMission(AutoMissionUI, Mission): raise RequestHumanTakeover return type_to_preset - def validate_area(self, mode, area_input): - area_list = [] + def validate_area(self, mode, area_input) -> list[int]: + """ + Validate the area input and returns the area as a list of integers + """ + area_list: list[int] = [] if isinstance(area_input, str): area_input = re.sub(r'[ \t\r\n]', '', area_input) area_input = (re.sub(r'[>﹥›˃ᐳ❯]', '>', area_input)).split('>') @@ -75,16 +82,16 @@ class AutoMission(AutoMissionUI, Mission): area_list = [str(area_input)] if area_list and len([x for x in area_list if x.isdigit()]) == len(area_list): - return area_list - - mode_name = "Normal" if mode == "N" else "H" - logger.error(f"Failed to read Mission {mode_name}'s area settings") - return None + return [int(x) for x in area_list] + else: + mode_name = "Normal" if mode == "N" else "H" + logger.error(f"Failed to read Mission {mode_name}'s area settings") + return None - def find_alternative(self, type, preset_list): + def find_alternative(self, type: str, preset_list: list[list[int, int]]) -> list[list[int, int]]: if not self.config.Formation_Substitute: return None - + alternatives_dictionary = { 'pierce1': ['pierce2', 'burst1', 'burst2', 'mystic1', 'mystic2'], 'pierce2': ['burst1', 'burst2', 'mystic1', 'mystic2'], @@ -104,17 +111,23 @@ class AutoMission(AutoMissionUI, Mission): return None @property - def mission_info(self) -> list: + def mission_info(self) -> list[str, list[int], bool]: + """ + Generate task, a list of list where each inner list is defined as + [mode, area_list, completion_level] e.g ["H", [6,7,8], "clear"] + """ valid = True mode = ("N", "H") - enable = (self.config.Normal_Enable, self.config.Hard_Enable) - area = (self.config.Normal_Area, self.config.Hard_Area) - area_list = [None, None] - completion_level = (self.config.Normal_Completion, self.config.Hard_Completion) + enable: tuple[bool] = (self.config.Normal_Enable, self.config.Hard_Enable) + area: tuple[str] = (self.config.Normal_Area, self.config.Hard_Area) + area_list: list[list[int]] = [None, None] + completion_level: tuple[bool] = (self.config.Normal_Completion, self.config.Hard_Completion) + for index in range(2): if enable[index]: area_list[index] = self.validate_area(mode[index], area[index]) valid = valid if area_list[index] else False + if valid: info = zip(mode, area_list, completion_level) return list(filter(lambda x: x[1], info)) @@ -126,44 +139,46 @@ class AutoMission(AutoMissionUI, Mission): @property def current_area(self): - return int(self.task[0][1][0]) + return self.task[0][1][0] @property def current_stage(self): return self._stage @current_stage.setter - def current_stage(self, value): + def current_stage(self, value: Stage): self._stage = value @property def current_completion_level(self): - return self.task[0][2] + return self.task[0][2] @property def current_count(self): - return 1 + # required to use update_ap() and get_realistic_count() + return 1 - def update_stages_data(self): + def update_stages_data(self) -> bool: if [self.previous_mode, self.previous_area] != [self.current_mode, self.current_area]: self.stages_data = self.get_stages_data(self.current_mode, self.current_area) if self.stages_data: return True return False - def update_current_type_to_preset(self): + def update_current_type_to_preset(self) -> bool: if [self.previous_mode, self.previous_area] == [self.current_mode, self.current_area]: + # set it to None. This will skip changing preset in self.formation self.current_type_to_preset = None return True - + mode_name = "Normal" if self.current_mode == "N" else "Hard" use_alternative = False for stage, info in self.stages_data.items(): if "start" not in info: continue - list_preset = [] - list_type = [] + list_preset: list[list[int, int]] = [] + list_type : list[str] = [] for type in info["start"]: preset = self.default_type_to_preset[type] list_type.append(type) @@ -171,7 +186,8 @@ class AutoMission(AutoMissionUI, Mission): if preset not in list_preset: list_preset.append(preset) continue - logger.error(f"Mission {mode_name} {self.current_area} requires {list_type} but they are both set to preset {preset}") + logger.error(f"Mission {mode_name} {self.current_area} requires\ + {list_type} but they are both set to preset {preset}") list_preset = self.find_alternative(type, list_preset) use_alternative = True if list_preset: @@ -179,11 +195,11 @@ class AutoMission(AutoMissionUI, Mission): return False if use_alternative: - d = {} + alt_type_to_preset: dict[str, list[list[int, int]]] = {} for index in range(len(list_type)): type, preset = list_type[index], list_preset[index] - d[type] = preset - self.current_type_to_preset = d + alt_type_to_preset[type] = preset + self.current_type_to_preset = alt_type_to_preset else: self.current_type_to_preset = self.default_type_to_preset return True @@ -225,7 +241,9 @@ class AutoMission(AutoMissionUI, Mission): raise RequestHumanTakeover case AutoMissionStatus.CHECK: - self.current_stage = self.check_stages(self.current_mode, self.current_area, self.stages_data, self.current_completion_level) + self.current_stage: Stage = self.check_stages( + self.current_mode, self.current_area, self.stages_data, self.current_completion_level + ) if self.current_stage: return AutoMissionStatus.START return AutoMissionStatus.END diff --git a/tasks/auto_mission/copilot.py b/tasks/auto_mission/copilot.py index 74aafb0..6188208 100644 --- a/tasks/auto_mission/copilot.py +++ b/tasks/auto_mission/copilot.py @@ -5,7 +5,7 @@ from module.ocr.ocr import Digit from tasks.base.ui import UI from tasks.base.assets.assets_base_page import MISSION_CHECK from tasks.auto_mission.assets.assets_auto_mission import * -from tasks.auto_mission.stage import StageState +from tasks.auto_mission.stage import StageState, Stage PRESETS = [PRESET1_ON, PRESET2_ON, PRESET3_ON, PRESET4_ON] @@ -41,8 +41,8 @@ class Copilot(UI): super().__init__(config, device) self.ocr_unit = Digit(OCR_UNIT) - """Utility methods""" - def sleep(self, num): + """---------------------- UTILITY METHODS ------------------------""" + def sleep(self, num: int): timer = Timer(num).start() while not timer.reached(): pass @@ -53,7 +53,7 @@ class Copilot(UI): # sleep because clicks can be too fast when executing actions self.sleep(interval) - def click_then_check(self, coords, dest_check): + def click_then_check(self, coords: tuple[int, int], dest_check: ButtonWrapper): while 1: self.device.screenshot() if self.appear(dest_check): @@ -68,7 +68,7 @@ class Copilot(UI): return True self.sleep(2) - def set_switch(self, switch): + def set_switch(self, switch: Switch): """ Set skip switch to on Returns: @@ -82,13 +82,11 @@ class Copilot(UI): switch.set('on', main=self) return True - """Formation methods""" - def choose_from_preset(self, type, type_to_preset): + """---------------------- FORMATION METHODS ------------------------""" + def choose_from_preset(self, type: str, type_to_preset: dict): preset, row = type_to_preset[type] preset_index = preset - 1 - row_index = row - 1 self.select_then_check(LAYOUT, PRESET_LIST) - #self.set_switch(PRESET_SWITCHES[preset_index]) PRESET = PRESETS[preset_index] while not self.match_color(PRESET, threshold=50): self.device.screenshot() @@ -121,15 +119,15 @@ class Copilot(UI): wait() click_second() - def choose_unit(self, unit): + def choose_unit(self, unit: int): unit_index = unit - 1 unit_switch = UNIT_SWITCHES[unit_index] self.set_switch(unit_switch) - def goto_formation_page(self, start_coords): + def goto_formation_page(self, start_coords: tuple[int, int]): self.click_then_check(start_coords, MOBILIZE) - def formation(self, stage, type_to_preset): + def formation(self, stage: Stage, type_to_preset: dict): if stage.state == StageState.SUB: # Select a unit to start the battle self.choose_unit(1) @@ -147,7 +145,7 @@ class Copilot(UI): self.select_then_check(MOBILIZE, MISSION_INFO) unit += 1 - """Fight methods""" + """---------------------- FIGHT METHODS ------------------------""" def begin_mission(self): # start the fight after formation. Not needed for SUB mission. self.select_then_check(BEGIN_MISSION, END_PHASE) @@ -157,7 +155,7 @@ class Copilot(UI): self.set_switch(SWITCH_SKIP_BATTLE) self.set_switch(SWITCH_AUTO_END) - def get_force(self): + def get_force(self) -> int: # detect the current active unit in the map self.device.screenshot() current_unit = self.ocr_unit.ocr_single_line(self.device.image) @@ -165,7 +163,7 @@ class Copilot(UI): return self.get_force() return current_unit - def wait_formation_change(self, force_index): + def wait_formation_change(self, force_index: int) -> int: logger.info("Wait formation change") origin = force_index while force_index == origin: @@ -186,7 +184,7 @@ class Copilot(UI): if self.appear_then_click(RECEIVED_CHEST): continue - def handle_mission_popup(self, button, skip_first_screenshot=True): + def handle_mission_popup(self, button: ButtonWrapper, skip_first_screenshot=True): while 1: if skip_first_screenshot: skip_first_screenshot = False @@ -198,6 +196,7 @@ class Copilot(UI): continue def confirm_teleport(self): + # Detect and confirm the end of the phase while 1: self.device.screenshot() if self.appear(MOVE_UNIT): @@ -219,7 +218,7 @@ class Copilot(UI): self.select_then_check(MISSION_INFO, MISSION_INFO_POPUP) self.handle_mission_popup(MISSION_INFO_POPUP) - def start_action(self, actions, manual_boss): + def start_action(self, actions, manual_boss: bool): for i, act in enumerate(actions): if manual_boss and i == len(actions) - 1: logger.warning("Actions completed. Waiting for manual boss...") @@ -332,7 +331,7 @@ class Copilot(UI): self.device.click_record_clear() self.device.stuck_record_clear() - def fight(self, stage, manual_boss): + def fight(self, stage: Stage, manual_boss: bool): if stage.state != StageState.SUB: # Click to start the task self.begin_mission() diff --git a/tasks/auto_mission/stage.py b/tasks/auto_mission/stage.py index 7171f4a..2c667a2 100644 --- a/tasks/auto_mission/stage.py +++ b/tasks/auto_mission/stage.py @@ -8,7 +8,7 @@ class StageState(Enum): CHEST = 4 class Stage: - def __init__(self, name, state, data): + def __init__(self, name: str, state: str, data: dict): self.name = name self.state = state self.data = data diff --git a/tasks/auto_mission/ui.py b/tasks/auto_mission/ui.py index a522960..c59ecc9 100644 --- a/tasks/auto_mission/ui.py +++ b/tasks/auto_mission/ui.py @@ -10,7 +10,7 @@ class AutoMissionUI(Copilot): """ Class dedicated to navigate the mission page and check stages """ - def get_stages_data(self, mode, area): + def get_stages_data(self, mode: str, area: int): # Dynamically generate the complete module path if mode == "N": module_path = f'tasks.auto_mission.normal_task.normal_task_' + str(area) @@ -27,11 +27,9 @@ class AutoMissionUI(Copilot): logger.error(f"Exploration not supported for Mission {mode_name} area {area}, under development...") return None - def wait_mission_info(self, mode, open_task=False, max_retry=99999): + def wait_mission_info(self, mode: str, open_task=False, max_retry=99999) -> str: """ - Wait for the task information popup to load - @param self: - @return: + Wait for the mission information popup to load in the mission page """ while max_retry > 0: self.device.screenshot() @@ -53,11 +51,9 @@ class AutoMissionUI(Copilot): logger.error("max_retry {0}".format(max_retry)) return None - def check_stage_state(self, mode, completion_level): + def check_stage_state(self, mode: str, completion_level: str) -> StageState: """ - Check the current task type - @param self: - @return: + Check the current stage type """ # Wait for the task information popup to load self.wait_mission_info(mode) @@ -78,7 +74,11 @@ class AutoMissionUI(Copilot): # Main task - Cleared return StageState.UNCLEARED - def get_stage_info(self, stage_name, stage_state, stages_data, completion_level): + def get_stage_info(self, stage_name: str, stage_state: StageState, stages_data: dict, completion_level: str) -> dict: + """ + Retrieves the stage info from stages_data trying + to find the most suited based on completion_level + """ possible_stages = [] for stage in stages_data: if stage_name in stage: @@ -99,12 +99,9 @@ class AutoMissionUI(Copilot): return stages_data[possible_stages[0]] return None - def check_stages(self, mode, area, stages_data, completion_level): + def check_stages(self, mode: str, area: int, stages_data: dict, completion_level: str) -> Stage: """ Find the stage that needs to be battled - @param self: - @param region: - @return: """ stage_index = 1 max_index = 4 if mode == "H" else 6 @@ -146,7 +143,7 @@ class AutoMissionUI(Copilot): if area != Digit(OCR_AREA).ocr_single_line(self.device.image): return None - def start_stage(self, stage): + def start_stage(self, stage: Stage): # Click to start the task if stage.state == StageState.SUB: self.select_then_check(ENTER_SUB, MOBILIZE)