2017 年 12 月 28 日下午,微信發布了 6.6.1 版本,加入了「小遊戲」功能,並提供了官方 DEMO「跳一跳」。這是一個 2.5D 插畫風格的益智遊戲,玩家可以通過按壓屏幕時間的長短來控制這個「小人」跳躍的距離。分數越高,那麼在好友排行榜更加靠前。
輔助腳本wechat_jump_game(https://github.com/wangshub/wechat_jump_game)是一個python2編寫的輔助外掛。操作簡單,易於配置,支持Windows和Mac平臺,通過移植也可以跑在Linux平臺。輔助通過Python的圖片操作庫分析手機實時的截圖,通過圖像識別獲取棋子位置和棋盤的位置,從而通過ADB模擬點擊操作完成自動操作。
下面是輔助腳本工作的視頻:
輔助工作原理adb shell screencap -p /sdcard/autojump.png
adb pull /sdcard/autojump.png .
adb shell input swipe x y x y time(ms)
核心:我們這次講解以最常用最好用的自動腳本為例。每次落穩之後截圖,根據截圖算出棋子的坐標和下一個塊頂面的中點坐標,根據兩個點的距離乘以一個時間係數獲得長按的時間
1.首先腳本會檢測ADB的可用性,並獲取當前手機型號,解析度等信息。
def _get_screen_size():
'''
獲取手機屏幕大小
'''
size_str = os.popen('adb shell wm size').read()
if not size_str:
print('請安裝 ADB 及驅動並配置環境變量')
sys.exit()
m = re.search(r'(\d+)x(\d+)', size_str)
if m:
return "{height}x{width}".format(height=m.group(2), width=m.group(1))
return "1920x1080"
注意re.serarch是正則表達式操作,目的是從ADB輸出中提取出屏幕解析度的兩個數字。
def open_accordant_config():
'''
調用配置文件
'''
screen_size = _get_screen_size()
config_file = "{path}/config/{screen_size}/config.json".format(
path=sys.path[0],
screen_size=screen_size
)
if os.path.exists(config_file):
with open(config_file, 'r') as f:
print("Load config file from {}".format(config_file))
return json.load(f)
else:
with open('{}/config/default.json'.format(sys.path[0]), 'r') as f:
print("Load default config")
return json.load(f)
這段函數實際的操作是通過獲取的屏幕大小獲取之前已經調試好的數據,包括根據屏幕大小測算好的二分之一的棋子底座高度,棋子的寬度,長按的時間係數(後文會繼續說明)等等。
2.截圖以及從手機拉取到PC
if screenshot_way == 2 or screenshot_way == 1:
process = subprocess.Popen('adb shell screencap -p', shell=True, stdout=subprocess.PIPE)
screenshot = process.stdout.read()
if screenshot_way == 2:
binary_screenshot = screenshot.replace(b'\r\n', b'\n')
else:
binary_screenshot = screenshot.replace(b'\r\r\n', b'\n')
f = open('autojump.png', 'wb')
f.write(binary_screenshot)
f.close()
3.關鍵步驟:尋找棋子坐標和下一跳棋盤坐標
這段代碼的核心是Python的PIL庫操作,如果不了解請自行Google學習。
def find_piece_and_board(im):
'''
尋找關鍵坐標
'''
w, h = im.size
piece_x_sum = 0
piece_x_c = 0
piece_y_max = 0
board_x = 0
board_y = 0
scan_x_border = int(w / 8)
scan_start_y = 0
im_pixel = im.load()
for i in range(int(h / 3), int(h*2 / 3), 50):
last_pixel = im_pixel[0, i]
for j in range(1, w):
pixel = im_pixel[j, i]
if pixel[0] != last_pixel[0] or pixel[1] != last_pixel[1] or pixel[2] != last_pixel[2]:
scan_start_y = i - 50
break
if scan_start_y:
break
print('scan_start_y: {}'.format(scan_start_y))
for i in range(scan_start_y, int(h * 2 / 3)):
for j in range(scan_x_border, w - scan_x_border):
pixel = im_pixel[j, i]
if (50 < pixel[0] < 60) and (53 < pixel[1] < 63) and (95 < pixel[2] < 110):
piece_x_sum += j
piece_x_c += 1
piece_y_max = max(i, piece_y_max)
if not all((piece_x_sum, piece_x_c)):
return 0, 0, 0, 0
piece_x = int(piece_x_sum / piece_x_c)
piece_y = piece_y_max - piece_base_height_1_2
這段代碼開始搜尋棋子底端近似一條直線的特徵,通過一個近似的RGB顏色區域來判斷是否是棋子,當程序搜尋到最後一行的時候,計算這一行所都X坐標的平均值記做中點X值,然後將Y坐標向上移動一半棋子底盤高度的值,也就是定位到棋子底面中心,以此確定最終棋子的確切位置
if piece_x < w/2:
board_x_start = piece_x
board_x_end = w
else:
board_x_start = 0
board_x_end = piece_x
上述代表根據棋子坐標確定下一跳棋盤的開始結束坐標
for i in range(int(h / 3), int(h * 2 / 3)):
last_pixel = im_pixel[0, i]
if board_x or board_y:
break
board_x_sum = 0
board_x_c = 0
for j in range(int(board_x_start), int(board_x_end)):
pixel = im_pixel[j, i]
if abs(j - piece_x) < piece_body_width:
continue
if abs(pixel[0] - last_pixel[0]) + abs(pixel[1] - last_pixel[1]) + abs(pixel[2] - last_pixel[2]) > 10:
board_x_sum += j
board_x_c += 1
if board_x_sum:
board_x = board_x_sum / board_x_c
last_pixel = im_pixel[board_x, i]
for k in range(i+274, i, -1):
pixel = im_pixel[board_x, k]
if abs(pixel[0] - last_pixel[0]) + abs(pixel[1] - last_pixel[1]) + abs(pixel[2] - last_pixel[2]) < 10:
break
board_y = int((i+k) / 2)
for l in range(i, i+200):
pixel = im_pixel[board_x, l]
if abs(pixel[0] - 245) + abs(pixel[1] - 245) + abs(pixel[2] - 245) == 0:
board_y = l+10
break
if not all((board_x, board_y)):
return 0, 0, 0, 0
return piece_x, piece_y, board_x, board_y
這時候從邊界最上方一行一行掃描,由於圓形的塊最頂上是一條線,方形的上面大概是一個點,所以就用類似識別棋子的做法多識別了幾個點求中點,這時候得到了塊中點的 X 軸坐標,這時候假設現在棋子在當前塊的中心,從上頂點往下 +274 的位置開始向上找顏色與上頂點一樣的點,為下頂點,取兩者平均則是棋盤上平面中心點Y坐標。另外,如果上一跳命中中間,則下個目標中心會出現 r245 g245 b245 的白點,尋找這個白點可以直接找到棋盤上平面的中心點Y坐標
中心點如圖:
274像素的尋找方式:
4.測算兩點距離和點按時間確定
math.sqrt((board_x - piece_x) ** 2 + (board_y - piece_y) ** 2)
def jump(distance):
'''
跳躍一定的距離
'''
press_time = distance * press_coefficient
press_time = max(press_time, 200)
press_time = int(press_time)
上述代碼提及了press_coefficient也就是上文提到的長按的時間係數,這個量與屏幕大小有關,作者通過大量的調試數據為我們提供了這個比例係數,在腳本初始化時,腳本會根據屏幕解析度(個別手機需要單獨考慮)來讀取各自的config文件,獲取這個係數,下面是1080*1920解析度屏幕的config文件:
{
"under_game_score_y": 300,
"press_coefficient": 1.392,
"piece_base_height_1_2": 20,
"piece_body_width": 70,
"swipe": {
"x1": 500,
"y1": 1600,
"x2": 500,
"y2": 1602
}
}
從github裡這麼多解析度的適配config可以看出,這不是一個人的工作,包括一些特殊機型的適配文件,這些都是Github上眾多開發者共同協作的結果,這就是開源精神。
其本質是通過adb命令,給手機模擬按壓事件
adb shell input swipe x y x y time
其中 x 和 y 是屏幕坐標, time 是觸摸時間,單位ms。在代碼中可以參見jump()函數。以上就完成了一次完整的操作。
5.整體循環邏輯和防和諧
while True:
pull_screenshot()
im = Image.open('./autojump.png')
piece_x, piece_y, board_x, board_y = find_piece_and_board(im)
ts = int(time.time())
print(ts, piece_x, piece_y, board_x, board_y)
set_button_position(im)
jump(math.sqrt((board_x - piece_x) ** 2 + (board_y - piece_y) ** 2))
if debug_switch:
debug.save_debug_screenshot(ts, im, piece_x, piece_y, board_x, board_y)
debug.backup_screenshot(ts)
i += 1
if i == next_rest:
print('已經連續打了 {} 下,休息 {}s'.format(i, next_rest_time))
for j in range(next_rest_time):
sys.stdout.write('\r程序將在 {}s 後繼續'.format(next_rest_time - j))
sys.stdout.flush()
time.sleep(1)
print('\n繼續')
i, next_rest, next_rest_time = 0, random.randrange(30, 100), random.randrange(10, 60)
time.sleep(random.uniform(0.9, 1.2))
以上是完整的主函數循環邏輯,其中還為了防止微信官方屏蔽特意加入了「喘息時間」的機制,即一定步數之後休眠一定時間,還是很機智的呀。
仍然存在的問題由於實現的方法還存在漏洞,程序還存在以下問題:
1.該算法對所有純色平面和部分非純色平面有效,對高爾夫草坪面、木紋桌面、藥瓶和非菱形的碟機(好像是)會判斷錯誤,若上一跳由於某種原因沒有跳到正中間,而下一跳恰好有無法正確識別花紋,則有可能遊戲失敗,由於花紋面積通常比較大,失敗概率較低
2.還沒有智能識別唱片機等加分物件。
總結此輔助是一個很具有趣味性的Python小程序,可以作為研究Python PIL編程和簡單圖片識別入門的小項目。有興趣的話還可以做一些優化給作者pull request啊~