??曾经在大二和大三的时候分别尝试用MATLAB和VB.NET去实现扫雷,但分别都因为没搞清扫雷的逻辑和不清楚VB.NET的特性(主要是没有区分开图形界面与游戏逻辑)而放弃了
??直到2021年的年底,在写完《飞机大战》后准备找一个新的选题时,才想起了这个一直没写出来的游戏,于是便很快就将本次的主题定了下来。在经过两三年的“锻炼”后,写出扫雷应该也不是一件难事了
基本思路
地图场景
??扫雷这个游戏是在一个由m
×
\times
×n个grid的区域内进行的,因此能够直接想到的自然是使用二维数组来表示。每一个grid所包含的信息无非是以下几种:
??1、是否被翻开
??2、是否为地雷
??3、周围一圈8个grid中的雷数,介于0-8之间
??由此设计两个二维数组,其中一个用于存储是否为地雷或周围地雷的数量,另外一个用于存储grid是否被翻开。前者使用字符’*’、‘0’——'8’表示,后者mask则采用bool类型,当值为真时表示未翻开:
def generateField(self):
"""
初始化二维数组
"""
self.mineField = np.zeros(shape=(self.height, self.width), dtype=str)
def generateMine(self):
"""
生成地雷
"""
indexList = list(np.arange(self.width * self.height, dtype=int))
mineList = []
for mineID in range(self.mineNumber):
thisMineIdx = indexList[int(np.random.rand() * len(indexList))]
mineList.append(thisMineIdx)
indexList.remove(thisMineIdx)
for eachMinePosIdx in mineList:
i = eachMinePosIdx // self.width
j = eachMinePosIdx % self.width
self.mineField[i][j] = '*'
def generateMarkNumber(self):
"""
生成数字标记
"""
for i in range(self.height):
for j in range(self.width):
if(self.mineField[i][j] == '*'):
continue
number = 0
for di in [-1, 0, 1]:
for dj in [-1, 0, 1]:
if((di == 0 and dj == 0) or (not (0 <= i + di < self.height)) or (not (0 <= j + dj < self.width))):
continue
if(self.mineField[i+di][j+dj] == '*'):
number += 1
self.mineField[i][j] = str(number)
def generateMaskField(self):
"""
生成Mask数组
"""
self.maskField = np.ones(shape=(self.height, self.width), dtype=bool)
右键:标记flag
??因为雷的数量是相对较少的,所以对应的flag数量也是一样,如果使用二维数组来表示,这个数组将会是非常稀疏的,这样就占用了很多不必要占用的空间。因此,flag的存储方式使用简单的列表形式,每一个元素都表示这个flag所存放的相应坐标位置。与此同时,还要记录当前flag的数量以及flag的数量上限,如果数量已满,则不能再继续进行标记flag的操作:
def initFlagInfo(self):
"""
初始化Flag信息,包括容器和flag数
"""
self.flagContainer = []
self.flagLimit = self.mineNumber
self.currentFlagNumber = 0
def flagGrid(self, i, j):
"""
右键点击i行j列的grid时执行,综合判断flag操作
Parameters
----------
i : int
i行
j : int
j列
"""
if(not self.maskField[i][j]):
return
self.updateFlag(i, j)
def updateFlag(self, i, j):
"""
对i行j列进行Flag操作
Parameters
----------
i : int
i行
j : int
j列
"""
if([i, j] in self.flagContainer):
self.flagContainer.remove([i, j])
self.currentFlagNumber -= 1
else:
if(not self.isFlagReachMax()):
self.flagContainer.append([i, j])
self.currentFlagNumber += 1
左键:翻开一个grid
??使用鼠标左键点击时,相应的grid会被翻开。随后,周围的所有无雷连续区域也会被自动翻开,这个区域是以数字介于1-9之间的grid为边界的:
??点击这个区域中的任何一个grid,都应该翻开这样的同一片连续的区域
??对于这类翻开一片连续区域的情形而言,使用队列遍历的算法再合适不过了。首先将点击的那个grid放进队列,然后取出并将mask标记为False,把周围满足条件的grid全部加入到队列中,再开始从队列中取出第一个元素,对其周围的grid进行判断……以此类推,直到最后队列为空时,结束遍历。这样一来,就可以把整片区域都遍历到了:
def flipGrid(self, i, j):
"""
翻开位于i行j列处的方块,如果踩雷则游戏结束,否则更新相应的状态后继续游戏
Parameters
----------
i : int
i行
j : int
j列
"""
if(not self.isMasked(i, j)):
return
if([i, j] in self.flagContainer):
return
if(self.mineField[i][j] == '*'):
self.gameover()
self.updateMask(i, j)
def updateMask(self, i, j):
"""
根据点击的位置,更新Mask的情况
Parameters
----------
i : int
i行
j : int
j列
"""
queue = [[i, j]]
while(queue):
p = queue[0]
queue.remove(queue[0])
for di in [-1, 0, 1]:
for dj in [-1, 0, 1]:
i = p[0]
j = p[1]
if((di == 0 and dj == 0) or (not (0 <= i + di < self.height)) or (not (0 <= j + dj < self.width))):
continue
if(di and dj):
pass
if(self.maskField[i+di, j+dj]):
if(self.mineField[i, j] not in ['0', '*'] and self.mineField[i+di, j+dj] == '0'):
queue.append([i+di, j+dj])
if(self.mineField[i, j] == '0' and self.mineField[i+di, j+dj] not in ['*']):
queue.append([i+di, j+dj])
if([i, j] in self.flagContainer):
self.flagGrid(i, j)
self.maskField[i, j] = False
左右键同时点击:对周围一圈的区域进行进一步的探索
??对着已经翻开的区域,同时点击左右键时,就会对周围一圈的区域进行检查。如果发现周围一圈的所有地雷都被flag标记的话,则会将剩下的几个没有被标记的地方全部翻开(效果和点击鼠标左键一致);如果没有标记周围一圈的所有地雷,则不会进行任何操作,并且如果此时flag已用光,则会直接触发地雷的爆炸:
def swampGrid(self, i, j):
"""
对着i, j的四周一圈扫雷
Parameters
----------
i : int
i行
j : int
j列
"""
if(self.maskField[i][j]):
return
for di in [-1, 0, 1]:
for dj in [-1, 0, 1]:
if((di == 0 and dj == 0) or (not (0 <= i + di < self.height)) or (not (0 <= j + dj < self.width))):
continue
if([i+di, j+dj] in self.flagContainer and self.mineField[i+di][j+dj] != "*"):
return
if([i+di, j+dj] not in self.flagContainer and self.mineField[i+di][j+dj] == "*"):
if(self.isFlagReachMax()):
self.flipGrid(i+di, j+dj)
return
for di in [-1, 0, 1]:
for dj in [-1, 0, 1]:
if((di == 0 and dj == 0) or (not (0 <= i + di < self.height)) or (not (0 <= j + dj < self.width))):
continue
if(self.maskField[i+di][j+dj] and [i+di, j+dj] not in self.flagContainer):
self.flipGrid(i+di, j+dj)
游戏场景与图形界面相连接
??在完成了游戏逻辑的编写后,就要考虑如何使用图形界面将这个游戏场景展现给用户看了。游戏中发生的各种事件,全都由游戏逻辑这个程序来执行,而图形界面的作用就相当于游戏与用户之间的互动窗口:用户通过点击鼠标和敲击键盘的方式将信号通过图形界面传递给游戏逻辑,而游戏逻辑则通过图形化的方式将当前状况展现给用户看
??在这里,用户使用不同的方式点击鼠标以及按下相应的键盘时,就会对游戏逻辑程序发出不同值的信号,以执行不同的操作:
def clickOperation(self, event, mousePos):
"""
点击鼠标时触发事件
"""
j = mousePos[0] // self.gridSize
i = mousePos[1] // self.gridSize
if(not self.stage.isGameover()):
if(pygame.mouse.get_pressed()[0] and pygame.mouse.get_pressed()[2]):
self.stage.action(3, i, j)
elif(event.button == 1):
self.stage.action(1, i, j)
elif(event.button == 3):
self.stage.action(2, i, j)
if(self.stage.isGamewin()):
self.stage.gamewin()
def gameStage(self):
"""
游戏图形界面
"""
while True:
events = pygame.event.get()
for event in events:
if(event.type == pygame.QUIT):
exit()
if(event.type == pygame.MOUSEBUTTONDOWN):
self.clickOperation(event, pygame.mouse.get_pos())
if(event.type == pygame.KEYDOWN):
if(pygame.key.get_pressed()[pygame.K_r]):
self.stage = Stage()
self.gameStageDraw()
pygame.display.update()
??游戏逻辑程序在接收到信号后,就会根据信号的值来执行不同的动作:
def action(self, signal, i, j):
"""
采取动作,与图像界面直接连接
信号1:翻雷
信号2:flag
信号3:扫雷
Parameters
----------
signal : int
信号
i : int
第i行
j : int
第j列
"""
if(signal == 1):
self.flipGrid(i, j)
elif(signal == 2):
self.flagGrid(i, j)
elif(signal == 3):
self.swampGrid(i, j)
??以上便是核心算法和思路的简述,其他还有很多细节,将会在文末的源代码中全部贴出,可以直接复制或使用git clone进行下载
效果展示
??注:此时还有一个bug没有移除,所以看起来会有那么一点不对劲……
源代码展示
??代码下载链接:https://github.com/VtaSkywalker/20211223_001_mines
stage.py
import numpy as np
import sys
class StageConfig:
height = 10
width = 10
mineNumber = 10
doFirstFlip = False
class Stage:
"""
场景类
Attributes
----------
height : int
雷区的高度
width : int
雷区的宽度
mineNumber : int
雷的数量
mineField : char[][]
雷区
maskField : bool[][]
雷区的Mask,True为挡住
flagContainer : int[2][]
雷区的flag,存储了哪些地方插旗的信息
flagLimit : int
插旗的上限
currentFlagNumber : int
当前插旗的数量
"""
def __init__(self):
self.height = StageConfig.height
self.width = StageConfig.width
self.mineNumber = StageConfig.mineNumber
self.initStage()
def initStage(self):
"""
初始化游戏
"""
if(not self.isMineNumberLegal()):
raise Exception("雷的数量不合理!")
self.generateField()
self.generateMine()
self.generateMarkNumber()
self.generateMaskField()
self.initFlagInfo()
if(StageConfig.doFirstFlip):
safeIdx = np.where(self.mineField != '*')
randomSelectIdx = int(np.random.rand() * len(safeIdx[0]))
self.flipGrid(safeIdx[0][randomSelectIdx], safeIdx[1][randomSelectIdx])
self.gameoverState = False
def isMineNumberLegal(self):
"""
雷的数量是否合理
Returns
-------
合理 / 不合理 : True / False
"""
if(self.width * self.height <= self.mineNumber):
return False
if(self.mineNumber <= 0):
return False
return True
def generateField(self):
"""
初始化二维数组
"""
self.mineField = np.zeros(shape=(self.height, self.width), dtype=str)
def generateMine(self):
"""
生成地雷
"""
indexList = list(np.arange(self.width * self.height, dtype=int))
mineList = []
for mineID in range(self.mineNumber):
thisMineIdx = indexList[int(np.random.rand() * len(indexList))]
mineList.append(thisMineIdx)
indexList.remove(thisMineIdx)
for eachMinePosIdx in mineList:
i = eachMinePosIdx // self.width
j = eachMinePosIdx % self.width
self.mineField[i][j] = '*'
def generateMarkNumber(self):
"""
生成数字标记
"""
for i in range(self.height):
for j in range(self.width):
if(self.mineField[i][j] == '*'):
continue
number = 0
for di in [-1, 0, 1]:
for dj in [-1, 0, 1]:
if((di == 0 and dj == 0) or (not (0 <= i + di < self.height)) or (not (0 <= j + dj < self.width))):
continue
if(self.mineField[i+di][j+dj] == '*'):
number += 1
self.mineField[i][j] = str(number)
def generateMaskField(self):
"""
生成Mask数组
"""
self.maskField = np.ones(shape=(self.height, self.width), dtype=bool)
def initFlagInfo(self):
"""
初始化Flag信息,包括容器和flag数
"""
self.flagContainer = []
self.flagLimit = self.mineNumber
self.currentFlagNumber = 0
def flipGrid(self, i, j):
"""
翻开位于i行j列处的方块,如果踩雷则游戏结束,否则更新相应的状态后继续游戏
Parameters
----------
i : int
i行
j : int
j列
"""
if(not self.isMasked(i, j)):
return
if([i, j] in self.flagContainer):
return
if(self.mineField[i][j] == '*'):
self.gameover()
self.updateMask(i, j)
def isGameover(self):
if(self.gameoverState):
return True
return False
def gameover(self):
"""
游戏结束时执行
"""
self.gameoverState = True
print("gameover")
def updateMask(self, i, j):
"""
根据点击的位置,更新Mask的情况
Parameters
----------
i : int
i行
j : int
j列
"""
queue = [[i, j]]
while(queue):
p = queue[0]
queue.remove(queue[0])
for di in [-1, 0, 1]:
for dj in [-1, 0, 1]:
i = p[0]
j = p[1]
if((di == 0 and dj == 0) or (not (0 <= i + di < self.height)) or (not (0 <= j + dj < self.width))):
continue
if(di and dj):
pass
if(self.maskField[i+di, j+dj]):
if(self.mineField[i, j] not in ['0', '*'] and self.mineField[i+di, j+dj] == '0'):
queue.append([i+di, j+dj])
if(self.mineField[i, j] == '0' and self.mineField[i+di, j+dj] not in ['*']):
queue.append([i+di, j+dj])
if([i, j] in self.flagContainer):
self.flagGrid(i, j)
self.maskField[i, j] = False
def isMasked(self, i, j):
"""
检查i行j列是否被Mask
Parameters
----------
i : int
i行
j : int
j列
"""
if(self.maskField[i][j]):
return True
return False
def flagGrid(self, i, j):
"""
右键点击i行j列的grid时执行,综合判断flag操作
Parameters
----------
i : int
i行
j : int
j列
"""
if(not self.maskField[i][j]):
return
self.updateFlag(i, j)
def isFlagReachMax(self):
"""
检查Flag数是否达到上限
"""
if(self.currentFlagNumber >= self.flagLimit):
return True
return False
def updateFlag(self, i, j):
"""
对i行j列进行Flag操作
Parameters
----------
i : int
i行
j : int
j列
"""
if([i, j] in self.flagContainer):
self.flagContainer.remove([i, j])
self.currentFlagNumber -= 1
else:
if(not self.isFlagReachMax()):
self.flagContainer.append([i, j])
self.currentFlagNumber += 1
def cmdShow(self):
"""
在cmd中打印当前状态
"""
print("X\t", end="")
for j in range(self.width):
print("\033[33m%s\033[0m\t" % j, end="")
print("")
for i in range(self.height):
print("\033[33m%s\033[0m\t" % i, end="")
for j in range(self.width):
if(self.maskField[i][j]):
if([i, j] in self.flagContainer):
print("F\t", end="")
else:
print(".\t", end="")
else:
print("%s\t" % self.mineField[i][j], end="")
print("")
def swampGrid(self, i, j):
"""
对着i, j的四周一圈扫雷
Parameters
----------
i : int
i行
j : int
j列
"""
if(self.maskField[i][j]):
return
for di in [-1, 0, 1]:
for dj in [-1, 0, 1]:
if((di == 0 and dj == 0) or (not (0 <= i + di < self.height)) or (not (0 <= j + dj < self.width))):
continue
if([i+di, j+dj] in self.flagContainer and self.mineField[i+di][j+dj] != "*"):
return
if([i+di, j+dj] not in self.flagContainer and self.mineField[i+di][j+dj] == "*"):
if(self.isFlagReachMax()):
self.flipGrid(i+di, j+dj)
return
for di in [-1, 0, 1]:
for dj in [-1, 0, 1]:
if((di == 0 and dj == 0) or (not (0 <= i + di < self.height)) or (not (0 <= j + dj < self.width))):
continue
if(self.maskField[i+di][j+dj] and [i+di, j+dj] not in self.flagContainer):
self.flipGrid(i+di, j+dj)
def isGamewin(self):
"""
判断是否游戏胜利
"""
if(sum(sum(self.maskField)) == self.mineNumber):
return True
return False
def gamewin(self):
"""
游戏胜利
"""
for i in range(self.height):
for j in range(self.width):
if(self.maskField[i][j] and [i, j] not in self.flagContainer):
self.flagContainer.append([i, j])
print("game win!")
def action(self, signal, i, j):
"""
采取动作,与图像界面直接连接
信号1:翻雷
信号2:flag
信号3:扫雷
Parameters
----------
signal : int
信号
i : int
第i行
j : int
第j列
"""
if(signal == 1):
self.flipGrid(i, j)
elif(signal == 2):
self.flagGrid(i, j)
elif(signal == 3):
self.swampGrid(i, j)
display.py
from pygame import event
from stage import *
import pygame_menu
import pygame
class Display:
"""
显示界面
"""
def __init__(self):
self.stage = Stage()
def clickOperation(self, event, mousePos):
"""
点击鼠标时触发事件
"""
j = mousePos[0] // self.gridSize
i = mousePos[1] // self.gridSize
if(not self.stage.isGameover()):
if(pygame.mouse.get_pressed()[0] and pygame.mouse.get_pressed()[2]):
self.stage.action(3, i, j)
elif(event.button == 1):
self.stage.action(1, i, j)
elif(event.button == 3):
self.stage.action(2, i, j)
if(self.stage.isGamewin()):
self.stage.gamewin()
def gameStage(self):
"""
游戏图形界面
"""
while True:
events = pygame.event.get()
for event in events:
if(event.type == pygame.QUIT):
exit()
if(event.type == pygame.MOUSEBUTTONDOWN):
self.clickOperation(event, pygame.mouse.get_pos())
if(event.type == pygame.KEYDOWN):
if(pygame.key.get_pressed()[pygame.K_r]):
self.stage = Stage()
self.gameStageDraw()
pygame.display.update()
def gameStageDraw(self):
"""
对gameStage进行绘制
"""
self.screen.fill((0,0,0))
maskedGrid_img = pygame.image.load("./img/maskedGrid.png")
maskedGrid_rect = maskedGrid_img.get_rect()
unmaskedGrid_img = pygame.image.load("./img/unmaskedGrid.png")
unmaskedGrid_rect = unmaskedGrid_img.get_rect()
flag_img = pygame.image.load("./img/flag.png")
flag_rect = flag_img.get_rect()
mine_img = pygame.image.load("./img/mine.png")
mine_rect = mine_img.get_rect()
for i in range(StageConfig.height):
for j in range(StageConfig.width):
centerx = self.gridSize * (0.5 + j)
centery = self.gridSize * (0.5 + i)
if(self.stage.maskField[i][j]):
maskedGrid_rect.centerx = centerx
maskedGrid_rect.centery = centery
self.screen.blit(maskedGrid_img, maskedGrid_rect)
else:
unmaskedGrid_rect.centerx = centerx
unmaskedGrid_rect.centery = centery
self.screen.blit(unmaskedGrid_img, unmaskedGrid_rect)
if('1' <= self.stage.mineField[i][j] <= '9'):
number_img = pygame.image.load("./img/%s.png" % self.stage.mineField[i][j])
number_rect = number_img.get_rect()
number_img = pygame.transform.scale(number_img, (int(number_rect.size[0] * 0.6), int(number_rect.size[1] * 0.6)))
number_rect = number_img.get_rect()
number_rect.centerx = centerx
number_rect.centery = centery
self.screen.blit(number_img, number_rect)
if(self.stage.isGameover() and self.stage.mineField[i][j] == '*'):
mine_rect.centerx = centerx
mine_rect.centery = centery
self.screen.blit(mine_img, mine_rect)
for eachFlagPoint in self.stage.flagContainer:
flag_rect.centerx = self.gridSize * (0.5 + eachFlagPoint[1])
flag_rect.centery = self.gridSize * (0.5 + eachFlagPoint[0])
self.screen.blit(flag_img, flag_rect)
flag_rect.centerx = 25
flag_rect.centery = self.gridSize * self.stage.height + 25
self.screen.blit(flag_img, flag_rect)
font = pygame.font.SysFont("arial", 35)
img = font.render('x %s' % str(self.stage.flagLimit - len(self.stage.flagContainer)), True, (255, 255, 255))
rect = img.get_rect()
rect.left = 50
rect.centery = self.gridSize * self.stage.height + 25
self.screen.blit(img, rect)
mousePos = pygame.mouse.get_pos()
j = mousePos[0] // self.gridSize
i = mousePos[1] // self.gridSize
if(0 <= j < self.stage.width and 0 <= i < self.stage.height):
highLightGrid = pygame.Surface((50, 50))
highLightGrid.set_alpha(128)
highLightGrid.fill((255, 255, 255))
self.screen.blit(highLightGrid, (j * self.gridSize, i * self.gridSize))
font = pygame.font.SysFont("arial", 35)
if(self.stage.isGameover()):
img = font.render('Game Over', True, (255, 0, 0))
rect = img.get_rect()
rect.centerx = self.width / 2
rect.centery = self.gridSize * self.stage.height + 25
self.screen.blit(img, rect)
elif(self.stage.isGamewin()):
img = font.render('Game Win', True, (0, 255, 0))
rect = img.get_rect()
rect.centerx = self.width / 2
rect.centery = self.gridSize * self.stage.height + 25
self.screen.blit(img, rect)
def initForm(self):
"""
初始化图形界面
"""
pygame.init()
self.gridSize = 50
self.width = StageConfig.width * self.gridSize
self.height = StageConfig.height * self.gridSize + 50
self.screen = pygame.display.set_mode((self.width, self.height))
def mainLoop(self):
"""
主循环
"""
self.initForm()
self.gameStage()
start.py
from display import *
d = Display()
d.mainLoop()
|