整体架构设计
游戏采用经典的MVC(模型-视图-控制器)架构模式:
模型(Model):
Minesweeper
和Cell
类,负责游戏逻辑和数据视图(View):
draw_menu()
和draw_game()
函数,负责界面渲染控制器(Controller):
main()
函数中的事件循环,处理用户输入
核心数据结构
Cell类
每个格子对象保存了游戏所需的所有状态信息。
class Cell:
def __init__(self, x, y):
self.x = x # 格子x坐标
self.y = y # 格子y坐标
self.is_mine = False # 是否是地雷
self.is_revealed = False # 是否已揭开
self.is_flagged = False # 是否被标记
self.neighbor_mines = 0 # 周围地雷数
Minesweeper类
游戏主类管理整个游戏状态。
class Minesweeper:
def __init__(self, difficulty=1):
self.grid = [[Cell(x, y) for y in range(GRID_HEIGHT)] for x in range(GRID_WIDTH)]
self.game_over = False
self.win = False
self.first_click = True
self.difficulty = difficulty
self.mine_count = MINE_COUNTS[difficulty]
self.place_mines()
self.calculate_neighbors()
self.start_time = 0
self.elapsed_time = 0
关键算法实现
地雷布置算法
使用随机数生成地雷位置,确保不会重复在同一个位置放置地雷。
def place_mines(self):
mines_placed = 0
while mines_placed < self.mine_count:
x = random.randint(0, GRID_WIDTH - 1)
y = random.randint(0, GRID_HEIGHT - 1)
if not self.grid[x][y].is_mine:
self.grid[x][y].is_mine = True
mines_placed += 1
计算相邻地雷数
def calculate_neighbors(self):
for x in range(GRID_WIDTH):
for y in range(GRID_HEIGHT):
if not self.grid[x][y].is_mine:
total = 0
for dx in [-1, 0, 1]:
for dy in [-1, 0, 1]:
nx, ny = x + dx, y + dy
if 0 <= nx < GRID_WIDTH and 0 <= ny < GRID_HEIGHT and self.grid[nx][ny].is_mine:
total += 1
self.grid[x][y].neighbor_mines = total
揭开格子算法
def reveal(self, x, y):
if not (0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT) or self.grid[x][y].is_revealed or self.grid[x][y].is_flagged:
return
if self.first_click:
self.start_time = pygame.time.get_ticks()
self.first_click = False
self.grid[x][y].is_revealed = True
if self.grid[x][y].is_mine:
self.game_over = True
self.reveal_all_mines()
return
if self.grid[x][y].neighbor_mines == 0:
for dx in [-1, 0, 1]:
for dy in [-1, 0, 1]:
self.reveal(x + dx, y + dy)
self.check_win()
界面设计思路
状态分离
开始菜单:显示游戏标题、难度选择和操作说明
游戏界面:顶部状态栏+底部游戏网格,胜利/失败时显示覆盖层
视觉反馈
不同难度使用不同颜色按钮(绿-黄-红)
数字使用不同颜色增强可读性
半透明覆盖层显示游戏结果不遮挡游戏界面
交互设计
# 主事件循环
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
if event.type == MOUSEBUTTONDOWN:
mouse_x, mouse_y = pygame.mouse.get_pos()
if restart_rect.collidepoint(mouse_x, mouse_y):
in_game = False
elif not game.game_over and mouse_y >= 100:
grid_x, grid_y = mouse_x // GRID_SIZE, (mouse_y - 100) // GRID_SIZE
if 0 <= grid_x < GRID_WIDTH and 0 <= grid_y < GRID_HEIGHT:
if event.button == 1: # 左键
game.reveal(grid_x, grid_y)
elif event.button == 3: # 右键
game.toggle_flag(grid_x, grid_y)
游戏流程设计
初始化:创建窗口、加载字体、设置常量
菜单循环:显示开始菜单,等待玩家选择难度
游戏循环:
初始化游戏状态
处理玩家输入
更新游戏状态
渲染游戏界面
结束处理:显示结果,提供重新开始选项
关键设计决策
首次点击保护:确保玩家第一次点击不会是地雷。
if game.first_click and game.grid[grid_x][grid_y].is_mine:
while game.grid[grid_x][grid_y].is_mine:
game = Minesweeper(game.difficulty)
递归揭开空白区域:自动揭开相连的空白区域,提升游戏体验。
if self.grid[x][y].neighbor_mines == 0:
for dx in [-1, 0, 1]:
for dy in [-1, 0, 1]:
self.reveal(x + dx, y + dy)
状态分离:明确区分游戏状态和渲染逻辑,使代码更易维护。
完整代码
import pygame
import random
import sys
from pygame.locals import *
# 游戏常量
WINDOW_WIDTH = 400
WINDOW_HEIGHT = 500
GRID_SIZE = 40
GRID_WIDTH = 10
GRID_HEIGHT = 10
MINE_COUNTS = [10, 15, 20] # 简单、中等、困难的地雷数量
# 颜色定义
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
GRAY = (192, 192, 192)
DARK_GRAY = (128, 128, 128)
RED = (255, 0, 0)
BLUE = (0, 0, 255)
GREEN = (0, 128, 0)
LIGHT_BLUE = (173, 216, 230)
# 初始化pygame
pygame.init()
WINDOW = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
pygame.display.set_caption('扫雷')
FONT = pygame.font.SysFont('simhei', 30)
SMALL_FONT = pygame.font.SysFont('simhei', 20)
MEDIUM_FONT = pygame.font.SysFont('simhei', 24)
class Cell:
def __init__(self, x, y):
self.x = x
self.y = y
self.is_mine = False
self.is_revealed = False
self.is_flagged = False
self.neighbor_mines = 0
def draw(self):
rect = pygame.Rect(self.x * GRID_SIZE, self.y * GRID_SIZE + 100, GRID_SIZE, GRID_SIZE)
if not self.is_revealed:
pygame.draw.rect(WINDOW, GRAY, rect)
pygame.draw.rect(WINDOW, WHITE, rect, 1)
if self.is_flagged:
flag_text = FONT.render("旗", True, RED)
WINDOW.blit(flag_text, (self.x * GRID_SIZE + 10, self.y * GRID_SIZE + 100))
else:
pygame.draw.rect(WINDOW, WHITE, rect)
pygame.draw.rect(WINDOW, DARK_GRAY, rect, 1)
if self.is_mine:
mine_text = FONT.render("雷", True, BLACK)
WINDOW.blit(mine_text, (self.x * GRID_SIZE + 10, self.y * GRID_SIZE + 100))
elif self.neighbor_mines > 0:
colors = [BLUE, GREEN, RED, (0, 0, 128), (128, 0, 0), (0, 128, 128), BLACK, GRAY]
text = FONT.render(str(self.neighbor_mines), True, colors[self.neighbor_mines - 1])
WINDOW.blit(text, (self.x * GRID_SIZE + 15, self.y * GRID_SIZE + 105))
class Minesweeper:
def __init__(self, difficulty=1):
self.grid = [[Cell(x, y) for y in range(GRID_HEIGHT)] for x in range(GRID_WIDTH)]
self.game_over = False
self.win = False
self.first_click = True
self.difficulty = difficulty
self.mine_count = MINE_COUNTS[difficulty]
self.place_mines()
self.calculate_neighbors()
self.start_time = 0
self.elapsed_time = 0
self.paused = False
self.pause_time = 0
self.total_paused_time = 0
def place_mines(self):
mines_placed = 0
while mines_placed < self.mine_count:
x = random.randint(0, GRID_WIDTH - 1)
y = random.randint(0, GRID_HEIGHT - 1)
if not self.grid[x][y].is_mine:
self.grid[x][y].is_mine = True
mines_placed += 1
def calculate_neighbors(self):
for x in range(GRID_WIDTH):
for y in range(GRID_HEIGHT):
if not self.grid[x][y].is_mine:
total = 0
for dx in [-1, 0, 1]:
for dy in [-1, 0, 1]:
nx, ny = x + dx, y + dy
if 0 <= nx < GRID_WIDTH and 0 <= ny < GRID_HEIGHT and self.grid[nx][ny].is_mine:
total += 1
self.grid[x][y].neighbor_mines = total
def reveal(self, x, y):
if not (0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT) or self.grid[x][y].is_revealed or self.grid[x][
y].is_flagged or self.paused:
return
if self.first_click:
self.start_time = pygame.time.get_ticks()
self.first_click = False
self.grid[x][y].is_revealed = True
if self.grid[x][y].is_mine:
self.game_over = True
self.reveal_all_mines()
return
if self.grid[x][y].neighbor_mines == 0:
for dx in [-1, 0, 1]:
for dy in [-1, 0, 1]:
self.reveal(x + dx, y + dy)
self.check_win()
def reveal_all_mines(self):
for x in range(GRID_WIDTH):
for y in range(GRID_HEIGHT):
if self.grid[x][y].is_mine:
self.grid[x][y].is_revealed = True
def toggle_flag(self, x, y):
if not self.game_over and not self.grid[x][y].is_revealed and not self.paused:
self.grid[x][y].is_flagged = not self.grid[x][y].is_flagged
def check_win(self):
for x in range(GRID_WIDTH):
for y in range(GRID_HEIGHT):
if not self.grid[x][y].is_mine and not self.grid[x][y].is_revealed:
return
self.game_over = True
self.win = True
self.elapsed_time = (pygame.time.get_ticks() - self.start_time - self.total_paused_time) // 1000
def toggle_pause(self):
if not self.game_over:
self.paused = not self.paused
if self.paused:
self.pause_time = pygame.time.get_ticks()
else:
self.total_paused_time += pygame.time.get_ticks() - self.pause_time
def draw_game(self):
# 绘制游戏状态栏
pygame.draw.rect(WINDOW, LIGHT_BLUE, (0, 0, WINDOW_WIDTH, 100))
# 显示难度
difficulties = ["简单", "中等", "困难"]
diff_text = MEDIUM_FONT.render(f"难度: {difficulties[self.difficulty]}", True, BLACK)
WINDOW.blit(diff_text, (20, 20))
# 显示剩余地雷数
flags = sum(cell.is_flagged for row in self.grid for cell in row)
mines_text = MEDIUM_FONT.render(f"剩余地雷: {self.mine_count - flags}", True, BLACK)
WINDOW.blit(mines_text, (20, 50))
# 显示用时
if not self.first_click and not self.game_over and not self.paused:
self.elapsed_time = (pygame.time.get_ticks() - self.start_time - self.total_paused_time) // 1000
# 如果暂停,显示"暂停中"
if self.paused:
time_text = MEDIUM_FONT.render(f"用时: {self.elapsed_time}秒 暂停中", True, BLACK)
else:
time_text = MEDIUM_FONT.render(f"用时: {self.elapsed_time}秒", True, BLACK)
WINDOW.blit(time_text, (200, 20))
# 绘制暂停/继续按钮
pause_text = "继续" if self.paused else "暂停"
pause_rect = pygame.Rect(300, 50, 80, 30)
pygame.draw.rect(WINDOW, GRAY, pause_rect)
pause_btn_text = SMALL_FONT.render(pause_text, True, BLACK)
WINDOW.blit(pause_btn_text, (310, 55))
# 绘制网格
for x in range(GRID_WIDTH):
for y in range(GRID_HEIGHT):
self.grid[x][y].draw()
# 绘制游戏结束信息
if self.game_over:
overlay = pygame.Surface((WINDOW_WIDTH, WINDOW_HEIGHT - 100))
overlay.set_alpha(180)
overlay.fill(WHITE)
WINDOW.blit(overlay, (0, 100))
if self.win:
result_text = FONT.render("恭喜获胜!", True, GREEN)
time_result = MEDIUM_FONT.render(f"用时: {self.elapsed_time}秒", True, BLACK)
WINDOW.blit(result_text, (WINDOW_WIDTH // 2 - 60, WINDOW_HEIGHT // 2 - 30))
WINDOW.blit(time_result, (WINDOW_WIDTH // 2 - 60, WINDOW_HEIGHT // 2 + 10))
else:
result_text = FONT.render("游戏结束!", True, RED)
WINDOW.blit(result_text, (WINDOW_WIDTH // 2 - 60, WINDOW_HEIGHT // 2 - 20))
# 重新开始按钮
restart_rect = pygame.Rect(WINDOW_WIDTH // 2 - 80, WINDOW_HEIGHT // 2 + 50, 160, 40)
pygame.draw.rect(WINDOW, GRAY, restart_rect)
restart_text = MEDIUM_FONT.render("重新开始", True, BLACK)
WINDOW.blit(restart_text, (WINDOW_WIDTH // 2 - 40, WINDOW_HEIGHT // 2 + 60))
return restart_rect, None
return None, pause_rect
def draw_menu():
WINDOW.fill(LIGHT_BLUE)
# 标题
title_text = FONT.render("扫雷游戏", True, BLACK)
WINDOW.blit(title_text, (WINDOW_WIDTH // 2 - 60, 50))
# 难度选择
subtitle_text = MEDIUM_FONT.render("选择难度:", True, BLACK)
WINDOW.blit(subtitle_text, (WINDOW_WIDTH // 2 - 60, 120))
# 难度按钮
easy_rect = pygame.Rect(WINDOW_WIDTH // 2 - 100, 170, 200, 50)
medium_rect = pygame.Rect(WINDOW_WIDTH // 2 - 100, 240, 200, 50)
hard_rect = pygame.Rect(WINDOW_WIDTH // 2 - 100, 310, 200, 50)
pygame.draw.rect(WINDOW, GREEN, easy_rect)
pygame.draw.rect(WINDOW, (255, 255, 0), medium_rect)
pygame.draw.rect(WINDOW, RED, hard_rect)
easy_text = MEDIUM_FONT.render("简单 (10个雷)", True, BLACK)
medium_text = MEDIUM_FONT.render("中等 (15个雷)", True, BLACK)
hard_text = MEDIUM_FONT.render("困难 (20个雷)", True, BLACK)
WINDOW.blit(easy_text, (WINDOW_WIDTH // 2 - 60, 185))
WINDOW.blit(medium_text, (WINDOW_WIDTH // 2 - 60, 255))
WINDOW.blit(hard_text, (WINDOW_WIDTH // 2 - 60, 325))
# 操作说明
instruction_text = SMALL_FONT.render("左键点击揭开格子, 右键点击标记地雷", True, BLACK)
WINDOW.blit(instruction_text, (WINDOW_WIDTH // 2 - 180, 400))
# 暂停说明
pause_text = SMALL_FONT.render("游戏中按暂停按钮可暂停游戏", True, BLACK)
WINDOW.blit(pause_text, (WINDOW_WIDTH // 2 - 150, 430))
return easy_rect, medium_rect, hard_rect
def main():
clock = pygame.time.Clock()
game = None
in_game = False
while True:
if in_game:
WINDOW.fill(WHITE)
restart_rect, pause_rect = game.draw_game()
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
if event.type == MOUSEBUTTONDOWN:
mouse_x, mouse_y = pygame.mouse.get_pos()
if restart_rect and restart_rect.collidepoint(mouse_x, mouse_y):
in_game = False
elif pause_rect and pause_rect.collidepoint(mouse_x, mouse_y):
game.toggle_pause()
elif not game.game_over and mouse_y >= 100 and not game.paused: # 确保点击在游戏区域且游戏未暂停
grid_x, grid_y = mouse_x // GRID_SIZE, (mouse_y - 100) // GRID_SIZE
if 0 <= grid_x < GRID_WIDTH and 0 <= grid_y < GRID_HEIGHT:
if event.button == 1: # 左键点击
if game.first_click and game.grid[grid_x][grid_y].is_mine:
# 如果第一次点击就是雷,重新生成游戏
while game.grid[grid_x][grid_y].is_mine:
game = Minesweeper(game.difficulty)
game.reveal(grid_x, grid_y)
elif event.button == 3: # 右键点击
game.toggle_flag(grid_x, grid_y)
else:
easy_rect, medium_rect, hard_rect = draw_menu()
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
if event.type == MOUSEBUTTONDOWN and event.button == 1:
mouse_x, mouse_y = pygame.mouse.get_pos()
if easy_rect.collidepoint(mouse_x, mouse_y):
game = Minesweeper(0)
in_game = True
elif medium_rect.collidepoint(mouse_x, mouse_y):
game = Minesweeper(1)
in_game = True
elif hard_rect.collidepoint(mouse_x, mouse_y):
game = Minesweeper(2)
in_game = True
pygame.display.update()
clock.tick(30)
if __name__ == "__main__":
main()