扫雷是一款相对简单好玩的小游戏,对于初学某种程序语言来说(特别是python),没有用一个项目来熟练自己所学的知识是很难过的去的。本次使用python语言去写一款居于python标准库模块tkinter的扫雷游戏,供python爱好者学习。

  1. 扫雷游戏规则与思路构建
  2. tkinter标准模块
  3. 扫雷游戏暗含那种像算法一样的东西
  4. 类与类的继承
  5. 背景图片显示与控件显示样式
  6. 自定义按钮
  7. 完整代码(直接看完整代码)

1.扫雷游戏规则与思路构建

扫雷的游戏规则简单来说就是点开所有背后不是雷的方块就通关了,每个被点开的方块都会有一个数字(除0外)表示以该方块为中心的九个宫格内地雷的个数,玩家可以通过每个方块的数字推测那些方块背后不是雷那些方块是雷,然后逐一点开非雷方块,完成游戏。扫雷有个默认规则,就是玩家点开的方块背后无数字,则表明以该方块的九个宫格都不可能会有雷,所有以该方块为中心的九个方块会自动打开并以本规则类推到被打开的方块.


从上图看,有红色辐条那块方块1数字,表明该方块九宫格内只有一个雷,因为这方块四周除了左上角一除没有被打开,其余地方都被打开了,所有我们很容易推出测左上角为雷,玩家可以把它标记为红旗。然后在上一个方块,有黄色标记的方块1,表明其九宫格内有一个雷,然而九宫格内已经有红旗标记了,所以可以推出?的方块不是雷,玩家可以把它点开,依此类推。

对于以上规则,在程序中我们应该如何处理呢?

首先,所表示的方块我们可以创建一个数组(python叫列表)来代表,假设每一个方块是一个对象,我们创建一个数组,数组保存上面的方块,形成一个地图map

创建一个继承了tkinter.Button按钮的特征类用于方块,该类还有一些公用成员

class Sweep(tkinter.Button):map = [] #地图w = 7#地图宽度h = 8#地图高度mine = 5 #地雷个数x0 = 30#地图相对于主窗口的起点y0 = 50count = 0#扫除计数state = 0#0play 1vectroy 2over 游戏状态mine_size = 40 #地图方块大小#数字0~9的字体颜色rgbfgs = [(255, 255,255),(9, 147, 62),(0, 187, 187),(240, 78, 0),(166, 19, 188) ,(185, 122, 87),(136, 0, 21),(163, 73, 164),(0, 0, 0),(0,0,0)]#方块状态颜色与背景图片bgs = [(128,128,128),(255,255,255),(0,255,0),(255,0,0),(255,200,0),(0,255,0),(163,73,164)]images = ['area.png','opened.png','flag.png','doubt.png','boom.png','sweeped.png','mistake.png']pic = []#创建地图def create_map(w=w,h=h,mine=mine,x0=x0,y0=x0, mine_size=mine_size,fgs=fgs,bgs=bgs,images=images):Sweep.w = wSweep.h = hSweep.mine = mineSweep.x0 = x0Sweep.y0 = y0Sweep.mine_size= mine_sizeSweep.fgs = fgsSweep.bgs = bgsSweep.images=imagesSweep.state = 0Sweep.count = 0#加载背景图片if not Sweep.pic :for filename in Sweep.images :image = get_image(filename,Sweep.mine_size,Sweep.mine_size)Sweep.pic.append(image)

Sweep类的公用成员是Sweep实例都可能用到的,它规定每一个方块的大小和地图大小,游戏状态等等一些公用变量。

2. tkinter标准模块

tkinter模块,是UI设计的保障,本次主要利用其中的按钮特性,还有菜单栏。

class Sweep(tkinter.Button):....def __init__(self, master=None, cnf={}, **kw):super().__init__(master, cnf, **kw)self.text = self['text']self.command = self['command']self.bind('', self.clicked)#绑定左点击,然后重写点击处理函数self.bind('', self.right_clicked)#绑定右点击self.r=0#按钮记录自身在map的位置 r,cself.c=0self.n=0#记录自身数字,9就是雷self.state=0 #0没有被打开 1已经被打开 2被标志 3被质疑 4打开是雷被爆炸 5被扫除 6标志错误self.update_style()self.__class__.map.append(self)def clicked(self, event):if self.__class__.state != 0 :#tkinter.messagebox.showinfo(title="游戏结束!",message = "游戏已经结束,请重新开始")returnif self.state == 1: return

菜单设置

root=tkinter.Tk()def del_menu(args):if args == "入门":Sweep.create_map(w=6,h=5,mine=1)elif args == "简单":Sweep.create_map(w=10,h=10,mine=15)elif args == "一般":Sweep.create_map(w=16,h=16,mine=40)elif args == "困难": Sweep.create_map(w=20,h=16,mine=60)elif args == "重新开始":passSweep.init_map(root) if __name__ == '__main__':root.title('扫雷')Sweep.create_map(w=6,h=5,mine=3)Sweep.init_map(root)menu_bar = tkinter.Menu(root)root.config(menu=menu_bar)game_menu = tkinter.Menu(menu_bar,tearoff=False)menu_bar.add_cascade(label="游戏",menu=game_menu)#使用lambda函数用来区分每个菜单点击传入的参数game_menu.add_command(label="入门",command=lambda: del_menu("入门"))game_menu.add_command(label="简单",command=lambda: del_menu("简单"))game_menu.add_command(label="一般",command=lambda: del_menu("一般"))game_menu.add_command(label="困难",command=lambda: del_menu("困难"))game_menu.add_command(label="重新开始",command=lambda: del_menu("重新开始"))root.iconbitmap('mine.ico')root.mainloop()

因为点击不同的菜单处理不同的事物,每个command都要重复写很麻烦,使用lambada,可以生成不同的匿名函数。
创建一个带参数del_menu(args),通过lambda传入不同参数,实际上是生成了不同的lambda函数,该方法只能适用参数是一个已知常量

3. 扫雷游戏暗含那种像算法一样的东西

创建扫雷地图之后,我们需要在地图里面随机埋雷,埋完雷之后,我们还需要根据雷的位置,在雷的九个宫格映射数字。

埋雷
根据地图的大小,随机产生0到地图大小范围的随机数保存到random_numbers列表中,个数为地雷的数目

random_numbers = random.sample(range(0, Sweep.w*Sweep.h), Sweep.mine)

因为这里我们默认每一个方块都编号了(r,c),它们的编号是0~Sweep.w*Sweep.h,而且不重复,所以在随机序号表的编号就是代表雷了

for i in range(Sweep.w*Sweep.h) :r = i // Sweep.w #i为按钮默认编号c = i % Sweep.w #通过编号计算出按钮在地图的行列位置n =9 if i in random_numbers else 0 #如何编号在随机列表中,则为地雷赋值为9,否则赋初值button = Sweep(root)#按钮创建时自身会加入到mapbutton.place(x = c*Sweep.mine_size+Sweep.x0,y = r*Sweep.mine_size+Sweep.y0 ,width = Sweep.mine_size,height = Sweep.mine_size)#方块的显示位置button.setPos(r=r,c=c,n=n)#设置方块在地图的行列位置和方块数字for mine in Sweep.map :#最后根据雷的位置给地图中方块更新数字映射mine.map_mine()

映射数字

地雷埋好了,我们要找到地雷的位置映射雷区,如下图(r,c)为地雷位置


因此在(r,c)四周的方块数字n都要加一(如果方块存在,而且不是雷)

def map_mine(self):if self.n != 9 : #如果n不是9,表明方块不是雷,不需要做映射returnfor mine inself.__class__.map :#变量地图中所有方块r = mine.rc = mine.cifmine == self or mine.n == 9 : #如果是自身或者映射方块是雷就跳过continue#依次找出四周方块if r==self.r and c==self.c+1 :mine.n += 1elif r==self.r and c==self.c-1:mine.n += 1elif r==self.r+1 and c==self.c+1:mine.n += 1elif r==self.r+1 and c==self.c-1:mine.n += 1elif r==self.r-1 and c==self.c+1:mine.n += 1elif r==self.r-1 and c==self.c-1:mine.n += 1elif r==self.r+1 and c==self.c:mine.n += 1elif r==self.r-1 and c==self.c:mine.n += 1

对于上面代码简单通俗易懂,但是效率不高代码重复,下面我们来做一些优化

def map_mine(self):if self.n != 9 :return#将(r,c)的邻居的位置都找出来 neighbors = [(self.r+i, self.c+j) for i in range(-1, 2) for j in range(-1, 2) if i != 0 or j != 0]for r, c in neighbors:for mine in self.__class__.map:if mine.r == r and mine.c == c and mine.n != 9:mine.n += 1

自动扫雷:我们知道扫雷默认规则就是打开一个没有数字的方块会自动打开其周围的方块,如果周围的方块也没有数字,就会类推下去,下面我们来实现这个规则

...#在方块打开是判断数字是否为0,如果为0就调用auto_sweep()if self.n == 0 :self.auto_sweep()self.vectory()return #以上一段代码是在clicked里面的...def auto_sweep(self):if self.state == 1 : returnself.state = 1self.update_style()self.__class__.count += 1if self.n != 0 :returnneighbors = [(self.r+i, self.c+j) for i in range(-1, 2) for j in range(-1, 2) if i != 0 or j != 0]for r, c in neighbors:for mine in self.__class__.map:if mine.r == r and mine.c == c and mine.n !=9 :mine.clicked(None)#自身点击实现自动打开 

4. 类与类的继承

本次设计的扫雷游戏,主要是用到按钮的点击,然后通过按钮判断自身的情况,去影响整个map.
#一个Sweep类继承tkinter.Buttonclass Sweep(tkinter.Button):map = [] #地图w,h,mine,bgs,images #地图大小背景图片地雷个数等等一些共有参数#创建地图公共函数def create_map(w=w,h=h,mine=mine,x0=x0,y0=x0, mine_size=mine_size,fgs=fgs,bgs=bgs,images=images):def init_map(root) :def reset_map():#实例化函数def __init__(self, master=None, cnf={}, **kw):#初始化,定义按钮方块一些大小状态变量等等,并把自身加入到mapdef clicked(self, event):#点击打开方块def right_clicked(self, event):#右键标记方块def setPos(self,r,c,n):#设置方块在map的位置和数字def map_mine(self):#如果方块是雷,则映射周围数字def update_style(self) :#根据方块状态显示样式def auto_sweep(self):#如果方块数字为零就会被调用#游戏结束和胜利def gameover(self) :def vectory(self) :

5. 背景图片显示与控件显示样式

update_style实现了根据方块的自身状态去显示不同的样式,其中images是保存在公共类的一个背景图片的路径,分别是’area.png’,‘opened.png’,‘flag.png’,‘doubt.png’,‘boom.png’,‘sweeped.png’,‘mistake.png’,表示方块的未打开,被打开,标准,疑问,爆炸,扫除,标记错误六种状态的图片路径,pic是加载了images图片对象列表,get_image是通过路径加载图片返回图片对象,如果图片路径不存在,则是None不显示背景图片。
fgs为0~8的数字字体颜色,bgs为默认的背景颜色(背景图片不存在情况下)

#通过路径获取图片def get_image(filename, w=None, h=None):try:im = tkinter.PhotoImage(file=filename)if w != None and h !=None :im = im.subsample(im.width() // w, im.height() // h)return imexcept:return None....#根据按钮自身的不同状态去显示按钮的样式def update_style(self) :text=str("")if self.n !=0 and self.n != 9 and self.state == 1:text=str(self.n)hex_fg = '#{:02x}{:02x}{:02x}'.format( self.__class__.fgs[self.n][0] , self.__class__.fgs[self.n][1], self.__class__.fgs[self.n][2])hex_bg = '#{:02x}{:02x}{:02x}'.format( self.__class__.bgs[self.state][0] , self.__class__.bgs[self.state][1], self.__class__.bgs[self.state][2])image = self.__class__.pic[self.state]self.image = imageif self.state == 2 : text = str("!")hex_fg = "red"elif self.state == 3 : text = str("" />)hex_fg = str("yellow")elif self.state == 6 : text = str("×")hex_fg = str("red")if image != None :hex_bg = Noneself.configure(bg=hex_bg,fg=hex_fg,image=image,text=text,compound=tkinter.CENTER)

compound=tkinter.CENTER这个是必须的配置的,否则背景就会挡住字体
self.image = image 应该的,先把image赋值给自身的image,否则背景配置无法显示。

有背景图片情况下

无背景图片情况下默认的背景颜色

修改一些有趣的图片

通过修改boom.png显示一个猪头,图片需要是一个长宽一样的,否则显示会有点问题。

6. 自定义按钮

通过自己自定义类继承按钮类重构了新的按钮,看以上代码

7. 完整代码

import tkinterimport tkinter.messageboximport random#通过路径获取图片def get_image(filename, w=None, h=None):try:im = tkinter.PhotoImage(file=filename)if w != None and h !=None :im = im.subsample(im.width() // w, im.height() // h)return imexcept:return Noneclass Sweep(tkinter.Button):map = [] #地图w = 7#地图宽度h = 8#地图高度mine = 5 #地雷个数x0 = 30#地图相对于主窗口的起点y0 = 50count = 0#扫除计数state = 0#0play 1vectroy 2over 游戏状态mine_size = 40 #地图方块大小#数字0~9的字体颜色rgbfgs = [(255, 255,255),(9, 147, 62),(0, 187, 187),(240, 78, 0),(166, 19, 188) ,(185, 122, 87),(136, 0, 21),(163, 73, 164),(0, 0, 0),(0,0,0)]#方块状态颜色与背景图片bgs = [(128,128,128),(255,255,255),(0,255,0),(255,0,0),(255,200,0),(0,255,0),(163,73,164)]images = ['area.png','opened.png','flag.png','doubt.png','boom.png','sweeped.png','mistake.png']pic = []#创建地图def create_map(w=w,h=h,mine=mine,x0=x0,y0=x0, mine_size=mine_size,fgs=fgs,bgs=bgs,images=images):Sweep.w = wSweep.h = hSweep.mine = mineSweep.x0 = x0Sweep.y0 = y0Sweep.mine_size= mine_sizeSweep.fgs = fgsSweep.bgs = bgsSweep.images=imagesSweep.state = 0Sweep.count = 0#加载背景图片if not Sweep.pic :for filename in Sweep.images :image = get_image(filename,Sweep.mine_size,Sweep.mine_size)Sweep.pic.append(image)def init_map(root) :size_str='{}x{}+400+80'.format(Sweep.w*Sweep.mine_size+80,Sweep.h*Sweep.mine_size+100)root.geometry(size_str)Sweep.state = 0Sweep.count = 0for button in Sweep.map :button.destroy()Sweep.map.clear()root.update()random_numbers = random.sample(range(0, Sweep.w*Sweep.h), Sweep.mine)for i in range(Sweep.w*Sweep.h) :r = i // Sweep.wc = i % Sweep.wn =9 if i in random_numbers else 0button = Sweep(root)button.place(x = c*Sweep.mine_size+Sweep.x0,y = r*Sweep.mine_size+Sweep.y0 ,width = Sweep.mine_size,height = Sweep.mine_size)button.setPos(r=r,c=c,n=n)for mine in Sweep.map :mine.map_mine() root.update()Sweep.reset_map()def reset_map():Sweep.state = 0for mine in Sweep.map:mine.state = 0mine.update_style()def __init__(self, master=None, cnf={}, **kw):super().__init__(master, cnf, **kw)self.text = self['text']self.command = self['command']self.bind('', self.clicked)self.bind('', self.right_clicked)self.r=0self.c=0self.n=0self.state=0 #0没有被打开 1已经被打开 2被标志 3被质疑 4打开是雷被爆炸 5被扫除 6标志错误self.update_style()self.__class__.map.append(self)def clicked(self, event):if self.__class__.state != 0 :#tkinter.messagebox.showinfo(title="游戏结束!",message = "游戏已经结束,请重新开始")returnif self.state == 1: returnif self.n == 9 :self.gameover()returnif self.n == 0 :self.auto_sweep()self.vectory()returnself.__class__.count += 1self.state = 1self.update_style()self.vectory()def right_clicked(self, event):if self.state == 1 : returnif self.state == 0 : self.state=2 elif self.state == 2 : self.state=3elif self.state == 3 : self.state=0self.update_style()def setPos(self,r,c,n): self.r=r self.c=c self.n=ndef map_mine(self):if self.n != 9 :returnneighbors = [(self.r+i, self.c+j) for i in range(-1, 2) for j in range(-1, 2) if i != 0 or j != 0]for r, c in neighbors:for mine in self.__class__.map:if mine.r == r and mine.c == c and mine.n != 9:mine.n += 1def update_style(self) :text=str("")if self.n !=0 and self.n != 9 and self.state == 1:text=str(self.n)hex_fg = '#{:02x}{:02x}{:02x}'.format( self.__class__.fgs[self.n][0] , self.__class__.fgs[self.n][1], self.__class__.fgs[self.n][2])hex_bg = '#{:02x}{:02x}{:02x}'.format( self.__class__.bgs[self.state][0] , self.__class__.bgs[self.state][1], self.__class__.bgs[self.state][2])image = self.__class__.pic[self.state]self.image = imageif self.state == 2 : text = str("!")hex_fg = "red"elif self.state == 3 : text = str("" />)hex_fg = str("yellow")elif self.state == 6 : text = str("×")hex_fg = str("red")if image != None :hex_bg = Noneself.configure(bg=hex_bg,fg=hex_fg,image=image,text=text,compound=tkinter.CENTER)def auto_sweep(self):if self.state == 1 : returnself.state = 1self.update_style()self.__class__.count += 1if self.n != 0 :returnneighbors = [(self.r+i, self.c+j) for i in range(-1, 2) for j in range(-1, 2) if i != 0 or j != 0]for r, c in neighbors:for mine in self.__class__.map:if mine.r == r and mine.c == c and mine.n !=9 :mine.clicked(None) def gameover(self) :self.state = 4self.__class__.state = 2for mine in self.__class__.map :if mine.n == 9 and mine.state != 2:mine.state = 4mine.update_style()elif mine.n != 9 and mine.state == 2:mine.state = 6mine.update_style() tkinter.messagebox.showinfo(parent = self,title = "游戏结束!",message = "失败!")def vectory(self) :if self.__class__.count == (self.__class__.h*self.__class__.w - self.__class__.mine) :if self.__class__.state != 1 :tkinter.messagebox.showinfo("游戏结束!","恭喜过关!!!")self.__class__.state = 1root=tkinter.Tk()def del_menu(args):if args == "入门":Sweep.create_map(w=6,h=5,mine=1)elif args == "简单":Sweep.create_map(w=10,h=10,mine=15)elif args == "一般":Sweep.create_map(w=16,h=16,mine=40)elif args == "困难": Sweep.create_map(w=20,h=16,mine=60)elif args == "重新开始":passSweep.init_map(root) if __name__ == '__main__':root.title('扫雷')Sweep.create_map(w=6,h=5,mine=3)Sweep.init_map(root)menu_bar = tkinter.Menu(root)root.config(menu=menu_bar)game_menu = tkinter.Menu(menu_bar,tearoff=False)menu_bar.add_cascade(label="游戏",menu=game_menu)game_menu.add_command(label="入门",command=lambda: del_menu("入门"))game_menu.add_command(label="简单",command=lambda: del_menu("简单"))game_menu.add_command(label="一般",command=lambda: del_menu("一般"))game_menu.add_command(label="困难",command=lambda: del_menu("困难"))game_menu.add_command(label="重新开始",command=lambda: del_menu("重新开始"))#root.iconbitmap('mine.ico')root.mainloop()

一些图片资源: