Python 玩微信跳一跳

Python 玩微信跳一跳

wechat_jump
wechat_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)
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372

推薦閱讀更多精彩內容