| 项目 | 内容 |
|---|---|
| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/fzu/2026-01SoftwareEngineeringandSoftwareEngineeringPractice |
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/fzu/2026-01SoftwareEngineeringandSoftwareEngineeringPractice/homework/16718 |
| 这个作业的目标 | 使用 Python 和 AIGC 完成"一箭又一箭"小游戏 |
| 学号 | 102401133 |
| GitHub 仓库 | https://github.com/52HzWhale111/arrow-game |
| 运行游戏:点这里在线玩 | |
| 一、项目展示 |
游戏用 Python + Pygame 开发,运行后是一个 480×800 的竖屏窗口,整体视觉参考了原版小游戏的风格:紫罗兰色背景配白色棋盘,深藏青色的圆头粗箭头,箭尾带渐变淡出效果。
1.基础版(第一次提交,3 关)

2.进阶版(第二次提交,10 关)
在基础版之上加了选关、星级评价、提示、撤销、存档、音效和粒子特效:

二、项目介绍
游戏规则
棋盘上每个格子最多有一个箭头,箭头有上、下、左、右四个方向。点击一个箭头后,程序检查它前进方向上、到棋盘边界之间还有没有别的箭头:
- 前方没有阻挡 → 箭头飞出去并消失;
- 前方有阻挡 → 箭头不能消失,会抖动、变红并弹出提示,同时扣掉一次失误机会。
把本关箭头全部清空就通关;失误次数(爱心)耗尽则本关失败。
规则本身很简单,但难点在于箭头之间会互相阻挡,必须先想清楚点哪个。比如下面这个局
→ · · ↑ ·
最左边那个箭头朝右,但它右边还有箭头,所以点它没用、还会白扣一次机会;而最右边那个朝右的箭头前方直达边界,可以放心点掉。
界面设计
| 界面 | 内容 |
|---|---|
| 开始界面 | 游戏标题、星星总进度、开始游戏、选择关卡、音效开关 |
| 游戏界面 | 顶部:关卡名、爱心(剩余失误次数)、计时、剩余箭头数、返回按钮;中间:棋盘;底部:撤销 / 提示 / 重来 三个按钮 + 本关进度条 |
| 通关界面 | 星级评价、用时、新纪录提示、下一关 / 返回选关 |
| 失败界面 | 挑战失败、重新开始 / 返回选关 |
| 暂停界面 | 继续游戏 / 重新开始 / 返回选关 |
| 主要功能 |
基本功能:图形界面、上下左右四种方向的箭头、鼠标点击选择、路径阻挡判断、飞出消除、碰撞反馈、失误次数、3 个以上可通关的关卡、通关 / 失败结算、重新开始。
进阶版额外功能:
- 10 个关卡 + 选关界面,通关后解锁下一关,未解锁的关卡显示一把锁;
- 星级评价:按剩余失误次数评 1~3 星,每关记录最好成绩(星级 + 用时),显示在选关界面上;
- 提示:每关 3 次,用金色光圈高亮一个当前可以消除的箭头;
- 撤销:可以退回上一步误点的操作;
- 进度存档:进度写入
save.json,关掉游戏再打开还在; - 音效:用代码合成波形,项目里没有任何音频素材文件;
- 动画与特效:箭头错峰弹出、飞出拖尾、碰撞粒子、屏幕轻微震动、结算卡片滑入、三颗星依次弹出。
两个版本的区别
| |version1_basic.py|version2_advanced.py|
| ---------------------- | ------------------- | ---------------------- |
| 关卡 | 3 关 | 10 关 |
| 选关 / 星级 / 计时记录 | — | ✅ |
| 提示 / 撤销 / 存档 / 音效 / 粒子 | — | ✅ |
| 代码行数 | 845 行 | 1233 行 |
三、实现思路
1. 箭头、方向、关卡怎么表示
一个箭头只需要三个信息:横坐标、纵坐标、方向。方向用 U/D/L/R 四个字符表示,坐标用 (列, 行),正好对应二维数组的下标。方向到坐标增量的映射用一张字典搞定:
DIR_VEC = {"U": (0, -1), "D": (0, 1), "L": (-1, 0), "R": (1, 0)}
关卡直接写成字符串列表,. 表示空格:
{"name": "第 1 关", "grid": [".D...",
"R...R",
"..U.U",
"UR..U"]}
这样做的好处是一眼就能看出棋盘长什么样,设计关卡和调整的时候非常方便,不用去数坐标。
2. 路径检测(核心)
这是整个游戏最核心的一段逻辑。思路很直白:从箭头所在格出发,沿着它的方向一格一格往边界走,路上遇到任何一个"还活着"的箭头就说明被挡住了;一路走出棋盘都没遇到,就说明可以飞出去。
def blocked_by(self, arrow):
dx, dy = arrow.vec # 该箭头的方向增量
cx, cy = arrow.x + dx, arrow.y + dy # 从它前面一格开始看
while 0 <= cx < self.grid_w and 0 <= cy < self.grid_h:
other = self.arrow_at(cx, cy)
if other is not None:
return other # 被这个箭头挡住了
cx += dx
cy += dy
return None # 一路到边界都没东西 -> 可以飞出去
几个值得说的点:
- 边界处理:循环条件写成
0 <= cx < self.grid_w and 0 <= cy < self.grid_h,只要走到棋盘外循环就自动结束,所以向上、向左这些"往坐标小的方向走"的情况不会出现负数下标越界。这一点我专门写了测试用例验证(见后面 T03、T08)。 - 只判断同一行或同一列:因为方向只有四个,沿方向走一格一格前进,天然就只在同一条直线上了,不需要额外的行列比较。
- 判断的是"还活着的"箭头:
arrow_at()会跳过已经飞出去的箭头。所以一个箭头飞走之后,它后面原本被挡住的那个箭头立刻就能飞了——这也是游戏推进的方式。
3. 点击之后发生了什么
def try_fly(self, arrow):
blocker = self.blocked_by(arrow)
if blocker is not None:
# 被挡住:抖动 + 变红 + 提示文字 + 扣一次失误
else:
# 没被挡住:先做逻辑消除(alive = False),再播放飞出的动画
这里有一个我特意处理的地方:先做逻辑消除,再放动画。也就是点中的瞬间箭头就已经从棋盘逻辑上消失了,飞出动画只是"表现层"。这样好处是后面被它挡住的箭头立刻就能点,玩家可以连续快速点击,不用等动画播完。如果反过来(等动画播完再消除),手速快的时候点击会没反应,手感很差。
4. 关卡怎么保证一定能通关
关卡当然可以手写,但随便摆箭头非常容易摆出死局。举个最小的例子,1×2 的棋盘上放两个箭头:
→ ←
左边的朝右,被右边的挡住;右边的朝左,被左边的挡住——谁也走不了,这一关永远通不了关。作业里明确写了"关卡实际上无法通关"要扣分,所以不能靠随机摆。
我的做法是写了个逆推构造法的生成脚本 tools/gen_levels.py:
- 从空棋盘开始,每次往一个空格里放一个箭头;
- 放的时候要求:它朝着自己方向到边界之间,没有任何已经放好的箭头;
- 重复到放满为止。
为什么这样生成的一定有解?设放置顺序是 p1、p2、…、pn。那么 pn 放下去的时候它的射线是干净的,所以在完整棋盘里 pn 一定能先飞走;它飞走之后棋盘回到 {p1…p(n-1)},正好是 p(n-1) 放下去时的样子,于是 p(n-1) 也一定能飞走……以此类推,按放置顺序的逆序点,一定可以通关。构造的过程本身就证明了关卡有解。
生成之后我还会再用程序验证一遍,并且把所有关卡的参考通关顺序保存到了 tools/solutions.txt,方便自己试玩时对照。后面测试部分的 T07 更是直接让程序把 10 关全部自动通关了一次。
5. 箭头是怎么画出来的
原版游戏里的箭头是"粗圆头的箭杆 + 大三角箭头,箭尾逐渐变淡",用 Pygame 的 draw.line 画不出渐变。我的做法是:
- 先在透明蒙版上画出整支箭头的白色剪影(箭杆用
draw.line加圆头draw.circle,箭头用draw.polygon); - 生成一张横向的透明度渐变图(箭尾最淡、箭头处最实);
- 用
BLEND_RGBA_MULT让剪影乘上渐变,得到"半透明的白色箭头"; - 再用同样的方式染上目标颜色。
mask.blit(grad, (0, 0), special_flags=pygame.BLEND_RGBA_MULT) # 剪影带上渐变透明度
tint.fill(color + (255,))
tint.blit(mask, (0, 0), special_flags=pygame.BLEND_RGBA_MULT) # 染成目标颜色
整支箭只在第一次用到时算一次,之后按 (格子大小, 方向, 颜色) 缓存复用,四个方向靠旋转 90°/180°/270° 得到,不用重复计算。
6. 音效怎么来的
进阶版的音效全部是用代码合成波形生成的,项目里没有任何 mp3/wav 素材文件:
buf = array("h", bytes(2 * n)) # 16 位 PCM 缓冲区
for i in range(n): # 频率从 f0 滑到 f1,再乘上衰减包络
buf[i] = int(math.sin(phase) * envelope * volume * 32767)
sound = pygame.mixer.Sound(buffer=buf.tobytes())
飞出去是频率下滑的"嗖",撞到是低频方波"咚",通关是一小段上行琶音。好处是整个项目拷到哪都能直接跑,不依赖素材路径;如果机器上没有声卡,程序会自动降级成静音,不影响玩。
四、AIGC 使用过程
我用的工具和方式
我用的 AIGC 工具是 Claude Code(Anthropic 的命令行编程 Agent)。
除了写代码,它还帮我做了需求拆解、关卡生成脚本、测试脚本的编写和 bug 排查。
代表性协作记录
| 子任务 | 借助何种 AIGC 技术 | AI 实现或提供了什么 | 效果如何 | 人工修改 |
|---|---|---|---|---|
| 关卡设计 | Claude Code | 提出"逆推构造法"生成关卡,并写了生成 + 验证脚本 | 生成的 13 关全部可通关 | 我调整了筛选条件(限制"开局就能直接消掉的箭头数量"),太简单的不采纳 |
| 路径检测 | Claude Code | 生成四个方向的检测函数 blocked_by() |
四个方向判定都正确 | 我补了 8 个手工构造的用例(互相阻挡、背对背等)逐条验证 |
| 游戏界面绘制 | Claude Code | 用 BLEND_RGBA_MULT 实现箭尾渐变、渐变填充标题字、立体圆角按钮 |
视觉上和参考图很接近 | 修了 3 处渲染 bug(见下面记录 3、5) |
| 测试脚本 | Claude Code | 写了 tools/test_game.py,模拟鼠标点击驱动游戏跑 T01~T08 |
8 项测试全部通过 | 测试抓出一个致命 bug:箭头全都点不动(见记录 4) |
| 音效 | Claude Code | 用 array 拼 16 位 PCM 采样合成音效,不依赖素材文件 |
音效正常,无素材依赖 | 把 mixer 初始化改成先 quit() 再指定单声道格式,否则波形会播错 |
| 关卡试玩验证 | Claude Code | 写了 tools/solutions.py 输出每关参考通关顺序 |
方便我逐关试玩核对 | 每一关我都自己实际玩过一遍,确认有合理通关顺序 |
记录 1:关卡怎么保证一定能通关
我提出的要求:先帮我确认一件事——随机生成的关卡会不会出现"无解"的情况?
AI 的回应:它指出确实会,并给了一个最小反例:1×2 棋盘上放 → ←,两个箭头互相指着对方,谁也走不了,这关就废了。然后它提出了"逆推构造法":从空棋盘开始逐个放箭头,每次要求"它到自己方向边界之间没有已放好的箭头",这样按放置的逆序消除必定能通关,并写了 tools/gen_levels.py。
实际效果:生成的关卡确实全部有解,脚本还会把参考解法打印出来。
我做的修改:它第一版生成的关卡里,有些一开局就有五六个箭头能直接消掉,玩起来太简单了。我加了"初始可直接消除的箭头数量上限"这个筛选条件,让它只保留更难一点的布局;另外还排除了"所有箭头挤在一条线上"的难看布局。改完之后每一关我都自己点了一遍确认能通。
记录 2:四个方向的路径检测
我提出的要求:写一个函数,判断某个箭头前方到棋盘边界之间有没有别的箭头,四个方向都要支持。
AI 的实现:用一张 DIR_VEC 字典把方向映射成坐标增量,然后沿方向一格一格走:
while 0 <= cx < self.grid_w and 0 <= cy < self.grid_h:
if (cx, cy) in occupied:
return other
cx += dx; cy += dy
实际效果:四个方向都对。
我做的修改:我没有直接信,而是手工构造了 8 个用例逐个验证,包括"朝上但上方就是边界""两个箭头背对背都朝外""互相指着对方"这些边界情况,全部写进了自动化测试的 T08。另外我一开始担心向上、向左走的时候会出现负数下标,确认了循环条件本身就拦住了,不会有越界。
记录 3:界面绘制——渐变效果踩的坑
我提出的要求:把界面做得和参考图一样——箭头是粗圆头、箭尾渐变淡出;标题是渐变填充的糖果立体字;按钮要有厚度和白边。
AI 的实现:它用了 BLEND_RGBA_MULT:先画白色剪影,再乘一张透明度渐变图,最后染色。标题字则是先合成"描边 + 投影"一层,再用"白字蒙版 × 竖直渐变"合成字身层叠上去。
实际效果:箭头效果很好,和参考图基本一致。
我做的修改:第一版标题字是整个图层一起乘渐变的,结果蓝色的描边和深色投影也被染成了黄色和橙色,看起来脏脏的。我让它改成"描边投影单独一层、字身单独一层,最后叠起来",才得到干净的效果。另外还有一处:菜单上的半透明面板我写的是 (255, 255, 255, 60),但主屏幕是没有 alpha 通道的,pygame.draw 会忽略透明度画成纯白,导致面板上的白色图标和白色文字完全看不见;改成不透明的浅紫色就正常了。
记录 4:测试抓出的致命 bug——箭头全都点不动
背景:我让 AI 写一个自动化测试脚本,模拟鼠标点击来跑作业要求的 T01~T06。
出现的问题:测试跑完,T01 通过了,但 T02、T04、T05、T07 全部失败。
排查过程:一开始我以为是测试脚本写错了,因为手动玩的时候明明能点。查下来才发现是游戏本身的 bug:箭头有个入场动画,用 spawn 表示进度(<0 还没出场,0~1 正在放大,1 就位)。但点击判定我写的是:
if a is not None and a.spawn <= 0: # ← 有问题
self.try_fly(a)
入场动画播完后 spawn 变成 1.0,1.0 <= 0 不成立,于是所有箭头都点不动了。之所以 T01 能过,是因为它点到的恰好是那个还停在 0.0 的箭头——纯属巧合,反而掩盖了问题。
修复:改成 a.spawn >= 0(小于 0 才表示"还没轮到它出场,不能点"),改完后 8 项测试全部通过。
这件事给我的教训:AI 写的代码"看着完全对、跑起来也能出画面",但逻辑上可能根本玩不了。必须真的去点、去测。而且这个 bug 如果不是因为写了自动化测试,很可能要等到交作业前才被发现。
记录 5:结算界面按钮跑出卡片外面
出现的问题:截图自查的时候发现,暂停界面有三个按钮,但第三个"返回选关"跑到白色卡片外面去了。
原因:卡片的宽度是固定的,高度我写死成了 300 像素,是按"两个按钮"算的;暂停界面有三个按钮,自然就放不下了。
修复:改成按按钮个数动态算高度:
height = start + (len(buttons) - 1) * 68 + 28 + 26
我的感受:这类"不影响逻辑、只影响观感"的问题,靠跑测试是发现不了的,只能一个界面一个界面截图看。我让 AI 写了个 tools/shot.py,可以把每个界面渲染成 PNG,这样不用一直开着窗口手动点,改完一键重新出图对比。
我用 AI 的一点体会
用下来感觉 AI 最擅长的是快速给出一个能跑的骨架和补全那些"知道怎么写但懒得写"的部分(比如心形参数方程、各种绘图辅助函数),但在两件事上一定要自己盯着:
- 规则性的东西——比如"关卡必须能通关",AI 不会主动帮你保证,得自己想清楚用什么方法去保证;
- 跑起来才知道的问题——它自己写完不会去验证,必须我来跑、来点、来截图。
所以我的做法是:让 AI 写,但坚持自己运行、自己测试、自己玩。这一轮下来,测试脚本抓出的那个"箭头点不动"的 bug 是最典型的例子——它是这次作业里最有价值的一次修改。
用下来感觉 AI 最擅长的是快速给出一个能跑的骨架和补全那些"知道怎么写但懒得写"的部分(比如心形参数方程、各种绘图辅助函数),但在两件事上一定要自己盯着:
- 规则性的东西——比如"关卡必须能通关",AI 不会主动帮你保证,得自己想清楚用什么方法去保证;
- 跑起来才知道的问题——它自己写完不会去验证,必须我来跑、来点、来截图。
所以我的做法是:让 AI 写,但坚持自己运行、自己测试、自己玩。这一轮下来,测试脚本抓出的那个"箭头点不动"的 bug 是最典型的例子——它是这次作业里最有价值的一次修改。
六、PSP 表格
| 任务 | 预估耗时(小时) | 实际耗时(小时) | 差异(小时) |
|---|---|---|---|
| 需求分析与游戏设计 | 1.0 | 1.0 | 0 |
| Python 与图形库学习 | 1.5 | 1.0 | -0.5 |
| 游戏界面实现 | 3.0 | 4.0 | +1.0 |
| 路径与碰撞逻辑实现 | 1.5 | 1.0 | -0.5 |
| 关卡设计(含生成与验证脚本) | 1.0 | 2.0 | +1.0 |
| AIGC 辅助开发 | 2.0 | 2.5 | +0.5 |
| 测试与修改 | 1.0 | 2.0 | +1.0 |
| README 与博客撰写 | 1.0 | 1.0 | 0 |
| 合计 | 12.0 | 14.5 | +2.5 |
差异最大的两项:一是"游戏界面实现",比预期多花了一小时——主要是渐变箭头和立体按钮这些视觉效果反复调了好几轮才满意;二是"测试与修改",也多花了一小时,因为测试抓出的那个"箭头点不动"的 bug 花了不少时间才定位到是入场动画的状态判断写错了。
七、心得体会
AI 帮了我什么
最大的帮助是降低了"从零开始"的门槛。我对 Pygame 其实不熟,如果自己去翻文档查"怎么画渐变""怎么画圆角矩形""怎么让文字有描边",可能要花掉一整个晚上。这次我把参考截图给 AI,它很快就给出了可运行的骨架,我可以把精力放在游戏规则和玩法逻辑上,而不是纠结绘图 API 怎么调。
另外它还很擅长补全那些"知道该有、但自己写很啰嗦"的部分,比如心形的参数方程、星星的多边形顶点计算、音效的波形合成。
出现的问题
但 AI 给的代码不能直接信。这次踩的坑很典型:
- 它写的点击判定
a.spawn <= 0逻辑上是错的,导致入场动画放完之后所有箭头都点不动,游戏根本没法玩,而画面上完全看不出来; - 渐变标题字它第一版把描边和投影也一起染色了,出来的字脏兮兮的;
- 半透明面板在无 alpha 的屏幕上变成实心白,导致白色文字直接看不见;
- 结算卡片高度写死,按钮多了就跑到卡片外面。
这些问题里,只有第一个是"功能性 bug",另外三个都是"看着不对" 的问题——一个是靠自动化测试发现的,另外三个是靠一张一张截图看出来的。给我的教训是:AI 写完必须自己跑起来看、自己点着玩,光看代码是看不出问题的。
我的收获
-
一开始我说"帮我做个游戏",它给的东西就很泛;后来我改成"帮我写一个函数,输入箭头坐标和方向,返回它前方有没有被挡住",它就写得又准又干净。要求越具体、边界越清楚,AI 的输出质量越高。
-
那个"箭头点不动"的 bug 就是自动化测试抓出来的——如果只是手动打开游戏随便点两下,很可能因为点到的正好是那个能点的箭头而蒙混过关,等交作业前才发现就来不及了。我现在觉得写完功能就顺手写个测试,比事后 debug 划算得多。
-
AI 会写代码,但不会主动帮你保证游戏设计上的正确性。用"逆推构造法"保证关卡有解,是我这次最有成就感的一个设计——而且它的正确性是可以证明的,不是靠碰运气试出来的。
附:如何运行
# 1. 安装依赖(只需要 pygame 一个第三方库)
pip install pygame
# 2. 运行基础版(第一次提交)
python version1_basic.py
# 3. 运行进阶版(第二次提交)
python version2_advanced.py
操作:鼠标左键点击箭头;Esc 暂停/返回;R 重开本关;U 撤销;H 提示;M 开关音效。
浙公网安备 33010602011771号