Pygame小游戏------经典扫雷
项目概述
Pygame 是 Python 中一个功能强大的 2D 游戏开发库,它提供了处理图形、声音、输入和事件循环的完整工具集。本文通过 Pygame 实现一个经典扫雷游戏项目。
在这个游戏中,玩家需要在一个网格中找出所有地雷而不触发它们。玩家点击格子揭示内容,数字表示周围 8 个相邻格子中地雷的数量。其中:
- 标记:经典操作模式,左键点击:揭示格子,翻开指定格子,查看内容;右键点击:标记/取消标记,对疑似地雷的格子添加或移除旗帜标记。
- 揭示:中键点击:对已揭示的数字格,若周围旗帜数=数字,自动揭示其余格子。
- 键盘操作:R:重新开始游戏;M:切换难度。
- 胜利条件:所有「非地雷格子」都被揭示 = 游戏胜利。
- 失败条件:揭示了一个「地雷格子」 = 游戏失败。
- 首次点击保护:第一次点击永远不会踩雷,地雷在点击后动态生成。
游戏实现
初始化与基础设置
在游戏启动前,我们需要初始化 Pygame 及其子模块,并定义游戏所需的基础参数。
python
# 初始化 Pygame
pygame.init()
# 难度配置:(行数, 列数, 地雷数)
DIFFICULTIES = {
'easy': (9, 9, 10),
'medium': (16, 16, 40),
'hard': (16, 30, 99)
}
# 默认难度
CURRENT_DIFFICULTY = 'medium'
ROWS, COLS, MINE_COUNT = DIFFICULTIES[CURRENT_DIFFICULTY]
# 屏幕设置
CELL_SIZE = 30
UI_HEIGHT = 60
WIDTH = COLS * CELL_SIZE
HEIGHT = ROWS * CELL_SIZE + UI_HEIGHT
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Pygame Minesweeper")
# 颜色定义(经典扫雷风格)
COLORS = {
'bg': (192, 192, 192),
'cell': (210, 210, 210),
'cell_revealed': (180, 180, 180),
'border_light': (255, 255, 255),
'border_dark': (128, 128, 128),
'mine': (0, 0, 0),
'flag': (255, 0, 0),
'wrong_flag': (255, 100, 100),
'text': {
1: (0, 0, 255), # 蓝
2: (0, 128, 0), # 绿
3: (255, 0, 0), # 红
4: (0, 0, 128), # 深蓝
5: (128, 0, 0), # 棕
6: (0, 128, 128), # 青
7: (0, 0, 0), # 黑
8: (128, 128, 128) # 灰
}
}
###字体加载函数
python
def load_safe_font(size, bold=False, italic=False):
"""加载字体"""
try:
return pygame.font.SysFont('arial', size, bold=bold, italic=italic)
except (TypeError, OSError, AttributeError):
try:
font_dir = os.path.join(os.environ.get("WINDIR", "C:\\Windows"), "Fonts")
font_path = os.path.join(font_dir, "arial.ttf")
if os.path.exists(font_path):
f = pygame.font.Font(font_path, size)
if bold:
f.set_bold(True)
if italic:
f.set_italic(True)
return f
except Exception:
pass
# 最终回退到默认字体
f = pygame.font.Font(None, size)
if bold:
f.set_bold(True)
if italic:
f.set_italic(True)
return f
# 初始化字体
font = load_safe_font(20, bold=True)
big_font = load_safe_font(36, bold=True)
small_font = load_safe_font(16)
ui_font = load_safe_font(18)
该函数采用三级降级策略:
- 优先使用
SysFont加载系统字体 - 失败时尝试直接读取 Windows 字体目录
- 最终回退到 pygame 内置默认字体
确保游戏在不同环境(Windows/Linux/macOS)和 pygame 版本下都能正常显示文字。
游戏状态枚举
python
from enum import Enum
class GameState(Enum):
READY = 1 # 游戏准备中(首次点击前)
PLAYING = 2 # 游戏进行中
WON = 3 # 游戏胜利
LOST = 4 # 游戏失败
使用枚举管理游戏状态,使状态判断更清晰、类型更安全。
核心类设计
1. 格子类(Cell)
Cell 类封装了扫雷中每个格子的所有状态和行为。
构造函数 __init__:
python
def __init__(self, row, col):
self.row = row # 行索引
self.col = col # 列索引
self.is_mine = False # 是否为地雷
self.is_revealed = False # 是否已被揭示
self.is_flagged = False # 是否被标记为旗帜
self.is_wrong_flag = False # 是否错误标记(游戏结束时显示)
self.adjacent_mines = 0 # 周围地雷数量(0-8)
计算周围地雷数 count_adjacent_mines:
python
def count_adjacent_mines(self, grid):
"""计算周围8格的地雷数量"""
count = 0
for dr in [-1, 0, 1]:
for dc in [-1, 0, 1]:
if dr == 0 and dc == 0:
continue
nr, nc = self.row + dr, self.col + dc
if 0 <= nr < len(grid) and 0 <= nc < len(grid[0]):
if grid[nr][nc].is_mine:
count += 1
self.adjacent_mines = count
遍历周围 8 个方向,统计地雷数量并存储。
揭示格子 reveal:
python
def reveal(self):
"""揭示格子"""
if self.is_revealed or self.is_flagged:
return
self.is_revealed = True
标记格子为已揭示,并避免重复操作。
绘制方法 draw:
python
def draw(self, surface, cell_size, offset_x, offset_y, game_state):
x = offset_x + self.col * cell_size
y = offset_y + self.row * cell_size
# 绘制格子背景(3D边框效果)
if self.is_revealed:
pygame.draw.rect(surface, COLORS['cell_revealed'],
(x, y, cell_size, cell_size))
else:
pygame.draw.rect(surface, COLORS['cell'],
(x, y, cell_size, cell_size))
# 绘制亮/暗边框营造立体感
pygame.draw.line(surface, COLORS['border_light'], (x, y), (x + cell_size, y), 2)
pygame.draw.line(surface, COLORS['border_light'], (x, y), (x, y + cell_size), 2)
pygame.draw.line(surface, COLORS['border_dark'], (x + cell_size, y), (x + cell_size, y + cell_size), 2)
pygame.draw.line(surface, COLORS['border_dark'], (x, y + cell_size), (x + cell_size, y + cell_size), 2)
# 绘制内容:地雷/数字/旗帜
if self.is_revealed:
if self.is_mine:
# 绘制地雷(圆形+十字)
cx, cy = x + cell_size//2, y + cell_size//2
pygame.draw.circle(surface, COLORS['mine'], (cx, cy), cell_size//3)
pygame.draw.line(surface, COLORS['flag'], (cx-8, cy-8), (cx+8, cy+8), 2)
pygame.draw.line(surface, COLORS['flag'], (cx+8, cy-8), (cx-8, cy+8), 2)
elif self.adjacent_mines > 0:
color = COLORS['text'].get(self.adjacent_mines, (0, 0, 0))
text = font.render(str(self.adjacent_mines), True, color)
text_rect = text.get_rect(center=(x + cell_size//2, y + cell_size//2))
surface.blit(text, text_rect)
elif self.is_flagged:
# 绘制旗帜(旗杆+旗面)
color = COLORS['wrong_flag'] if (self.is_wrong_flag and game_state == GameState.LOST) else COLORS['flag']
pygame.draw.line(surface, (100, 100, 100),
(x + cell_size//3, y + cell_size//4),
(x + cell_size//3, y + 3*cell_size//4), 2)
pygame.draw.polygon(surface, color, [
(x + cell_size//3, y + cell_size//4),
(x + cell_size//3 + 12, y + cell_size//3),
(x + cell_size//3, y + cell_size//2 - 2)
])
2. 游戏主类(MineSweeperGame)
MineSweeperGame 类是整个游戏的控制中心,负责初始化、状态管理、事件处理、逻辑更新和渲染。
构造函数 __init__:
python
def __init__(self, rows=16, cols=16, mines=40):
self.rows = rows
self.cols = cols
self.mine_count = mines
self.cell_size = CELL_SIZE
self.offset_x = 0
self.offset_y = UI_HEIGHT
self.grid = [] # 格子二维数组
self.first_click = True # 是否首次点击(用于保证首次安全)
self.state = GameState.READY
self.start_time = None
self.elapsed_time = 0
self.flags_placed = 0
self.create_grid()
创建网格 create_grid:
python
def create_grid(self):
"""创建空白网格"""
self.grid = []
for r in range(self.rows):
row = [Cell(r, c) for c in range(self.cols)]
self.grid.append(row)
生成地雷 place_mines:
python
def place_mines(self, exclude_row, exclude_col):
"""排除首次点击位置后随机放置地雷"""
positions = [(r, c) for r in range(self.rows) for c in range(self.cols)
if not (r == exclude_row and c == exclude_col)]
mine_positions = random.sample(positions, self.mine_count)
for r, c in mine_positions:
self.grid[r][c].is_mine = True
# 计算每个格子的相邻地雷数
for row in self.grid:
for cell in row:
cell.count_adjacent_mines(self.grid)
扩散算法 flood_fill:
python
def flood_fill(self, start_row, start_col):
"""BFS扩散揭示空白区域:揭示所有相连的空白格+边缘数字格"""
queue = deque([(start_row, start_col)])
visited = set()
while queue:
r, c = queue.popleft()
if (r, c) in visited:
continue
visited.add((r, c))
cell = self.grid[r][c]
# 只跳过被标记的格子,允许已揭示的格子继续扩散
if cell.is_flagged:
continue
# 揭示当前格子(如果已揭示,reveal() 会直接返回,无副作用)
cell.reveal()
# 只有空白格子(相邻地雷数=0)才继续扩散周围
if cell.adjacent_mines == 0 and not cell.is_mine:
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 < self.rows and 0 <= nc < self.cols:
neighbor = self.grid[nr][nc]
# 只将未揭示且未标记的邻居加入队列
if not neighbor.is_revealed and not neighbor.is_flagged:
queue.append((nr, nc))
检查胜利条件 check_win:
python
def check_win(self):
"""检查是否所有非地雷格子都被揭示"""
for row in self.grid:
for cell in row:
if not cell.is_mine and not cell.is_revealed:
return False
return True
快速揭示 chord_reveal:
python
def chord_reveal(self, row, col):
"""双击已揭示数字:若旗帜数=数字,揭示其余格子"""
cell = self.grid[row][col]
if not cell.is_revealed or cell.adjacent_mines == 0:
return
flag_count = 0
to_reveal = []
for dr in [-1, 0, 1]:
for dc in [-1, 0, 1]:
if dr == 0 and dc == 0:
continue
nr, nc = row + dr, col + dc
if 0 <= nr < self.rows and 0 <= nc < self.cols:
neighbor = self.grid[nr][nc]
if neighbor.is_flagged:
flag_count += 1
elif not neighbor.is_revealed:
to_reveal.append((nr, nc))
if flag_count == cell.adjacent_mines:
for nr, nc in to_reveal:
target = self.grid[nr][nc]
if target.is_mine:
self.state = GameState.LOST
self.reveal_all_mines()
return
target.reveal()
if target.adjacent_mines == 0 and not target.is_mine:
self.flood_fill(nr, nc)
if self.check_win():
self.state = GameState.WON
self.mark_correct_flags()
事件处理方法 handle_events:
python
def handle_events(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
return False
if event.type == pygame.MOUSEBUTTONDOWN:
if self.state in [GameState.READY, GameState.PLAYING]:
mx, my = pygame.mouse.get_pos()
col = (mx - self.offset_x) // self.cell_size
row = (my - self.offset_y) // self.cell_size
if 0 <= row < self.rows and 0 <= col < self.cols:
cell = self.grid[row][col]
# 左键:揭示
if event.button == 1 and not cell.is_flagged:
if self.first_click:
self.first_click = False
self.place_mines(row, col)
self.start_time = pygame.time.get_ticks()
self.state = GameState.PLAYING
if cell.is_mine:
self.state = GameState.LOST
self.reveal_all_mines()
else:
# 先揭示起始格子
cell.reveal()
# 如果是空白格,触发扩散
if cell.adjacent_mines == 0:
self.flood_fill(row, col)
# 检查胜利
if self.check_win():
self.state = GameState.WON
self.mark_correct_flags()
# 右键:标记旗帜
elif event.button == 3 and not cell.is_revealed:
if cell.is_flagged:
cell.is_flagged = False
self.flags_placed -= 1
else:
cell.is_flagged = True
self.flags_placed += 1
# 中键 或 Ctrl+左键:快速揭示
elif (event.button == 2 or
(event.button == 1 and pygame.key.get_mods() & pygame.KMOD_CTRL)):
if cell.is_revealed and cell.adjacent_mines > 0:
self.chord_reveal(row, col)
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_r: # R: 重开
self.__init__(self.rows, self.cols, self.mine_count)
elif event.key == pygame.K_m: # M: 切换难度
self.change_difficulty()
elif event.key == pygame.K_ESCAPE: # ESC: 退出
return False
return True
绘制方法 draw:
python
def draw(self):
screen.fill(COLORS['bg'])
# 绘制所有格子
for row in self.grid:
for cell in row:
cell.draw(screen, self.cell_size, self.offset_x, self.offset_y, self.state)
# 绘制顶部信息栏
self.draw_ui()
# 游戏状态遮罩
if self.state == GameState.WON:
self.draw_overlay("VICTORY!", "Time: " + str(self.elapsed_time) + "s",
(100, 255, 100), (200, 255, 200))
elif self.state == GameState.LOST:
self.draw_overlay("BOOM!", "Better luck next time!",
(255, 100, 100), (255, 200, 200))
pygame.display.flip()
主循环方法 run:
python
def run(self):
clock = pygame.time.Clock()
running = True
while running:
running = self.handle_events()
self.draw()
clock.tick(60)
pygame.quit()
sys.exit()
全部代码
python
import pygame
import random
import sys
import os
from enum import Enum
from collections import deque
# ==================== 初始化配置 ====================
pygame.init()
# 难度配置:(行数, 列数, 地雷数)
DIFFICULTIES = {
'easy': (9, 9, 10),
'medium': (16, 16, 40),
'hard': (16, 30, 99)
}
# 默认难度
CURRENT_DIFFICULTY = 'medium'
ROWS, COLS, MINE_COUNT = DIFFICULTIES[CURRENT_DIFFICULTY]
CELL_SIZE = 30
UI_HEIGHT = 60
WIDTH = COLS * CELL_SIZE
HEIGHT = ROWS * CELL_SIZE + UI_HEIGHT
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Pygame Minesweeper")
# 颜色定义(经典扫雷风格)
COLORS = {
'bg': (192, 192, 192),
'cell': (210, 210, 210),
'cell_revealed': (180, 180, 180),
'border_light': (255, 255, 255),
'border_dark': (128, 128, 128),
'mine': (0, 0, 0),
'flag': (255, 0, 0),
'wrong_flag': (255, 100, 100),
'text': {
1: (0, 0, 255), # Blue
2: (0, 128, 0), # Green
3: (255, 0, 0), # Red
4: (0, 0, 128), # Dark Blue
5: (128, 0, 0), # Brown
6: (0, 128, 128), # Teal
7: (0, 0, 0), # Black
8: (128, 128, 128) # Gray
}
}
# ==================== 字体加载函数 ====================
def load_safe_font(size, bold=False, italic=False):
"""安全加载字体,修复 pygame 2.6+ 字体初始化 bug"""
try:
return pygame.font.SysFont('arial', size, bold=bold, italic=italic)
except (TypeError, OSError, AttributeError):
try:
font_dir = os.path.join(os.environ.get("WINDIR", "C:\\Windows"), "Fonts")
font_path = os.path.join(font_dir, "arial.ttf")
if os.path.exists(font_path):
f = pygame.font.Font(font_path, size)
if bold:
f.set_bold(True)
if italic:
f.set_italic(True)
return f
except Exception:
pass
f = pygame.font.Font(None, size)
if bold:
f.set_bold(True)
if italic:
f.set_italic(True)
return f
# 初始化字体
font = load_safe_font(20, bold=True)
big_font = load_safe_font(36, bold=True)
small_font = load_safe_font(16)
ui_font = load_safe_font(18)
# ==================== 游戏状态枚举 ====================
class GameState(Enum):
READY = 1
PLAYING = 2
WON = 3
LOST = 4
# ==================== 格子类 ====================
class Cell:
def __init__(self, row, col):
self.row = row
self.col = col
self.is_mine = False
self.is_revealed = False
self.is_flagged = False
self.is_wrong_flag = False
self.adjacent_mines = 0
def count_adjacent_mines(self, grid):
"""计算周围8格的地雷数量"""
count = 0
for dr in [-1, 0, 1]:
for dc in [-1, 0, 1]:
if dr == 0 and dc == 0:
continue
nr, nc = self.row + dr, self.col + dc
if 0 <= nr < len(grid) and 0 <= nc < len(grid[0]):
if grid[nr][nc].is_mine:
count += 1
self.adjacent_mines = count
def reveal(self):
"""揭示格子"""
if self.is_revealed or self.is_flagged:
return
self.is_revealed = True
def draw(self, surface, cell_size, offset_x, offset_y, game_state):
x = offset_x + self.col * cell_size
y = offset_y + self.row * cell_size
# 绘制格子背景
if self.is_revealed:
pygame.draw.rect(surface, COLORS['cell_revealed'],
(x, y, cell_size, cell_size))
else:
pygame.draw.rect(surface, COLORS['cell'],
(x, y, cell_size, cell_size))
pygame.draw.line(surface, COLORS['border_light'],
(x, y), (x + cell_size, y), 2)
pygame.draw.line(surface, COLORS['border_light'],
(x, y), (x, y + cell_size), 2)
pygame.draw.line(surface, COLORS['border_dark'],
(x + cell_size, y), (x + cell_size, y + cell_size), 2)
pygame.draw.line(surface, COLORS['border_dark'],
(x, y + cell_size), (x + cell_size, y + cell_size), 2)
# 绘制内容
if self.is_revealed:
if self.is_mine:
cx, cy = x + cell_size // 2, y + cell_size // 2
pygame.draw.circle(surface, COLORS['mine'], (cx, cy), cell_size // 3)
pygame.draw.line(surface, COLORS['flag'], (cx - 8, cy - 8), (cx + 8, cy + 8), 2)
pygame.draw.line(surface, COLORS['flag'], (cx + 8, cy - 8), (cx - 8, cy + 8), 2)
elif self.adjacent_mines > 0:
color = COLORS['text'].get(self.adjacent_mines, (0, 0, 0))
text = font.render(str(self.adjacent_mines), True, color)
text_rect = text.get_rect(center=(x + cell_size // 2, y + cell_size // 2))
surface.blit(text, text_rect)
elif self.is_flagged:
color = COLORS['wrong_flag'] if (self.is_wrong_flag and game_state == GameState.LOST) else COLORS['flag']
pygame.draw.line(surface, (100, 100, 100),
(x + cell_size // 3, y + cell_size // 4),
(x + cell_size // 3, y + 3 * cell_size // 4), 2)
pygame.draw.polygon(surface, color, [
(x + cell_size // 3, y + cell_size // 4),
(x + cell_size // 3 + 12, y + cell_size // 3),
(x + cell_size // 3, y + cell_size // 2 - 2)
])
# ==================== 游戏主类 ====================
class MineSweeperGame:
def __init__(self, rows=16, cols=16, mines=40):
self.rows = rows
self.cols = cols
self.mine_count = mines
self.cell_size = CELL_SIZE
self.offset_x = 0
self.offset_y = UI_HEIGHT
self.grid = []
self.first_click = True
self.state = GameState.READY
self.start_time = None
self.elapsed_time = 0
self.flags_placed = 0
self.create_grid()
def create_grid(self):
"""创建空白网格"""
self.grid = []
for r in range(self.rows):
row = [Cell(r, c) for c in range(self.cols)]
self.grid.append(row)
def place_mines(self, exclude_row, exclude_col):
"""排除首次点击位置后随机放置地雷"""
positions = [(r, c) for r in range(self.rows) for c in range(self.cols)
if not (r == exclude_row and c == exclude_col)]
mine_positions = random.sample(positions, self.mine_count)
for r, c in mine_positions:
self.grid[r][c].is_mine = True
for row in self.grid:
for cell in row:
cell.count_adjacent_mines(self.grid)
def flood_fill(self, start_row, start_col):
"""BFS扩散揭示空白区域:揭示所有相连的空白格+边缘数字格"""
queue = deque([(start_row, start_col)])
visited = set()
while queue:
r, c = queue.popleft()
if (r, c) in visited:
continue
visited.add((r, c))
cell = self.grid[r][c]
# 只跳过被标记的格子,允许已揭示的格子继续扩散
if cell.is_flagged:
continue
# 揭示当前格子(如果已揭示,reveal() 会直接返回,无副作用)
cell.reveal()
# 只有空白格子(相邻地雷数=0)才继续扩散周围
if cell.adjacent_mines == 0 and not cell.is_mine:
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 < self.rows and 0 <= nc < self.cols:
neighbor = self.grid[nr][nc]
# 只将未揭示且未标记的邻居加入队列
if not neighbor.is_revealed and not neighbor.is_flagged:
queue.append((nr, nc))
def check_win(self):
"""检查是否所有非地雷格子都被揭示"""
for row in self.grid:
for cell in row:
if not cell.is_mine and not cell.is_revealed:
return False
return True
def reveal_all_mines(self):
"""游戏失败时揭示所有地雷"""
for row in self.grid:
for cell in row:
if cell.is_mine:
cell.is_revealed = True
elif cell.is_flagged and not cell.is_mine:
cell.is_wrong_flag = True
def mark_correct_flags(self):
"""游戏胜利时自动标记剩余地雷"""
for row in self.grid:
for cell in row:
if cell.is_mine and not cell.is_flagged:
cell.is_flagged = True
def chord_reveal(self, row, col):
"""双击已揭示数字:若旗帜数=数字,揭示其余格子"""
cell = self.grid[row][col]
if not cell.is_revealed or cell.adjacent_mines == 0:
return
flag_count = 0
to_reveal = []
for dr in [-1, 0, 1]:
for dc in [-1, 0, 1]:
if dr == 0 and dc == 0:
continue
nr, nc = row + dr, col + dc
if 0 <= nr < self.rows and 0 <= nc < self.cols:
neighbor = self.grid[nr][nc]
if neighbor.is_flagged:
flag_count += 1
elif not neighbor.is_revealed:
to_reveal.append((nr, nc))
if flag_count == cell.adjacent_mines:
for nr, nc in to_reveal:
target = self.grid[nr][nc]
if target.is_mine:
self.state = GameState.LOST
self.reveal_all_mines()
return
target.reveal()
if target.adjacent_mines == 0 and not target.is_mine:
self.flood_fill(nr, nc)
if self.check_win():
self.state = GameState.WON
self.mark_correct_flags()
def draw_ui(self):
"""绘制顶部信息栏"""
pygame.draw.rect(screen, (80, 80, 90), (0, 0, WIDTH, UI_HEIGHT))
pygame.draw.line(screen, (255, 255, 255), (0, UI_HEIGHT), (WIDTH, UI_HEIGHT), 2)
if self.start_time and self.state == GameState.PLAYING:
self.elapsed_time = (pygame.time.get_ticks() - self.start_time) // 1000
time_text = ui_font.render("Time: " + str(self.elapsed_time) + "s", True, (255, 255, 255))
screen.blit(time_text, (10, 20))
remaining = max(0, self.mine_count - self.flags_placed)
mine_text = ui_font.render("Mines: " + str(remaining), True, (255, 80, 80))
mine_width = mine_text.get_width()
screen.blit(mine_text, (WIDTH - mine_width - 10, 20))
diff_text = ui_font.render("[" + CURRENT_DIFFICULTY.upper() + "]", True, (150, 200, 255))
diff_width = diff_text.get_width()
screen.blit(diff_text, (WIDTH // 2 - diff_width // 2, 20))
if self.state == GameState.READY:
status = ui_font.render("Click to Start", True, (200, 200, 200))
elif self.state == GameState.PLAYING:
status = ui_font.render("Good Luck!", True, (150, 255, 150))
elif self.state == GameState.WON:
status = ui_font.render("YOU WIN!", True, (100, 255, 100))
else:
status = ui_font.render("GAME OVER", True, (255, 100, 100))
status_width = status.get_width()
screen.blit(status, (WIDTH // 2 - status_width // 2, 40))
def draw_overlay(self, title, subtitle, title_color, subtitle_color):
"""绘制游戏结束遮罩"""
overlay = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
overlay.fill((0, 0, 0, 160))
screen.blit(overlay, (0, 0))
title_surf = big_font.render(title, True, title_color)
subtitle_surf = font.render(subtitle, True, subtitle_color)
hint_surf = ui_font.render("Press [R] Restart | [M] Change Difficulty", True, (200, 200, 200))
screen.blit(title_surf, (WIDTH // 2 - title_surf.get_width() // 2, HEIGHT // 2 - 60))
screen.blit(subtitle_surf, (WIDTH // 2 - subtitle_surf.get_width() // 2, HEIGHT // 2 - 10))
screen.blit(hint_surf, (WIDTH // 2 - hint_surf.get_width() // 2, HEIGHT // 2 + 40))
def change_difficulty(self):
"""循环切换难度"""
global CURRENT_DIFFICULTY, ROWS, COLS, MINE_COUNT
diff_list = list(DIFFICULTIES.keys())
idx = diff_list.index(CURRENT_DIFFICULTY)
CURRENT_DIFFICULTY = diff_list[(idx + 1) % len(diff_list)]
ROWS, COLS, MINE_COUNT = DIFFICULTIES[CURRENT_DIFFICULTY]
self.__init__(ROWS, COLS, MINE_COUNT)
def handle_events(self):
"""处理输入事件"""
for event in pygame.event.get():
if event.type == pygame.QUIT:
return False
if event.type == pygame.MOUSEBUTTONDOWN:
if self.state in [GameState.READY, GameState.PLAYING]:
mx, my = pygame.mouse.get_pos()
col = (mx - self.offset_x) // self.cell_size
row = (my - self.offset_y) // self.cell_size
if 0 <= row < self.rows and 0 <= col < self.cols:
cell = self.grid[row][col]
# 左键:揭示
if event.button == 1 and not cell.is_flagged:
if self.first_click:
self.first_click = False
self.place_mines(row, col)
self.start_time = pygame.time.get_ticks()
self.state = GameState.PLAYING
if cell.is_mine:
self.state = GameState.LOST
self.reveal_all_mines()
else:
# 先揭示起始格子
cell.reveal()
# 如果是空白格,触发扩散
if cell.adjacent_mines == 0:
self.flood_fill(row, col)
# 检查胜利
if self.check_win():
self.state = GameState.WON
self.mark_correct_flags()
# 右键:标记旗帜
elif event.button == 3 and not cell.is_revealed:
if cell.is_flagged:
cell.is_flagged = False
self.flags_placed -= 1
else:
cell.is_flagged = True
self.flags_placed += 1
# 中键:快速揭示
elif event.button == 2:
if cell.is_revealed and cell.adjacent_mines > 0:
self.chord_reveal(row, col)
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_r:
self.__init__(self.rows, self.cols, self.mine_count)
elif event.key == pygame.K_m:
self.change_difficulty()
elif event.key == pygame.K_ESCAPE:
return False
return True
def draw(self):
"""绘制整个游戏画面"""
screen.fill(COLORS['bg'])
for row in self.grid:
for cell in row:
cell.draw(screen, self.cell_size, self.offset_x, self.offset_y, self.state)
self.draw_ui()
if self.state == GameState.WON:
self.draw_overlay("VICTORY!", "Time: " + str(self.elapsed_time) + "s",
(100, 255, 100), (200, 255, 200))
elif self.state == GameState.LOST:
self.draw_overlay("BOOM!", "Better luck next time!",
(255, 100, 100), (255, 200, 200))
pygame.display.flip()
def run(self):
"""游戏主循环"""
clock = pygame.time.Clock()
running = True
while running:
running = self.handle_events()
self.draw()
clock.tick(60)
pygame.quit()
sys.exit()
# ==================== 程序入口 ====================
if __name__ == "__main__":
print("Pygame Minesweeper - Classic Edition")
print("Controls:")
print(" Left Click : Reveal cell")
print(" Right Click : Place/Remove flag")
print(" Middle Click / Ctrl+Left: Quick reveal around number")
print(" [R] : Restart game")
print(" [M] : Change difficulty (Easy/Medium/Hard)")
print(" [ESC]: Exit")
print("-" * 40)
game = MineSweeperGame(ROWS, COLS, MINE_COUNT)
game.run()
附:文章说明
本文仅为个人理解,若有不当之处,欢迎指正~