一箭又一箭开发

Posted on 2026-09-22 16:47  52hzwhale  阅读(10)  评论(0)    收藏  举报
项目 内容
这个作业属于哪个课程 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 关)
9月22日
2.进阶版(第二次提交,10 关)
在基础版之上加了选关、星级评价、提示、撤销、存档、音效和粒子特效:
9月22日(1)
二、项目介绍
游戏规则
棋盘上每个格子最多有一个箭头,箭头有上、下、左、右四个方向。点击一个箭头后,程序检查它前进方向上、到棋盘边界之间还有没有别的箭头

  • 前方没有阻挡 → 箭头飞出去并消失;
  • 前方有阻挡 → 箭头不能消失,会抖动、变红并弹出提示,同时扣掉一次失误机会
    把本关箭头全部清空就通关;失误次数(爱心)耗尽则本关失败。
    规则本身很简单,但难点在于箭头之间会互相阻挡,必须先想清楚点哪个。比如下面这个局
    → · · ↑ ·
    最左边那个箭头朝右,但它右边还有箭头,所以点它没用、还会白扣一次机会;而最右边那个朝右的箭头前方直达边界,可以放心点掉。
    界面设计
界面 内容
开始界面 游戏标题、星星总进度、开始游戏、选择关卡、音效开关
游戏界面 顶部:关卡名、爱心(剩余失误次数)、计时、剩余箭头数、返回按钮;中间:棋盘;底部:撤销 / 提示 / 重来 三个按钮 + 本关进度条
通关界面 星级评价、用时、新纪录提示、下一关 / 返回选关
失败界面 挑战失败、重新开始 / 返回选关
暂停界面 继续游戏 / 重新开始 / 返回选关
主要功能

基本功能:图形界面、上下左右四种方向的箭头、鼠标点击选择、路径阻挡判断、飞出消除、碰撞反馈、失误次数、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

  1. 从空棋盘开始,每次往一个空格里放一个箭头;
  2. 放的时候要求:它朝着自己方向到边界之间,没有任何已经放好的箭头
  3. 重复到放满为止。

为什么这样生成的一定有解?设放置顺序是 p1、p2、…、pn。那么 pn 放下去的时候它的射线是干净的,所以在完整棋盘里 pn 一定能先飞走;它飞走之后棋盘回到 {p1…p(n-1)},正好是 p(n-1) 放下去时的样子,于是 p(n-1) 也一定能飞走……以此类推,按放置顺序的逆序点,一定可以通关。构造的过程本身就证明了关卡有解。

生成之后我还会再用程序验证一遍,并且把所有关卡的参考通关顺序保存到了 tools/solutions.txt,方便自己试玩时对照。后面测试部分的 T07 更是直接让程序把 10 关全部自动通关了一次。

5. 箭头是怎么画出来的

原版游戏里的箭头是"粗圆头的箭杆 + 大三角箭头,箭尾逐渐变淡",用 Pygame 的 draw.line 画不出渐变。我的做法是:

  1. 先在透明蒙版上画出整支箭头的白色剪影(箭杆用 draw.line 加圆头 draw.circle,箭头用 draw.polygon);
  2. 生成一张横向的透明度渐变图(箭尾最淡、箭头处最实);
  3. BLEND_RGBA_MULT 让剪影乘上渐变,得到"半透明的白色箭头";
  4. 再用同样的方式染上目标颜色。
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.01.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 最擅长的是快速给出一个能跑的骨架补全那些"知道怎么写但懒得写"的部分(比如心形参数方程、各种绘图辅助函数),但在两件事上一定要自己盯着:

  1. 规则性的东西——比如"关卡必须能通关",AI 不会主动帮你保证,得自己想清楚用什么方法去保证;
  2. 跑起来才知道的问题——它自己写完不会去验证,必须我来跑、来点、来截图。

所以我的做法是:让 AI 写,但坚持自己运行、自己测试、自己玩。这一轮下来,测试脚本抓出的那个"箭头点不动"的 bug 是最典型的例子——它是这次作业里最有价值的一次修改。
用下来感觉 AI 最擅长的是快速给出一个能跑的骨架补全那些"知道怎么写但懒得写"的部分(比如心形参数方程、各种绘图辅助函数),但在两件事上一定要自己盯着:

  1. 规则性的东西——比如"关卡必须能通关",AI 不会主动帮你保证,得自己想清楚用什么方法去保证;
  2. 跑起来才知道的问题——它自己写完不会去验证,必须我来跑、来点、来截图。

所以我的做法是:让 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 写完必须自己跑起来看、自己点着玩,光看代码是看不出问题的。

我的收获

  1. 一开始我说"帮我做个游戏",它给的东西就很泛;后来我改成"帮我写一个函数,输入箭头坐标和方向,返回它前方有没有被挡住",它就写得又准又干净。要求越具体、边界越清楚,AI 的输出质量越高。

  2. 那个"箭头点不动"的 bug 就是自动化测试抓出来的——如果只是手动打开游戏随便点两下,很可能因为点到的正好是那个能点的箭头而蒙混过关,等交作业前才发现就来不及了。我现在觉得写完功能就顺手写个测试,比事后 debug 划算得多。

  3. AI 会写代码,但不会主动帮你保证游戏设计上的正确性。用"逆推构造法"保证关卡有解,是我这次最有成就感的一个设计——而且它的正确性是可以证明的,不是靠碰运气试出来的。

附:如何运行

# 1. 安装依赖(只需要 pygame 一个第三方库)
pip install pygame

# 2. 运行基础版(第一次提交)
python version1_basic.py

# 3. 运行进阶版(第二次提交)
python version2_advanced.py

操作:鼠标左键点击箭头;Esc 暂停/返回;R 重开本关;U 撤销;H 提示;M 开关音效。

博客园  ©  2004-2026
浙公网安备 33010602011771号 浙ICP备2021040463号-3