美文网首页
Python 玩微信跳一跳

Python 玩微信跳一跳

作者: _阿龙_ | 来源:发表于2018-01-24 11:26 被阅读0次

    Python 玩微信跳一跳

    wechat_jumpwechat_jump

    环境:Python, Android SDK

    方法:利用Python识别棋子和落点位置,自动执行跳一跳

    1. 棋子落稳之后,截图
    2. 根据像素 RGB,计算棋子的坐标和下一个落点的坐标
    3. 根据两个点的距离乘以一个时间系数获得长按的时间

    识别棋子:通过棋子底部(大概是一条直线)颜色识别位置

    识别落点:根据背景色和落点的色差识别

    参考 wangshub
    我的个人博客

    文件下载:

    1. settings.conf
    2. wechat_jump.py

    与手机相关函数

    函数 说明
    get_device_id() 获取手机ID
    get_screen_size() 获取手机屏幕大小
    pull_screenshot() 获取屏幕截图
    check_screenshot() 检查获取截图的方式
    def get_device_id():
        """获取手机ID,若连接多个手机,按1/2/3...选择"""
        devices_list = os.popen('adb devices').read().split('\n')
        devices = []
        for loop in range(1, len(devices_list)):
            if devices_list[loop]:
                devices.append(devices_list[loop].split('\t')[0])
    
        if 0 == len(devices):
            print('\r未找到设备!')
            return(-1)
        elif 1 == len(devices):
            return(devices[0])
        else:
            prompt = ''
            for num in range(len(devices)):
                prompt += '%d. %s\n' % (num+1, devices[num])
            prompt += '请选择设备: '
    
            select = input(prompt)
        return(devices[int(select)-1])
    
    def get_screen_size(device_id):
        """获取手机屏幕大小"""
        size_str = os.popen('adb -s %s shell wm size' % device_id).read()
    
        if not size_str:
            print('\r获取屏幕分辨率失败!')
            sys.exit()
    
        m = re.search(r'(\d+)x(\d+)', size_str)
        width, height = m.group(1), m.group(2)
    
        return(width, height)
    
    def pull_screenshot(device_id, image_phone_path, image_pc_path):
        """获取屏幕截图,目前有 0 1 2 3 四种方法
        添加新的平台监测方法时,可根据效率及适用性由高到低排序"""
        global SCREENSHOT_WAY
        if 1 <= SCREENSHOT_WAY <= 3:
            process = subprocess.Popen('adb -s %s shell screencap -p' % device_id, \
                shell=True, \
                stdout=subprocess.PIPE)
            binary_screenshot = process.stdout.read()
    
            if SCREENSHOT_WAY == 2:
                binary_screenshot = binary_screenshot.replace(b'\r\n', b'\n')
            elif SCREENSHOT_WAY == 1:
                binary_screenshot = binary_screenshot.replace(b'\r\r\n', b'\n')
    
            fp = open(image_pc_path, 'wb')
            fp.write(binary_screenshot)
            fp.close()
        elif SCREENSHOT_WAY == 0:
            os.system('adb -s %s shell screencap -p %s' % (device_id, image_phone_path))
            os.system("adb -s %s pull %s %s" % \
                (device_id, image_phone_path, image_pc_path))
    
    def check_screenshot(device_id, image_phone_path, image_pc_path):
        """检查获取截图的方式"""
        global SCREENSHOT_WAY
        if os.path.isfile(image_pc_path):
            try:
                os.remove(image_pc_path)
            except Exception:
                pass
    
        if SCREENSHOT_WAY < 0:
            print('\r暂不支持当前设备')
            sys.exit()
    
        pull_screenshot(device_id, image_phone_path, image_pc_path)
    
        try:
            Image.open(image_pc_path)
            print('\r采用方式 %d 获取截图' % SCREENSHOT_WAY)
        except Exception:
            SCREENSHOT_WAY -= 1
            check_screenshot(device_id, image_phone_path, image_pc_path)
    

    获取设置

    使用 ConfigParser 读取配置文件参数

    def get_settings(config_file, device_id):
        config = ConfigParser.ConfigParser()
        config.read(config_file)
    
        width, height =  get_screen_size(device_id)
        screen_size = '%s-%s' % (width, height)
    
        settings = {}
    
        settings['man_color'] = config.get('game', 'man_color').split(';')
        settings['man_color'] = list(map(int, settings['man_color']))
    
        settings['next_center_rgb'] = config.get('game', 'next_center_rgb').split(';')
        settings['next_center_rgb'] = list(map(int, settings['next_center_rgb']))
        settings['next_center_offset'] = config.getint('game', 'next_center_offset')
    
        settings['pixel_offset'] = config.getint('game', 'pixel_offset')
        settings['side_scale'] = config.getint('game', 'side_scale')
        settings['board_color_offset'] = config.getint('game', 'board_color_offset')
        settings['press_min_time'] = config.getint('game', 'press_min_time')
        settings['screenshot_way'] = config.getint('game', 'screenshot_way')
        settings['image_phone_path'] = config.get('game', 'image_phone_path')
        settings['image_pc_path'] = config.get('game', 'image_pc_path')
        settings['press_coefficient'] = config.getfloat(screen_size, 'press_coefficient')
        settings['man_base_height'] = config.getfloat(screen_size, 'man_base_height')
        settings['man_body_width'] = config.getfloat(screen_size, 'man_body_width')
        settings['target_height'] = config.getint('game', 'target_height')
    
        return(settings)
    

    默认配置文件:settings.conf

    通用配置 说明
    man_color 棋子底座颜色范围,默认50;60;53;63;95;110表示R: 50-60, G: 53-63, B: 95-110
    pixel_offset 纵向探测步长,默认50
    side_scale 两侧空隙占比,默认8
    board_color_offset 颜色对比差值,默认10
    next_center_rgb 如果上一跳命中中间,则下个目标中心会出现 RGB(245, 245, 245) 的点,默认245;245;245
    next_center_offset 下个目标中心偏差范围,默认200
    press_min_time 最小的按压时间,默认200ms
    target_height 落点目标的最大高度,取开局时最大的方块的上下顶点距离,默认274
    screenshot_way 截图方式,默认3,不要修改
    image_phone_path 手机端截图存放路径,默认/sdcard/Download/screenshot.png
    image_pc_path PC端截图存放路径,默认./screenshot.png
    [game]
    man_color = 50;60;53;63;95;110
    pixel_offset = 50
    side_scale = 8
    board_color_offset = 10
    next_center_rgb = 245;245;245
    next_center_offset = 200
    press_min_time = 200
    target_height = 274
    screenshot_way = 3
    image_phone_path = /sdcard/Download/screenshot.png
    image_pc_path = ./screenshot.png
    
    分辨率:宽-长 说明
    press_coefficient 长按的时间系数
    man_base_height 棋子底座高度
    man_body_width 棋子的宽度
    [540-960]
    press_coefficient = 2.732
    man_base_height = 40
    man_body_width = 70
    
    [720-1280]
    press_coefficient = 2.099
    man_base_height = 26
    man_body_width = 47
    
    [720-1440]
    press_coefficient = 2.099
    man_base_height = 26
    man_body_width = 47
    
    [1080-1920]
    press_coefficient = 1.392
    man_base_height = 40
    man_body_width = 70
    
    [1080-2160]
    press_coefficient = 1.372
    man_base_height = 50
    man_body_width = 85
    
    [1440-2560]
    press_coefficient = 1.475
    man_base_height = 56
    man_body_width = 110
    

    游戏相关

    函数 说明
    calc_man_board_position() 计算棋子以及落点坐标
    set_button_position() 点击的位置以及“再来一局”按钮的位置
    jump() 跳跃一定的距离
    def calc_man_board_position(image, settings):
        """计算棋子以及落点坐标"""
        width, height = image.size
    
        image_pixel = image.load()  # 图像像素点RGB集合
        top_space = 0               # 棋子或下一步落点顶部空白高度,起始 y 坐标
        side_space = int(float(width) / settings['side_scale'])    # 棋子左右两侧空白宽度
        man_x_sum = 0               # 棋子底部 x 坐标总和
        man_x_count = 0             # 棋子底部 x 坐标个数
        man_y_max = 0               # 棋子底部 y 坐标最大值
        board_x = 0                 # 落点 x 坐标
        board_y = 0                 # 落点 y 坐标
    
        ##################################################
        # 计算棋子坐标
        ##################################################
    
        # 纵向探测屏幕中间 1/3 处
        # 计算棋子或下一步落点顶部空白高度
        for y in range(int(height / 3), int(height*2 / 3), settings['pixel_offset']):
            last_pixel = image_pixel[0, y]  # 记录最左侧像素点 RGB
            for x in range(1, width):
                pixel = image_pixel[x, y]   # 获取像素点 RGB
    
                # 与最左侧像素点 RGB 不一致
                # 找到棋子或下一步落点最顶部的位置
                if pixel != last_pixel:
                    top_space = y - settings['pixel_offset']
                    break
    
            if top_space:
                break
    
        # 纵向探测屏幕中间 1/3 处剩下的部分,计算棋子坐标
        for y in range(top_space, int(height * 2 / 3)):
            for x in range(side_space, width - side_space):
                pixel = image_pixel[x, y]   # 获取像素点 RGB
                # 根据棋子的最低行的颜色,找棋子底部那些点的平均值
                # 这个颜色这样应该 OK,暂时不提出来
                if (settings['man_color'][0] < pixel[0] < settings['man_color'][1]) and \
                    (settings['man_color'][2] < pixel[1] < settings['man_color'][3]) and \
                    (settings['man_color'][4] < pixel[2] < settings['man_color'][5]):
                    man_x_sum += x
                    man_x_count += 1
                    man_y_max = max(y, man_y_max)
    
        if not all((man_x_sum, man_x_count)):
            return(0, 0, 0, 0)
    
        # 棋子坐标
        man_x = int(man_x_sum / man_x_count)
        # 上移棋子底盘高度的一半
        man_y = man_y_max - settings['man_base_height'] / 2
    
        ##################################################
        # 计算落点坐标
        ##################################################
    
        # 限制落点扫描的横坐标,避免音符 bug
        if man_x < width/2:
            board_x_start = man_x
            board_x_end = width
        else:
            board_x_start = 0
            board_x_end = man_x
    
        for y in range(int(height / 3), int(height * 2 / 3)):
            last_pixel = image_pixel[0, y]  # 记录最左侧像素点 RGB
    
            if board_x or board_y:
                break
    
            board_x_sum = 0
            board_x_count = 0
    
            for x in range(int(board_x_start), int(board_x_end)):
                pixel = image_pixel[x, y]   # 获取像素点 RGB
    
                # 修掉脑袋比下一个小格子还高的情况的 bug
                if abs(x - man_x) < settings['man_body_width']:
                    continue
    
                # 若像素点 RGB 与最左侧像素点 RGB 偏差大于 board_color_offset,则认定为边界
                # 修掉圆顶的时候一条线导致的小 bug,这个颜色判断应该 OK,暂时不提出来
                if abs(pixel[0] - last_pixel[0]) \
                        + abs(pixel[1] - last_pixel[1]) \
                        + abs(pixel[2] - last_pixel[2]) > settings['board_color_offset']:
                    board_x_sum += x
                    board_x_count += 1
    
            if board_x_sum:
                board_x = board_x_sum / board_x_count
    
        last_pixel = image_pixel[board_x, y]
    
        # 从上顶点往下 +target_height 的位置开始向上找颜色与上顶点一样的点,为下顶点
        # 该方法对所有纯色平面和部分非纯色平面有效
        # 对高尔夫草坪面、木纹桌面、药瓶和非菱形的碟机(好像是)会判断错误
        for k in range(y+settings['target_height'], y, -1):
            pixel = image_pixel[board_x, k]
            if abs(pixel[0] - last_pixel[0]) + abs(pixel[1] - last_pixel[1]) + abs(pixel[2] - last_pixel[2]) < settings['board_color_offset']:
                break
    
        board_y = int((y+k) / 2)
    
        # 如果上一跳命中中间,则下个目标中心会出现 RGB(245, 245, 245) 的点
        # 利用这个属性弥补上一段代码可能存在的判断错误
        # 若上一跳由于某种原因没有跳到正中间,而下一跳恰好有无法正确识别花纹
        # 则有可能游戏失败,由于花纹面积通常比较大,失败概率较低
        for k in range(y, y+settings['next_center_offset']):
            pixel = image_pixel[board_x, k]
            if abs(pixel[0] - settings['next_center_rgb'][0]) + \
                abs(pixel[1] - settings['next_center_rgb'][1]) + \
                abs(pixel[2] - settings['next_center_rgb'][2]) == 0:
                board_y = k + settings['board_color_offset']
                break
    
        if not all((board_x, board_y)):
            return(0, 0, 0, 0)
    
        return(man_x, man_y, board_x, board_y)
    
    def set_button_position(image):
        """“再来一局”位置"""
        width, height = image.size
    
        left = int(width / 2)
        top = int(1584 * (height / 1920.0))
    
        left = int(random.uniform(left-50, left+50))
        top = int(random.uniform(top-10, top+10))   # 随机防 ban
    
        return(left, top, left, top)
    
    def jump(distance, press_position, press_coefficient):
        """跳跃一定的距离"""
        press_time = distance * press_coefficient
        press_time = max(press_time, settings['press_min_time'])
        press_time = int(press_time)
    
        cmd = 'adb -s %s shell input swipe %s %s %s %s %s' % \
            (device_id, \
            press_position[0], press_position[1], \
            press_position[2], press_position[3], \
            press_time)
    
        os.system(cmd)
    
        return(press_time)
    

    主函数

    # -*- coding: utf-8 -*-
    
    import os, sys, re, ConfigParser, subprocess, math, random, time
    from PIL import Image
    
    def main(device_id, settings):
        check_screenshot(device_id, \
            settings['image_phone_path'], \
            settings['image_pc_path'])
    
        count = 0
        next_rest = random.randrange(3, 10)
        next_rest_time = random.randrange(5, 10)
    
        while True:
            pull_screenshot(device_id, \
                settings['image_phone_path'], \
                settings['image_pc_path'])
    
            image = Image.open(settings['image_pc_path'])
    
            # 获取棋子和落点的位置
            man_x, man_y, board_x, board_y = calc_man_board_position(image, settings)
            # 计算距离
            distance = math.sqrt((board_x - man_x) ** 2 + (board_y - man_y) ** 2)
            # 点击位置的坐标
            press_position = set_button_position(image)
    
            press_time = jump(distance, press_position, settings['press_coefficient'])
    
            print('%.2f, %dms: (%d, %d) -> (%d, %d)' % (distance, press_time, man_x, man_y, board_x, board_y))
    
            image.close()
    
            count += 1
            if count == next_rest:
                print('\r已经连续打了 %s 下,休息 %ss' % (count, next_rest_time))
                for i in range(next_rest_time):
                    sys.stdout.write('\r程序将在 %ds 后继续' % (next_rest_time - i))
                    sys.stdout.flush()
                    time.sleep(1)
                print('\n继续')
    
                count = 0
                next_rest = random.randrange(30, 100)
                next_rest_time = random.randrange(10, 20)
    
            # 为了保证截图的时候应落稳了,多延迟一会儿
            # 随机值防 ban
            time.sleep(random.uniform(0.9, 1.2))
    
    if '__main__' == __name__:
        global SCREENSHOT_WAY
        if 1 == len(sys.argv):
            config_file = './settings.conf'
        else:
            config_file = sys.argv[1]
    
        if not os.path.isfile(config_file):
            print('\r配置文件不存在!')
            sys.exit(-1)
    
        device_id = get_device_id()
        if -1 == device_id:
            print('\r未找到设备!')
            sys.exit(-1)
    
        settings = get_settings(config_file, device_id)
        SCREENSHOT_WAY = settings['screenshot_way']
    
        main(device_id, settings)
    

    相关文章

      网友评论

          本文标题:Python 玩微信跳一跳

          本文链接:https://www.haomeiwen.com/subject/gsayaxtx.html