今天我是小鱼,今天是2021年11月17日,我想自己写一个扫雷,用Python
我用类似日记的方式记录我的过程,想看代码直接拖到最后。
分享出来仅为交流与学习。
前几天儿子说别的小朋友都有电脑,玩游戏。于是就把家里的笔记本给他玩,但那是一台工作本,没有游戏,我就说那你玩扫雷吧,正好锻炼一下怎么用鼠标。
但我发现win11的扫雷跟XP很不同,要去appstore下,画面动画很累赘,很慢。重点是有广告!!!强制看30秒,不能取消。每5分钟就跳出来——孔子不能忍,孟子不能忍,老子也不能忍!
毕竟,XP是2001年出来的,20年过去了——“大人,时代不同了”。
因为最近小半年都在自学Python,于是有了种强烈的要自己写的想法,虽然CSDN上已经有好多人都写过,但我想自己写。不为儿子,就为了练习。
我今天先记录下我的构想:
1.只有初级,中级,高级,没有自定义地图大小功能,地图大小固定;雷数先固定,预留可以自定义雷数的接口。
2.分三个层面,运算层,界面层,控制层。运算层用来生成地图,自动布雷,自动写出雷周边的数字,数字要有不同颜色。预留自定义雷数功能,要增加算法,每个地图最多能放多少个雷,再多就不合理了。界面层是窗口,窗口里的内容,包括计时,雷数,emoj按钮,有下拉的菜单。还有雷上的“土”(我管那些灰色的没有点开的方块叫“泥土”),还有土上的插旗,以及点击到空土(下面没有雷没有数字的)时自动打开周边土直到遇到非空土为止的算法。控制层是收集鼠标的操作,左键,右键。并执行相关的操作,F2是开始新的游戏,F1打开帮助文档。选择初级,中级,高级。稍微与XP有点不同的地方,左键开土,右键插旗,没有插问号,右键双击数字代替原来的左右键同击数字。
3.先不做扫雷排行榜,待上面功能实现之后再做。
运算层中:根据控制层选择的等级,定义二维数组,数组大小来自于控制层定义的等级,获取控制层中第一次开土的坐标,第一次开土作为触发运算层的信号,用随机算法自动布雷,布雷的算法必须避开第一次开土的坐标。数字算法在布雷算法完成之后,用9表示雷,用数字表示周边有多少个9. 用0表示空土(初始所有数组值都为0)
界面层中:根据控制层选择的等级画出地图,窗口只有3种大小(不设计自定义地图)。用类来定义窗口,在类中定义窗口上的各种内容,包括泥土,插旗的泥土。两种开图算法一种是左键空土,一种是右键双击开土数字。
控制层中:捕捉鼠标与键盘动作,鼠标动作是左键,右键,右键双击。F1,F2。退出游戏通过点击右上角红叉和菜单中退出游戏实现。
今天先写这些。
今天是11月18日,昨天和今天将运算层的代码写完了。运行的结果还是比较满意的。我先贴上运算层的代码,如果将来发现我的程序构架有问题,后面再说吧。大不了这个贴就不公开了
# _*_ coding : UTF-8 _*_
# @time: 2021/11/17 13:18
# @file: yunsuan.py
# @Author:yyh
# @Software:PyCharm
import numpy as np
from gongyong import log
def autobulei(dengj, x, y):
"""
用随机算法自动布雷,布雷的算法必须避开第一次开土的坐标。
数字算法在布雷算法完成之后,用9表示雷,用数字表示周边有多少个9.
用0表示空土
:param dengj: 用来接收来自控制层的等级选择
:param x,y:是第一次开土时地图上的坐标位置。
:return: 返回的应该是自动布完雷的二维数组
"""
daxiao = { # daxiao是一个表驱动,用来储存不同等级下二维数组的大小与总的雷数
"chuj": [9, 9, 10],
"zhongj": [16, 16, 40],
"gaoj": [16, 30, 99]
}
shuzu_dx = (daxiao[dengj][0], daxiao[dengj][1]) # 获取数组的大小
shuzu = np.zeros(shuzu_dx, dtype=int) # 创建二维数组
i = 0
while i < daxiao[dengj][2]:
x_shuzu = np.random.randint(0, daxiao[dengj][0])
y_shuzu = np.random.randint(0, daxiao[dengj][1])
if shuzu[x_shuzu][y_shuzu] == 9 or ((x_shuzu, y_shuzu) == (x, y)):
continue # 如果随机生成的位置凑巧与第一块土相同,或者已经布过雷了,那就回到循环开头重新随机
else: # 如果是空土
shuzu[x_shuzu][y_shuzu] += 9 #这句应该直接等于9,一开始我用+=发现了自己没有检测是否已经布过雷的bug,所以我就保留了+
i += 1
return shuzu
def autoshuzi(shuzu):
"""
:param shuzu:自动布完雷,但是还没有写雷周围的数字的数组
:return: 返回在雷周围已经写完数字的数组
"""
row = len(shuzu)
col = len(shuzu[0])
sz = shuzu # 换个短一点的名字
for i in range(0, row):
for j in range(0, col): # 用两个for循环对数组逐行逐列遍历
if sz[i][j] == 9: # 如果是雷,那么我们对雷周围的8个格子都加上1
for n in range(i - 1, i + 2):
for m in range(j - 1, j + 2): # 再用一个双for循环,对雷附近的3x3的区域遍历,每个格子+1
if n >= 0 and m >= 0 and row - n >= 1 and col - m >= 1 and sz[n][m] < 9:
sz[n][m] += 1 # 这句if是判断当前操作的元素是否在地图范围内,并且这个元素他不是雷,然后给这个单元格+1
else: # 这句else可以不写,但为了逻辑完整
continue
else: # 这句也是为了逻辑完整。另外,我是遍历整个数组中的雷,并不是遍历整个数组中每个位置,并且对每个位置统计周边雷数。
# 所以,只要不是雷我都会跳过,我认为这样执行的语句会比较少。
continue
return sz
if __name__ == '__main__':
ditu = autoshuzi(autobulei('chuj', 3, 6))
log("地图\n", ditu)
但是这个运算速度实在是感人,以前XP上我点下第一格之后马上就打开了,打开就显示周边雷数了。说明当时地图都已经布局好了,我的运算过程大概得有1~2秒钟。哎,先实现功能再说吧。
今天先到这里吧。今天还有不少事要做。
今天是11月19日,昨天后来找了一个做窗口的库,叫tkinter,简称TK。之前做自学的时候做过一个“小蜜蜂”,当时用的是pygame,对于图形界面的库,我用且只用过pygame。但是感觉pygame有点复杂,扫雷这个小游戏使用标准的一些窗口与控件就可以了。既然是学习,就学习使用一个新的库,而且TK是属于标准库,不需要再另外安装。另外,昨天在网上找了图片,就是雷的图片,旗子的图片。今天不忙的话就好好看一下TK的说明文档。明天又要上一天的课,只能下周再继续了。
现在已经是下午了,今天还是看了不少TK的说明文档,另外我将yunsuan.py稍微修改了一下,布完雷之后顺便再输出一个列表,里面包含了所有地雷的坐标元组。我发现后面界面层还有要使用这张列表,游戏失败后要显示出所有的雷,但今天就不贴新代码了,等后面完成了一起贴。另外,今天也看了一部分TK中对于键鼠事件的内容。看完我觉得我有信心能完成这个游戏。
今天就到这里吧,不再想扫雷的事了。
今天是11月22日,今天太忙了,没有做什么工作。
今天是11月24日,昨天休假了,没有工作,今天学习了tk中的标签与按钮控件,我准备用标签显示地图,用按钮做泥土。但是按钮的大小与标签的大小我控制不好。CSDN上有一个叫荷蒲的博主关于tk帖子不错,链接如下:
https://so.csdn/so/search?q=tkinter&t=blog&u=hepu8
今天就到这里,太累了,明天要弄明白按钮的大小与标签大小都是什么因素影响的。
今天是11月25日,今天仔细在研究按钮和标签的大小,我知道按钮是会按文本的大小来变化的,所以,我弄了好多的参数,有字体,字号,有内边距,有button自身属性的width和height。我测试了好多次,希望能找出其中的规律,但是,我失败了。标签的大小与字符像素的大小有很明确的公式关系,但是按钮的我实在找不到。我把我的结果截图放在这里,以后有时间再来研究。我用穷举发凑出了一个可以使标签大小与按钮大小一致的值。
今天先这样吧。明天将按钮与标签都放到frame中,自定义定义一个类。顺便明天看看扫雷的数字都用什么颜色.
今天是11月29日,最近几天是比较忙的,没有做什么事,仅仅看了扫雷数字的颜色,但是我对标签与按钮大小的事,有点过于深入了,我测试了Arial3-19字号的字符大小,并且看了对应的宽度增加的步长与高度增加的步长,然后我发现这是一个挺奇怪的序列,需要用三元一次方程来拟合,我觉得这应该是另外一个话题了,我要开另一个帖子来记录。明天绝对不再去想这个问题了。明天练习frame类。
传送门
今天是11月30日,今天其实做了挺多事,我把另一个帖子写了,另外我也弄了Frame模块,把主窗口做了一个类,把菜单做了一个类,另外做了两个Frame,一个用来放游戏信息,另一个用来放游戏的内容。
今天是12月1日,已经是下午四点多了,今天我把地图上的每一个格子都做成了一个类,是继承Label的类,每一块泥土也做成了一个类,是继承Button的。然后用place布局到游戏内容的框架当中去。但是,今天我遇到一个问题,应该是泥土先生成,然后点击第一块泥土时生成地图,我用Label做地图,但是后生成的组件会覆盖在先生成的组件的上面,也就是说地图被覆盖到泥土的上面了。我不知道tk能不能设置层级,我在百度中没有找到,我想如果不行的话,我不能在这上面浪费时间,我要换种思路了。比如说每开一块泥土然后生成一个Label。明天先把上面信息栏的东西弄完,今天不再想这件事了。对了,明天记得把框架与窗口的大小调一下,比生成的泥土来的小了一点。
今天是12月2日,昨天几乎没有做什么工作,昨天在忙自己工作上的事,但是昨天我因为嫌字太细,加粗字体之后发现标签Lable的大小又变化了。然后顺便又测试了几个字号的按钮,发现在某些字号下加粗字体会增大按钮,但在有些字号下又会使按钮变小,这让我太抓狂了。
今天在下午的时候我又用枚举法找出了一系列的加粗后的修正,又是一串毫无规律的数字,小号一点的加不加粗都不影响大小,大号一点的有些影响,有些不影响。总的来说是pad值对加粗修正不影响,边框粗细对加粗修正不影响,边框形式也不影响,但是height与width的值会影响加粗的修正值,并且是成1倍的关系。见下面表格。
另外,今天还修改了窗口的大小,发现geometry参数好像是包含了菜单栏,标题栏的高度的。我对窗口的大小也开始有点迷茫了,明天再弄吧。
今天是12月6日,今天还是做了不少事情,但是今天有点不舒服,状态不太好。今天终于将窗口的大小调整好了,发现了一个有趣的现象,如果我不使用菜单,窗口的高度就是所有内容加起来的高度,如果我使用了菜单,就要额外增加20。即tk.geometry中高度的参数额外加20。另外菜单的字体我是控制不了的,即我不能通过改变菜单字体来实现控制额外增加的高度参数,这个20也是我用眼睛一点一点对比出来的,可能也不太准,菜单的字体就是windows控制面板中设置的那个主题字体。但是如果我将窗口锁定,即我使用tk.resizable方法,将height的参数调整为False的话。我又不需要考虑菜单的那20的高度了,直接将内容的高度加起来就好,真是有趣又意外。今天另外一个收获就是将主窗口的类好好的整理了一下,现在我可以将每一块泥土单独的调用出来了,今后点击泥土时可以显示泥土下的数字,我准备把生成的地图放到主窗口的类里面,与泥土的编号对应起来。同时将地图数字传递给泥土的类。明天先把信息窗口的东西收拾一下吧,我还不知道怎么做计时器,怎么实时更新。我还不知道怎么显示剩余雷数。另外,系统默认的字体里面好像是没有像计算器那样的数字字体的。我决定先用别的字体代替一下,我先用Stencil字体来代替。
今天是12月7日,今天我的问答有位高手告诉了我tkraise()函数,可以将先生成的部件提升到前面来。于是今天我今天的时间都花在了修改之前的策略上了。现在的状态是能出现泥土,点击泥土,泥土会消失显露出下面的数字,如果是0就什么都不显示,如果是9,还是显示9。我测试了一下,生成的时间实在是太感人了,能有明显的看到码方块的一个过程。今天我累了,今天状态还是不太好,我想明天再按我原来的策略试试,即生成的地图在背后,点击某一块泥土的时候才生成那一块地图。这个版本保存为view0.2,试玩之后再弄时间和剩余雷数那两个标签吧。后面再把9换成雷的图片。等把这两个弄好了,我就贴一张图片上来,我觉得可以算是把界面层的工作告一段落了。
今天是12月8日,今天我按我昨天的想法把地图生成在背后,点击泥土时再生成这一小块地图,初始化的过程并没有变快,还是能明显看到码方块的过程,但我还是想继续按这个思路做下去,我觉得这样更加合理一些。昨天的那个版本被保存为了view0.11.另外,今天把剩余雷数的标签弄上去了,就是字体丑,我又换成了Consolas字体。明天弄上时间框和中间的emoj按钮
今天是12月9日,发薪日,心情不错,身体也觉得舒服多了,状态也不错。今天算是把界面层的东西都弄好了,有时间框,但时间框的时间不会动,有中间按钮,但是按钮背后的动作还没做,有雷数框,但因为右键的动作还没有做,所以雷数不会随着右键而减少。菜单也弄好了,但后面的命令都没有做。现在地图上的数字9已经可以显示成地雷了。我给图片涂颜色图了好久,涂了两张,一张的背景色与地图背景色一致,另一张地雷的背景色是红色的,作为引爆的那颗雷。先上两张图片吧。我觉得可以算是界面层的一个小节点。
另外,再说一下这两天在按钮和标签上贴图的发现,我发现当按钮和标签上面是文本的时候height与width属性会以参数的形式进入部件的大小的运算,但如果部件上面是图片的时候,这两个属性好像是直接表示部件的高和宽,为什么好像要加粗,因为我没有验证过。等做完扫雷了有时间了我再去验证一下,我再去补充我的另一篇文章。
我是11月17日开始做的,到今天正好是完整的3周,做运算层只花了1、2天的时间,后面一直都在弄界面。细想一下,是我对tk不熟悉,为了控制标签与按钮大小我花了不少的时间,相信后面应该会越来越顺利的。明天开始做控制层。
今天是12月10日,今天又开始不舒服了,今天还有一些别的事,今天不做了。下周一还要出差一趟。下周二再说吧。周二之前先不去想了。
今天是12月20日,上周因为出差,出了一个星期,什么都没有做,感觉自己懈怠了,有了点成绩而放松了。今天回看之前的代码,我想做emoj按钮的开始功能,也想做计时器,也想做菜单的命令。但是脑子里一团浆糊,什么都做不好。再看看之前做过的《小蜜蜂》的游戏,是不是我对窗口内的东西的构架方式不对,我应该给每件东西都设为类,包括计时器,包括emoj按钮。之后的工作是要再梳理一下自己的代码。我现在的很多功能是直接做在窗口类中的一个方法,可能这样的构架就不行。另外,今天看到几个帖子是关于鼠标操作的,把链接贴在这里,方便后面查
Python笔记之Tkinter(鼠标事件)——潇洒哥的CSDN博客
知乎上的一篇回答“实现一个按钮左右键各触发一种操作”
博客园上的一篇关于鼠标与键盘事件的文章
今天是12月21日,冬至。今天我重新整理了一下计时器,剩余雷数,emoj,把他们都弄成了单独的类,把计时器的功能做了。我是直接搜了一段代码。见下面的连接,虽然我能理解这段代码,但是我无法修改,一改就错。只能直接使用,我把这段代码放到我控制层(gamefunction)当中去了。
计时器的代码
另外,在腾讯云上也有一段类似的计时器代码,我也把链接放在这里,供学习用。
腾讯云上的计时器代码
明天先把菜单当中等级的内容做好,等级功能是多选一,不能多选多,等级选择后要能改变记录等级的变量。
今天是12月22日,今天学到了一个点,在command这个关键字后面只能跟函数名,不能加函数后面的括号。如果要传递参数怎么办,用lambda来传递。这是我的截图
这里有一个链接,我是从博客园上的一篇文章中学到的
Python中的TK的Button如何在command后面函数添加参数解决方法
今天我把游戏菜单中的命令做了,现在点击开始游戏可以链接到一个函数,但是函数我还没写,点击等级可以改变游戏的等级变量,但改变了之后干嘛还没写,点击退出游戏可以直接退出。
明天要把初始化游戏那个函数写好,点击开始游戏,点击emoj,改变等级之后都要调用这个函数。这个函数做在控制层当中去,gamefunction.
今天是12月23日,昨天做初始化游戏的函数的时候遇到了困难,我让主窗口用方法destory,"chuangkou.destory()",但是程序报错“AttributeError:'_tkinter.tkapp'对象没有属性'destoy”今天在查找解决方法的时候,看到runoob上的一篇tkinter的菜鸟教程,在那里面他创建主窗口时不是继承Tk这个类,而是自定义的一个类,这个类的实参是在主程序中的Tk的实例,然后在主窗口的方法中去定义各种参数,我觉得这是个挺好的思路,我想按它的方法来修改,尝试一下是否可以解决我遇到的问题。但是这么修改的工作量挺大的,今天又比较忙,没时间和精力来做,明天或者。。。。下周吧。这里先放上这篇文章的链接,方便以后查询。
Python GUI编程(Tkinter)菜鸟教程
今天是12月24日,按昨天的想法,我将所有的内容都修改好了,花了一天的时间,但是现在初始化游戏的功能做好了,绕过了昨天的问题。现在可以点击菜单中的开始游戏来开始游戏,点击emoj开始游戏,点击切换等级切换窗口大小并重新开始游戏。明天,,,呃,下周,要把主程序中的while true的内容做好,让主程序循环起来。然后调试好游戏状态,根据游戏状态再把泥土下面的地图再回复出来。
今天是12月27日,虽然也不是什么都没做,但今天一直在忙,也实在没做多少,刚刚把泥土左键从原来的command命令改成了一个bind的绑定,绑定到另一个函数上。还没有往下细做。但是今天遇到个有意思的事,bind的函数我并没有形参,因为不需要。但是运行时报错了。报错是“takes 1 positional argument but 2 were given”,他的意思是我少一个参数。百度了一下,随便打个形参就可以了。明天继续吧。明天把左键的功能完善掉,插旗的方案我的想法是换一块泥土,在插旗的泥土上再邮件的时候,再换一块泥土,并且与原来的一样。凡是点击右键了,原来的泥土就destroy掉,释放这块内存。
今天是12月29日,昨天突然出差了,什么都没做。昨天睡太少,今天一天脑子都是懵的。今天把左键的功能前进了一点。现在第一次点击左键会生成地图的数组,这个地图数组以及地雷坐标数组我都放到公用里面作为一个公共变量了。第一次点击之后可以变更游戏状态,现在游戏状态是布尔值,False是游戏未开始,True是开始了,以是否生成地图为标志。第二次点击点击泥土左键开始可以返回泥土的坐标,调用公用地雷地图列表里的数字。今天就先到这里。明天把泥土打开后画地板的函数做好,其实之前已经做过了,只是赋予一个函数就可以了。接下来的工作是做鼠标单击右键的函数,再接下来是做game over的函数。
今天是12月30日,今天我本来把左键的功能都做完了,遇到0自动开图,但是突然发现有一个随机出现的BUG,就是有些地板生产出来跟我预想的不一样,好像自己出现了边框,又好像布局的位置不是我想要的。我用日志打印了一下,又没有看到错误,见下图。于是我不停地往后退。可能退的时候出现了问题,我一直回退到不生成地板,现在我第一次生成地图是没问题的,但我初始化游戏后(点emoj按钮),再点击泥土,就会出现BGU,要点击两次才能开土,而且也是随机出现的。我觉得今天的工作都白做了,我明天要回档到昨天的存档位置去工作了。但今天的工作还是保留一下。作为明天的参考。今天的心情以自动开图为分界,过山车啊。幸好昨天的工作内容保存了。收拾一下心情,明天重新来过。哈哈哈
今天是12月31日,今天回档到前天的存档开是工作,马上就发现,其实前天的结果也是有BUG的,某块泥土在第一次点击时是正常的,重新开始游戏之后就要点击两次,第二次重新开始后就要点三次,下面的截图是我重新开始了4次游戏,要点击5次,明显是内存没有清空掉造成的。
后来我将泥土打印出来,如下图,很明显。当我点击重新开始之后在同一个frame当中新生成了一套泥土,所以当我再次点击泥土的时候出现了mud141,第二次点击是mud60,也就是说我先把后生成的泥土给点掉了,这些泥土是重复地覆盖在上面了。现在回想昨天地板出现的BUG,应该也是相同的问题,每次打开程序的第一次运行是正常的,但后面就不再正常了。我有个想法,再点击重新开始游戏时,我不使用muds=[]来清空存放泥土的列表,而是用双for循环加destory()方法来把每一块泥土都释放掉。
我在网上搜到一个帖子:https://wwwpython/qa/479977
这个帖子里人家说了两种方法
您可以将对每个小部件的引用存储在一个列表中,然后在列表上迭代调用
destroy()
方法。您还可以将所有的条目小部件放在一个框架中。当您删除框架时,框架内的所有小部件也将被删除。
这两种方法我都尝试了一下,可以解决我的问题,但我发现各有优缺点,如果用for循环来删除每一个小部件,小部件的编号还是继续向上编号的。见下图左。明显,我的frame编号还是2,但是mud的编号已经超过81了(我以初级9*9地图做的测试)。如果我用destroy掉frame的方法的话,重新开始后frame的编号会继续向下编,而mud的编号不再往下。见下图右。个人感觉用右图的方法比较简单,而且后面地板应该也会同时被清理掉。但我不知道之前的frame是不是也存在内存里面。会不会游戏进行了一小时后内存越来越多。
今天算是把左键的功能全部做完了,而且我也发现了昨天那个BUG的原因了,就是那个“看上去好像地板自己出现了边框,又好像布局的位置不是我想要的”,其实是因为直接把地板贴在泥土上面了,所以出现了像边框一样的东西,因为我的底板是比泥土要小一圈的。没有边框的。
今天其实做了很多事,game_over的函数我也已经写好了,游戏失败之后会显示出所有地雷,踩炸的雷颜色与其他不同,并且游戏会处于停止状态,计时器停止,所有泥土点击不再有响应。现在每块地板都被储存到了gongyong里的一个列表里面了。我在重置游戏的时候我还是用for循环迭代调用destroy()方法。而且好像并没有出现我上图的问题,我在想下一步要不我也把所有的泥土也存到gongyong里面的一个列表吧。尝试一下会怎么样,但是因为泥土修改的量比较大,下周(明年)再试试吧。今天的先存档备份。做完这个尝试之后就要接着做鼠标单击右键的函数,我的现在的想法是新建一个gongyong列表用来存放插过旗子的泥土,然后直接把这块泥土覆盖在原来的泥土上面,如果再次点击邮件,就把这块泥土destroy掉,露出原来灰色的泥土。今天先到这里,新年快乐^_^
今天是2022年1月6日,元旦我多休息了一天,昨天又忙了一天,都没看。今天工作效率极差。每次很长时间没看之后要回到状态总是很难。
今天一直在做插旗子的功能,现在我能做出一块插过旗子的泥土,但是他与原来的泥土是怎么一个关系还困扰着我,现在我在原来泥土中又新增加了一个布尔值,叫flag。因为我发现点到空土的时候会给周边错点的查过旗的泥土上再覆盖上地板。所以自动开图的命令里面还是要识别这个位置是否插过旗子。另外,还遇到一个问题,插旗的泥土与原来的泥土的关系,我发现我有几种选择。第一是像之前想的那样,再建一块新的,覆盖上去。但是新的泥土也要放一个列表,还是字典?第二是在原来的泥土上直接修改重新设置泥土的属性,第三是把原来的泥土删除了,再放一块新的上去,直接就放在原来的泥土列表当中。我现在在用第三种方法,原来的泥土被删除了,再次右键的时候我怎么恢复出原来的泥土?我还没想好。今天我要仔细想想这个问题,以及上面三种方案的利弊。我现在正在第二和第三种之间摇摆。今天做的事非常少,简单来说,我今天就做了一块泥土,没了。。。。。。而且我又发现一个Button类的大小的问题,插了旗子的泥土必须使用height=17,width=18 才能使之与之前的泥土大小匹配,也就是12月2日日记中贴图的25、26的那两个大小值。这跟我在做地板时得到的结果又不一样。这又让我对大小的事情更加的整不明白了。以后慢慢摸索吧。
今天是1月7日,昨天安静下来想了想,感觉这个问题根本不值得犹豫,明显第二种方式是最好的,tk中给的config命令就是用来做这种事情的。无论是代码量还是程序的运行逻辑来说,都是最合理的一个方案。今天我按这个方案来做,一开始出现了一个问题,在debug模式下一切都正常,但是正式run的时候,右键虽然执行了命令,但泥土上没有出现旗子。后来我把这段代码挪到界面层中,也是一样,直到我把调用旗子图片的那句语句放到上面Mud的__init__里面作为一个类属性之后,这个问题才解决。后来想想,插旗子这个动作其实也是界面层的事,我就没再把他移回去了。但是我依然把插旗子的那些代码修改好后放在控制层里。
今天我完成了右键双击地板的功能,可以快速开图了,顺便还做了一张图片,就是标记错的雷,在游戏失败的时候要用这块地板替换掉标记错的泥土。本来想做一个双击地板后如果标记数与地板数字不同时出现一个按下泥土但不打开的动作,但失败了,这大概跟我用的是bind而不是command有关,先不管他了。
今天双击地板应该还有个BUG,我测试的时候曾经出现过2个炸雷的图片,当时我顺手点了重开,没有截图下来。明天的工作是再测试一下这个双击地板的功能,找出这个BUG。然后把标记错的泥土,在游戏失败后替换成错雷的地板,就是今天做好的那张图片。明天是周六了,下周要准备python考试的东西,可能又没时间弄了。
接下来要做的事情是对胜利的判断,我发现这事儿没这么简单,胜利可以有两种情况。
第一,正确标记了全部地雷,(即使还有泥土没打开)
第二,所有未打开的泥土下面都是雷(无论是否标记过)
我曾经就喜欢玩不用鼠标右键标记来完成游戏。但要实现上面这个逻辑还是不太简单的。我现在的想法是这样的,无论左键单击还是右键单击都会触发胜利,所以都需要调用胜利的判断。右键点下的判断首先判断当前的插旗的数量是否与地图的雷数一致,如果一致继续判断是否与地雷列表完全相等。左键点下的判断是首先判断剩余的没有打开的泥土数量是否与地图的雷数一致,如果一致继续判断是否与地雷列表完全相等。
还有一件事就是赢了之后要干嘛,因为我不准备做排行榜,我现在的想法是计时器要停止,然后跳个框出来表示恭喜,并显示等级与胜利所用的时间。这个框下面有一个按钮,点击按钮‘再来一局’。就是把游戏初始化一下。今天先到这里。
今天是1月10日,今天很不舒服,一直咳嗽。今天修正掉了上周五说的那个BUG,做好了游戏失败后用错雷的图片来替代有旗子的泥土的功能。到此游戏失败的功能已经都做好了,今天还做了一个弹出窗口,用来表示游戏胜利了之后显示游戏的等级与时间。我还没有做游戏胜利的判断,今天的时间不多,还要分出时间去复习考试的东西。我本来是打算用一个窗口来做游戏胜利的提示的,小窗口都设置好了,但是当我做完chuangkou.quit的命令之后我发现所有的窗口都退出了,主窗口也退出了。这不是我想要的,本来想花时间找找原因,可是咳嗽让我一点心情都没有,就直接用了messagebox去做,这样的缺点是:那个按钮上的字就是确定,不能修改成‘再来一局’。我还没找到怎么设置这个按钮的命令。明天再说吧。希望明天能好一些。今天就这样吧,我现在要去买个冰淇淋,让我喉咙舒服一下。
今天是1月11日,今天我完成了右键的胜利判断,只差最后一步了,就是左键的胜利判断。因为每一块泥土都是作为一个Mud类的实例储存在一个二维列表当中的,所谓二维列表就是列表中的每一行也是一个列表。我掀掉泥土的时候我并没有删除这个实例,我依然让它存在并占有二维列表中的这个位置,这样后面用坐标调用的时候不会出错。但是我也遇到一个问题。当我做左键的胜利判断的时候我需要用双for循环去逐个读取每一块泥土,查看它们的打开标记,插旗标记的状态。如果我玩的是高级的话,最后几个格子会很麻烦。运算量会很大。我要换一种思路。当我生成地图的同时,我再生成一个一维的列表,当中的每个元素都是每块泥土坐标的字符串(‘m_x_y’)这种形式,当我掀掉一块泥土的时候,我就移除这个列表里的对应的坐标元素,最后我只要用一个for循环就可以做上面的事了,并且只需要查找几块泥土就够了。
方法我已经想好了,这个列表我就叫泥土集(muds_list),放在gongyong里面。它的元素的格式与插旗泥土(flag_muds)的元素格式是一样的,当泥土集等于地图雷数的时候,我可以用两个列表相减的方法获得未插旗泥土的集合(noflag_muds),用一个for循环来获取这些坐标的整数值,并去地雷列表里查找他们。要是完全吻合的话。那就是胜利了,否则就返回。
两个列表是不能直接相减的,对于我这种不重复值的列表,可以先转换成集合再相减,减完再转换成列表就可以了。具体可以见下面这个帖子
python 2个列表相减的2种写法
今天就到这里,剩下的工作我明天再做吧。
今天是1月12日,我完成了。今天先是把菜单栏中的帮助文件那个命令完善了一下,其实就是点击帮助打开一个txt文件。但还是花了我一点时间。第二件事就是把左键的判断做完了。我发现我昨天想太复杂了。其实当我左键点开泥土后,剩下的泥土(无论插没插旗)的总数能与地雷的总数一样,其实我就已经赢了。因为要是我没赢的话,那就应该已经输了。所以左键判断的命令最后变成了给剩余没有插旗的泥土插旗的命令了。插旗的动作我又是直接调用了右键的方法。所以等于是最后所有泥土都插上了旗子,然后是以右键胜利判断的方式判断我获胜的。在这里我先贴上我所有的代码:
一共分6个py文件:saolei.py是我的主程序,yunsuan.py是运算层,view.py是界面层,game_function是控制层,settings.py是一些游戏设置的东西,gongyong.py用来存放一些公共变量。
saolei.py
# _*_ coding : UTF-8 _*_
# @time: 2021/11/30 16:34
# @file: saolei.py
# @Author:yyh
# @Software:PyCharm
import tkinter as tk
import view
from settings import Settings
import gongyong
def run_game():
"""
这是扫雷游戏的主程序
"""
# 每当游戏开始的时候,先生成一个初级的地图,没有地图上的数字,就是完全用泥土覆盖的一个界面。
game_st = Settings() # 给设置的东西一个实例,其实就是给设置弄个短点好记点的名字。
root = tk.Tk() # 先创建窗口
gongyong.chuangkou = view.Chuangkou(root, game_st) # 用Chuangkou这个类给主窗口赋予属性,并给这个已赋予属性的窗口实例化为chuangkou
gongyong.chuangkou.update_chuangkou() # 用Chuangkou这个类里的方法更新窗口来给窗口画出来
root.mainloop() # 保持窗口循环
if __name__ == '__main__':
run_game()
yunsuan.py
# _*_ coding : UTF-8 _*_
# @time: 2021/11/17 13:18
# @file: yunsuan.py
# @Author:yyh
# @Software:PyCharm
import numpy as np
import gongyong
def autobulei(dengj, r, c, game_st):
"""
用随机算法自动布雷,布雷的算法必须避开第一次开土的坐标。
数字算法在布雷算法完成之后,用9表示雷,用数字表示周边有多少个9.
用0表示空土
:param dengj: 用来接收来自控制层的等级选择
:param r,c:是第一次开土时地图上的坐标位置, r表是行,c表示列,是从0开始数的。
:return: 返回的应该是自动布完雷的二维数组
"""
shuzu_dx = (game_st.dengji_bqd[dengj][0], game_st.dengji_bqd[dengj][1]) # 获取数组的大小
shuzu = np.zeros(shuzu_dx, dtype=int) # 创建二维数组
i = 0
dilei_s = []
while i < game_st.dengji_bqd[dengj][2]:
r_shuzu = np.random.randint(0, game_st.dengji_bqd[dengj][0]) # 随机某行
c_shuzu = np.random.randint(0, game_st.dengji_bqd[dengj][1]) # 随机某列,获得一个随机位置
if shuzu[r_shuzu][c_shuzu] == 9 or ((r_shuzu, c_shuzu) == (r, c)):
continue # 如果随机生成的位置凑巧与第一块土相同,或者已经布过雷了,那就回到循环开头重新随机
else: # 如果是空土
shuzu[r_shuzu][c_shuzu] += 9 # 这句应该直接等于9,一开始我用+=发现了自己没有检测是否已经布过雷的bug,所以我就保留了+
dilei_s.append((r_shuzu, c_shuzu))
i += 1
gongyong.L_ditu = shuzu # 给公用的雷地图赋值
gongyong.dilei_list = dilei_s # 给公用的地雷坐标的列表赋值
def autoshuzi(ditu, dilei_lis):
"""
这个函数跟在自动布雷后面,为雷附近的单元格填写数字,就叫自动数字
:param ditu:自动布完雷,但是还没有写雷周围的数字的数组
:param dilei_lis:获得布完雷之后的地雷的坐标列表
:return: 返回在雷周围已经写完数字的数组
"""
row = len(ditu)
col = len(ditu[0])
dt = ditu # 换个短一点的名字
for dilei in dilei_lis: # 在地雷位置的清单中循环
i, j = dilei[0], dilei[1] # 把位置坐标先赋值给两个名字短一点的变量
if dt[i][j] == 9: # 如果是雷,那么我们对雷周围的8个格子都加上1
for n in range(i - 1, i + 2):
for m in range(j - 1, j + 2): # 再用一个双for循环,对雷附近的3x3的区域遍历,每个格子+1
if n >= 0 and m >= 0 and row - n >= 1 and col - m >= 1 and dt[n][m] < 9:
dt[n][m] += 1 # 这句if是判断当前操作的元素是否在地图范围内,并且这个元素他不是雷,然后给这个单元格+1
else: # 这句else可以不写,但为了逻辑完整
continue
else: # 这句也是为了逻辑完整。另外,我是遍历整个数组中的雷,并不是遍历整个数组中每个位置,并且对每个位置统计周边雷数。
# 所以,只要不是雷我都会跳过,我认为这样执行的语句会比较少。
continue
gongyong.L_ditu = dt # 吧写过数字的地图在赋值回给公用里的那张地雷地图
view.py
# _*_ coding : UTF-8 _*_
# @time: 2021/11/30 11:53
# @file: view_0.1.py
# @Author:yyh
# @Software:PyCharm
import tkinter as tk
import tkinter.font as tkf
from os import startfile
import gongyong
import game_function as gf
class Chuangkou():
"""
创建一个类,用来‘做'主窗口,他不继承Tk,他是自定义的一个类,下面chuangkou那个变量是在主程序中Tk类的一个实例。送到这个类里来进行操作。
这个类就是操作主窗口中那个实例的类。简单点说,这个类不是窗口,这个类是用来设置窗口里的那些东西的类。
:param: chuangkou是主程序当中那个创建的Tk,他才是真正的窗口。
:param: dengji是传递参数,用来传递游戏等级,根据不同的等级做出不同大小的主窗口
:param: game_st 是游戏设置,一些设置参数传递过来
"""
def __init__(self, chuangkou, game_st, ):
# 先设置一些属性,传递参数或者字体,开始的时候
self.ck = chuangkou # 给类一个窗口名字,下面设置窗口内容的时候都用这个名字
self.gst = game_st # 把settings换个短一点的名字
# 下面这些内容我本来是放在设置里的,但是没有窗口不能是用tkf,所以我先放在chuangkou的类里面。
self.ck.ziti_b = tkf.Font(family='Arial', size=7, ) # b 表示button,按钮的字体
self.ck.ziti_l = tkf.Font(family='Arial', size=12, weight='bold', ) # l 表示label,标签的字体
self.ck.ziti_m = tkf.Font(family='宋体', size=12, ) # m 表示menu,菜单的字体。
self.ck.ziti_i = tkf.Font(family='Consolas', size=25, ) # i 表示informa是viewinfor中两个数字显示框的字体
# 为什么我要给这些都设置字体,因为我要存放他们的那些标签和按钮的高度宽度能被我控制,
self.menubar = MenuBar(self.ck.ziti_m, self, ) # 給菜单栏一个实例,菜单栏与等级无关,不会随着等级的变化而变化
self.viewinfor = tk.Frame(self.ck) # 实例化一个框架,这个框架用来存放上面的游戏信息,计时器,剩余雷数,emoj的开始按钮
self.viewgame = tk.Frame(self.ck) # 实例化另一个框架,这个框架用来存放游戏地图,泥土。
self.lei_label = tk.Label(self.viewinfor) # 给剩余雷数做一个标签的实例
self.time_label = tk.Label(self.viewinfor) # 给计时器也做一个标签的实例
self.leishu = Info(self.lei_label, self.ck.ziti_i, )
self.jishiqi = Info(self.time_label, self.ck.ziti_i)
self.emoj = tk.Button(self.viewinfor)
self.image = tk.PhotoImage(file=self.gst.emoj_pic)
# 下面是窗口上emoj按钮的属性,跟计时器和剩余雷数有点不同,我没有再单独设置一个Emoj的类。
# 因为只有他自己用,所以我直接在这里设置他的属性了
self.emoj['font'] = self.ck.ziti_b
self.emoj['image'] = self.image
self.emoj['bd'] = 3
self.emoj['height'] = 30
self.emoj['width'] = 30
self.emoj['relief'] = 'raised'
self.emoj['padx'] = 0
self.emoj['pady'] = 0
self.emoj['command'] = lambda: gf.init_game(self)
def update_chuangkou(self, dengji='chuj', ):
# 下面内容用来设置这个窗口,部分内容根据等级的变化要变化,放入一个函数中,dengji是一个带默认值的形参,默认是初级
self.ck.title("扫雷") # 窗口的标题
self.ck.iconbitmap(self.gst.ico) # 标题前面的logo
self.ck.geometry(self.gst.gm_bqd[dengji]) # 根据等级画出窗口的大小
self.ck.config(menu=self.menubar) # 给出窗口画上菜单栏
self.ck.resizable(width=False, height=False) # 不允许手动调整窗口的大小
self.viewgame.destroy() # 把初始化游戏,把刚刚创建的框架先清除掉,这个操作可以同时清除框架里的东西。
self.viewgame = tk.Frame(self.ck) # 再创建一个框架
self.viewinfor['height'] = 50 # 游戏信息框高50
self.viewinfor['width'] = self.gst.dengji_bqd[dengji][1] * 26 + 6 # 游戏信息框宽与等级有关,窗口多宽框多宽
self.viewinfor['bd'] = 3 # 设置边框为3
self.viewinfor['relief'] = 'sunken' # 边框形式为下沉式
# self.viewinfor['contain'] = False # 默认值就是False,一开始我认为我要把东西放进去,要写True。后来明白了True表示框架做为容器
# 里面放其他的应用程序,并不是放部件的意思,放部件不要True,所以我又改为Fales,后来运行了发现,这行必须不能写,因为不能在Frame创建之后
# 再给contain赋值,必须在创建的时候就赋值,所以最后我就把这句给注释掉了
self.viewinfor.pack(padx=4, pady=0, anchor='w') # 布置信息框,距离左边留4个单位。不为什么就为了好看。
self.viewinfor.pack_propagate(0) # 这行代码的意思是固定frame的大小。如果不固定,frame里面放入东西后会改变frame的大小
self.viewgame['height'] = self.gst.dengji_bqd[dengji][0] * 25 + 6 # 游戏框高度与等级有关,行数有多少高度就要有多少
self.viewgame['width'] = self.gst.dengji_bqd[dengji][1] * 26 + 6 # 同信息框,
# 好像直接等于上面那个self.viewinfor['width']也可以,但这样思路不清晰。
self.viewgame['bd'] = 3 # 同信息框一样设置为3,保持一致
self.viewgame['bg'] = '#808080' # 背景色与数字8一样是一种浅灰色,作为每个格子的边框颜色
self.viewgame['relief'] = 'sunken' # 同信息框一样,保持一致
# self.viewgame['contain'] = False # 同上面一样。
self.viewgame.pack(padx=4, pady=4, anchor='w') # 布置游戏框,距离左边和上边那个框都留4个单位
self.leishu.update_leishu(self.gst.dengji_bqd[dengji][2])
self.jishiqi.update_jishiqi()
self.emoj.pack()
self.update_muds(dengji)
def update_muds(self, dengji):
"""
用来给游戏框内画上泥土,先别管泥土下有没有雷
:param : dengji 是游戏的等级,传递过来确定泥土要创建多少块
:return: 对实例的一个方法,不存在返回
"""
for i in range(self.gst.dengji_bqd[dengji][0]):
gongyong.muds.append([]) # 看有多少行,就在泥土列表里在放多少个子列表
for j in range(self.gst.dengji_bqd[dengji][1]): # 给列表中每一块泥土添加上去
zuobiao = 'm_{}_{}'.format(i, j)
gongyong.muds[i].append(Mud( # 循环生成泥土
self.viewgame,
self.ck,
zuobiao, # 在每一块泥土中放置一个number表示这块泥土的编号,否则每块泥土自己不知道自己排老几
self.gst, # 传递settings
)
)
gongyong.muds_list.append(zuobiao)
gongyong.muds[i][j].bind("<Button-1>", gongyong.muds[i][j].zuojian) # 绑定鼠标左键单击命令
gongyong.muds[i][j].bind("<Button-3>", gongyong.muds[i][j].youjian) # 绑定鼠标右键单击命令
gongyong.muds[i][j].place(x=j * 26, y=i * 25) # 把泥土码好(我不是码农,我是个叠码仔)
# place这个命令中()里的x表示的是这一行的第几个,y表示的是第几行,这里的含义与我在运算层中是反的。我要去把运算层中的x,y修改成r和c
class MenuBar(tk.Menu):
"""
主窗口上的菜单是单独一个类,用来做所有的命令。菜单栏中的内容与等级无关,不会随着等级的变化而变化,所以这个类直接继承了Menu的类
"""
def __init__(self, ziti, chuangkou, ):
self.ziti = ziti
# 我设置了菜单的字体,但是我发现窗口菜单栏的字体不受我控制,是系统默认的,只有下拉出来的菜单字体才是受我的控制的。
self.chuangkou = chuangkou
self.dj = tk.StringVar() # 这个实例化是给菜单栏中的radio按钮使用的,如果选中某个按钮,同一组的其他按钮要关闭。
self.dj.set('chuj')
super(MenuBar, self).__init__() # 下面这些内容必须要写在super的下面,在这个super的属性括号里写font是不受控制的
self.gamemenu = tk.Menu(self, tearoff=False, font=self.ziti) # 做一个游戏子菜单的实例,他将来还有子菜单
self.gamemenu.add_command(label='重新开始',
command=self.initgame
) # 在"游戏"子菜单中添加‘重新开始’的命令,这个是初始化一下游戏的意思
self.gamemenu.add_separator() # 分割线
self.gamemenu.add_radiobutton(label='初级', variable=self.dj, value='chuj',
command=lambda: gf.update_dj(self.dj.get(), self.chuangkou)) # 多选一按钮,初级
self.gamemenu.add_radiobutton(label='中级', variable=self.dj, value='zhongj',
command=lambda: gf.update_dj(self.dj.get(), self.chuangkou)) # 多选一按钮,
self.gamemenu.add_radiobutton(label='高级', variable=self.dj, value='gaoj',
command=lambda: gf.update_dj(self.dj.get(), self.chuangkou)) # 多选一按钮,
self.gamemenu.add_separator() # 分割线
self.gamemenu.add_command(label='退出游戏', command=chuangkou.ck.quit) # 退出游戏的命令
self.helpmenu = tk.Menu(self, tearoff=False, font=self.ziti) # 做一个帮助子菜单的实例,他将来还有子菜单
self.helpmenu.add_command(label='游戏说明', command=self.openhelp) # 在"帮助"子菜单中添加‘游戏说明’命令
self.add_cascade(label='游戏', menu=self.gamemenu) # 在菜单栏中添加游戏按钮,这个按钮调出"游戏"子菜单
self.add_cascade(label='帮助', menu=self.helpmenu) # 在菜单栏中添加帮助按钮,这个按钮调出"帮助"子菜单
def initgame(self):
"""
重新开始,就是把窗口按当前等级重新初始化一下,这里跟emoj不一样的地方在与我是在菜单栏的类里面做这件事。
我也可以在上面菜单栏中用command = lambda …… 的命令,但我还是想直接调用一个方法,体现一下command后面跟的函数、方法是不能加()的
"""
gf.init_game(self.chuangkou)
def openhelp(self):
startfile('help.txt') # 用默认程序打开一个文件。这方法我百度来的。
class Info():
"""
给信息栏内的两个数字显示框做一类,继承标签类。用来规范标签内的格式
"""
def __init__(self, infor, ziti, ):
self.infor = infor
self.infor['font'] = ziti
self.infor['height'] = 1
self.infor['width'] = 3
self.infor['bd'] = 0 # 无边框
self.infor['bg'] = 'black' # 黑底
self.infor['fg'] = 'red' # 红字
self.infor['relief'] = 'solid' # 这句表示边框为实线形式,但我上面已经写无边框了,呵呵呵,这句应该可以省略了。
self.infor['padx'] = 0 # 无横向内边距
self.infor['pady'] = 0 # 无纵向内边距
def update_leishu(self, text=0):
self.infor['text'] = "{:0>3}".format(text)
gongyong.shenyuleishu = text # 把从等级中获得的雷数赋值给一个公用变量
# 这个format的用法不常用,:后面是格式要求,>是右对齐的意思,3表示用三个字符来显示内容,0表示不足三个字符的话用0来补位。
# 另外,<是左对齐,^是居中对齐。这个可以具体看python的官方说明文档
self.infor.pack(side='right') # 放在信息框的右边, 问,是不是也可以写成 self.infor.pack['side']='right'
def update_jishiqi(self, text=0):
self.infor['text'] = "{:0>3}".format(text)
self.infor.pack(side='left')
class Mud(tk.Button):
"""
写一个泥土的类,继承按钮,每一块泥土都是来自这个类的
:param: number表示这块泥土在窗口中的坐标位置,是一个字符串
shuzi 表示的是这个位置上地图上的数字,首次生成泥土的时候都写成了0
st 是传递过来的Settings
"""
def __init__(self, jiemian, chuangkou, number, game_st):
self.jiemian = jiemian # 向下层传递,也是Button部件必须的一个参数
self.chuangkou = chuangkou
self.number = number # 表示这块泥土在窗口中的坐标位置,是一个字符串
self.st = game_st # 是传递过来的Settings
self.qizi = tk.PhotoImage(file=self.st.flag) # 从设置里调出用来做旗子的图片
self.ziti = chuangkou.ziti_b # 字体是窗口那个类里定义的的ziti_b字体
self.kai = False # 做一个布尔值,用来判断这块泥土是否已经被挖开
self.flag = False # 用一个布尔值来表示这块泥土是不是做插旗的泥土,其实插旗的泥土是另外覆盖在现有的泥土上面的。
super(Mud, self).__init__(
self.jiemian,
font=self.ziti, # 字体
height=1, width=2, # 大小
bd=3, # 边框粗细
bg='#e0e0e0', # 给按钮设置一个背景色,让按钮与地图的颜色稍微有点不同
relief='raised', # 边框形式
padx=2, pady=2, # 内边距
)
def zuojian(self, elf=True):
"""
左键点击泥土要做的事
"""
if not gongyong.game_over and elf:
# 如果你还没有gameover,那么就调用控制层中挖土(watu)的函数 # 本来这个elf是用来做双击右键的一个效果的,没做出来,先放着吧。
gf.watu(self.jiemian, self.chuangkou.ziti_l, self.number, self.st)
# 调用游戏功能中的挖图的函数,把这块土的坐标传给他,把游戏设置的实例也传过去
# 当时码放泥土时生成的number就为了现在可以作为坐标使用
# 调用游戏功能中的挖图的函数,把这块土的坐标传给他,把游戏设置的实例也传过去.在那个函数中顺便也把地板给画了。
# 所以,要把界面也传过去,字体也传过去
# print(self) # 用来监视当前泥土的储存编号。正式程序注释掉这句。
else: # 如果你已经输了,那我就啥都不干了。
pass
if len(gongyong.muds_list) == self.st.dengji_bqd[gongyong.dengji][2]:
# 如果挖完土,剩下的泥土能与地图的雷数相同,你就已经赢了
gf.win_judge_1() # 调用这个函数给所有未开泥土插上旗子。
else:
pass
def youjian(self, elf):
if not gongyong.game_over and gongyong.game_state: # 如果你还没有gameover,并且游戏已经开始。那么
# gf.flag(self.number, self.st) # 调用控制层的插旗函数 # 不再调用,原因见控制层插旗函数的说明
if not self.flag: # 如果没有插过旗子
self.config(image=self.qizi, height=17, width=18, ) # 重新定义这块泥土的一些属性
self.flag = True # 把旗子的标记改为开
gongyong.flag_muds.append(self.number) # 偷个懒,直接把左边存到插旗泥土的列表里去
gf.update_shenyuleishu(-1) # 插面期就给剩余雷数-1
else: # 如果插过旗子了,右键就是拔旗。这个游戏中没有?这个不确定状态。
self.config(image='', height=1, width=2, ) # 再重新定义这块泥土的属性,把他回复到之前的状态
self.flag = False # 把旗子的标记改为开
gongyong.flag_muds.remove(self.number) # 把这块泥土从插过的列表里给删了
gf.update_shenyuleishu(1) # 拔面旗就给剩余雷数+1
else: # 如果你已经输了,或者说游戏还没开始。(与的反义是或,没错)
pass # 那右键就啥都不干了。
class Ditu(tk.Label):
def __init__(self, jiemian, ziti, zuobiao, st, text='', image="", height=1, width=2):
self.jiemian = jiemian
self.ziti = ziti
self.zuobiao = zuobiao
self.st = st
self.text = text
self.image = image
self.height = height
self.width = width
super(Ditu, self).__init__(
self.jiemian,
text=self.text,
image=self.image,
font=self.ziti,
height=self.height, width=self.width,
bd=0, # 原来边框宽度是1.但是标签边框颜色是黑色的,我嫌不好看,我又改不了,
# 所以我干脆取消了标签边框,给游戏框架(viewgame)设置底色,布置的时候按原来bd=1的边框空开来布局,直接显示框架底色作为边框,
relief='solid',
padx=2, pady=2
)
def you_shuangji(self, elf): # 右键双击地板的功能,直接去调用控制层里的开图的函数
gf.kaitu(self.jiemian, self.ziti, self.zuobiao, self.st, self.text)
game_function.py
# _*_ coding : UTF-8 _*_
# @time: 2021/12/20 13:43
# @file: game_function.py
# @Author:yyh
# @Software:PyCharm
import tkinter as tk
import tkinter.messagebox
import view
import gongyong
import yunsuan
def run_counter(digit): # digit是标签类的实例
"""这段是计时器的代码"""
def counting():
digit.config(text="{:0>3}".format(gongyong.counter)) # .config()的效果就是把括号里的内容加到标签,这里还需要刷一次格式
if gongyong.counter < 999 and gongyong.game_state: # 如顾用时小于999秒,并且游戏还没有结束的话
gongyong.counter += 1 # 计时器加1
digit.after(1000, counting) # 在1000毫秒后执行counting()函数,即循环执行counting
else: # 如果用时大于999秒,就不在往上数了,留点面子。
pass
counting()
def update_shenyuleishu(c):
if gongyong.shenyuleishu > -99: # 如果剩余雷数大于-99的话,
gongyong.shenyuleishu += c # 加上变化量
gongyong.chuangkou.lei_label.config(text="{:0>3}".format(gongyong.shenyuleishu)) # 给那个类重新赋值一下属性,再刷一次格式
if gongyong.shenyuleishu == 0: # 如果剩余雷数等于0,
win_judge_2() # 调用一下胜利判断函数
else: # 如果不是0,无论正负,都说明没有完全正确标记出所有的雷
pass
else: # 如果已经-99了,那就pass不执行了
pass # 为什么是-99,三位数显从-99到999,大地图16*30只有480格子,不会出现999.但会出现-99
def update_dj(dj, chuangkou):
# 用一个函数来吧公用的等级参数修改掉
gongyong.dengji = dj
# 通常修改了等级之后要重新开始游戏,所以下面接初始化游戏。
init_game(chuangkou)
def init_game(chuangkou, ):
"""
初始化游戏,就是根据等级画出窗口及里面的东西。
:return:
"""
gongyong.muds_list = [] # 将储存泥土坐标的列表情况
gongyong.L_ditu = [] # 将雷的地图清空
gongyong.dilei_list = [] # 将雷的坐标列表清空
gongyong.flag_muds = [] # 把插旗泥土的列表也清空
gongyong.game_state = False # 将游戏状态改为未开始
gongyong.game_over = False # 将gameover改为Fales。
gongyong.counter = 0 # 初始化一下计时器
for item in gongyong.dibanliebiao: # 用for语句清空掉地板列表里的所有地板
item.destroy()
if gongyong.muds: # 如果泥土列表不空的话
for i in range(len(gongyong.muds)): # 获取一下有几行
for item in gongyong.muds[i]:
item.destroy()
else: # 如果泥土列表是空的话,那啥都不用管
pass # 为啥跟上面地板列表比要增加一个if语句的判断,因为泥土是二维列表。不判断的话第一个for就无法执行。
gongyong.muds = [] # 而且作为二维数组,必须要清空掉,如果没有这句会报错的。
chuangkou.update_chuangkou(gongyong.dengji) # 初始化主窗口
def watu(jiemian, ziti, zuobiao, st):
'''
用于鼠标左键泥土的命令,顺便也把地板给画了
:return:
'''
# 先把获得的坐标拆解开来,行坐标是i, 列坐标是j
i = int(zuobiao.split('_')[1])
j = int(zuobiao.split('_')[2])
if gongyong.muds[i][j].flag or gongyong.muds[i][j].kai:
# 如果这块泥土已经插过旗子了,或者已经打开了的话。因为双击开土的命令,存在这种可能,已经在递归中把泥土打开了,但是双击开土的时候又要开它一次。
return # 那就返回,跳出这个函数,不再继续挖了
else: # 如果没有插旗
pass # 那就继续
gongyong.muds[i][j].destroy() # 先把这块泥土删了
gongyong.muds_list.remove(zuobiao) # 从泥土坐标列表里把这块土的坐标删掉
gongyong.muds[i][j].kai = True # 把泥土打开的标记改为开
if gongyong.game_state: # 如果游戏状态已经开始,
shuzi = gongyong.L_ditu[i][j] # 获取地图中的数字
else: # 如果游戏没有开始,启动开始游戏。画地图并且获取地图中的数字
shuzi = start_game(i, j, st)
# 到这里地图肯定已经生成了,下面两行获取一下地图的行数与列数。我没有去等级表驱动里读,而是直接获取实际已生成的地图的大小
row = len(gongyong.L_ditu) # 获取一下地图的行数
col = len(gongyong.L_ditu[0]) # 获取一下地图的列数
if shuzi == 9: # 如果数字是9,说明是雷。那就调用游戏失败的函数
game_over(jiemian, ziti, i, j, st) # game over
return # 跳出这个函数,不再执行后面的语句
elif shuzi == 0: # 如果数字是0,就
text = '' # 给text设定为空字符
# 下面的双for循环是用来获取0周边几个格子(最少3个,最多8个)的坐标,然后开土。
for r in range(i - 1, i + 2): # 这里跟运算层给9周边写数字的代码一样,用双for循环来获取周边有效单元格的坐标
for c in range(j - 1, j + 2):
if r >= 0 and c >= 0 and row - r >= 1 and col - c >= 1 and (r, c) != (i, j):
# 上面这句的意思就是这个当前循环到的格子是在周边,并且也不是它自身。
ls_zb = 'm_{}_{}'.format(r, c) # 创建一个临时坐标传递给递归的自己。
watu(jiemian, ziti, ls_zb, st) # 递归调用自己挖土,换成地板
else:
continue
else: # 如果是其他数字,就把数字类型转换成字符类型,后面画地板要字符类型
text = str(shuzi)
diban = view.Ditu(jiemian, ziti, zuobiao, st, text) # 生成一小块地板,实例化Ditu的类,画地板。
gongyong.dibanliebiao.append(diban) # 把它存到公用的地板列表里,方便重置的时候清空掉
gongyong.dibanliebiao[-1].bind('<Double-Button-3>', gongyong.dibanliebiao[-1].you_shuangji)
diban.place(x=j * 26 + 1, y=i * 25 + 1) # 布局到位,
# 注意,place方法中x表示的是横向位置,y表示的是纵向位置,所以x等于的是列坐标,y等于的是行坐标。
# 曾经我想用grid来布局,但是我发现grid布局的话是一个整体,这里用一土换一图的方式更合适
# 另外,这里布局的时候+1,是因为原来地板是有边框的,我嫌边框颜色不好看,我又改不了边框颜色,于是就去除边框了,
# 布置的时候空开原来边框的宽度,即1. 这样会直接露出viewgame的背景色,框架的背景色我能设置
diban.config(fg=st.yanse_bqd[shuzi]) # 根据数字涂不同的颜色
def flag(zuobiao, st):
'''
用于单击鼠标右键的命令
我本来是想在控制层中来实现这个功能的,但是run后不插旗,debug才会插旗,我把他挪到界面层来做,一样。直到我把调用
旗子的图片那句语句放到上面的Mud类的__init__中后,这个问题才解决。后来想想,插旗这事也是界面层的事,就不打算再放到控制层里去了。
但是我是明白了控制层要怎么修改了,所以我还是保留了控制层中flag这个函数没有删除
'''
# 先把获得的坐标拆解开来,行坐标是i, 列坐标是j
i = int(zuobiao.split('_')[1])
j = int(zuobiao.split('_')[2])
if not gongyong.muds[i][j].flag: # 如果这块土没有被插过旗子的话。
gongyong.muds[i][j].config(image=gongyong.muds[i][j].qizi, height=17, width=18, ) # 重新定义这块泥土的一些属性
gongyong.muds[i][j].flag = True # 把旗子的标记改为开
print(gongyong.muds[i][j], "插旗")
else: # 如果这块泥土已经被插过旗子了。
print(gongyong.muds[i][j], '拔旗')
gongyong.muds[i][j].flag = False # 先把原来泥土的旗子的标记改为关
gongyong.muds[i][j].config(image='', height=1, width=2, )
def kaitu(jiemian, ziti, zuobiao, st, text):
'''
用于鼠标右键双击数字,打开周围8格未插旗的泥土,前提条件是插旗数与数字必须相同。
这里与windows当中的稍微有所不同。windouw当中是左右键同时按下。
:return:
'''
flags = 0 # 这个变量用来统计周围几个格子有几面旗子
wa = [] # 这个列表用来储存需要被挖的图的坐标
i = int(zuobiao.split('_')[1])
j = int(zuobiao.split('_')[2])
row = len(gongyong.L_ditu) # 获取一下地图的行数
col = len(gongyong.L_ditu[0]) # 获取一下地图的列数
for r in range(i - 1, i + 2): # 这段双for循环是直接复制了上面的watu的那段
for c in range(j - 1, j + 2):
if r >= 0 and c >= 0 and row - r >= 1 and col - c >= 1 and (r, c) != (i, j):
if gongyong.muds[r][c].flag: # 如果插旗的话,
flags += 1
elif not gongyong.muds[r][c].kai: # 如果没有插旗的话,看看这块土有没有被打开过
wa.append('m_{}_{}'.format(r, c)) # 没有挖过的话,把他的坐标存到一个列表里去。
else: # 如果是被挖过了,那就没什么事了
pass
else:
continue
if not wa: # 如果那个wa的列表是空的话,那就是瞎点的,直接返回了。
return
if flags == int(text): # 如果周围的旗子数等于地板上的数字的话
for item in wa: # 在wa这个列表中循环去挖土。
if not gongyong.game_over: # 在正式开挖挖之前再对gameover判断一下,只有还没gameover我才挖
watu(jiemian, ziti, item, st) # 挖土
else: # 要是已经gameover了,那就不用再继续下去了
break # 直接跳出循环了
else: # 如果周围的旗子数不等于地板上的数字的话,
pass # 本来想用下面两句做一个按钮被按下但没有动作的效果,但是没有成功。
# for item in wa: # 用这个循环把周围没有挖过的泥土做一个按下去但没有打开的效果,注意,这里elf用了false
# gongyong.muds[int(item.split('_')[1])][int(item.split('_')[2])].zuojian(elf=False)
# print('鼠标右键双击', zuobiao) # 用来监视双击的是哪块地板。正式程序注释掉这句。
def start_game(r, c, game_st):
'''
开始游戏,点击下第一块泥土之后,要开始游戏,运算背后的地图,游戏状态要修改为开始状态
:return: 要返回第一块泥土的坐标。
'''
gongyong.game_state = True # 游戏开始了,把游戏状态修改一下
run_counter(gongyong.chuangkou.time_label) # 启动计时器
yunsuan.autobulei(gongyong.dengji, r, c, game_st) # 生成大小与等级相符的地图并随机雷的位置
# print('m_{}_{}'.format(r, c)) # 第一块泥土的坐标。 正式程序注释掉这句。
yunsuan.autoshuzi(gongyong.L_ditu, gongyong.dilei_list) # 给雷周围写上数字,并且把刚刚的地图更新一下。
print(gongyong.L_ditu) # 打印一下整个地图。正式程序注释掉这句。
return gongyong.L_ditu[r][c] # 把第一块土里是数字返回回去
def win_judge_1():
"""
本来这个按钮是用来给左键做判断是否胜利的,后来发现能达到这一步其实已经是胜利了,这个函数就用来给没有插旗的泥土都插上旗子。
"""
# 先获取一下没有插旗的泥土的坐标列表,因为列表不能相减,所以借用一下集合来做减法,如果列表内容有重复,就不可以这么弄了。
noflag_muds = list(set(gongyong.muds_list) - set(gongyong.flag_muds))
for zb in noflag_muds: # 给这些泥土逐个触发一下右键的动作
r, c = int(zb.split('_')[1]), int(zb.split('_')[2]) # 获取其中的坐标值,r,c分别
gongyong.muds[r][c].youjian(0) # 调用右键的方法,这里因为必须要有一个实参,所以随便打了个0
def win_judge_2():
"""
右键胜利判断,当按下右键时,如果当前标记的雷数与地图雷数相同则触发这个判断,
判断所有标记的泥土的坐标是否与所有雷的坐标相同,如相同则触发游戏胜利函数
"""
linshi = gongyong.flag_muds[:] # 把当前的插旗泥土先复制到一个临时的列表里
while linshi: # 当这个列表不为空的时候
item = linshi.pop() # 逐个弹出这个列表里的元素
r, c = int(item.split('_')[1]), int(item.split('_')[2]) # 获取其中的坐标值,r,c分别为行与列
if (r, c) in gongyong.dilei_list: # 判断这个(r,c)元祖是否在地雷列表里
continue # 要是在,那就继续下一个循环
else: # 要是不在,那就说明有至少有一个旗子插错了,直接返回函数,不执行while后面的语句
return # 返回函数
game_win() # 如果while语句全部执行完,证明所有的旗子都插对了,就调取游戏胜利的函数
def game_win():
"""做游戏胜利的一些事件"""
bqd = { # 我之前偷懒总是用拼音来代替等级的中文,这里放个表驱动,用来对应拼音和中文
'chuj': "初级",
'zhongj': "中级",
'gaoj': "高级",
}
gongyong.game_state = False # 先把游戏状态调停,暂停之后时间会暂停。
# 下面是个简单的文本框,用来显示这个等级多少时间完成
tk.messagebox.showinfo(title='恭喜你找出了所有地雷',
message="恭喜你!\n用时 {} 秒完成 {} \n再来一局"
.format(gongyong.counter, bqd[gongyong.dengji]),
)
init_game(gongyong.chuangkou) # 这行是点击了提示框的确定后执行的命令,就是把游戏初始化一下。
def game_over(jiemian, ziti, r, c, st):
"""
这个函数用来做游戏失败的一些事件
:param: jiemian是传递过来的viewgame游戏框,因为炸雷图片也是要用Ditu类实例后放到这个游戏框里的
:param: r,c 是传递过来触发游戏失败的那个雷的坐标,就是哪个雷触发了
:param: st 是传递过来的游戏设置,用来获取游戏设置当中的雷与炸雷的图片
:return:无返回
"""
# print('game over') # 正式程序注释掉这句。
gongyong.game_state = False # 先把游戏状态调停
gongyong.game_over = True # gameove状态改为真,因为你是真的输了,哈哈
lei_image = tk.PhotoImage(file=st.lei_pic) # 从设置里调出用来做雷的图片,必须用photoimage实例,因为下面标签的image属性只认它。
zhalei_image = tk.PhotoImage(file=st.zhalei_pic) # 从设置里调出用来做踩到炸了的那个雷的图片。
cuolei_image = tk.PhotoImage(file=st.cuolei_pic) # 从设置里调出用来做标记错了的了的那个图片。
zuobiao = 'm_{}_{}'.format(r, c)
for item in gongyong.dilei_list: # 这里在地雷列表里循环
if item == (r, c): # 如果是踩到的那颗雷
diban = view.Ditu(jiemian, ziti, zuobiao, st, image=zhalei_image, height=23, width=24) # 用炸雷图片
else:
diban = view.Ditu(jiemian, ziti, zuobiao, st, image=lei_image, height=23, width=24) # 用普通雷图片
# 上面两句中的height与width很有意思,好像如果标签是图片的话,这两个值就直接是标签的大小了。
if not gongyong.muds[item[0]][item[1]].flag: # 如果这块泥土没有插过旗的,那么执行下面这些操作
gongyong.dibanliebiao.append(diban) # 把有地雷的地板也存到那个列表里。
gongyong.muds[item[0]][item[1]].destroy() # 这句话是先把这块泥土给掀了。
diban.place(x=item[1] * 26 + 1, y=item[0] * 25 + 1) # 布局上有地雷的地板
# print(diban) # 用来监视当前这块地板的储存编号。正式程序注释掉这句。
else: # 如果是插过旗子的,那就不管他。
pass
# print(gongyong.flag_muds) # 用来监视一下插旗的泥土有几块,正式程序注释掉这句。
for item in gongyong.flag_muds:
r, c = int(item.split('_')[1]), int(item.split('_')[2])
if (r, c) not in gongyong.dilei_list: # 如果插过旗的那块泥土不在地雷列表里的话
# 要做一块插错雷的图片来替换掉这块插过旗的泥土
diban = view.Ditu(jiemian, ziti, zuobiao, st, image=cuolei_image, height=23, width=24)
gongyong.dibanliebiao.append(diban) # 也放到那个地板列表里面去
gongyong.muds[r][c].destroy() # 先把这块无辜的泥土掀了
diban.place(x=c * 26 + 1, y=r * 25 + 1) # 替换上刚刚的错雷图片
# print(diban) # 用来监视当前这块地板的储存编号。正式程序注释掉这句。
else: # 如果它在列表里,那就不要做什么了,因为已经做对了。
pass
def emoj_test():
"""
用来做一些测试性的命令,给它附在emoj按钮上,等游戏开发完成之后,再把emoj命令改回原来的初始化游戏的命令
"""
pass
settings.py
# _*_ coding : UTF-8 _*_
# @time: 2021/12/8 11:24
# @file: settings.py
# @Author:yyh
# @Software:PyCharm
class Settings():
def __init__(self):
self.dengji_bqd = {
'chuj': [9, 9, 10],
'zhongj': [16, 16, 40],
'gaoj': [16, 30, 99],
}
self.yanse_bqd = {
0: '#000000', # 0 应该是空的,但是会搜索到0,表驱动会出错
1: '#0000FF', # 从1到8是不同的数字显示的颜色,颜色的十六进制表示值
2: '#008000',
3: '#FF0000',
4: '#000080',
5: '#933A3A',
6: '#008080',
7: '#000000',
8: '#808080',
9: '#000000', # 9 应该是个雷的图案,先放一个颜色否则表驱动会出错
}
self.gm_bqd = { # gm是一个表驱动,用来定义主窗口的大小。
'chuj': '248x289', # 比框架稍微大一点,两边各留4的边框,考虑frame的bd=3,所以还要加6
# 26*9+6+8=248, 上边还要留50的信息框,再加20的菜单栏的高度,25*9+6+8+50+20=309
'zhongj': '430x464', # w=26*16+6+8=430, h=25*16+6+8+50+20=484
'gaoj': '794x464', # w=26*30+6+8=794, h=25*16+6+8+50+20=484
# 上面高度的公式中额外+20是为了菜单的高度,这是我用眼睛“试出来”的。
# 如果没有菜单,可以不用加20.但是如果下面的resizable设置成False的话,
# 即表示窗口大小不可调整,又可以不用+20了。我实际使用是要求固定窗口大小的,所以最后用的值没有+20
}
self.ico = 'image\saolei.ico' # 用来做窗口左上角的那个logo的图片,是个ico文件
self.emoj_pic = 'image\emoji.gif' # 中间那个emoj按钮上的emoj图片
self.lei_pic = 'image\lei.gif' # 正常地雷的图片
self.zhalei_pic = 'image\lei_2.gif' # 踩炸了的炸雷的图片
self.cuolei_pic = 'image\lei_3.gif' # 标记错了的错雷图片
self.flag = 'image\qi.gif' # 标记泥土的旗子的图片
self.dengji = 'chuj' # 存在settings里的等级
gongyong.py
# _*_ coding : UTF-8 _*_
# @time: 2021/11/17 14:07
# @file: gongyong.py
# @Author:yyh
# @Software:PyCharm
import time
'''
这个文件中放一些公用的作为全局的变量,函数等
'''
chuangkou = ''
root2 = ''
win_ck = ''
dengji = 'chuj'
counter = 0 # 这就是一个计数器的变量,用来给窗口上的时间用
shenyuleishu = 0
shenyumuds = 0
muds_list = [] # 储存泥土坐标的列表,这个列表中的元素格式与厦门插旗泥土(flag_muds)当中的一样。是码放泥土的时候存入所有泥土的坐标
L_ditu = [] # 数字地图
dilei_list = [] # 地雷列表
flag_muds = [] # 用来存放所有插过旗子的泥土,如果拔旗了,要从这个列表把他删除。
dibanliebiao = [] # 用来存放生成的所有地板,当重置游戏的时候,这个列表要for语句destroy掉
muds = [] # 用来存放生成的所有泥土。当重置游戏的时候,这个列表要用双for循环来destroy掉里面的所有东西,并且再清空。
game_state = False # 这里放一个游戏状态,如果游戏还没有开始,False,如果点开了第一块土了,就是Ture
game_over = False # 这里再放一个游戏失败的状态,如果没有踩到雷,那就是False。如果踩到了就是True,表示游戏‘真’的结束了。
# 其实游戏有三种状态,(a) 初始状态,还没有点开任何泥土.(b) 进行状态,点开了泥土,正在游戏。(c)失败状态,即踩到雷了。但还没有重新开始。
# 本来想用一个值来表示三种状态的,后来想想还是用两个布尔值来做。从逻辑上来说更合适一些。因为很多语句都是用的if-else语句。
def log(*args, **kwargs):
# time.time() 返回 unix time
# 如何把 unix time 转换为普通人类可以看懂的格式呢?
time_format = '%Y/%m/%d %H:%M:%S'
value = time.localtime(int(time.time()))
dt = time.strftime(time_format, value)
with open('log.txt', 'a', encoding='utf-8') as f:
print(dt, *args, **kwargs)
print(dt, *args, file=f, **kwargs)
总结
做个简单的总结,我是从11月17日开始的,接近2个月的时间。但是中间有休假,有出差,还有忙于正式的工作。谁让我是上班摸鱼做的呢!在公开日记之前,因为是私密的,所以只有我一个人浏览,这个私密浏览量是每日更新,截止到今天是19个浏览量。我一般有做东西,就会做记录。所以,我可以不要脸一点:其实我就做了19天。大概也就是一个月的工作日的样子吧。但我每天又不是全部时间在做,所以按工时算,最多只能算一半,就当自己做了100个小时吧。
在这里面,我花的时间最多的是在界面层,因为之前从来没有用过tk,连听都没有听说过。所以花了大量的时间在学习怎么使用tk上,本来还有很多的test.py文件,都是我用来尝试一些操作的。以前我玩扫雷玩得还是不错的,巅峰时高级可以80多秒。但真正深入了之后发现这个游戏里的逻辑性的东西还是很多的,自己基本上都是一边思考一边在做。缺少一份计划,缺少大纲。
另外,在做这个程序的时候,我有过几次大的修改,近似于将一大部分东西推倒重做的过程。这是我一个人设计,如果是多人合作的话,这样是非常不可取的,影响团队士气(假如我是这个统筹的人)。下次做这种小的项目的东西,要先做大纲。统筹要有一个非常强的逻辑策略,统筹自己控制公共变量的文件,每个成员尽量只与统筹有界面,与其他成员的界面要少。
接下来要做的事,先生成一个.exe文件给儿子玩儿去。然后优化自己的代码,我知道我的代码量有点大,可以有很大的优化的空间。然后自己测试。等觉得挺满意了之后,可以再上传一次。
更多推荐
我想自己写一个扫雷,用Python
发布评论