from PIL import ImageGrab # 屏幕截图
import numpy as np # 用于图像处理
import win32gui # 用于Windows GUI操作
import win32con # API的常量定义
import win32api # 用于执行一些底层Windows操作
import random # 随机数生成库
import cv2 # 图像处理
import time # 用于控制时间间隔
# 窗体相关参数
WINDOW_TITLE = "QQ游戏 - 连连看角色版"
# 时间间隔参数
MIN_TIME_INTERVAL = 0.00 # 最小时间间隔(秒)
MAX_TIME_INTERVAL = 0.06 # 最大时间间隔(秒)
# 游戏区域偏移参数
GAME_AREA_LEFT_MARGIN = 14 # 游戏区域左侧的偏移量
GAME_AREA_TOP_MARGIN = 181 # 游戏区域顶部的偏移量
# 游戏区域方块布局参数
HORIZONTAL_BLOCKS = 19 # 横向的方块数量
VERTICAL_BLOCKS = 11 # 纵向的方块数量
# 方块尺寸参数
BLOCK_WIDTH = 31 # 方块的宽度
BLOCK_HEIGHT = 35 # 方块的高度
# 特殊图像编号
EMPTY_BLOCK_ID = 0 # 空图像的编号
# 图像切片坐标参数
SLICE_LEFT_TOP_X = 8 # 切片左上角的x坐标
SLICE_RIGHT_BOTTOM_X = 27 # 切片右下角的x坐标
SLICE_LEFT_TOP_Y = 8 # 切片左上角的y坐标
SLICE_RIGHT_BOTTOM_Y = 27 # 切片右下角的y坐标
def get_game_window():
# 使用win32gui的FindWindow函数来查找具有指定标题的窗口
game_window_handle = win32gui.FindWindow(None, WINDOW_TITLE)
# 初始化一个循环,用于不断尝试查找窗口,直到找到为止
while not game_window_handle:
print('未能定位到游戏窗口,请确保游戏窗口已打开,并在10秒后重试...')
time.sleep(10) # 等待10秒后重试查找
game_window_handle = win32gui.FindWindow(None, WINDOW_TITLE) # 再次尝试查找窗口
# 使用SetForegroundWindow函数将游戏窗口设置为前台窗口(即置顶)
win32gui.SetForegroundWindow(game_window_handle)
# 使用GetWindowRect函数获取窗口的位置和大小信息
game_window_rect = win32gui.GetWindowRect(game_window_handle)
# 打印窗口的位置信息,包括左上角和右下角的坐标
print(f"游戏窗口位置:{game_window_rect}")
# 返回窗口左上角的x坐标和y坐标
return game_window_rect[0], game_window_rect[1]
def get_screen_image():
# 打印提示信息,告知用户正在截图
print('正在截图...')
# 使用ImageGrab捕获整个屏幕的截图,并返回Image对象
screenshot_image = ImageGrab.grab()
# 将PIL的Image对象转换为numpy数组
screenshot_np = np.array(screenshot_image)
# 转换颜色通道顺序,因为PIL使用'RGB',而OpenCV使用'BGR'
screenshot_cv = cv2.cvtColor(screenshot_np, cv2.COLOR_RGB2BGR)
# 返回OpenCV格式的图像数组
return screenshot_cv
def get_all_squares(screen_image, game_position):
# 初始化游戏窗体边缘的偏移量
margin_left = GAME_AREA_LEFT_MARGIN # 左侧偏移量
margin_height = GAME_AREA_TOP_MARGIN # 高度偏移量
# 根据游戏窗体位置计算游戏区域的左上角坐标
game_x = game_position[0] + margin_left
game_y = game_position[1] + margin_height
# 初始化一个列表,用于存储所有切割后的小方块
all_squares = []
# 遍历游戏区域的每个小方块
for x in range(0, HORIZONTAL_BLOCKS): # 遍历水平方向的小方块
for y in range(0, VERTICAL_BLOCKS): # 遍历垂直方向的小方块
# 根据小方块的坐标切割屏幕图像
square = screen_image[
game_y + y * BLOCK_HEIGHT: game_y + (y + 1) * BLOCK_HEIGHT,
game_x + x * BLOCK_WIDTH: game_x + (x + 1) * BLOCK_WIDTH
]
all_squares.append(square)
# 初始化一个列表,用于存储去除边缘后的小方块
final_squares = []
# 遍历所有小方块,去除边缘像素
for square in all_squares:
# 假设SUB_LT_X, SUB_LT_Y, SUB_RB_X, SUB_RB_Y是预定义的边缘去除参数
# 去除每个小方块边缘的一圈像素
edge_removed_square = square[SLICE_LEFT_TOP_Y:SLICE_RIGHT_BOTTOM_Y, SLICE_LEFT_TOP_X:SLICE_RIGHT_BOTTOM_X]
final_squares.append(edge_removed_square)
return final_squares
def is_image_exist(image, image_list):
# 将传入的图像转换为numpy数组(如果它还不是numpy数组的话)
image_array = np.asarray(image)
# 遍历图像列表中的每张图像
for idx, existing_image in enumerate(image_list):
# 确保要比较的图像也是numpy数组
existing_image_array = np.asarray(existing_image)
# 计算两张图像之间的差异
difference = np.subtract(existing_image_array, image_array)
# 如果所有像素的差异都是0,即两张图像相同
if not np.any(difference):
return idx # 返回找到相同图像的索引
# 如果没有找到相同的图像
return -1
def get_all_square_types(all_squares):
print("初始化方块类型...")
# 存储不同方块类型的列表
square_types = []
# 存储每个类型出现次数的列表
type_counts = []
# 遍历所有小方块
for square in all_squares:
# 检查当前方块是否已存在于类型列表中
square_index = is_image_exist(square, square_types)
if square_index == -1:
# 如果不存在,则添加到类型列表中,并将计数器初始化为1
square_types.append(square)
type_counts.append(1)
else:
# 如果存在,则增加对应类型的计数器
type_counts[square_index] += 1
# 找到出现次数最多的方块类型的计数器
max_count = max(type_counts)
# 找到出现次数最多的方块类型的索引
max_count_index = type_counts.index(max_count)
# 更新全局变量 EMPTY_BLOCK_ID,假设 EMPTY_BLOCK_ID 表示空白块的索引
global EMPTY_BLOCK_ID
EMPTY_BLOCK_ID = max_count_index
print('空白块的ID是:' + str(EMPTY_BLOCK_ID))
return square_types
def get_square_record(all_squares, square_types, v_num):
print("转换地图...")
# 存储最终记录
record = []
# 当前行的方块类型索引
current_line = []
for square in all_squares:
found_match = False
# 遍历所有方块类型
for type_idx, type_square in enumerate(square_types):
# 计算当前方块与类型方块的差异
diff = np.abs(square - type_square)
# 检查差异是否全为0(即是否找到完全匹配的方块)
if np.all(diff == 0):
current_line.append(type_idx)
found_match = True
break
# 如果没有找到匹配项,则添加-1表示未找到匹配
if not found_match:
current_line.append(-1)
# 如果当前行已满或到达最后一个方块,则保存当前行并重置
if len(current_line) == v_num or square is all_squares[-1]:
record.append(current_line)
current_line = []
# 检查是否有未添加到记录的最后一行(包含-1的情况)
if current_line:
record.append(current_line)
return record
def can_connect(x1, y1, x2, y2, matrix):
# 复制矩阵,避免修改原矩阵
grid = matrix[:]
# 定义空点的常量,这里需要外部定义EMPTY_ID
# EMPTY_ID = ... # 需要根据实际情况定义
# 如果两个点中有一个为空点,则直接返回False
if grid[x1][y1] == EMPTY_BLOCK_ID or grid[x2][y2] == EMPTY_BLOCK_ID:
return False
# 如果两个点相同,则它们不是两个不同的点,直接返回False
if x1 == x2 and y1 == y2:
return False
# 如果两个点所在的区域不同(即值不同),则它们不连通,返回False
if grid[x1][y1] != grid[x2][y2]:
return False
# 判断横向是否连通
if is_horizontally_connected(x1, y1, x2, y2, grid):
return True
# 判断纵向是否连通
if is_vertically_connected(x1, y1, x2, y2, grid):
return True
# 判断是否可以通过一个拐点连通
if is_connected_with_one_turn(x1, y1, x2, y2, grid):
return True
# 判断是否可以通过两个拐点连通
if is_connected_with_two_turns(x1, y1, x2, y2, grid):
return True
# 不可连通,返回False
return False
# 判断横向是否连通
def is_horizontally_connected(x1, y1, x2, y2, grid):
# 如果两点坐标相同,则它们不是两个不同的点,直接返回False
if x1 == x2 and y1 == y2:
return False
# 如果两点不在同一行,则它们不是水平连通的,直接返回False
if x1 != x2:
return False
# 获取两点中较小的y坐标作为起始点
start_y = min(y1, y2)
# 获取两点中较大的y坐标作为结束点
end_y = max(y1, y2)
# 如果两点之间只有一个点的距离,则它们是直接相邻的,返回True
if (end_y - start_y) == 1:
return True
# 遍历两点之间的所有点
for i in range(start_y + 1, end_y):
# 如果发现有一个点不是空的(不是EMPTY_ID),则两点不是水平连通的,返回False
if grid[x1][i] != EMPTY_BLOCK_ID:
return False
# 如果所有点都是空的,则两点是水平连通的,返回True
return True
# 判断纵向是否连通
def is_vertically_connected(x1, y1, x2, y2, grid):
# 如果两点坐标相同,则它们不是两个不同的点,直接返回False
if x1 == x2 and y1 == y2:
return False
# 如果两点不在同一列,则它们不是垂直连通的,直接返回False
if y1 != y2:
return False
# 获取两点中较小的x坐标作为起始点
start_x = min(x1, x2)
# 获取两点中较大的x坐标作为结束点
end_x = max(x1, x2)
# 如果两点之间只有一个点的距离,则它们是直接相邻的,返回True
if end_x - start_x == 1:
return True
# 遍历两点之间的所有点
for current_x in range(start_x + 1, end_x):
# 如果发现有一个点不是空的(不是EMPTY_ID),则两点不是垂直连通的,返回False
if grid[current_x][y1] != EMPTY_BLOCK_ID:
return False
# 如果所有点都是空的,则两点是垂直连通的,返回True
return True
# 判断是否可以通过一个拐点连通
def is_connected_with_one_turn(x1, y1, x2, y2, grid):
# 如果两点在同一行或同一列,则它们不需要转弯即可连通,直接返回False
if x1 == x2 or y1 == y2:
return False
# 定义拐点的坐标
cx, cy = x1, y2 # 第一个可能的拐点
dx, dy = x2, y1 # 第二个可能的拐点
# 检查第一个拐点的情况
# 如果第一个拐点为空,并且从起点到拐点水平连通,从拐点到终点垂直连通,则返回True
if grid[cx][cy] == EMPTY_BLOCK_ID:
if is_horizontally_connected(x1, y1, cx, cy, grid) and is_vertically_connected(cx, cy, x2, y2, grid):
return True
# 检查第二个拐点的情况
# 如果第二个拐点为空,并且从起点到拐点垂直连通,从拐点到终点水平连通,则返回True
if grid[dx][dy] == EMPTY_BLOCK_ID:
if is_vertically_connected(x1, y1, dx, dy, grid) and is_horizontally_connected(dx, dy, x2, y2, grid):
return True
# 如果两种情况都不满足,则返回False
return False
# 判断是否可以通过两个拐点连通
def is_connected_with_two_turns(x1, y1, x2, y2, grid):
# 如果起点和终点相同,则它们不需要转弯即可连通,直接返回False
if x1 == x2 and y1 == y2:
return False
# 遍历整个数组找合适的拐点
for i in range(0, len(grid)):
for j in range(0, len(grid[1])):
# 不为空不能作为拐点
if grid[i][j] != EMPTY_BLOCK_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 is_connected_with_one_turn(x1, y1, i, j, grid) and (
is_horizontally_connected(i, j, x2, y2, grid) or is_vertically_connected(i, j, x2, y2, grid)):
return True
if is_connected_with_one_turn(i, j, x2, y2, grid) and (
is_horizontally_connected(x1, y1, i, j, grid) or is_vertically_connected(x1, y1, i, j, grid)):
return True
return False
def auto_release(grid, game_x, game_y):
for i in range(len(grid)): # 遍历矩阵的行
for j in range(len(grid[0])): # 遍历矩阵的列
if grid[i][j] != EMPTY_BLOCK_ID: # 如果当前位置不是空
for m in range(len(grid)): # 再次遍历矩阵的行
for n in range(len(grid[0])): # 再次遍历矩阵的列
if grid[m][n] != EMPTY_BLOCK_ID: # 如果另一个位置也不是空
if can_connect(i, j, m, n, grid): # 如果两个位置可以连接消除
grid[i][j] = EMPTY_BLOCK_ID # 将两个位置设置为空
grid[m][n] = EMPTY_BLOCK_ID
print(f'消除:{i + 1},{j + 1} 和 {m + 1},{n + 1}')
# 计算鼠标操作的坐标
x1 = game_x + j * BLOCK_WIDTH
y1 = game_y + i * BLOCK_HEIGHT
x2 = game_x + n * BLOCK_WIDTH
y2 = game_y + m * BLOCK_HEIGHT
# 移动鼠标并点击消除两个位置
win32api.SetCursorPos((x1 + 15, y1 + 18))
click(x1 + 15, y1 + 18)
time.sleep(random.uniform(MIN_TIME_INTERVAL, MAX_TIME_INTERVAL))
win32api.SetCursorPos((x2 + 15, y2 + 18))
click(x2 + 15, y2 + 18)
time.sleep(random.uniform(MIN_TIME_INTERVAL, MAX_TIME_INTERVAL))
return True # 消除成功,返回True
return False # 没有找到可消除的元素对,返回False
def click(x, y):
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, x, y, 0, 0)
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, x, y, 0, 0)
def auto_remove(game_board, game_window_position):
# 计算游戏面板的左上角坐标
game_x = game_window_position[0] + GAME_AREA_LEFT_MARGIN
game_y = game_window_position[1] + GAME_AREA_TOP_MARGIN
# 初始化消除计数为0
elimination_count = 0
# 循环调用 auto_release 函数,直到没有可消除的方块对为止
while auto_release(game_board, game_x, game_y):
# 假设 auto_release 在每次成功消除时返回 True,并且每次消除至少一个方块
# 因此,我们增加消除计数
elimination_count += 1
# 返回消除的总数量
return elimination_count
def main():
# 设置随机数生成器的种子,确保结果的可复现性
random.seed()
# 获取游戏窗口的位置
game_pos = get_game_window()
# 获取屏幕图像
screen_image = get_screen_image()
# 从屏幕图像中提取所有的方块列表
all_square_list = get_all_squares(screen_image, game_pos)
# 获取所有方块的类型
types = get_all_square_types(all_square_list)
# 根据方块列表、类型以及垂直方块规则,获取方块记录
result_list = get_square_record(all_square_list, types, VERTICAL_BLOCKS)
# 将列表转换为NumPy数组,便于后续操作
result_array = np.array(result_list)
# 设置合适的维度,用于重塑数组
m, n = 19, 11
# 将数组重塑为指定的形状
result_array_reshaped = result_array.reshape((m, n))
# 打印重塑后的数组,用于调试或展示
print(result_array_reshaped)
# 对数组进行转置,可能用于适应游戏的特定逻辑
result_transposed = result_array_reshaped.T
# 使用转置后的数组进行自动消除操作,并打印消除的总数量
elimination_amount = auto_remove(result_transposed, game_pos)
# 打印消除的总数量
print('消除的总数量是', elimination_amount)
if __name__ == '__main__':
# 调用主函数
main()
python-QQ游戏-连连看-秒杀
于 2024-04-25 11:36:47 首次发布