使用Python制造扫雷游戏
整体架构设计
游戏采用经典的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 pygameimport randomimport sysfrom pygame.locals import *# 游戏常量WINDOW_WIDTH = 400WINDOW_HEIGHT = 500GRID_SIZE = 40GRID_WIDTH = 10GRID_HEIGHT = 10MINE_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)# 初始化pygamepygame.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 = 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()