郭靈紋

Python 基于图像识别的游戏辅助

前言

最近在玩一款微信小游戏,名叫棋逢对手-象棋。游戏玩法是在一个棋盘上有我方‘車’棋子和若干敌方象棋子,棋子移动方式跟象棋规则一致。游戏目标是玩家控制车吃掉棋盘上的地方棋子,每回合后敌方棋子会执行闪避,若干回合后会生成新的棋子。

因为对游戏辅助这块有兴趣,之前还没深入接触过。感觉这个游戏的辅助逻辑应该不会很复杂,随即决定实现它。

技术选型上因为 Python 在 Windows 下的自动测试比较成熟,所以直接选择它。不过实现完后感觉也就是调调 win32api 和 OpenCV api,JavaScript 目前也有相关的库,下次准备尝试使用 nodejs 实现。

想法

熟悉完玩法规则后,我的想法是实现一个程序。通过图像识别出棋盘上的棋子,并标记出它所能影响到的危险区。拆分步骤如下:

  1. 使用 win32aip 获取游戏窗口的句柄
  2. 使用 OpenCV 模板匹配敌方棋子
  3. 处理匹配到的敌方棋子,绘制危险区
  4. 将绘制完的截图展示在UI上

实现

代码比较简单,只贴出一些关键代码和完整代码,不做解释。

枚举窗口句柄

def _find_hwnd(hwnd, _):
global game_hwnd
if IsWindow(hwnd) and IsWindowEnabled(hwnd) and IsWindowVisible(hwnd):
if GetWindowText(hwnd) == '棋逢对手 象棋':
game_hwnd = hwnd

EnumWindows(_find_hwnd, 0)

获取可供 OpenCV 处理的屏幕截图

screen = np.asarray(grab(GetWindowRect(game_hwnd)))

OpenCV 模板匹配

screen_gray = cv2.cvtColor(screen, cv2.COLOR_BGR2GRAY)
res = cv2.matchTemplate(screen_gray, templates[t - 1], cv2.TM_CCOEFF_NORMED)
threshold = 0.8
loc = np.where( res >= threshold)

OpenCV 绘制矩形(危险区)

cv2.rectangle(screen, pt, (pt[0] + chess_w, pt[1] + chess_h), (0, 0, 255), 1)

完整代码

import tkinter
import cv2
import numpy as np
from PIL import Image, ImageTk
from PIL.ImageGrab import grab
from win32api import SendMessage
from win32con import WM_SYSCOMMAND, SC_RESTORE
from win32gui import EnumWindows, IsWindow, IsWindowEnabled, IsWindowVisible, GetWindowText, GetWindowRect, \
SetForegroundWindow

game_hwnd = None

board_x, board_y = (13, 219) # 棋盘偏移
chess_w, chess_h = (50, 50) # 棋子大小
board_x_max, board_y_max = (board_x + (8 * chess_w) + (7 * 3), board_y + (9 * chess_h) + (8 * 3)) # 棋盘大小

templates = [
cv2.imread('t1.png', 0), # 卒
cv2.imread('t2.png', 0), # 士
cv2.imread('t3.png', 0), # 马
cv2.imread('t4.png', 0), # 象
cv2.imread('t5.png', 0), # 車
cv2.imread('t1.png', 0), # 炮
cv2.imread('t7.png', 0), # 将
]


def _find_hwnd(hwnd, _):
global game_hwnd
if IsWindow(hwnd) and IsWindowEnabled(hwnd) and IsWindowVisible(hwnd):
if GetWindowText(hwnd) == '棋逢对手 象棋':
game_hwnd = hwnd


def draw_danger(screen, x, y):
if (board_x < x + (chess_w / 2) < board_x_max) and (board_y < y + (chess_h / 2) < board_y_max):
cv2.rectangle(screen, (x + 5, y + 5), (x + chess_w - 5, y + chess_h - 5), (255, 0, 0), 1)


def draw_chess(screen, t, pt):
# print(pt)
cv2.rectangle(screen, pt, (pt[0] + chess_w, pt[1] + chess_h), (0, 0, 255), 1)

# 卒 or 将
if t == 1 or t == 7:
draw_danger(screen, pt[0], pt[1] - chess_h) # 上
draw_danger(screen, pt[0] - chess_w, pt[1]) # 左
draw_danger(screen, pt[0] + chess_w, pt[1]) # 下
draw_danger(screen, pt[0], pt[1] + chess_h) # 右
# 士
elif t == 2:
draw_danger(screen, pt[0] - chess_w, pt[1] - chess_h) # 左上
draw_danger(screen, pt[0] - chess_w, pt[1] + chess_h) # 左下
draw_danger(screen, pt[0] + chess_w, pt[1] - chess_h) # 右上
draw_danger(screen, pt[0] + chess_w, pt[1] + chess_h) # 右下
# 马
# TODO:拌马腿
elif t == 3:
draw_danger(screen, pt[0] - chess_w, pt[1] - (chess_h * 2)) # 左上上
draw_danger(screen, pt[0] - chess_w, pt[1] + (chess_h * 2)) # 左下下
draw_danger(screen, pt[0] + chess_w, pt[1] - (chess_h * 2)) # 右上上
draw_danger(screen, pt[0] + chess_w, pt[1] + (chess_h * 2)) # 右下下
draw_danger(screen, pt[0] - (chess_w * 2), pt[1] - chess_h) # 左左上
draw_danger(screen, pt[0] - (chess_w * 2), pt[1] + chess_h) # 左左下
draw_danger(screen, pt[0] + (chess_w * 2), pt[1] - chess_h) # 右右上
draw_danger(screen, pt[0] + (chess_w * 2), pt[1] + chess_h) # 右右下
# 象
# TODO:塞象眼
elif t == 4:
draw_danger(screen, pt[0] - (chess_w * 2), pt[1] - (chess_h * 2)) # 左上
draw_danger(screen, pt[0] - (chess_w * 2), pt[1] + (chess_h * 2)) # 左下
draw_danger(screen, pt[0] + (chess_w * 2), pt[1] - (chess_h * 2)) # 右上
draw_danger(screen, pt[0] + (chess_w * 2), pt[1] + (chess_h * 2)) # 右下
# 車
elif t == 5:
# TODO:待实现
return
# 炮
elif t == 6:
# TODO:待实现
return


class App(object):
def __init__(self):
self.root = tkinter.Tk()

self.image = None
self.img_label = tkinter.Label()
self.img_label.pack()

self.root.after(0, self.loop)
self.root.mainloop()

def update(self):
# noinspection PyTypeChecker
screen = np.asarray(grab(GetWindowRect(game_hwnd)))
screen_gray = cv2.cvtColor(screen, cv2.COLOR_BGR2GRAY)
cv2.rectangle(screen, (board_x, board_y), (board_x_max, board_y_max), (255, 255, 255), 1) # 画棋盘

for t in [1, 2, 3, 4, 5, 7]:
res = cv2.matchTemplate(screen_gray, templates[t - 1], cv2.TM_CCOEFF_NORMED)
for pt in zip(*np.where(res >= 0.7)[::-1]):
# 跳过待选区的棋子
if pt[1] < board_y:
continue

draw_chess(screen, t, pt)

self.image = ImageTk.PhotoImage(Image.fromarray(screen))
self.img_label.config(image=self.image)

def loop(self):
self.update()
self.root.after(1000, self.loop)


if __name__ == "__main__":
EnumWindows(_find_hwnd, 0)

if game_hwnd is None:
print('未找到游戏')
exit()

# 还原最小化
SendMessage(game_hwnd, WM_SYSCOMMAND, SC_RESTORE, 0)
# 设置前景窗口
SetForegroundWindow(game_hwnd)

app = App()

后续

目前代码还未实现‘拌马腿’、‘塞象眼’、敌方车、炮的处理。因为不能根据位置直接绘制出危险区,需要获取到棋盘的信息后做逻辑处理,往下投入的时间将会超出了我的学习目的。

参考