9.6.4 添加功能
有以下几个功能需要确定:
1.点到雷失败
在左键单击事件里,判断当前矩阵的值是不是9,如果是,那么点到雷了,弹出框失败,并且禁用所有按钮
首先弹出框可以这么写:
from tkinter import messageboxmessagebox.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=0repeat()root.mainloop()