软件工程第二次作业
2026秋软件工程个人作业(第二次)—— 一箭又一箭
| 项目 | 内容 |
|---|---|
| 这个作业属于哪个课程 | 202601 软件工程(福州大学 计算机与大数据学院) |
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/fzu/202601SofwareEngineering/homework/16717 |
| 这个作业的目标 | 使用 Python 和 AIGC 完成"一箭又一箭"小游戏 |
| 学号 | 102401121 |
| GitHub 仓库 | https://github.com/zzx0607/arrow-and-arrow |
一、项目展示
开始界面(正中为游戏标题,下方依次是四条规则说明与"开始游戏 / 随机挑战"两个入口按钮):

对局界面(顶部信息栏从左到右显示关卡进度、剩余箭头、剩余失误,底部固定一个"重新开始"按钮):

箭头成功飞出时(沿指向平滑滑出棋盘并消失,顶部箭头计数同步减一):

点击被挡箭头时(原地晃动并变红,顶部弹出"前方有箭头阻挡!失误 -1"提示):

通关画面(半透明遮罩覆盖棋盘,中央显示"第 X 关通过"与下一关按钮):

失败画面(失误机会全部消耗后显示"本关失败",可从本关重来):

二、项目介绍
2.1 玩法规则
棋盘里散布着若干带指向的箭头(↑↓←→),规则围绕"哪个箭头可以先点"展开:
- 玩家选定一个箭头后,程序从它紧挨着的下一格出发,顺着它的指向一直检查到棋盘边缘;
- 这条检查路线上一路为空,则该箭头飞出棋盘并消失;
- 路线中途只要碰到另一个箭头,就判定被阻挡:箭头原地晃动变红提示碰撞,同时扣除一次失误机会;
- 本关全部箭头清空 → 进入下一关;3 次失误全部耗尽 → 本关失败,可重新开局。
2.2 界面结构
- 整款游戏共三种界面形态:菜单(menu)、对局(playing)与结算浮层(LEVEL_CLEAR / GAME_OVER / ALL_CLEAR),彼此之间靠状态机切换;
- 对局界面顶部实时刷新三组数据:当前关卡 / 总关数、剩余箭头数、剩余失误数;
- 底部常驻"重新开始"按钮,对局中按键盘
R也能立即重开本关; - 视觉反馈分两类:成功时箭头沿指向滑出棋盘;失败时箭头变红并左右抖动 0.45 秒,顶部同时出现提示文字。
2.3 主要功能与特色
- 全部由 Python + pygame-ce 实现,不依赖任何外部素材:箭头图形用 pygame 直接绘制,中文用系统自带的微软雅黑字体渲染;
- 规则与显示彻底分离:
game.py只做数据与判定,不 import pygame,因此不开窗口也能在命令行跑单元测试; - 内置 4 个手工编排、并由求解器逐一验证可通关的关卡(难度逐关提升);
- 附加玩法:随机挑战模式——每次随机生成 5×5、含 8 个箭头的棋盘,经求解器确认可通关后才呈现给玩家。
三、实现思路
3.1 箭头、方向与关卡的表示
棋盘底层是一个按行索引的二维列表,grid[row][col] 要么是 None(空格),要么是一个 Arrow 对象。方向只用单个字符编码:U/D/L/R 分别对应上/下/左/右,再经 DIRS 字典换算成行列坐标的增量:
DIRS = {
"U": (0, -1), # 行号减小
"D": (0, 1), # 行号增大
"L": (-1, 0), # 列号减小
"R": (1, 0), # 列号增大
}
class Arrow:
def __init__(self, row, col, direction):
self.row = row # 行
self.col = col # 列
self.direction = direction # "U" / "D" / "L" / "R"
每个关卡在 levels.py 里就是一行配置字典,例如第 1 关(4×4):
→ · ↑ ·
· · · ·
↓ · ← ·
· · · ·
3.2 路径检测(核心算法)
整个游戏最关键的判断集中在 Board.can_fly(row, col)。它的思路是沿箭头方向在棋盘内"走一条射线":起点取箭头相邻的下一格(而不是箭头自己),随后一格一格推进,途中碰到任何非空格即立刻返回"被挡";顺利走到棋盘外则返回"可飞":
def can_fly(self, row, col):
arrow = self.get(row, col)
if arrow is None:
return False
dcol, drow = DIRS[arrow.direction]
r, c = row + drow, col + dcol # 从相邻一格开始扫,不看自己
while 0 <= r < self.rows and 0 <= c < self.cols:
if self.grid[r][c] is not None: # 路上还有别的箭头 → 挡住
return False
r += drow
c += dcol
return True # 一路走到边界都空 → 可飞
为什么单方向线性扫描就足够?因为题目只要求考察"同一行/同一列上、箭头与边界之间是否还有别的箭头",不存在需要绕路的场景,所以用不上 BFS/DFS 这类搜索算法。
边界处理细节:循环条件同时约束行、列下标不越界,等于把"走到边界外"统一表达成"循环自然结束"。箭头本来就贴着边、且指向棋盘外时,相邻格直接越界,循环体一次都不执行就返回 True——不会出现下标 -1 之类的数组越界错误。测试 T03 用四方向边缘箭头专门验证了这一点。
3.3 点击与状态流转
Game.click(row, col) 是逻辑层唯一入口,每次点击返回一个事件字典:
- 点到空格 →
{"event": "empty"},不改变任何状态、不扣失误; - 点到可飞箭头 → 该格置空,返回
{"event": "flew"},界面层据此播放飞出动画;若这是本关最后一个箭头则附带cleared标记并切换状态机; - 点到被挡箭头 → 棋盘保持原样,失误次数减 1,返回
{"event": "blocked"},界面层播放晃动动画;失误扣到 0 则进入失败状态。
界面层(ui.py)只负责画图和转发鼠标事件,"能不能飞""是否通关"全部交给 game.py 裁决。此外,动画播放期间界面层会拒绝新的棋盘点击,防止操作与画面状态脱节。
3.4 关卡的可解性验证
关卡定稿前,我没有凭感觉判断"应该能过",而是用贪心求解器 Board.find_solution() 实际解一遍:循环找任意一个当前可飞的箭头消掉,直到棋盘清空或卡死。这里有一个关键的性质支撑贪心的正确性——消除箭头只会减少阻挡,绝不会让原本可飞的箭头变得不可飞,因此"贪心能清空棋盘"与"关卡存在解"是等价的。4 个固定关卡分别能用 4、5、5、6 步清空;随机挑战模式采用"生成 → 求解验证 → 不合格则重试(上限 500 次)"的流程,保证玩家拿到的每一盘都必然可通关。
四、AIGC 使用过程
开发全程借助 Claude Code 辅助编程,下面记录 4 段印象最深的协作。
协作 1:方向向量与边界写法
我最初自己写的路径检测,循环是从箭头所在的格子开始扫的,结果箭头总会把自己误判成障碍,而且箭头指向棋盘外时下标会变成负数报错。AI 看完后给了两个改动建议:起点改为相邻的下一格;用 while 0 <= r < rows and 0 <= c < cols 让越界即结束循环。两个问题一次解决,我随后按它的提示补了 T03 边缘四方向测试用例。
协作 2:让"一次点击"变成"一个事件字典"
刚开始 ui.py 里散布着各种"如果可飞就……否则如果被挡就……"的判断,界面和规则搅在一起。AI 提议把 Game.click 的返回值设计成事件字典(flew / blocked / empty),UI 只根据返回的事件去决定播哪段动画。这样逻辑层变得可以单独断言,测试里直接检查返回字典即可,不用开窗口。
协作 3:为贪心求解器"要一个证明"
设计随机挑战模式时,我担心贪心会不会漏掉某些有解的布局。AI 没有直接说"能行",而是给了一段论证:消掉一个箭头只会让棋盘更宽松,可飞的箭头不会因此被挡住,所以有解的关卡贪心一定走得通,卡死即无解。这个性质也写进了 find_solution 的文档注释,成为整个关卡验证体系的依据。
协作 4:无窗口端到端模拟抓出两个问题
测试全部通过后,AI 又帮我做了一次不开窗口的完整点击流程模拟(菜单 → 开始 → 连续点击 → 通关),结果揪出两个肉眼难以发现的问题:
- 测试用例的操作顺序有误:T06 中我先消掉了右侧的箭头,再点左侧箭头时它已经不再被阻挡,断言自然失败。按 AI 的提醒调整成"先点被挡箭头制造失误、再消除箭头",测试才真正反映设计意图;
- 真实游戏里的一处崩溃:点击"开始游戏"后棋盘布局没有重新计算,第一次点击箭头时
board_rect还是 None。这个 bug 正是模拟到"开始游戏 → 点击箭头"这一步时暴露的,修复后整个流程稳定通过。
说明:AI 生成的代码我没有直接粘贴进项目,而是先弄清每一段在做什么,再自己录入,并补上对应的测试用例。
五、测试结果
运行 python -m unittest test_game -v,共 11 项测试全部通过。作业要求的 T01~T06 对应如下:
| 编号 | 测试内容 | 预期结果 | 实际结果 | 是否通过 |
|---|---|---|---|---|
| T01 | 点击前方无阻挡的箭头 | 箭头飞出棋盘并消失 | 该格被置空,事件为 flew,剩余数减 1 | ✅ 通过 |
| T02 | 点击前方有阻挡的箭头 | 箭头不消失,失误次数减 1 | 箭头保留,事件为 blocked,失误 3→2 | ✅ 通过 |
| T03 | 点击位于边缘且朝向棋盘外的箭头 | 正常飞出,不越界 | 上/下/左/右四方向边缘箭头均可飞,无异常 | ✅ 通过 |
| T04 | 消除本关全部箭头 | 显示通关并进入下一关 | 状态变为 level_clear,进入下一关后布局正常 | ✅ 通过 |
| T05 | 失误次数耗尽 | 显示失败并允许重新开始 | 连续 3 次被挡后状态变为 game_over,重开后失误恢复 3 | ✅ 通过 |
| T06 | 游戏进行中重新开始 | 箭头布局和失误次数恢复 | 布局快照与失误次数完全恢复 | ✅ 通过 |
额外校验:4 个固定关卡分别以 4、5、5、6 步可解,不存在死局;随机生成的 20 个关卡全部可解。各界面(按钮、动画)已通过无窗口截图脚本逐界面实际运行验证。
六、PSP 表格
| 任务 | 预估耗时(小时) | 实际耗时(小时) | 差异(小时) |
|---|---|---|---|
| 需求分析与游戏设计 | 1.0 | 1.5 | +0.5 |
| Python 与图形库学习 | 1.5 | 1.5 | 0 |
| 游戏界面实现 | 2.0 | 2.5 | +0.5 |
| 路径与碰撞逻辑实现 | 1.5 | 1.5 | 0 |
| 关卡设计 | 0.5 | 1.0 | +0.5 |
| AIGC 辅助开发 | 1.0 | 1.0 | 0 |
| 测试与修改 | 1.0 | 1.5 | +0.5 |
| README 与博客撰写 | 1.5 | 2.0 | +0.5 |
| 合计 | 10.0 | 13.0 | +3.0 |
实际用时超出预估,超出的部分主要耗在"Python 3.14 下原版 pygame 装不上、换用 pygame-ce"以及界面细节调优上。
七、心得体会
- 带着具体问题去提问,比"帮我写完整个游戏"更高效:这次最有用的提问都是小切口——边界条件怎么写、点击结果怎么表达、贪心为什么正确。问题拆得越细,AI 的回答越容易核对,也越能转化为自己的理解。
- 先让逻辑可测,再谈界面:把规则层从 pygame 里剥出来之后,改 UI 的时候不再需要每改一版就手动点一遍全流程,命令行一条命令就能回归 T01~T06。端到端模拟还顺手抓出了一个真实崩溃,这是纯靠眼睛试玩发现不了的。
- 对 AI 给出的结论也要追问依据:贪心求解器那一次,我本可以拿了代码就走,但"为什么贪心不会漏解"这个问题换来的是整个关卡验证体系的基石——现在连随机关卡都敢放心交给玩家。
- 环境问题要自己兜底:Python 3.14 与 pygame 的兼容问题是 AI 帮忙定位的,但换库的决策、依赖的锁定(requirements.txt)都是自己完成的,作业环境别人替代不了。
- 不足与改进:目前的动画只有飞出与晃动两种,没有音效;固定关卡仅 4 个,重复游玩价值有限。后续想加入"提示当前哪个箭头能飞"与"撤销上一步"两个功能。
浙公网安备 33010602011771号