4399小游戏—宠物连连看经典版2—游戏辅助脚本

引言

期末的时候看到一篇博客,写的宠物连连看的辅助脚本,感觉很有意思,就自己跟着博客自己实现了一遍,开发过程中遇到了一些问题,也体会到了解决问题的乐趣,遂在此记录一下。

先放一下博客的链接:https://www.cnblogs.com/reader/p/10111777.html

这篇博客给出了完整版的代码,大体上我是根据他的思路实现的,只有部分细节按照我自己的想法做了修改。

一、总体开发思路

1 游戏规则

  • 鼠标操作
    • 鼠标点击两个相同图标,如果图标间的连线转折不超过两次并且不经过其他图标,即可消除。
  • 游戏目标
    • 在规定的时间内消去所有的牌。

2 实现流程

​ 具体流程如下:

  • 首先获取屏幕截图,然后将截图分割成一个个小图标,可以得到一个图标矩阵(8*12)
  • 然后将每种图标转化成数字,得到一个数字矩阵,相同的数字表示相同的图标
  • 核心算法是根据游戏规则找到可以相连的两个图标然后模拟鼠标点击消去

二、 具体实现

1、获取窗口句柄、并将窗口显示在最前面

窗口句柄: 简单理解就是窗口的id,可以根据这个id识别已经打开的窗口

在Windows中,句柄是一个系统内部数据结构的引用。例如当你操作一个窗口,或说是一个Delphi窗体时,系统会给你一个该窗口的句柄,系统会通知你:你正在操作142号窗口,就此你的应用程序就能要求系统对142号窗口进行操作——移动窗口、改变窗口大小、把窗口最小化等等。

--《百度百科》

可以使用winspy,spy++ 等工具获取某个窗口的句柄。

使用360浏览器打开宠物连连看小游戏后,我用winspy获取到的句柄"宠物连连看经典版2小游戏,在线玩,4399小游戏 - 360安全浏览器 10.0"

获取到窗口的句柄之后,就可以使用python的库操纵窗口了,使用的库是win32gui

# 获取窗口的句柄
self.hwnd = win32gui.FindWindow(0, wdname)
if self.hwnd == 0:
	print('no such hwnd')
	exit(1)
# 将该窗口显示在最前面
win32gui.SetForegroundWindow(self.hwnd)

执行之后,连连看的窗口就显示到最前面了,下一步就可以截图了

2、 获取屏幕截图,并将图像分割,得到一个图标矩阵

主要使用到的库是PIL的ImageGrab,PIL现在官网上不去,pillow好像和PIL功能是差不多的,文档可以参考pillow的。

这一步的重点是要计算出每个图标的左上角、右下角的坐标,准确将其从截图中分割出来。

 	'''
        获取屏幕截图,并将图标分割
    '''
    def screen_grab(self):
        # 获取整个屏幕截图
        image_grab = ImageGrab.grab()
        # 获取截图中间所有动物图标的截图
        box = (399, 305, 1247, 873)
        animals_iamge  = image_grab.crop(box)

        # 将每个动物图像分割,得到图像矩阵
        images_list = []
        offset = 71   # 将截图用windows自带的画图打开,就可以查看某个点的位置,
        			  # 计算出每个图标的大小大约是71px,不是特别准确,但是基本可以分割出每个图标了
        x0 = 0
        y0 = 0

        for i in range(8):
            images_row = []
            for j in range(12):
                # 小图标左上角的坐标
                x1 = x0 + j*offset
                y1 = y0 + i*offset
                # 小图标右下角的坐标
                x2 = x1 + offset
                y2 = y1 + offset

                # 5px的偏移是为了去掉小图标周围,只保留中间,这样区分不同的图片更容易
                images_row.append(animals_iamge.crop((x1+5, y1+5, x2-5, y2-5)))

            images_list.append(images_row)

        return images_list

3、将图标矩阵转换成数字矩阵

这一步是比较复杂的,主要的目标是将每个图标转换成一个数字,要求相同的图标数字相同。

这里分为两步:

  • 首先将每个图标转成一个灰度图标,然后将这个灰度图标转换成01字符串。

  • 然后比较两个字符串各个位置0、1的区别,记录不同的个数,然后设定一个阈值,不同的个数如果低于这个阈值(即两个图标相差不多),可以认为它们是同一种图标,否者不是。

比较麻烦的是阈值的确定,只能将两个图片的灰度值慢慢比较,找到一个threshold,高于这个threshold能区分为两个不同的图片;低于这threshold保证两个图标相同。

我用到的一个技巧就是,在创建图标矩阵的时候(上一步),每个图标截取的时候往中间多收缩了5px,这样就可以去掉截取的图标周围的一些"杂质",更容易确定阈值。

    def get_index(self, str01, threshold, str01_list):
        for i in range(len(str01_list)):
            diff = sum(map(operator.ne, str01, str01_list[i]))
            if diff < threshold:
                return i

        return -1 	
    
    '''
        将每个图片转换成一个数字,相同的图标数字相同
    '''
    def image2num(self, animal_images):

        # 将每个图标转换成灰度图标,然后再将灰度图标转换成一个01字符串
        num_str01_matrix = []
        for i in range(8):
            num_row = []
            for j in range(12):
                im = animal_images[i][j]
                im_L = im.convert("L")
                # im_L.show()
                pixels = list(im_L.getdata())  # 每个点的像素值
                avg_pixel = sum(pixels) / len(pixels)
                str01 = "".join(map(lambda x: "1" if x > avg_pixel else "0", pixels))
                num_row.append(str01)

            num_str01_matrix.append(num_row)

        # 将每个01字符串转换成一个数字
        threshold = 800 # 低于这个阈值认为是同一种图片
        
        image_type_list = [] # 所有图标的类型
        num_matrix = np.zeros((8,12), dtype=np.uint32)  #创建一个全0矩阵
       
        for i in range(8):
            for j in range(12):
                index = self.get_index(num_str01_matrix[i][j], threshold, image_type_list)
                if index < 0:
                    image_type_list.append(num_str01_matrix[i][j])
                    num_matrix[i][j] = len(image_type_list)
                else:
                    num_matrix[i][j] = index+1
        return num_matrix

得到的一个矩阵实例

[[ 0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  1  2  2  3  2  4  5  6  7  7  1  8  0]
 [ 0  9  4  9 10  8  3  1  1  2  3 11  5  0]
 [ 0  6 12  6  3  1  2  9  4 12  8 11  3  0]
 [ 0  3 12 10 11 12  3  8  5  9  6  6 10  0]
 [ 0  7  4  7 11  4  8 12  5 12  7  5  2  0]
 [ 0 10  1  4  2 11  7  6  6 11  5  5  3  0]
 [ 0 11  9  5  8  4  2 10  9 10 11  9  9  0]
 [ 0 10 12  7  1 12 10  7  6  4  1  8  8  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0]]

4、判断两个点是否可以点击消除

根据游戏规则,对一个点(x1, y1) 得到它可以直接到达的点的集合list1,所谓直接到达,指的是从(x1, y1)出发上下左右连续为0的点;对于(x2, y2)得到list2,然后判断list1中每个点和list2中每个点有没有可以直接到达(即两个点在同一行或同一列,且中间都是0),如果存在这样地点,就说明(x1, y1) 、(x2, y2)可以到达。

    def is_row_connected(self, x, y1,  y2):
        if y1 > y2:
            tmp = y1
            y1  = y2
            y2  = tmp

        if y2 - y1 == 1:
            return True

        for i in range(y1+1, y2):
            if self.map_matrix[x][i] != 0:
                return False
        return True



    def is_col_connected(self, x1, x2, y):
        if x1 > x2:
            tmp = x1
            x1 = x2
            x2 = tmp

        if x2 - x1 == 1:
            return True

        for i in range(x1+1, x2):
            if self.map_matrix[i][y] != 0:
                return False

        return True

    
    def get_direct_connected(self, x, y):
        # 同一行直接相连的点
        ans_list = []

        row =  x - 1
        while row >= 0:
           if self.map_matrix[row][y] == 0:
                ans_list.append([row, y])
           else:
                break
           row = row - 1

        row = x + 1
        while row < self.map_matrix.shape[0]:
            if self.map_matrix[row][y] == 0:
                ans_list.append([row, y])
            else:
                break
            row = row + 1

        col = y - 1
        while col >= 0:
            if self.map_matrix[x][col] == 0:
                ans_list.append([x, col])
            else:
                break
            col = col - 1

        col = y + 1
        while col < self.map_matrix.shape[1]:
            if self.map_matrix[x][col] == 0:
                ans_list.append([x, col])
            else:
                break
            col = col + 1

        return ans_list

    
    def is_reachable(self, x1, y1, x2, y2):
        # 如果数字不相同,直接返回不可到达
        if self.map_matrix[x1][y1] != self.map_matrix[x2][y2]:
            return False

        list1  = self.get_direct_connected(x1, y1)
        list2  = self.get_direct_connected(x2, y2)

        for x1,y1 in list1:
            for x2,y2 in list2:
                if x1 == x2:
                    if self.is_row_connected(x1, y1, y2):
                        return True

                elif y1 == y2:
                    if self.is_col_connected(x1, x2, y1):
                        return True
        return False

5、扫描整个矩阵,进行点击消除

由于数量比较少,可以直接枚举消除;

pymouse的使用环境主要需要PyHook(下载对应python版本的)和PyUserInput(pip install PyUserInput即可)两个,而且需要先下载PyHook再下载PyUserInut。安装博客: https://blog.csdn.net/dianmomanxue/article/details/95044676

PyHook和PyUserInput都下载好了后:

pip install python-xlib(安装pymouse必须要xlib的支持)

pip install pym

	'''
        依次点击(x1, y1) (x2, y2), 并且将这两个位置的数字变成0
    '''
    def click_and_set0(self, x1, y1, x2, y2):
        # 确定需要点击的坐标的中心位置
        c_x1 =  int(self.base_x + (y1 - 1)*self.width + self.width/2)
        c_y1 =  int(self.base_y + (x1 - 1)*self.width + self.width/2)

        c_x2 =  int(self.base_x + (y2 - 1)*self.width + self.width/2)
        c_y2 =  int(self.base_y + (x2 - 1)*self.width + self.width/2)

        time.sleep(self.click_time)
        self.mouse.click(c_x1, c_y1)
        time.sleep(self.click_time)
        self.mouse.click(c_x2, c_y2)

        # 矩阵中设为0
        self.map_matrix[x1][y1] = 0
        self.map_matrix[x2][y2] = 0

    '''
        扫描整个矩阵,并点击相消
    '''
    def scan_game(self):
        row_num = self.map_matrix.shape[0]
        col_num = self.map_matrix.shape[1]

        # self.click_and_set0(1,5,1,7)
        print(row_num)
        print(col_num)

        for i in range(1, row_num):
            for j in range(1, col_num):

                if self.map_matrix[i][j] == 0:
                    continue

                for l in range(1, row_num):
                    for k in range(1, col_num):

                        if i == l and j == k:
                            continue

                        if self.map_matrix[l][k] == 0:
                            continue

                        if self.is_reachable(i, j, l, k):
                            self.click_and_set0(i, j, l, k)
	# 开始游戏!
	 def start(self):
        # 获取图像矩阵
        animal_images = self.screen_grab()
        # 获取图标的数字矩阵
        self.num_matrix = self.image2num(animal_images)
        # 四周添加上0,做成地图矩阵
        self.map_matrix = np.zeros((self.num_matrix.shape[0] + 2, self.num_matrix.shape[1] + 2), dtype=np.uint32)
        # print(map_matrix.shape)
        self.map_matrix[1:9, 1:13] = self.num_matrix

        print(self.map_matrix)
        self.scan_game()
        self.scan_game() # 很不优雅地扫描两遍,不过数据量小,没有关系

6、 完整代码

https://github.com/ScorpioWZ/4399game

原文地址:https://www.cnblogs.com/VanHa0101/p/14308623.html