软工第二次作业
| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/fzu/202601SofwareEngineering |
|---|---|
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/fzu/202601SofwareEngineering/homework/16717 |
| 这个作业的目标 | 使用 Python 和 AIGC 完成“一箭又一箭”小游戏 |
| 学号 | 102401531 |
| GitHub 仓库 | https://github.com/matougui-x/arrow-after-arrow |
第一步:让AIGC参与子任务分解
在正式开发前,先将作业要求(一箭又一箭游戏)发送给 AIGC 工具,让其辅助分解开发阶段、梳理功能模块(如界面、路径逻辑、关卡、测试等),制定开发顺序,并生成提示词。

第二步:游戏开发
阶段一:项目初始化与开始界面搭建
核心玩法
点击棋盘上的一个箭头:
1. 看它前进方向(同行或同列)上有没有别的箭头阻挡;
2. 没有阻挡 -> 箭头飞出棋盘并消失;
3. 有阻挡 -> 产生碰撞反馈,且玩家失误次数 -1;
4. 把棋盘上所有箭头都清掉 = 通关;失误次数用完 = 失败。
棋盘的数据结构:离散化的"邻接表"
棋盘不需要每次点击都去逐格扫描,而是把"同行/同列最近的邻居"直接存下来:
每个 Arrow 有 neighbors = {上: 谁, 右: 谁, 下: 谁, 左: 谁}
于是:一个箭头能飞,当且仅当它前进方向上的邻居是 None —— O(1),无需扫描。
一个箭头飞走后就地"拆链":把它上下两个邻居对接、左右两个邻居对接
(也就是更新它各个方向邻居的反向指针),其他箭头的邻接信息自动保持正确。
代码实现class Arrow:
def init(self, row, col, direction):
self.row, self.col = row, col
self.direction = direction
self.neighbors = {DIR_UP: None, DIR_RIGHT: None, DIR_DOWN: None, DIR_LEFT: None}
@property
def can_fly(self):
return self.neighbors[self.direction] is None
游戏画面

阶段二:关卡系统与界面不断完善
增加了三个固定关卡,屏幕顶部或侧边显示:当前关卡数、剩余箭头数量、剩余失误次数,增加碰撞反馈,箭头对应方向有邻居是震动,让箭头更像箭头而非三角形,箭头可以多格
提示词

游戏画面

进一步优化:增加完成三关后的无尽模式以及最佳记录,挑战极限的速度模式,让碰撞反馈更真实,箭头向前飞直到撞到他的邻居之后弹回,一个箭头不仅可以有长度,它内部还可以转弯(因此判断逻辑要改,一根箭判定点是箭头那一格,而且要沿邻居链跳过自己的箭身(箭身可能落在自己箭头的射线上,那不是阻挡)。)
原理及其实现
随机关卡怎么保证一定可解(无尽 / 速度模式)
设清除顺序是 a1, a2, ..., an(a1 先飞走)。ai 能飞的条件是「那一刻它箭头前方没有活着的箭头」。 于是倒着摆:先摆 an,再摆 a(n-1) …… 最后摆 a1。摆 ai 时棋盘上正好只有 a(i+1)...an, 所以只要"此刻箭头前方这条射线是空的"就行。
无尽模式难度曲线 :6x6 → 7x7 → 8x8,箭最长 4 → 6 格,越来越密。
提示词


游戏画面


阶段三:拓展功能
增加了创意工坊玩家,理想状态可以上传自己设计的功能和地图,目前只实现了上传地图,增加了提示和撤销功能,属于创意工坊自带的功能,可以打开或关闭.
提示词

游戏画面


进一步拓展,加入基础的两种飞出和碰撞音效(玩家可选)并加入创意工坊
游戏画面

更进一步,创意工坊分为功能与地图两部分,玩家可自己通关后可上传自己创作的地图,原计划做可视化创意工坊地图创作,发现工作量接近当前的整个游戏,遂放弃
提示词

游戏画面

阶段四:打包成可执行文件ArrowAfterArrow.exe
第三步:进行测试
可通过run_tests.py复现或打开游戏手动复现
| 编号 | 测试内容 | 预期结果 | 实际结果 | 是否通过 |
|---|---|---|---|---|
| T01 | 点击前方无阻挡的箭头 | 箭头飞出棋盘并消失 | 返回 'cleared';它占的格子已清空;剩余箭头 6 -> 5;动画期间 flying=True,结束后对象回收 |
PASS |
| T02 | 点击前方有阻挡的箭头 | 箭头不消失,失误次数减 1 | 返回 'blocked';箭头仍在原地;失误 0 -> 1;剩余箭头保持 6;产生了弹回动画 |
PASS |
| T03 | 点击位于边缘且朝向棋盘外的箭头 | 箭头正常消失,不发生越界错误 | 遍历 3 个关卡共 9 支边缘箭,全部正常飞出,无任何越界异常 | PASS |
| T04 | 消除本关全部箭头 | 显示通关并进入下一关 | 清空后状态 state_win;点击「进入下一关」后加载第 2 关(8 支箭);共用了 6 次点击 |
PASS |
| T05 | 失误次数耗尽 | 显示失败并允许重新开始 | 状态 state_fail;点「重新开始」后状态 state_playing,失误 0/3,箭头数恢复为 6 |
PASS |
| T06 | 游戏进行中重新开始 | 箭头布局和失误次数恢复 | 重开后剩余箭头、失误、点击数、邻居图格子数与开局签名完全一致 | PASS |
| T07 | 音效:飞出/碰撞各一组、两套音色 | 有飞出与碰撞音效,且至少两种可选 | 波形现场合成成功,两套音色互不相同,播放序列正常 | PASS |
| T08 | 音效开关与选择保存 | 关掉后无声,选择写入存档 | 关闭时 play_sound() 返回 False,重新读取存档 sound_style='wood' sound_on=False |
PASS |
| T09 | 音效适配不同 mixer 格式 | 有声卡的机器上也能正常出声 | 分别用 44100/立体声、48000/立体声、22050/单声道预先初始化,均可正常合成并播放 | PASS |
运行run_tests.py以及python arrow_puzzle.py --selftest结果


第四步:aigc使用表格
| 子任务 | 借助何种 AIGC 技术 | AI 实现或提供了什么 | 效果如何 | 人工修改 |
|---|---|---|---|---|
| 需求分解与开发顺序 | DeepSeek 网页版 | 把"做一个一箭又一箭小游戏"拆成界面 / 路径逻辑 / 关卡 / 测试等模块并排顺序 | 拆得太碎,第一版方向跑偏:做成了"旋转箭头对齐方向"的玩法 | 自己重写核心玩法:点一下到底发生什么、判定范围、两种结果 |
| 核心路径判定 | DeepSeek harness | 生成方向位移映射 | 代码能跑,但"画出来朝上、判定朝左",只在边缘暴露 | 收敛成唯一一份 DIR_STEPS,判定与绘制共用;加"渲染朝向必须和数据方向一致"的测试 |
| 动画与碰撞反馈 | DeepSeek harness | 先实现为"本体不动 + 另画一个特效飞出去" | 视觉上像幻影:原地留着一支箭,另一支飞出去又回来 | 改成 Arrow.offset 驱动本体沿自己的轨道运动,判定随之简化成只看箭头那一格 |
| 关卡可解性 | DeepSeek harness | 提出"随机生成 + 求解器筛选无解"的方案 | 满盘时能飞的箭只有贴边朝外那几支,随机 6×6 可解率约万分之一,拒绝采样几乎永远失败 | 改为反向构造(按拆除顺序倒着摆),并加"每关必须可解"的自动化测试 |
| 撤销 / 提示 / 音效 / 创意工坊 | DeepSeek harness | 按需求实现各个拓展功能 | 撤销"只恢复箭头位置"会破坏邻居图;音效在真机上完全静默 | 撤销改"最初那张图 + 操作序列重放";音效改为先读回 mixer 真实格式再合成 |
| 打包可执行文件 | DeepSeek harness | 给出 PyInstaller 打包脚本与参数 | --onefile 在写不了 %TEMP% 的环境下解包失败、进程卡死 | 改 --onedir,并新增"让 exe 自己跑一遍自检、看退出码"的验证方式 |
第五步:上传github仓库以及克隆验证
过程中计划分六个部分分步上传
| 第几次 | 提交信息 | 上传的文件 |
|---|---|---|
| 1 | chore: 项目骨架与内置 pygame | .gitignore、requirements.txt、vendor/ |
| 2 | feat: 游戏主体(状态机 + 棋盘 + 沿轨道移动) | arrow_puzzle.py |
| 3 | feat: 创意工坊(功能开关 + 自定义地图上传) | arrow_puzzle.py(这一轮改的部分)、maps/ |
| 4 | test: 22 项自检 + T01~T09 验收用例 | run_tests.py |
| 5 | build: 打包脚本与可执行文件 | build_exe.py、dist/ArrowAfterArrow.zip |
| 6 | docs: 说明文档与作业博客 | README.md、BLOG_测试记录.md、交作业版博客.md、博客补充材料.md、PyCharm建仓库与提交步骤.md、docs/ |
前六步是先本地提交最后一起推送的
完整历史见仓库
完成后以zip形式下载到本地进行克隆验证
第一次失败(见问题七)
第二次成功
游戏能正常打开

自检通过

测试用例也通过

第六步:psp表
| 阶段 | 预计耗时(分钟) | 实际耗时(分钟) | 差异(实际 − 预计) |
|---|---|---|---|
| 需求、环境和任务拆分 | 10 | 3 | −7 |
| 规则、关卡与测试;界面并行开发 | 240 | 120 | −120 |
| 集成、自动化验收与画面检查 | 30 | 15 | −15 |
| README、截图和测试记录 | 30 | 20 | −10 |
| 合计 | 310 | 158 | −152 |
第七步:遇到的问题以及如何解决
问题一
拆分的提示词太碎导致一开始ai完全没有正确理解要做一个什么游戏,做成了这样


解决方法:把截图丢给ai让它重新生成更有效可靠的提示词.
问题二
生成的游戏描述上符合我的要求,但实际上并不是


可以看到,第一行第二个本来不应该被阻挡,但被它左边的箭头阻挡了.而最后一行第一个本应被阻挡却没有.
解决方法:判定和绘制各用了一套方向映射(一边按 (dx,dy) 写、一边按 (dr,dc) 解包,于是 UP=(0,-1) 变成了"向左",索性改变逻辑,自己动手写提示词,不用位置加方向逻辑,改用邻接表存储来优化.


问题三
箭要沿着箭头移动,不是整体刚性移动,碰撞反馈的时候原地还留下一个箭,就好像只是一个幻影被发射出去又回来了,这不对,应该是本体出去(过程太短难以截图)
(这之前的问题是我找出,ai解决)
(之后的问题都是ai测试找出并解决)
问题四
连续开关多局游戏导致段错误崩溃
- 发现过程:T03 用例需要遍历 3 个关卡,即在一个进程里连续创建/销毁 3 个
Game对象。第一次执行到第 2 个 Game 时,进程直接报Segmentation Fault。 - 根因分析:字体对象缓存
_FONT_CACHE是模块级的。Game.shutdown()调用pygame.quit()后,旧的 Font 对象已失效。第二次Game.__init__时缓存未清空,再次font.render()踩踏内存导致段错误。 - 修复与回归:新增
clear_font_cache(),在Game.__init__(重新 init 后)和Game.shutdown()(quit 前)各清理一次。修复后,T03 从崩溃变为 PASS,其余 7 条用例和 20 项自检全绿。
问题五(严重)音效在任何有声卡的机器上都是哑的
- 发现过程:dummy 测试环境下,T07、T08 均 PASS。但将游戏放到真实音频驱动(
driver: windows)上运行,发现mixer起来了但SoundKit.ok是False,音效被静默丢弃。 - 根因分析:
pygame.init()在有音频设备时会先用默认参数开启 mixer(如 44100/-16/2 声道)。而pygame.mixer.init()对已初始化的 mixer 是空操作。代码却按单声道合成 PCM 数据,导致pygame.mixer.Sound(buffer=...)抛出异常被except吞掉。 - 修复与回归:先读取 mixer 的真实格式再按该格式合成(多声道重复采样点),位深不对时先
quit()再重开。修复后真机音效可用,并补充了 T09 用例将不同预设格式钉进测试。
问题六(中)类的成员被放错了类
- 发现过程:使用文本替换方式往文件里插
SoundKit类时,Records后半截成员(endless_unlocked等)被留在了新插入的SoundKit类体内。语法合法,但自检运行时报AttributeError。 - 修复与回归:将这 4 个成员挪回
Records类。这种“语法合法、语义错位”的问题静态阅读很难发现,但有测试去调这些方法就会一秒露馅。
问题七 pygame必需文件被ai加入,gitignore
克隆验证不成功

询问ai,ai回复:
你截图里的报错是:
File "vendor\pygame_init_.py", line 83, in
from pygame.base import *
ModuleNotFoundError: No module named 'pygame.base'
pygame.base 是个 .pyd(Windows 上的编译扩展,相当于 DLL)。我在 .gitignore 里写了很常见的一行:*.py[cod]
这个字符组匹配 .pyc、.pyo、.pyd —— 于是 pygame 的 38 个核心扩展(base/display/draw/font/event/mixer…,共 2.0 MB)全被忽略掉了,一个都没推上去。任何人克隆下来都跑不起来——这正好命中作业那条"克隆到干净目录能否跑通"的检查项。
解决:重新提交并推送补丁fix: 补上被 .gitignore 误伤的 38 个 .pyd 编译扩展(否则克隆后 pygame 无法导入)
第八步:心得体会
AI 帮了什么。 省时间的主要是样板代码和查错。界面绘制、状态机、动画这些我不太想手写的部分,AI 给得很快; 排查问题也快——比如段错误那个崩溃,堆栈指向 font.render(),我一开始完全不知道从哪下手, AI 直接说是模块级字体缓存跨 pygame.quit() 复用导致对象失效。还有反向构造法这个思路是 AI 提的: 我原来的想法是随机摆布局再用求解器筛掉无解的,效率极低(实测随机 6×6 可解率约万分之一), 反向构造从根上保证有解,这个我没想到。
出了问题的地方。
AI 第一版给的代码不一定对。 方向映射那次最典型:程序不报错、绝大多数情况看着也正常,只在边缘暴露, 而且错误是"画出来朝上、判定朝左"这种,光看运行日志根本看不出来,是我盯着截图看出来的。 这让我意识到,不能因为"跑起来了"就当成对的。
我自己摆的关卡也是错的。 手工摆的关卡里有死局(箭头互相指着形成闭环),肉眼完全看不出来。 这件事让我明白"把正确性写成自动化测试"不是形式主义,是真的能挡住错误。
测试本身也会错。 我的自检里有一版判据写反了(以为三角形质心偏向箭尖,其实偏向底边), 结果 33 支箭全报错;还有一次自测通过,但那是循环论证——用 .gitignore的规则写脚本模拟了一遍要提交的文件,得出 221 个,跟 GitHub 上实际的 221 个一对,就说"完全一致 ✓".(问题七)可模拟用的就是那套有 bug 的规则,所以规则本身错了也测不出来。
最危险的是"测试全绿、功能全死"。 音效那次,无窗口测试里一切正常,放到真机上才发现功能是哑的—— 因为 try/except 把初始化错误吞掉了。"能跑起来"和"好使"中间差着一个验证。
AI 不知道界面好不好看。 文字压到按钮上、动画像幻影,都是我截图给它看之后才改的。
收获:这次最大的收获不是学会 Pygame,而是摸出了和 AI 协作的分寸: 需求要写清"输入是什么、输出是什么、什么算对";让 AI 写,但关键逻辑自己得看懂、自己验证; 复杂的东西先要一个小版本,再一轮一轮改,不要大水漫灌.
第12138步:写在后面
主体工作已经完成,不过想到这个仓库没什么人会注意到,注意到的人里也不会有几个记得给我star,就不禁悲从中来(并没有),同时想到了安全方面顾虑,于是经过讨论和ai一起设计了这个功能



具体来说玩家可以自己算一遍每个文件的 SHA-256,跟官方清单比对,确认文件没被改动过。
命令行: python arrow_puzzle.py --verify # 0=一致 1=不一致 2=没有清单
python arrow_puzzle.py --hash # 打印每个文件的校验码和整体指纹
游戏里: 首页 →「文件校验」→「本地校验」或「联网校验」(从 GitHub 取官方清单)并且,现在玩家可以通过这个功能跳转到我的仓库给我star了(这才是主要目的)
一开始只能比对本地的清单,那样意义其实有限:攻击者改了游戏文件、再把本地那份清单一起改掉,还不能给我加star(不是)
校验照样显示"一致"。改成从仓库取官方清单之后,他改不了 GitHub 上那份,这种情况就能被发现了。
由于主体已经完成,于是不再修改前面的博客,另开一步
联网检验结果:
浙公网安备 33010602011771号