基于python的“扫雷”游戏实现
一、引言:
最近在学习python语言,想着尝试通过python来实现儿时玩过的小游戏,于是从"扫雷"游戏开始,依据自己的理解,编写游戏代码。若有不周到之处,还望大家批评指正。
环境配置:python3.12, pygame2.6.1, numpy1.26.4
二、效果展示
灰色为未揭开的单元,红色表示地雷,绿色表示“插旗”(即认定此单元格为地雷)
三、程序思路
1.程序框架图

如图所示,我们需要创建“扫雷”用的棋盘,并通过实时的输入来修改棋盘中的数据,从而检测是否达到游戏结束的条件。
2.具体程序实现
(1)生成棋盘(初始化)
本项目采用正方形棋盘,基于numpy的array对象来生成棋盘上每一个点对应的属性。每个单元格的数值说明如下:
数值 | 含义 |
---|---|
-1 | 未触碰的地雷 |
0~8 |
当前九宫格内的地雷总数 |
-2 | 被触碰过的地雷 |
10~18 | 被触碰过的普通单元(无地雷) |
19~28 | 被认定为地雷的单元 |
例如 1 表示当前九宫格内的地雷数为1,当鼠标左键点击这个单元后,1自加10变成11,于是这个单元变为被触碰过的普通单元(对应显示为数字);而当鼠标右键单机这个单元时,1自加20变成21,于是这个单元被标记为“可能含有地雷”(对应显示为绿色)
01.生成地雷
本项目采用numpy.random.randint()生成的随机数来生成地雷,具体生成方式为:遍历棋盘上每一个单元格,若当前单元格数值为0,则尝试在该单元格生成地雷(概率为0.5),直到生成的地雷总数达到预设值。
def set_bomb(self):
bomb_total = 0
enough = False
while not enough:
for i in range(1, self.size + 1):
for j in range(1, self.size + 1):
if self.board[i, j] == 0 and not enough:
temp = np.random.randint(0, 100)
if temp < 50:
self.board[i, j] = -1
bomb_total += 1
if bomb_total == self.num_bomb:
enough = True
这里索引值设置为(1,self.size+1)目的是为了使得棋盘边缘处单元格数值均为0,便于后续的计算。例如下面的棋盘,游戏区域中心的为10*10区域,其外侧均为0,此时计算(1,1)位置单元格(下方标红位置)所处九宫格的地雷总数时,便不会存在溢出的问题。
[ 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[ 0. 1. -1. 3. -1. -1. -1. -1. 3. 3. -1. 0.]
[ 0. 1. 1. 3. -1. -1. 7. -1. 4. -1. -1. 0.]
[ 0. 2. 3. 3. 4. -1. 4. -1. 4. 5. -1. 0.]
[ 0. -1. -1. -1. 4. 2. 3. 4. -1. 5. -1. 0.]
[ 0. -1. -1. -1. -1. 3. 2. -1. -1. 6. -1. 0.]
[ 0. -1. -1. -1. -1. 3. -1. 4. -1. -1. 2. 0.]
[ 0. 2. 3. 3. 2. 2. 1. 2. 2. 2. 1. 0.]
[ 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[ 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[ 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[ 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
02.生成普通单元格
即生成普通单元格为中心的九宫格内的地雷总数,并将该数值写入该单元格。
def set_number(self):
for i in range(1, self.size + 1):
for j in range(1, self.size + 1):
number = 0
if self.board[i, j] == 0:
for x in range(-1, 2):
for y in range(-1, 2):
if self.board[i + x, j + y] == -1:
number += 1
self.board[i, j] = number
(2)图形化棋盘
被触碰的地雷,渲染对应的单元格为 红色 正方形
被 “插旗”(被认定为地雷)的单元格,渲染对应的单元格为 绿色 正方形
被触碰的普通单元格,显示当前单元格所在九宫格的地雷数
未被触碰的普通单元格,渲染对应的单元格为 灰色 正方形
def draw(self):
for i in range(1, self.size + 1):
for j in range(1, self.size + 1):
if 19 > self.board[i, j] > 9:
font = pygame.font.SysFont('Arial', 10)
if self.board[i, j] == 10:
pass
else:
text = font.render(f'{int(self.board[i, j] - 10)}', True, (0, 0, 0))
self.screen.blit(text, [i*35 + 10, j*35 + 10])
else:
pygame.draw.rect(self.screen, (100, 100, 100), pygame.Rect([i*35, j*35, 35, 35]))
if self.board[i, j] == -2:
pygame.draw.rect(self.screen, 'red', pygame.Rect([i*35, j*35, 35, 35]))
if self.board[i, j] > 18:
pygame.draw.rect(self.screen, (0, 180, 0), pygame.Rect([i*35, j*35, 35, 35]))
pygame.draw.rect(self.screen, (0, 0, 0), [i*35, j*35, 35, 35], 1)
pygame.display.flip()
(3)检测输入并更新棋盘
检测鼠标点击的位置和点击方式,执行对应的程序。当鼠标左键单击时,将当前单元格标记为 “已触碰” ,当鼠标右键单击时,将当前单元格标记为 “ 认定为地雷”(对应“插旗”),再次鼠标右键单击则取消标记。
def update_board(self):
if not self.game_over:
x, y = self._pos
x, y = x//35, y//35
if self.board.board[x, y] == -1:
self.board.board[x, y] = -2
self.game_over = True
self.find_neighbour(x, y)
def update_board2(self):
if not self.game_over:
x, y = self._pos
x, y = x//35, y//35
if -2 < self.board.board[x, y] < 10:
self.board.board[x, y] += 20
elif 18 < self.board.board[x, y]:
self.board.board[x, y] -= 20
01.自动触碰“8联通”的单元格
当鼠标左键单击某一普通单元时,会自动将该单元格的 “8联通” 区域执行触碰操作,以下算法又被称为 “递归回溯法” ,感兴趣的伙伴可以自行了解更多内容。
def find_neighbour(self, x, y): # 八连通关系
if 0 < x < self.board.size + 1 and 0 < y < self.board.size + 1:
if 10 > self.board.board[x, y] > -1:
for i in range(-1, 2):
for j in range(-1, 2):
if 10 > self.board.board[i + x, y + j] > -1:
self.board.board[i + x, y + j] += 10
self.find_neighbour(x + 2, y)
self.find_neighbour(x - 2, y)
self.find_neighbour(x, y + 2)
self.find_neighbour(x, y - 2)
(4)检测棋盘
01.游戏失败
当触碰到地雷时,判定为游戏失败。
def update_board(self):
if not self.game_over:
x, y = self._pos
x, y = x//35, y//35
if self.board.board[x, y] == -1:
self.board.board[x, y] = -2
self.game_over = True
self.find_neighbour(x, y)
02.游戏胜利
当找到所有地雷,即将所有地雷标记为 “认定为地雷” 时,判定游戏胜利。
def win(self):
found_bomb = 0
for i in range(1, self.size + 1):
for j in range(1, self.size + 1):
if self.board[i, j] == 19:
found_bomb += 1
if found_bomb == self.num_bomb:
self.win_flag = True
(5)游戏结束
游戏胜利显示“win”,游戏失败显示"GAME OVER"。
def end(self):
pos = ((self.board.size // 2 * 35), 5)
font = pygame.font.SysFont('Arial', 20)
if self.board.win_flag:
text = font.render('Win', 1, (0, 0, 0))
else:
text = font.render('GAME OVER', True, (0, 0, 0))
self.screen.blit(text, pos)
3.主函数
size表示棋盘的大小(例如size=10,则棋盘为10*10),bomb为设定的地雷总数。可以自行更改数值,来实现不同大小的游戏区域和地雷总数。
def main():
pygame.init()
size = 10
bomb = size ** 2 // 3
screen = pygame.display.set_mode(((size + 2) * 35, (size + 2) * 35))
game_section = Board(size, bomb, screen)
# print(game_section.board)
click = Click((0, 0), game_section, screen)
screen.fill((230, 230, 230))
game_section.draw()
running = True
while running:
for event in pygame.event.get():
game_section.win()
if event.type == pygame.QUIT:
running = False
if event.type == pygame.MOUSEBUTTONDOWN:
screen.fill((230, 230, 230))
click.pos = pygame.mouse.get_pos()
if event.button == 1:
click.update_board()
if event.button == 3:
click.update_board2()
if click.game_over or game_section.win_flag:
click.end()
game_section.draw()
四、完整代码
import pygame
import numpy as np
class Board(object):
def __init__(self, size, num_bomb, screen):
self.size = size
self.num_bomb = num_bomb
self.board = np.zeros((self.size + 2, self.size + 2))
self.screen = screen
self.set_bomb()
self.set_number()
self.win_flag = False
def set_bomb(self):
bomb_total = 0
enough = False
while not enough:
for i in range(1, self.size + 1):
for j in range(1, self.size + 1):
if self.board[i, j] == 0 and not enough:
temp = np.random.randint(0, 100)
if temp < 50:
self.board[i, j] = -1
bomb_total += 1
if bomb_total == self.num_bomb:
enough = True
def set_number(self):
for i in range(1, self.size + 1):
for j in range(1, self.size + 1):
number = 0
if self.board[i, j] == 0:
for x in range(-1, 2):
for y in range(-1, 2):
if self.board[i + x, j + y] == -1:
number += 1
self.board[i, j] = number
def draw(self):
for i in range(1, self.size + 1):
for j in range(1, self.size + 1):
if 19 > self.board[i, j] > 9:
font = pygame.font.SysFont('Arial', 10)
if self.board[i, j] == 10:
pass
else:
text = font.render(f'{int(self.board[i, j] - 10)}', True, (0, 0, 0))
self.screen.blit(text, [i*35 + 10, j*35 + 10])
else:
pygame.draw.rect(self.screen, (100, 100, 100), pygame.Rect([i*35, j*35, 35, 35]))
if self.board[i, j] == -2:
pygame.draw.rect(self.screen, 'red', pygame.Rect([i*35, j*35, 35, 35]))
if self.board[i, j] > 18:
pygame.draw.rect(self.screen, (0, 180, 0), pygame.Rect([i*35, j*35, 35, 35]))
pygame.draw.rect(self.screen, (0, 0, 0), [i*35, j*35, 35, 35], 1)
pygame.display.flip()
def win(self):
found_bomb = 0
for i in range(1, self.size + 1):
for j in range(1, self.size + 1):
if self.board[i, j] == 19:
found_bomb += 1
if found_bomb == self.num_bomb:
self.win_flag = True
class Click(object):
def __init__(self, pos, board, screen):
self._pos = pos
self.board = board
self.screen = screen
self.game_over = False
@property
def pos(self):
return self._pos
@pos.setter
def pos(self, value):
self._pos = value
def update_board(self):
if not self.game_over:
x, y = self._pos
x, y = x//35, y//35
if self.board.board[x, y] == -1:
self.board.board[x, y] = -2
self.game_over = True
self.find_neighbour(x, y)
def update_board2(self):
if not self.game_over:
x, y = self._pos
x, y = x//35, y//35
if -2 < self.board.board[x, y] < 10:
self.board.board[x, y] += 20
elif 18 < self.board.board[x, y]:
self.board.board[x, y] -= 20
def end(self):
pos = ((self.board.size // 2 * 35), 5)
font = pygame.font.SysFont('Arial', 20)
if self.board.win_flag:
text = font.render('Win', 1, (0, 0, 0))
else:
text = font.render('GAME OVER', True, (0, 0, 0))
self.screen.blit(text, pos)
def find_neighbour(self, x, y): # 八连通关系
if 0 < x < self.board.size + 1 and 0 < y < self.board.size + 1:
if 10 > self.board.board[x, y] > -1:
for i in range(-1, 2):
for j in range(-1, 2):
if 10 > self.board.board[i + x, y + j] > -1:
self.board.board[i + x, y + j] += 10
self.find_neighbour(x + 2, y)
self.find_neighbour(x - 2, y)
self.find_neighbour(x, y + 2)
self.find_neighbour(x, y - 2)
def main():
pygame.init()
size = 10
bomb = size ** 2 // 3
screen = pygame.display.set_mode(((size + 2) * 35, (size + 2) * 35))
game_section = Board(size, bomb, screen)
# print(game_section.board)
click = Click((0, 0), game_section, screen)
screen.fill((230, 230, 230))
game_section.draw()
running = True
while running:
for event in pygame.event.get():
game_section.win()
if event.type == pygame.QUIT:
running = False
if event.type == pygame.MOUSEBUTTONDOWN:
screen.fill((230, 230, 230))
click.pos = pygame.mouse.get_pos()
if event.button == 1:
click.update_board()
if event.button == 3:
click.update_board2()
if click.game_over or game_section.win_flag:
click.end()
game_section.draw()
if __name__ == '__main__':
main()
五、项目总结
本项目基于pycharm2024实现,使用了numpy和pygame库,代码总共155行(加上所有的空行),是一个轻量化的项目。本项目实现了“扫雷”游戏的主体内容,即生成棋盘,根据输入更新棋盘。此外,本项目的扩展方向可以为优化显示内容,例如将触碰后的地雷渲染为 “地雷图片” ,而不是简单的以红色来表示。
作者:m0_74802518