0 项目简介
🔥 Hi,各位同学好呀,这里是L学长!
🥇今天向大家分享一个今年(2022)最新完成的毕业设计项目作品
python小游戏毕设 扫雷小游戏设计与实现 (源码)
🥇 学长根据实现的难度和等级对项目进行评分(最低0分,满分5分)
-
难度系数:3分
-
工作量:3分
-
创新点:4分
1 游戏介绍
《扫雷》是一款大众类的益智小游戏,于1992年发行。游戏目标是在最短的时间内根据点击格子出现的数字找出所有非雷格子,同时避免踩雷,踩到一个雷即全盘皆输。
今天我们利用Python实现经典扫雷游戏
2 实现效果
3 开发工具
3.1 环境配置
-
Python版本:3.6.4
-
相关模块:
-
pygame模块;
-
以及一些Python自带的模块。
3.2 Pygame介绍
简介
Pygame是一系列专门为编写电子游戏而设计的Python模块(modules)。Pygame在已经非常优秀的SDL库的基础上增加了许多功能。这让你能够用Python语言编写出丰富多彩的游戏程序。
Pygame可移植性高,几乎能在任何平台和操作系统上运行。
Pygame已经被下载过数百万次。
Pygame免费开源。它在LGPL许可证(Lesser General Public License,GNU宽通用公共许可证)下发行。使用Pygame,你可以创造出免费开源,可共享,或者商业化的游戏。详情请见LGPL许可证。
优点
-
能够轻松使用多核CPU(multi core CPUs) :如今双核CPU很常用,8核CPU在桌面系统中也很便宜,而利用好多核系统,能让你在你的游戏中实现更多东西。特定的pygame函数能够释放令人生畏的python GIL(全局解释器锁),这几乎是你用C语言才能做的事。
-
核心函数用最优化的C语言或汇编语言编写:C语言代码通常比Python代码运行速度快10-20倍。而汇编语言编写的代码(assembly code)比Python甚至快到100多倍。
-
安装便捷:一般仅需包管理程序或二进制系统程序便能安装。
-
真正地可移植:支持Linux (主要发行版), Windows (95, 98, ME, 2000, XP, Vista, 64-bit Windows,), Windows CE, BeOS, MacOS, Mac OS X, FreeBSD, NetBSD, OpenBSD, BSD/OS, Solaris, IRIX, and QNX等操作系统.也能支持AmigaOS, Dreamcast, Atari, AIX, OSF/Tru64, RISC OS, SymbianOS and OS/2,但是还没有受到官方认可。你也可以在手持设备,游戏控制台, One Laptop Per Child (OLPC) computer项目的电脑等设备中使用pygame.
-
用法简单:无论是小孩子还是大人都能学会用pygame来制作射击类游戏。
-
很多Pygame游戏已发行:其中包括很多游戏大赛入围作品、非常受欢迎的开源可分享的游戏。
-
由你来控制主循环:由你来调用pygame的函数,pygame的函数并不需要调用你的函数。当你同时还在使用其他库来编写各种各种的程序时,这能够为你提供极大的掌控权。
-
不需要GUI就能使用所有函数:仅在命令行中,你就可以使用pygame的某些函数来处理图片,获取游戏杆输入,播放音乐……
-
对bug反应迅速:很多bug在被上报的1小时内就能被我们修复。虽然有时候我们确实会卡在某一个bug上很久,但大多数时候我们都是很不错的bug修复者。如今bug的上报已经很少了,因为许多bug早已被我们修复。
-
代码量少:pygame并没有数以万计的也许你永远用不到的冗杂代码。pygame的核心代码一直保持着简洁特点,其他附加物诸如GUI库等,都是在核心代码之外单独设计研发的。
-
模块化:你可以单独使用pygame的某个模块。想要换着使用一个别的声音处理库?没问题。pygame的很多核心模块支持独立初始化与使用。
最小开发框架
import pygame,sys #sys是python的标准库,提供Python运行时环境变量的操控
pygame.init() #内部各功能模块进行初始化创建及变量设置,默认调用
size = width,height = 800,600 #设置游戏窗口大小,分别是宽度和高度
screen = pygame.display.set_mode(size) #初始化显示窗口
pygame.display.set_caption("小游戏程序") #设置显示窗口的标题内容,是一个字符串类型
while True: #无限循环,直到Python运行时退出结束
for event in pygame.event.get(): #从Pygame的事件队列中取出事件,并从队列中删除该事件
if event.type == pygame.QUIT: #获得事件类型,并逐类响应
sys.exit() #用于退出结束游戏并退出
pygame.display.update() #对显示窗口进行更新,默认窗口全部重绘
代码执行流程
4 具体实现
首先还是先初始化一下游戏:
# 游戏初始化
pygame.init()
screen = pygame.display.set_mode(cfg.SCREENSIZE)
pygame.display.set_caption('mine sweeper —— DCGAME')
然后把需要用到的字体,图片,音乐啥的都导入进来:
# 导入所有图片
images = {}
for key, value in cfg.IMAGE_PATHS.items():
if key in ['face_fail', 'face_normal', 'face_success']:
image = pygame.image.load(value)
images[key] = pygame.transform.smoothscale(image, (int(cfg.GRIDSIZE*1.25), int(cfg.GRIDSIZE*1.25)))
else:
image = pygame.image.load(value).convert()
images[key] = pygame.transform.smoothscale(image, (cfg.GRIDSIZE, cfg.GRIDSIZE))
# 载入字体
font = pygame.font.Font(cfg.FONT_PATH, cfg.FONT_SIZE)
# 导入并播放背景音乐
pygame.mixer.music.load(cfg.BGM_PATH)
pygame.mixer.music.play(-1)
接着,我们来定义一个文字板,用于显示左上角的埋雷数量和右上角的游戏已进行时间:
'''文字板'''
class TextBoard(pygame.sprite.Sprite):
def __init__(self, text, font, position, color, **kwargs):
pygame.sprite.Sprite.__init__(self)
self.text = text
self.font = font
self.position = position
self.color = color
def draw(self, screen):
text_render = self.font.render(self.text, True, self.color)
screen.blit(text_render, self.position)
def update(self, text):
self.text = text
其实很简单,只需要把用字体渲染之后的文本对象绑定到屏幕上就行,然后设置一个update函数,来实时更新里面的文本内容。
然后,我们再定义一个表情按钮类:
'''表情按钮'''
class EmojiButton(pygame.sprite.Sprite):
def __init__(self, images, position, status_code=0, **kwargs):
pygame.sprite.Sprite.__init__(self)
# 导入图片
self.images = images
self.image = self.images['face_normal']
self.rect = self.image.get_rect()
self.rect.left, self.rect.top = position
# 表情按钮的当前状态
self.status_code = status_code
'''画到屏幕上'''
def draw(self, screen):
# 状态码为0, 代表正常的表情
if self.status_code == 0:
self.image = self.images['face_normal']
# 状态码为1, 代表失败的表情
elif self.status_code == 1:
self.image = self.images['face_fail']
# 状态码为2, 代表成功的表情
elif self.status_code == 2:
self.image = self.images['face_success']
# 绑定图片到屏幕
screen.blit(self.image, self.rect)
'''设置当前的按钮的状态'''
def setstatus(self, status_code):
self.status_code = status_code
当鼠标点击到这个按钮的时,就重新开始新的游戏(无论当前的游戏状态如何,都将重新开始新的游戏):
接下来,我们需要定义的就是下面的方格类了:
'''雷'''
class Mine(pygame.sprite.Sprite):
def __init__(self, images, position, status_code=0, **kwargs):
pygame.sprite.Sprite.__init__(self)
# 导入图片
self.images = images
self.image = self.images['blank']
self.rect = self.image.get_rect()
self.rect.left, self.rect.top = position
# 雷当前的状态
self.status_code = status_code
# 真雷还是假雷(默认是假雷)
self.is_mine_flag = False
# 周围雷的数目
self.num_mines_around = -1
'''设置当前的状态码'''
def setstatus(self, status_code):
self.status_code = status_code
'''埋雷'''
def burymine(self):
self.is_mine_flag = True
'''设置周围雷的数目'''
def setnumminesaround(self, num_mines_around):
self.num_mines_around = num_mines_around
'''画到屏幕上'''
def draw(self, screen):
# 状态码为0, 代表该雷未被点击
if self.status_code == 0:
self.image = self.images['blank']
# 状态码为1, 代表该雷已被点开
elif self.status_code == 1:
self.image = self.images['mine'] if self.is_mine_flag else self.images[str(self.num_mines_around)]
# 状态码为2, 代表该雷被玩家标记为雷
elif self.status_code == 2:
self.image = self.images['flag']
# 状态码为3, 代表该雷被玩家标记为问号
elif self.status_code == 3:
self.image = self.images['ask']
# 状态码为4, 代表该雷正在被鼠标左右键双击
elif self.status_code == 4:
assert not self.is_mine_flag
self.image = self.images[str(self.num_mines_around)]
# 状态码为5, 代表该雷在被鼠标左右键双击的雷的周围
elif self.status_code == 5:
self.image = self.images['0']
# 状态码为6, 代表该雷被踩中
elif self.status_code == 6:
assert self.is_mine_flag
self.image = self.images['blood']
# 状态码为7, 代表该雷被误标
elif self.status_code == 7:
assert not self.is_mine_flag
self.image = self.images['error']
# 绑定图片到屏幕
screen.blit(self.image, self.rect)
@property
def opened(self):
return self.status_code == 1
它的主要作用就是记录游戏地图中某个方格的状态(比如是不是埋了雷呀,有没有被点开呀,有没有被标记呀之类的)。
最后定义一个游戏地图类,来把游戏地图中的所有方格都整合在一起方便在游戏主循环里调用更新:
'''扫雷地图'''
class MinesweeperMap():
def __init__(self, cfg, images, **kwargs):
self.cfg = cfg
# 雷型矩阵
self.mines_matrix = []
for j in range(cfg.GAME_MATRIX_SIZE[1]):
mines_line = []
for i in range(cfg.GAME_MATRIX_SIZE[0]):
position = i * cfg.GRIDSIZE + cfg.BORDERSIZE, (j + 2) * cfg.GRIDSIZE
mines_line.append(Mine(images=images, position=position))
self.mines_matrix.append(mines_line)
# 随机埋雷
for i in random.sample(range(cfg.GAME_MATRIX_SIZE[0]*cfg.GAME_MATRIX_SIZE[1]), cfg.NUM_MINES):
self.mines_matrix[i//cfg.GAME_MATRIX_SIZE[0]][i%cfg.GAME_MATRIX_SIZE[0]].burymine()
count = 0
for item in self.mines_matrix:
for i in item:
count += int(i.is_mine_flag)
# 游戏当前的状态
self.status_code = -1
# 记录鼠标按下时的位置和按的键
self.mouse_pos = None
self.mouse_pressed = None
'''画出当前的游戏状态图'''
def draw(self, screen):
for row in self.mines_matrix:
for item in row: item.draw(screen)
'''设置当前的游戏状态'''
def setstatus(self, status_code):
# 0: 正在进行游戏, 1: 游戏结束, -1: 游戏还没开始
self.status_code = status_code
'''根据玩家的鼠标操作情况更新当前的游戏状态地图'''
def update(self, mouse_pressed=None, mouse_pos=None, type_='down'):
assert type_ in ['down', 'up']
# 记录鼠标按下时的位置和按的键
if type_ == 'down' and mouse_pos is not None and mouse_pressed is not None:
self.mouse_pos = mouse_pos
self.mouse_pressed = mouse_pressed
# 鼠标点击的范围不在游戏地图内, 无响应
if self.mouse_pos[0] < self.cfg.BORDERSIZE or self.mouse_pos[0] > self.cfg.SCREENSIZE[0] - self.cfg.BORDERSIZE or \
self.mouse_pos[1] < self.cfg.GRIDSIZE * 2 or self.mouse_pos[1] > self.cfg.SCREENSIZE[1] - self.cfg.BORDERSIZE:
return
# 鼠标点击在游戏地图内, 代表开始游戏(即可以开始计时了)
if self.status_code == -1:
self.status_code = 0
# 如果不是正在游戏中, 按鼠标是没有用的
if self.status_code != 0:
return
# 鼠标位置转矩阵索引
coord_x = (self.mouse_pos[0] - self.cfg.BORDERSIZE) // self.cfg.GRIDSIZE
coord_y = self.mouse_pos[1] // self.cfg.GRIDSIZE - 2
mine_clicked = self.mines_matrix[coord_y][coord_x]
# 鼠标按下
if type_ == 'down':
# --鼠标左右键同时按下
if self.mouse_pressed[0] and self.mouse_pressed[2]:
if mine_clicked.opened and mine_clicked.num_mines_around > 0:
mine_clicked.setstatus(status_code=4)
num_flags = 0
coords_around = self.getaround(coord_y, coord_x)
for (j, i) in coords_around:
if self.mines_matrix[j][i].status_code == 2:
num_flags += 1
if num_flags == mine_clicked.num_mines_around:
for (j, i) in coords_around:
if self.mines_matrix[j][i].status_code == 0:
self.openmine(i, j)
else:
for (j, i) in coords_around:
if self.mines_matrix[j][i].status_code == 0:
self.mines_matrix[j][i].setstatus(status_code=5)
# 鼠标释放
else:
# --鼠标左键
if self.mouse_pressed[0] and not self.mouse_pressed[2]:
if not (mine_clicked.status_code == 2 or mine_clicked.status_code == 3):
if self.openmine(coord_x, coord_y):
self.setstatus(status_code=1)
# --鼠标右键
elif self.mouse_pressed[2] and not self.mouse_pressed[0]:
if mine_clicked.status_code == 0:
mine_clicked.setstatus(status_code=2)
elif mine_clicked.status_code == 2:
mine_clicked.setstatus(status_code=3)
elif mine_clicked.status_code == 3:
mine_clicked.setstatus(status_code=0)
# --鼠标左右键同时按下
elif self.mouse_pressed[0] and self.mouse_pressed[2]:
mine_clicked.setstatus(status_code=1)
coords_around = self.getaround(coord_y, coord_x)
for (j, i) in coords_around:
if self.mines_matrix[j][i].status_code == 5:
self.mines_matrix[j][i].setstatus(status_code=0)
'''打开雷'''
def openmine(self, x, y):
mine_clicked = self.mines_matrix[y][x]
if mine_clicked.is_mine_flag:
for row in self.mines_matrix:
for item in row:
if not item.is_mine_flag and item.status_code == 2:
item.setstatus(status_code=7)
elif item.is_mine_flag and item.status_code == 0:
item.setstatus(status_code=1)
mine_clicked.setstatus(status_code=6)
return True
mine_clicked.setstatus(status_code=1)
coords_around = self.getaround(y, x)
num_mines = 0
for (j, i) in coords_around:
num_mines += int(self.mines_matrix[j][i].is_mine_flag)
mine_clicked.setnumminesaround(num_mines)
if num_mines == 0:
for (j, i) in coords_around:
if self.mines_matrix[j][i].num_mines_around == -1:
self.openmine(i, j)
return False
'''获得坐标点的周围坐标点'''
def getaround(self, row, col):
coords = []
for j in range(max(0, row-1), min(row+1, self.cfg.GAME_MATRIX_SIZE[1]-1)+1):
for i in range(max(0, col-1), min(col+1, self.cfg.GAME_MATRIX_SIZE[0]-1)+1):
if j == row and i == col:
continue
coords.append((j, i))
return coords
'''是否正在游戏中'''
@property
def gaming(self):
return self.status_code == 0
'''被标记为雷的雷数目'''
@property
def flags(self):
num_flags = 0
for row in self.mines_matrix:
for item in row: num_flags += int(item.status_code == 2)
return num_flags
'''已经打开的雷的数目'''
@property
def openeds(self):
num_openeds = 0
for row in self.mines_matrix:
for item in row: num_openeds += int(item.opened)
return num_openeds
这里只解释几个可能有小伙伴看不太懂的地方:
-
打开雷的时候我们用了递归,作用是当点击到的方格周围都没有雷的时候,系统就自动打开这个方格周围的方格,以实现有时候点击一个方格可以打开一大片方格的效果,这里的周围都特指以目标方格为中心的九宫格内的所有方格;
-
鼠标左右键一起按在已经打开的方格上的话,如果这个方格周围的方格已经被标记为雷的数目和这个方格上显示的数字一致,就把这个方格周围未被标记为雷的方格都打开(所以如果你标记错的话,一起打开的时候会显示你游戏已经GG了)。
其他也没啥好说的了,感觉关注我的小伙伴应该都挺聪明的,自己看代码就能看懂吧。
定义完这些游戏中必要的元素类之后就在游戏主函数里实例化它们:
# 实例化游戏地图
minesweeper_map = MinesweeperMap(cfg, images)
position = (cfg.SCREENSIZE[0] - int(cfg.GRIDSIZE * 1.25)) // 2, (cfg.GRIDSIZE * 2 - int(cfg.GRIDSIZE * 1.25)) // 2
emoji_button = EmojiButton(images, position=position)
fontsize = font.size(str(cfg.NUM_MINES))
remaining_mine_board = TextBoard(str(cfg.NUM_MINES), font, (30, (cfg.GRIDSIZE*2-fontsize[1])//2-2), cfg.RED)
fontsize = font.size('000')
time_board = TextBoard('000', font, (cfg.SCREENSIZE[0]-30-fontsize[0], (cfg.GRIDSIZE*2-fontsize[1])//2-2), cfg.RED)
time_board.is_start = False
然后写个游戏主循环以根据用户的操作来更新当前的游戏状态就ok啦:
# 游戏主循环
clock = pygame.time.Clock()
while True:
screen.fill(cfg.BACKGROUND_COLOR)
# --按键检测
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
elif event.type == pygame.MOUSEBUTTONDOWN:
mouse_pos = event.pos
mouse_pressed = pygame.mouse.get_pressed()
minesweeper_map.update(mouse_pressed=mouse_pressed, mouse_pos=mouse_pos, type_='down')
elif event.type == pygame.MOUSEBUTTONUP:
minesweeper_map.update(type_='up')
if emoji_button.rect.collidepoint(pygame.mouse.get_pos()):
minesweeper_map = MinesweeperMap(cfg, images)
time_board.update('000')
time_board.is_start = False
remaining_mine_board.update(str(cfg.NUM_MINES))
emoji_button.setstatus(status_code=0)
# --更新时间显示
if minesweeper_map.gaming:
if not time_board.is_start:
start_time = time.time()
time_board.is_start = True
time_board.update(str(int(time.time() - start_time)).zfill(3))
# --更新剩余雷的数目显示
remianing_mines = max(cfg.NUM_MINES - minesweeper_map.flags, 0)
remaining_mine_board.update(str(remianing_mines).zfill(2))
# --更新表情
if minesweeper_map.status_code == 1:
emoji_button.setstatus(status_code=1)
if minesweeper_map.openeds + minesweeper_map.flags == cfg.GAME_MATRIX_SIZE[0] * cfg.GAME_MATRIX_SIZE[1]:
minesweeper_map.status_code = 1
emoji_button.setstatus(status_code=2)
# --显示当前的游戏状态地图
minesweeper_map.draw(screen)
emoji_button.draw(screen)
remaining_mine_board.draw(screen)
time_board.draw(screen)
# --更新屏幕
pygame.display.update()
clock.tick(cfg.FPS)
到现在就大功告成了