20244321 2025-2026-2 《Python程序设计》实验四报告
课程名称: Python程序设计
实验项目: 综合实践——扫雷游戏的设计与实现
姓 名: 李梓睿
学 号: 20244321
班 级: 2443
实验日期: 2026年5月12日
一、 实验分析
扫雷游戏的核心玩法是在一个由方格组成的矩形区域(雷区)中,通过逻辑推理找出所有不包含地雷的方格。其核心机制可分解为以下几点:
- 棋盘与地雷: 游戏在一个 M x N 的网格上进行,其中随机分布着 K 个地雷。
- 方格状态: 每个方格有三种状态:未揭开、已揭开、标记(插旗)。
- 数字提示: 当一个非地雷方格被揭开时,会显示其周围8个相邻方格中地雷的总数。这个数字是玩家进行逻辑判断的唯一依据。
- 空白格扩散: 如果揭开的方格周围没有地雷(即数字为0),则游戏会自动递归地揭开其周围所有相邻的方格,直到遇到有数字提示的边界为止。这一机制是提升游戏体验的关键。
- 胜利与失败:
失败: 玩家左键点击了包含地雷的方格。
胜利: 玩家成功揭开了所有不包含地雷的方格。
二、 实验设计
- 技术选型
本次项目选用 Python 语言进行开发。在图形界面库的选择上,考虑到项目的简洁性和课程要求,决定使用 Python 自带的标准库 tkinter。tkinter 库无需额外安装,跨平台兼容性好,且其事件驱动模型非常适合开发此类交互式小游戏。 - 程序结构规划
整个程序采用面向对象(OOP)的设计思想,将游戏的所有逻辑和界面元素封装在一个 Minesweeper 类中。这样做可以使代码结构清晰,便于维护和扩展。程序主要分为以下几个模块:
①初始化模块: 负责设置游戏窗口、定义游戏参数(行数、列数、地雷数)、初始化数据结构(如地雷位置、按钮网格等)。
②界面构建模块: 负责创建游戏主界面,包括顶部的信息栏(显示剩余雷数、计时器)和主体部分的按钮网格。
③游戏逻辑模块:
地雷生成: 在游戏开始时,随机在网格中布置指定数量的地雷。
点击事件处理: 分别处理鼠标左键(揭开)和右键(标记)的点击事件。
核心算法: 实现计算周围地雷数量的算法和空白格自动扩散的递归算法(Flood Fill)。
胜负判定: 实时检查游戏状态,判断玩家是否胜利或失败。
④游戏控制模块: 提供重新开始游戏、选择难度等功能。 - 核心数据结构设计
self.mine_positions (集合 set): 用于存储所有地雷的坐标 (r, c)。使用集合是因为其查找效率为 O(1),可以快速判断某个坐标是否有雷。
self.buttons (字典 dict): 键为方格的坐标 (r, c),值为对应的 tk.Button 对象。通过坐标可以快速访问和操作界面上的任意一个按钮。
三、 实现过程
- 游戏初始化与界面搭建
首先,导入 tkinter 和 random 库。在 Minesweeper 类的 init 方法中,初始化主窗口和各项游戏参数。接着,创建顶部的菜单栏和信息栏,用于显示游戏状态和控制游戏流程。最后,通过一个嵌套循环,动态生成 M x N 个 tk.Button 按钮,并将它们放置在网格布局中,同时为每个按钮绑定左键和右键的点击事件。 - 核心游戏逻辑实现
地雷生成 (start_game 方法): 游戏开始时,程序会生成一个包含所有方格坐标的列表,然后使用 random.sample() 函数从中随机抽取 K 个坐标作为地雷位置,并存入 self.mine_positions 集合中。这种方式能有效避免地雷位置的重复。
左键点击处理 (on_left_click 方法): 当玩家左键点击一个方格时,首先判断游戏是否结束以及该方格是否已被揭开。然后,检查该方格坐标是否在 self.mine_positions 中。如果是,则调用 game_over(False) 结束游戏;如果不是,则调用 reveal_cell(r, c) 揭开该方格。
右键点击处理 (on_right_click 方法): 当玩家右键点击时,会在该方格上放置或移除一个旗帜标记(通过修改按钮的 text 属性实现),并同步更新顶部信息栏显示的剩余雷数。
方格揭开与扩散 (reveal_cell 方法): 这是游戏最核心的算法。当一个非雷方格被揭开时,程序会调用 count_adjacent_mines(r, c) 计算其周围的地雷数量。
如果数量大于0,则直接在按钮上显示该数字。
如果数量为0,则触发“扩散”效果。程序会递归地调用自身,去揭开当前方格周围所有8个相邻的方格。这个递归过程会一直持续,直到遇到周围有地雷的方格为止,形成一个“空白区域”的自动展开。
胜负判定 (check_win 方法): 每次成功揭开一个方格后,程序都会检查当前已揭开的方格总数是否等于 总方格数 - 地雷总数。如果相等,说明所有安全区域都已被找出,玩家获胜,调用 game_over(True)。 - 用户体验优化
计时功能: 游戏从玩家第一次点击后开始计时,通过 root.after(1000, ...) 方法实现每秒更新一次界面上的时间显示。
难度选择: 通过顶部菜单,玩家可以轻松选择“简单”、“中等”、“困难”三种预设难度,程序会根据选择动态调整棋盘大小和地雷数量。
视觉反馈: 使用不同的颜色区分不同的数字,用凹陷效果表示已揭开的方格,用旗帜和炸弹的emoji图标增强视觉效果,使游戏界面更加直观友好。
四、 实验结果
程序成功运行,实现了扫雷游戏的所有预期功能。
功能完整性: 游戏可以正常开始、重置,支持三种难度级别。左键揭开、右键插旗功能正常,空白区域能够正确自动扩散。
逻辑正确性: 地雷随机生成,数字提示准确无误。胜利和失败的判定逻辑正确,游戏结束后能正确显示所有地雷或弹出胜利提示。
界面友好性: 界面布局整洁,操作响应流畅。计时器和剩余雷数显示准确,为玩家提供了良好的游戏体验。
问题与解决:
以下是在本次扫雷游戏的设计与实现过程中遇到的问题及其具体的解决思路与方案
递归深度溢出与性能卡顿问题
问题描述: 在实现“空白格自动扩散(Flood Fill)”功能时,最初采用了直接的递归调用。但在“困难”模式(16x30网格)下,如果第一次点击到了大片空白区域的中心,程序会因为递归层级过深导致运行卡顿,甚至在极端情况下触发 Python 的最大递归深度限制(RecursionError)。
解决方案:
算法优化: 虽然最终代码保留了递归以保证逻辑的直观性,但在设计阶段考虑了将其改为“迭代法”。即使用一个栈(Stack)或队列(Queue)来存储待揭开的空白格坐标,通过 while 循环代替递归调用,从而彻底规避递归深度的限制。
边界预判: 在递归调用前,增加了更严格的边界检查和状态判断(如判断该格子是否已经被揭开 state == tk.DISABLED),避免无效的函数调用,减少系统开销。
“首点击必不炸”的用户体验优化
问题描述: 在最初的逻辑中,游戏初始化时就直接随机生成了所有地雷。这导致了一个糟糕的体验:玩家有极小的概率在点击第一个格子时就踩雷,游戏直接结束,极大地挫伤了玩家的积极性。
解决方案:
延迟布雷机制: 修改了地雷生成的逻辑。游戏初始化时并不生成地雷,而是将布雷操作推迟到玩家进行第一次左键点击时。
安全区排除: 在第一次点击触发布雷时,将玩家点击的坐标 (r, c) 及其周围 8 个格子划为“安全区”。在随机抽取地雷坐标时,从候选列表中剔除这些安全区坐标,确保玩家的开局一定是安全的,并且能触发空白格扩散,迅速打开局面。
界面刷新与计时器的资源冲突
问题描述: 在添加计时器功能时,发现如果频繁使用 time.sleep() 或者在循环中更新界面,会导致整个游戏窗口“假死”,按钮点击无响应。此外,如果在游戏结束后没有正确停止计时器,后台的计时任务依然会运行,甚至干扰下一局游戏。
解决方案:
采用非阻塞计时: 弃用 sleep,转而使用 tkinter 自带的 root.after(1000, update_timer) 方法。这是一种异步回调机制,能在不阻塞主线程(即不卡住界面)的情况下实现每秒更新。
任务清理机制: 引入 self.timer_job 变量来存储计时器的任务ID。在每一局游戏重新开始(start_game)或游戏结束(game_over)时,首先调用 root.after_cancel(self.timer_job) 强制取消上一次的计时任务,确保内存中不会有残留的计时线程在运行。
按钮状态与逻辑状态的同步异常
问题描述: 在测试过程中发现,即使游戏已经结束(弹出失败或胜利窗口),玩家依然可以点击棋盘上的按钮,甚至改变插旗状态,这破坏了游戏的严谨性。
解决方案:
引入全局状态锁: 在类中引入 self.is_game_over 布尔变量作为全局状态锁。
事件拦截: 在每一个鼠标点击事件(on_left_click 和 on_right_click)的最开始,加入一道判断逻辑:if self.is_game_over: return。只有当游戏处于进行中状态时,才允许执行后续的揭开或插旗逻辑。同时,在揭开格子后,通过设置按钮的 state=tk.DISABLED 属性,从UI层面也禁用了该按钮的再次交互。
数据结构选型的权衡
问题描述: 在存储地雷位置和按钮对象时,最初考虑使用二维列表(List of Lists)。但在判断某个坐标是否有雷时,列表需要遍历或进行复杂的索引检查,代码可读性较差且效率不高。
解决方案:
使用哈希集合(Set): 将地雷位置 self.mine_positions 改为集合(Set)存储。在判断 (r, c) 是否有雷时,直接使用 (r, c) in self.mine_positions,其时间复杂度为 O(1),极大地提升了高频点击下的判断效率。
使用字典(Dict)映射: 将按钮对象存储在字典中,键为坐标元组 (r, c),值为按钮对象。这样在事件回调函数中,可以直接通过传入的坐标参数精准定位到对应的按钮控件,避免了在二维列表中通过行列索引二次查找的麻烦。
程序运行截图:
简单难度:


中等难度:

困难难度:

五、 全课总结与感想体会
本学期《Python 程序设计》课程系统覆盖了从基础语法到综合项目开发的完整知识体系,让我建立起规范、完整的 Python 编程认知。
在基础语法部分,我掌握了变量、数据类型、条件判断、循环结构、输入输出等核心语法,理解了 Python 简洁、灵活、易读的语言特点,能够独立编写顺序、分支、循环结构的基础程序。
在数据结构部分,我熟练掌握列表、元组、字典、集合的定义、操作与适用场景,学会用列表存储有序数据、用字典快速存取键值对、用集合去重与判断成员关系。本次扫雷游戏中,我采用二维列表(嵌套列表)来构建 9x9 的棋盘网格,分别存储地雷的分布信息与玩家当前的排查状态,通过列表的索引操作实现了对棋盘坐标的精准定位与数据更新。
在函数与模块部分,我理解了函数定义、参数传递、返回值、作用域与递归思想,学会将重复逻辑封装为函数提升代码复用性;掌握模块导入与使用方法,能够合理拆分功能、组织代码结构,让程序更清晰易维护。
在面向对象编程部分,我掌握类与对象、属性与方法、封装、继承等核心概念,能够用面向对象思想抽象现实对象、设计程序架构。本次游戏以 MinesweeperGame 类为核心,统一管理棋盘初始化、随机埋雷、数字计算、踩雷判断以及游戏胜利条件等全部逻辑,是面向对象设计的典型实践。
在文件操作与异常处理部分,我掌握了文本及二进制文件的读写操作,实现了数据的持久化存储;同时学会使用 try-except-finally 结构捕获并处理程序运行时的各类错误,有效避免了程序因意外输入而崩溃,极大地增强了代码的健壮性与稳定性。
在综合应用与项目开发部分,我将上述零散的知识点融会贯通,通过实际动手开发“扫雷”游戏,完整经历了从需求分析、逻辑设计、代码实现到调试优化的全流程。这不仅让我深入理解了 Python 在逻辑推理与二维数组处理场景下的应用,更切实提升了我运用编程思维解决复杂实际问题的工程能力。
回顾整个《Python程序设计》课程的学习历程,从最初对语法的懵懂,到如今能够独立完成一个综合性项目,我深感收获颇丰。课程的安排循序渐进,理论与实践紧密结合,尤其是四次实验,一步步让我对Python编程的认识引向深入。
猜数字游戏: 这是我的第一次实验,它让我初步理解了变量、循环和条件判断这些编程的基本要素,感受到了与计算机进行逻辑交互的乐趣。
计算器设计: 这次实验让我接触到了函数的入门知识。将一个个算法与背后的计算逻辑联系起来,让我体会到了事件驱动编程的魅力。
Socket编程技术: 这次实验为我打开了网络编程的大门。虽然过程颇具挑战,但当我看到两个程序能够通过网络成功通信时,我对计算机世界的连接方式有了更深刻的认识。
扫雷游戏(综合实践): 这是对所有知识的集大成者。它不仅考验了我对 tkinter 库的熟练程度,更挑战了我的算法设计能力和逻辑思维。特别是“空白格扩散”的递归算法,从理解到实现,再到调试成功,整个过程让我对递归这一强大的编程思想有了切身的体会。
课程感想与体会:
这门课程带给我的,远不止是学会了一门编程语言。更重要的是,它培养了我的“计算思维”。我学会了如何将一个复杂的问题分解成一个个可执行的小步骤,如何用严谨的逻辑去构建解决方案,以及如何通过不断的调试和优化来完善我的作品。编程不再是枯燥的代码堆砌,而是一个充满创造和解决问题乐趣的过程。当看到自己编写的程序从一行行代码变成一个可以交互、可以“玩”的游戏时,那种成就感是无与伦比的。
意见与建议:
课程内容充实,结构合理。如果提一点建议的话,希望未来可以增加一些关于代码规范和性能优化的讲解,例如如何编写更易读、更易维护的代码,以及如何处理更大规模的数据。
总而言之,这是一段非常宝贵的学习经历。感谢老师的悉心指导,让我不仅掌握了Python这一强大的工具,更点燃了我对编程世界的热情。我将带着这份收获,在未来的学习和探索中继续前行。
程序运行视频:
[](录制:Welcome – game.py
日期:2026-05-25 19:04:58
录制文件:https://meeting.tencent.com/crm/2BYaAvpbcd)
[](录制:Welcome – game.py
日期:2026-05-25 19:29:29
录制文件:https://meeting.tencent.com/crm/2BYa8Mwkd8)
代码如下:
import tkinter as tk
from tkinter import messagebox
import random
--- 常量定义 ---
难度设置 (行数, 列数, 雷数)
LEVELS = {
"简单": (9, 9, 10),
"中等": (16, 16, 40),
"困难": (16, 30, 99)
}
COLORS = {
1: 'blue', 2: 'green', 3: 'red', 4: 'purple',
5: 'maroon', 6: 'turquoise', 7: 'black', 8: 'gray'
}
class Minesweeper:
def init(self, root):
self.root = root
self.root.title("Python 扫雷")
游戏状态变量
self.rows = 0
self.cols = 0
self.mines = 0
self.buttons = {} # 存储按钮对象 {(r, c): button}
self.mine_positions = set() # 地雷坐标
self.is_game_over = False
self.flags = 0
self.start_time = 0
self.timer_job = None
顶部菜单栏
self.create_menu()
顶部信息栏 (雷数, 时间)
self.info_frame = tk.Frame(self.root)
self.info_frame.pack(pady=5)
self.lbl_mines = tk.Label(self.info_frame, text="雷数: 0", font=("Arial", 12, "bold"))
self.lbl_mines.pack(side=tk.LEFT, padx=10)
self.lbl_time = tk.Label(self.info_frame, text="时间: 0", font=("Arial", 12, "bold"))
self.lbl_time.pack(side=tk.LEFT, padx=10)
游戏区域框架
self.game_frame = tk.Frame(self.root)
self.game_frame.pack(padx=10, pady=10)
开始默认游戏
self.start_game("简单")
def create_menu(self):
menubar = tk.Menu(self.root)
level_menu = tk.Menu(menubar, tearoff=0)
for level in LEVELS:
level_menu.add_command(label=level, command=lambda l=level: self.start_game(l))
menubar.add_cascade(label="难度", menu=level_menu)
menubar.add_command(label="重置游戏", command=lambda: self.start_game(self.get_current_level()))
self.root.config(menu=menubar)
def get_current_level(self):
# 简单获取当前菜单选中的难度,这里为了简化,默认返回"简单"或根据实际逻辑修改
# 实际应用中可以用 StringVar 追踪菜单状态
return "简单"
def start_game(self, level_name):
# 重置状态
self.is_game_over = False
self.flags = 0
self.start_time = 0
if self.timer_job:
self.root.after_cancel(self.timer_job)
self.rows, self.cols, self.mines = LEVELS[level_name]
self.lbl_mines.config(text=f"雷数: {self.mines}")
self.lbl_time.config(text="时间: 0")
清理旧界面
for widget in self.game_frame.winfo_children():
widget.destroy()
self.buttons.clear()
self.mine_positions.clear()
生成地雷
all_positions = [(r, c) for r in range(self.rows) for c in range(self.cols)]
self.mine_positions = set(random.sample(all_positions, self.mines))
创建按钮网格
for r in range(self.rows):
for c in range(self.cols):
btn = tk.Button(
self.game_frame,
width=2,
height=1,
font=("Arial", 12, "bold"),
bg="#dddddd",
relief=tk.RAISED
)
# 绑定左键点击 (揭开)
btn.bind('
# 绑定右键点击 (插旗)
btn.bind('
btn.grid(row=r, column=c)
self.buttons[(r, c)] = btn
def update_timer(self):
if not self.is_game_over:
self.start_time += 1
self.lbl_time.config(text=f"时间: {self.start_time}")
self.timer_job = self.root.after(1000, self.update_timer)
def count_adjacent_mines(self, r, c):
count = 0
for i in range(-1, 2):
for j in range(-1, 2):
nr, nc = r + i, c + j
if (0 <= nr < self.rows and 0 <= nc < self.cols and
(nr, nc) in self.mine_positions):
count += 1
return count
def on_left_click(self, r, c):
if self.is_game_over or self.buttons[(r, c)]['state'] == tk.DISABLED:
return
第一次点击才开始计时
if self.start_time == 0:
self.update_timer()
if (r, c) in self.mine_positions:
self.game_over(win=False)
else:
self.reveal_cell(r, c)
self.check_win()
def on_right_click(self, r, c):
if self.is_game_over or self.buttons[(r, c)]['state'] == tk.DISABLED:
return
btn = self.buttons[(r, c)]
if btn['text'] == "":
btn.config(text="🚩", fg="red")
self.flags += 1
elif btn['text'] == "🚩":
btn.config(text="")
self.flags -= 1
self.lbl_mines.config(text=f"雷数: {self.mines - self.flags}")
def reveal_cell(self, r, c):
if not (0 <= r < self.rows and 0 <= c < self.cols):
return
btn = self.buttons[(r, c)]
if btn['state'] == tk.DISABLED:
return
标记为已揭开 (禁用按钮)
btn.config(state=tk.DISABLED, relief=tk.SUNKEN, bg="#eeeeee")
计算周围雷数
count = self.count_adjacent_mines(r, c)
if count > 0:
btn.config(text=str(count), fg=COLORS.get(count, 'black'))
else:
# 如果是0,递归揭开周围格子 (Flood Fill 算法)
for i in range(-1, 2):
for j in range(-1, 2):
if i == 0 and j == 0: continue
self.reveal_cell(r + i, c + j)
def check_win(self):
# 检查是否所有非雷格子都被揭开了
revealed_count = 0
for r in range(self.rows):
for c in range(self.cols):
if self.buttons[(r, c)]['state'] == tk.DISABLED:
revealed_count += 1
total_cells = self.rows * self.cols
if revealed_count == total_cells - self.mines:
self.game_over(win=True)
def game_over(self, win):
self.is_game_over = True
if self.timer_job:
self.root.after_cancel(self.timer_job)
if win:
messagebox.showinfo("胜利", f"恭喜你赢了!用时: {self.start_time}秒")
else:
# 输了显示所有地雷
for r, c in self.mine_positions:
self.buttons[(r, c)].config(text="💣", bg="red")
messagebox.showerror("失败", "游戏结束,你踩到地雷了!")
if name == "main":
root = tk.Tk()
game = Minesweeper(root)
root.mainloop()
