软件工程第二次作业

软件工程第二次个人作业:用 AIGC 完成「一箭又一箭」小游戏

| 项目 | 一箭又一箭 |
| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/fzu/202601SofwareEngineering|
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/fzu/202601SofwareEngineering/homework/16717 |
| 这个作业的目标 | 使用 Python 和 AIGC 完成「一箭又一箭」小游戏 |
| 学号 | <102401203> |
| GitHub 仓库 | https://github.com/aurogons/aurogons |


一、项目展示

开始界面

01-开始界面

标题下方是「开始游戏」按钮和关卡选择。未解锁的关卡显示为灰色不可点击,
首次进入时只有第一关可选。底部列出了完整规则,玩家不需要看说明文档就能上手。

游戏过程

02-游戏界面-第一关

顶部信息栏显示当前关卡、剩余箭头数量(带进度条)、剩余失误次数和用时。
棋盘上四种方向的箭头用了四种颜色,扫一眼就能分清朝向。

03-游戏界面-第五关

第五关是 6×6 的大棋盘,16 个箭头。格子大小会根据棋盘尺寸自动缩放,
所以大棋盘也不会挤爆窗口。

碰撞反馈

04-碰撞反馈

点击一个飞不出去的箭头时,箭头会原地抖动、闪烁变红,同时挡住它的那个箭头会被
高亮圈出
,顶部还会提示剩余失误次数。让玩家一眼就知道「我为什么飞不出去」,
而不是只看到一个笼统的错误提示。

提示功能

05-提示功能

点「提示」会高亮一个当前可以飞出的箭头。连续点会依次轮换所有可选项,
方便玩家对照着找规律。

通关与失败

06-通关界面

07-失败界面

结算界面保留最后的棋盘作为背景,上面盖一层半透明遮罩,显示用时、点击次数和
剩余失误。通关时如果还有下一关,会多出一个「下一关」按钮。


二、项目介绍

游戏规则

棋盘上散布着朝上、下、左、右四个方向的箭头。点击箭头时检查它前进方向上
到棋盘边界之间还有没有其他箭头:

  • 前方畅通 → 箭头沿自身方向飞出棋盘并消失;
  • 前方有阻挡 → 箭头飞不出去,消耗一次失误机会;
  • 清空全部箭头即通关;失误次数用尽则本关失败。

界面设计

三个界面:开始界面(标题、开始游戏、关卡选择、退出、规则说明)、
游戏界面(信息栏 + 棋盘 + 操作按钮)、结果界面(通关 / 失败结算)。

游戏界面按作业要求显示了当前关卡、棋盘、剩余箭头数量、剩余失误次数和重新开始按钮,
另外加了用时、提示、撤销和返回。

配色上刻意避开了红色系作为箭头颜色(四方分别用琥珀、青绿、天蓝、淡紫),
把红色专门留给碰撞反馈,避免玩家把「朝右的箭头」误认成「出错了」。

主要功能

功能 说明
四方向箭头与阻挡判定 同一行或同一列上的路径检测
飞出动画 箭头沿自身方向滑出窗口,后半程淡出
碰撞反馈 抖动 + 闪红 + 高亮挡路箭头 + 文字提示
失误次数 界面实时显示,用尽则失败
5 个关卡 4×4 到 6×6,规模与失误上限递增
关卡选择与解锁 通关后解锁下一关,可从主菜单直接跳关
重新开始 恢复本关初始布局与失误次数
撤销 退回上一步,箭头与失误次数一起回退
提示 高亮一个当前可飞出的箭头,可连点轮换
计时与统计 记录用时和点击次数

三、实现思路

1. 箭头、方向与关卡怎么表示

方向用枚举表示,枚举值直接就是前进增量

class Direction(Enum):
    UP    = (-1, 0)
    DOWN  = ( 1, 0)
    LEFT  = ( 0, -1)
    RIGHT = ( 0, 1)

这样四方向不需要写四套分支,路径检测可以共用同一段代码。

箭头本身是一个不可变的 Arrow(row, col, direction),棋盘则用
dict[(row, col)] -> Arrow 存放。用字典而不是二维数组有两个好处:
棋盘通常是稀疏的,字典更省;而且越界坐标只会取到 None
不会抛出 IndexError

关卡用字符画描述,而不是一串坐标元组:

art = """
    v . v . .
    > . . ^ .
    . . . ^ .
    . . . ^ <
    > v . . .
"""

2. 路径检测(重点)

这是本项目的核心算法。写法是沿方向逐格推进,直到走出棋盘

def path(self, arrow):
    """按箭头朝向,依次产出它前方棋盘内的所有格子。"""
    drow, dcol = arrow.direction.delta
    row = arrow.row + drow
    col = arrow.col + dcol
    while 0 <= row < self.rows and 0 <= col < self.cols:
        yield (row, col)
        row += drow
        col += dcol

def is_free(self, arrow):
    return all(cell not in self.arrows for cell in self.path(arrow))

这段代码有两个值得说的地方:

第一,四个方向共用一段逻辑。 方向不是用 if direction == UP: ... elif ...
去分支,而是化成了增量 (drow, dcol)。四套分支的写法在实现碰撞反馈时
还要再写一遍,容易出现某个方向改了、另一个方向忘了改的情况。

第二,循环条件本身就是边界检查。 走出棋盘循环自然结束,
不需要额外判断「下一步会不会越界」。所以调用方拿到的坐标永远是合法的。

边界情况(T03)也就自然成立了:位于边缘且朝向棋盘外的箭头,
第一步就走出棋盘,path 产出空序列,all(...) 对空序列返回 True
箭头顺利飞出。

顺带一提,path 是生成器,is_freeblocker_of 都复用了它:

def blocker_of(self, arrow):
    """返回挡在箭头前方的第一个箭头,用于高亮提示。"""
    for cell in self.path(arrow):
        blocker = self.arrows.get(cell)
        if blocker is not None:
            return blocker
    return None

碰撞反馈要「高亮是谁挡住了我」,直接复用同一个 path 就行,
不用再写一遍遍历。

3. 关卡的可通关性是可以机械验证的

设计关卡时最容易犯的错误是摆出一个无论怎么点都通不了的死局。
人肉试玩验证既慢又不可靠,所以我写了一个校验器 tools/validate_levels.py

它能自动验证,靠的是游戏规则的一个性质:消除箭头只会让棋盘更空,
绝不会把别的箭头从「能飞」变回「被挡」。
也就是说「当前可飞出的箭头集合」
随消除单调递增——已经解锁的箭头永远是解锁的。

由此得到两条结论:

  • 如果这一关存在通关顺序,那么「每次随便挑一个当前能飞的箭头点掉」这种
    完全不看全局的贪心策略,也一定能清空棋盘(先走哪一步都不会把路走死);
  • 反过来,如果贪心过程中卡住了(还剩箭头,但一个都飞不出去),
    这一关就必定无解,不存在某条巧妙路线能救回来。

于是关卡的可通关性就变成了一个几行代码的循环。校验器还会额外检出
「互相阻挡的箭头对」:如果 A 前方的第一个箭头是 B,B 前方的第一个箭头又是 A,
那么放走 A 必须先移走 B,移走 B 又必须先放走 A——死循环。
这类布局用肉眼看字符画时特别容易漏掉,尤其是当两个箭头隔了好几格、
中间还夹着别的空格的时候。

4. 动画与逻辑解耦

一个设计上的选择:点击时立即更新棋盘状态,飞出动画只是一个与棋盘无关的
视觉残影对象。

如果改成「动画播放期间锁住输入」(这也是常见做法),每点一下都要等 0.2~0.3 秒
才能点下一个,手感会明显发滞。而现在状态已经更新完了,动画只是画面上在飘,
玩家可以连着快速点击,也不会出现「上一次动画没结束就点下一发」导致状态错乱的问题。

碰撞反馈的抖动方向取的是垂直于箭头朝向的那一轴:横着的箭头上下抖,
竖着的左右抖。一开始试过顺着箭头方向抖,结果看起来像「飞出动画卡了一下」,
垂直抖动才能明确表达「被挡回来了」。


四、AIGC 使用过程

本项目的开发全程使用 Claude Code 协助,采用「我提要求 → AI 出方案和代码 →
我实际运行验证 → 发现问题回头修改」的方式推进。下面记录 5 次比较有代表性的过程。

汇总表

子任务 借助何种 AIGC 技术 AI 实现或提供了什么 效果如何 人工修改
关卡设计 Claude Code 生成 5 个关卡的字符画布局 第 4、5 关摆出了死局,校验器报错 定位到两处「正面相对」的箭头对,把其中一个改成同向
关卡校验器 Claude Code 提出用贪心法判定可通关性,并实现校验脚本 有效,能自动判定每一关是否可解 追加了「互相阻挡箭头对」的专门检测,让报错指向病根
路径检测 Claude Code 给出用增量 + while 循环的统一写法 四方向共用一段代码,无越界问题 采用;并确认边界情况(T03)自然成立
中文字体 Claude Code 按系统字体路径依次回退的加载方案 中文正常显示,不再是方块 补上找不到字体时退回 SysFont 的兜底
界面排版 Claude Code 生成界面并自动渲染截图 截图里发现三处排版问题 数值列左边缘对齐、进度条加宽、结算按钮整体居中
单元测试 Claude Code 按 T01~T06 编号生成测试 有 6 个用例失败 检查后确认是测试写错了,不是代码有 bug,改正用例

记录 1:AI 设计的关卡摆出了死局,靠校验器抓出来

这是整个开发里最有价值的一次协作。

我提出需求:「设计 5 个关卡,规模从 4×4 递增到 6×6,难度递增,用一个校验器保证每关都能通关。」

AI 先把自己的设计思路写清楚了——它指出了一条硬约束:同一条直线上的两个箭头
不能正面相对,否则互相阻挡、谁也飞不出去;想造「链式阻挡」应该让同一条线上的
箭头朝向一致,这样最上面的飞走后下面的依次解锁。然后它给出了 5 关布局和
校验脚本。

实际运行校验器的结果:

[1] 第一关 · 初识规则        可通关:贪心解法 4 步清空棋盘
[2] 第二关 · 阻挡链          可通关:贪心解法 6 步清空棋盘
[3] 第三关 · 拆外圈          可通关:贪心解法 9 步清空棋盘
[4] 第四关 · 长距离阻挡      无法通关:卡住时还剩 4 个箭头
    死局箭头 (0,0)v  (0,2)v  (4,0)>  (4,2)^
[5] 第五关 · 综合            无法通关:卡住时还剩 6 个箭头

有意思的是,AI 明明在注释里把「不能正面相对」这条规则写对了,
自己设计关卡时却还是踩了进去——第四关的 (0,2)v(4,2)^ 在同一列上正面相对,
第五关修完纵向的问题之后,又在 (4,0)>(4,4)< 之间造出了一个横向的同类错误。

我的处理:把这两个箭头改成同向,重新跑校验器确认全部通过。同时让 AI 给校验器
加了一段专门检出「互相阻挡的箭头对」的逻辑,这样以后报错会直接指出是哪两个箭头,
而不是只说「卡住了」。

这次经历说明:AI 能准确地讲清规则,但不保证自己遵守规则
把规则写成可以自动执行的检查(校验器、测试),比指望 AI 每次都做对要可靠得多。

记录 2:单元测试报错,但错的是测试本身

写完测试后运行,出现 6 个失败,都来自路径检测的用例。例如:

FAIL: test_路径只包含棋盘内的格子
AssertionError: Lists differ: [] != [(1, 0), (2, 0)]

AI 写的用例是这样的:棋盘上 ^ 放在 (0,0),期望它的路径是 [(1,0), (2,0)]

我的处理:检查后发现是测试写错了——^ 表示朝上,放在第 0 行意味着
它就在棋盘最上边缘、第一步就走出去了,路径本来就应该是空的
(这恰好是 T03 要测的情形)。AI 想要的是「沿这一列往下走」,那应该用 v
改正用例后全部通过。

这次提醒了我一件事:测试失败时不能默认「代码有 bug」,
也可能是测试的预期本身就是错的。判断的依据应该是回到规则本身去想
——「朝上的箭头放在最上面一行,它前方还有格子吗?」

记录 3:中文字体渲染成方块

第一次运行游戏,所有中文都变成了方块(俗称「豆腐块」)。
原因是 Pygame 的默认字体不含中文字形。

AI 的处理:给出了显式加载系统中文字体的方案,按
msyh.ttc → simhei.ttf → simsun.ttc 的顺序查找,找到第一个存在的就用。

我的修改:补上了兜底——万一这三个字体都不存在(比如换到 Linux 或 macOS 上),
退回 pygame.font.SysFont 按字体名查找,至少不至于直接崩掉。
另外给字体对象加了缓存,避免每帧都重新加载字体文件。

记录 4:靠自动截图发现界面排版问题

游戏跑通之后,AI 提出可以写一个脚本用 SDL 的无界面驱动把各个界面渲染成 PNG。
这本来是为了给 README 准备截图,结果意外成了有效的界面检查手段——
把截图打开一看,发现了三处之前没意识到的问题:

  1. 信息栏三栏数值左边缘没有对齐:标签宽度不同(「用时」比「剩余箭头」短得多),
    而代码是给所有栏用同一个固定偏移,导致「用时」和它的数值之间空了一大块;
  2. 通关进度条只有 400 像素宽,缩在左边,看起来像一条多余的线;
  3. 失败界面上只有两个按钮(没有「下一关」),但按钮位置是按三个按钮的布局写死的,
    整体偏右没有居中。

我的处理:三项都改了——数值位置改成按最宽标签动态计算,
进度条改成横贯整个信息栏,结算按钮改成按实际宽度整体居中。

记录 5:控制台中文乱码与窗口尺寸自适应

两个小问题,但都属于「不实际跑一遍就发现不了」的类型。

一是 Windows 控制台的默认编码是 GBK,工具脚本输出的中文全是乱码。
AI 的解决办法是在脚本开头把 sys.stdout 重新配置成 UTF-8。

二是 6×6 的棋盘如果沿用 4×4 的格子尺寸,棋盘高度会超出窗口。
改成格子边长根据棋盘尺寸和可用区域动态计算,并设了上下限,
这样 4×4 时格子更大更好点,6×6 时自动缩小也能完整显示。


五、测试结果

手工测试

编号 测试内容 预期结果 实际结果 是否通过
T01 点击前方无阻挡的箭头 箭头飞出棋盘并消失 箭头沿方向滑出并淡出,剩余箭头数 -1 通过
T02 点击前方有阻挡的箭头 箭头不消失,失误次数减 1 箭头原地抖动变红,挡路箭头被高亮,失误 -1 通过
T03 点击位于边缘且朝向棋盘外的箭头 箭头正常消失,不发生越界错误 四条边的箭头均正常飞出,无异常 通过
T04 消除本关全部箭头 显示通关并进入下一关 弹出通关结算,点「下一关」正常切换 通过
T05 失误次数耗尽 显示失败并允许重新开始 弹出失败结算,可重玩本关,失误数恢复 通过
T06 游戏进行中重新开始 箭头布局和失误次数恢复 布局、失误数、用时、撤销历史全部复原 通过

自动化测试

python -m unittest discover -s tests -v

72 个用例,全部通过:

Ran 72 tests in 2.882s

OK

测试分三个文件:

  • tests/test_board.py —— 棋盘规则与路径检测,按 T01~T06 编号组织,
    另加边界用例(点击空格、点击越界坐标、点击已经飞出的格子、
    失误归零后继续点击、非法构造参数等);
  • tests/test_levels.py —— 断言每一关都能通关、没有互相阻挡的箭头对、
    开局至少有一个可选项、难度随关卡递增;
  • tests/test_integration.py —— 用 SDL 的 dummy 视频驱动把整个游戏跑起来,
    覆盖场景切换、通关解锁、失败重玩、碰撞反馈、撤销与提示。

关卡校验

python tools/validate_levels.py
[1] 第一关 · 初识规则      4x4  4 个箭头   初始可飞出 2 个 (50%)   可通关
[2] 第二关 · 阻挡链        4x4  6 个箭头   初始可飞出 3 个 (50%)   可通关
[3] 第三关 · 拆外圈        5x5  9 个箭头   初始可飞出 3 个 (33%)   可通关
[4] 第四关 · 长距离阻挡    5x5  12 个箭头  初始可飞出 5 个 (42%)   可通关
[5] 第五关 · 综合          6x6  16 个箭头  初始可飞出 6 个 (38%)   可通关

全部 5 关均可通关。

六、PSP 表格

任务 预估耗时(小时) 实际耗时(小时) 差异(小时)
需求分析与游戏设计 1.0
Python 与图形库学习 1.5
游戏界面实现 2.0
路径与碰撞逻辑实现 2.0
关卡设计 1.5
AIGC 辅助开发 2.0
测试与修改 2.0
README 与博客撰写 1.5
合计 12

七、心得体会

AI 帮到了什么

最直接的帮助是把想法快速变成能跑的东西。四方向枚举设计成携带增量、
path 用生成器复用、动画与逻辑解耦——这些设计上的选择,AI 都能一次给出比较合理的版本,
省去了大量查文档和试错的时间。写字符画关卡解析、中文字体回退这类
「知道该有、但写起来琐碎」的代码也很省事。

出现的问题

但这次开发里,AI 犯的错误也很有代表性,而且恰恰都犯在它自己刚讲清楚的规则上

  • 它在注释里写明了「同一条线上两个箭头不能正面相对」,然后自己设计的第 4、5 关
    就摆出了这种死局,修完一个又犯一个;
  • 它写测试用例时把 ^ 放在棋盘顶端还期望路径往下走,方向理解反了。

这让我意识到,AI 输出的内容「读起来对」和「实际对」是两回事。
它能把规则表述得很准确,但不保证自己在生成具体内容时遵守了规则。

收获

最大的收获是学会了把规则变成可以自动执行的检查,而不是靠反复检查 AI 的输出。

校验器就是很好的例子:我没有去逐关盯着一格格检查布局,而是先想清楚
「可通关」这件事能不能自动判定——结果发现因为「消除只会让棋盘更空」这个单调性,
贪心法就能判定,于是整个验证变成了几行代码。这个工具后来真的抓出了两处死局,
而且抓出来的方式比我肉眼看字符画可靠得多。

测试也一样。72 个用例里有 6 个一开始是失败的,其中一部分是我的测试预期写错了。
这个过程让我养成了习惯:测试报错时先回头想规则本身是什么样,
再判断到底是代码错了还是测试错了。

另一个体会是关于分层。把 board.py 写成不依赖 Pygame 的纯逻辑模块,
一开始只是为了「方便测试」,但实际收益比预想的大得多——
核心规则可以在没有显示器的环境里秒级验证,改关卡后跑一遍测试就知道有没有改坏。
如果规则判断和界面绘制混在一起,这些都会变得很麻烦。

最后,我觉得 AI 更适合当「执行力很强的合作者」而不是「正确答案的来源」。
方案的取舍、规则的正确性、以及最后「这个东西到底对不对」的判断,还是得自己来。

posted on 2026-09-21 19:44  鼠尾草uu  阅读(21)  评论(0)    收藏  举报