|
|
# _*_ coding:utf-8 _*_
|
|
|
# @Time:2023/6/29 8:30
|
|
|
# @File:main.py
|
|
|
# @SoftWare:PyCharm
|
|
|
# @Project:MS
|
|
|
# @author:yzf
|
|
|
from tkinter import scrolledtext, END, messagebox # 滚动框, 滚动框自动下拉到最后一行
|
|
|
from tkinter import *
|
|
|
from pymouse import PyMouse # 模拟鼠标自动点击
|
|
|
from PIL import Image, ImageTk
|
|
|
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 = 46 # 总的雷数
|
|
|
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 * 30 # 头部栏的大小
|
|
|
HEADER_HEIGHT = 40
|
|
|
RIGHT_WIDTH = 300 # 右侧栏宽度,固定不变
|
|
|
BOTTOM_HEIGHT = 200 # 底部栏高度,固定不变
|
|
|
face_x = 20 * 30 / 2 # 脸图的位置
|
|
|
face_y = 20
|
|
|
clock_x = 20 * 40 - 40 # 计时器的位置
|
|
|
clock_y = 20
|
|
|
MINE_WITH_FLAG = 0 # 是地雷被标旗子的按钮总数
|
|
|
NO_MINE_BUT_FLAG = 0 # 不是地雷但是被标旗子的按钮总数
|
|
|
OPEN_BUTTONS = 0 # 所有被打开的按钮的总数,等于所有按钮数-所有雷数
|
|
|
Distance_X = 0
|
|
|
Distance_Y = 0
|
|
|
CUSTOM = 0
|
|
|
|
|
|
|
|
|
def to_image(path, resize=None) -> ImageTk.PhotoImage:
|
|
|
if resize:
|
|
|
return ImageTk.PhotoImage(Image.open(path).resize(resize))
|
|
|
return ImageTk.PhotoImage(Image.open(path))
|
|
|
|
|
|
|
|
|
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('1300x1150')
|
|
|
# self.root.configure(bg='#E3EBEF')
|
|
|
# 全局图片资源,图片格式必须是'xx.gif'
|
|
|
self.face1_img = self.element('/home/headless/Desktop/workspace/myshixun/src/img/face1.png') # 笑脸
|
|
|
self.face2_img = self.element('/home/headless/Desktop/workspace/myshixun/src/img/face2.png') # 耍酷脸
|
|
|
self.face3_img = self.element('/home/headless/Desktop/workspace/myshixun/src/img/face3.png') # 哭脸
|
|
|
self.p0 = self.element('/home/headless/Desktop/workspace/myshixun/src/img/12.png', 43, 43) # 空白方块
|
|
|
self.p1 = self.element('/home/headless/Desktop/workspace/myshixun/src/img/1.png', 43, 43) # 数字1
|
|
|
self.p2 = self.element('/home/headless/Desktop/workspace/myshixun/src/img/2.png', 43, 43) # 数字2
|
|
|
self.p3 = self.element('/home/headless/Desktop/workspace/myshixun/src/img/3.png', 43, 43) # 数字3
|
|
|
self.p4 = self.element('/home/headless/Desktop/workspace/myshixun/src/img/4.png', 43, 43) # 数字4
|
|
|
self.p5 = self.element('/home/headless/Desktop/workspace/myshixun/src/img/5.png', 43, 43) # 数字5
|
|
|
self.p6 = self.element('/home/headless/Desktop/workspace/myshixun/src/img/6.png', 43, 43) # 数字6
|
|
|
self.p7 = self.element('/home/headless/Desktop/workspace/myshixun/src/img/7.png', 43, 43) # 数字7
|
|
|
self.p8 = self.element('/home/headless/Desktop/workspace/myshixun/src/img/8.png', 43, 43) # 数字8
|
|
|
self.p9 = self.element('/home/headless/Desktop/workspace/myshixun/src/img/9.png', 43, 43) # 爆炸雷
|
|
|
self.p10 = self.element('/home/headless/Desktop/workspace/myshixun/src/img/10.png', 43, 43) # 标错雷
|
|
|
self.p11 = self.element('/home/headless/Desktop/workspace/myshixun/src/img/11.png', 43, 43) # 旗子
|
|
|
self.p12 = self.element('/home/headless/Desktop/workspace/myshixun/src/img/0.png', 43, 43) # 立体方块
|
|
|
self.p13 = self.element('/home/headless/Desktop/workspace/myshixun/src/img/13.png', 43, 43) # 未爆炸雷
|
|
|
self.zidong = self.element('/home/headless/Desktop/workspace/myshixun/src/img/zidong.png', 100, 50) # 自动挖雷
|
|
|
self.tijiao_image = self.element('/home/headless/Desktop/workspace/myshixun/src/img/tijiao.png', 100, 50) # 提交
|
|
|
self.dilei = self.element('/home/headless/Desktop/workspace/myshixun/src/img/dilei.png') # 上方地雷
|
|
|
self.shijian = self.element('/home/headless/Desktop/workspace/myshixun/src/img/shijian.png') # 时间
|
|
|
self.rightbg = self.element('/home/headless/Desktop/workspace/myshixun/src/img/bg_1.png', 400, 1250) # 右侧背景
|
|
|
self.bg = self.element('/home/headless/Desktop/workspace/myshixun/src/img/bg.png', 1400, 1250) # 背景
|
|
|
self.lay_mines = self.element('/home/headless/Desktop/workspace/myshixun/src/img/自定义布雷.png', 150, 50) # 背景
|
|
|
self.random_mines = self.element('/home/headless/Desktop/workspace/myshixun/src/img/随机布雷.png', 150, 50) # 背景
|
|
|
self.aut_mines = self.element('/home/headless/Desktop/workspace/myshixun/src/img/自动挖雷.png', 150, 50) # 背景
|
|
|
|
|
|
# 创建一个Canvas部件并设置背景图片
|
|
|
self.canvas = tk.Canvas(self.root, width=1400, height=1250)
|
|
|
self.canvas.pack(fill="both", expand=True)
|
|
|
self.canvas.create_image(0, 0, image=self.bg, anchor="nw")
|
|
|
|
|
|
# 头部栏设计
|
|
|
self.top = tk.Frame(self.root, bg='#E3EBEF', relief="sunken", width=900, height=50)
|
|
|
self.top.place(x=0, y=0)
|
|
|
# 显示雷数
|
|
|
self.label_mine = tk.Label(self.top, text=str(MINES), height=50, width=70, image=self.dilei, compound=tk.LEFT,
|
|
|
padx=10, bg='#E3EBEF',
|
|
|
fg='red', font=('Montserrat', 13))
|
|
|
self.label_mine.place(x=20, y=0)
|
|
|
# 显示扫雷用时
|
|
|
self.label_clock = tk.Label(self.top, text=str(CLOCK), height=50, width=100, image=self.shijian,
|
|
|
compound=tk.LEFT, padx=10, bg='#E3EBEF',
|
|
|
fg='black', font=('Montserrat', 13))
|
|
|
self.label_clock.place(x=510, y=0)
|
|
|
# 游戏设置
|
|
|
self.label_set = tk.Label(self.top, text='限时', bg='#E3EBEF', fg='black',
|
|
|
font=('Montserrat', 13))
|
|
|
self.label_set.place(x=740, y=10)
|
|
|
# 显示限时时间是多少
|
|
|
self.label_time = tk.Label(self.top, text=str(TIME_LIMIT), bg='#E3EBEF', fg='red',
|
|
|
font=('Montserrat', 13))
|
|
|
self.label_time.place(x=810, y=10)
|
|
|
|
|
|
self.btn = tk.Button(self.top, bg='#E3EBEF', height=50, width=50,
|
|
|
command=lambda: self.thread(self.game_timer), relief="flat")
|
|
|
self.btn.place(x=410, y=0)
|
|
|
|
|
|
# 右侧栏
|
|
|
self.right = tk.Frame(self.root, bg='white', relief='sunken', width=400, height=1250)
|
|
|
# self.right = Canvas(self.root, width=500, height=1000)
|
|
|
self.right.place(x=900, y=0)
|
|
|
# 创建一个Label部件并设置背景图片
|
|
|
# 创建一个Canvas部件并设置背景图片
|
|
|
self.canvas_right = tk.Canvas(self.right, width=400, height=1150)
|
|
|
self.canvas_right.place(x=0, y=0)
|
|
|
self.canvas_right.create_image(0, 0, image=self.rightbg, anchor="nw")
|
|
|
# bg_label = tk.Label(self.right, image=self.rightbg)
|
|
|
# bg_label.place(x=0, y=0, relwidth=1, relheight=1)
|
|
|
|
|
|
# self.right.place(x=1200, y=0)
|
|
|
# self.right.create_image(0, 0, image=self.rightbg, anchor=tk.NW)
|
|
|
#
|
|
|
# 底部栏
|
|
|
# self.bottom = tk.Frame(self.root, bg='white', relief='sunken', height=190, width=1540)
|
|
|
self.scroll = scrolledtext.ScrolledText(self.root)
|
|
|
self.scroll.place(x=0, y=950, height=198, width=1300)
|
|
|
# self.bottom.place(x=0, y=1200)
|
|
|
# 用户自定义设置行值大小
|
|
|
# tk.StringVar()表示输入框中输入的类型是字符串。highlightcolor:输入控件获得输入焦点时的边框颜色。highlightthickness:输入控件的边框宽度
|
|
|
self.canvas_right.create_text(100, 20, text='BOARD_ROWS:', font=('TimesNewRoman', 12))
|
|
|
self.entry1 = tk.Entry(self.right, textvariable=tk.StringVar(), bd=1, width=17, highlightcolor='black',
|
|
|
highlightthickness=2, font=('楷体', 12))
|
|
|
self.entry1.place(x=20, y=60)
|
|
|
# 用户自定义设置列值大小
|
|
|
self.canvas_right.create_text(100, 120, text='BOARD_COLS:', font=('TimesNewRoman', 12))
|
|
|
|
|
|
self.entry2 = tk.Entry(self.right, textvariable=tk.StringVar(), bd=1, width=17, highlightcolor='black',
|
|
|
highlightthickness=2, font=('楷体', 12))
|
|
|
self.entry2.place(x=20, y=140)
|
|
|
# 用户自定义设置地雷数量
|
|
|
self.canvas_right.create_text(100, 200, text='MINES:', font=('TimesNewRoman', 12))
|
|
|
|
|
|
self.entry3 = tk.Entry(self.right, textvariable=tk.StringVar(), bd=1, width=17, highlightcolor='black',
|
|
|
highlightthickness=2, font=('楷体', 12))
|
|
|
self.entry3.place(x=20, y=220)
|
|
|
# 用户自定义设置时间限制为多少,默认噩梦等级,2400s
|
|
|
self.canvas_right.create_text(100, 280, text='TIME_LIMIT:', font=('TimesNewRoman', 12))
|
|
|
self.entry4 = tk.Entry(self.right, textvariable=tk.StringVar(), bd=1, width=17, highlightcolor='black',
|
|
|
highlightthickness=2, font=('楷体', 12))
|
|
|
self.entry4.place(x=20, y=300)
|
|
|
# 提交和自动扫雷按钮
|
|
|
self.btn2 = tk.Button(self.right, image=self.random_mines, command=lambda: self.get_data(2),
|
|
|
relief="flat")
|
|
|
self.btn2.place(x=70, y=400)
|
|
|
# self.btn3 = tk.Button(self.right, image=self.zidong, width=102, height=38,
|
|
|
# command=lambda: self.thread(self.data.auto_mine_sweeper), relief="flat")
|
|
|
self.btn3 = tk.Button(self.right, image=self.aut_mines, # width=100, height=50,
|
|
|
command=lambda: self.thread(self.data.auto_sweeper), relief="flat")
|
|
|
self.btn3.place(x=70, y=600)
|
|
|
|
|
|
self.btn4 = tk.Button(self.right, image=self.lay_mines, font=('TimesNewRoman', 12), command=lambda: self.get_data(1),
|
|
|
relief="flat")
|
|
|
self.btn4.place(x=70, y=500)
|
|
|
|
|
|
def element(self, path, width=50, height=50):
|
|
|
# 加载图元对应的图片文件
|
|
|
img = Image.open(path)
|
|
|
# 使用resize方法调整图片
|
|
|
# img = img.resize((70, 60))
|
|
|
img = img.resize((width, height))
|
|
|
# 把Image对象转换成PhotoImage对象
|
|
|
img = ImageTk.PhotoImage(img)
|
|
|
# 保存图片的引用,防止被垃圾回收
|
|
|
self.root.img = img
|
|
|
return img
|
|
|
|
|
|
def create_boards(self): # 创建按钮方块,并绑定对应函数
|
|
|
global BOARD_ROWS, BOARD_COLS, BUTTONS, Distance_X, Distance_Y
|
|
|
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), bg='black',
|
|
|
relief="flat")
|
|
|
button.place(y=Distance_X + 50 + row * 45, x=Distance_X + col * 45, width=45, height=45) # 设置每个按钮的位置。(row+1):头部栏占了一行,所以雷图整体下移一行
|
|
|
button.bind("<Button-3>", on_right_click) # 绑定鼠标右键,绑定on_right_button_down(event, x, y)
|
|
|
BUTTONS[row, col] = button # 以坐标为键,把该坐标处的button作为值,一一对应便于修改对应button的信息
|
|
|
|
|
|
def create_custom(self): # 创建空白按钮方块,并绑定对应函数
|
|
|
global BOARD_ROWS, BOARD_COLS, BUTTONS, Distance_X, Distance_Y
|
|
|
for row in range(BOARD_ROWS):
|
|
|
for col in range(BOARD_COLS):
|
|
|
def on_right_click(event, x=row, y=col):
|
|
|
self.cancellation_mine(event, x, y) # 鼠标左、右键分别绑定函数
|
|
|
button = tk.Button(self.root, command=lambda x=row, y=col: self.down_mine(x, y), bg='black',
|
|
|
relief="flat")
|
|
|
# 设置每个按钮的位置。(row+1):头部栏占了一行,所以雷图整体下移一行
|
|
|
button.place(y=Distance_X + 50 + row * 45, x=Distance_X + col * 45, width=45, height=45)
|
|
|
button.bind("<Button-3>", on_right_click) # 绑定鼠标右键,绑定cancellation_mine(event, x, y)
|
|
|
BUTTONS[row, col] = button # 以坐标为键,把该坐标处的button作为值,一一对应便于修改对应button的信息
|
|
|
|
|
|
# 创建信息选择窗口
|
|
|
def create_messagebox(self):
|
|
|
# messagebox.showinfo("提示", "是否自定义完成布雷")
|
|
|
result = messagebox.askyesno("提示", "是否自定义完成布雷")
|
|
|
if result:
|
|
|
self.custom_reset()
|
|
|
def down_mine(self, x, y):
|
|
|
global GAME_OVER, TIMER_RUN
|
|
|
if initData[x][y] == 'M': # 游戏结束不再响应、做了雷标记按钮左键无效
|
|
|
return
|
|
|
number = MINES - sum([i.count('M') for i in initData])
|
|
|
if number == 0: # 雷数为0,左键无效
|
|
|
self.create_messagebox()
|
|
|
return
|
|
|
if initData[x][y] == 0 : # 未点开的方块才响应
|
|
|
initData[x][y] = 'M'
|
|
|
curData[x][y] = 'M'
|
|
|
self.data.auto_number() # 更新雷边数
|
|
|
self.show_game_window() # 点击的不是雷,刷新游戏窗口显示
|
|
|
number = number - 1
|
|
|
self.label_mine['text'] = str(number) # 显示剩余地雷数 # 更新剩余雷数
|
|
|
|
|
|
|
|
|
def cancellation_mine(self, event, x, y):
|
|
|
if curData[x][y] == 'B': # 没有雷标记按钮右键无效
|
|
|
return
|
|
|
number = MINES - sum([i.count('M') for i in initData])
|
|
|
if number >= MINES: # 雷数大于预备雷数,右键键无效
|
|
|
return
|
|
|
if initData[x][y] == 'M': # 为
|
|
|
curData[x][y] = 'B'
|
|
|
initData[x][y] = 0
|
|
|
self.data.auto_number() # 更新雷边数
|
|
|
self.show_game_window() # 点击的不是雷,刷新游戏窗口显示
|
|
|
number = number + 1
|
|
|
self.label_mine['text'] = str(number) # 显示剩余地雷数 # 更新剩余雷数
|
|
|
|
|
|
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 + 70, y=20, anchor=tk.CENTER) # "游戏设置"的显示位置,"右侧栏"中部
|
|
|
|
|
|
self.label_time.place(x=HEADER_WIDTH + 70 + 60, y=20, anchor=tk.CENTER) # 游戏限时的显示位置,"右侧栏"右上角
|
|
|
|
|
|
def right_frame(self):
|
|
|
global BOARD_ROWS, HEADER_WIDTH, HEADER_C
|
|
|
HEIGHT = BOARD_ROWS * 5
|
|
|
|
|
|
# 以最后的按钮位置y=160为依据(加上按钮本身有高度,大致为180)
|
|
|
self.right['height'] = 1000 # 右侧栏高度和雷图高度一致,将右侧完全覆盖
|
|
|
self.right.place(x=1000 + 1, y=0, anchor=tk.NW) # 右侧栏紧挨着(一个像素的距离)地雷区域
|
|
|
self.label_row.place(x=100, y=15, anchor=tk.CENTER) # 各标签和输入框的位置
|
|
|
self.entry1.place(x=130, y=30 + BOARD_ROWS, anchor=tk.CENTER)
|
|
|
|
|
|
self.label_col.place(x=100, y=15 + HEIGHT, anchor=tk.CENTER)
|
|
|
self.entry2.place(x=130, y=30 + HEIGHT + BOARD_ROWS, anchor=tk.CENTER)
|
|
|
|
|
|
self.label_m.place(x=70, y=15 + HEIGHT * 2, anchor=tk.CENTER)
|
|
|
self.entry3.place(x=130, y=30 + HEIGHT * 2 + BOARD_ROWS, anchor=tk.CENTER)
|
|
|
|
|
|
self.label_limit.place(x=100, y=15 + HEIGHT * 3, anchor=tk.CENTER)
|
|
|
self.entry4.place(x=130, y=30 + HEIGHT * 3 + BOARD_ROWS, anchor=tk.CENTER)
|
|
|
|
|
|
self.btn2.place(x=80, y=15 + HEIGHT * 4 + BOARD_ROWS, anchor=tk.CENTER) # 提交按钮位置
|
|
|
self.btn3.place(x=200, y=15 + HEIGHT * 4 + BOARD_ROWS, anchor=tk.CENTER) # 自动扫雷按钮位置
|
|
|
|
|
|
def bottom_frame(self):
|
|
|
global BOARD_ROWS, BOARD_COLS, HEADER_HEIGHT
|
|
|
self.bottom['width'] = 1520 # 设置底部栏的宽度
|
|
|
self.bottom['height'] = 180 # 设置底部栏的高度
|
|
|
self.bottom.place(x=0, y=1200, anchor=tk.NW) # 底部栏紧挨着地雷区域底部
|
|
|
self.scroll['width'] = 1508 # 文本框的宽度
|
|
|
self.scroll['height'] = 180 # 设置底部栏的高度
|
|
|
self.scroll.place(x=0, y=1200, 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
|
|
|
# col为列,0到BOARD_COLS-1 #如果雷没被标记,显示雷。雷被正确标记为旗子,不显示雷,且非爆炸雷
|
|
|
for col in range(BOARD_COLS):
|
|
|
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
|
|
|
# print(f'进来了{x},{y}')
|
|
|
if GAME_OVER or curData[x][y] == 'F': # 游戏结束不再响应、做了雷标记按钮左键无效
|
|
|
return
|
|
|
# print(1)
|
|
|
if not TIMER_RUN: # 游戏未开始,左右键均不能用
|
|
|
self.scroll.insert('end', '游戏未开始,请点击脸图开始游戏之后,再使用左键\n')
|
|
|
self.scroll.see(END) # 消息框自动下拉到最后一行
|
|
|
return
|
|
|
# print(1)
|
|
|
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 # 已显示过信息,不重复显示
|
|
|
# print(1)
|
|
|
self.show_game_window() # 点击的不是雷,刷新游戏窗口显示
|
|
|
self.data.remaining_mine_num() # 更新剩余雷数
|
|
|
self.win() # 判断是否胜利,此处依据为非雷的方块是否全部点开
|
|
|
# print('出去了1')
|
|
|
|
|
|
def on_right_button_down(self, event, x, y): # event代表鼠标事件,这里默认event.num=3,即鼠标右键,x,y为点击的坐标
|
|
|
global curData, TIMER_RUN, GAME_OVER, BUTTONS
|
|
|
# 该按钮已被打开,已显示其相邻按钮下的地雷数,不能做标记
|
|
|
# print(f'进来了{x},{y}')
|
|
|
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) # 修改被右击的方块状态
|
|
|
# print(curData[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() # 判断是否胜利,此处依据为雷的方块是否全部被正确标记且没有错误标记
|
|
|
# print('出去了2')
|
|
|
|
|
|
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 # 开始计时
|
|
|
# TIMER_RUN=Ture且秒数<1000,将一直计算秒数。退出该函数,子线程结束,计算秒数结束
|
|
|
while TIMER_RUN and CLOCK <= TIME_LIMIT:
|
|
|
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默认以噩梦等级进入游戏,玩家点击左上角游戏按钮,可切换等级。\n' \
|
|
|
'右侧栏是游戏设置栏,用户可在规则内进行自定义游戏设置。\n规则:参数必须为正整数,且列数必须在[6,40]范围之内,\n行数必须在[6,20]范围之内,' \
|
|
|
'雷数不能大于行列数的乘积,游戏限时不能超过99999s,\n且必须在右上角的规定的限制时间内完成,否则游戏失败。\n' \
|
|
|
'左击方块,是雷游戏结束,显示哭脸,否则显示相邻八块方块的总地雷数,\n为空表示相邻八块方块无雷。当把所有无雷方块都翻开或者把有雷的方块都标记,\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) # 右上角显示游戏限时
|
|
|
if CUSTOM:
|
|
|
self.data.init_mine_map(0) # 初始化initData,并在列表中随机增加地雷
|
|
|
else:
|
|
|
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 custom_reset(self): # 重玩游戏函数
|
|
|
global MINES, BOARD_ROWS, BOARD_COLS, GAME_OVER, BUTTONS, CLOCK, TIMER_RUN,CUSTOM
|
|
|
CUSTOM = 0
|
|
|
self.create_boards() # 创建新按钮方块并绑定左右键函数
|
|
|
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_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, Distance_X, Distance_Y, CUSTOM
|
|
|
if MINES == mine and BOARD_COLS == col and BOARD_ROWS == row and TIME_LIMIT == time and CUSTOM == 0:
|
|
|
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
|
|
|
Distance_X = int(10 - row / 2) * 45
|
|
|
Distance_Y = int(10 - col / 2) * 45
|
|
|
TIME_LIMIT = time
|
|
|
HEADER_WIDTH = BOARD_COLS * 30 # 更新头部栏和右侧栏宽度,脸图以及计时器位置
|
|
|
face_x = HEADER_WIDTH / 2
|
|
|
clock_x = HEADER_WIDTH - 30
|
|
|
# print(CUSTOM)
|
|
|
if CUSTOM:
|
|
|
self.create_custom() # 创建新按钮方块并绑定左右键函数
|
|
|
else:
|
|
|
self.create_boards() # 创建新按钮方块并绑定左右键函数
|
|
|
# self.root.geometry('1600x1400') # 加上右侧栏与底部栏区域
|
|
|
# self.header_frame() # 更新头部栏属性信息
|
|
|
# self.right_frame() # 更新右侧栏属性信息
|
|
|
# self.bottom_frame() # 更新底部栏属性信息
|
|
|
self.menu() # 菜单栏
|
|
|
self.reset() # 依照数据初始化
|
|
|
|
|
|
# def set_custom_game(self, row, col, mine, time): # 根据参数设置游戏难度
|
|
|
# global MINES, BOARD_ROWS, BOARD_COLS, BUTTONS, TIME_LIMIT, HEADER_WIDTH, face_x, clock_x, BOTTOM_HEIGHT, Distance_X, Distance_Y
|
|
|
# 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
|
|
|
# Distance_X = int(10 - row / 2) * 60
|
|
|
# Distance_Y = int(10 - col / 2) * 60
|
|
|
# TIME_LIMIT = time
|
|
|
# HEADER_WIDTH = BOARD_COLS * 30 # 更新头部栏和右侧栏宽度,脸图以及计时器位置
|
|
|
# face_x = HEADER_WIDTH / 2
|
|
|
# clock_x = HEADER_WIDTH - 30
|
|
|
# self.create_custom() # 创建新按钮方块并绑定左右键函数
|
|
|
# self.menu() # 菜单栏
|
|
|
# self.custom_reset() # 依照数据初始化
|
|
|
|
|
|
def get_data(self, event): # 获取用户在文本框中填写的数据,只能填正整数!
|
|
|
global CUSTOM
|
|
|
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
|
|
|
# 用户参数连接字符串,含有非0-9的任何字符都不行
|
|
|
dataString = self.entry1.get() + self.entry2.get() + self.entry3.get() + self.entry4.get()
|
|
|
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 > 20:
|
|
|
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 # 雷数不能比总方块数量还多
|
|
|
# 998为Python最大递归深度,超过998时,雷数不能太少,防止超过最大递归深度报错
|
|
|
# if row * col > 998 and mine < row * col / 5:
|
|
|
# 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
|
|
|
if event == 1:
|
|
|
CUSTOM = 1
|
|
|
# self.set_custom_game(row, row, mine, time) # 自定义布雷界面
|
|
|
else:
|
|
|
CUSTOM = 0
|
|
|
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)] # 无雷初始化,先列后行
|
|
|
if mines == 0: # 自定义布雷
|
|
|
return
|
|
|
for i in random.sample(range(BOARD_COLS * BOARD_ROWS), mines):
|
|
|
initData[i // BOARD_COLS][i % BOARD_COLS] = 'M' # 在BOARD_COLS*BOARD_ROWS范围中随机生成mines个雷
|
|
|
# 雷行下标为随机数除以列数取整,雷列下标为随机数对列数取模
|
|
|
# print(initData)
|
|
|
return
|
|
|
|
|
|
def init_board_state(self): # 初始化方块状态
|
|
|
global curData, SHOW_BOARD_STATE, BOARD_ROWS, BOARD_COLS
|
|
|
if sum([i.count('M') for i in initData]):
|
|
|
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)] # 显示状态
|
|
|
else:
|
|
|
curData = [['B' 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, GAME_OVER # 雷的状态,布雷方案,是否计算过
|
|
|
if initData[x][y] == 'M': # 挖开的是雷,游戏结束
|
|
|
curData[x][y] = 'X' # 更新状态为翻开的雷
|
|
|
GAME_OVER = 1
|
|
|
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 * 30 / 2) # 脸图的位置在头部栏中间
|
|
|
face_p_y = 80 # 脸图y坐标固定为65,在[70,90]内均可
|
|
|
first_block_x = 30 # 第一个方块的位置x,在[20,40]内均可
|
|
|
first_block_y = 130 # 第一个方块的位置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 + 30 * col, first_block_y + y0 + 30 * row, 1) # 左键单击按钮
|
|
|
time.sleep(0.1) # 让鼠标慢一点
|
|
|
if GAME_OVER: # 游戏结束,不再响应,鼠标停止自动点击
|
|
|
break
|
|
|
|
|
|
def find_coordinates_with_E(self, input_list):
|
|
|
coordinates_list = []
|
|
|
for i in range(len(input_list)):
|
|
|
for j in range(len(input_list[i])):
|
|
|
if input_list[i][j] == 'E':
|
|
|
coordinates_list.append((i, j))
|
|
|
return coordinates_list
|
|
|
def auto_sweeper(self): # 自动扫雷
|
|
|
global BOARD_ROWS, BOARD_COLS, BUTTONS, curData, TIMER_RUN, GAME_OVER # 全局变量
|
|
|
if GAME_OVER: # 游戏结束,不再响应
|
|
|
return
|
|
|
if TIMER_RUN: # 如果游戏已经开始,先暂停,确保在游戏进行途中也能使用自动扫雷
|
|
|
# TIMER_RUN = False
|
|
|
time.sleep(1) # 沉睡一秒,与计时器进程保持时间一致
|
|
|
m_list = [i for i in range(1, 9)]
|
|
|
while 1:
|
|
|
if GAME_OVER: # 游戏结束,不再响应
|
|
|
return
|
|
|
print('执行了')
|
|
|
if sum([i.count('X') for i in curData]) == 1:
|
|
|
break
|
|
|
curData_new = [i.count('F') for i in curData]
|
|
|
for row in range(BOARD_ROWS):
|
|
|
for col in range(BOARD_COLS): # x对应列,y对应行
|
|
|
if curData[row][col] in m_list: # 如果不为E证明被打开了,则跳过
|
|
|
self.counting_surrounding_points(row, col)
|
|
|
for row in range(BOARD_ROWS):
|
|
|
for col in range(BOARD_COLS): # x对应列,y对应行
|
|
|
if curData[row][col] in m_list: # 如果不为E证明被打开了,则跳过
|
|
|
self.counting_surrounding_points(row, col)
|
|
|
if curData_new == [i.count('F') for i in curData]:
|
|
|
if MINES == sum(curData_new):
|
|
|
break
|
|
|
else:
|
|
|
coordinates_list = self.find_coordinates_with_E(curData)
|
|
|
# print(coordinates_list)
|
|
|
coordinates_new = coordinates_list[random.randint(0,len(coordinates_list)-1)]
|
|
|
# print(coordinates_new)
|
|
|
test.on_left_button_down(coordinates_new[0], coordinates_new[1])
|
|
|
time.sleep(0.1) # 沉睡一秒,与计时器进程保持时间一致
|
|
|
print('扫雷完成')
|
|
|
# for row in range(BOARD_ROWS-1):
|
|
|
# for col in range(BOARD_COLS-1): # x对应列,y对应行
|
|
|
# if curData[row][col] == 'E': # 如果不为E证明被打开了,则跳过
|
|
|
# pass
|
|
|
|
|
|
def find_neighbors(self, matrix, row, col):
|
|
|
rows = len(matrix)
|
|
|
cols = len(matrix[0])
|
|
|
neighbors = []
|
|
|
coordinate = []
|
|
|
# 遍历周围的8个点(包括对角线方向)
|
|
|
for i in range(-1, 2):
|
|
|
for j in range(-1, 2):
|
|
|
# 排除当前点以及超出矩阵边界的点
|
|
|
if i == 0 and j == 0:
|
|
|
continue
|
|
|
if row + i >= 0 and row + i < rows and col + j >= 0 and col + j < cols:
|
|
|
neighbors.append(matrix[row + i][col + j])
|
|
|
coordinate.append((row + i,col + j))
|
|
|
return neighbors, coordinate
|
|
|
|
|
|
def counting_surrounding_points(self, x, y):
|
|
|
global curData, BOARD_ROWS, BOARD_COLS, Distance_X, Distance_Y
|
|
|
window = pyautogui.getWindowsWithTitle('扫雷')[0] # 获取窗口句柄
|
|
|
neighbors, coordinate = self.find_neighbors(curData, x, y)
|
|
|
if neighbors.count('E') == 0:
|
|
|
return
|
|
|
time.sleep(0.1) # 沉睡一秒,与计时器进程保持时间一致
|
|
|
if neighbors.count('F') == curData[x][y]:
|
|
|
indexes = [index for index, value in enumerate(neighbors) if value == 'E']
|
|
|
for i in indexes:
|
|
|
# test.data.num_of_mine(counting_list[1][i][0], counting_list[1][i][1])
|
|
|
test.on_left_button_down(coordinate[i][0], coordinate[i][1])
|
|
|
# m.click(50 + x0 + 60 * coordinate[i][0] + Distance_X, Distance_Y + y0 + 60 * coordinate[i][1], 1) # 左键单击按钮
|
|
|
time.sleep(0.1) # 沉睡一秒,与计时器进程保持时间一致
|
|
|
# counting_list[0].count('E') == curData[x][y] and
|
|
|
elif (neighbors.count('E') + neighbors.count('F')) == curData[x][y]:
|
|
|
indexes = [index for index, value in enumerate(neighbors) if value == 'E']
|
|
|
# print(indexes)
|
|
|
# print(2)
|
|
|
for i in indexes:
|
|
|
# test.data.show_flag(counting_list[1][i][0], counting_list[1][i][1])
|
|
|
test.on_right_button_down(3, coordinate[i][0], coordinate[i][1])
|
|
|
# m.click(50 + x0 + 60 * coordinate[i][0] + Distance_X, Distance_Y + y0 + 60 * coordinate[i][1], 2) # 右键单击按钮
|
|
|
time.sleep(0.1) # 沉睡一秒,与计时器进程保持时间一致
|
|
|
|
|
|
def replace_elements(self):
|
|
|
global curData
|
|
|
# 遍历每一行
|
|
|
for row in range(BOARD_ROWS):
|
|
|
# 遍历每一列
|
|
|
for col in range(BOARD_COLS):
|
|
|
if curData[row][col] != 'M':
|
|
|
curData[row][col] = 'B'
|
|
|
def auto_number(self): # 自动更新雷别上数值
|
|
|
global curData
|
|
|
self.replace_elements()
|
|
|
for row in range(BOARD_ROWS):
|
|
|
for col in range(BOARD_COLS): # x对应列,y对应行
|
|
|
if curData[row][col] == 'M': # 如果为M,则需要计算周围数字
|
|
|
|
|
|
neighbors, coordinate = self.find_neighbors(curData, row, col)
|
|
|
# print(neighbors, coordinate)
|
|
|
for drop in range(len(neighbors)):
|
|
|
if neighbors[drop] != 'M':
|
|
|
if neighbors[drop] == 'B':
|
|
|
curData[coordinate[drop][0]][coordinate[drop][1]] = 1
|
|
|
else:
|
|
|
curData[coordinate[drop][0]][coordinate[drop][1]] = int(neighbors[drop])+1
|
|
|
time.sleep(0.1)
|
|
|
# for i in curData:
|
|
|
# print(i)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
# 创建test对象
|
|
|
test = Show() # 初始化游戏界面
|
|
|
test.set_game_level(0, 0, 0, 0) # 调用函数完成游戏初始化,并进入游戏
|
|
|
test.root.mainloop() # 显示UI
|