最近突然很喜欢玩《扫雷》这个小游戏,特别是上班摸鱼的时候,嘿嘿。但是非常蛋疼,windows10取消了这款游戏内置,若想玩还必须要到windows store上面下载。此外,网络上的扫雷大都带了很多广告,非常影响体验。因此我决定自己写一个出来,想想应该也挺简单的,事实的确如此。刚好最近想学一下Python的GUI,因此就用PyQt5好了。一边学一边写,大概花了一整天终于把它写出来了,这里来分享一下实现。
先来看效果图:
主菜单:
8x8界面:
30x20界面:
胜利者的界面:
主菜单页面
主界面如上图,很简陋。直接上代码:
index.py
import sys import os from PyQt5.QtWidgets import * from PyQt5.QtGui import QIcon from PyQt5.QtCore import QCoreApplication from content import Content class Index(QMainWindow): def __init__(self): super().__init__() self.initUI() self.basePath=os.path.join(os.getcwd(),'Saolei') def initUI(self): self.btnQuit = QPushButton('退出游戏', self) self.btnQuit.resize(self.btnQuit.sizeHint()) self.btnQuit.setStyleSheet('font-size:20px;font-family:"黑体";color:rgb(150,150,255)') self.btnStart8x8 = QPushButton('开始游戏-8x8', self) self.btnStart8x8.setStyleSheet('font-size:20px;font-family:"黑体";color:rgb(150,150,255)') self.btnStart8x8.resize(self.btnStart8x8.sizeHint()) self.btnStart16x16 = QPushButton('开始游戏-16x16', self) self.btnStart16x16.setStyleSheet('font-size:20px;font-family:"黑体";color:rgb(150,150,255)') self.btnStart16x16.resize(self.btnStart16x16.sizeHint()) self.btnStart16x30 = QPushButton('开始游戏-16x30', self) self.btnStart16x30.setStyleSheet('font-size:20px;font-family:"黑体";color:rgb(150,150,255)') self.btnStart16x30.resize(self.btnStart16x30.sizeHint()) self.btnStart30x20 = QPushButton('开始游戏-30x20', self) self.btnStart30x20.setStyleSheet('font-size:20px;font-family:"黑体";color:rgb(150,150,255)') self.btnStart30x20.resize(self.btnStart30x20.sizeHint()) self.btnAbout = QPushButton('关于作者', self) self.btnAbout.setStyleSheet('font-size:20px;font-family:"黑体";color:rgb(150,150,255)') self.btnAbout.resize(self.btnAbout.sizeHint()) vbox = QVBoxLayout()#纵向布局 vbox.addStretch(1)#添加伸展因子 vbox.addWidget(self.btnStart8x8) vbox.addWidget(self.btnStart16x16) vbox.addWidget(self.btnStart16x30) vbox.addWidget(self.btnStart30x20) vbox.addWidget(self.btnAbout) vbox.addWidget(self.btnQuit) vbox.addStretch(1) hbox = QHBoxLayout()#横向布局 hbox.addStretch(1) hbox.addLayout(vbox) hbox.addStretch(1) widget=QWidget() widget.setLayout(hbox)#把布局放到widget self.setCentralWidget(widget) #把widget放到window里面 iconPath=os.path.join(os.getcwd(),r'Saolei/res/icon/window.png')#设置图标 self.setWindowIcon(QIcon(iconPath)) self.setWindowTitle('扫雷-主菜单') self.setGeometry(300, 300, 300, 200) self.show() def aboutMe(self): reply = QMessageBox.question(self, '关于作者', 'Coded by 陈建驱 \n Github:Github:https://github.com/chenjianqu \n'+\ '个人博客:www.chenjianqu.com', QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply == QMessageBox.Yes: pass else: self.close() if __name__ == '__main__': app = QApplication(sys.argv) index = Index() content=Content() index.btnQuit.clicked.connect(QCoreApplication.instance().quit) index.btnStart8x8.clicked.connect(content.setSize8x8) index.btnStart16x16.clicked.connect(content.setSize16x16) index.btnStart16x30.clicked.connect(content.setSize16x30) index.btnStart30x20.clicked.connect(content.setSize30x20) index.btnAbout.clicked.connect(index.aboutMe) sys.exit(app.exec_())
代码逻辑也简单,就是定义六个按钮,这些按钮可以通过setStyleSheet()这个函数设置样式。整体布局:最外面是横向布局,里面一层是纵向布局,布局通过添加伸展因子使得按钮居中。
由于该类继承自QMainWindow,不能直接添加布局,需要先在窗口中添加一个widget,然后把布局放置到widget当中。
在主函数中,创建了Index界面和Content页面对象,然后将Index类中的按钮的点击信号量连接到Content类的函数,用于启动不同窗口大小的游戏。
kernel.py
把扫雷中方块的数字存放至一个数字数组中,kernel.py封装对这个数组的操作。首先看类Brick的构造函数:
import numpy as np class Brick: def __init__(self,size,n): oneArr=np.ones((1,n)) zeroArr=np.zeros((1,size[0]*size[1]-n)) self.brickArray=np.append(oneArr,zeroArr) #值为1表示有炸弹 np.random.shuffle(self.brickArray) self.brickArray=self.brickArray.astype(int).reshape(size)#得到只有炸弹的矩阵 #设置数字数组——非边缘 arr=np.zeros(self.brickArray.shape,dtype=int) for i in range(0,self.brickArray.shape[0]): for j in range(0,self.brickArray.shape[1]): if(self.brickArray[i,j]==0): #四个角落的值 if(i==0 and j==0): arr[i,j]=self.brickArray[i,j+1]+self.brickArray[i+1,j]+self.brickArray[i+1,j+1] elif(i==self.brickArray.shape[0]-1 and j==0): arr[i,j]=self.brickArray[i-1,j]+self.brickArray[i,j+1]+self.brickArray[i-1,j+1] elif(i==0 and j==self.brickArray.shape[1]-1): arr[i,j]=self.brickArray[i,j-1]+self.brickArray[i+1,j]+self.brickArray[i+1,j-1] elif(i==self.brickArray.shape[0]-1 and j==self.brickArray.shape[1]-1): arr[i,j]=self.brickArray[i,j-1]+self.brickArray[i-1,j]+self.brickArray[i-1,j-1] #四个边缘的值 elif(i==0): arr[i,j]=self.brickArray[i,j-1]+self.brickArray[i,j+1]+self.brickArray[i+1,j-1]+self.brickArray[i+1,j]+self.brickArray[i+1,j+1] elif(i==self.brickArray.shape[0]-1): arr[i,j]=self.brickArray[i-1,j-1]+self.brickArray[i-1,j]+self.brickArray[i-1,j+1]+self.brickArray[i,j-1]+self.brickArray[i,j+1] elif(j==0): arr[i,j]=self.brickArray[i-1,j]+self.brickArray[i-1,j+1]+self.brickArray[i,j+1]+self.brickArray[i+1,j]+self.brickArray[i+1,j+1] elif(j==self.brickArray.shape[1]-1): arr[i,j]=self.brickArray[i-1,j-1]+self.brickArray[i-1,j]+self.brickArray[i,j-1]+self.brickArray[i+1,j-1]+self.brickArray[i+1,j] else: arr[i,j]=self.brickArray[i-1,j-1]+self.brickArray[i-1,j]+self.brickArray[i-1,j+1]+self.brickArray[i,j-1]+\ self.brickArray[i,j+1]+self.brickArray[i+1,j-1]+self.brickArray[i+1,j]+self.brickArray[i+1,j+1] self.brickArray=self.brickArray*10+arr self.OpenFlagArr=np.zeros((self.brickArray.shape[0],self.brickArray.shape[1]),dtype=int)
可以看到,Brick类定义了一个numpy二维数组brickArray,用于存放扫雷的数字,数组里数字10代表该位置是个炸弹,其它数字表示该位置周围8个方块中总共存在的炸弹的数量。炸弹数量通过一个双重循环计算得到,由于需要考虑到数组边缘和四周,故这个循环略显复杂。构造函数传入pos参数的是数字数组的大小,参数n是该数组中存在的炸弹数量。
OpenFlagArr存放游戏过程中某个方块的标志,如当OpenFlagArr[i,j]的值为0时,表示该方块未被掀开,当值为1时,表示该方块已掀开,当值为2时,表示该方块被玩家标识为炸弹。
下面再来看Brick类中的其他函数。
def isVictory(self): flag=True rows=self.brickArray.shape[0] cols=self.brickArray.shape[1] for i in range(rows): for j in range(cols): if(self.brickArray[i,j]==10 and self.OpenFlagArr[i,j]!=2): flag=False elif(self.OpenFlagArr[i,j]==2 and self.brickArray[i,j]!=10): flag=False return flag def getSurplusNumber(self): n=0 rows=self.brickArray.shape[0] cols=self.brickArray.shape[1] for i in range(rows): for j in range(cols): if(self.brickArray[i,j]==10 and self.OpenFlagArr[i,j]==0): n=n+1 return n
上面代码中的isVictory()函数判断是否已经找到所有的炸弹,getSurplusNumer()获取剩余的炸弹数量。
下面两个函数用于判断某个方块的周围是否还存在炸弹。
def isNoBomb(self,pos): i=pos[0] j=pos[1] brickPosList=[] #四个角落的值 if(i==0 and j==0): brickPosList.append((i,j+1)) brickPosList.append((i+1,j)) brickPosList.append((i+1,j+1)) elif(i==self.brickArray.shape[0]-1 and j==0): brickPosList.append((i-1,j)) brickPosList.append((i,j+1)) brickPosList.append((i-1,j+1)) elif(i==0 and j==self.brickArray.shape[1]-1): brickPosList.append((i,j-1)) brickPosList.append((i+1,j-1)) brickPosList.append((i+1,j)) elif(i==self.brickArray.shape[0]-1 and j==self.brickArray.shape[1]-1): brickPosList.append((i,j-1)) brickPosList.append((i-1,j)) brickPosList.append((i-1,j-1)) #四个边缘的值 elif(i==0): brickPosList.append((i,j-1)) brickPosList.append((i,j+1)) brickPosList.append((i+1,j-1)) brickPosList.append((i+1,j)) brickPosList.append((i+1,j+1)) elif(i==self.brickArray.shape[0]-1): brickPosList.append((i-1,j-1)) brickPosList.append((i-1,j)) brickPosList.append((i-1,j+1)) brickPosList.append((i,j-1)) brickPosList.append((i,j+1)) elif(j==0): brickPosList.append((i-1,j)) brickPosList.append((i-1,j+1)) brickPosList.append((i,j+1)) brickPosList.append((i+1,j)) brickPosList.append((i+1,j+1)) elif(j==self.brickArray.shape[1]-1): brickPosList.append((i-1,j-1)) brickPosList.append((i-1,j)) brickPosList.append((i,j-1)) brickPosList.append((i+1,j-1)) brickPosList.append((i+1,j)) else: brickPosList.append((i-1,j-1)) brickPosList.append((i-1,j)) brickPosList.append((i-1,j+1)) brickPosList.append((i,j-1)) brickPosList.append((i,j+1)) brickPosList.append((i+1,j-1)) brickPosList.append((i+1,j)) brickPosList.append((i+1,j+1)) return self.checkSame(brickPosList) def checkSame(self,brickPosList): flag=True for p in brickPosList: if(self.brickArray[p[0],p[1]]==10 and self.OpenFlagArr[p[0],p[1]]!=2): flag=False return flag
篇幅所限,下一篇(http://www.chenjianqu.com/show-35.html)继续。。