软件工程——QyPt——01——扫雷游戏完善

软件工程——QyPt——01——扫雷游戏完善

游戏规则设定

  1. 初始化扫雷界面,并显示地雷数量
  2. 格子种类有两种,地雷与普通格,普通格中数字表示相邻的8个格子中地雷的数量
  3. 鼠标右键点击格子实现reveal,鼠标左键设置flag插旗,主要用于辅助玩家标记地雷位置
  4. 若reveal所有非雷格子,玩家胜利;若过程中reveal地雷格,玩家失败并reveal所有格子

初始化界面

初始化界面

在这里插入图片描述

玩家失败界面

标记起始格子、暴雷格子

具体实现效果,即上图中起始格子被黄色线条标记,暴雷格子被红色背景突出

# 如果是开始格子
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)附近的8for 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_()

  • 9
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

gantnd

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值