Example #1
0
class Automator:
    def __init__(self, device: str, keyboard: Queue):
        """
        device: 如果是 USB 连接,则为 adb devices 的返回结果;如果是模拟器,则为模拟器的控制 URL 。
        """
        self.d = u2.connect(device)
        self.config = Reader()
        self.upgrade_iter_round = 0
        self.keyboard = keyboard
        self.command_mode = False
        self._check_uiautomator()
        self.time_start_working = time.time()
        self.refresh_times = 0
        self.delivered_times = 0

    def _need_continue(self):
        if not self.keyboard.empty():
            # 不在命令模式下时才接受回车暂停
            if not self.command_mode:
                txt = self.keyboard.get()
                if txt == prop.END:
                    logger.info('End')
                    return False
                logger.info('Pause')
            txt = self.keyboard.get()
            if txt == prop.END:
                logger.info('End')
                return False
            # 判断是否输入命令
            elif txt.split(' ')[0] == prop.RUN:
                # 若输入了命令则进行解析
                self._interpreter(txt.split(' ')[1:])
                return True
            else:
                logger.info('Restart')
                return True
        else:
            return True

    def _interpreter(self, cmd):
        """
        cmd: 用户输入的命令
        """
        # logger.info(txt.split(' ')[1:])
        op = cmd[0]
        # 命令 - 升至 x 级
        if op == prop.UPGRADE_TO:
            try:
                target_level = int(cmd[1])
            except Exception:
                logger.warn("Invalid number. Ignored.")
            else:
                self._upgrade_to(target_level)
        # 命令 - 升级 x 次
        elif op == prop.UPGRADE_TIMES:
            try:
                input_num = int(cmd[1])
            except Exception:
                logger.warn("Invalid number. Ignored.")
            else:
                self._upgrade_times(input_num)
        # 命令 - 命令模式
        elif op == prop.COMMAND_MODE:
            if len(cmd) == 2 and cmd[1] == 'on':
                self.command_mode = True
                logger.info('Enter command mode.')
            elif len(cmd) == 2 and cmd[1] == 'off':
                self.command_mode = False
                logger.info('Exit command mode.')
                self._return_main_area()
            else:
                logger.warn("Unknown parameter. Ignored.")
        # 命令 - 拆红包
        elif op == prop.UNPACK:
            if len(cmd) == 3 and cmd[1] in ['s', 'm', 'l']:
                try:
                    input_num = int(cmd[2])
                except Exception:
                    logger.warn("Invalid number. Ignored.")
                else:
                    self._unpack_times(cmd[1], input_num)
                    logger.info('Unpack complete.')
            else:
                logger.warn("Unknown parameter. Ignored.")
        elif op == prop.OPEN_ALBUM:
            try:
                input_num = int(cmd[1])
            except Exception:
                logger.warn("Invalid number. Ignored.")
            else:
                self._open_albums(input_num)
                logger.info('Open complete.')
        elif op == prop.SUMMARY:
            self._print_summary()
        # 无法识别命令
        else:
            logger.warn("Unknown command. Ignored.")
        if not self.command_mode:
            logger.info('Restart')

    def start(self):
        """
        启动脚本,请确保已进入游戏页面。
        """
        tmp_upgrade_last_time = time.time()
        logger.info("Start Working")
        while True:
            # 检查是否有键盘事件
            if not self._need_continue():
                break

            # 进入命令模式后不继续执行常规操作
            if self.command_mode:
                continue

            # 更新配置文件
            self.config.refresh()

            if self.config.debug_mode:
                logger.info("Debug mode")
                # 重启游戏法
                # self._refresh_train_by_restart()

                # 重连 wifi 法
                # self._refresh_train_by_reconnect()

            # 是否检测货物
            if self.config.detect_goods:
                logger.info('-' * 30)
                logger.info("Start matching goods")
                # 获取当前屏幕快照
                screen = self._safe_screenshot()
                # 判断是否出现货物。
                has_goods = False
                refresh_flag = False
                for target in self.config.goods_2_building_seq.keys():
                    has_goods |= self._match_target(screen, target)
                # 如果需要刷新火车并且已送过目标货物
                if has_goods and self.config.refresh_train:
                    refresh_flag = True
                    logger.info("All target goods delivered.")
                # 如果需要刷新火车并且未送过目标货物
                elif self.config.refresh_train:
                    for target in self.config.goods_2_building_seq_excpet_target.keys(
                    ):
                        if UIMatcher.match(screen, target) is not None:
                            has_goods = True
                            break
                    if has_goods:
                        refresh_flag = True
                        logger.info("Train detected with no target goods.")
                    else:
                        logger.info("Train not detected.")
                if refresh_flag:
                    # 刷新火车
                    logger.info("Refresh train.")
                    logger.info("-" * 30)
                    self.refresh_times += 1
                    if not self._refresh_train_by_restart():
                        # 重启不成功(超时)时中止脚本
                        logger.warn("Timed out waiting for restart!")
                        break
                else:
                    logger.info("End matching")

            # 简单粗暴的方式,处理 “XX之光” 的荣誉显示。
            # 当然,也可以使用图像探测的模式。
            self.d.click(550, 1650)

            # 滑动屏幕,收割金币。
            logger.info("Collect coins")
            self._swipe()

            # 自动升级建筑
            tmp_upgrade_interval = time.time() - tmp_upgrade_last_time
            if tmp_upgrade_interval >= self.config.upgrade_interval_sec:
                if self.config.upgrade_building is True:
                    self._auto_upgrade_building()
                tmp_upgrade_last_time = time.time()
            else:
                logger.info(
                    f"Left {round(self.config.upgrade_interval_sec - tmp_upgrade_interval, 2)}s to upgrade"
                )

            time.sleep(self.config.swipe_interval_sec)
        self._print_summary()
        logger.info('Sub process end')

    def _swipe(self):
        """
        滑动屏幕,收割金币。
        """
        for i in range(3):
            # 横向滑动,共 3 次。
            sx, sy = self._get_position(i * 3 + 1)
            ex, ey = self._get_position(i * 3 + 3)
            self.d.swipe(sx, sy, ex, ey)

    @staticmethod
    def _get_position(key):
        """
        获取指定建筑的屏幕位置。

        ###7#8#9#
        ##4#5#6##
        #1#2#3###
        """
        return prop.BUILDING_POS.get(key)

    def _get_target_position(self, target: TargetType):
        """
        获取货物要移动到的屏幕位置。
        """
        return self._get_position(self.config.goods_2_building_seq.get(target))

    def _match_target(self, screen, target: TargetType):
        """
        探测货物,并搬运货物。
        """
        # 由于 OpenCV 的模板匹配有时会智障,故我们探测次数实现冗余。
        counter = 6
        logged = False
        while counter != 0:
            counter = counter - 1

            # 使用 OpenCV 探测货物。
            result = UIMatcher.match(screen, target)

            # 若无探测到,终止对该货物的探测。
            # 实现冗余的原因:返回的货物屏幕位置与实际位置存在偏差,导致移动失效
            if result is None:
                break

            rank = result[-1]
            result = result[:2]
            sx, sy = result
            # 获取货物目的地的屏幕位置。
            ex, ey = self._get_target_position(target)

            if not logged:
                self.delivered_times += 1
                logger.info(f"Detect {target} at ({sx},{sy}), rank: {rank}")
                logged = True

            # 搬运货物。
            self.d.swipe(sx, sy, ex, ey)
        # 侧面反映检测出货物
        return logged

    def _auto_upgrade_building(self):
        """
        按顺序升级建筑
        """
        logger.info("Start upgrade buildings")
        self.d.click(*prop.BUILDING_DETAIL_BTN)
        time.sleep(0.5)
        for pos in self.config.upgrade_building_list:
            self.d.click(*self._get_position(pos))
            time.sleep(0.5)
            self.d.click(*prop.BUILDING_UPGRADE_BTN)
            time.sleep(0.5)
        self.d.click(*prop.BUILDING_DETAIL_BTN)
        logger.info("Upgrade complete")

    def _upgrade_to(self, target_level):
        """
        target_level: 目标等级
        升至 target_level 级
        利用 Tesseract 识别当前等级后点击升级按钮 target_level - 当前等级次
        """
        screen = self._safe_screenshot()
        screen = UIMatcher.pre_building_panel(screen)
        tmp = UIMatcher.cut(screen, prop.BUILDING_INFO_PANEL_LEVEL_POS,
                            (120, 50))
        # import cv2
        # cv2.imwrite("./tmp/screen.jpg", screen)
        tmp = UIMatcher.plain(tmp)
        tmp = UIMatcher.fill_color(tmp)
        tmp = UIMatcher.plain(tmp)
        txt = UIMatcher.image_to_txt(tmp, plus='-l chi_sim --psm 7')
        txt = UIMatcher.normalize_txt(txt)
        try:
            cur_level = int(txt)
            logger.info(f'Current level -> {cur_level}')
        except Exception:
            logger.warning(f'Current level -> {txt}')
            return
        click_times = target_level - cur_level
        self._upgrade_times(click_times)

    def _upgrade_times(self, click_times: int):
        """
        click_times: 点击/升级次数
        执行点击升级按钮的操作 click_times 次
        """
        # assert(times >= 0)
        while click_times > 0:
            click_times -= 1
            bx, by = prop.BUILDING_INFO_PANEL_UPGRADE_BTN
            self.d.click(bx, by)
            time.sleep(0.015)
        logger.info("Upgrade complete")
        # 非命令模式下完成操作后返回主界面以继续常规流程
        if not self.command_mode:
            self._return_main_area()

    def _return_main_area(self):
        """
        通过点击两次导航栏内建设按钮来回到主界面
        """
        time.sleep(0.5)
        tx, ty = prop.CONSTRUCT_BTN
        self.d.click(tx, ty)
        time.sleep(0.1)
        self.d.click(tx, ty)
        time.sleep(0.5)

    def _unpack_times(self, pack_type, num: int):
        """
        开红包 num 个
        """
        # 红包标题栏坐标 开红包后点这里直到开完这个红包
        tx, ty = prop.REDPACKET_TITLE_POS
        if pack_type == 'm':
            bx, by = prop.REDPACKET_BTN_M
            t = 12
        elif pack_type == 'l':
            bx, by = prop.REDPACKET_BTN_L
            t = 24
        else:
            bx, by = prop.REDPACKET_BTN_S
            t = 6
        self.d.click(bx, by)
        time.sleep(1)
        while num > 1:
            num -= 1
            self.d.press("enter")
            time.sleep(0.08)
        time.sleep(1)
        # 防止意外多点几下 例如升星或开出史诗
        for _ in range(t):
            self.d.click(tx, ty)
            time.sleep(0.25)
        if not self.command_mode:
            self._return_main_area()

    def _open_albums(self, num: int):
        """
        开相册 num 个
        """
        self.d.click(*prop.ALBUM_BTN)
        time.sleep(1)
        while num > 1:
            num -= 1
            self.d.press("enter")
            time.sleep(0.08)
        time.sleep(1)
        for _ in range(4):
            self.d.click(*prop.REDPACKET_TITLE_POS)
            time.sleep(0.5)
        if not self.command_mode:
            self._return_main_area()

    def _is_good_to_go(self):
        """
        检测是否有排行图标来判断是否进入了游戏界面
        """
        screen = self._safe_screenshot()
        return UIMatcher.match(screen, TargetType.Rank_btn) is not None

    def _refresh_train_by_restart(self):
        """
        通过重启游戏的方法来刷新火车
        全程用时大约在 20s 左右
        qq 账号测试不用授权 20s 左右
        """
        time_before_restart = time.time()
        self.d.app_stop("com.tencent.jgm")
        self.d.app_start("com.tencent.jgm", activity=".MainActivity")
        time.sleep(5)
        good_to_go = False
        try_times = 0
        while not good_to_go:
            try_times += 1
            if self._is_good_to_go():
                good_to_go = True
                logger.info(
                    f"Refresh train costs {round(time.time() - time_before_restart, 2)}s."
                )
            elif try_times >= 60:
                return False
            else:
                time.sleep(1)
        return True

    def _refresh_train_by_reconnect(self):
        """
        通过关闭开启 wifi 的方法刷新火车
        要重新登陆+授权 暂时弃用
        """
        self.d.press("home")
        time.sleep(0.5)
        logger.info("Wifi disable.")
        logger.info(self.d.adb_shell("svc wifi disable"))
        time.sleep(0.5)
        logger.info("Wifi enable.")
        logger.info(self.d.adb_shell("svc wifi enable"))
        time.sleep(5)
        self.d.app_start("com.tencent.jgm", activity=".MainActivity")

    def _check_uiautomator(self):
        """
        检查 uiautomator 运行状态
        """
        if not self.d.uiautomator.running():
            self.d.reset_uiautomator()

    def _safe_screenshot(self):
        """
        防止执行 screenshot 时报错终止
        """
        self._check_uiautomator()
        return self.d.screenshot(format="opencv")

    def _print_summary(self):
        logger.info('-' * 30)
        pass_time = time.time() - self.time_start_working
        logger.info(
            f"本次启动运行了 {int(pass_time // 3600)} 小时 {int(pass_time % 3600 // 60)} 分钟 {round(pass_time % 60, 2)} 秒"
        )
        logger.info(
            f"重启了 {self.refresh_times} 次, 检测到 {self.delivered_times} 车厢目标货物(非总送货次数)"
        )
        logger.info('-' * 30)
Example #2
0
class Scheduler:
    def __init__(self, keyboard: Queue, pipe: Queue):
        self.config = Reader()
        self.keyboard = keyboard
        self.pipe = pipe

    def _generate_do_2_time(self):
        res = {}
        for config_name in CONFIG_NAME_2_METHOD_NAME.keys():
            res[config_name] = set()
        return res

    # 判断哪些任务需要重新放回调度队列,有可能出现关闭一个任务后重启,而此时这个任务将会饿死,因为没有trigger
    def _generate_restart_list(self, do_2_time: dict, interval_map: dict):
        res = []
        for config_name in interval_map.keys():
            if interval_map[config_name] > 0 and len(
                    do_2_time[config_name]) == 0:
                res.append(config_name)
        return res

    def _add_time_2_do(self, time_2_do: dict, new_time: int, do: str):
        if new_time not in time_2_do:
            time_2_do[new_time] = {do}
        else:
            time_2_do[new_time].add(do)

    def run(self):
        default_loop_interval = 5
        loop_time = 0
        # 记录哪个时间干哪些事(实际上只是塞队列)
        time_2_do = {}
        # 记录哪些事在哪些时候干
        do_2_time = self._generate_do_2_time()
        while self.keyboard.empty():
            # 更新配置
            self.config.refresh()

            # self.pipe.put(f'{CONFIG_PREFIX}{self.config.to_string()}')
            self.pipe.put(self.config.to_string())

            interval_map = self.config.interval_map
            pipe_msg = []
            for config_name, interval in interval_map.items():
                if interval <= 0:
                    interval_map.pop(config_name)
                    # 如果一个任务被置于<=0的值代表不希望被执行,那么同时需要把它移出任务队列
                    for _time in do_2_time[config_name]:
                        time_2_do.get(_time, set()).discard(config_name)
                    do_2_time[config_name] = set()
            # print(interval_map)
            if len(interval_map) == 0:
                loop_interval = default_loop_interval
            else:
                loop_interval = list_gcd(interval_map.values())

                to_do_set = time_2_do.get(loop_time, set()) | set(
                    self._generate_restart_list(do_2_time, interval_map))
                # print(to_do_set)
                for config_name in to_do_set:
                    # 塞入调度队列
                    pipe_msg.append(CONFIG_NAME_2_METHOD_NAME[config_name])
                    # 计算这个任务下一次何时调度
                    next_time = interval_map[
                        config_name] / loop_interval + loop_time
                    # 更新time_2_do
                    self._add_time_2_do(time_2_do, next_time, config_name)
                    # 更新do_2_time
                    do_2_time[config_name].discard(loop_time)
                    do_2_time[config_name].add(next_time)
                # 把当前时间的任务队列销掉,节省内存
                time_2_do.pop(loop_time, '')
            # print(pipe_msg)
            if len(pipe_msg) != 0:
                pipe_msg = f"{METHOD_PREFIX}{METHOD_SEP.join(pipe_msg)}"
                self.pipe.put(pipe_msg)
            loop_time += 1
            time.sleep(loop_interval)
Example #3
0
class Automator:
    def __init__(self, device: str, keyboard: Queue):
        """
        device: 如果是 USB 连接,则为 adb devices 的返回结果;如果是模拟器,则为模拟器的控制 URL 。
        """
        self.d = u2.connect(device)
        self.config = Reader()
        self.upgrade_iter_round = 0
        self.keyboard = keyboard
        self.command_mode = False
        # 检查 uiautomator
        if not self.d.uiautomator.running():
            self.d.reset_uiautomator()

    def _need_continue(self):
        if not self.keyboard.empty():
            # 不在命令模式下时才接受回车暂停
            if not self.command_mode:
                txt = self.keyboard.get()
                # logger.info('txt1:' + txt)
                if txt == prop.END:
                    logger.info('End')
                    return False
                logger.info('Pause')
            txt = self.keyboard.get()
            # logger.info('txt2:' + txt)
            if txt == prop.END:
                logger.info('End')
                return False
            # 判断是否输入命令
            elif txt.split(' ')[0] == prop.RUN:
                # logger.info(txt.split(' ')[1:])
                cmd = txt.split(' ')[1]
                # 命令 - 升至 x 级
                if cmd == prop.UPGRADE_TO:
                    try:
                        target_level = int(txt.split(' ')[2])
                    except Exception:
                        logger.warn("Invalid number. Ignored.")
                    else:
                        self._upgrade_to(target_level)
                    # logger.info('target_level: ' + str(target_level))
                # 命令 - 升级 x 次
                elif cmd == prop.UPGRADE_TIMES:
                    try:
                        input_num = int(txt.split(' ')[2])
                    except Exception:
                        logger.warn("Invalid number. Ignored.")
                    else:
                        self._upgrade_times(input_num)
                # 命令 - 命令模式
                elif cmd == prop.COMMAND_MODE:
                    if txt.split(' ')[2] == 'on':
                        self.command_mode = True
                        logger.info('Enter command mode.')
                    elif txt.split(' ')[2] == 'off':
                        self.command_mode = False
                        logger.info('Exit command mode.')
                        self._return_main_area()
                    else:
                        logger.warn("Unknown parameter. Ignored.")
                # 无法识别命令
                else:
                    logger.warn("Unknown command. Ignored.")
                if not self.command_mode:
                    logger.info('Restart')
                return True
            else:
                logger.info('Restart')
                return True
        else:
            return True

    def start(self):
        """
        启动脚本,请确保已进入游戏页面。
        """
        tmp_upgrade_last_time = time.time()
        logger.info("Start Working")
        while True:
            # 检查是否有键盘事件
            if not self._need_continue():
                break

            # 进入命令模式后不继续执行常规操作
            if self.command_mode:
                continue

            # 更新配置文件
            self.config.refresh()

            # 在下午五点以后再开始拿火车,收益最大化
            # if datetime.now().hour > 17:
            if True:
                logger.info("Start matching goods")
                # 获取当前屏幕快照
                screen = self.d.screenshot(format="opencv")
                # 判断是否出现货物。
                has_goods = False
                for target in self.config.goods_2_building_seq.keys():
                    has_goods |= self._match_target(screen, target)
                if has_goods:
                    UIMatcher.write(screen)
                    # pass
                logger.info("End matching")

            # 简单粗暴的方式,处理 “XX之光” 的荣誉显示。
            # 当然,也可以使用图像探测的模式。
            self.d.click(550, 1650)

            # 滑动屏幕,收割金币。
            # logger.info("swipe")
            self._swipe()

            # 升级建筑
            tmp_upgrade_interval = time.time() - tmp_upgrade_last_time
            if tmp_upgrade_interval >= self.config.upgrade_interval_sec:
                if self.config.upgrade_type_is_assign is True:
                    self._assigned_uprade()
                else:
                    self._upgrade()
                tmp_upgrade_last_time = time.time()
            else:
                logger.info(
                    f"Left {round(self.config.upgrade_interval_sec - tmp_upgrade_interval, 2)}s to upgrade"
                )

            time.sleep(self.config.swipe_interval_sec)
        logger.info('Sub process end')

    def _swipe(self):
        """
        滑动屏幕,收割金币。
        """
        for i in range(3):
            # 横向滑动,共 3 次。
            sx, sy = self._get_position(i * 3 + 1)
            ex, ey = self._get_position(i * 3 + 3)
            self.d.swipe(sx, sy, ex, ey)

    @staticmethod
    def _get_position(key):
        """
        获取指定建筑的屏幕位置。

        ###7#8#9#
        ##4#5#6##
        #1#2#3###
        """
        return prop.BUILDING_POS.get(key)

    def _get_target_position(self, target: TargetType):
        """
        获取货物要移动到的屏幕位置。
        """
        return self._get_position(self.config.goods_2_building_seq.get(target))

    def _match_target(self, screen, target: TargetType):
        """
        探测货物,并搬运货物。
        """
        # 由于 OpenCV 的模板匹配有时会智障,故我们探测次数实现冗余。
        counter = 6
        logged = False
        while counter != 0:
            counter = counter - 1

            # 使用 OpenCV 探测货物。
            result = UIMatcher.match(screen, target)

            # 若无探测到,终止对该货物的探测。
            # 实现冗余的原因:返回的货物屏幕位置与实际位置存在偏差,导致移动失效
            if result is None:
                break

            rank = result[-1]
            result = result[:2]
            sx, sy = result
            # 获取货物目的地的屏幕位置。
            ex, ey = self._get_target_position(target)

            if not logged:
                logger.info(f"Detect {target} at ({sx},{sy}), rank: {rank}")
                logged = True

            # 搬运货物。
            self.d.swipe(sx, sy, ex, ey)
        # 侧面反映检测出货物
        return logged

    def __find_selected_building_seq(self):
        selected_seq_list = elect(len(self.config.upgrade_order),
                                  self.upgrade_iter_round)
        tmp_set = set()
        for order_seq in selected_seq_list:
            tmp_set |= self.config.upgrade_order[order_seq]
        res = []
        for i, building in enumerate(self.config.building_pos):
            if building in tmp_set:
                res.append(i + 1)
        if len(res) == 0:
            return list(prop.BUILDING_POS.keys())
        else:
            return res

    def _select_min_building(self):
        screen = self.d.screenshot(format="opencv")
        screen = UIMatcher.pre(screen)
        min_level = float('inf')
        min_building_seq = None
        for key in self.__find_selected_building_seq():
            pos = prop.BUILDING_LEVEL_POS[key]
            tmp = UIMatcher.cut(screen, pos)
            tmp = UIMatcher.plain(tmp)
            tmp = UIMatcher.fill_color(tmp)
            tmp = UIMatcher.plain(tmp)
            txt = UIMatcher.image_to_txt(tmp, plus='-l chi_sim --psm 7')
            txt = UIMatcher.normalize_txt(txt)
            try:
                level = int(txt)
                logger.info(
                    f'{self.config.building_pos[key - 1]} tesser -> {level}')
            except Exception:
                logger.warning(
                    f'{self.config.building_pos[key - 1]} tesser -> {txt}')
                continue
            if level < min_level:
                min_level = level
                min_building_seq = key

        # 一个屋子的等级都没拿到
        if min_building_seq is None:
            res = choice(list(prop.BUILDING_POS.keys()))
            logger.warning(
                f'No tesseract result, random to {self.config.building_pos[res - 1]}'
            )
            return res
        else:
            logger.info(
                f'Minimum level is {min_level} from {self.config.building_pos[min_building_seq - 1]}'
            )
            return min_building_seq

    def _upgrade(self):
        logger.info("Start upgrading")
        # 迭代次数加一
        self.upgrade_iter_round += 1

        self.d.click(*prop.BUILDING_DETAIL_BTN)
        time.sleep(1)
        need_upgrade_building_seq = self._select_min_building()
        self.d.click(*self._get_position(need_upgrade_building_seq))
        time.sleep(1)
        self.d.long_click(prop.BUILDING_UPGRADE_BTN[0],
                          prop.BUILDING_UPGRADE_BTN[1],
                          self.config.upgrade_press_time_sec)
        time.sleep(0.5)
        self.d.click(*prop.BUILDING_DETAIL_BTN)
        logger.info("Upgrade complete")

    def _assigned_uprade(self):
        logger.info("Start assigned upgrading")
        self.d.click(*prop.BUILDING_DETAIL_BTN)
        time.sleep(0.5)
        self.d.click(*self._get_position(self.config.assigned_building_pos))
        time.sleep(0.5)
        self.d.long_click(prop.BUILDING_UPGRADE_BTN[0],
                          prop.BUILDING_UPGRADE_BTN[1],
                          self.config.upgrade_press_time_sec)
        time.sleep(0.5)
        self.d.click(*prop.BUILDING_DETAIL_BTN)
        logger.info("Upgrade complete")

    def _upgrade_to(self, target_level):
        """
        target_level: 目标等级
        升至 target_level 级
        利用 Tesseract 识别当前等级后点击升级按钮 target_level - 当前等级次
        """
        screen = self.d.screenshot(format="opencv")
        screen = UIMatcher.pre_building_panel(screen)
        tmp = UIMatcher.cut(screen, prop.BUILDING_INFO_PANEL_LEVEL_POS,
                            (120, 50))
        # import cv2
        # cv2.imwrite("./tmp/screen.jpg", screen)
        tmp = UIMatcher.plain(tmp)
        tmp = UIMatcher.fill_color(tmp)
        tmp = UIMatcher.plain(tmp)
        txt = UIMatcher.image_to_txt(tmp, plus='-l chi_sim --psm 7')
        txt = UIMatcher.normalize_txt(txt)
        try:
            cur_level = int(txt)
            logger.info(f'Current level -> {cur_level}')
        except Exception:
            logger.warning(f'Current level -> {txt}')
            return
        click_times = target_level - cur_level
        self._upgrade_times(click_times)

    def _upgrade_times(self, click_times: int):
        """
        click_times: 点击/升级次数
        执行点击升级按钮的操作 click_times 次
        """
        # assert(times >= 0)
        while click_times > 0:
            click_times -= 1
            bx, by = prop.BUILDING_INFO_PANEL_UPGRADE_BTN
            self.d.click(bx, by)
            time.sleep(0.015)
        logger.info("Upgrade complete")
        # 非命令模式下完成操作后返回主界面以继续常规流程
        if not self.command_mode:
            self._return_main_area()

    def _return_main_area(self):
        """
        通过点击两次导航栏内建设按钮来回到主界面
        """
        time.sleep(0.5)
        tx, ty = prop.CONSTRUCT_BTN
        self.d.click(tx, ty)
        time.sleep(0.1)
        self.d.click(tx, ty)
        time.sleep(0.5)
Example #4
0
class Automator:
    def __init__(self, device: str, keyboard: Queue):
        """
        device: 如果是 USB 连接,则为 adb devices 的返回结果;如果是模拟器,则为模拟器的控制 URL 。
        """
        self.d = u2.connect(device)
        self.config = Reader()
        self.upgrade_iter_round = 0
        self.keyboard = keyboard
        self.command_mode = False
        # 检查 uiautomator
        if not self.d.uiautomator.running():
            self.d.reset_uiautomator()
        self.time_start_working = time.time()
        self.refresh_times = 0
        self.delivered_times = 0

    def _need_continue(self):
        if not self.keyboard.empty():
            # 不在命令模式下时才接受回车暂停
            if not self.command_mode:
                txt = self.keyboard.get()
                # logger.info('txt1:' + txt)
                if txt == prop.END:
                    logger.info('End')
                    return False
                logger.info('Pause')
            txt = self.keyboard.get()
            # logger.info('txt2:' + txt)
            if txt == prop.END:
                logger.info('End')
                return False
            # 判断是否输入命令
            elif txt.split(' ')[0] == prop.RUN:
                # logger.info(txt.split(' ')[1:])
                cmd = txt.split(' ')[1]
                # 命令 - 升至 x 级
                if cmd == prop.UPGRADE_TO:
                    try:
                        target_level = int(txt.split(' ')[2])
                    except Exception:
                        logger.warn("Invalid number. Ignored.")
                    else:
                        self._upgrade_to(target_level)
                    # logger.info('target_level: ' + str(target_level))
                # 命令 - 升级 x 次
                elif cmd == prop.UPGRADE_TIMES:
                    try:
                        input_num = int(txt.split(' ')[2])
                    except Exception:
                        logger.warn("Invalid number. Ignored.")
                    else:
                        self._upgrade_times(input_num)
                # 命令 - 命令模式
                elif cmd == prop.COMMAND_MODE:
                    if txt.split(' ')[2] == 'on':
                        self.command_mode = True
                        logger.info('Enter command mode.')
                    elif txt.split(' ')[2] == 'off':
                        self.command_mode = False
                        logger.info('Exit command mode.')
                        self._return_main_area()
                    else:
                        logger.warn("Unknown parameter. Ignored.")
                # 命令 - 拆红包
                elif cmd == prop.UNPACK:
                    pack_type = txt.split(' ')[2]
                    if pack_type in ['s', 'm', 'l']:
                        try:
                            input_num = int(txt.split(' ')[3])
                        except Exception:
                            logger.warn("Invalid number. Ignored.")
                        else:
                            self._unpack_times(pack_type, input_num)
                            logger.info('Unpack complete.')
                    else:
                        logger.warn("Unknown parameter. Ignored.")
                elif cmd == prop.OPEN_ALBUM:
                    try:
                        input_num = int(txt.split(' ')[2])
                    except Exception:
                        logger.warn("Invalid number. Ignored.")
                    else:
                        self._open_albums(input_num)
                        logger.info('Open complete.')
                # 无法识别命令
                else:
                    logger.warn("Unknown command. Ignored.")
                if not self.command_mode:
                    logger.info('Restart')
                return True
            else:
                logger.info('Restart')
                return True
        else:
            return True

    def start(self):
        """
        启动脚本,请确保已进入游戏页面。
        """
        tmp_upgrade_last_time = time.time()
        logger.info("Start Working")
        while True:
            # 检查是否有键盘事件
            if not self._need_continue():
                logger.info('-' * 30)
                pass_time = time.time() - self.time_start_working
                logger.info(f"本次启动运行了 {int(pass_time // 3600)} 小时 {int(pass_time % 3600 // 60)} 分钟 {round(pass_time % 60, 2)} 秒")
                logger.info(f"重启了 {self.refresh_times} 次, 检测到 {self.delivered_times} 次货物(非总送货次数)")
                break
            
            # 进入命令模式后不继续执行常规操作
            if self.command_mode:
                continue

            # 更新配置文件
            self.config.refresh()

            if self.config.debug_mode:
                None
                # 重启游戏法
                # self._refresh_train_by_restart()
                
                # 重连 wifi 法
                # self._refresh_train_by_reconnect()
            
            # 是否检测货物
            if self.config.detect_goods:
                logger.info('-' * 30)
                logger.info("Start matching goods")
                # 获取当前屏幕快照
                screen = self.d.screenshot(format="opencv")
                # 判断是否出现货物。
                has_goods = False
                refresh_flag = False
                for target in self.config.goods_2_building_seq.keys():
                    has_goods |= self._match_target(screen, target)
                # 如果需要刷新火车并且已送过目标货物
                if has_goods and self.config.refresh_train:
                    refresh_flag = True
                    logger.info("All target goods delivered.")
                # 如果需要刷新火车并且未送过目标货物
                elif self.config.refresh_train:
                    for target in self.config.goods_2_building_seq_excpet_target.keys():
                        if UIMatcher.match(screen, target) is not None:
                            has_goods = True
                            break
                    if has_goods:
                        refresh_flag = True
                        logger.info("Train detected with no target goods.")
                    else:
                        logger.info("Train not detected.")
                if refresh_flag:
                    # 刷新火车
                    logger.info("Refresh train.")
                    logger.info("-" * 30)
                    self.refresh_times += 1
                    self._refresh_train_by_restart()                    
                else:
                    logger.info("End matching")

            # 简单粗暴的方式,处理 “XX之光” 的荣誉显示。
            # 当然,也可以使用图像探测的模式。
            self.d.click(550, 1650)

            # 滑动屏幕,收割金币。
            # logger.info("swipe")
            self._swipe()

            # 升级建筑
            tmp_upgrade_interval = time.time() - tmp_upgrade_last_time
            if tmp_upgrade_interval >= self.config.upgrade_interval_sec:
                if self.config.upgrade_type_is_assign is True:
                    self._assigned_uprade()
                else:
                    self._upgrade()
                tmp_upgrade_last_time = time.time()
            else:
                logger.info(f"Left {round(self.config.upgrade_interval_sec - tmp_upgrade_interval, 2)}s to upgrade")

            time.sleep(self.config.swipe_interval_sec)
        logger.info('Sub process end')

    def _swipe(self):
        """
        滑动屏幕,收割金币。
        """
        for i in range(3):
            # 横向滑动,共 3 次。
            sx, sy = self._get_position(i * 3 + 1)
            ex, ey = self._get_position(i * 3 + 3)
            self.d.swipe(sx, sy, ex, ey)

    @staticmethod
    def _get_position(key):
        """
        获取指定建筑的屏幕位置。

        ###7#8#9#
        ##4#5#6##
        #1#2#3###
        """
        return prop.BUILDING_POS.get(key)

    def _get_target_position(self, target: TargetType):
        """
        获取货物要移动到的屏幕位置。
        """
        return self._get_position(self.config.goods_2_building_seq.get(target))

    def _match_target(self, screen, target: TargetType):
        """
        探测货物,并搬运货物。
        """
        # 由于 OpenCV 的模板匹配有时会智障,故我们探测次数实现冗余。
        counter = 6
        logged = False
        while counter != 0:
            counter = counter - 1

            # 使用 OpenCV 探测货物。
            result = UIMatcher.match(screen, target)

            # 若无探测到,终止对该货物的探测。
            # 实现冗余的原因:返回的货物屏幕位置与实际位置存在偏差,导致移动失效
            if result is None:
                break

            rank = result[-1]
            result = result[:2]
            sx, sy = result
            # 获取货物目的地的屏幕位置。
            ex, ey = self._get_target_position(target)

            if not logged:
                self.delivered_times += 1
                logger.info(f"Detect {target} at ({sx},{sy}), rank: {rank}")
                logged = True

            # 搬运货物。
            self.d.swipe(sx, sy, ex, ey)
        # 侧面反映检测出货物
        return logged

    def __find_selected_building_seq(self):
        selected_seq_list = elect(len(self.config.upgrade_order), self.upgrade_iter_round)
        tmp_set = set()
        for order_seq in selected_seq_list:
            tmp_set |= self.config.upgrade_order[order_seq]
        res = []
        for i, building in enumerate(self.config.building_pos):
            if building in tmp_set:
                res.append(i + 1)
        if len(res) == 0:
            return list(prop.BUILDING_POS.keys())
        else:
            return res

    def _select_min_building(self):
        screen = self.d.screenshot(format="opencv")
        screen = UIMatcher.pre(screen)
        min_level = float('inf')
        min_building_seq = None
        for key in self.__find_selected_building_seq():
            pos = prop.BUILDING_LEVEL_POS[key]
            tmp = UIMatcher.cut(screen, pos)
            tmp = UIMatcher.plain(tmp)
            tmp = UIMatcher.fill_color(tmp)
            tmp = UIMatcher.plain(tmp)
            txt = UIMatcher.image_to_txt(tmp, plus='-l chi_sim --psm 7')
            txt = UIMatcher.normalize_txt(txt)
            try:
                level = int(txt)
                logger.info(f'{self.config.building_pos[key - 1]} tesser -> {level}')
            except Exception:
                logger.warning(f'{self.config.building_pos[key - 1]} tesser -> {txt}')
                continue
            if level < min_level:
                min_level = level
                min_building_seq = key

        # 一个屋子的等级都没拿到
        if min_building_seq is None:
            res = choice(list(prop.BUILDING_POS.keys()))
            logger.warning(f'No tesseract result, random to {self.config.building_pos[res - 1]}')
            return res
        else:
            logger.info(f'Minimum level is {min_level} from {self.config.building_pos[min_building_seq - 1]}')
            return min_building_seq

    def _upgrade(self):
        logger.info("Start upgrading")
        # 迭代次数加一
        self.upgrade_iter_round += 1

        self.d.click(*prop.BUILDING_DETAIL_BTN)
        time.sleep(1)
        need_upgrade_building_seq = self._select_min_building()
        self.d.click(*self._get_position(need_upgrade_building_seq))
        time.sleep(1)
        self.d.long_click(prop.BUILDING_UPGRADE_BTN[0], prop.BUILDING_UPGRADE_BTN[1],
                          self.config.upgrade_press_time_sec)
        time.sleep(0.5)
        self.d.click(*prop.BUILDING_DETAIL_BTN)
        logger.info("Upgrade complete")
    
    def _assigned_uprade(self):
        logger.info("Start assigned upgrading")
        self.d.click(*prop.BUILDING_DETAIL_BTN)
        time.sleep(0.5)
        self.d.click(*self._get_position(self.config.assigned_building_pos))
        time.sleep(0.5)
        self.d.long_click(prop.BUILDING_UPGRADE_BTN[0], prop.BUILDING_UPGRADE_BTN[1],
                          self.config.upgrade_press_time_sec)
        time.sleep(0.5)
        self.d.click(*prop.BUILDING_DETAIL_BTN)
        logger.info("Upgrade complete")

    def _upgrade_to(self, target_level):
        """
        target_level: 目标等级
        升至 target_level 级
        利用 Tesseract 识别当前等级后点击升级按钮 target_level - 当前等级次
        """
        screen = self.d.screenshot(format="opencv")
        screen = UIMatcher.pre_building_panel(screen)
        tmp = UIMatcher.cut(screen, prop.BUILDING_INFO_PANEL_LEVEL_POS, (120, 50))
        # import cv2
        # cv2.imwrite("./tmp/screen.jpg", screen)
        tmp = UIMatcher.plain(tmp)
        tmp = UIMatcher.fill_color(tmp)
        tmp = UIMatcher.plain(tmp)
        txt = UIMatcher.image_to_txt(tmp, plus='-l chi_sim --psm 7')
        txt = UIMatcher.normalize_txt(txt)
        try:
            cur_level = int(txt)
            logger.info(f'Current level -> {cur_level}')
        except Exception:
            logger.warning(f'Current level -> {txt}')
            return
        click_times = target_level - cur_level
        self._upgrade_times(click_times)

    def _upgrade_times(self, click_times: int):
        """
        click_times: 点击/升级次数
        执行点击升级按钮的操作 click_times 次
        """
        # assert(times >= 0)
        while click_times > 0:
            click_times -= 1
            bx, by = prop.BUILDING_INFO_PANEL_UPGRADE_BTN
            self.d.click(bx, by)
            time.sleep(0.015)
        logger.info("Upgrade complete")
        # 非命令模式下完成操作后返回主界面以继续常规流程
        if not self.command_mode:
            self._return_main_area()

    def _return_main_area(self):
        """
        通过点击两次导航栏内建设按钮来回到主界面
        """
        time.sleep(0.5)
        tx, ty = prop.CONSTRUCT_BTN
        self.d.click(tx, ty)
        time.sleep(0.1)
        self.d.click(tx, ty)
        time.sleep(0.5)

    def _unpack_times(self, pack_type, sum: int):
        # 红包标题栏坐标 开红包后点这里直到开完这个红包
        tx, ty = prop.REDPACKET_TITLE_POS
        if pack_type == 'm':
            bx, by = prop.REDPACKET_BTN_M
            t = 6
        elif pack_type == 'l':
            bx, by = prop.REDPACKET_BTN_L
            t = 12
        else:
            # logger.inf("暂不支持开小红包")
            bx, by = prop.REDPACKET_BTN_S
            t = 3
        while sum > 0:
            sum -= 1
            self.d.click(bx, by)
            time.sleep(0.5)
            self.d.click(tx, ty)
            time.sleep(0.5)
            # 防止意外多点几下 例如升星或开出史诗
            for i in range(t):
                # logger.info(f"第{i}次点击")
                self.d.click(tx, ty)
                time.sleep(0.5)
    
    def _open_albums(self, sum: int):
        tx, ty = prop.ALBUM_BTN
        bx, by = prop.REDPACKET_TITLE_POS
        while sum > 0:
            sum -= 1
            self.d.click(tx, ty)
            time.sleep(1)
            for i in range(5):
                logger.info(f"第{i}次点击")
                self.d.click(bx, by)
                time.sleep(0.5)

    def _is_good_to_go(self):
        screen = self.d.screenshot(format="opencv")
        return UIMatcher.match(screen, TargetType.Rank_btn) is not None

    def _refresh_train_by_restart(self):
        """
        通过重启游戏的方法来刷新火车
        全程用时大约在 20s 左右
        qq 账号测试不用授权 20s 左右
        """
        time_before_restart = time.time()
        self.d.app_stop("com.tencent.jgm")
        self.d.app_start("com.tencent.jgm", activity=".MainActivity")
        time.sleep(5)
        good_to_go = False
        while not good_to_go:
            if self._is_good_to_go():
                good_to_go = True
                logger.info(f"Refresh train costs {round(time.time() - time_before_restart, 2)}s.")
            else:
                time.sleep(1)

    def _refresh_train_by_reconnect(self):
        """
        通过关闭开启 wifi 的方法刷新火车
        要重新登陆+授权 暂时弃用
        """
        self.d.press("home")
        time.sleep(0.5)
        logger.info("Wifi disable.")
        logger.info(self.d.adb_shell("svc wifi disable"))
        time.sleep(0.5)
        logger.info("Wifi enable.")
        logger.info(self.d.adb_shell("svc wifi enable"))
        time.sleep(5)
        self.d.app_start("com.tencent.jgm", activity=".MainActivity")