upload Minesweeper Project

master
bettleChen 1 year ago
parent 773ca51f0c
commit 0841e9fd71

@ -1,2 +1,195 @@
# Minesweeper
# 项目编程基本原则
## 文档及代码编辑的注意事项
### 进度汇报
**查文档和编程任务是否符合。**
首先自查一遍文档,每个编程任务是否符合文档上编程任务的要求---请在进度表中标示【自查:符合 或 不符合】是按编程任务不是按X1,X2...
**查命名问题,函数框架,函数名称,和变量名规范**
(2)按我们前两天讨论的,主函数框架、函数关系调用图、函数名称与变量名称规范化等方面回复【已优化完成,未优化完成】
### 主函数框架示例:
```python
# 需要导入的库
from tkinter import scrolledtext # 下拉框
from tkinter import END # 下拉框自动下拉到最后一行
from pymouse import PyMouse # 模拟鼠标自动点击
import tkinter as tk # UI界面
import threading # 多线程任务
import win32gui # 获取扫雷窗口信息
import random # 随机数
import time # 应用sleep函数睡眠
"""按照上宽下窄的方式排列引用"""
"""
所有的系统库或第三方库,一次性引出,避免因为确少库而导致运行不了。
如果是自写的模块,可以在函数前引出
"""
#方式1--在文件中定义全局变量,加载全局变量定义文件(如果全局变量实在太多)
from setting import * # setting为自定义程序存放的是全局变量导入全局变量
# setting中定义了哪些全局变量及其定义
#方式2--直接定义全局常量和变量(一般多)
全局常量
GLOBAL ... ... # 常量用大写,大写字母下划线链接
全局变量
Init_Matrix ... ... # 全局变量首字母大写
"""按照上宽下窄的方式排列引用"""
注释标准:
就近对齐规则
print("数据集中图片的个数:", len(data_list)) # 打印图片数据集中图片的个数
print("第一个图片数组的形状:", data_list[0].shape) # 打印第一个图片数组的形状
print("第一个图片数组的内容:\n", data_list[0]) # 打印第一个图片数组的内容
return data_list # 返回加载成功的图像数据列表
###########
'此处说明是放函数定义的地方'
###########
# 主控函数, 将函数定义拷贝到main函数上面将函数调用放在main函数
if __name__ == '__main__':
pass
###########
'此处说明是函数调用的地方'
###########
"""
要求主函数框架单独运行不报错
"""
```
```python
# 文档贴函数提示说明 如下图所示
###########################################################################
# 以func1()[编程章节]==>func2()[编程章节]的方式说明调用当前函数要用的前述函数。
set_random_state()[5.1]==>create_boards()[5.4]==>show_window()[5.4]
```
![img_2.png](img_2.png)
### 根据不同情况不同的贴程序方法
```python
# 情况一
##### 第一次出现init初始化方法需要写全 #####
##### 在类里面增添方法,需要将类名写出,再写方法 #####
X2:
class MineSweeper: # 建立扫雷类
def __init__(self): # 第一次出现init初始化方法需要写全
def f2():
pass
# 情况二
##### 如果函数实在太多,需要标出函数调用关系图,同时将贴合编程要求的程序给出 #####
X3: # f1(), f2(), f3(), f4(), ..., f10() 本段仅给出f4(), 完整的在[位置](可以在文档末尾建立附录)。
Class Class_some
...
def f4(): 体现核心功能的函数给出。
pass
# 情况三
##### 如果需要在后续编程章节中扩充原有的功能,可以扩写函数,#####
##### 而且不要出现已经出现过的代码,已经出现过的代码用...代指 #####
X4: 【友好阅读性原则;最小重复代码原则;递增式代码表达原则;界面元素处理与业务逻辑完全分开原则】
class MineSweeper: # 扩展扫雷类--在X2基础上进一步定义
# _init_(self), f2(), 一并处理
def _init_(self): #在原函数基础上增加/调整
...
self.par = "example"
def f3():
...
def f4():
...
每个函数不超一页,原则上就是半页。
```
## 项目编程的普遍要求
### GUI界面和数据结构分开
借用前后端分离的架构模式。
GUI界面负责处理用户界面的展示和交互数据结构部分负责处理业务逻辑和数据处理。
将GUI界面负和数据结构分为两个独立的部分各自负责不同的功能和任务。GUI界面通过接口函数与更新和加载数据结构。
### 代码命名规则:
在Python中有一些命名规则和约定被广泛遵循以提高代码的可读性和一致性。
全局变量:首字母大写单词之间用下划线分隔。
变量和函数名使用小写字母单词之间使用下划线snake_case进行分隔。例如my_variable, calculate_result。
常量使用全大写字母单词之间使用下划线进行分隔。例如MAX_VALUE, PI。
类名使用驼峰命名法CamelCase即首字母大写不使用下划线。例如MyClass, Calculator。
模块名使用小写字母单词之间使用下划线进行分隔。例如my_module, utils。
- 首先是做好程序的优化,即做好程序函数的封装。 每个函数的函数名尽可能符合文档的编程任务及其要求--要让读者看到函数名就能想到编程任务,这样会更易于理解。
- 函数调用关系图的理解是正确的,就是要有一张全局关系图。
- 正确理解X1、X2、X3、X4和X5。通常X1仅是围绕数据结构赋值和界面或者界面元素坐标无关。X2是依据数据结构中的数据做输出通常是仅输出。X3通常可以定义函数或者定义类及其中的函数。X4是在X2基础上做界面交互元素同时将界面交互元素相关的事件/消息绑定X3或X5的函数。X5也是一些函数。这样界面逻辑和业务逻辑是完全分离的。
- 注意自前向后编程是越来越复杂但不要用后面的编程直接替代前面的编程任务中的程序前面是简单的后面是复杂的。例如“Select Sno from Student”,这是前面的编程Select :attr1 from :table1"尽管将attr1赋值成Sno将table1赋值成Student,也能实现前面的语句,但不可用后面的替换前面的。
### 函数调用关系图:
函数调用图Function Call Graph是用于描述程序中函数之间调用关系的图形表示。它展示了函数之间的依赖关系和调用流程帮助我们理解程序的执行流程和函数之间的交互
在函数调用图中,函数被表示为节点,函数之间的调用关系被表示为边。每个函数调用都会生成一个新的节点和一条连接调用者和被调用者的边。函数调用图可以是有向图或无向图,具体取决于函数调用的方向性。
调用图可以用软件生成。
![img.png](img.png)
### 项目文件应包含(后续规范)
项目应该包含以下基本文件:
1. README.md项目的说明文档包含项目的介绍、使用方法、安装指南、贡献指南等重要信息。README.md通常是其他开发者了解和使用项目的入口。
2. LICENSE项目的开源许可证明确了项目的使用条件和权利限制。选择适合项目的开源许可证对于保护项目的权益和推动开源合作非常重要。
3. .gitignoreGit版本控制系统的忽略文件配置用于指定哪些文件或目录应该被忽略不纳入版本控制。通常包括一些编译生成的文件、临时文件、敏感信息等。
4. requirements.txt项目的依赖项清单列出了项目所需的外部库、框架和工具的版本信息。这样其他开发者可以方便地安装相同的依赖项确保项目的可重复性和一致性。
5. setup.py 或者 setup.cfg用于打包和发布项目的配置文件。可以定义项目的元数据、依赖关系、安装过程等以便其他人能够方便地安装和使用项目。
6. docs 目录包含项目的文档例如用户手册、API文档、开发者指南等。良好的文档对于其他开发者和用户理解和使用项目非常重要。
7. tests 目录:包含项目的测试代码和测试数据,用于验证项目的正确性和稳定性。包括单元测试、集成测试等,帮助开发者确保项目的质量和可靠性。
8. src 或者 lib 目录:包含项目的源代码文件。根据项目的规模和结构,可以进一步组织成子目录或包,方便代码的组织和维护。
除了上述基本文件,根据项目的特点和需求,还可以包含其他文件,如配置文件、示例代码、演示视频等。重要的是根据项目的具体情况进行文件的组织和描述,确保项目的可理解性、可维护性和可扩展性。
```bash
# 将输出重定向到文件:
pip freeze > requirements.txt
# 老师一键安装项目所有依赖一般像Pycharm会自动识别这个文件按提示安装即可
pip install -r requirements.txt
```

104
X1.py

@ -0,0 +1,104 @@
# _*_ coding:utf-8 _*_
# @Time2023/6/29 9:37
# @File:X1.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 # 所有被打开的按钮的总数,等于所有按钮数-所有雷数
def init_mine_map(mines):
global BOARD_ROWS, BOARD_COLS, initData, curData # 全局变量行数,列数,布雷方案
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个雷
# 雷行下标为随机数除以列数取整,雷列下标为随机数对列数取模
curData = initData
return curData, initData
def set_random_state(): # 随机设置状态
global curData, BOARD_COLS, BOARD_ROWS
randomList = ['E', 'M', 'B', 'X', 'F', 1, 2, 3, 4, 5, 6, 7, 8] # 随机状态数列
curData = [[0 for j in range(BOARD_COLS)] for i in range(BOARD_ROWS)] # 矩阵初始化
for r in range(BOARD_ROWS):
for c in range(BOARD_COLS):
r_index = random.randint(0, len(randomList) - 1) # 获取随机下标
curData[r][c] = randomList[r_index] # 随机赋予状态
return curData
def around(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_mine_num_data(): # BOARD_ROWS行数,BOARD_COLS列数
global initData, curData, BOARD_COLS, BOARD_ROWS
mineNumData = initData # 拷贝布雷方案
for i in range(BOARD_ROWS):
for j in range(BOARD_COLS):
if initData[i][j] == 'M': # 如果为雷,点击则游戏结束,不计算周围的雷数
continue
else: # 如果不为雷则把这个位置值改成周围的雷数0-8
aroundxy = around(i, j)
minesum = 0
for r, c in aroundxy:
if initData[r][c] == 'M':
minesum += 1
mineNumData[i][j] = minesum # 把值改成周围雷的数量
curData = mineNumData
return curData, mineNumData
if __name__ == '__main__':
init_mine_map(MINES) # 初始化initData赋值给curData并输出
for i in range(BOARD_ROWS):
print(curData[i])
print('\n')
set_random_state() # 随机设置curData并输出
for i in range(BOARD_ROWS):
print(curData[i])
print('\n')
get_mine_num_data() # 获取mineNumData赋值给curData并输出需要调用around函数
for i in range(BOARD_ROWS):
print(curData[i])
print('\n')

143
X2.py

@ -0,0 +1,143 @@
# _*_ coding:utf-8 _*_
# @Time2023/6/29 9:37
# @File:X2.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 # 所有被打开的按钮的总数,等于所有按钮数-所有雷数
def set_random_state(): # 设置随机状态curData
global curData
randomList = ['E', 'M', 'B', 'X', 'F', 1, 2, 3, 4, 5, 6, 7, 8] # 随机状态数列
curData = [[0 for j in range(BOARD_COLS)] for i in range(BOARD_ROWS)] # 矩阵初始化
for r in range(BOARD_ROWS):
for c in range(BOARD_COLS):
r_index = random.randint(0, len(randomList) - 1) # 获取随机下标
curData[r][c] = randomList[r_index] # 随机赋予状态
return curData
class Show: # 显示类
def __init__(self):
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}') # f'{}x{}',该格式{}里面是表达式,方便改变窗口大小
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.btn = tk.Button(self.top, bg='white', height=FACE_HEIGHT, width=FACE_WIDTH, relief='raised') # 脸图按钮
# 加载图片资源,图片格式必须是'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): # 初始化BOARD_ROWS*BOARD_COLS个button对象
global 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
button = tk.Button(self.root) # 创建button对象
button.place(y=(row + 1) * 40, x=col * 40)
BUTTONS[row, col] = button # 以坐标为键,按钮对象为值,使其唯一确定
def show_game_window(self): # 游戏方块窗口显示
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
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)
if __name__ == '__main__':
set_random_state()
test = Show()
test.create_boards()
test.show_game_window()
test.header_frame()
test.root.mainloop()

156
X3.py

@ -0,0 +1,156 @@
# _*_ coding:utf-8 _*_
# @Time2023/6/29 9:37
# @File:X3.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 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 game_timer(self): # 计时器
global GAME_OVER, TIMER_RUN # TIMER_RUN为计时器是否运行
if GAME_OVER: # 如果游戏结束,不再响应
return
def count(): # 该函数完成读秒功能,将运行在子线程中
global TIMER_RUN, CLOCK, GAME_OVER, TIME_LIMIT
while TIMER_RUN and CLOCK <= TIME_LIMIT: # 开始计时条件
CLOCK += 1
print(CLOCK) # 测试代码
time.sleep(1) # 休眠1秒。模拟读秒
if CLOCK > TIME_LIMIT: # 如果超时,游戏失败
GAME_OVER = True
if not TIMER_RUN: # 子线程状态为未启动则启动子进程并把状态设为True
TIMER_RUN = True
count() # 开始计时
elif TIMER_RUN: # 子线程状态为启动则暂停子进程并把状态设为False
TIMER_RUN = False # 暂停计时
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
if __name__ == '__main__':
test = Data()
test.init_mine_map(MINES) # 初始化initData
test.init_board_state() # 初始化curData、SHOW_BOARD_STATE
for i in range(BOARD_ROWS): # 输出initData
print(initData[i])
print('\n')
test.get_around_mine_num(0, 0)
for i in range(BOARD_ROWS): # 获取(0,0)周围雷数,并输出查看结果
print(curData[i])
print('\n')
test.show_flag(5, 5)
for i in range(BOARD_ROWS): # 标记(5,5)为雷,并输出查看结果
print(curData[i])
print('\n')
print(test.remaining_mine_num(), '\n') # 输出剩余雷数
print(test.num_of_mine(1, 1), '\n') # 获取(1,1)周围雷数并输出。为0表示(1,1)为雷
test.game_timer() # 输出显示计时器效果

487
X4.py

@ -0,0 +1,487 @@
# _*_ coding:utf-8 _*_
# @Time2023/6/29 9:37
# @File:X4.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): # 自动扫雷
pass
if __name__ == '__main__':
# 创建test对象
test = Show() # 初始化游戏界面
test.set_game_level(20, 20, 96, 2400) # 调用函数完成游戏初始化,并进入游戏
test.root.mainloop() # 显示UI

514
X5.py

@ -0,0 +1,514 @@
# _*_ coding:utf-8 _*_
# @Time2023/6/29 9:40
# @File:X5.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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

@ -0,0 +1,514 @@
# _*_ 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

@ -0,0 +1,23 @@
filelock==3.12.0
Jinja2==3.1.2
MarkupSafe==2.1.2
MouseInfo==0.1.3
mpmath==1.3.0
networkx==3.1
pyaes==1.6.1
PyAutoGUI==0.9.53
pycallgraph==1.0.1
PyGetWindow==0.0.9
PyMouse==1.0
PyMsgBox==1.0.9
pyperclip==1.8.2
PyRect==0.2.0
Pyrogram==2.0.106
PyScreeze==0.1.28
PySocks==1.7.1
pytweening==1.0.7
PyUserInput==0.1.10
pywin32==306
sympy==1.12
TgCrypto==1.2.5
typing_extensions==4.5.0

@ -0,0 +1,536 @@
# _*_ coding:utf-8 _*_
# @Time2023/7/10 8:30
# @File:main.py
# @SoftWare:PyCharm
# @Project:MineSweeper
# @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}+100+100')
# 头部栏设计
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)
# 鼠标左键绑定函数on_left_button_down(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): # 游戏方块窗口显示
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
# 坐标轴中的x为列y为行所以这里x对应coly对应row
if 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
elif curData[row][col] == 'F':
BUTTONS[row, col]['image'] = self.p11
elif curData[row][col] == 'E':
BUTTONS[row, col]['image'] = self.p12
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)右侧栏高度大于180时作出对应的变化每行的高度为30总高度BOARD_ROWS * 30
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.num=3即鼠标右键
global curData, TIMER_RUN, GAME_OVER
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, 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
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:
curData[x][y] = 'B' # 如果为0更新状态为B表示周围雷数为0方块
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 # 距离左侧屏幕的偏移量
x0 = 100 # 距离左侧屏幕的偏移量
# y0 = window.top # 距离顶部屏幕的偏移量
y0 = 100 # 距离顶部屏幕的偏移量
print(x0, y0)
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

@ -0,0 +1,527 @@
from tkinter import scrolledtext, END # 滚动框, 滚动框自动下拉到最后一行
# from pymouse import PyMouse # 模拟鼠标自动点击
import tkinter as tk # UI界面
import threading # 多线程任
import pyautogui # 获取窗口位置信息
import random # 随机数
import time
# 全局变量
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 = 160 # 底部栏高度,固定不变
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=6,
font=('楷体', 14))
# 用户自定义设置行值大小
# tk.StringVar()表示输入框中输入的类型是字符串。highlightcolor:输入控件获得输入焦点时的边框颜色。highlightthickness:输入控件的边框宽度
self.label_row = tk.Label(self.right, text='BOARD_ROW:', height=1, width=14, 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_COL:', height=1, width=14, 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=14, 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=14, 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)
# 鼠标左键绑定函数on_left_button_down(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): # 游戏方块窗口显示
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
# 坐标轴中的x为列y为行所以这里x对应coly对应row
if 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
elif curData[row][col] == 'F':
BUTTONS[row, col]['image'] = self.p11
elif curData[row][col] == 'E':
BUTTONS[row, col]['image'] = self.p12
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)右侧栏高度大于180时作出对应的变化每行的高度为30总高度BOARD_ROWS * 30
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) / 11.2) # 文本框的宽度
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=128, 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 = 120 # 脸图y坐标固定为65在[70,90]内均可
first_block_x = 30 # 第一个方块的位置x在[20,40]内均可
first_block_y = 150 # 第一个方块的位置y在[110,130]内均可
x0 = 0 # 距离左侧屏幕的偏移量
y0 = 0 # 距离顶部屏幕的偏移量
if TIMER_RUN: # 如果游戏已经开始,先暂停,确保在游戏进行途中也能使用自动扫雷
TIMER_RUN = False
time.sleep(1) # 沉睡一秒,与计时器进程保持时间一致
pyautogui.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' # 确保当前未打开的非雷方块能被点开
pyautogui.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(12, 24, 72, 600) # 调用函数完成游戏初始化,并进入游戏
test.root.mainloop() # 显示UI
Loading…
Cancel
Save