软件工程第二次作业
软件工程第二次个人作业:用 AIGC 完成「一箭又一箭」小游戏
| 项目 | 一箭又一箭 |
| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/fzu/202601SofwareEngineering|
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/fzu/202601SofwareEngineering/homework/16717 |
| 这个作业的目标 | 使用 Python 和 AIGC 完成「一箭又一箭」小游戏 |
| 学号 | <102401203> |
| GitHub 仓库 | https://github.com/aurogons/aurogons |
一、项目展示
开始界面

标题下方是「开始游戏」按钮和关卡选择。未解锁的关卡显示为灰色不可点击,
首次进入时只有第一关可选。底部列出了完整规则,玩家不需要看说明文档就能上手。
游戏过程

顶部信息栏显示当前关卡、剩余箭头数量(带进度条)、剩余失误次数和用时。
棋盘上四种方向的箭头用了四种颜色,扫一眼就能分清朝向。

第五关是 6×6 的大棋盘,16 个箭头。格子大小会根据棋盘尺寸自动缩放,
所以大棋盘也不会挤爆窗口。
碰撞反馈

点击一个飞不出去的箭头时,箭头会原地抖动、闪烁变红,同时挡住它的那个箭头会被
高亮圈出,顶部还会提示剩余失误次数。让玩家一眼就知道「我为什么飞不出去」,
而不是只看到一个笼统的错误提示。
提示功能

点「提示」会高亮一个当前可以飞出的箭头。连续点会依次轮换所有可选项,
方便玩家对照着找规律。
通关与失败


结算界面保留最后的棋盘作为背景,上面盖一层半透明遮罩,显示用时、点击次数和
剩余失误。通关时如果还有下一关,会多出一个「下一关」按钮。
二、项目介绍
游戏规则
棋盘上散布着朝上、下、左、右四个方向的箭头。点击箭头时检查它前进方向上
到棋盘边界之间还有没有其他箭头:
- 前方畅通 → 箭头沿自身方向飞出棋盘并消失;
- 前方有阻挡 → 箭头飞不出去,消耗一次失误机会;
- 清空全部箭头即通关;失误次数用尽则本关失败。
界面设计
三个界面:开始界面(标题、开始游戏、关卡选择、退出、规则说明)、
游戏界面(信息栏 + 棋盘 + 操作按钮)、结果界面(通关 / 失败结算)。
游戏界面按作业要求显示了当前关卡、棋盘、剩余箭头数量、剩余失误次数和重新开始按钮,
另外加了用时、提示、撤销和返回。
配色上刻意避开了红色系作为箭头颜色(四方分别用琥珀、青绿、天蓝、淡紫),
把红色专门留给碰撞反馈,避免玩家把「朝右的箭头」误认成「出错了」。
主要功能
| 功能 | 说明 |
|---|---|
| 四方向箭头与阻挡判定 | 同一行或同一列上的路径检测 |
| 飞出动画 | 箭头沿自身方向滑出窗口,后半程淡出 |
| 碰撞反馈 | 抖动 + 闪红 + 高亮挡路箭头 + 文字提示 |
| 失误次数 | 界面实时显示,用尽则失败 |
| 5 个关卡 | 4×4 到 6×6,规模与失误上限递增 |
| 关卡选择与解锁 | 通关后解锁下一关,可从主菜单直接跳关 |
| 重新开始 | 恢复本关初始布局与失误次数 |
| 撤销 | 退回上一步,箭头与失误次数一起回退 |
| 提示 | 高亮一个当前可飞出的箭头,可连点轮换 |
| 计时与统计 | 记录用时和点击次数 |
三、实现思路
1. 箭头、方向与关卡怎么表示
方向用枚举表示,枚举值直接就是前进增量:
class Direction(Enum):
UP = (-1, 0)
DOWN = ( 1, 0)
LEFT = ( 0, -1)
RIGHT = ( 0, 1)
这样四方向不需要写四套分支,路径检测可以共用同一段代码。
箭头本身是一个不可变的 Arrow(row, col, direction),棋盘则用
dict[(row, col)] -> Arrow 存放。用字典而不是二维数组有两个好处:
棋盘通常是稀疏的,字典更省;而且越界坐标只会取到 None,
不会抛出 IndexError。
关卡用字符画描述,而不是一串坐标元组:
art = """
v . v . .
> . . ^ .
. . . ^ .
. . . ^ <
> v . . .
"""
2. 路径检测(重点)
这是本项目的核心算法。写法是沿方向逐格推进,直到走出棋盘:
def path(self, arrow):
"""按箭头朝向,依次产出它前方棋盘内的所有格子。"""
drow, dcol = arrow.direction.delta
row = arrow.row + drow
col = arrow.col + dcol
while 0 <= row < self.rows and 0 <= col < self.cols:
yield (row, col)
row += drow
col += dcol
def is_free(self, arrow):
return all(cell not in self.arrows for cell in self.path(arrow))
这段代码有两个值得说的地方:
第一,四个方向共用一段逻辑。 方向不是用 if direction == UP: ... elif ...
去分支,而是化成了增量 (drow, dcol)。四套分支的写法在实现碰撞反馈时
还要再写一遍,容易出现某个方向改了、另一个方向忘了改的情况。
第二,循环条件本身就是边界检查。 走出棋盘循环自然结束,
不需要额外判断「下一步会不会越界」。所以调用方拿到的坐标永远是合法的。
边界情况(T03)也就自然成立了:位于边缘且朝向棋盘外的箭头,
第一步就走出棋盘,path 产出空序列,all(...) 对空序列返回 True,
箭头顺利飞出。
顺带一提,path 是生成器,is_free 和 blocker_of 都复用了它:
def blocker_of(self, arrow):
"""返回挡在箭头前方的第一个箭头,用于高亮提示。"""
for cell in self.path(arrow):
blocker = self.arrows.get(cell)
if blocker is not None:
return blocker
return None
碰撞反馈要「高亮是谁挡住了我」,直接复用同一个 path 就行,
不用再写一遍遍历。
3. 关卡的可通关性是可以机械验证的
设计关卡时最容易犯的错误是摆出一个无论怎么点都通不了的死局。
人肉试玩验证既慢又不可靠,所以我写了一个校验器 tools/validate_levels.py。
它能自动验证,靠的是游戏规则的一个性质:消除箭头只会让棋盘更空,
绝不会把别的箭头从「能飞」变回「被挡」。 也就是说「当前可飞出的箭头集合」
随消除单调递增——已经解锁的箭头永远是解锁的。
由此得到两条结论:
- 如果这一关存在通关顺序,那么「每次随便挑一个当前能飞的箭头点掉」这种
完全不看全局的贪心策略,也一定能清空棋盘(先走哪一步都不会把路走死); - 反过来,如果贪心过程中卡住了(还剩箭头,但一个都飞不出去),
这一关就必定无解,不存在某条巧妙路线能救回来。
于是关卡的可通关性就变成了一个几行代码的循环。校验器还会额外检出
「互相阻挡的箭头对」:如果 A 前方的第一个箭头是 B,B 前方的第一个箭头又是 A,
那么放走 A 必须先移走 B,移走 B 又必须先放走 A——死循环。
这类布局用肉眼看字符画时特别容易漏掉,尤其是当两个箭头隔了好几格、
中间还夹着别的空格的时候。
4. 动画与逻辑解耦
一个设计上的选择:点击时立即更新棋盘状态,飞出动画只是一个与棋盘无关的
视觉残影对象。
如果改成「动画播放期间锁住输入」(这也是常见做法),每点一下都要等 0.2~0.3 秒
才能点下一个,手感会明显发滞。而现在状态已经更新完了,动画只是画面上在飘,
玩家可以连着快速点击,也不会出现「上一次动画没结束就点下一发」导致状态错乱的问题。
碰撞反馈的抖动方向取的是垂直于箭头朝向的那一轴:横着的箭头上下抖,
竖着的左右抖。一开始试过顺着箭头方向抖,结果看起来像「飞出动画卡了一下」,
垂直抖动才能明确表达「被挡回来了」。
四、AIGC 使用过程
本项目的开发全程使用 Claude Code 协助,采用「我提要求 → AI 出方案和代码 →
我实际运行验证 → 发现问题回头修改」的方式推进。下面记录 5 次比较有代表性的过程。
汇总表
| 子任务 | 借助何种 AIGC 技术 | AI 实现或提供了什么 | 效果如何 | 人工修改 |
|---|---|---|---|---|
| 关卡设计 | Claude Code | 生成 5 个关卡的字符画布局 | 第 4、5 关摆出了死局,校验器报错 | 定位到两处「正面相对」的箭头对,把其中一个改成同向 |
| 关卡校验器 | Claude Code | 提出用贪心法判定可通关性,并实现校验脚本 | 有效,能自动判定每一关是否可解 | 追加了「互相阻挡箭头对」的专门检测,让报错指向病根 |
| 路径检测 | Claude Code | 给出用增量 + while 循环的统一写法 | 四方向共用一段代码,无越界问题 | 采用;并确认边界情况(T03)自然成立 |
| 中文字体 | Claude Code | 按系统字体路径依次回退的加载方案 | 中文正常显示,不再是方块 | 补上找不到字体时退回 SysFont 的兜底 |
| 界面排版 | Claude Code | 生成界面并自动渲染截图 | 截图里发现三处排版问题 | 数值列左边缘对齐、进度条加宽、结算按钮整体居中 |
| 单元测试 | Claude Code | 按 T01~T06 编号生成测试 | 有 6 个用例失败 | 检查后确认是测试写错了,不是代码有 bug,改正用例 |
记录 1:AI 设计的关卡摆出了死局,靠校验器抓出来
这是整个开发里最有价值的一次协作。
我提出需求:「设计 5 个关卡,规模从 4×4 递增到 6×6,难度递增,用一个校验器保证每关都能通关。」
AI 先把自己的设计思路写清楚了——它指出了一条硬约束:同一条直线上的两个箭头
不能正面相对,否则互相阻挡、谁也飞不出去;想造「链式阻挡」应该让同一条线上的
箭头朝向一致,这样最上面的飞走后下面的依次解锁。然后它给出了 5 关布局和
校验脚本。
实际运行校验器的结果:
[1] 第一关 · 初识规则 可通关:贪心解法 4 步清空棋盘
[2] 第二关 · 阻挡链 可通关:贪心解法 6 步清空棋盘
[3] 第三关 · 拆外圈 可通关:贪心解法 9 步清空棋盘
[4] 第四关 · 长距离阻挡 无法通关:卡住时还剩 4 个箭头
死局箭头 (0,0)v (0,2)v (4,0)> (4,2)^
[5] 第五关 · 综合 无法通关:卡住时还剩 6 个箭头
有意思的是,AI 明明在注释里把「不能正面相对」这条规则写对了,
自己设计关卡时却还是踩了进去——第四关的 (0,2)v 和 (4,2)^ 在同一列上正面相对,
第五关修完纵向的问题之后,又在 (4,0)> 和 (4,4)< 之间造出了一个横向的同类错误。
我的处理:把这两个箭头改成同向,重新跑校验器确认全部通过。同时让 AI 给校验器
加了一段专门检出「互相阻挡的箭头对」的逻辑,这样以后报错会直接指出是哪两个箭头,
而不是只说「卡住了」。
这次经历说明:AI 能准确地讲清规则,但不保证自己遵守规则。
把规则写成可以自动执行的检查(校验器、测试),比指望 AI 每次都做对要可靠得多。
记录 2:单元测试报错,但错的是测试本身
写完测试后运行,出现 6 个失败,都来自路径检测的用例。例如:
FAIL: test_路径只包含棋盘内的格子
AssertionError: Lists differ: [] != [(1, 0), (2, 0)]
AI 写的用例是这样的:棋盘上 ^ 放在 (0,0),期望它的路径是 [(1,0), (2,0)]。
我的处理:检查后发现是测试写错了——^ 表示朝上,放在第 0 行意味着
它就在棋盘最上边缘、第一步就走出去了,路径本来就应该是空的
(这恰好是 T03 要测的情形)。AI 想要的是「沿这一列往下走」,那应该用 v。
改正用例后全部通过。
这次提醒了我一件事:测试失败时不能默认「代码有 bug」,
也可能是测试的预期本身就是错的。判断的依据应该是回到规则本身去想
——「朝上的箭头放在最上面一行,它前方还有格子吗?」
记录 3:中文字体渲染成方块
第一次运行游戏,所有中文都变成了方块(俗称「豆腐块」)。
原因是 Pygame 的默认字体不含中文字形。
AI 的处理:给出了显式加载系统中文字体的方案,按
msyh.ttc → simhei.ttf → simsun.ttc 的顺序查找,找到第一个存在的就用。
我的修改:补上了兜底——万一这三个字体都不存在(比如换到 Linux 或 macOS 上),
退回 pygame.font.SysFont 按字体名查找,至少不至于直接崩掉。
另外给字体对象加了缓存,避免每帧都重新加载字体文件。
记录 4:靠自动截图发现界面排版问题
游戏跑通之后,AI 提出可以写一个脚本用 SDL 的无界面驱动把各个界面渲染成 PNG。
这本来是为了给 README 准备截图,结果意外成了有效的界面检查手段——
把截图打开一看,发现了三处之前没意识到的问题:
- 信息栏三栏数值左边缘没有对齐:标签宽度不同(「用时」比「剩余箭头」短得多),
而代码是给所有栏用同一个固定偏移,导致「用时」和它的数值之间空了一大块; - 通关进度条只有 400 像素宽,缩在左边,看起来像一条多余的线;
- 失败界面上只有两个按钮(没有「下一关」),但按钮位置是按三个按钮的布局写死的,
整体偏右没有居中。
我的处理:三项都改了——数值位置改成按最宽标签动态计算,
进度条改成横贯整个信息栏,结算按钮改成按实际宽度整体居中。
记录 5:控制台中文乱码与窗口尺寸自适应
两个小问题,但都属于「不实际跑一遍就发现不了」的类型。
一是 Windows 控制台的默认编码是 GBK,工具脚本输出的中文全是乱码。
AI 的解决办法是在脚本开头把 sys.stdout 重新配置成 UTF-8。
二是 6×6 的棋盘如果沿用 4×4 的格子尺寸,棋盘高度会超出窗口。
改成格子边长根据棋盘尺寸和可用区域动态计算,并设了上下限,
这样 4×4 时格子更大更好点,6×6 时自动缩小也能完整显示。
五、测试结果
手工测试
| 编号 | 测试内容 | 预期结果 | 实际结果 | 是否通过 |
|---|---|---|---|---|
| T01 | 点击前方无阻挡的箭头 | 箭头飞出棋盘并消失 | 箭头沿方向滑出并淡出,剩余箭头数 -1 | 通过 |
| T02 | 点击前方有阻挡的箭头 | 箭头不消失,失误次数减 1 | 箭头原地抖动变红,挡路箭头被高亮,失误 -1 | 通过 |
| T03 | 点击位于边缘且朝向棋盘外的箭头 | 箭头正常消失,不发生越界错误 | 四条边的箭头均正常飞出,无异常 | 通过 |
| T04 | 消除本关全部箭头 | 显示通关并进入下一关 | 弹出通关结算,点「下一关」正常切换 | 通过 |
| T05 | 失误次数耗尽 | 显示失败并允许重新开始 | 弹出失败结算,可重玩本关,失误数恢复 | 通过 |
| T06 | 游戏进行中重新开始 | 箭头布局和失误次数恢复 | 布局、失误数、用时、撤销历史全部复原 | 通过 |
自动化测试
python -m unittest discover -s tests -v
共 72 个用例,全部通过:
Ran 72 tests in 2.882s
OK
测试分三个文件:
tests/test_board.py—— 棋盘规则与路径检测,按 T01~T06 编号组织,
另加边界用例(点击空格、点击越界坐标、点击已经飞出的格子、
失误归零后继续点击、非法构造参数等);tests/test_levels.py—— 断言每一关都能通关、没有互相阻挡的箭头对、
开局至少有一个可选项、难度随关卡递增;tests/test_integration.py—— 用 SDL 的 dummy 视频驱动把整个游戏跑起来,
覆盖场景切换、通关解锁、失败重玩、碰撞反馈、撤销与提示。
关卡校验
python tools/validate_levels.py
[1] 第一关 · 初识规则 4x4 4 个箭头 初始可飞出 2 个 (50%) 可通关
[2] 第二关 · 阻挡链 4x4 6 个箭头 初始可飞出 3 个 (50%) 可通关
[3] 第三关 · 拆外圈 5x5 9 个箭头 初始可飞出 3 个 (33%) 可通关
[4] 第四关 · 长距离阻挡 5x5 12 个箭头 初始可飞出 5 个 (42%) 可通关
[5] 第五关 · 综合 6x6 16 个箭头 初始可飞出 6 个 (38%) 可通关
全部 5 关均可通关。
六、PSP 表格
| 任务 | 预估耗时(小时) | 实际耗时(小时) | 差异(小时) |
|---|---|---|---|
| 需求分析与游戏设计 | 1.0 | ||
| Python 与图形库学习 | 1.5 | ||
| 游戏界面实现 | 2.0 | ||
| 路径与碰撞逻辑实现 | 2.0 | ||
| 关卡设计 | 1.5 | ||
| AIGC 辅助开发 | 2.0 | ||
| 测试与修改 | 2.0 | ||
| README 与博客撰写 | 1.5 | ||
| 合计 | 12 |
七、心得体会
AI 帮到了什么
最直接的帮助是把想法快速变成能跑的东西。四方向枚举设计成携带增量、
path 用生成器复用、动画与逻辑解耦——这些设计上的选择,AI 都能一次给出比较合理的版本,
省去了大量查文档和试错的时间。写字符画关卡解析、中文字体回退这类
「知道该有、但写起来琐碎」的代码也很省事。
出现的问题
但这次开发里,AI 犯的错误也很有代表性,而且恰恰都犯在它自己刚讲清楚的规则上:
- 它在注释里写明了「同一条线上两个箭头不能正面相对」,然后自己设计的第 4、5 关
就摆出了这种死局,修完一个又犯一个; - 它写测试用例时把
^放在棋盘顶端还期望路径往下走,方向理解反了。
这让我意识到,AI 输出的内容「读起来对」和「实际对」是两回事。
它能把规则表述得很准确,但不保证自己在生成具体内容时遵守了规则。
收获
最大的收获是学会了把规则变成可以自动执行的检查,而不是靠反复检查 AI 的输出。
校验器就是很好的例子:我没有去逐关盯着一格格检查布局,而是先想清楚
「可通关」这件事能不能自动判定——结果发现因为「消除只会让棋盘更空」这个单调性,
贪心法就能判定,于是整个验证变成了几行代码。这个工具后来真的抓出了两处死局,
而且抓出来的方式比我肉眼看字符画可靠得多。
测试也一样。72 个用例里有 6 个一开始是失败的,其中一部分是我的测试预期写错了。
这个过程让我养成了习惯:测试报错时先回头想规则本身是什么样,
再判断到底是代码错了还是测试错了。
另一个体会是关于分层。把 board.py 写成不依赖 Pygame 的纯逻辑模块,
一开始只是为了「方便测试」,但实际收益比预想的大得多——
核心规则可以在没有显示器的环境里秒级验证,改关卡后跑一遍测试就知道有没有改坏。
如果规则判断和界面绘制混在一起,这些都会变得很麻烦。
最后,我觉得 AI 更适合当「执行力很强的合作者」而不是「正确答案的来源」。
方案的取舍、规则的正确性、以及最后「这个东西到底对不对」的判断,还是得自己来。
浙公网安备 33010602011771号