2026秋软件工程个人作业(第二次)
2026秋软件工程个人作业(第二次)
运行环境:Python 3.13 + pygame 2.6.1(只依赖 pygame 一个第三方库)
pip install pygame
python main.py # 菜单:开始游戏(固定 5 关)/ 更多关卡(关卡选项)
python main.py --level 3 # 直接从第 3 关开始
python main.py --random # 随机布局:每次开局现场生成 5 关
python main.py --random --seed 20260919 # 指定种子,复现同一批关卡
| 这个作业属于哪个课程 | H202601软件工程与软件工程实践 |
|---|---|
| 这个作业要求在哪里 | 2026秋软件工程个人作业(第2次) |
| 这个作业的目标 | 使用 Python 和 AIGC 完成“一箭又一箭”小游戏 |
| 学号 | 102401114 |
| GitHub仓库 | 仓库链接 |
1.项目展示
游戏开始界面
游戏过程、通关
失败界面和更多关卡
2.项目介绍
游戏规则
棋盘上有若干带方向的单格箭头(上 / 下 / 左 / 右)。
- 点击一个箭头:如果它前进方向到棋盘边界之间没有其他箭头,它就沿该方向飞出棋盘并消失;
- 如果路径上有其他箭头挡路:箭头不能消除,会晃动变红、挡路者闪红、弹出提示,并扣 1 次失误;
- 清空本关全部箭头 → 通关;失误次数耗尽 → 本关失败,可重新挑战。
例如下面这行箭头,第一支朝右但右侧还有别的箭头,点不动;第三支朝右且右侧到边界为空,可以飞出:
→ · · ↑ · 第一支朝右,右侧有箭头 → 被挡
↑ · · · → 最后一支朝右,右侧到边界无箭头 → 飞出
界面设计
- 四个界面:开始界面、关卡选项、对局界面、结算界面(通关弹窗 / 失败弹窗 / 全部通关);
- 布局统一:卡片(圆角 + 浅阴影 + 细边框)、按钮(400×62 / 400×52 / 200×58 三档尺寸)、
字号(64 / 44 / 28 / 24 / 20 / 17 六档)全部复用同一套常量,四个界面视觉一致; - 配色:浅色底 + 蓝色箭头 + 红色失误,底纹用 AI 生成的浅色面板图(低透明度铺底);
- 反馈:反馈分三层——动画(晃动 / 变色 / 飞出 / 涟漪 / 星星回弹)、文字(漂浮提示)、
声音(掌声 / 失败 / 烟花),保证任何一次点击都有明确回应。
主要功能清单
- 鼠标点击选择箭头,上下左右四方向;
- 路径检测(同行 / 同列到边界),点对飞出、点错扣失误;
- 飞出动画、碰撞晃动变红、挡路箭头闪红、漂浮文字提示;
- 失误次数显示(爱心图片,满 / 空两态);
- 固定 5 关,难度递进(4×4/5 支 → 6×6/15 支,失误上限 3~4);
- 随机布局模式:每局现场生成 5 关,逐关用求解器验证「必定可通关」,同一种子可复现;
- 关卡选项界面:任选一关直接挑战,卡片显示历史最佳星级与最高分;
- 计分与星级:按用时与失误评 1~3 星,最高分存本地
records.json; - 提示功能:一键高亮一个当前能飞的箭头;
- 重新开始:恢复初始状态、失误复原、计时归零,且不改变本关布局;
- 音效:正确选择鼓掌、错误选择失败音、通关烟花;
M键静音; - 鼠标悬停预览前进光路(绿=通,红=被挡);
- 键盘快捷键:
R重新开始、Esc返回菜单、M静音。
特色
- 关卡不是手编的,是求解器验证出来的:固定 5 关由「随机生成 + DFS 求解器验证」产出,
随机关卡在开局时现场生成并即时验证,不存在死局; - 发现并利用了游戏的一条数学性质(见 3.4):箭头减少只会解除阻挡 ⇒ 贪心必通、
没有顺序陷阱。这条性质同时保证了随机关卡的安全性和菜单自动演示的合理性; - AIGC 全链路:图形素材由 AI 生图 + 自写脚本做白底抠图、裁水印;音效由 Python 标准库
现场合成(不下载第三方音频,无版权风险); - 可验证:100 项断言的自动试玩测试 + 帧率实测脚本,所有结论都有数据支撑。
3.实现思路:
3.1 箭头与方向怎么表示
方向用四个字符常量表示,配套一张增量表 DELTA,值是 (行增量, 列增量):
UP, DOWN, LEFT, RIGHT = "U", "D", "L", "R"
DELTA = {
UP: (-1, 0),
DOWN: ( 1, 0),
LEFT: ( 0, -1),
RIGHT: ( 0, 1),
}
# 关卡用 ASCII 书写,人读人改都方便
SYMBOL_2_DIR = {"^": UP, "v": DOWN, "<": LEFT, ">": RIGHT}
箭头对象只存三个信息 + 一个编号:
class Arrow:
__slots__ = ("r", "c", "d", "uid") # uid 供动画跟踪同一个箭头
关卡用 ASCII 网格书写,比坐标数组直观得多,也方便人工检查:
Level(
name="第3关 · 十字路口",
grid=[
"^>.>.",
".^v.>",
">>...",
"..v..",
".....",
],
mistakes=3,
)
难度曲线(棋盘尺寸 / 箭头数 / 失误上限 / 开局至少被挡数)集中在一张常量表里,
固定关卡与随机关卡共用同一套配置:
LEVEL_CONFIGS = [
(4, 4, 5, 3, 1, "初次上手"),
(4, 4, 7, 3, 2, "两两相望"),
(5, 5, 9, 3, 3, "十字路口"),
(5, 5, 12, 4, 4, "箭阵"),
(6, 6, 15, 4, 5, "一箭又一箭"),
]
3.2 一个必须小心的坑:行列坐标 vs 屏幕坐标
DELTA 是「(行增量, 列增量)」,而行号增大是向下、列号增大是向右。
画箭头、算速度时必须换成屏幕的 (x, y):
dr, dc = DELTA[direction]
ux, uy = dc, dr # 列增量 -> 屏幕 x,行增量 -> 屏幕 y
我第一版就是在这里栽了跟头:箭头整体画反了(上变成左、右变成下)。
逻辑是对的,只有渲染错了,所以单元测试全绿、肉眼一看全错。后面用一张
「四方向对照图」(tools/check_sprites.py)逐个方向肉眼确认,才彻底修掉。
教训很直白:能跑的测试不代表看得对,涉及坐标系的改动一定要有可视化验证。
3.3 路径检测(重点)
判定规则只有一句:同一行或同一列、箭头与边界之间是否还有其他箭头。
实现就是从箭头的下一格出发,沿方向一格一格走到棋盘外,路上遇到谁就返回谁:
def scan(self, arrow):
"""返回 (是否通畅, 挡路的箭头 or None)"""
dr, dc = DELTA[arrow.d]
r, c = arrow.r + dr, arrow.c + dc
while 0 <= r < self.rows and 0 <= c < self.cols: # 走出边界 = 通畅
other = self.arrow_at(r, c)
if other is not None:
return False, other # 撞到第一个箭头
r += dr
c += dc
return True, None
几个实现细节:
- 为什么不用预计算:棋盘最大 6×6,最坏 5 步就能走完,
O(行+列)的线性扫描完全够用;
点击是低频事件,没必要为了理论性能引入额外数据结构。 - "谁是挡路的"顺便返回:碰撞反馈要把挡路的那支箭头也闪红,所以扫描时返回第一个
挡路的箭头,而不是只返回一个布尔值——这点让界面层的反馈实现变得很简单。 - 一处实现,两处使用:点击判定用
scan();鼠标悬停时的光路预览(绿线/红线、
以及被挡格子的红框)也调用同一个scan(),规则只有一份,不存在"预览和实际判定不一致"。 - 失误判定放在核心层:
Board.click()返回结构化的结果
({'ok': True, 'result': 'fly'}或{'ok': False, 'result': 'blocked', 'blocker': …}),
失误次数在核心层扣减。界面层只负责"播动画 / 播音效",规则与表现彻底分开。
3.4 关卡怎么生成:DFS 求解器 + 一条有趣的数学性质
求解器:状态就是「剩余箭头的集合」,用 frozenset 表示,DFS + 记忆化搜一条清空顺序:
def solve_state(state, rows, cols, memo=None):
"""state: {(r, c, 方向), ...};返回一条清空顺序,无解返回 None"""
...
def dfs(st):
if not st:
return []
if st in memo:
return memo[st]
for a in _removable_in(st, rows, cols): # 当前能飞的箭头
sub = dfs(st - {a})
if sub is not None:
memo[st] = [a] + sub
return memo[st]
memo[st] = None # 死局,记住它,避免重复搜索
return None
return dfs(state)
随机关卡:随机撒箭头 → 立刻用求解器验证 → 只接受「可解、且开局至少有若干支箭头被挡」
的布局,否则重撒。整局 5 关平均生成耗时 8.8ms(实测区间 1~25ms),进游戏前没有任何可感知的等待。
顺手发现的数学性质:我让求解器统计每个关卡「点错一步就会死局的首步」有多少个,
在 20 局共 100 个关卡上统计,结果 不可解 0 个、陷阱首步 0 个。想了一下原因,其实是个单调性结论:
阻挡只取决于「剩余箭头集合」:多一支箭只可能多一个障碍,不会少。
所以移除箭头后,任何原本能飞的箭头仍然能飞(可飞集合单调不减)。
于是若关卡可解(存在某个清空顺序),把其中任意一个"当前可飞"的箭头提前到最前,
剩余部分依然可解 —— 贪心必通,不存在顺序陷阱。
由此得到三个直接结论,全都用在了程序里:
- 玩家不会因为"点错顺序"而死局,失败只可能来自点了被挡的箭头浪费失误次数 →
关卡难度必须靠密度 / 阻挡数来做(LEVEL_CONFIGS的第 5 个字段就是这个门槛); - 随机关卡只要被求解器判定"可解",玩家必定能通关 → 随机生成是安全的;
- 菜单里的自动演示可以随便挑一个可飞的箭头飞出,永远不会演示到死局。
3.5 界面架构与流畅度
- 逻辑与界面分离:
game_core.py是纯逻辑(棋盘、路径、失误、求解器、计分,不 import pygame),
main.py只管画和收鼠标事件。好处是自动测试可以不开窗口、直接调Game.on_click()
走真实点击路径跑完 5 关; - 动画用 dt 驱动:所有动画(飞出、晃动、闪烁、涟漪、星星回弹)都按帧间隔推进,
不依赖具体帧率; - 图片按需缓存:箭头按
(方向, 像素尺寸)缓存"旋转 + 缩放"的结果,碰撞用的红色版本
预生成,每帧只做 blit; - 界面静态层预渲染:菜单和关卡选项里不动的部分(底纹、标题、卡片、文字)渲染成一张
离屏图缓存起来,每帧只 blit 一次 —— 菜单帧耗时从 7.28ms 降到 0.21ms; - 实测数据(
tools/perf_check.py,dummy 驱动纯软件渲染,60 FPS 的预算是 16.7ms/帧):
| 场景 | 平均单帧 | 余量 |
|---|---|---|
| 菜单(自动演示在动) | 0.21 ms | 约 80 倍 |
| 关卡选项(5 张卡片) | 0.15 ms | 约 110 倍 |
| 对局(6×6 / 15 支箭头) | 2.60 ms | 约 6 倍 |
| 连续飞出动画 | 3.24 ms | 约 5 倍 |
4.AIGC 使用过程:
过程一:提出游戏需实现的基本功能要去,让ai生成代码

使用deepseek ai给了我一个游戏的基本demo 基本满足作业所需要去 ui界面有点问题,改了字体大小
过程二:让ai添加更多关卡的功能,提高游戏可玩性

使用deepseek ai新增了难度曲线和即使验证功能 基本可用
过程三:让ai生成箭头图形和心图形

使用deepseek ai生成了具体图像 动画流程
过程四:性能与音效——把"感觉"换成数字
- 我的需求:「换成图片素材后动画要保证流畅」+「点对鼓掌、点错失败音、通关烟花音效」。
- AI 的产出:给出两个方向——①素材缓存 + 界面静态层预渲染;
②用 Python 标准库(wave/array/math)现场合成音效,避免下载第三方音频的版权问题。 - 我的验证与修改:
- 写了
tools/perf_check.py实测三段最费时间的场景,发现菜单逐帧重绘大量中文是最贵的
(7.28ms/帧),改成静态层预渲染后降到 0.21ms;对局 2.60ms、连续飞出 3.24ms,
都在 16.7ms 预算内(余量 5 倍以上),"流畅"就从感觉变成了数字; - 音效做完后我听不出问题,就改用可量化的检查:统计过零率、强瞬态个数、RMS,
发现错误音效的过零率没有随之下滑(说明"往下掉"的听感没做出来),
定位到是最后加的高通滤波把下行基频滤掉了,改为一阶低通后过零率正常下降;
掌声则统计出 13 个明显的强瞬态(对应一下下"巴掌")。
- 写了
- 结果:4 个音效(鼓掌 / 失败 / 通关琶音 / 烟花)全部接入,
M键静音,没有声卡或素材缺失时自动静默降级;
自动测试用sound.last_played断言"点对=鼓掌、点错=失败、通关=烟花",不用听声音也能回归。
5.测试结果:
测试方法
- 核心逻辑单测 + 全流程自动试玩:
tools/autoplay_test.py,不弹窗口(SDL 用 dummy 驱动),
通过Game.on_click()走真实点击路径(和玩家点击完全同一条代码路径),
求解顺序由求解器给出,每关都真的"打完"; - 关卡可解性复核:
tools/verify_levels.py(逐关打印一条通关顺序); - 性能实测:
tools/perf_check.py。
运行命令:
python tools/autoplay_test.py # 106 项断言
python tools/verify_levels.py # 5 关可解性复核
python tools/perf_check.py # 帧率实测
测试结果表(摘自真实运行输出)
| # | 测试项目 | 预期结果 | 实际结果 | 结论 |
|---|---|---|---|---|
| 1 | 零失误且快通关的星级与得分 | 3 星 / 1000 分 | 3 星 / 1000 分 | 通过 |
| 2 | 失误 1 次且略超时 | 2 星 | 2 星 | 通过 |
| 3 | 失误 2 次且严重超时 | 1 星 | 1 星 | 通过 |
| 4 | 分数随失误 / 超时递减 | 单调递减 | 1000 > 751 > 350 | 通过 |
| 5 | 极端情况分数保底 | ≥100 分 | 100 分 | 通过 |
| 6 | 目标用时是 3 星 / 2 星的分界 | 正好 par 算 3 星 | 符合 | 通过 |
| 7 | 音效加载 | 4 个音效全部加载 | 4 个 | 通过 |
| 8 | 点错箭头的音效接线 | 播放 pick_bad | pick_bad | 通过 |
| 9 | 点对箭头的音效接线 | 播放 pick_ok(鼓掌) | pick_ok | 通过 |
| 10 | 静音开关 | 静音后不出声 | 静音后 play 返回 False | 通过 |
| 11 | 本关通关音效 | 播放 fireworks(烟花) | fireworks | 通过 |
| 12 | 菜单「更多关卡」 | 进入关卡选项界面 | 进入 | 通过 |
| 13 | 关卡选项点第 4 关卡片 | 直接进入第 4 关(箭阵) | 第 4 关 | 通过 |
| 14 | 关卡选项「随机挑战」 | 现场生成 5 关 | 5 关(种子每次不同,本次 446330394) | 通过 |
| 15 | 菜单「开始游戏」 | 进入固定 5 关的第 1 关 | 第 1 关 | 通过 |
| 16 | 点被挡箭头 | 失误数 -1,箭头仍在,播失败音 | 失误 3→2,箭头未消失 | 通过 |
| 17 | 碰撞动画 | 箭头处于晃动状态 | 晃动动画中 | 通过 |
| 18 | 点被挡箭头后箭头总数 | 不减少 | 未减少 | 通过 |
| 19 | 重新开始按钮 | 失误复原、计时归零 | 失误复原、计时 0.0 | 通过 |
| 20 | 重新开始是否换布局 | 保持同一布局 | 布局不变 | 通过 |
| 21 | 飞出动画方向 | 与箭头朝向一致 | 一致(D) | 通过 |
| 22 | 清空全部箭头 | 进入通关结算界面 | 进入结算 | 通过 |
| 23 | 零失误通关成绩 | 3 星 / 1000 分 | 3 星 / 1000 分(用时 2.4s / 目标 15s) | 通过 |
| 24 | 本局总分累计 | 正确累计 | 1000 | 通过 |
| 25 | 最高分写入存档 | 记录 score/stars/best_time | 写入成功 | 通过 |
| 26 | 更差成绩的覆盖行为 | 不覆盖最高分 | 仍为 1000 | 通过 |
| 27 | 失误耗尽 | 进入失败界面 | 进入失败界面 | 通过 |
| 28 | 失败是否计分 | 不计分、不写记录 | result 为空、无记录 | 通过 |
| 29 | 失败界面「关卡选项」 | 返回关卡选项界面 | 返回 | 通过 |
| 30 | 随机关卡可复现性 | 同种子布局完全相同 | 完全相同 | 通过 |
| 31 | 随机关卡随机性 | 不同种子布局不同 | 不同 | 通过 |
| 32 | 随机关卡可解性 | 全部可解 | 100% 可解 | 通过 |
| 33 | 随机关卡每关开局有阻挡 | 至少 1 支被挡 | 满足(均值 1.6~7.6) | 通过 |
| 34 | 随机模式 6 局 × 5 关自动通关 | 30 关全部清空 | 全部清空 | 通过 |
| 35 | 随机模式不写固定关卡存档 | 记录为空 | 为空 | 通过 |
| 36 | 经典 5 关连打 | 5 关全部清空并进入全部通关 | 通过 | 通过 |
| 37 | 5 关成绩全部入档 | 5 条记录 | 5 条 | 通过 |
| 38 | 随机模式不误报"刷新最高分" | 不显示(随机布局不入档) | new_record=False | 通过 |
汇总:106 项断言,0 项失败,脚本退出码 0。
其它验证
| 测试项目 | 预期结果 | 实际结果 | 结论 |
|---|---|---|---|
5 关可解性(verify_levels.py) |
全部存在通关顺序 | 第 1~5 关全部可解 | 通过 |
| 帧率(菜单 / 关卡选项 / 对局 / 飞出动画) | 单帧远低于 16.7ms | 0.21 / 0.15 / 2.60 / 3.24 ms | 通过 |
| 素材缺失容错 | 能退回程序绘制不崩溃 | 程序绘制兜底生效 | 通过 |
| 无声卡环境 | 静默降级不影响游戏 | 正常运行 | 通过 |
6.PSP 表格:
| 任务 | 预估耗时(分钟) | 实际耗时(分钟) | 差异(分钟) |
|---|---|---|---|
| 需求分析与玩法设计 | 5 | 5 | 0 |
| 箭头 / 方向 / 关卡表示 | 15 | 20 | +5 |
| 路径检测与失误判定 | 15 | 15 | 0 |
| 图形界面与动画 | 5 | 10 | +5 |
| 关卡选项 + 计分星级 + 存档 | 20 | 15 | -5 |
| AIGC 素材与音效 | 15 | 15 | 0 |
| 测试与性能调优 | 10 | 10 | 0 |
| 文档与博客 | 30 | 50 | +20 |
| 合计 | 115 | 140 | +25 |
7.心得体会:
AI帮了我什么
- 把重复劳动压缩掉:四界面 UI、按钮 / 卡片组件、动画框架、四五十行的样例数据,
这些"知道要做什么但很费手"的部分,AI 基本一次就能给到可用的程度; - 它提的方案往往比我最初想的多一层:比如我本来只想"随机生成关卡",
它建议"生成后立刻用求解器验证",这才让随机关卡敢直接上线;也是顺着它的建议做统计,
我才发现"陷阱步恒为 0"这条性质,反过来指导了难度设计;
学到了什么
- 使用ai开发的具体流程: 确实之前没什么开发经验,拿到作业的第一时间我想的是,直接复制
具体的任务要求给ai,直接让他生成代码就好了,但这样做出来的游戏有很多缺陷(可能就是我个人
思考问题不够全面吧),然后去b站和博客上学习如何使用ai开发,学会先让ai从小的做起,先完成MVP
,可以在完成基础内容后生成个demo试看一下,加功能的时候不要连着做多个不同的功能,一方面一方面
地完成,提高效率。 - markdown的撰写:也是花了好多的时间在写博客上了说是,确实还用不太熟,对效果不大满意,
使用ai生成了部分markdown代码,学到了挺多的hhh:D。




浙公网安备 33010602011771号