9.6.4 添加功能

有以下几个功能需要确定:
1.点到雷失败
在左键单击事件里,判断当前矩阵的值是不是9,如果是,那么点到雷了,弹出框失败,并且禁用所有按钮
首先弹出框可以这么写:
from tkinter import messagebox
messagebox.showwarning("你点到雷了""失败!")
点击到9的时候触发。
失败后就不能再玩了,需要让所有按钮失效:
 if t==9:
     messagebox.showwarning("""失败!")
     #遍历root上面所有子控件,设置状态为不可用,并且禁用左键和右键
     for child in root.winfo_children():
           child.config(state="disabled")
           child.unbind("<Button-1>")
           child.unbind("<Button-3>")

2.胜利。
胜利条件有两种,所有非雷的按钮都打开了,或者所有雷都被标记了。
其实这需要另外一个矩阵,命名为mine_status,来保存按钮点击情况,按钮有三种状态:未点击、已点击(不是雷)、标记雷,可以分别记作0、1、2
#...前面略
root = tk.Tk()
# 创建一个10x10的矩阵,用来记录点击状态,0为未点击;1为打开;2为标记为雷
mine_status= [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]
# 创建一个10x10的按钮矩阵
mines = generateMines()  # 生成地雷矩阵
#...后面略
然后增加以下逻辑:
每当左键点击完毕,就要把对于的位置的值设置成1
每当右键设置旗子,就要把对于的位置的值的设置成2

于是需要定义一个和mines同维度的矩阵mines_clicked,初值是0。参考4.5二维列表第1题
点击某个按钮后,除了从mines读取对应的值显示在按钮上,还要修改mines_clicked对应的值

状态1在左键修改,状态2在右键修改。
如果mines_clicked里面所有的值都不为0,说明已经开了所有按钮,但不能判断就胜利了,因为没有检查雷放得对不对,例如全部都标记为雷。但也没必要检查所有雷,只需要再增加一个变量记录已经标记的数量,也就是小红旗(标记雷)不能无限制。
胜利条件一就是:小红旗的额度已经用完(等于0),点击矩阵mine_clicked所有值都不等于0。
按照传统扫雷的胜利逻辑,还有以下情形:
1.剩下未开的按钮数量和剩余小红旗的数量相等
2.剩余小红旗为0,而且标记全部都对,此时无视还有几个雷未开。可以在小红旗剩余为0的时候,发起两个矩阵的对比:值为9的元素是否在另一个矩阵也是9,如果不是,停止检查,并没有胜利。
最后胜利也可以用弹出框的形式
 messagebox.showinfo("""恭喜!胜利!")

3.左键点击按钮后设置按钮为白色,同时禁用按钮
数字0被点中的时候,是不需要显示出0的
因为左键不能点了又点,点完就要禁用,设置白色是为了用颜色区别已经点过,下面的代码在左键点击函数里:
t = mines[row][column]
if t != 0 :
    #设置文字的代码,略
button.config(bg='white')  # 修改背景颜色为白色
button.config(state=tk.DISABLED)  # 禁用按钮

4.点到数字0打开周围8个按钮,并且继续打开所有0的按钮。
首先写一个函数open_neighbour(i,j),功能是打开给定横坐标、纵坐标的周围8个格子,8个邻居要判断是否越界,新建一个列表to_be_opened,用来保存0按钮
然后解决这两个问题:
1.把所有邻居打开(最多8个邻居,最少3个邻居),如果打开的数字刚好是0,并且把此邻居的坐标((横坐标,纵坐标)的元组形式加入列表)加入列表to_be_opened
2.如果列表to_be_opened不为空,取一个按钮并从列表删除,并回到第1步,直到列表为空。
#专门保存为0的按钮,并且它的邻居还没打开和检查过
to_be_opened=[]
#添加i,j的八个值为0的邻居到列表toBeOpened    
def open_neighbour(i,j)(i,j):
   #左上角邻居的下标检查:
    if i-1>=0 and j-1>=0: 
        #左键点击(i-1,j-1)这个按钮
        button_click(i-1,j-1)       
        if mines[i-1][j-1]==0:
           to_be_opened.append((i-1,j-1))
    #后面还有7个邻居要打开以及检查,自行补充    
    #=========未完成=========
#处理to_be_opened列表,逐个取出按钮,然后打开它八个邻居
def handle_list():
    #遍历to_be_opened列表,不能用for,因为to_be_opened列表随时都可能发生变化
    while len(to_be_opened)>0:
        t=to_be_opened.pop() # pop的功能是取出一个元素并从原列表删除,t是元组,(i,j)的形式
        i=t[0]
        j=t[1]
        #这个函数模拟左键点击按钮,实际上鼠标并没有点击到,但功能要和左键点击一样
        #button_click有可能把新的为0的按钮加入列表to_be_opened
        button_click(i,j)
def button_click(i,j):
   #左键点击坐标为(i,j)的按钮
   print("暂时还没有实现")    
#左键点击事件
def left_click(event):
  button = event.widget
  
  #9.6.2练习做的
  row= #获取行=========未完成=========
  column= #获取列=========未完成=========
  #设置文字,=========未完成=========
  t=mines[row][column]
  if t==9:
    #输掉的逻辑=========未完成=========
    pass
  elif t==0:
    #设置成-1是为了防止本按钮又被邻居按钮加入列表,形成了死循环,
    #例如:按钮A是0,A的邻居B也是0,A打开后把B加入了列表,然后B检查邻居发现A也是0,又加入列表,实际上A已经开过了
    mines[row][column]=-1
    #打开所有邻居,并把为0的邻居加入到列表
    open_neighbour(row,column)
    #处理所有是0的按钮
    handle_list(row,column)
此时第25行的button_click的功能就是打开上面的数字,基本功能和left_click函数一样,最好就能直接调用,但是问题在于left_click函数可以通过event事件对象取得当前被点击的按钮,而button_click,只能通过根据横竖坐标取得按钮。现在把左键的功能提取出来成为一个新的函数left_click_logic,然后给left_click和button_click使用:
#前面略
def left_click(event):
  button = event.widget
  left_click_logic(button)
def button_click(i,j):
  #根据坐标取得按钮
  button_list = root.grid_slaves(row=i, column=j)
  button = button_list[0]
  left_click_logic(button)
def left_click_logic(button):
  #9.6.2练习做的
  row= #获取行=========未完成=========
  column= #获取列=========未完成=========
  #设置文字,=========未完成=========
  t=mines[row][column]
  if t==9:
    #输掉的逻辑=========未完成=========
    pass
  elif t==0:
    #设置成-1是为了防止本按钮又被邻居按钮加入列表,形成了死循环,
    #例如:按钮A是0,A的邻居B也是0,A打开后把B加入了列表,然后B检查邻居发现A也是0,又加入列表,实际上A已经开过了
    mines[row][column]=-1
    #打开所有邻居,并把为0的邻居加入到列表
    open_neighbour(row,column)
    #处理列表
    handle_list()
效果图,白色为已经打开的按钮,灰色为未打开:
5.按钮重置游戏
在矩阵上方再加一个开始游戏的按钮,点击后就重新开始游戏。但这样做要改变布局,前面按钮矩阵要放在一个框架(frame)里面,上面再放一个frame(里面只包含一个开始游戏按钮),两个frame按照pack布局。
还有一种方案是做一个菜单,在菜单中做一个新游戏的命令。
重置游戏需要做到的事情:
1.重新计算地雷矩阵
2.重新排列按钮矩阵
3.重置所有中间计算用的变量

6.自定义地雷
自定义行列数量以及雷的个数,这就一定需要菜单了。而且需要弹出框输入数量。

7.剩余未标记地雷倒数和时间流逝窗口
这两个可以用label显示,在失败的时候时间流逝窗口停止计时,在开始的时候重置为0秒。
在开始的时候,先记下当前的时间戳(第10行代码),然后执行repeat函数,先计算一次秒(通过调用print_seconds),然后用root.after设定1000毫秒后再执行repeat函数,形成了一个链条一样的执行链——A执行B,然后设定1000毫秒后执行A:
def print_seconds():
    #计算逝去的时间
    passTime=int(time.time()-start)
    print(passTime)
def repeat():
    print_seconds()
    root.after(1000, repeat)
start=time.time()
root = tk.Tk()
# 启动重复任务
passTime=0
repeat()
root.mainloop()