2026秋软件工程第二次个人作业
一箭又一箭 · 实验报告
作业信息
| 项目 | 内容 |
|---|---|
| 这个作业属于哪个课程 | 《软件工程》( https://edu.cnblogs.com/campus/fzu/202601SofwareEngineering ) |
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/fzu/202601SofwareEngineering/homework/16717 |
| 这个作业的目标 | 使用 Python 和 AIGC 完成"一箭又一箭"小游戏 |
| 学号 | 102401418 |
| GitHub 仓库 | https://github.com/Bari328/arrow-puzzle |
| 开发工具 | Codex(GPT-5)辅助编码 + 人工设计、试玩与验证 |
一、项目展示
1.1 运行截图
开始界面:发光标题、背景 16 支缓慢飘动的箭头、左侧"怎么玩"卡片、右侧"实时演示"小棋盘
(每 0.6 秒自动射出一支箭并补位)、右上角音效开关。

游戏界面:顶部信息条显示当前关卡、剩余箭头(带进度条)、失误机会(圆点)、得分;
中间是棋盘;底部是重新开始 / 返回主菜单按钮与操作提示。
鼠标悬停在箭头时,会画出这条箭头的飞行路径 —— 绿色表示一路畅通,红色表示被挡,并把挡路的箭头
描成橙色框,这样玩家在点击前就能判断能不能飞出去:


碰撞与飞出同时发生的瞬间:左边有一支箭正在飞出并淡出,右边被点中的箭头正在左右晃动、
闪红框,挡住它的箭头被描成橙框,同时飘出"被挡住了!失误剩余 N"的提示文字。

失误归零瞬间:晃动动画还在播放,屏幕中央提示"失误耗尽,本关失败!",动画播完才切到失败界面。

通关界面(按剩余失误给 1~3 星评价,倒计时后自动进入下一关)与失败界面:


二、项目介绍
2.1 游戏规则
-
棋盘是二维网格,每格取值
0=空、1=向上↑、2=向下↓、3=向左←、4=向右→。 -
点击一支箭后,程序沿箭头方向检查它到棋盘边界之间的格子:
- 中间没有其它箭头 → 箭头飞出棋盘并消失,得 10 分;
- 中间有其它箭头 → 箭头不能消除,出现碰撞反馈(左右晃动 + 红框 + 标出阻挡者 + 文字提示),
消耗 1 次失误并扣 5 分。
-
每关初始 3 次失误,失误耗尽即本关失败,可以重玩本关或返回主菜单。
-
清空全部箭头即通关,展示战绩后自动进入下一关(也可以按空格立即进入)。
-
关卡会越来越难:棋盘从 5×5 逐渐增大到 9×9,箭头密度从 30% 提高到 62%。
2.2 界面设计
| 界面 | 内容 |
|---|---|
| 开始界面 | 发光标题与副标题、背景飘动箭头、怎么玩 卡片、实时演示 卡片、开始 / 继续 / 退出三个主按钮、右上角音效开关、最高通关与累计得分、键位提示 |
| 游戏界面 | 关卡号、剩余箭头 "N / M" 与进度条、失误机会圆点、得分、棋盘与箭头、悬停路径辅助线、重新开始与返回主菜单按钮、底部操作提示 |
| 通关界面 | 半透明遮罩 + 战绩面板:本关得分、剩余失误、星级评价、自动进入下一关的倒计时进度条、进入下一关按钮 |
| 失败界面 | 半透明遮罩 + 失败面板:关卡号、剩余箭头、两条玩法小提示、重玩本关与返回主菜单按钮 |
配色上用四种颜色区分四个方向(上=蓝、下=粉、左=黄、右=绿),深色背景衬托箭头,
关键状态用颜色说话:可飞出=绿色路径、被挡住=红色路径、阻挡者=橙色描边、失误=红色圆点。
2.3 功能清单
| 作业要求 | 实现情况 |
|---|---|
| 图形化界面、可正常操作 | ✅ 1024×768 窗口,鼠标点击操作,60 FPS |
| 四种方向的箭头 | ✅ 上/下/左/右,四色区分,用字体字符 ↑ ↓ ← → 绘制 |
| 鼠标点击选择箭头 | ✅ 支持点击判定与鼠标悬停高亮 |
| 判断前方是否存在其它箭头 | ✅ board.check_block(),四个方向分别实现 |
| 前方无阻挡时飞出消失 | ✅ 绿色路径提示 + 飞出动画 + 淡出 + 得分 |
| 前方有阻挡时不能消失并给出明显反馈 | ✅ 左右晃动 + 红框 + 橙色标出阻挡者 + 飘字提示 + 音效 |
| 设置并显示剩余失误次数 | ✅ 每关 3 次,顶部用圆点显示,实时更新 |
| 至少 3 个可正常通关的关卡 | ✅ 关卡数量不限(棋盘 5×5 → 9×9 递进),已逐关试玩验证 |
| 通关后显示结果并进入下一关 | ✅ 通关面板 + 2 秒倒计时自动进关(空格可跳过) |
| 失误耗尽显示失败结果 | ✅ 晃动动画播完后自动切换到失败界面 |
| 重新开始功能,恢复初始状态 | ✅ R 键或按钮,棋盘布局与失误次数全部还原 |
| 界面包含开始 / 游戏 / 结果界面 | ✅ 四个界面(开始、游戏、通关、失败) |
| 飞出与碰撞有动画或视觉反馈 | ✅ 飞出淡出、左右晃动、碰撞闪光、文字飘出 |
2.4 特色
- 悬停路径辅助线(可按
H开关):实时画出待点击箭头的整条飞行路径,把"能不能飞出"可视化。 - 程序化关卡生成:用逆向构造法保证生成的关卡一定可解,可以一直玩下去,不限于固定关卡表。
- 星级评价与得分:清关时按剩余失误给 1~3 星,累计得分记录在开始界面。
- 代码合成音效:7 种音效全部由代码生成波形,仓库里没有任何音频文件。
- 帧计数动画:碰撞晃动用主循环帧号驱动,不使用
time.sleep,不阻塞主循环。 - 自动化自检:280 项断言覆盖规则、边界、排版、像素与音效。
三、实现思路
3.1 数据是怎么表示的
| 对象 | 表示方式 |
|---|---|
| 箭头方向 | 整数常量 UP=1 / DOWN=2 / LEFT=3 / RIGHT=4,空地为 0(与作业约定一致) |
| 棋盘 | 二维列表 grid[row][col],外层再包一个 Board 类,提供读写、快照与规则判定 |
| 方向向量 | 字典 DIRECTION_VECTORS = {UP: (-1,0), DOWN: (1,0), LEFT: (0,-1), RIGHT: (0,1)} |
| 关卡 | LevelConfig(序号, 行数, 列数, 箭头数量, 失误次数);棋局本身由生成器产出,不写死关卡表 |
| 关卡进度 | App.level_index(当前关)、best_level(最高通关)、total_score(累计得分) |
| 当前状态 | App.state 取 menu / play / clear / over 四值,构成一个状态机 |
| 动画状态 | 飞出动画用剩余时长;晃动动画用"晃动坐标 + 起始帧"两个变量(见 3.5) |
3.2 路径检测(重点)
核心函数是 board.check_block(grid, row, col, direction, rows, cols),它就是作业里"检查指定位置的
箭头是否被阻挡"这个需求:从当前格的下一格开始,沿箭头方向一路走到边界,遇到非 0 就返回 True,
走到边界都为空就返回 False。四个方向分别实现,避免互相干扰:
if direction == UP:
r = row - 1 # 从当前格的上一格开始
while r >= 0:
if grid[r][col] != EMPTY:
return True # 上面有箭头挡着
r -= 1
return False # 一路到上边界都是空的
if direction == DOWN:
r = row + 1
while r < rows:
if grid[r][col] != EMPTY:
return True
r += 1
return False
左、右两个方向同理,只是把行号换成列号、把 r >= 0 换成 c >= 0、把 r < rows 换成 c < cols。
边界处理(这是最容易出错的地方):
- 起点自身不参与判断 —— 箭头不会挡住自己,所以循环从
row ± 1、col ± 1开始。 - 起点越界直接返回
False:if not (0 <= row < rows and 0 <= col < cols): return False。 rows / cols参数被限制在真实数组范围内(rows = min(rows, len(grid))),
即使调用方传错(比如传了 99)也不会越界。- 兼容参差不齐的二维数组:列数取所有行里最短的那一行(
min(len(line) for line in grid))。 - 空棋盘、未知方向(比如传 9)都安全返回
False,不抛异常。 - 返回值统一是布尔类型,方便直接用于
if。
在它之上还有两个复用的地方:
path_is_clear(...)就是not check_block(...),关卡生成时用来判断"这个落点放回去合不合法";Board.can_fly(row, col)=该格有箭头 且 没有被阻挡,点击判定用它。
也就是说整份代码里"能不能飞出"只有一条规则、一处实现,不会出现两处逻辑不一致的问题。
3.3 点击之后发生什么
def handle_board_click(self, pos):
cell = self.layout.cell_at(pos) # 屏幕坐标 -> 格子坐标
result = self.board.click(row, col) # 返回 empty / fly / blocked
if result.kind == "fly":
self.fire_arrow(row, col, rect) # 移除箭头 + 播飞出动画 + 加分
elif result.kind == "blocked":
self.hit_blocker(row, col, rect, result.blocker) # 扣失误 + 碰撞反馈
Board.click() 只做判定、不改棋盘,改棋盘的动作留给上层,这样判定逻辑可以单独测试。
3.4 游戏流程:一个简单的状态机
┌────────── 开始界面 (menu) ──────────┐
│ 开始游戏 / 继续 / 退出 / 音效开关 │
└───────────────┬─────────────────────┘
│ 生成第 N 关
┌───────▼────────┐
点空/点错 ───│ 游戏界面(play) │─── 清空全部箭头 ──▶ 通关界面(clear) ── 2 秒后 ──▶ 下一关
└───────┬────────┘
│ 失误归零
晃动动画播完(18 帧)
▼
失败界面(over) ── 重玩本关 ──▶ 回到同一关
清关、重开、返回菜单都会重置特效、晃动动画与关卡状态;restart_level() 用出题时保存的
snapshot 还原棋盘,所以重开后布局与失误次数与初始完全一致。
3.5 动画与反馈
| 效果 | 实现方式 |
|---|---|
| 箭头飞出 | 用时间步长推进:位置按缓出曲线加速离开棋盘,同时透明度衰减到 0,0.42 秒后回收 |
| 碰撞晃动 | 基于帧计数:ShakeAnimation 记录 cell(晃动坐标)与 start_frame(起始帧),绘制时用 (当前帧 - 起始帧) 算偏移;0.3 秒 = 60FPS 下 18 帧,偏移只有横向分量,幅度随时间衰减,播完自动清理 |
| 碰撞闪光 | 被点格子闪红框、阻挡者闪橙框,0.5 秒内淡出 |
| 文字提示 | 上浮并淡出的 toast,例如"被挡住了!失误剩余 2" |
| 失误归零 | 打上 fail_pending 标记,等晃动动画播完(not shake.is_active())才切换到失败界面 |
晃动偏移在绘制函数里应用,这也是作业要求的做法:
offset_x, offset_y = shake.offset((row, col), frame) if shake else (0.0, 0.0)
center = (rect.centerx + offset_x, rect.centery + offset_y)
整个过程只改数据、不做等待,所以不会阻塞主循环(代码里没有任何 time.sleep)。
3.6 关卡是怎么来的(保证一定能通关)
如果随机撒箭头,很容易生出一张"怎么点都点不完"的图。这里的做法是逆向构造:
- 先把棋盘当成空的 —— 相当于"所有箭都已经射出去了",也就是通关后的状态;
- 再按"被射掉的顺序"倒着把箭头放回去:每放一支箭,都要求它前方到边界之间全是空的,
这正好等价于正着玩时它可以飞出; - 反复放置直到达到目标箭头数量。
于是"把放置顺序倒过来点击"就是一条合法的通关路线,题目一定可解。为了双保险,代码里还有
is_solvable() 用贪心模拟再校验一遍:只要某支箭当前路径畅通就立刻射掉它,因为移除箭头只会让
其它箭头的路更通畅(现在能飞的以后一定能飞),所以贪心不会漏掉任何可行解。
实测:40 组随机种子 × 12 关,全部可解。
3.7 界面与字体
- 不使用任何图片素材:网格、按钮、进度条、失误圆点、星级、卡片底板全部用 pygame 图元绘制。
- 箭头用字体里的
↑ ↓ ← →字符渲染(带一层淡投影);程序启动时会用
font.metrics()检查所选字体是否包含这四个字符,如果不包含就自动退回用多边形绘制箭头,
所以在缺字体的机器上也不会出现方块字。 - 中文字体按"微软雅黑 → 黑体 → 宋体 → 思源黑体 …"的顺序查找文件路径与系统字体名,
找不到时退回 pygame 默认字体。 - 箭头字符图像做了缓存(
_arrow_cache),界面上有几十支箭时不会每帧重复渲染字符。
3.8 音效
7 种音效全部由代码合成:把"频率曲线 × 起音衰减包络"逐采样算出来,用标准库 array 打包成
16bit 缓冲,再交给 pygame.mixer.Sound(buffer=...)。
for index in range(frames):
freq = max(freq_fn(index / frames), 20.0) # 频率可以随进度滑动
phase += math.tau * freq * dt
value = _wave(wave, phase) * _envelope(t) # 波形 × 包络
sample = int(max(-1.0, min(1.0, value * volume)) * 32767)
for _ in range(self.channels): # 每个声道写同样的采样
buffer.append(sample)
| 音效 | 触发时机 | 合成方式 |
|---|---|---|
| shoot | 箭头飞出 | 700→2200Hz 短上滑 + 16% 噪声 |
| block | 撞到阻挡 | 200Hz 三角波下滑 + 噪声 |
| clear | 通关 | C-E-G-C 上行琶音 |
| fail | 失败 | 392/311/233Hz 三个下行音 |
| click / hover | 按钮点击 / 划过 | 1000Hz / 1500Hz 极短音 |
| start | 进入关卡 | 659→988Hz 两音上行 |
合成时会读取音频设备的实际采样率与声道数(本机实测是 8 声道),所以音高和时长都准确;
设备不可用时自动降级为无声,游戏照常运行。按 M 或点右上角按钮可以随时开关。
四、AIGC 使用过程
开发全程使用 Codex(GPT-5,Codex 桌面端) 辅助,下面记录 4 次有代表性的协作过程。
每次都是"我提要求 → AI 产出 → 我实际运行验证 → 发现不足并修改"的循环,不是一次生成就完事。
记录 1:项目骨架与路径检测
| 项目 | 内容 |
|---|---|
| 我的要求 | 用 Python + Pygame 做一个"一箭又一箭",规则为点箭头→检查到边界的路径→畅通则飞出、有阻挡则撞停并扣失误;要求结构清晰、函数拆分、中文注释、不使用外部图片素材、用字符绘制箭头;清空箭头自动进下一关、支持重新开始、包含开始/游戏/通关/失败四个界面;关卡必须能通关 |
| AI 提供了什么 | 模块划分方案(配置 / 棋盘逻辑 / 关卡生成 / 特效 / 控件 / 绘制 / 主程序),check_block() 与 path_is_clear() 两个判定函数,用逆向构造法生成关卡的思路,四个界面的状态机骨架 |
| 实际效果 | 首次运行即可跑通;40 组随机种子 × 12 关全部可解,说明生成器可靠 |
| 我做了哪些修改 | ① 我最初写的规则测试用例是错的:把 (0,0) 放成 ←,箭头朝左本来就该直接飞出,我却断言它被挡住。核对规则后改成"→ 与 ← 互相挡住"的 3×3 棋盘,测试才正确;② 把"能否飞出"的判定统一收敛到 check_block 一处,避免关卡生成和点击判定各写一套;③ 把关卡生成里"逆向构造"的合法性条件重新推导了一遍,确认它就是正向可解性的等价条件 |
记录 2:帧计数的碰撞晃动 + 失误次数管理
| 项目 | 内容 |
|---|---|
| 我的要求 | 点击被阻挡的箭头时该箭头做左右晃动动画、持续 0.3 秒、不阻塞主循环;全局维护剩余失误次数(每关 3 次,点错扣 1 次),归零后自动切换到失败界面;用变量记录晃动坐标与动画起始帧,在绘制函数中应用偏移;不使用 time.sleep,基于帧计数实现;与现有游戏状态兼容 |
| AI 提供了什么 | 新增 animation.py:ShakeAnimation 用 cell + start_frame 两个变量描述动画,offset(frame) 返回横向偏移;ShakeManager 管理多支同时晃动的箭头;renderer.draw_board() 里用 shake.offset((row, col), frame) 应用偏移;App 增加 frame_count 帧计数器 |
| 实际效果 | 像素级测量:晃动帧的箭头重心比静止时横向移动 7.0px、纵向 0.0px,18 帧后动画自动清理;失误 3→2→1→0 后自动进入失败界面 |
| 我做了哪些修改 | ① 原来的实现是按累积时间做的双向抖动,我把晃动改成纯横向并严格按"帧数"控制时长(SHAKE_FRAMES = int(FPS × 0.3) = 18);② 失败切换时机原本是"固定等 0.65 秒",改成"等晃动动画播完",让切界面时机和动画严格对齐;③ 我的自检脚本一开始用字符串包含判断是否用了 time.sleep,结果被注释里的"不使用 time.sleep"这几个字误报,改成用正则只匹配真实的 import time / time.sleep( 调用 |
记录 3:音效 + 开始界面美化
| 项目 | 内容 |
|---|---|
| 我的要求 | 加点音效,并重新设计开始界面让游戏更美观;仍然不能引入外部素材(不能用音频文件、图片) |
| AI 提供了什么 | audio.py:用标准库 array 合成 7 种波形(射箭、撞停、通关、失败、点击、悬停、进关);menu_scene.py:背景 16 支飘动箭头 + 右侧"实时演示"小棋盘(每 0.6 秒自动射一箭再补位);renderer.draw_menu():发光标题、两张卡片、按钮横排、右上角音效胶囊 |
| 实际效果 | 真实窗口下开始界面 59 FPS、游戏界面 60 FPS;7 种音效全部合成成功并可播放 |
| 我做了哪些修改 | ① 最典型的一个 Bug:AI 最初硬性要求混音器必须是"44100Hz / 16bit / 双声道",但真实窗口里音效一直没就绪;查 pygame.mixer.get_init() 发现本机音频设备是 8 声道。改成读取设备的实际采样率与声道数来合成后,音效正常;② 菜单按钮从竖排改成横排后,主循环冒烟测试里硬编码的点击坐标打空了(点在了空白处),改成按按钮矩形的中心点击;③ 给箭头图像加了缓存,避免几十支箭每帧重复渲染字符 |
记录 4:自动化自检与测试用例
| 项目 | 内容 |
|---|---|
| 我的要求 | 帮我写能自动跑的测试:覆盖四个方向的阻挡判定与越界、关卡一定能通关、失误与失败流程、排版不压字、动画真的画出来了;并记录测试结果方便写报告 |
| AI 提供了什么 | 三套脚本:check_logic.py(规则 / 可解性 / 流程 / 边界 / 帧动画)、check_layout.py(界面元素矩形不越界不压字、字形可用性)、check_render.py(像素级验证箭头、辅助线、碰撞框、晃动位移、开始界面、音效);以及 tests/run_all.py 一键运行 |
| 实际效果 | 共 280 项断言全部通过(81 + 154 + 45),且每次改代码后都能在几秒内回归 |
| 我做了哪些修改 | ① 修正了上面提到的 time.sleep 误报;② 修正了菜单改版后失效的点击坐标;③ 把 check_block 与 Board 的判定做交叉对比(200 张随机棋盘上逐格比对,不一致 0 处),确认没有两套规则;④ 把测试脚本从临时目录整理进仓库的 tests/ 目录,并统一路径与无窗口模式(SDL_VIDEODRIVER=dummy),保证在仓库里能直接运行 |
汇总表
| 子任务 | 借助何种 AIGC 技术 | AI 实现或提供了什么 | 效果如何 | 人工修改 |
|---|---|---|---|---|
| 项目结构 | Codex(GPT-5) | 模块划分、状态机骨架、中文注释 | 结构清晰,一次跑通 | 调整模块职责,把判定统一到一处 |
| 路径检测 | Codex | 四个方向分别实现的 check_block 与边界处理 |
200 张随机棋盘与游戏判定一致 | 修正自己写错的测试用例;补充 rows/cols 收敛与参差数组兼容 |
| 关卡设计 | Codex | 逆向构造法 + 贪心可解性校验 | 40 组种子 × 12 关全部可解 | 重新推导可解性条件,确认生成顺序与通关顺序互为逆序 |
| 碰撞动画 | Codex | 帧计数驱动的 ShakeAnimation(坐标 + 起始帧) |
横向位移 7.0px、纵向 0px,18 帧自动结束 | 从"时间累积的双向抖动"改为"帧计数 + 纯横向";失败切界面改为等动画播完 |
| 失误与流程 | Codex | 失误扣减、fail_pending、失败/通关界面切换 |
3→2→1→0 后自动进失败界面 | 把固定的 0.65 秒延时改成与动画对齐 |
| 音效 | Codex | 7 种波形合成(无音频文件) | 真实设备可播放 | 修掉"只认双声道导致 8 声道设备没声音"的 Bug |
| 开始界面 | Codex | 发光标题、飘动箭头、实时演示棋盘、卡片排版 | 59–60 FPS,排版无重叠 | 调整卡片尺寸与文案宽度;给箭头图像加缓存 |
| 测试用例 | Codex | 三套自检脚本、280 项断言 | 全绿,可回归 | 修两处误报;统一到 tests/ 并改路径 |
五、测试结果
5.1 作业要求的测试项
| 编号 | 测试内容 | 预期结果 | 实际结果 | 是否通过 |
|---|---|---|---|---|
| T01 | 点击前方无阻挡的箭头 | 箭头飞出棋盘并消失 | 箭头立即从棋盘移除,播放飞出淡出动画并 +10 分;模拟点击连续通关 3 关(共 30 次点击) | ✅ |
| T02 | 点击前方有阻挡的箭头 | 箭头不消失,失误次数减 1 | 箭头保留在原地晃动 0.3 秒并闪红框,失误 3→2→1,飘出"被挡住了!失误剩余 N" | ✅ |
| T03 | 点击位于边缘且朝向棋盘外的箭头 | 箭头正常消失,不发生越界错误 | 边界与非法入参共 9 项(行/列为负、超出范围、空棋盘、rows/cols 传成 99、参差不齐数组)全部安全返回,无 IndexError |
✅ |
| T04 | 消除本关全部箭头 | 显示通关并进入下一关 | 显示通关面板与星级,2 秒倒计时后自动进入下一关(空格可立即进入),最高关卡记录更新 | ✅ |
| T05 | 失误次数耗尽 | 显示失败并允许重新开始 | 第 3 次点错后标记待失败,晃动动画播完(18 帧)自动切到失败界面;点"重玩本关"可重新开始 | ✅ |
| T06 | 游戏进行中重新开始 | 箭头布局和失误次数恢复 | 棋盘快照逐格比对与初始完全一致,失误恢复为 3,得分清零,晃动动画与失败标记被清空 | ✅ |
5.2 补充测试
| 项目 | 预期 | 实际 | 是否通过 |
|---|---|---|---|
| 关卡可解性 | 生成的关卡都能通关 | 40 组随机种子 × 12 关全部可解;is_solvable() 贪心校验通过 |
✅ |
| 四方向判定 | 四个方向都正确 | 上/下/左/右的阻挡与畅通用例各覆盖,含"下一格就是边界"的极端情况 | ✅ |
| 判定一致性 | 规则只有一处实现 | 200 张随机棋盘上 check_block 与 can_fly / blocker_of 不一致 0 处 |
✅ |
| 悬停辅助线 | 畅通绿色、被挡红色并标出阻挡者 | 采样点像素验证:畅通路径偏绿、被挡路径偏红、阻挡者橙色像素 640 | ✅ |
| 晃动动画 | 横向位移、纵向不动、18 帧结束 | 箭头重心横向移动 7.0px、纵向 0.0px,18 帧后自动清理 | ✅ |
| 排版与字形 | 元素不压字、中文不缺字 | 四个界面在 5×5~9×9 棋盘下元素零重叠零越界;548 个中文字/符号全部有字形 | ✅ |
| 音效 | 可播放、可静音、无素材文件 | 波形长度 31752 字节 = 0.18 秒 @44100Hz × 2 声道;音量按配置缩放;按 M 后 play() 静默跳过;game/ 目录无任何音频/图片文件 |
✅ |
| 真实设备运行 | 真实窗口 + 真实音频设备下稳定 | 显示驱动 windows、8 声道设备下音效就绪;开始界面 59 FPS、游戏界面 60 FPS |
✅ |
5.3 自动化测试
在项目根目录执行 python tests/run_all.py,三套脚本共 280 项断言:
| 脚本 | 断言数 | 结果 |
|---|---|---|
tests/check_logic.py |
81 | 全部通过 |
tests/check_layout.py |
154 | 全部通过 |
tests/check_render.py |
45 | 全部通过 |
5.4 测试过程中发现并修复的问题
| 问题 | 现象 | 原因 | 修复 |
|---|---|---|---|
| 规则测试用例写错 | 断言"朝左的箭头被挡住"失败 | 测试棋盘把箭头放在最左列还朝左,它本来就该直接飞出 | 改成 → 与 ← 互相挡住的 3×3 棋盘 |
| 真实窗口下没有音效 | 开始界面显示"音效:不可用" | 代码硬性要求混音器为双声道,而本机设备是 8 声道 | 改为按 pygame.mixer.get_init() 返回的实际采样率与声道数合成 |
| 冒烟测试失效 | 主循环测试报告"未推进关卡" | 开始界面按钮改成横排后,测试里硬编码的点击坐标点到了空白处 | 改为按按钮矩形中心点击 |
| 自检误报 | 报告"代码里用了 time.sleep" | 检查用字符串包含匹配,把注释里的"不使用 time.sleep"当成了调用 | 改成正则匹配真实的 import time / time.sleep( |
六、PSP 表格
下表是参考数据,请按你自己的实际耗时修改(尤其是 AI 使用与测试部分)。
| 任务 | 预估耗时(小时) | 实际耗时(小时) | 差异(小时) |
|---|---|---|---|
| 需求分析与游戏设计 | 1.0 | 1.5 | +0.5 |
| Python 与图形库学习 | 2.0 | 2.0 | 0 |
| 游戏界面实现 | 3.0 | 4.0 | +1.0 |
| 路径与碰撞逻辑实现 | 2.5 | 3.5 | +1.0 |
| 关卡设计 | 1.5 | 2.0 | +0.5 |
| AIGC 辅助开发 | 2.0 | 3.0 | +1.0 |
| 测试与修改 | 2.0 | 3.5 | +1.5 |
| README 与博客撰写 | 2.0 | 2.5 | +0.5 |
| 合计 | 16.0 | 22.0 | +6.0 |
耗时比预估多的地方主要是:调碰撞动画的手感、找出"8 声道设备没声音"这种环境相关的 Bug,
以及为了让自检脚本可靠而反复修正测试本身。
七、心得体会
AI 能显著加速,但不能代替判断。 这次开发里代码量最大的部分(模块骨架、路径检测、动画、
音效合成、自检脚本)都是在 AI 协助下完成的,省掉了大量查 API 的时间。但真正让我印象最深的是
三个"AI 也会错"的时刻:最初硬性要求双声道导致 8 声道设备上完全没有音效;注释里的文字让自检
脚本误报"使用了 time.sleep";界面改版后测试里硬编码的坐标失效。这些问题都不是靠"再让 AI 写
一遍"解决的,而是要靠实际运行、看输出、再定位原因 —— 必须自己理解代码,才能发现 AI 的假设
和真实环境之间的差距。
测试是理解规则的最快方式。 我一开始凭直觉写了个规则测试用例,结果它自己是错的(把朝左的
箭头放在最左列还断言它被挡住)。为了写对这条断言,我不得不把"边界"和"阻挡"的判定从头推导
一遍,反而把规则彻底弄清楚了。后面我还让 AI 用 200 张随机棋盘交叉比对两套判定函数,确认
它们完全一致 —— 这种"用程序验证程序"的思路,比手工点几十次可靠得多。
关卡"一定能通关"需要设计,不是靠运气。 一开始我以为随机撒箭头、多试几次总能出题,但很快
发现密集的棋盘很容易变成死局。改用"逆向构造 + 贪心校验"之后,题目的可解性从概率问题变成了
确定性问题,这也让我第一次意识到:一个看起来只是"随机生成"的功能,背后往往需要一个数学上
说得通的构造方法。顺便也理解了为什么贪心判可解是完备的 —— 因为移除箭头只会让路更通畅。
界面手感来自细节。 悬停时画出飞行路径(绿色畅通 / 红色被挡 + 阻挡者描橙框)看起来只是
锦上添花,但它把"要不要点这一箭"从猜测变成了判断,配合 0.3 秒的左右晃动和音效反馈,整个操作
才顺手。为了让动画不阻塞主循环,晃动改成用帧号推进;为了不引入素材文件,音效直接合成波形 ——
这些限制反而逼着我搞清楚了"动画的时间基准"和"声音就是采样"这两件底层的事。

浙公网安备 33010602011771号