天天看点

python — Auto_QQ连连看

目录

​​1.前言​​

​​2.使用的扩展库​​

​​3.思路​​

​​4.具体实现​​

​​1)   __main__​​

​​2)   GetGameWindow()​​

​​3)  getScreenImage()​​

​​4)  getAllSquare()​​

​​5)  getAllSquareTypes()​​

​​6)  getAllSquareRecord()​​

​​7)  autoRemove()​​

​​5.思考​​

​​1)  窗体大小可修改​​

​​2)  若空白块变成渐变色的问题 /  方块不完全相同  / 同种周围存在某些小特效导致pending结果为不同​​

​​6.总结​​

1.前言

继python跳一跳之后的下一份python练习....先上一个成果图 , 当然这里的东西仅供学习和参考 , 若用于其他途径后果自负

当然开始之前要先来一句...Python大法好~...Java下这个代码大概需要2000行左右,而python算上注释也才300行而已

python — Auto_QQ连连看

2.使用的扩展库

python3.6 + opencv-python + pywin32 + PIL + numpy

若缺失库,这里给出安装命令

pip install opencv-python

pip install pypiwin32

pip install PIL

pip install numpy

若无法安装请自行百度

注意分辨率需要 1920 * 1080

3.思路

  1. 通过win32定位游戏窗体,并获取截图
  2. 从截图中定位需要pending的游戏区域
  3. 将图片切片成二维单个方块的Img地图,并将方块分类编号
  4. 将Img地图转换成二维编号矩阵
  5. 处理是否可以消除并模拟点击

4.具体实现

1)   __main__

if __name__ == '__main__':
    # 撒随机种子
    random.seed()
    # i. 定位游戏窗体
    game_pos = getGameWindow()
    # 定位到游戏窗体后等待一秒
    time.sleep(1)
    # ii. 获取屏幕截图
    screen_image = getScreenImage()
    # iii. 对截图切片,形成一张二维地图
    all_square_list = getAllSquare(screen_image,game_pos)
    # iv. 获取所有类型的图形,并编号
    types = getAllSquareTypes(all_square_list)
    # v. 讲获取的图片地图转换成数字矩阵
    result = np.transpose(getAllSquareRecord(all_square_list,types))
    # vi. 执行消除 , 并输出消除数量
    print('The total elimination amount is ' + str(autoRemove(result,game_pos)) )      

2)   GetGameWindow()

# 获取窗体坐标位置
def getGameWindow():
    # 通过窗口标题名称定位游戏窗口
    window = win32gui.FindWindow(None,WINDOW_TITLE)
    
    # 若没有定位到游戏窗体
    # 则10s后重新定位,直到定位到游戏窗口
    while not window:
        print('Failed to locate the game window , reposition the game window after 10 seconds...')
        time.sleep(10)
        window = win32gui.FindWindow(None,WINDOW_TITLE)

    # 将游戏窗口置顶
    win32gui.SetForegroundWindow(window) 
    
    # 获取窗口左上角的坐标
    pos = win32gui.GetWindowRect(window)
    print("Game windows at " + str(pos))
    return (pos[0],pos[1])      

3)  getScreenImage()

# 获取屏幕截图
def getScreenImage():
    print('Shot screen...')
    # 获取屏幕截图 
    # 储存在根目录的 'screen.png'
    scim = ImageGrab.grab()  
    scim.save('screen.png')
    # 用opencv读取屏幕截图  
    # 获取ndarray
    return cv2.imread("screen.png")      

4)  getAllSquare()

# 从截图中分辨图片 处理成地图
def getAllSquare(screen_image,game_pos):
    print('Processing pictures...')
    # 通过游戏窗体定位 加上偏移量获取游戏区域的左上角坐标
    game_x = game_pos[0] + MARGIN_LEFT
    game_y = game_pos[1] + MARGIN_HEIGHT

    # 从游戏区域左上开始
    # 把图像按照具体大小切割成相同的小块
    # 切割标准是按照小块的横纵坐标
    all_square = []
    for x in range(0,H_NUM):
        for y in range(0,V_NUM):
            # ndarray的切片方法 : [纵坐标起始位置:纵坐标结束为止,横坐标起始位置:横坐标结束位置]
            square = screen_image[game_y + y * POINT_HEIGHT :game_y + (y+1) * POINT_HEIGHT,game_x + x * POINT_WIDTH:game_x + (x+1) * POINT_WIDTH]
            all_square.append(square)

    # 因为有些图片的边缘会造成干扰 所以统一把图片往内缩小一圈
    # 对所有的方块进行处理 去掉边缘一圈后返回
    finalresult = []
    for square in all_square:
        s = square[SUB_LT_Y:SUB_RB_Y, SUB_LT_X:SUB_RB_X]
        finalresult.append(s)
    return finalresult      

5)  getAllSquareTypes()

# 判断列表中是否存在相同图形
# 存在返回进行判断图片所在的id 
# 否则返回-1
def isImageExist(img,img_list):
    i = 0
    for existed_img in img_list:
        # 两个图片进行比较 返回的是两个图片的标准差
        b = np.subtract(existed_img,img) 
        # 若标准差全为0 即两张图片没有区别
        if not np.any(b):  
            return i
        i = i + 1
    return -1

# 获取所有的方块类型
def getAllSquareTypes(all_square):
    print("Init pictures types...")
    types = []
    # number列表用来记录每个id的出现次数
    number = []
    # 当前出现次数最多的方块 
    # 这里我们默认出现最多的方块应该是空白块
    nowid = 0;
    for square in all_square:
        nid = isImageExist(square,types)
        # 如果这个图像不存在则插入列表
        if nid == -1 :
            types.append(square)
            number.append(1);
        else:
            # 若这个图像存在则给计数器 + 1
            number[nid] = number[nid] + 1
            if (number[nid] > number[nowid]):
                nowid = nid
    # 更新EMPTY_ID 
    # 即判断在当前这张图中的空白块id
    global EMPTY_ID
    EMPTY_ID = nowid
    print('EMPTY_ID = ' + str(EMPTY_ID))
    return types      

6)  getAllSquareRecord()

# 将二维图片矩阵转换为二维数字矩阵
# 注意因为在上面对截屏切片时是以列为优先切片的 
# 所以生成的record二维矩阵每行存放的其实是游戏屏幕中每列的编号
# 换个说法就是record其实是游戏屏幕中心对称后的列表
def getAllSquareRecord(all_square_list,types):
    print("Change map...")
    record = []  
    line = []   
    for square in all_square_list:  
        num = 0
        for type in types:   
            res = cv2.subtract(square,type) 
            if not np.any(res):     
                line.append(num)    
                break               
            num += 1                
        # 每列的数量为V_NUM 
        # 那么当当前的line列表中存在V_NUM个方块时我们认为本列处理完毕
        if len(line) == V_NUM:     
            print(line);    
            record.append(line)
            line = []
    return record      

7)  autoRemove()

# 判断给出的两个图像能否消除
def canConnect(x1,y1,x2,y2,r):
    
    result = r[:]
    
    # 如果两个图像中有一个为0 直接返回False
    if result[x1][y1] == EMPTY_ID or result[x2][y2] == EMPTY_ID:
        return False
    if x1 == x2 and y1 == y2 :
        return False
    if result[x1][y1] != result[x2][y2]:
        return False
    # 判断横向连通
    if horizontalCheck(x1,y1,x2,y2,result):
        return True
    # 判断纵向连通
    if verticalCheck(x1,y1,x2,y2,result):
        return True
    # 判断一个拐点可连通
    if turnOnceCheck(x1,y1,x2,y2,result):
        return True
    # 判断两个拐点可连通
    if turnTwiceCheck(x1,y1,x2,y2,result):
        return True
    # 不可联通返回False
    return False

# 判断横向联通
def horizontalCheck(x1,y1,x2,y2,result):

    if x1 == x2 and y1 == y2:
        return False
    if x1 != x2:
        return False
    startY = min(y1, y2)
    endY = max(y1, y2)
    # 判断两个方块是否相邻
    if (endY - startY) == 1:
        return True
    # 判断两个方块通路上是否都是0,有一个不是,就说明不能联通,返回false
    for i in range(startY+1,endY):
        if result[x1][i] != EMPTY_ID:
            return False
    return True

# 判断纵向联通
def verticalCheck(x1,y1,x2,y2,result):
    if x1 == x2 and y1 == y2:
        return False
    
    if y1 != y2:
        return False
    startX = min(x1, x2)
    endX = max(x1, x2)
    # 判断两个方块是否相邻
    if (endX - startX) == 1:
        return True
    # 判断两方块儿通路上是否可连。
    for i in range(startX+1,endX):
        if result[i][y1] != EMPTY_ID:
            return False
    return True

# 判断一个拐点可联通
def turnOnceCheck(x1,y1,x2,y2,result):
    if x1 == x2 or y1 == y2:
        return False
    
    cx = x1
    cy = y2
    dx = x2
    dy = y1
    # 拐点为空,从第一个点到拐点并且从拐点到第二个点可通,则整条路可通。
    if result[cx][cy] == EMPTY_ID:
        if horizontalCheck(x1, y1, cx, cy, result) and verticalCheck(cx, cy, x2, y2, result):
            return True
    if result[dx][dy] == EMPTY_ID:
        if verticalCheck(x1, y1, dx, dy, result) and horizontalCheck(dx, dy, x2, y2, result):
            return True
    return False

# 判断两个拐点可联通
def turnTwiceCheck(x1,y1,x2,y2,result):
    if x1 == x2 and y1 == y2:
        return False
    
    # 遍历整个数组找合适的拐点
    for i in range(0,len(result)):
        for j in range(0,len(result[1])):
            # 不为空不能作为拐点
            if result[i][j] != EMPTY_ID:
                continue
            # 不和被选方块在同一行列的不能作为拐点
            if i != x1 and i != x2 and j != y1 and j != y2:
                continue
            # 作为交点的方块不能作为拐点
            if (i == x1 and j == y2) or (i == x2 and j == y1):
                continue
            if turnOnceCheck(x1, y1, i, j, result) and (horizontalCheck(i, j, x2, y2, result) or verticalCheck(i, j, x2, y2, result)):
                return True
            if turnOnceCheck(i, j, x2, y2, result) and (horizontalCheck(x1, y1, i, j, result) or verticalCheck(x1, y1, i, j, result)):
                return True
    return False


# 自动消除
def autoRelease(result,game_x,game_y):
    # 遍历地图
    for i in range(0,len(result)):
        for j in range(0,len(result[0])):
            # 当前位置非空
            if result[i][j] != EMPTY_ID:
                # 再次遍历地图 寻找另一个满足条件的图片
                for m in range(0,len(result)):
                    for n in range(0,len(result[0])):
                        if result[m][n] != EMPTY_ID:
                            # 若可以执行消除
                            if canConnect(i,j,m,n,result):
                                # 消除的两个位置设置为空
                                result[i][j] = EMPTY_ID
                                result[m][n] = EMPTY_ID
                                print('Remove :'+ str(i+1) + ',' + str(j+1) + ' and ' + str(m+1) + ',' + str(n+1))
                                
                                # 计算当前两个位置的图片在游戏中应该存在的位置
                                x1 = game_x + j * POINT_WIDTH
                                y1 = game_y + i * POINT_HEIGHT
                                x2 = game_x + n * POINT_WIDTH
                                y2 = game_y + m * POINT_HEIGHT

                                # 模拟鼠标点击第一个图片所在的位置
                                win32api.SetCursorPos((x1 + 15,y1 + 18))
                                win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, x1+15, y1+18, 0, 0)
                                win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, x1+15, y1+18, 0, 0)
                                
                                # 等待随机时间 ,防止检测
                                time.sleep(random.uniform(TIME_INTERVAL_MIN,TIME_INTERVAL_MAX))

                                # 模拟鼠标点击第二个图片所在的位置
                                win32api.SetCursorPos((x2 + 15, y2 + 18))
                                win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, x2 + 15, y2 + 18, 0, 0)
                                win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, x2 + 15, y2 + 18, 0, 0)
                                time.sleep(random.uniform(TIME_INTERVAL_MIN,TIME_INTERVAL_MAX))
                                #执行消除后返回True
                                return True
    return False


def autoRemove(squares,game_pos):
    game_x = game_pos[0] + MARGIN_LEFT
    game_y = game_pos[1] + MARGIN_HEIGHT
    # 重复执行消除直到不存在可消除的方块
    # 这里给出一个消除上限次数,防止代码某些地方出错对空白块进行pending后死循环
    # while True:
    for i in range(0,MAX_ROUND):
        if not autoRelease(squares,game_x,game_y):
            # 当不再有可消除的方块时结束 , 返回消除数量
            return i;

      

5.思考

首先这个代码肯定有许多的不足之处:

1)  窗体大小可修改

当然如果要解决这个问题也不难,定位窗体时会获得pos[4]四个参数,左上角和右下角的坐标,一般来说游戏界面是等比例放大的,所以只要计算具体的游戏区域占整个窗体的百分比即可

如果存在上面的问题,那么每个方块的大小也需要修改

修改方法也不是很麻烦...同样的首先获取游戏区域的像素大小,不管游戏如何放大,游戏区域内能放下的方格矩阵长宽是不会变的,比如QQ游戏就是11 * 19 , 所以每个方块的大小就用游戏区域的像素大小除一下就行了,边缘处理也可以简单的使用60%,至于为什么是60%....自己想吧

2)  若空白块变成渐变色的问题 /  方块不完全相同  / 同种周围存在某些小特效导致pending结果为不同

其实也不难,这里判断两个方块是否相同的方式是通过两个图像的标准差为0,那么如果存在渐变色,那么我们只要计算出一个误差上限即可,标准差在误差内,我们就认为它们是同一种方块,这个问题就跟上面的60%一样,因为我们这里一定要标准差为0才认为两个方块是一样的,所以pending方块不能有一点误差,所以需要减掉的边缘需要大一些,那么如果不强制要求100%相似度判断的话,那自然也不需要减掉那么多边缘,因为边缘减掉的越多,也会造成另一种误差

6.总结

显然这里给出的代码不包含config参数.....毕竟连config都一起给出来了那不是直接变成发布外挂了吗....

至于我的参数是怎么得到的....我打开画图用手描出来的参数我会乱说?