使用 Python 中的 Pygame 完成扫雷游戏并附带源代码
使用 Python 和 Pygame 实现扫雷游戏 — 完整教程与代码示例
本教程将详细指导你如何使用 Python 编程语言和 Pygame 库从零开始构建一个经典的扫雷游戏。我们将涵盖项目设置、核心游戏逻辑、Pygame 渲染与用户交互的实现,并提供可直接运行的代码片段。
目录
1. 引言
扫雷(Minesweeper)是一款经典的单人益智类游戏,其核心在于推理和排除地雷。本教程将带你一步步使用 Pygame 库,从头实现这款游戏。Pygame 是一个用于开发 2D 游戏和多媒体应用程序的 Python 库,它提供了丰富的功能,包括图形绘制、声音播放、事件处理等。
2. 项目概览与功能特性
我们将实现一个功能相对完整的扫雷游戏,包括:
- 可配置的游戏板:支持自定义行、列数和地雷数量。
- 基本交互:鼠标左键揭开方块,右键标记/取消标记旗帜。
- 智能展开:当揭开一个没有地雷的空白方块时,自动展开周围所有相邻的空白方块,直到遇到数字方块。
- 胜负判定:踩到地雷则失败,成功揭开所有非地雷方块则胜利。
- 图形界面:使用 Pygame 绘制游戏板、方块状态(未揭开、已揭开、数字、地雷、旗帜)。
3. 环境准备与依赖安装
在开始之前,请确保你的系统已安装 Python。本教程建议使用 Python 3.8 或更高版本。
-
安装 Python:
如果你尚未安装 Python,请访问 Python 官方网站 下载并安装适合你操作系统的版本。安装时请确保勾选 “Add Python to PATH”(将 Python 添加到系统路径)。 -
创建虚拟环境(推荐):
为了项目的依赖隔离,建议创建一个虚拟环境。打开命令行或终端,进入你希望存放项目的目录,然后执行:# 创建名为 'venv' 的虚拟环境 python -m venv venv -
激活虚拟环境:
根据你的操作系统,选择相应的命令激活虚拟环境:- Windows:
.\venv\Scripts\activate - macOS / Linux:
source venv/bin/activate
激活后,你的命令行提示符前会显示
(venv),表示你已在虚拟环境中。 - Windows:
-
安装 Pygame:
在虚拟环境激活的状态下,安装 Pygame 库:pip install pygame你也可以将项目所需依赖写入
requirements.txt文件:# requirements.txt pygame然后通过以下命令安装:
pip install -r requirements.txt
4. 核心游戏逻辑设计
游戏逻辑是扫雷项目的核心。我们将把这部分代码组织在独立的模块中。
4.1 游戏板与单元格表示
我们可以使用一个二维列表(或 NumPy 数组)来表示游戏板。每个单元格需要存储其状态:
- 是否是地雷
- 是否已揭开
- 是否被标记为旗帜
- 如果不是地雷,周围有多少个地雷(0-8)
# 示例:一个单元格的内部表示
class Cell:
def __init__(self):
self.is_mine = False # 是否是地雷
self.is_revealed = False # 是否已揭开
self.is_flagged = False # 是否被标记
self.mines_around = 0 # 周围地雷数量
# 游戏板可以是一个 Cell 对象的二维列表
# 例如:board = [[Cell() for _ in range(cols)] for _ in range(rows)]
4.2 地雷的生成与放置
地雷应该随机但均匀地分布在游戏板上。
- 计算总单元格数和要放置的地雷数。
- 生成一个包含所有单元格索引的列表。
- 随机选择地雷数量的索引,并将对应单元格设置为地雷。
import random
def place_mines(board, rows, cols, num_mines):
all_cells = [(r, c) for r in range(rows) for c in range(cols)]
mine_locations = random.sample(all_cells, num_mines)
for r, c in mine_locations:
board[r][c].is_mine = True
return board
4.3 计算周围地雷数量
当地雷放置完毕后,需要遍历所有非地雷单元格,计算其八个方向(包括对角线)相邻单元格中的地雷数量。
def calculate_mines_around(board, rows, cols):
for r in range(rows):
for c in range(cols):
if board[r][c].is_mine:
continue
count = 0
# 检查周围8个方向
for dr in [-1, 0, 1]:
for dc in [-1, 0, 1]:
if dr == 0 and dc == 0: # 跳过自身
continue
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and board[nr][nc].is_mine:
count += 1
board[r][c].mines_around = count
return board
4.4 单元格的揭开与旗帜标记
- 左键点击:
- 如果单元格已标记,则不能揭开。
- 如果单元格是地雷,游戏结束(失败)。
- 如果单元格没有地雷且
mines_around为 0,触发自动展开。 - 将单元格
is_revealed设为True。
- 右键点击:
- 如果单元格已揭开,则不能标记。
- 切换
is_flagged状态。
def reveal_cell(board, r, c, rows, cols):
cell = board[r][c]
if cell.is_revealed or cell.is_flagged:
return False # 无法揭开
cell.is_revealed = True
if cell.is_mine:
return True # 踩到地雷,游戏失败
# 如果揭开的单元格周围地雷数为0,则自动展开
if cell.mines_around == 0:
auto_expand(board, r, c, rows, cols)
return False # 未踩到地雷
def toggle_flag(board, r, c):
cell = board[r][c]
if not cell.is_revealed:
cell.is_flagged = not cell.is_flagged
4.5 自动展开空白区域算法
当玩家点击一个没有地雷且周围地雷数为 0 的单元格时,游戏会自动揭开其周围所有相邻的空白单元格(包括对角线),并递归地对这些新揭开的、周围地雷数为 0 的空白单元格重复此操作,直到遇到带有数字的单元格。这通常通过递归或使用队列(BFS)实现。
使用队列(BFS)实现:
from collections import deque
def auto_expand(board, start_r, start_c, rows, cols):
q = deque([(start_r, start_c)])
while q:
r, c = q.popleft()
for dr in [-1, 0, 1]:
for dc in [-1, 0, 1]:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols:
neighbor_cell = board[nr][nc]
if not neighbor_cell.is_revealed and not neighbor_cell.is_flagged:
neighbor_cell.is_revealed = True
if neighbor_cell.mines_around == 0:
q.append((nr, nc)) # 只有周围没有地雷的空白格才继续扩展
4.6 胜负判定
- 游戏失败:当玩家揭开一个地雷单元格时。
- 游戏胜利:当所有非地雷单元格都被成功揭开时。
def check_win(board, rows, cols, num_mines):
revealed_count = 0
for r in range(rows):
for c in range(cols):
if board[r][c].is_revealed and not board[r][c].is_mine:
revealed_count += 1
return revealed_count == (rows * cols - num_mines)
# 示例:游戏状态
GAME_ACTIVE = 0
GAME_OVER_LOSE = 1
GAME_OVER_WIN = 2
5. Pygame 渲染与用户交互
这部分将处理游戏的视觉呈现和玩家输入。
5.1 窗口初始化与基本设置
设置窗口大小、标题和加载字体等。
import pygame
# 初始化 Pygame
pygame.init()
# 屏幕尺寸和颜色定义 (在单独的 config.py 中定义更好)
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
CELL_SIZE = 40
BOARD_ROWS = 15
BOARD_COLS = 15
# 颜色
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
GRAY = (180, 180, 180)
LIGHT_GRAY = (220, 220, 220)
BLUE = (0, 0, 255)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
# 设置屏幕
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Pygame 扫雷")
# 字体
font = pygame.font.Font(None, 36) # 默认字体,大小36
5.2 绘制游戏板
根据每个单元格的状态(未揭开、已揭开、旗帜、地雷、数字)进行绘制。
def draw_board(screen, board, rows, cols, cell_size):
for r in range(rows):
for c in range(cols):
cell_x = c * cell_size
cell_y = r * cell_size
rect = pygame.Rect(cell_x, cell_y, cell_size, cell_size)
cell = board[r][c]
# 绘制背景
if cell.is_revealed:
pygame.draw.rect(screen, LIGHT_GRAY, rect)
pygame.draw.rect(screen, GRAY, rect, 1) # 边框
if cell.is_mine:
pygame.draw.circle(screen, BLACK, rect.center, cell_size // 3) # 绘制地雷
elif cell.mines_around > 0:
text_surface = font.render(str(cell.mines_around), True, BLUE) # 绘制数字
text_rect = text_surface.get_rect(center=rect.center)
screen.blit(text_surface, text_rect)
else:
pygame.draw.rect(screen, GRAY, rect) # 未揭开
pygame.draw.rect(screen, BLACK, rect, 1)
if cell.is_flagged:
pygame.draw.polygon(screen, RED, [
(cell_x + cell_size * 0.25, cell_y + cell_size * 0.25),
(cell_x + cell_size * 0.75, cell_y + cell_size * 0.5),
(cell_x + cell_size * 0.25, cell_y + cell_size * 0.75)
]) # 绘制旗帜
5.3 处理鼠标事件
Pygame 的事件循环会捕获所有用户输入。我们需要监听 MOUSEBUTTONDOWN 事件来处理玩家的点击。
# 在游戏主循环中
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.MOUSEBUTTONDOWN:
mouse_x, mouse_y = event.pos
clicked_col = mouse_x // CELL_SIZE
clicked_row = mouse_y // CELL_SIZE
if event.button == 1: # 左键
game_over_state = reveal_cell(game_board, clicked_row, clicked_col, BOARD_ROWS, BOARD_COLS)
if game_over_state: # 踩到地雷
current_game_state = GAME_OVER_LOSE
# 显示所有地雷
for r in range(BOARD_ROWS):
for c in range(BOARD_COLS):
if game_board[r][c].is_mine:
game_board[r][c].is_revealed = True
elif check_win(game_board, BOARD_ROWS, BOARD_COLS, NUM_MINES):
current_game_state = GAME_OVER_WIN
elif event.button == 3: # 右键
toggle_flag(game_board, clicked_row, clicked_col)
5.4 游戏循环与状态更新
游戏的主循环会不断地更新游戏状态和重绘屏幕。
def run_game():
# 游戏板初始化
game_board = initialize_board(BOARD_ROWS, BOARD_COLS)
game_board = place_mines(game_board, BOARD_ROWS, BOARD_COLS, NUM_MINES)
game_board = calculate_mines_around(game_board, BOARD_ROWS, BOARD_COLS)
current_game_state = GAME_ACTIVE # 初始状态
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if current_game_state == GAME_ACTIVE:
# 处理鼠标点击逻辑 (如上述示例)
pass # 在此处添加鼠标事件处理代码
# 绘制背景
screen.fill(WHITE)
# 绘制游戏板
draw_board(screen, game_board, BOARD_ROWS, BOARD_COLS, CELL_SIZE)
# 显示游戏状态信息 (胜利/失败)
if current_game_state == GAME_OVER_LOSE:
lose_text = font.render("游戏失败!", True, RED)
screen.blit(lose_text, (SCREEN_WIDTH // 2 - lose_text.get_width() // 2, SCREEN_HEIGHT // 2))
elif current_game_state == GAME_OVER_WIN:
win_text = font.render("恭喜,你赢了!", True, GREEN)
screen.blit(win_text, (SCREEN_WIDTH // 2 - win_text.get_width() // 2, SCREEN_HEIGHT // 2))
# 更新显示
pygame.display.flip()
pygame.quit()
6. 完整示例代码
为了更好的组织,我们将代码分成几个文件。
6.1 config.py (配置)
# config.py
import pygame
# 游戏板配置
BOARD_ROWS = 16
BOARD_COLS = 16
NUM_MINES = 40 # 简单:10, 中等:40, 困难:99
# 视觉配置
CELL_SIZE = 30 # 每个单元格的像素大小
HEADER_HEIGHT = 60 # 顶部信息栏高度
SCREEN_WIDTH = BOARD_COLS * CELL_SIZE
SCREEN_HEIGHT = BOARD_ROWS * CELL_SIZE + HEADER_HEIGHT
# 颜色定义
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
GRAY = (180, 180, 180)
LIGHT_GRAY = (220, 220, 220)
DARK_GRAY = (100, 100, 100)
# 数字颜色 (为了美观,通常数字1-8有不同颜色)
NUM_COLORS = [
(0, 0, 0), # 占位符,数字0不显示
(0, 0, 255), # 1: 蓝色
(0, 128, 0), # 2: 绿色
(255, 0, 0), # 3: 红色
(0, 0, 128), # 4: 深蓝
(128, 0, 0), # 5: 深红
(0, 128, 128), # 6: 青色
(0, 0, 0), # 7: 黑色 (或灰色)
(128, 128, 128) # 8: 灰色
]
# 游戏状态
GAME_ACTIVE = 0
GAME_OVER_LOSE = 1
GAME_OVER_WIN = 2
6.2 game_logic.py (游戏逻辑)
# game_logic.py
import random
from collections import deque
from config import BOARD_ROWS, BOARD_COLS, NUM_MINES, GAME_ACTIVE, GAME_OVER_LOSE, GAME_OVER_WIN
class Cell:
def __init__(self):
self.is_mine = False
self.is_revealed = False
self.is_flagged = False
self.mines_around = 0
def initialize_board():
"""初始化一个全新的游戏板"""
board = [[Cell() for _ in range(BOARD_COLS)] for _ in range(BOARD_ROWS)]
return board
def place_mines(board):
"""随机放置地雷"""
all_cells = [(r, c) for r in range(BOARD_ROWS) for c in range(BOARD_COLS)]
mine_locations = random.sample(all_cells, NUM_MINES)
for r, c in mine_locations:
board[r][c].is_mine = True
return board
def calculate_mines_around(board):
"""计算每个非地雷单元格周围的地雷数量"""
for r in range(BOARD_ROWS):
for c in range(BOARD_COLS):
if board[r][c].is_mine:
continue
count = 0
for dr in [-1, 0, 1]:
for dc in [-1, 0, 1]:
if dr == 0 and dc == 0:
continue
nr, nc = r + dr, c + dc
if 0 <= nr < BOARD_ROWS and 0 <= nc < BOARD_COLS and board[nr][nc].is_mine:
count += 1
board[r][c].mines_around = count
return board
def reveal_cell(board, r, c):
"""
揭开一个单元格。
返回 True 如果踩到地雷,False 否则。
"""
cell = board[r][c]
if cell.is_revealed or cell.is_flagged:
return False
cell.is_revealed = True
if cell.is_mine:
return True # 踩到地雷
if cell.mines_around == 0:
auto_expand(board, r, c)
return False
def auto_expand(board, start_r, start_c):
"""自动展开相邻的空白区域"""
q = deque([(start_r, start_c)])
while q:
r, c = q.popleft()
for dr in [-1, 0, 1]:
for dc in [-1, 0, 1]:
nr, nc = r + dr, c + dc
if 0 <= nr < BOARD_ROWS and 0 <= nc < BOARD_COLS:
neighbor_cell = board[nr][nc]
if not neighbor_cell.is_revealed and not neighbor_cell.is_flagged:
neighbor_cell.is_revealed = True
if neighbor_cell.mines_around == 0:
q.append((nr, nc))
def toggle_flag(board, r, c):
"""标记或取消标记一个单元格"""
cell = board[r][c]
if not cell.is_revealed:
cell.is_flagged = not cell.is_flagged
return True # 成功标记/取消标记
return False
def check_game_state(board):
"""
检查当前游戏状态(进行中、胜利、失败)。
假定在调用此函数前已经处理了踩雷导致失败的情况。
"""
all_non_mines_revealed = True
for r in range(BOARD_ROWS):
for c in range(BOARD_COLS):
cell = board[r][c]
if not cell.is_mine and not cell.is_revealed:
all_non_mines_revealed = False
break
if not all_non_mines_revealed:
break
if all_non_mines_revealed:
return GAME_OVER_WIN
return GAME_ACTIVE
6.3 main.py (主程序与渲染)
# main.py
import pygame
from config import (
SCREEN_WIDTH, SCREEN_HEIGHT, CELL_SIZE, BOARD_ROWS, BOARD_COLS, NUM_MINES,
WHITE, BLACK, GRAY, LIGHT_GRAY, DARK_GRAY, NUM_COLORS, HEADER_HEIGHT,
GAME_ACTIVE, GAME_OVER_LOSE, GAME_OVER_WIN
)
from game_logic import (
initialize_board, place_mines, calculate_mines_around,
reveal_cell, toggle_flag, check_game_state
)
# 初始化 Pygame
pygame.init()
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Pygame 扫雷")
# 字体
font = pygame.font.Font(None, 30) # 用于数字和信息
def draw_board(current_board):
"""绘制游戏板上的所有单元格"""
for r in range(BOARD_ROWS):
for c in range(BOARD_COLS):
cell_x = c * CELL_SIZE
cell_y = r * CELL_SIZE + HEADER_HEIGHT # 加上顶部偏移
rect = pygame.Rect(cell_x, cell_y, CELL_SIZE, CELL_SIZE)
cell = current_board[r][c]
if cell.is_revealed:
pygame.draw.rect(screen, LIGHT_GRAY, rect) # 已揭开的背景
pygame.draw.rect(screen, DARK_GRAY, rect, 1) # 边框
if cell.is_mine:
# 绘制地雷 (一个黑色的圆)
pygame.draw.circle(screen, BLACK, rect.center, CELL_SIZE // 3)
elif cell.mines_around > 0:
# 绘制数字
text_color = NUM_COLORS[cell.mines_around]
text_surface = font.render(str(cell.mines_around), True, text_color)
text_rect = text_surface.get_rect(center=rect.center)
screen.blit(text_surface, text_rect)
else:
# 未揭开的方块
pygame.draw.rect(screen, GRAY, rect)
pygame.draw.rect(screen, BLACK, rect, 1)
if cell.is_flagged:
# 绘制旗帜 (一个红色三角形)
pygame.draw.polygon(screen, (255, 0, 0), [
(rect.left + CELL_SIZE * 0.25, rect.top + CELL_SIZE * 0.25),
(rect.right - CELL_SIZE * 0.25, rect.top + CELL_SIZE * 0.5),
(rect.left + CELL_SIZE * 0.25, rect.bottom - CELL_SIZE * 0.25)
])
def draw_header(current_game_state, mines_left):
"""绘制顶部信息栏(地雷剩余数量、游戏状态等)"""
header_rect = pygame.Rect(0, 0, SCREEN_WIDTH, HEADER_HEIGHT)
pygame.draw.rect(screen, DARK_GRAY, header_rect) # 顶部背景
# 剩余地雷数
mines_text = font.render(f"剩余地雷: {mines_left}", True, WHITE)
screen.blit(mines_text, (10, 15))
# 游戏状态信息
status_text = ""
text_color = WHITE
if current_game_state == GAME_OVER_LOSE:
status_text = "游戏失败!"
text_color = (255, 50, 50) # 红色
elif current_game_state == GAME_OVER_WIN:
status_text = "恭喜,你赢了!"
text_color = (50, 255, 50) # 绿色
if status_text:
status_surface = font.render(status_text, True, text_color)
status_rect = status_surface.get_rect(center=(SCREEN_WIDTH // 2, HEADER_HEIGHT // 2))
screen.blit(status_surface, status_rect)
def count_flags(board):
"""计算当前标记的旗帜数量"""
count = 0
for r in range(BOARD_ROWS):
for c in range(BOARD_COLS):
if board[r][c].is_flagged:
count += 1
return count
def reset_game():
"""重置游戏到初始状态"""
new_board = initialize_board()
new_board = place_mines(new_board)
new_board = calculate_mines_around(new_board)
return new_board, GAME_ACTIVE
def main():
game_board, current_game_state = reset_game() # 初始游戏板和状态
running = True
while running:
mines_left_count = NUM_MINES - count_flags(game_board)
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.MOUSEBUTTONDOWN:
# 检查点击是否在游戏板区域内
if event.pos[1] > HEADER_HEIGHT: # Y 坐标大于顶部信息栏高度
clicked_col = event.pos[0] // CELL_SIZE
clicked_row = (event.pos[1] - HEADER_HEIGHT) // CELL_SIZE
if not (0 <= clicked_row < BOARD_ROWS and 0 <= clicked_col < BOARD_COLS):
continue # 点击超出游戏板范围
if current_game_state == GAME_ACTIVE: # 只有游戏进行中才能交互
if event.button == 1: # 左键
mine_hit = reveal_cell(game_board, clicked_row, clicked_col)
if mine_hit:
current_game_state = GAME_OVER_LOSE
# 揭示所有地雷
for r in range(BOARD_ROWS):
for c in range(BOARD_COLS):
if game_board[r][c].is_mine:
game_board[r][c].is_revealed = True
else:
if check_game_state(game_board) == GAME_OVER_WIN:
current_game_state = GAME_OVER_WIN
elif event.button == 3: # 右键
toggle_flag(game_board, clicked_row, clicked_col)
else: # 游戏已结束,点击可重置
game_board, current_game_state = reset_game()
# 渲染
screen.fill(WHITE) # 填充背景
draw_header(current_game_state, mines_left_count)
draw_board(game_board)
pygame.display.flip() # 更新显示
pygame.quit()
if __name__ == "__main__":
main()
7. 如何运行游戏
-
保存代码:
将上述三个代码块分别保存为:config.pygame_logic.pymain.py
确保它们都在同一个文件夹下。
-
打开命令行/终端:
进入你存放这些文件的目录。 -
激活虚拟环境(如果之前创建了):
- Windows:
.\venv\Scripts\activate - macOS / Linux:
source venv/bin/activate
- Windows:
-
运行主程序:
python main.py这时,你将看到一个 Pygame 窗口弹出,扫雷游戏界面应该会显示出来。
8. 进一步的扩展与优化
这个基础版本可以作为你进一步开发的起点。你可以考虑添加以下功能来增强游戏体验:
- 计时器:在顶部信息栏显示游戏时间。
- 难度选择:在游戏开始前让玩家选择简单、中等或困难模式,并根据选择调整
BOARD_ROWS,BOARD_COLS,NUM_MINES。 - 游戏重新开始按钮:在游戏结束时显示一个“再玩一次”的按钮。
- 中键点击展开:实现当数字方块周围标记的旗帜数量等于数字时,中键点击该方块自动揭开周围未标记的方块。
- 音效:点击、标记、踩雷、胜利等音效。
- 动画效果:揭开方块、爆炸等简单动画。
- 自定义图案/主题:允许用户更换方块或地雷的图片。
- 更完善的错误处理:例如点击超出边界。
- 记录最佳时间:保存玩家在不同难度下的最佳通关时间。
9. 结语
通过本教程,你已经学习了如何使用 Python 和 Pygame 从零开始构建一个扫雷游戏。这不仅涵盖了 Pygame 的基础用法,还深入探讨了扫雷游戏的核心算法。希望这个项目能帮助你更好地理解游戏开发逻辑,并激发你进一步探索和创造的热情!
更多推荐


所有评论(0)