You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

514 lines
35 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# _*_ coding:utf-8 _*_
# @Time2023/6/29 8:30
# @File:main.py
# @SoftWare:PyCharm
# @Project:MS
# @author:yzf
from tkinter import scrolledtext, END # 滚动框, 滚动框自动下拉到最后一行
from pymouse import PyMouse # 模拟鼠标自动点击
import tkinter as tk # UI界面
import threading # 多线程任
import pyautogui # 获取窗口位置信息
import random # 随机数
import time # 应用sleep函数睡眠
# 全局变量
TIME_LIMIT = 10 # 扫雷限时
CLOCK = 0 # 扫雷用时记录
TIMER_RUN = False # 计时器是否启动
GAME_OVER = False # 游戏是否结束
curData = [] # 方块状态数组
initData = [] # 初始布雷方案数组
SHOW_BOARD_STATE = [] # 是否计算周围雷数数组
BUTTONS = {} # 方块按钮字典
BOARD_ROWS = 20 # 扫雷方块行数
BOARD_COLS = 20 # 扫雷方块列数
MINES = 96 # 总的雷数
mine_number = 0 # 剩余的雷数
DIGIT_WIDTH = 5 # 数字的大小
DIGIT_HEIGHT = 1
FACE_WIDTH = 40 # 笑脸的大小
FACE_HEIGHT = 40
mine_number_x = 40 # 计雷数器的位置,不管窗口怎么变,计雷数器位置不变
mine_number_y = 20
HEADER_WIDTH = 20 * 40 # 头部栏的大小
HEADER_HEIGHT = 40
RIGHT_WIDTH = 300 # 右侧栏宽度,固定不变
BOTTOM_HEIGHT = 150 # 底部栏高度,固定不变
face_x = 20 * 40 / 2 # 脸图的位置
face_y = 20
clock_x = 20 * 40 - 40 # 计时器的位置
clock_y = 20
MINE_WITH_FLAG = 0 # 是地雷被标旗子的按钮总数
NO_MINE_BUT_FLAG = 0 # 不是地雷但是被标旗子的按钮总数
OPEN_BUTTONS = 0 # 所有被打开的按钮的总数,等于所有按钮数-所有雷数
class Show: # 显示类
def __init__(self): # 构造函数,可用于对象成员属性的初始化。如不用,可不写
self.data = Data() # 创建数据类对象,用于调用数据类中的方法
# 主体窗口设计
self.root = tk.Tk() # 初始化窗口
self.root.title('扫雷') # 窗口标题
self.root.resizable(width=False, height=False) # 设置窗口是否可变宽不可变高不可变默认为True
self.root.geometry(f'{BOARD_COLS * 40}x{(BOARD_ROWS + 1) * 40}')
# 头部栏设计
self.top = tk.Frame(self.root, bg='white', relief="sunken", width=HEADER_WIDTH, height=HEADER_HEIGHT)
# 显示雷数
self.label_mine = tk.Label(self.top, text=str(MINES), height=DIGIT_HEIGHT, width=DIGIT_WIDTH, bg='white', fg='red', font=('幼圆', 22))
# 显示扫雷用时
self.label_clock = tk.Label(self.top, text=str(CLOCK), height=DIGIT_HEIGHT, width=DIGIT_WIDTH, bg='white', fg='red', font=('幼圆', 22))
# 游戏设置
self.label_set = tk.Label(self.top, text='设置', height=DIGIT_HEIGHT, width=5, bg='white', fg='red', font=('幼圆', 20))
# 显示限时时间是多少
self.label_time = tk.Label(self.top, text=str(TIME_LIMIT), height=DIGIT_HEIGHT, width=5, bg='white', fg='red', font=('幼圆', 22))
self.btn = tk.Button(self.top, bg='white', height=FACE_HEIGHT, width=FACE_WIDTH, command=lambda: self.thread(self.game_timer), relief='raised')
# 右侧栏
self.right = tk.Frame(self.root, bg='yellow', relief='sunken', width=RIGHT_WIDTH)
# 底部栏
self.bottom = tk.Frame(self.root, bg='white', relief='sunken', height=BOTTOM_HEIGHT)
self.scroll = scrolledtext.ScrolledText(self.root, bg='white', bd=2, relief='sunken', height=5, font=('楷体', 14))
# 用户自定义设置行值大小
# tk.StringVar()表示输入框中输入的类型是字符串。highlightcolor:输入控件获得输入焦点时的边框颜色。highlightthickness:输入控件的边框宽度
self.label_row = tk.Label(self.right, text='BOARD_ROWS:', height=1, width=11, bg='white', fg='black', font=('TimesNewRoman', 14), relief='raised')
self.entry1 = tk.Entry(self.right, textvariable=tk.StringVar(), bd=2, width=13, highlightcolor='black', highlightthickness=2, font=('楷体', 12))
# 用户自定义设置列值大小
self.label_col = tk.Label(self.right, text='BOARD_COLS:', height=1, width=11, bg='white', fg='black', font=('TimesNewRoman', 14), relief='raised')
self.entry2 = tk.Entry(self.right, textvariable=tk.StringVar(), bd=2, width=13, highlightcolor='black', highlightthickness=2, font=('楷体', 12))
# 用户自定义设置地雷数量
self.label_m = tk.Label(self.right, text='MINES:', height=1, width=11, bg='white', fg='black', font=('TimesNewRoman', 14), relief='raised')
self.entry3 = tk.Entry(self.right, textvariable=tk.StringVar(), bd=2, width=13, highlightcolor='black', highlightthickness=2, font=('楷体', 12))
# 用户自定义设置时间限制为多少默认噩梦等级2400s
self.label_limit = tk.Label(self.right, text='TIME_LIMIT:', height=1, width=11, bg='white', fg='black', font=('TimesNewRoman', 14), relief='raised')
self.entry4 = tk.Entry(self.right, textvariable=tk.StringVar(), bd=2, width=13, highlightcolor='black', highlightthickness=2, font=('楷体', 12))
# 提交和自动扫雷按钮
self.btn2 = tk.Button(self.right, text='提交', bd=3, bg='white', font=('楷体', 15), fg='black', width=4, relief='raised', activebackground='red', command=self.get_data)
self.btn3 = tk.Button(self.right, text='自动挖雷', bd=3, bg='white', font=('楷体', 15), fg='black', width=8, relief='raised', activebackground='red', command=lambda: self.thread(self.data.auto_mine_sweeper))
# 全局图片资源,图片格式必须是'xx.gif'
self.face1_img = tk.PhotoImage(file='img/face1.gif') # 笑脸
self.face2_img = tk.PhotoImage(file='img/face2.gif') # 耍酷脸
self.face3_img = tk.PhotoImage(file='img/face3.gif') # 哭脸
self.p0 = tk.PhotoImage(file='img/0.gif') # 空白方块
self.p1 = tk.PhotoImage(file='img/1.gif') # 数字1
self.p2 = tk.PhotoImage(file='img/2.gif') # 数字2
self.p3 = tk.PhotoImage(file='img/3.gif') # 数字3
self.p4 = tk.PhotoImage(file='img/4.gif') # 数字4
self.p5 = tk.PhotoImage(file='img/5.gif') # 数字5
self.p6 = tk.PhotoImage(file='img/6.gif') # 数字6
self.p7 = tk.PhotoImage(file='img/7.gif') # 数字7
self.p8 = tk.PhotoImage(file='img/8.gif') # 数字8
self.p9 = tk.PhotoImage(file='img/9.gif') # 爆炸雷
self.p10 = tk.PhotoImage(file='img/10.gif') # 标错雷
self.p11 = tk.PhotoImage(file='img/11.gif') # 旗子
self.p12 = tk.PhotoImage(file='img/12.gif') # 立体方块
self.p13 = tk.PhotoImage(file='img/13.gif') # 未爆炸雷
def create_boards(self): # 创建按钮方块,并绑定对应函数
global BOARD_ROWS, BOARD_COLS, BUTTONS
for row in range(BOARD_ROWS):
for col in range(BOARD_COLS):
def on_right_click(event, x=row, y=col):
self.on_right_button_down(event, x, y) # 鼠标左、右键分别绑定函数
button = tk.Button(self.root, command=lambda x=row, y=col: self.on_left_button_down(x, y))
button.place(y=(row + 1) * 40, x=col * 40) # 设置每个按钮的位置。(row+1):头部栏占了一行,所以雷图整体下移一行
button.bind("<Button-3>", on_right_click) # 绑定鼠标右键绑定on_right_button_down(event, x, y)
BUTTONS[row, col] = button # 以坐标为键把该坐标处的button作为值一一对应便于修改对应button的信息
def show_game_window(self): # 根据当前curData显示游戏界面
global curData, BOARD_ROWS, BOARD_COLS, BUTTONS
for row in range(BOARD_ROWS): # row为行,0到BOARD_ROWS-1
for col in range(BOARD_COLS): # col为列,0到BOARD_COLS-1
if curData[row][col] == 'M': # 如果是雷
BUTTONS[row, col]['image'] = self.p13
elif curData[row][col] == 'X': # 如果是挖开的雷
BUTTONS[row, col]['image'] = self.p9
elif curData[row][col] == 'E': # 如果是未挖开的方块
BUTTONS[row, col]['image'] = self.p12
elif curData[row][col] == 'F': # 如果是旗子
BUTTONS[row, col]['image'] = self.p11
elif curData[row][col] == 'B': # 如果是挖开的空方块
BUTTONS[row, col]['image'] = self.p0
elif curData[row][col] == 1: # 如果是数字1-8
BUTTONS[row, col]['image'] = self.p1
elif curData[row][col] == 2:
BUTTONS[row, col]['image'] = self.p2
elif curData[row][col] == 3:
BUTTONS[row, col]['image'] = self.p3
elif curData[row][col] == 4:
BUTTONS[row, col]['image'] = self.p4
elif curData[row][col] == 5:
BUTTONS[row, col]['image'] = self.p5
elif curData[row][col] == 6:
BUTTONS[row, col]['image'] = self.p6
elif curData[row][col] == 7:
BUTTONS[row, col]['image'] = self.p7
elif curData[row][col] == 8:
BUTTONS[row, col]['image'] = self.p8
BUTTONS[row, col]['relief'] = 'groove' # 按钮变为平面,不再有立体感
def header_frame(self):
global HEADER_WIDTH, mine_number_x, mine_number_y, clock_x, clock_y, face_x, face_y
self.top['width'] = HEADER_WIDTH + RIGHT_WIDTH
self.top.place(x=0, y=0, anchor=tk.NW) # 右边栏
self.label_mine.place(x=mine_number_x, y=mine_number_y, anchor=tk.CENTER)
self.label_clock.place(x=clock_x, y=clock_y, anchor=tk.CENTER)
self.btn['image'] = self.face1_img
self.btn.place(x=face_x, y=face_y, anchor=tk.CENTER)
self.label_set.place(x=HEADER_WIDTH + RIGHT_WIDTH / 2, y=20, anchor=tk.CENTER) # "游戏设置"的显示位置,"右侧栏"中部
self.label_time.place(x=HEADER_WIDTH + RIGHT_WIDTH - 50, y=20, anchor=tk.CENTER) # 游戏限时的显示位置,"右侧栏"右上角
def right_frame(self):
global BOARD_ROWS, HEADER_WIDTH, HEADER_HEIGHT
# 以最后的按钮位置y=160为依据(加上按钮本身有高度大致为180)
self.right['height'] = BOARD_ROWS * 40 # 右侧栏高度和雷图高度一致,将右侧完全覆盖
self.right.place(x=HEADER_WIDTH + 1, y=HEADER_HEIGHT, anchor=tk.NW) # 右侧栏紧挨着(一个像素的距离)地雷区域
self.label_row.place(x=80, y=15 + (BOARD_ROWS * 40 - 180) / 2, anchor=tk.CENTER) # 各标签和输入框的位置
self.entry1.place(x=220, y=15 + (BOARD_ROWS * 40 - 180) / 2, anchor=tk.CENTER)
self.label_col.place(x=80, y=50 + (BOARD_ROWS * 40 - 180) / 2, anchor=tk.CENTER)
self.entry2.place(x=220, y=50 + (BOARD_ROWS * 40 - 180) / 2, anchor=tk.CENTER)
self.label_m.place(x=80, y=85 + (BOARD_ROWS * 40 - 180) / 2, anchor=tk.CENTER)
self.entry3.place(x=220, y=85 + (BOARD_ROWS * 40 - 180) / 2, anchor=tk.CENTER)
self.label_limit.place(x=80, y=120 + (BOARD_ROWS * 40 - 180) / 2, anchor=tk.CENTER)
self.entry4.place(x=220, y=120 + (BOARD_ROWS * 40 - 180) / 2, anchor=tk.CENTER)
self.btn2.place(x=220, y=170 + (BOARD_ROWS * 40 - 180) / 2, anchor=tk.CENTER) # 提交按钮位置
self.btn3.place(x=120, y=170 + (BOARD_ROWS * 40 - 180) / 2, anchor=tk.CENTER) # 自动扫雷按钮位置
def bottom_frame(self):
global BOARD_ROWS, BOARD_COLS, HEADER_HEIGHT
self.bottom['width'] = BOARD_COLS * 40 + 300 # 设置底部栏的宽度
self.bottom.place(x=0, y=HEADER_HEIGHT + BOARD_ROWS * 40, anchor=tk.NW) # 底部栏紧挨着地雷区域底部
self.scroll['width'] = int((BOARD_COLS * 40 + 300)/12.3) # 文本框的宽度
self.scroll.place(x=0, y=HEADER_HEIGHT + BOARD_ROWS * 40 + 3, anchor=tk.NW)
def show_all_mines(self, x, y): # 翻开雷游戏结束,并显示所有的雷
global initData, curData, BOARD_ROWS, BOARD_COLS, BUTTONS
if x >= 0 and y >= 0: # 如果是因为超过限时而失败传来的无意义参数-1-1则不处理
BUTTONS[x, y]['relief'] = 'groove' # 按钮变为平面,不再有立体感
BUTTONS[x, y]['image'] = self.p9 # 该按钮显示被左击的爆炸雷
curData[x][y] = 'X' # 更新状态,翻开的爆炸雷
for row in range(BOARD_ROWS): # row为行,0到BOARD_ROWS-1
for col in range(BOARD_COLS): # col为列,0到BOARD_COLS-1 #如果雷没被标记,显示雷。雷被正确标记为旗子,不显示雷,且非爆炸雷
if initData[row][col] == 'M' and curData[row][col] != 'F' and curData[row][col] != 'X':
BUTTONS[row, col]['relief'] = 'groove' # 按钮变为平面,不再有立体感
BUTTONS[row, col]['image'] = self.p13
curData[row][col] = 'M' # 更新状态,未挖开的雷
if initData[row][col] != 'M' and curData[row][col] == 'F': # 不是雷被标记为旗子,显示雷有红叉
BUTTONS[row, col]['relief'] = 'groove' # 按钮变为平面,不再有立体感
BUTTONS[row, col]['image'] = self.p10
curData[row][col] = initData[row][col] # 更新为正确的状态
def win(self): # 判断是否胜利
global GAME_OVER, TIMER_RUN, MINE_WITH_FLAG, NO_MINE_BUT_FLAG, OPEN_BUTTONS
self.label_mine['text'] = str(mine_number) # 显示剩余地雷数
if (MINE_WITH_FLAG == MINES and NO_MINE_BUT_FLAG == 0) \
or (OPEN_BUTTONS == BOARD_COLS * BOARD_ROWS - MINES and OPEN_BUTTONS != 0):
self.btn['image'] = self.face2_img # 胜利条件是:1.正确标记雷的按钮数=雷的实际数量且不正确标记为0。
GAME_OVER = True # 2.单击打开的非雷按钮数=按钮总数-雷的实际数量。且打开的按钮不能为0防止全部为雷时直接显示胜利
TIMER_RUN = False
self.scroll.insert('end', '在限时内完成游戏,游戏结束,你赢了!\n') # 显示信息
self.scroll.see(END)
def on_left_button_down(self, x, y): # 鼠标左键事件
global curData, SHOW_BOARD_STATE, GAME_OVER, TIMER_RUN
if GAME_OVER or curData[x][y] == 'F': # 游戏结束不再响应、做了雷标记按钮左键无效
return
if not TIMER_RUN: # 游戏未开始,左右键均不能用
self.scroll.insert('end', '游戏未开始,请点击脸图开始游戏之后,再使用左键\n')
self.scroll.see(END) # 消息框自动下拉到最后一行
return
if curData[x][y] == 'E': # 未点开的方块才响应
if self.data.get_around_mine_num(x, y) == 0: # 返回值为0左击了有雷的按钮游戏结束
self.show_all_mines(x, y) # 将所有雷显示出来,同时把点击的雷改成爆炸雷
GAME_OVER = True # 游戏结束标志
TIMER_RUN = False # 计时暂停
self.btn['image'] = self.face3_img # 把笑脸图改成哭脸图
self.scroll.insert('end', '点击到(' + str(x) + ',' + str(y) + ')' + '雷方块,游戏结束,你输了!\n') # 显示游戏结束信息
self.scroll.see(END) # 消息框自动下拉到最后一行
return
for i in range(BOARD_ROWS):
for j in range(BOARD_COLS): # 显示所有被点开且未显示的状态信息
if curData[i][j] != 'E' and curData[i][j] != 'F' and SHOW_BOARD_STATE[i][j] == 0:
self.scroll.insert('end', '(' + str(i) + ',' + str(j) + ')' + '方块被点开,方块的状态是' + str(
curData[i][j]) + '\n') # 显示信息
self.scroll.see(END) # 消息框自动下拉到最后一行
SHOW_BOARD_STATE[i][j] = 1 # 已显示过信息,不重复显示
self.show_game_window() # 点击的不是雷,刷新游戏窗口显示
self.data.remaining_mine_num() # 更新剩余雷数
self.win() # 判断是否胜利,此处依据为非雷的方块是否全部点开
def on_right_button_down(self, event, x, y): # event代表鼠标事件这里默认event.num=3即鼠标右键xy为点击的坐标
global curData, TIMER_RUN, GAME_OVER, BUTTONS
if GAME_OVER or (curData[x][y] != 'E' and curData[x][y] != 'F'): # 该按钮已被打开,已显示其相邻按钮下的地雷数,不能做标记
return # 或者游戏已经结束,不再响应
if not TIMER_RUN: # 游戏未开始,左右键均不能用
self.scroll.insert('end', '游戏未开始,请点击脸图开始游戏之后,再使用右键\n')
self.scroll.see(END) # 消息框自动下拉到最后一行
return
self.data.show_flag(x, y) # 修改被右击的方块状态
if curData[x][y] == 'F': # 根据更新的状态显示信息
self.scroll.insert('end', '(' + str(x) + ',' + str(y) + ')' + '方块被标记为雷。\n') # 在底部消息框中显示信息
self.scroll.see(END)
else:
self.scroll.insert('end', '(' + str(x) + ',' + str(y) + ')' + '方块取消标记为雷。\n') # 在底部消息框中显示信息
self.scroll.see(END)
self.show_game_window() # 刷新游戏窗口显示
self.data.remaining_mine_num() # 更新剩余雷数
self.win() # 判断是否胜利,此处依据为雷的方块是否全部被正确标记且没有错误标记
def game_timer(self): # 控制游戏开始和暂停
global GAME_OVER, TIMER_RUN # TIMER_RUN为计时器是否运行timer为计时器对象
if GAME_OVER: # 如果游戏结束,不再响应
return
def game_start():
global GAME_OVER, TIMER_RUN, timer, CLOCK, TIME_LIMIT # TIMER_RUN为计时器是否运行timer为计时器对象
self.scroll.insert('end', '游戏已经开始,请在规定时间内完成游戏,再次点击脸图可暂停游戏。\n') # 显示游戏开始信息
self.scroll.see(END)
TIMER_RUN = True # 开始计时
while TIMER_RUN and CLOCK <= TIME_LIMIT: # TIMER_RUN=Ture且秒数<1000将一直计算秒数。退出该函数子线程结束计算秒数结束
CLOCK += 1
self.label_clock['text'] = str(CLOCK) # 将秒数显示在label_clock
time.sleep(1) # 休眠1秒。模拟读秒
if CLOCK > TIME_LIMIT: # 如果超时,游戏失败
self.btn['image'] = self.face3_img
self.show_all_mines(-1, -1) # 把所有的地雷都展开,并把打开的雷设为爆炸雷。但因超时失败,只能传递两个非坐标参数做判断
self.scroll.insert('end', '未在限时内完成游戏,游戏结束,你输了!\n') # 显示游戏结束信息
GAME_OVER = True
def game_stop():
global TIMER_RUN # TIMER_RUN为计时器是否运行
TIMER_RUN = False # 暂停计时并把状态设为False
self.scroll.insert('end', '游戏已经暂停,点击脸图重新启动游戏。\n') # 显示游戏暂停信息
self.scroll.see(END)
if not TIMER_RUN:
game_start()
else:
game_stop()
def menu(self):
# 菜单设计
menubar = tk.Menu(self.root) # 创建一个菜单栏,可把它理解成一个容器,在窗口的上方,可放置多个能下拉菜单项
gameMenu = tk.Menu(menubar, tearoff=0) # 创建下拉菜单项tearoff=0表示不能单独呈现
menubar.add_cascade(label='游戏', menu=gameMenu) # 将能下拉菜单项放入menubar并指定其名称为游戏
gameMenu.add_command(label='重玩游戏', command=self.reset)
gameMenu.add_separator() # 添加一条分隔线,上句为能下拉菜单项的第一个子菜单项:重玩
gameMenu.add_command(label='简单等级',
command=lambda row=6, col=6, mine=6, time=600: self.set_game_level(row, col, mine, time))
gameMenu.add_command(label='一般等级',
command=lambda row=12, col=12, mine=32, time=1200: self.set_game_level(row, col, mine, time))
gameMenu.add_command(label='困难等级',
command=lambda row=16, col=16, mine=64, time=1800: self.set_game_level(row, col, mine, time))
gameMenu.add_command(label='噩梦等级',
command=lambda row=20, col=20, mine=96, time=2400: self.set_game_level(row, col, mine,
time))
gameMenu.add_command(label='自定义请看右侧') # 修改以上3条语句可修改每级的行数、列数、地雷数和限时
gameMenu.add_separator() # 添加一条分隔线
gameMenu.add_command(label='退出游戏', command=self.root.quit) # 用tkinter里面自带的quit()函数
helpMenu = tk.Menu(menubar, tearoff=0) # 创建第2个能下拉菜单项,点击后显示下拉菜单,下拉菜单可包括多个子菜单项
menubar.add_cascade(label='帮助', menu=helpMenu) # 将能下拉菜单项放入menubar并指定其名称为帮助
helpMenu.add_command(label='关于', command=self.help) # 能下拉菜单项的第一个子菜单项:关于本游戏
self.root.config(menu=menubar) # 让菜单显示出来
def help(self): # 关于
s = ' 游戏说明:游戏等级共四个,分别是简单、一般、困难和噩梦,默认以噩梦等级进入游戏,玩家点击左上角游戏按钮,可切换等级。\n' \
' 右侧栏是游戏设置栏,用户可在规则内进行自定义游戏设置。规则:参数必须为正整数,且列数必须在[6,40]范围之内,行数必须在[6,20]范围之内,' \
'雷数不能大于行列数的乘积游戏限时不能超过99999s且必须在右上角的规定的限制时间内完成否则游戏失败。\n' \
' 左击方块,是雷游戏结束,显示哭脸,否则显示相邻八块方块的总地雷数,为空表示相邻八块方块无雷。当把所有无雷方块都翻开或者把有雷的方块都标记,则游戏胜利,显示耍酷脸。\n' \
' 右击方块标记红旗表示有雷,再右击取消标记。点击脸图开始游戏,再次点击暂停游戏。\n' \
' 点击自动扫雷,程序可自动完成扫雷。\n\n'
self.scroll.insert('end', s)
self.scroll.see(END)
def thread(self, func): # 开多线程防止自动扫雷程序调用click函数时tk界面卡住不动计时器也无法计时
global GAME_OVER
if GAME_OVER: # 游戏结束不再响应
return
t = threading.Thread(target=func) # 将函数装进线程
t.daemon = True # 守护线程,防止游戏未结束就关闭窗口,导致报错
t.start() # 启动线程
def reset(self): # 重玩游戏函数
global MINES, BOARD_ROWS, BOARD_COLS, GAME_OVER, BUTTONS, CLOCK, TIMER_RUN
GAME_OVER = False # 游戏重新开始
TIMER_RUN = False # 计时器暂停
self.label_mine['text'] = str(MINES) # 初始显示该游戏等级初始的雷数用红旗标记一个雷该值减1
self.btn['image'] = self.face1_img # 初始显示笑脸
CLOCK = 0 # 计时器清零
self.label_clock['text'] = str(CLOCK) # 游戏重新开始计时器清0
self.label_time['text'] = str(TIME_LIMIT) # 右上角显示游戏限时
self.data.init_mine_map(MINES) # 初始化initData并在列表中随机增加地雷
self.data.init_board_state() # 初始化curData、SHOW_BOARD_STATE
self.show_game_window() # 按照初始curData状态信息显示游戏窗口
self.scroll.delete('1.0', 'end') # 清空上局游戏消息记录
self.scroll.insert('end', '本局游戏参数为(' + str(BOARD_ROWS) + ',' + str(BOARD_COLS) + ',' + str(MINES) + ',' + str(
TIME_LIMIT) + ')\n') # 在底部消息框显示游戏参数
self.help() # 开局就将游戏说明显示在消息框中
def set_game_level(self, row, col, mine, time): # 根据参数设置游戏难度
global MINES, BOARD_ROWS, BOARD_COLS, BUTTONS, TIME_LIMIT, HEADER_WIDTH, face_x, clock_x, BOTTOM_HEIGHT
if MINES == mine and BOARD_COLS == col and BOARD_ROWS == row and TIME_LIMIT == time:
self.reset() # 如果新旧行列数、雷数、时间相同,即为重玩当前等级的游戏,无需删除方块重建
return
if len(BUTTONS) != 0: # 如果新旧数值不同,则需要把旧的方块全删除,重新建立雷图
for r in range(BOARD_ROWS):
for c in range(BOARD_COLS):
BUTTONS[r, c].destroy() # 删除旧方块
MINES = mine # 更新的行列数、雷数、时间
BOARD_ROWS = row
BOARD_COLS = col
TIME_LIMIT = time
HEADER_WIDTH = BOARD_COLS * 40 # 更新头部栏和右侧栏宽度,脸图以及计时器位置
face_x = HEADER_WIDTH / 2
clock_x = HEADER_WIDTH - 40
self.create_boards() # 创建新按钮方块并绑定左右键函数
self.root.geometry(f'{BOARD_COLS * 40 + RIGHT_WIDTH}x{(BOARD_ROWS + 1) * 40 + BOTTOM_HEIGHT}') # 加上右侧栏与底部栏区域
self.header_frame() # 更新头部栏属性信息
self.right_frame() # 更新右侧栏属性信息
self.bottom_frame() # 更新底部栏属性信息
self.menu() # 菜单栏
self.reset() # 依照数据初始化
def get_data(self): # 获取用户在文本框中填写的数据,只能填正整数!
if self.entry1.get() == '' or self.entry2.get() == '' or self.entry3.get() == '' or self.entry4.get() == '':
self.scroll.insert('end', '参数不能为空,请重新设置参数\n') # 有空参数,弹出提示
self.scroll.see(END) # 自动下拉到最后一行
return
dataString = self.entry1.get() + self.entry2.get() + self.entry3.get() + self.entry4.get() # 用户参数连接字符串,含有非0-9的任何字符都不行
for i in range(len(dataString)):
if dataString[i] >= '0' and dataString[i] <= '9': # 是整数则继续判断,不是整数则跳出提示
continue
else:
self.scroll.insert('end', '参数只能为正整数,请重新设置参数\n') # 非整数参数,显示提示
self.scroll.see(END) # 自动下拉到最后一行
return
row = int(self.entry1.get()) # 用get方法取得用户填写的正整数参数
col = int(self.entry2.get())
mine = int(self.entry3.get())
time = int(self.entry4.get())
if row < 6 or col < 6 or row > 20 or col > 40:
self.scroll.insert('end', '行数范围是[6,20],列数的范围是[6,40],请重新设置参数\n') # 防止雷图区域过大,超出屏幕
self.scroll.see(END) # 自动下拉到最后一行
return # 最简单的等级行列数为6不能比6小。数值不能太大导致区域超出屏幕
if mine > row * col:
self.scroll.insert('end', '雷数不能多于行列数乘积,请重新设置参数\n')
self.scroll.see(END)
return # 雷数不能比总方块数量还多
# if row * col > 998 and mine < row * col / 5: # 998为Python最大递归深度超过998时雷数不能太少防止超过最大递归深度报错
# self.scroll.insert('end', '当行列数乘积大于998时雷数不能少于行列数乘积的五分之一请重新设置参数\n')
# self.scroll.see(END)
# return
if time > 99999: # 时间阈值限制
self.scroll.insert('end', '最高限时99999s请重新设置参数\n')
self.scroll.see(END)
return
self.set_game_level(row, col, mine, time) # 调用函数按照用户提交的参数重新启动游戏
class Data: # 数据类
def init_mine_map(self, mines): # 初始化布雷方案
global BOARD_ROWS, BOARD_COLS, initData # 全局变量行数,列数,布雷方案
initData = [[0 for i in range(BOARD_COLS)] for j in range(BOARD_ROWS)] # 无雷初始化,先列后行
for i in random.sample(range(BOARD_COLS * BOARD_ROWS), mines):
initData[i // BOARD_COLS][i % BOARD_COLS] = 'M' # 在BOARD_COLS*BOARD_ROWS范围中随机生成mines个雷
# 雷行下标为随机数除以列数取整,雷列下标为随机数对列数取模
return initData
def init_board_state(self): # 初始化方块状态
global curData, SHOW_BOARD_STATE, BOARD_ROWS, BOARD_COLS
curData = [['E' for i in range(BOARD_COLS)] for j in range(BOARD_ROWS)] # 立体方块
SHOW_BOARD_STATE = [[0 for i in range(BOARD_COLS)] for j in range(BOARD_ROWS)] # 显示状态
def get_around_xy(self, x, y): # 返回对应坐标周围的坐标列表
global BOARD_ROWS, BOARD_COLS
return [(i, j) for i in range(max(0, x - 1), min(BOARD_ROWS - 1, x + 1) + 1) # 行号最小为0不为负,最大BOARD_ROWS-1
for j in range(max(0, y - 1), min(BOARD_COLS - 1, y + 1) + 1) if i != x or j != y] # 不包括自己即x行y列方块
def get_around_mine_num(self, x, y): # 递归获取周围雷数
global curData, initData # 雷的状态,布雷方案,是否计算过
if initData[x][y] == 'M': # 挖开的是雷,游戏结束
curData[x][y] = 'X' # 更新状态为翻开的雷
return 0 # 返回结果,是雷
around_xy = self.get_around_xy(x, y) # 周围按钮的坐标
num = self.num_of_mine(x, y) # 记录周围的总雷数
if num == 0: # 如果雷数为0更新状态为B且递归调用函数进行雷数计算
curData[x][y] = 'B'
for i, j in around_xy:
if curData[i][j] == 'E': # 把周围未打开的方块都检查一遍
self.get_around_mine_num(i, j)
return 1 # 返回结果不是雷,且方块已经打开,显示结果
def show_flag(self, x, y): # 标志旗子
global curData
if curData[x][y] == 'E': # 如果按钮未打开,且未标记为旗子则显示旗子标志
curData[x][y] = 'F'
elif curData[x][y] == 'F': # 如果按钮未打开,且已经标记为旗子,则取消显示
curData[x][y] = 'E'
else:
return # 如果按钮已经打开,则不做任何操作
def remaining_mine_num(self): # 得到剩余雷数的同时,判断是否胜利
global MINES, BOARD_ROWS, BOARD_COLS, GAME_OVER, TIMER_RUN, initData, curData, mine_number, \
MINE_WITH_FLAG, NO_MINE_BUT_FLAG, OPEN_BUTTONS
MINE_WITH_FLAG = 0 # 所有按钮下有地雷被标旗子的总数
NO_MINE_BUT_FLAG = 0 # 所有按钮下无地雷被标旗子的总数
OPEN_BUTTONS = 0 # 所有被打开的按钮的总数,等于所有按钮数-所有雷数
for i in range(BOARD_ROWS): # i为行,0到BOARD_ROWS-1
for j in range(BOARD_COLS): # j为列,0到BOARD_COLS-1
if initData[i][j] == 'M' and curData[i][j] == 'F': # 如果该按钮下地雷被标旗子
MINE_WITH_FLAG += 1
elif initData[i][j] != 'M' and curData[i][j] == 'F': # 如果该按钮下无地雷被标旗子
NO_MINE_BUT_FLAG += 1
elif initData[i][j] != 'M' and curData[i][j] != 'E': # 如果该按钮不是雷且已被打开
OPEN_BUTTONS += 1
mine_number = MINES - (MINE_WITH_FLAG + NO_MINE_BUT_FLAG) # mine_number为剩余的地雷数
if mine_number < 0: # 如无雷也被标记红旗,可能出现标记红旗的按钮数大于地雷数,雷数不能为负
mine_number = 0 # 标记为旗子的所有块>实际雷数仍显示0个雷
return mine_number
def num_of_mine(self, x, y): # 获取(x,y)处周围的雷数
global initData, curData
minenum = 0 # 保存雷数
if initData[x][y] != 'M': # 如果不是雷
for i, j in self.get_around_xy(x, y): # 遍历周围的方块
if initData[i][j] == 'M':
minenum += 1 # 是雷则雷数加1
curData[x][y] = minenum # 更新改方块的状态
return minenum
def auto_mine_sweeper(self): # 自动扫雷
global BOARD_ROWS, BOARD_COLS, BUTTONS, curData, TIMER_RUN, GAME_OVER # 全局变量
if GAME_OVER: # 游戏结束,不再响应
return
# 根据pyautogui.position()方法确定窗口在屏幕的(0,0)位置,即左上角时的大致位置
face_p_x = int(BOARD_COLS * 40 / 2) # 脸图的位置在头部栏中间
face_p_y = 80 # 脸图y坐标固定为65在[70,90]内均可
first_block_x = 30 # 第一个方块的位置x在[20,40]内均可
first_block_y = 120 # 第一个方块的位置y在[110,130]内均可
window = pyautogui.getWindowsWithTitle('扫雷')[0] # 获取窗口句柄
x0 = window.left # 距离左侧屏幕的偏移量
y0 = window.top # 距离顶部屏幕的偏移量
if TIMER_RUN: # 如果游戏已经开始,先暂停,确保在游戏进行途中也能使用自动扫雷
TIMER_RUN = False
time.sleep(1) # 沉睡一秒,与计时器进程保持时间一致
m = PyMouse() # 调用鼠标对象
m.click(face_p_x + x0, face_p_y + y0, 1) # click(x,y,左键=1/右键=2,点击次数)模拟左键点击脸图开始游戏
time.sleep(0.2) # 缓冲时间,防止鼠标点击太快,程序未及时响应(确保能点击脸图开始游戏)
for row in range(BOARD_ROWS):
for col in range(BOARD_COLS): # x对应列y对应行
if curData[row][col] == 'E': # 如果不为E证明被打开了则跳过
if self.get_around_mine_num(row, col) == 0: # 调用get_around_mine_num函数计算周围雷数
curData[row][col] = 'F' # 标记旗子
else: # 如果不为雷且已经通过get_around_mine_num更新状态则直接点击翻开
curData[row][col] = 'E' # 确保当前未打开的非雷方块能被点开
m.click(first_block_x + x0 + 40 * col, first_block_y + y0 + 40 * row, 1) # 左键单击按钮
time.sleep(0.1) # 让鼠标慢一点
if GAME_OVER: # 游戏结束,不再响应,鼠标停止自动点击
break
if __name__ == '__main__':
# 创建test对象
test = Show() # 初始化游戏界面
test.set_game_level(20, 20, 96, 2400) # 调用函数完成游戏初始化,并进入游戏
test.root.mainloop() # 显示UI