| 项目 | 内容 |
|---|---|
| 这个作业属于哪个课程 | 202601 软件工程班级博客 |
| 这个作业要求在哪里 | 第二次个人作业要求 |
| 这个作业的目标 | 使用 Python 和 AIGC 完成"一箭又一箭"小游戏 |
| 学号 | 102401402 |
| GitHub 仓库 | https://github.com/Anna-khalil/ArrowAfterArrow |
一、项目展示
1.1 开始界面
游戏启动后进入开始界面,包含游戏标题"一箭又一箭"、副标题"看清方向 · 找到出口",以及三个功能按钮:开始游戏、关卡选择、继续游戏。底部附有操作说明。
1.2 游戏界面
游戏界面顶部为 HUD 区域,显示当前关卡名称、剩余箭头数量、用时和剩余失误次数(心形图标)。中部为游戏棋盘,箭头按方向着色(上=绿、下=蓝、左=橙、右=紫)。底部为功能按钮区:重新开始、撤销、提示、AI 求解、返回。
1.3 通关界面
通关后显示星级评价(1-3星)、得分、用时和剩余失误,提供"下一关""重玩""返回主页"按钮。
1.4 关卡选择界面
以 5 列网格展示 10 个关卡,未解锁的关卡显示锁图标。通关后自动解锁下一关。
1.5 关卡可解性验证
使用 AI 求解器自动验证所有 10 个关卡均存在合法通关顺序。
二、项目介绍
2.1 游戏规则
"一箭又一箭"是一类点击式箭头解谜游戏。棋盘中包含若干带方向的箭头,方向分为上、下、左、右四种。玩家点击某个箭头后,程序检查该箭头前进方向上的路径:
- 如果箭头与棋盘边界之间没有其他箭头阻挡,该箭头飞出棋盘并被消除;
- 如果路径上存在其他箭头,该箭头不能被消除,并通过晃动、变色提示碰撞,同时消耗一次失误机会;
- 清除本关全部箭头后进入下一关;
- 失误次数(每关 3 次)耗尽时本关失败,可重新开始。
2.2 界面设计
采用深色主题(深蓝灰背景),四方向箭头使用不同颜色便于快速识别。界面分为三个主要区域:顶部 HUD(关卡信息)、中部棋盘(游戏主体)、底部按钮(操作功能)。布局简洁,操作直观。
2.3 主要功能与特色
| 类别 | 功能 |
|---|---|
| 基础 | 四方向箭头、路径检测、飞出动画、碰撞反馈、失误机制、10 关卡、关卡切换、重新开始 |
| 附加 | 撤销、智能提示(AI 最优下一步)、AI 自动求解演示、关卡选择与解锁、存档系统、计时与得分、星级评价、继续游戏 |
核心特色:内置基于 DFS 回溯 + 记忆化的 AI 求解器,不仅能自动求解任意关卡,还能为"提示"功能提供最优下一步,而非简单地随机选择一个可消除箭头。
三、实现思路
3.1 箭头、方向和关卡的表示
箭头:使用 Arrow 类表示,核心属性为网格坐标 (row, col) 和方向 direction(字符串 'up'/'down'/'left'/'right')。箭头同时维护动画状态(飞行、晃动、提示高亮)。
方向:使用字典 DIRECTIONS 将方向字符串映射为位移元组 (dx, dy),其中 dx 表示列变化、dy 表示行变化:
DIRECTIONS = {
'up': (0, -1), 'down': (0, 1),
'left': (-1, 0), 'right': (1, 0),
}
关卡:每个关卡是一个字典,包含关卡编号、名称、棋盘行列数、最大失误次数和箭头布局列表。箭头布局中每个元素为 (row, col, direction) 元组。共设计 10 个关卡,箭头数量从 5 个递增到 22 个,棋盘从 5×5 扩大到 9×9。所有关卡均通过 AI 求解器验证存在合法通关顺序。
3.2 路径检测方法(核心算法)
路径检测是本作业的核心逻辑,对应评分标准中 25 分的"箭头与路径判断"。
算法思路:对于被点击的箭头,从其所在位置的下一格开始,沿箭头方向逐格扫描,直到超出棋盘边界。如果扫描过程中遇到任意其他箭头,则说明有阻挡,返回 False;如果一直扫描到边界都没有遇到箭头,则返回 True。
关键代码(board.py 中 is_path_clear 方法):
def is_path_clear(self, arrow):
r, c = arrow.row, arrow.col
dx, dy = DIRECTIONS[arrow.direction]
# dx 影响列(col),dy 影响行(row)
nr, nc = r + dy, c + dx
while 0 <= nr < self.rows and 0 <= nc < self.cols:
if self.grid[nr][nc] is not None:
return False
nr += dy
nc += dx
return True
边界处理:当箭头位于棋盘边缘且朝向棋盘外时(如最右列的朝右箭头),循环的初始位置 (nr, nc) 已经超出棋盘范围,循环一次都不执行,直接返回 True。这保证了边缘朝外的箭头可以正常飞出,不会发生数组越界错误。
坐标对应关系:开发中最容易出错的点是方向向量与网格坐标的对应关系。direction=(dx, dy) 中,dx 对应列的变化量,dy 对应行的变化量。因此下一格的坐标为 nr = r + dy, nc = c + dx,而非直觉上的 nr = r + dx, nc = c + dy。
3.3 AI 求解器
AI 求解器使用 DFS(深度优先搜索)回溯 + 记忆化(memoization) 算法。
算法思路:
- 如果棋盘已无箭头,返回空列表(通关);
- 获取所有当前可消除的箭头;
- 依次尝试消除每个可消除箭头(临时移除),递归求解剩余棋盘;
- 如果递归返回有效解,则将当前箭头加入解的开头并返回;
- 如果所有尝试都失败,返回 None(无解)。
记忆化优化:将棋盘状态序列化为不可变元组(所有箭头的 (row, col, direction) 排序后组成的元组)作为 key,缓存该状态的求解结果。避免重复计算相同棋盘状态,大幅提升求解效率。
关键代码(solver.py):
def _dfs(self, board):
if not board.has_arrows():
return []
key = board.get_state_key()
if key in self.memo:
return self.memo[key]
for arrow in board.get_clear_arrows():
board.remove_arrow(arrow)
result = self._dfs(board)
board.add_arrow(arrow.row, arrow.col, arrow.direction, arrow=arrow)
if result is not None:
solution = [(arrow.row, arrow.col, arrow.direction)] + result
self.memo[key] = solution
return solution
self.memo[key] = None
return None
求解器有两个应用场景:
- 智能提示:调用求解器获取完整通关步骤,取第一步作为提示,高亮对应箭头;
- AI 自动求解:获取完整步骤后,每隔 0.55 秒自动执行一步,演示通关过程。
3.4 撤销与存档
撤销:使用历史栈(列表)实现。每次成功消除箭头前,将当前棋盘的箭头状态和失误次数保存为快照压入栈中。撤销时弹出栈顶快照,重建棋盘和失误次数。
存档:使用 JSON 文件保存游戏进度,包括已解锁关卡、当前关卡索引、各关卡最高分、最短用时和最高星级。通关后自动更新存档并解锁下一关。游戏启动时读取存档,"继续游戏"按钮可从上次进度继续。
四、AIGC 使用过程
开发过程中主要使用 Cursor(AI 代码编辑器)和 DeepSeek(代码推理与答疑)两款 AIGC 工具。以下记录 4 次具有代表性的使用过程。
记录 1:路径检测算法生成与坐标调试
| 项目 | 内容 |
|---|---|
| 子任务 | 四方向路径检测算法 |
| 借助何种 AIGC | Cursor |
| AI 实现或提供了什么 | 输入需求后,Cursor 生成了 is_path_clear 方法的完整代码,包括方向向量定义和逐格扫描逻辑 |
| 效果如何 | 代码结构清晰,但方向向量与网格坐标的对应关系写反了(nr = r + dx, nc = c + dy),导致向上/向下检测时实际扫描的是左右方向,测试时边缘箭头判定错误 |
| 人工修改 | 修正坐标对应关系为 nr = r + dy, nc = c + dx,并补充了 4 个方向的边界测试用例,验证通过 |
记录 2:AI 求解器递归溢出问题修复
| 项目 | 内容 |
|---|---|
| 子任务 | AI 自动求解器调试 |
| 借助何种 AIGC | DeepSeek + Cursor |
| AI 实现或提供了什么 | 先在 DeepSeek 中询问"箭头消除游戏如何用回溯算法求解",得到 DFS 思路和伪代码;然后在 Cursor 中生成具体 Python 实现 |
| 效果如何 | 基础 DFS 可以求解小关卡(5×5),但在第 6 关开始出现 RecursionError: maximum recursion depth exceeded。排查发现:回溯恢复时 add_arrow 创建了新的 Arrow 对象,导致后续 remove_arrow 中 self.grid[arrow.row][arrow.col] is arrow 的身份检查失败(grid 中是新对象,不是旧对象),箭头没有被真正移除,递归在原地循环 |
| 人工修改 | 1. 修改 add_arrow 方法,增加 arrow=None 参数,允许传入已有对象;2. 回溯时调用 add_arrow(..., arrow=arrow) 恢复原对象引用;3. 去掉 remove_arrow 中的 is 身份检查,改为按位置直接清空 grid,提升健壮性。修复后第 6-10 关求解正常 |
记录 3:关卡不可解问题与反向构造法
| 项目 | 内容 |
|---|---|
| 子任务 | 10 个关卡设计与验证 |
| 借助何种 AIGC | DeepSeek |
| AI 实现或提供了什么 | 手动设计前 5 关并验证可解后,第 6-10 关手动设计出现 5 关全部无解(求解器返回 None)。询问 DeepSeek"如何保证随机生成的箭头关卡一定可解",AI 给出了反向构造法思路:从空棋盘开始,每次添加一个当前可消除的箭头,添加顺序的反向即为合法消除顺序 |
| 效果如何 | 基于该思路编写了随机关卡生成器(固定随机种子保证可复现),自动生成第 6-10 关;生成后用求解器逐一验证,全部 10 关均确认存在合法通关顺序 |
| 人工修改 | 调整了生成器的棋盘大小和箭头数量参数(第 10 关为 9×9、22 个箭头),确保难度递增合理 |
记录 4:测试脚本路径问题
| 项目 | 内容 |
|---|---|
| 子任务 | 关卡可解性测试脚本 |
| 借助何种 AIGC | Cursor |
| AI 实现或提供了什么 | 生成了 verify_levels.py 测试脚本,自动遍历所有关卡调用求解器验证 |
| 效果如何 | 在项目根目录运行正常,但将脚本移到 tests/ 子目录后运行报 ModuleNotFoundError: No module named 'board',因为 sys.path 只加了脚本所在目录而非项目根目录 |
| 人工修改 | 将 sys.path.insert 的路径从 os.path.dirname(os.path.abspath(__file__)) 改为上两级目录 os.path.dirname(os.path.dirname(os.path.abspath(__file__))),修复后在 tests/ 目录下正常运行 |
五、测试结果
5.1 功能测试(T01-T06)
| 编号 | 测试内容 | 预期结果 | 实际结果 | 是否通过 |
|---|---|---|---|---|
| T01 | 点击前方无阻挡的箭头 | 箭头飞出棋盘并消失,剩余箭头数 -1 | 箭头沿方向飞出并缩小消失,剩余箭头数正确减少 | 通过 |
| T02 | 点击前方有阻挡的箭头 | 箭头不消失,失误次数 -1,有碰撞反馈 | 箭头变红并左右晃动后回到原位,失误心形 -1 | 通过 |
| T03 | 点击边缘且朝向棋盘外的箭头 | 箭头正常消失,不发生越界错误 | 最右列朝右、最左列朝左、最上行朝上、最下行朝下的箭头均正常飞出,无异常 | 通过 |
| T04 | 消除本关全部箭头 | 显示通关界面,可进入下一关 | 最后一个箭头飞出后自动切换到通关界面,显示星级和得分,点击"下一关"成功进入下一关 | 通过 |
| T05 | 失误次数耗尽 | 显示失败界面,允许重新开始 | 连续点击被阻挡箭头 3 次后切换到失败界面,点击"重新开始"恢复当前关卡 | 通过 |
| T06 | 游戏进行中重新开始 | 箭头布局和失误次数恢复初始状态 | 消除部分箭头并扣除失误后点击"重新开始",棋盘恢复初始布局,失误次数恢复为 3 | 通过 |
5.2 附加功能测试
| 测试内容 | 预期结果 | 实际结果 | 是否通过 |
|---|---|---|---|
| 撤销功能 | 撤销后箭头和失误次数恢复到上一步 | 成功恢复,多次撤销可逐步回退 | 通过 |
| 提示功能 | 高亮一个可消除箭头 | 金色闪烁高亮,且该箭头确实可消除 | 通过 |
| AI 自动求解 | 自动演示完整通关步骤 | 逐步自动点击,最终通关 | 通过 |
| 关卡解锁 | 通关后下一关解锁 | 选关界面中下一关变为可点击 | 通过 |
| 存档持久化 | 关闭后重开,进度保留 | 存档文件正确写入,重开后"继续游戏"进入上次关卡 | 通过 |
| 全部关卡可解性 | 10 关均有解 | 求解器验证全部通过 | 通过 |
六、PSP 表格
| 任务 | 预估耗时(小时) | 实际耗时(小时) | 差异(小时) |
|---|---|---|---|
| 需求分析与游戏设计 | 1.5 | 1.0 | -0.5 |
| Python 与图形库学习 | 2.0 | 1.5 | -0.5 |
| 游戏界面实现 | 3.0 | 3.5 | +0.5 |
| 路径与碰撞逻辑实现 | 2.5 | 2.0 | -0.5 |
| 关卡设计 | 2.0 | 3.0 | +1.0 |
| AIGC 辅助开发 | 2.0 | 2.5 | +0.5 |
| 进阶功能开发(求解器/撤销/存档) | 3.0 | 4.0 | +1.0 |
| 测试与修改 | 2.0 | 2.5 | +0.5 |
| README 与博客撰写 | 2.0 | 2.0 | 0 |
| 合计 | 20.0 | 22.0 | +2.0 |
七、心得体会
AI 带来的帮助
- 代码生成效率高:Cursor 的代码补全和对话功能极大提升了编码速度,尤其是界面布局、按钮组件等重复性代码,AI 可以一次生成可用的骨架,省去了查文档的时间。
- 算法思路清晰:在设计 AI 求解器时,DeepSeek 给出的 DFS 回溯 + 记忆化思路非常清晰,比自己从零摸索节省了大量时间。尤其是在关卡设计遇到困难时,AI 给出的"反向构造法"直接解决了关卡不可解的问题。
- Bug 定位快:遇到
RecursionError等问题时,将报错信息和相关代码粘贴给 AI,能快速定位到回溯恢复时对象引用不一致的根本原因。
开发中遇到的主要问题
- 求解器回溯的对象引用问题(最棘手):基础 DFS 写好后,前 5 关正常,但第 6 关开始就报
RecursionError。一开始以为是递归深度太大,后来发现根本原因是:回溯恢复时用add_arrow创建了新的 Arrow 对象,导致remove_arrow中的is身份检查失败,箭头没有被真正移除,求解器在原地无限递归。修复方法是让add_arrow支持传入已有对象,回溯时恢复原引用。这个问题让我深刻体会到"回溯算法不仅要恢复状态,还要保持对象引用一致性"。 - 关卡不可解:手动设计 10 个关卡时,前 5 关靠反向思路勉强设计出来,但第 6-10 关越设计越乱,求解器验证全部返回无解。最后借助 AI 给出的反向构造法,写了一个自动生成器,从空棋盘开始每次添加一个可消除的箭头,才系统地解决了关卡设计问题。这也说明靠人工肉眼验证复杂棋盘的可解性非常不可靠,必须用算法验证。
- 路径检测坐标方向:AI 生成的路径检测代码把
dx/dy和row/col的对应关系写反了,导致向上/向下检测时实际扫描的是左右方向。这个问题不报错,只是游戏行为不对,很隐蔽。后来通过单独写测试脚本逐方向验证才发现。 - 测试脚本路径问题:把测试脚本移到
tests/子目录后,sys.path没有包含项目根目录,导致找不到模块。这个小问题暴露了在多目录项目中如何正确设置 Python 模块搜索路径的细节。 - README 图片路径:用 Typora 写 README 时图片默认用本地绝对路径,push 到 GitHub 后图片全部裂图。后来把图片放到仓库的
docs/images/目录下,改用相对路径才解决。
自己的收获
- 对 AIGC 的定位更清晰:AI 是高效的"副驾驶",能快速生成代码骨架和思路,但核心逻辑的正确性、边界条件的处理、代码间的一致性仍需开发者自己把控。AI 生成的代码不能直接用,必须自己读一遍并写测试验证——路径坐标写反、回溯对象引用错误这两个问题,如果不是亲自测试根本发现不了。
- 软件工程实践能力提升:从需求分析、模块设计、编码实现到测试验证,完整经历了一个小型软件项目的开发流程。模块化的代码结构(棋盘、状态、求解器、界面分离)让后续维护和功能扩展变得容易。
- 算法理解加深:通过亲手实现 DFS 回溯 + 记忆化求解器,对状态空间搜索、剪枝、记忆化等概念有了更直观的理解。尤其是记忆化优化前后的性能差异(从递归溢出到秒级求解),让我切身体会到算法优化的力量。
- 测试的重要性:作业要求的 T01-T06 测试用例看起来简单,但实际开发中正是这些测试帮助发现了路径坐标错误、边缘越界等问题。尤其是写了一个自动化的关卡可解性验证脚本后,每改一次关卡数据都能立刻确认是否可解,大大提升了迭代效率。
浙公网安备 33010602011771号