arrow game 2026 秋软件工程个人作业(第二次)
2026秋软件工程个人作业(第二次)——一箭又一箭
| 项目 | 内容 |
|---|---|
| 这个作业属于哪个课程 | H202601 软件工程与软件工程实践(福州大学·计算机与大数据学院) |
| 这个作业要求在哪里 | 2026 秋软件工程个人作业(第二次) |
| 这个作业的目标 | 使用 Python 和 AIGC 完成"一箭又一箭"小游戏 |
| 学号 | 032401135 |
| GitHub 仓库 | https://github.com/otonash1/arrow-game |
一、项目展示
开始界面(游戏标题 + 玩法说明 + 开始按钮):

第 1 关(顶部显示关卡名、剩余箭头数、剩余失误次数,底部是"重新开始"按钮):

点击前方被挡住的箭头:箭头变红并抖动,顶部提示"前方有箭头阻挡,不能飞出(失误 +1)",失误次数扣除一次:

前方畅通时,箭头沿自己的方向平滑地飞出棋盘,同时逐渐变淡:

第 2 关(十字形布局,9 支箭头):

第 3 关(6×6 棋盘、20 支箭头,失误上限 4 次):

过关界面(点"下一关"继续):

失败界面(失误次数耗尽,可点"重新开始"重试本关):

全部通关界面:

二、项目介绍
本项目用 Python + pygame 实现了一个"一箭又一箭"风格的点击式箭头解谜小游戏。全部图形都由代码直接绘制,不依赖任何外部图片素材。
运行方式(也可以从 Releases 下载免安装的 ArrowGame.exe,双击直接运行):
pip install -r requirements.txt
python main.py
2.1 游戏规则
- 棋盘上分布着若干带方向的箭头,方向分为上、下、左、右四种;
- 点击一个箭头,程序检查它前进方向到棋盘边界之间是否还有其他箭头;
- 前方无阻挡 → 箭头沿该方向飞出棋盘并被消除;
- 前方有阻挡 → 箭头不能飞出,变红抖动并弹出提示,失误次数 +1;
- 清空当前关卡的箭头 → 过关并进入下一关;失误次数耗尽 → 本关失败,可以重新开始;
- 重新开始会把当前关卡恢复成初始状态:飞出的箭头回来、失误次数清零。
2.2 界面设计
游戏共五个界面状态,由一个状态机统一切换:
| 界面 | 内容 |
|---|---|
| 开始界面 | 游戏标题、玩法说明、"开始游戏"按钮,ESC 退出 |
| 游戏界面 | 顶部显示当前关卡名、剩余箭头数、剩余失误次数;中间是棋盘;底部是"重新开始"按钮 |
| 过关界面 | 半透明遮罩 + "过关!"提示 + "下一关"按钮 |
| 失败界面 | "本关失败"提示 + "重新开始"按钮 |
| 全部通关界面 | 完成所有关卡后的提示 + "返回主菜单"按钮 |
2.3 主要功能与特色
- 3 个手工设计、逐一验证可通关的关卡,难度渐进:第 1 关 5×5 共 5 支箭头 → 第 2 关 5×5 共 9 支箭头 → 第 3 关 6×6 共 20 支箭头;
- 飞出动画:箭头沿自身方向平滑滑出棋盘,越飞越淡,动画正好在"刚出界"时结束;
- 碰撞反馈:被挡住的箭头朝自己的方向抖动一下、变红并套上红色圆环,顶部提示文字停留一段时间后淡出;
- 纯代码绘制,无外部素材:箭头是代码画的多边形(以朝右为基准形状,按方向旋转得到其余三个方向),中文使用系统自带的微软雅黑字体渲染;
- 逻辑与界面分离:
board.py只负责数据和规则、不导入 pygame,可以脱离窗口单独测试; - 棋盘几何自适应:格子大小按关卡尺寸自动缩放,5×5 和 6×6 都能完整显示;
- 免安装可执行文件:提供打包好的
ArrowGame.exe(GitHub Releases 下载),Windows 下双击即可运行,不依赖 Python 环境。
三、实现思路
3.1 项目结构
main.py 程序入口:初始化 pygame,进入主循环
config.py 全局配置:窗口尺寸、配色、字体、动画参数等常量
levels.py 关卡数据(字符矩阵)
board.py 棋盘与规则逻辑(纯逻辑,不依赖 pygame)
game.py 游戏状态机:场景切换、事件分发、动画推进
renderer.py 绘制层:棋盘、箭头、按钮和各界面
tests.py T01–T06 自动化测试
模块之间是单向依赖:game.py 调用 board.py 的规则判定,renderer.py 只负责"把数据画出来"、不做任何规则判断。
3.2 箭头、方向与关卡的表示
四个方向用枚举表示,枚举值同时就是关卡文件里使用的字符;每个方向自带一个位移 (行增量, 列增量),四个方向就能共用同一套扫描逻辑:
class Direction(Enum):
UP = "^"
DOWN = "v"
LEFT = "<"
RIGHT = ">"
@property
def delta(self):
return {
Direction.UP: (-1, 0),
Direction.DOWN: (1, 0),
Direction.LEFT: (0, -1),
Direction.RIGHT: (0, 1),
}[self]
关卡数据写成字符矩阵,. 是空格,^ v < > 是四个方向的箭头。例如第 1 关:
"grid": [
".....",
"..v..",
".<v>.",
"..v..",
".....",
]
这样加关卡就是往 levels.py 里加一组字符串,不需要改动任何逻辑代码。
3.3 路径检测(核心)
整个游戏最关键的判断是"点击的箭头能不能飞出",实现在 Board.can_fly_out() 里:从箭头所在格的下一格出发,沿它的方向一格一格地往边界走;途中碰到任何箭头就返回"被挡住",一路走到棋盘外就返回"可以飞出"。
def can_fly_out(self, row, col):
"""判断 (row, col) 上的箭头前方是否没有其他箭头阻挡。"""
direction = self.arrows.get((row, col))
if direction is None:
return False
delta_row, delta_col = direction.delta
r, c = row + delta_row, col + delta_col # 从相邻的一格开始扫,不看自己
while 0 <= r < self.rows and 0 <= c < self.cols:
if (r, c) in self.arrows: # 路上碰到其他箭头 -> 被挡住
return False
r += delta_row
c += delta_col
return True # 一路走到棋盘外 -> 可以飞出
两个细节:
- 扫描从相邻的下一格开始,而不是箭头自己所在的格子,否则箭头会把自己当成阻挡物;
- 循环条件
0 <= r < rows and 0 <= c < cols同时限制行和列,因为这个游戏只需要检查同一行/同一列,这个条件也顺便保证了贴着边缘、朝向棋盘外的箭头不会发生数组越界(测试 T03 专门覆盖了这种情况)。
3.4 点击处理与动画反馈
点击流程由 game.py 统一组织:先把点击坐标换算成格子坐标,交给 board.click() 判定,得到三种结果之一:
| 判定结果 | 含义 | 后续处理 |
|---|---|---|
FLY_OUT |
前方畅通 | 箭头从棋盘移除,播放飞出动画;若已清空则进入过关 |
BLOCKED |
前方被挡住 | 失误 +1,箭头变红抖动并提示;若失误耗尽则进入失败 |
IGNORED |
点到空格或棋盘外 | 不产生任何影响 |
动画由主循环每帧传入的 dt 推进:飞出动画记录起始位置和方向,按进度沿方向平移并降低透明度;碰撞抖动用正弦函数让箭头朝自己的方向"撞一下再弹回来",配上红色圆环,提示文字停留 1.6 秒后消失。所有时长、幅度参数都放在 config.py 里,方便调整手感。
3.5 关卡设计与可解性验证
设计关卡时遵循两个原则:
- 同一行/列上排成一串、方向相同的箭头,总能按"最靠近边界的那支先飞走"的顺序清完;
- 要避免两支箭头互相指向(比如同行左边朝右、右边朝左),那样谁也飞不出去,会形成死局。
以第 3 关(6×6、20 支箭头)为例,中间是两条三连的竖链(上半朝上、下半朝下),两侧各接一条横链,相邻箭头全部"背对背",存在多种清空顺序:
..^^..
<<^^>>
..^^..
..vv..
<<vv>>
..vv..
三关的总箭头数、失误上限都不同,但每关都实际验证过存在能把所有箭头清空的点击顺序(该验证后来固化进了自动化测试 T04:对三个关卡都按"能飞就飞"的顺序模拟点击,确认可以全部清空)。
四、AIGC 使用过程
开发全程使用 Trae(内置 Coding Agent 的 AI 编程工具)辅助。下面记录 6 次有代表性的协作过程:
| 子任务 | 借助何种 AIGC 技术 | AI 实现或提供了什么 | 效果如何 | 人工修改 |
|---|---|---|---|---|
| 项目骨架与模块划分 | Trae | 把项目拆成 6 个文件(配置/关卡/棋盘/状态机/绘制/入口),并用 Scene 枚举管理五个场景 | 骨架一次跑通,开始界面和棋盘正常显示 | 检查并确认 board.py 不导入 pygame,保证规则逻辑可以脱离窗口单独测试 |
| 路径检测 | Trae | 生成沿方向逐格扫描的阻挡检测代码 | 四个方向的判定一次通过 | 确认扫描必须从"相邻的下一格"开始(不能把箭头自己算成阻挡),并补上防止越界的边界条件 |
| 中文字体崩溃修复 | Trae | 给出绕过 SysFont、直接读取系统字体文件的方案 |
修复崩溃,中文正常显示 | 把字体路径做成 config.py 里的优先级列表,换机器时可以直接调整 |
| 关卡设计 | Trae | 生成十字形的第 2 关和 6×6 的第 3 关布局 | 布局基本可用,但初版存在箭头互相指向的死局隐患 | 手工调整箭头方向改成"背对背"结构,并写脚本按"能飞就飞"的顺序验证每关都能清空 |
| 自动化测试 | Trae | 按 T01–T06 编写 unittest 测试(棋盘规则 3 项 + 完整流程 3 项) | 6 项测试全部通过 | 确认 T04 对三个关卡都跑一遍;T06 除了棋盘层还原,还覆盖界面层点击"重新开始"按钮的路径 |
| 打包与发布 | Trae | 用 PyInstaller 把游戏打包成单文件免安装 exe,并发布到 GitHub Releases | 生成的 ArrowGame.exe 约 14.6 MB,双击即可运行,不需要 Python 环境 |
把生成的 .spec 打包配置提交进仓库,同时在 README 中补充"下载 exe / 自行打包"的说明 |
几个过程的补充说明:
路径检测是游戏最核心的逻辑,AI 给出的循环结构基本可直接使用,但"从哪里开始扫"这个细节很容易出错——一开始的直觉写法是从箭头自己那一格开始,结果每个箭头都会把自己当成阻挡物。改成从相邻格开始后,再用 T03(贴着边界、朝向棋盘外的箭头)确认了边界处理没有问题。
中文字体崩溃是实际开发中遇到的一个真实 bug:用 pygame.font.SysFont() 加载中文字体时,程序在部分 Windows 上会抛 TypeError(读取系统字体注册表时出错),窗口起不来。AI 建议不再走 SysFont,而是直接用 pygame.font.Font() 读取 C:\Windows\Fonts\msyh.ttc(微软雅黑)字体文件,问题解决。
关卡设计不能只靠 AI 生成就完事:AI 排出来的初始布局里有几处箭头互相指向,一旦玩家点错就会卡死。最终改成"背对背"的布局,并且每一关都用脚本按"能飞就飞"的顺序模拟清空验证过。
五、测试结果
测试采用自动化测试:用 Python 标准库 unittest 编写(tests.py),并在导入 pygame 之前设置 SDL_VIDEODRIVER=dummy,因此在没有真实显示器的环境里也能运行。在项目目录下执行:
python tests.py
覆盖作业要求中的 T01–T06 六项测试:
| 编号 | 测试内容 | 预期结果 | 实际结果 | 是否通过 |
|---|---|---|---|---|
| T01 | 点击前方无阻挡的箭头 | 箭头飞出棋盘并消失 | 箭头从棋盘移除,剩余箭头数 -1,失误次数不变 | 通过 |
| T02 | 点击前方有阻挡的箭头 | 箭头不消失,失误次数减 1 | 箭头留在原地,失误次数 +1,剩余失误数正确 | 通过 |
| T03 | 点击位于边缘且朝向棋盘外的箭头 | 箭头正常消失,不发生越界错误 | 四个朝外的边缘箭头都能正常飞出 | 通过 |
| T04 | 消除本关全部箭头 | 显示通关并进入下一关 | 三个关卡都能按"能飞就飞"的顺序清空,清空后进入过关界面 | 通过 |
| T05 | 失误次数耗尽 | 显示失败并允许重新开始 | 失误记满后进入失败界面,按钮为"重新开始" | 通过 |
| T06 | 游戏进行中重新开始 | 箭头布局和失误次数恢复 | 飞出的箭头全部回来、失误清零、回到游戏界面 | 通过 |
运行结果:
Ran 6 tests in 0.360s
OK
对应的运行截图(完整输出见仓库 README):

六、PSP 表格
| 任务 | 预估耗时(小时) | 实际耗时(小时) | 差异(小时) |
|---|---|---|---|
| 需求分析与游戏设计 | 0.5 | 0.5 | 0 |
| Python 与图形库学习 | 0.5 | 0.3 | -0.2 |
| 游戏界面实现 | 1.0 | 1.2 | +0.2 |
| 路径与碰撞逻辑实现 | 1.0 | 1.0 | 0 |
| 关卡设计 | 0.5 | 0.8 | +0.3 |
| AIGC 辅助开发 | 1.0 | 1.2 | +0.2 |
| 测试与修改 | 1.0 | 0.7 | -0.3 |
| 开发环境与 Git 配置 | 0.5 | 1.5 | +1.0 |
| README 与博客撰写 | 0.5 | 0.5 | 0 |
| 合计 | 6.5 | 7.7 | +1.2 |
实际耗时超出预估的主要在"开发环境与 Git 配置":安装 Git、配置账号与代理、处理 GitHub 认证花费的时间比预想的多。
七、心得体会
这次作业是我第一次用 AIGC 工具从头到尾参与一个小项目的开发,整体感受可以概括为三句话:AI 能把"知道要做什么但写起来慢"的部分变得很快;但 AI 的产出必须自己验证;核心逻辑必须自己能讲清楚。
AIGC 带来的帮助:项目骨架的模块划分、飞出动画的绘制细节、unittest 测试的模板,这些内容 AI 都能在很短时间内给出可用的结果,省下了大量查文档的时间。比如箭头形状按方向旋转的画法、动画进度控制这些 pygame 细节,直接问 AI 比翻文档快得多。
出现的问题:
- AI 生成的环境相关代码不一定适配自己的机器——中文字体崩溃那次,最后是换了一种字体加载方式才解决,这类问题只能实际运行才能发现;
- AI 生成的关卡布局存在"互相指向"的死局隐患,必须自己逐个检查、实际试玩(或者写脚本验证)才能确认可通关;
- 动画的手感(时长、抖动幅度)AI 给的默认值不一定合适,需要在
config.py里反复微调。
收获:对"逻辑与界面分离"有了实际体会——board.py 不碰 pygame,所以六项测试全部可以脱离窗口、在命令行里秒级跑完,改动规则后回归测试非常方便。也因为助教可能对关键代码提问,我在写完每段 AI 生成的代码后都会自己过一遍:为什么路径检测要从相邻格开始扫、为什么循环条件能防止越界、状态机是怎么在五个界面之间切换的。这种"先理解再采纳"的习惯,大概是这次作业里最有价值的部分。

浙公网安备 33010602011771号