软件工程——QyPt——01——扫雷游戏完善
软件工程——QyPt——01——扫雷游戏完善
游戏规则设定
- 初始化扫雷界面,并显示地雷数量
- 格子种类有两种,地雷与普通格,普通格中数字表示相邻的8个格子中地雷的数量
- 鼠标右键点击格子实现reveal,鼠标左键设置flag插旗,主要用于辅助玩家标记地雷位置
- 若reveal所有非雷格子,玩家胜利;若过程中reveal地雷格,玩家失败并reveal所有格子
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/b6d9314ec96edd6dc47caa7d937562bd.png)
标记起始格子、暴雷格子
具体实现效果,即上图中起始格子被黄色线条标记,暴雷格子被红色背景突出
# 如果是开始格子
if self.is_start:
# 创建一个黄色的 QPen
pen = QPen(QColor('yellow'))
pen.setWidth(6)
# 设置画笔
p.setPen(pen)
# 绘制矩形边框
p.drawRect(r)
#如果是雷
if self.is_mine:
#如果是爆炸的雷
#注意,QPainter有类似图层的概念,注意绘制的先后顺序
if self.is_boomed:
# 创建一个红色的画刷
brush = QBrush(QColor('red'))
# 设置画刷
p.setBrush(brush)
# 绘制填充红色的矩形
p.drawRect(r)
改进Flag标记功能
原模板中Flag标记后仍可被reveal
在click函数中添加判断条件
def click(self):
if not self.is_revealed and not self.is_flagged:
self.reveal()
#如果雷的数目为零
if self.adjacent_n == 0:
#触发信号
self.expandable.emit(self.x, self.y)
self.clicked.emit()
Flag标记后应该可以被取消
def flag(self):
#标记为雷
if not self.is_flagged:
self.is_flagged = True
self.update()
#触发信号
self.clicked.emit()
else:
self.is_flagged = False
self.update()
# 触发信号
self.clicked.emit()
实现鼠标中键功能
需求描述
规则描述中已说明可以通过鼠标左键辅助玩家标记地雷位置,那么假设当前条件:一个格子已经被揭开并且其周围有地雷,当前玩家已经正确标记出其周围的地雷位置,那么其周围剩下的普通格子将会被逐个揭开
那么我们考虑增加鼠标中键功能,即通过中键点击已经揭开的格子,从而省去“逐个”的过程。
功能实现
简单的思路就是便利当前格子周围的8个格子,检查是否有雷被错标、漏标
如果有,那么直接引爆地雷,结束游戏;没有,则揭开非雷格子
\
# Tips翻开安全区域
def show_tip(self, x, y):
# if not self.is_revealed and not self.is_flagged:
Reveal_Flag = True
for xi in range(max(0, x - 1), min(x + 2, self.b_size)):
for yi in range(max(0, y - 1), min(y + 2, self.b_size)):
w = self.grid.itemAtPosition(yi, xi).widget()
# 是雷,但没有被标出
if w.is_mine and not w.is_flagged:
Reveal_Flag = False
w.click()
w.ohno.emit(w.x, w.y) # 信号函数发射信号并传递数据
w.is_boomed = True
# 不是雷,但被标出
if not w.is_mine and w.is_flagged:
Reveal_Flag = False
w.is_flagged = False
w.click()
w.ohno.emit(w.x, w.y) # 信号函数发射信号并传递数据
w.is_boomed = True
if Reveal_Flag:
for xi in range(max(0, x - 1), min(x + 2, self.b_size)):
for yi in range(max(0, y - 1), min(y + 2, self.b_size)):
w = self.grid.itemAtPosition(yi, xi).widget()
if not w.is_flagged:
w.click()
添加引爆时音效
在主窗口类中添加变量(文件格式需要时wav,mp3不行,不知原因)
self.sound = QSound("./music/EXPLO.wav")
在结束游戏函数调用
# 游戏结束
def game_over(self, x, y):
#揭开所有的格子
self.sound.play()
self.reveal_map()
self.update_status(STATUS_FAILED)
添加自主修改地雷数量
主窗口中添加两个部件
# 设置文本读取窗口
self.lineEdit = QLineEdit()
self.lineEdit.setFixedSize(QSize(40, 40))
self.lineEdit.setObjectName("lineEdit")
# 读取自定义地雷数量(用于新开游戏等)
_translate = QtCore.QCoreApplication.translate
self.read_mine = QPushButton()
self.read_mine.setFixedSize(QSize(40, 40))
self.read_mine.setIconSize(QSize(32, 32))
self.read_mine.setText(_translate("MainWindow", "重置"))
self.read_mine.setFlat(True)
添加接口,将click与函数功能联系起来
# 按下状态按钮后,执行重开
def read_mine_pressed(self):
# 读取最新定义地雷数量并重新启动
num_mines = int(self.lineEdit.text())
if num_mines < self.b_size * self.b_size:
self.n_mines = num_mines
self.mines.setText("%03d" % self.n_mines)
self.update_status(STATUS_FAILED)
self.reset_map()
完整代码
from PyQt5.QtGui import * # QtGui:包含了窗口系统、事件处理、2D图像、基本绘画、字体和文字类
from PyQt5.QtWidgets import * # QtWidgets:包含了一些列创建桌面应用的UI元素
from PyQt5.QtCore import * # QtCore:包含了核心的非GU的功能。主要和时间、文件与文件夹、各种数据、流、URLs、mime类文件、进程与线程一起使用
from PyQt5.QtMultimedia import QSound
from PyQt5 import QtCore, QtGui, QtWidgets
import random
import time
#雷、flag、时钟图标
IMG_BOMB = QImage("./images/bug.png")
IMG_FLAG = QImage("./images/flag.png")
IMG_CLOCK = QImage("./images/clock-select.png")
#显示雷数量的数字的颜色
NUM_COLORS = {
1: QColor('#f44336'),
2: QColor('#9C27B0'),
3: QColor('#3F51B5'),
4: QColor('#03A9F4'),
5: QColor('#00BCD4'),
6: QColor('#4CAF50'),
7: QColor('#E91E63'),
8: QColor('#FF9800')
}
#游戏状态
STATUS_READY = 0
STATUS_PLAYING = 1
STATUS_FAILED = 2
STATUS_SUCCESS = 3
#游戏正中间的状态图标
STATUS_ICONS = {
STATUS_READY: "./images/plus.png",
STATUS_PLAYING: "./images/smiley.png",
STATUS_FAILED: "./images/cross.png",
STATUS_SUCCESS: "./images/smiley-lol.png",
}
#该类定义了棋盘上每个格子的属性和方法
class Pos(QWidget):
#声明带特定数据类型参数的信号函数
#可以查看Tutorial中的相关介绍(Step 4)
expandable = pyqtSignal(int, int)
clicked = pyqtSignal()
ohno = pyqtSignal(int, int)
resistant = pyqtSignal(int, int)
# 初始化
def __init__(self, x, y, *args, **kwargs):
super(Pos, self).__init__(*args, **kwargs)
#设置格子的大小
self.setFixedSize(QSize(20, 20))
# 保存格子在grip上的位置
self.x = x
self.y = y
def reset(self):
#是否是游戏开始时自动点击的第一个格子
self.is_start = False
#是否是雷
self.is_mine = False
# 该格子附近8格的地雷数
self.adjacent_n = 0
#是否已经点开
self.is_revealed = False
#是否标记为棋子
self.is_flagged = False
#是否是触发爆炸的雷
self.is_boomed = False
self.update()
#重写虚函数,该虚函数在每次update()时会被调用
def paintEvent(self, event):
#QPainter为PyQt的一个绘图类
#可以查看Tutorial中的相关介绍(Step 2)
p = QPainter(self)
#与抗锯齿等相关
p.setRenderHint(QPainter.Antialiasing)
#获得该格子在面板中的位置矩形坐标
r = event.rect()
#设置揭开和没揭开两种颜色格式
if self.is_revealed:
color = self.palette().color(QPalette.Background) # 调色板QPalette类
outer, inner = color, color
else:
outer, inner = Qt.gray, Qt.lightGray
#QBrush为PyQt的一个基本图形对象,常用于填充矩形等
brush = QBrush(inner)
#填充矩形
p.fillRect(r, brush)
#外轮廓
pen = QPen(outer)
#设置笔的粗细,绘制外轮廓
pen.setWidth(2)
p.setPen(pen)
#绘制外轮廓
p.drawRect(r)
#如果被揭开了
if self.is_revealed:
# 如果是开始格子
if self.is_start:
# 创建一个黄色的 QPen
pen = QPen(QColor('yellow'))
pen.setWidth(6)
# 设置画笔
p.setPen(pen)
# 绘制矩形边框
p.drawRect(r)
#如果是雷
if self.is_mine:
#如果是爆炸的雷
#注意,QPainter有类似图层的概念,注意绘制的先后顺序
if self.is_boomed:
# 创建一个红色的画刷
brush = QBrush(QColor('red'))
# 设置画刷
p.setBrush(brush)
# 绘制填充红色的矩形
p.drawRect(r)
#绘制图案
p.drawPixmap(r, QPixmap(IMG_BOMB))
#如果周围有雷,那么这个格子应该显示雷的数量
elif self.adjacent_n > 0:
#根据不同的颜色设置QPen
pen = QPen(NUM_COLORS[self.adjacent_n])
p.setPen(pen)
#设置字体
f = p.font()
f.setBold(True)
p.setFont(f)
#绘制文本
p.drawText(r, Qt.AlignHCenter | Qt.AlignVCenter, str(self.adjacent_n))
#如果被标记为flag
elif self.is_flagged:
p.drawPixmap(r, QPixmap(IMG_FLAG))
def flag(self):
#标记为雷
if not self.is_flagged:
self.is_flagged = True
self.update()
#触发信号
self.clicked.emit()
else:
self.is_flagged = False
self.update()
# 触发信号
self.clicked.emit()
def reveal(self):
#标记为已揭开
if not self.is_flagged:
self.is_revealed = True
self.update()
def click(self):
if not self.is_revealed and not self.is_flagged:
self.reveal()
#如果雷的数目为零
if self.adjacent_n == 0:
#触发信号
self.expandable.emit(self.x, self.y)
self.clicked.emit()
def showTips(self):
# 触发信号
self.resistant.emit(self.x, self.y)
#可以在此处添加代码实现Tips功能
#重写虚函数,该虚函数在收到鼠标点击释放时被调用
def mouseReleaseEvent(self, event): #判断点击后的逻辑
if (event.button() == Qt.RightButton and not self.is_revealed):
self.flag()
elif (event.button() == Qt.LeftButton):
self.click()
if self.is_mine and not self.is_flagged:
self.ohno.emit(self.x, self.y) #信号函数发射信号并传递数据
self.is_boomed = True
elif (event.button() == Qt.MiddleButton):
if self.is_revealed:
self.showTips()
class MainWindow(QMainWindow):
#初始化类
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
# 添加引爆时的音乐
self.sound = QSound("./music/EXPLO.wav")
#设置扫雷行列大小与雷的数量
self.b_size = 16
self.n_mines = 40
#对UI进行布局
w = QWidget()
hb = QHBoxLayout()
vb = QVBoxLayout()
#显示雷的个数的Label
self.mines = QLabel()
self.mines.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
#显示时钟的Label
self.clock = QLabel()
self.clock.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
#设置字体大小
f = self.mines.font()
f.setPointSize(24)
f.setWeight(75)
self.mines.setFont(f)
self.clock.setFont(f)
#创建定时器
#该定时器每秒调用一次self.update_timer,并更新时间
self._timer = QTimer()
#信号与槽机制
# 可以查看Tutorial中的相关介绍(Step 4)
self._timer.timeout.connect(self.update_timer)
self._timer.start(1000)
#初始化,显示雷的数量和时间
self.mines.setText("%03d" % self.n_mines)
self.clock.setText("000")
#状态按钮(用于新开游戏等)
self.button = QPushButton()
self.button.setFixedSize(QSize(32, 32))
self.button.setIconSize(QSize(32, 32))
self.button.setIcon(QIcon("./images/smiley.png"))
self.button.setFlat(True)
#信号与槽机制
# 可以查看Tutorial中的相关介绍(Step 4)
self.button.pressed.connect(self.button_pressed)
# 设置文本读取窗口
self.lineEdit = QLineEdit()
self.lineEdit.setFixedSize(QSize(40, 40))
self.lineEdit.setObjectName("lineEdit")
# 读取自定义地雷数量(用于新开游戏等)
_translate = QtCore.QCoreApplication.translate
self.read_mine = QPushButton()
self.read_mine.setFixedSize(QSize(40, 40))
self.read_mine.setIconSize(QSize(32, 32))
self.read_mine.setText(_translate("MainWindow", "重置"))
self.read_mine.setFlat(True)
# 信号与槽机制
# 可以查看Tutorial中的相关介绍(Step 4)
self.read_mine.clicked.connect(self.read_mine_pressed)
#将所有Label按顺序装入hb和vb两个布局中
#雷的图标
l = QLabel()
l.setPixmap(QPixmap.fromImage(IMG_BOMB))
l.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
#按顺序添加雷的图标、炸弹数量、状态按钮、时间的图标
hb.addWidget(l)
hb.addWidget(self.mines)
hb.addWidget(self.button)
hb.addWidget(self.clock)
#时钟显示
l = QLabel()
l.setPixmap(QPixmap.fromImage(IMG_CLOCK))
l.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
#添加时间显示
hb.addWidget(l)
hb.addWidget(self.lineEdit)
hb.addWidget(self.read_mine)
#grid布局,用于创建扫雷每个格子
self.grid = QGridLayout()
#设置每个格子的Spacing
self.grid.setSpacing(5)
#垂直布局
vb.addLayout(hb)
vb.addLayout(self.grid)
w.setLayout(vb)
self.setCentralWidget(w)
#初始化棋盘
self.init_map()
#更新游戏状态
self.update_status(STATUS_READY)
#重制棋盘
self.reset_map()
#更新游戏状态
self.update_status(STATUS_READY)
self.show()
# 添加扫雷棋盘的格子 self.b_size * self.b_size的大小
def init_map(self):
for x in range(0, self.b_size):
for y in range(0, self.b_size):
#Pos类
w = Pos(x, y)
self.grid.addWidget(w, y, x)
#槽和Pos类的信号连接到一起
#例如,当w的clicked被emit时,将会调用self.trigger_start并传递数据
w.clicked.connect(self.trigger_start)
w.expandable.connect(self.expand_reveal)
w.ohno.connect(self.game_over)
w.resistant.connect(self.show_tip)
def reset_map(self):
# 清除雷的标记
for x in range(0, self.b_size):
for y in range(0, self.b_size):
#获得( x, y )位置的Pos类(注意查找位置时候的顺序为( y, x ))
w = self.grid.itemAtPosition(y, x).widget()
#重置格子的状态
w.reset()
# 添加地雷
positions = []
while len(positions) < self.n_mines:
x, y = random.randint(0, self.b_size - 1), random.randint(0, self.b_size - 1)
if (x, y) not in positions:
w = self.grid.itemAtPosition(y, x).widget()
w.is_mine = True
positions.append((x, y))
#获得该格子附件的雷的数量
def get_adjacency_n(x, y):
positions = self.get_surrounding(x, y)
n_mines = sum(1 if w.is_mine else 0 for w in positions)
return n_mines
# 添加该格子附件的地雷数
for x in range(0, self.b_size):
for y in range(0, self.b_size):
w = self.grid.itemAtPosition(y, x).widget()
w.adjacent_n = get_adjacency_n(x, y)
#设置开始的格子
while True:
x, y = random.randint(0, self.b_size - 1), random.randint(0, self.b_size - 1)
#避免最开始的格子是炸弹
if (x, y) not in positions:
w = self.grid.itemAtPosition(y, x).widget()
w.is_start = True
#揭开附近的格子
for w in self.get_surrounding(x, y):
if not w.is_mine:
w.click()
break
# 获得该格子附近格子的Pos对象
def get_surrounding(self, x, y):
positions = []
for xi in range(max(0, x - 1), min(x + 2, self.b_size)):
for yi in range(max(0, y - 1), min(y + 2, self.b_size)):
positions.append(self.grid.itemAtPosition(yi, xi).widget())
return positions
# 按下状态按钮后,执行重开
def button_pressed(self):
#没输但是重开
if self.status == STATUS_PLAYING:
self.update_status(STATUS_FAILED)
self.reveal_map()
#输了然后重开
elif self.status == STATUS_FAILED:
self.update_status(STATUS_READY)
self.reset_map()
# 按下状态按钮后,执行重开
def read_mine_pressed(self):
# 读取最新定义地雷数量并重新启动
num_mines = int(self.lineEdit.text())
if num_mines < self.b_size * self.b_size:
self.n_mines = num_mines
self.mines.setText("%03d" % self.n_mines)
self.update_status(STATUS_FAILED)
self.reset_map()
# 揭开所有的格子
def reveal_map(self):
for x in range(0, self.b_size):
for y in range(0, self.b_size):
w = self.grid.itemAtPosition(y, x).widget()
w.reveal()
# 揭开格子
def expand_reveal(self, x, y):
#(x,y)附近的8格
for xi in range(max(0, x - 1), min(x + 2, self.b_size)):
for yi in range(max(0, y - 1), min(y + 2, self.b_size)):
w = self.grid.itemAtPosition(yi, xi).widget()
if not w.is_mine:
w.click()
# Tips翻开安全区域
def show_tip(self, x, y):
# if not self.is_revealed and not self.is_flagged:
Reveal_Flag = True
for xi in range(max(0, x - 1), min(x + 2, self.b_size)):
for yi in range(max(0, y - 1), min(y + 2, self.b_size)):
w = self.grid.itemAtPosition(yi, xi).widget()
# 是雷,但没有被标出
if w.is_mine and not w.is_flagged:
Reveal_Flag = False
w.click()
w.ohno.emit(w.x, w.y) # 信号函数发射信号并传递数据
w.is_boomed = True
# 不是雷,但被标出
if not w.is_mine and w.is_flagged:
Reveal_Flag = False
w.is_flagged = False
w.click()
w.ohno.emit(w.x, w.y) # 信号函数发射信号并传递数据
w.is_boomed = True
if Reveal_Flag:
for xi in range(max(0, x - 1), min(x + 2, self.b_size)):
for yi in range(max(0, y - 1), min(y + 2, self.b_size)):
w = self.grid.itemAtPosition(yi, xi).widget()
if not w.is_flagged:
w.click()
#可在此添加代码实现Tips功能
# 记录开始时间
def trigger_start(self, *args):
#只有游戏开始后的第一次点击执行此函数,用于记录开始时间
if self.status != STATUS_PLAYING:
# 第一次点击
self.update_status(STATUS_PLAYING)
# 记录开始时间
self._timer_start_nsecs = int(time.time())
# 更新游戏状态并更新图标
def update_status(self, status):
self.status = status
self.button.setIcon(QIcon(STATUS_ICONS[self.status]))
# 计算时间
def update_timer(self):
if self.status == STATUS_PLAYING:
n_secs = int(time.time()) - self._timer_start_nsecs
self.clock.setText("%03d" % n_secs)
# 游戏结束
def game_over(self, x, y):
#揭开所有的格子
self.sound.play()
self.reveal_map()
self.update_status(STATUS_FAILED)
if __name__ == '__main__':
app = QApplication([])
window = MainWindow()
#运行,程序进入循环等待状态
app.exec_()