第二次软件工程作业
| 项目 | 内容 |
|---|---|
| 这个作业属于哪个课程 | 202601 软件工程 |
| 这个作业要求在哪里 | 软件工程第二次作业 |
| 这个作业的目标 | 使用 Python 和 AIGC 完成“一箭又一箭”小游戏 |
| 学号 | 102401317 |
| GitHub 仓库 | 仓库链接 |
一、项目展示
游戏一共四个界面,共 4 个关卡(前两关 5×5、后两关 6×6),每个关卡都经过程序求解验证,保证存在合理的通关顺序。
开始界面 —— 启动后停在这里,显示标题、玩法说明和「开始游戏」按钮。
![开始界面]

游戏界面 —— 顶部信息栏显示当前关卡、剩余箭头、剩余失误和「重新开始」按钮,下方是棋盘。四种箭头用颜色区分:绿=上、红=下、蓝=左、橙=右。
![游戏界面]

通关界面 —— 清空本关全部箭头后弹出「本关通过!」,点「下一关」继续;最后一关显示「全部通关!」。
![通关界面]

失败界面 —— 失误耗尽后弹出「失误耗尽」,点「重新开始」恢复到本关初始状态。
![失败界面]

二、项目介绍
2.1 游戏规则
棋盘上分布着上、下、左、右四个方向的箭头,玩家需要观察箭头方向以及它们之间的相互阻挡关系,按合适的顺序点击,让所有箭头依次飞出棋盘:
- 点击某个箭头,程序沿它所指方向、从前方一格扫描到棋盘边界;
- 前方无其它箭头阻挡 → 箭头飞出棋盘并被消除;
- 前方有其它箭头阻挡 → 箭头无法飞出,触发碰撞反馈(原地晃动 + 红色闪烁),并消耗一次失误机会;
- 清空本关全部箭头 → 进入下一关;
- 失误次数耗尽 → 本关失败,可重新开始。
每关有 3 次失误机会。共 4 关,前两关 5×5、后两关 6×6,难度随箭头数量与布局复杂度递增。
2.2 界面设计
- 窗口:520×720,60 FPS 刷新;
- 布局:顶部信息栏(HUD)固定显示当前关卡、剩余箭头数、剩余失误次数与「重新开始」按钮,下方为棋盘区域;
- 配色:四种箭头用四种颜色区分(绿=上、红=下、蓝=左、橙=右),碰撞反馈统一用红色,通关用绿色,视觉上「安全 / 警告」一目了然;
- 字体:直接加载系统自带微软雅黑
msyh.ttc,保证中文正常显示且不依赖外部素材。
2.3 主要功能与特色
- 四个方向箭头的路径阻挡检测与飞出消除;
- 飞出动画(沿方向移动 + 淡出)、碰撞动画(晃动 + 闪烁);
- 4 个手工设计的关卡,每关都经过程序求解验证可通关;
- 完整的五状态机:开始 / 游戏中 / 通关 / 全部通关 / 失败;
- 自动化测试脚本,覆盖可解性、通关流程与碰撞逻辑;
- 零外部素材:箭头、界面全部用 Pygame 图形原语绘制,字体用系统字体。
三、实现思路
代码集中在单文件 arrow_game.py 中,结构分为:常量与配色 → 关卡数据 → 工具函数 → Game 类(核心逻辑)→ 主程序。
3.1 箭头、方向与关卡的表示
箭头方向用一个「行增量、列增量」的字典表示(行向下增长、列向右增长):
DIRS = {
'U': (-1, 0), # 上
'D': (1, 0), # 下
'L': (0, -1), # 左
'R': (0, 1), # 右
}
箭头图标统一预渲染成「朝右」,再用 pygame.transform.rotate 旋转出其它方向(R:0° / U:90° / L:180° / D:270°),省去为每个方向单独画图的麻烦。
关卡用字符串数组表示:. 是空格,U/D/L/R 是箭头。例如第 1 关:
{
"name": "第 1 关",
"mistakes": 3,
"grid": [
".RRR.",
"U...D",
"U.U.D",
"U...D",
".LLL.",
],
}
加载关卡时把字符串数组转换成二维 board,None 表示空格、方向字符表示箭头。
3.2 路径检测(核心算法)
这游戏 90% 的逻辑都集中在「判断一个箭头能否飞出棋盘」上:
def can_fly(self, r, c):
dr, dc = DIRS[self.board[r][c]]
rr, cc = r + dr, c + dc
while 0 <= rr < self.rows and 0 <= cc < self.cols:
if self.board[rr][cc] is not None: # 路上有箭头 → 被挡
return False
rr, cc = rr + dr, cc + dc
return True # 走到边界都没挡 → 能飞
思路就是「沿方向一步步走,撞到边界就是能飞,撞到箭头就是被挡」。对应题目里的两个例子:→ · · ↑ · 里第一个 → 一路往右会撞上 ↑,返回 False;↑ · · · → 里最后的 → 往右一路到边界都是空的,返回 True。这个函数一写对,整个游戏的骨架就立起来了。
3.3 飞出与碰撞动画
点击箭头的处理在 click_arrow 中分成两支:
- 能飞:把
board[r][c]置为None,同时往fly_anims里压入一条动画记录(起点坐标、方向、初始透明度、速度),在update中让图标沿方向以speed=540像素/秒移动,透明度线性衰减,完全淡出后移除; - 被挡:
mistakes_left -= 1,往hit_flashes里压入碰撞记录,绘制时用sin函数做左右小幅晃动,并叠加一层逐渐消失的红色半透明闪烁,持续约 0.4 秒。
飞出动画的时机我特别注意过:只有箭头完全飞出窗口后才算真正消除,避免「还没飞出就从棋盘上消失」的割裂感。
3.4 关卡可解性验证
手工设计关卡最容易出的问题是「设计出一个死局」——某些箭头互相卡死,永远消不完。为此我写了一个 DFS 求解器 solve(level):反复寻找当前「能飞」的箭头、消除它、递归求解剩余棋盘,直到清空或证明无解。4 个关卡全部通过该求解器验证后才定稿。
四、AIGC 使用过程
本次开发全程使用 Claude Code 辅助。协作节奏是:我把要求丢给它,它写代码;我再让它写测试,把测出来的问题喂回去修,循环到全绿。分工上,AI 负责「写」,我负责「判」——通过实际运行和测试,对 AI 给出的代码进行判断、修改和完善。
4.1 协作过程概览
| 子任务 | AI 提供的内容 | 实际效果 | 人工修改 |
|---|---|---|---|
| 学习 Pygame 用法 | 梳理主循环、鼠标事件、图形绘制 | 直接可用 | 无 |
| 设计代码结构 | 把作业要求拆成模块,设计单文件结构 | 清晰 | 无 |
| 编写核心算法 | can_fly 路径检测、棋盘绘制 |
正确 | 无 |
| 设计关卡数据 | 设计 4 个关卡 | 部分关卡存在死局风险 | 加 DFS 求解器逐关验证 |
| 查找修改 Bug | 定位并修复启动跳关、字体崩溃 | 基本修复 | 见下文三个案例 |
| 编写测试用例 | 自动化测试脚本 | 覆盖不全 | 补充碰撞逻辑、动画稳定性测试 |
4.2 案例一:游戏一启动就跳过开始界面
问题:作业明确要求有「开始界面」,但游戏一运行直接蹦进了游戏界面。
定位:AI 在构造函数里调用了关卡加载函数,而这个函数顺手把状态设成了「游戏中」——游戏刚出生就被踹进了游戏里。
修复:启动时停在开始界面,等玩家点「开始游戏」再加载第一关。
启发:这种「状态机初始状态没摆对」的 bug,不实跑一遍根本发现不了——逻辑上看每句话都没错,合起来就是错的。
4.3 案例二:字体加载直接崩溃
问题:跑起来一到画文字的地方就报错:
TypeError: expected str, bytes or os.PathLike object, not int
定位:pygame.font.match_font() 遍历系统字体注册表时,撞上了一个 DWORD 类型的注册表项直接炸了(Pygame 2.6.1 在部分 Windows 上的已知坑)。
修复:不再查询系统字体,改为按文件路径直接加载微软雅黑 msyh.ttc,并做多字体回退(雅黑 → 黑体 → 宋体 → 默认)。
启发:具体到报错堆栈的描述,比「字体显示不出来」这种笼统说法有用得多。
4.4 案例三:控制台编码的坑
问题:测试脚本往 GBK 编码的 Windows 控制台里打印 emoji,直接 UnicodeEncodeError。
修复:把标准输出重定向成 UTF-8(sys.stdout.reconfigure(encoding="utf-8")),并少用控制台打不了的字符。
三个坑,一个是逻辑 bug,两个是环境兼容问题。它们有个共同点:都是 AI 初次生成的代码里没有的,只有真正跑起来才暴露。
五、测试结果
通过 python test_arrow_game.py 运行自动化测试(使用虚拟显示器,无需真实窗口),测试结果如下:
| 编号 | 测试内容 | 预期结果 | 实际结果 | 是否通过 |
|---|---|---|---|---|
| T01 | 4 个关卡可解性 | 每关均能找出完整通关顺序 | 4 关全部可解 | 通过 |
| T02 | 开始界面 → 游戏界面 | 点击「开始游戏」进入 PLAYING | 状态正确切换 | 通过 |
| T03 | 按正确顺序消除 | 剩余箭头为 0,且不消耗失误 | 全部通关 | 通过 |
| T04 | 通关进入下一关 / 全部通关 | CLEAR → 下一关,最后一关 WIN | 正确 | 通过 |
| T05 | 碰撞扣失误 | 点被挡箭头消耗 1 次失误且箭头不消失 | 正确 | 通过 |
| T06 | 动画与绘制 | 连续 60 帧更新 + 绘制无异常 | 无异常 | 通过 |
此外,求解器在每关求解时都校验了「消除后剩余箭头数归零」和「正确顺序不消耗失误」,保证关卡设计本身没有死锁。
六、PSP 表格
| 任务 | 预估耗时(h) | 实际耗时(h) | 差异 |
|---|---|---|---|
| 需求分析与玩法设计 | 1.0 | 1.0 | 0 |
| Python / Pygame 学习 | 1.5 | 2.0 | +0.5 |
| 路径检测算法实现 | 1.5 | 1.5 | 0 |
| 关卡设计与可解性验证 | 1.5 | 1.0 | -0.5 |
| 界面与动画实现 | 2.0 | 2.5 | +0.5 |
| 测试与调试 | 1.5 | 2.0 | +0.5 |
| 文档撰写 | 1.0 | 1.5 | +0.5 |
| 总计 | 10.0 | 11.5 | +1.5 |
七、心得体会
这次作业让我重新理解了「AI 写代码」这件事。
AI 确实能大幅提效:搭框架、写算法、画关卡、写测试、定位 bug,样样都行,而且快得离谱。
但 AI 不是替你写代码,而是和你一起写。它第一次交出来的东西可能带着逻辑 bug(跳过开始界面),可能撞上环境兼容问题(字体崩溃、控制台编码)——这些都得靠你自己去跑、去读、去判断、去改。
最深的一点体会是:「能跑」和「能用」之间隔着一条河。AI 给出的代码逻辑上往往是对的,但真正跑起来,问题常藏在环境、编码、字体这些边边角角的地方。所以题目里那句「要基本理解自己提交的代码」,我这次是真的体会到了——不是背下来的理解,是踩过坑之后的理解。
说白了:AI 负责出草稿,你负责把草稿改对,判断力始终在自己手上。
八、资源与仓库说明
- 图形全部用 Pygame 图形原语绘制(多边形箭头 + 旋转),字体用系统自带微软雅黑,无外部素材依赖;
- 主程序
arrow_game.py,测试test_arrow_game.py,游戏截图位于screenshots/; - 代码与文档见 GitHub 仓库:power819/arrow-escape。
# 运行游戏
python arrow_game.py
# 运行自动化测试
python test_arrow_game.py
本文由 Claude Code 协助完成开发,由我本人完成验证与成文。 |

浙公网安备 33010602011771号