Python 玩微信跳一跳

wechat_jump
環境:Python, Android SDK
方法:利用Python識別棋子和落點位置,自動執行跳一跳
- 棋子落穩之后,截圖
- 根據像素 RGB,計算棋子的坐標和下一個落點的坐標
- 根據兩個點的距離乘以一個時間系數獲得長按的時間
識別棋子:通過棋子底部(大概是一條直線)顏色識別位置
識別落點:根據背景色和落點的色差識別
文件下載:
與手機相關函數
函數 | 說明 |
---|---|
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)