2026 秋软件工程个人作业(第二次)
| 这个作业属于哪个课程 | H202601软件工程与软件工程实践 |
|---|---|
| 这个作业要求在哪里 | 2026秋软件工程个人作业(第二次) |
| 这个作业的目标 | 使用 Python 和 AIGC 完成「一箭又一箭」小游戏 |
| 学号 | 102401339 |
| GitHub 仓库 | homework2/ at renbaoshuo/202601-software-engineering-homework |
项目展示
游戏名叫“一箭又一箭”,使用 Python 和 pygame-ce 编写。
视频:https://pan.baidu.com/s/1zt_TC95Yu3E1bh7VoZPnCw?pwd=fzdx
开始界面列出规则,点击“开始游戏”进入第一关。

游戏中,棋盘位于中间,上方显示当前关卡、剩余箭头和剩余失误次数,右上角可以重开本关。下图是第三关的初始布局。

点击被挡住的箭头时,箭头变色并晃动,下方显示阻挡提示,失误次数减一。

清空最后一关后显示全部通关,可以从第一关重玩,也可以返回首页。失误次数用完则显示失败,可以重开当前关卡。


项目介绍
游戏规则
棋盘由网格组成,每格为空,或放置一个朝上、下、左、右的箭头。玩家用鼠标左键点击箭头所在的格子,让箭头沿自身方向离开棋盘。箭头不能旋转,也不能移动到别的格子。
能否飞出,要看箭头前方到棋盘边界之间有没有其他箭头。中间可以隔着空格,只要路径上还有箭头,就不能消除。阻挡箭头朝向哪里不影响判断。
路径畅通时,箭头飞出并消失。路径被挡住时,箭头留在原位,扣除一次失误机会。每关有三次机会,清空棋盘就通关,用完机会就失败。清除一个箭头后,原先被它挡住的箭头可能就有了出路。
界面和功能
界面使用米白色背景、浅绿色格子和深绿色箭头,橙色用于提示,红色用于碰撞和失误提醒。方向由箭头形状表示。中文字体随项目分发,箭头、按钮和棋盘由程序绘制。
开始页说明规则,游戏页显示棋盘和计数,结果页提供下一关、重玩或返回首页的操作。结果页出现后,底层棋盘不再响应点击。
飞出和碰撞都有动画。动画期间,棋盘忽略后续点击,重开和关闭窗口仍然有效。点击空格、棋盘外区域和已消除的位置不会扣次数,右键和长按也不会触发连续点选。
项目内置三个固定关卡:
| 关卡 | 棋盘大小 | 箭头数量 | 设计内容 |
|---|---|---|---|
| 认识阻挡 | 4 × 4 | 8 | 观察边界和箭头之间的阻挡顺序 |
| 交叉观察 | 5 × 5 | 12 | 加入隔着空格的阻挡和跨行列的依赖 |
| 连续解锁 | 6 × 6 | 20 | 连续解除多组阻挡,增加需要观察的箭头数量 |
游戏在本地运行,不需要联网。
实现思路
箭头、方向和关卡的表示
规则代码放在 game.py,关卡放在 levels.py,绘制和鼠标事件放在 ui.py。路径判断不依赖图形库,可以单独运行测试。
棋盘坐标使用 (row, col),从零开始。左上角是 (0, 0),向下增加行号,向右增加列号。每个格子用一个字符表示:U、D、L、R 分别表示上、下、左、右,. 表示空格。
第一关的模板如下:
Level(1, "认识阻挡", ("..RD", "U..D", ".R.D", "L..R"))
Level 保存编号、名称、布局和初始失误次数。模板中的布局是字符串元组,开始游戏和重开时再复制成二维列表:
board = [list(row) for row in level.rows]
消除箭头只修改运行中的棋盘,不修改模板。重开时重新复制,就能恢复初始布局。剩余箭头数直接统计棋盘中非空格子的数量,避免棋盘已经改变、计数却忘记更新。
方向转换为行列增量后,四个方向可以共用一段检测逻辑:
| 方向 | 编码 | 行列增量 (dr, dc) |
|---|---|---|
| 上 | U |
(-1, 0) |
| 下 | D |
(1, 0) |
| 左 | L |
(0, -1) |
| 右 | R |
(0, 1) |
路径检测
路径检测从箭头前方的相邻格开始,沿方向逐格检查到棋盘边界。遇到非空格就返回 False,走出边界仍未遇到箭头就返回 True。
项目中的实现如下:
DIRECTIONS = {"U": (-1, 0), "D": (1, 0), "L": (0, -1), "R": (0, 1)}
def can_exit(board: Sequence[Sequence[str]], row: int, col: int) -> bool:
"""从相邻格检查到边界;无效坐标或空格返回 False。"""
if not board or not 0 <= row < len(board) or not 0 <= col < len(board[0]):
return False
direction = board[row][col]
if direction not in DIRECTIONS:
return False
dr, dc = DIRECTIONS[direction]
row, col = row + dr, col + dc
while 0 <= row < len(board) and 0 <= col < len(board[0]):
if board[row][col] != ".":
return False
row, col = row + dr, col + dc
return True
例如,某一行是 R..U。点击最左侧的 R 时,检测会经过两个空格,再遇到 U,因此判定为阻挡。这里的 U 虽然朝上,但仍占据了向右离开的路径。等它被消除,这一行变成 R...,再次点击 R 就可以飞出。
检测不能只看相邻格,也不能从箭头自身开始,否则会漏掉远处的阻挡,或把自己当成阻挡。另一处要注意的是先检查边界,再读取格子。Python 允许负索引,如果先读取 board[-1],上边界就可能被误读成最后一行。
这个问题只需要沿一条直线检查,不涉及转弯和寻找其他路线。一次检测的时间复杂度为 \(O(\text{max(行数, 列数)})\),额外空间复杂度为 \(O(1)\)。
关卡可解性
验证关卡时,程序复制一份棋盘,找出当前可以飞出的箭头并删除,再继续查找。能清空就说明可解;如果棋盘还有箭头,却找不到任何可以飞出的箭头,就说明存在无法解除的阻挡。
这里可以采用贪心检查,因为删除箭头只会减少阻挡,不会制造新的阻挡。一个原本可以飞出的箭头,不会因为别的箭头被删除而变得不能飞出。因此不需要枚举所有点击顺序。
三个内置关卡都通过了这个检查,PRD 中给出的三组通关顺序也通过了独立测试。这个求解函数用于开发验证,游戏界面没有提供自动求解按钮。
动画和状态切换
程序区分开始、游戏中、动画中、单关通关、失败和全部通关六种状态。同一时刻只处理一个棋盘动作。
成功点击后,程序先播放飞出动画,结束时才把原格子改成 .。如果这时剩余箭头为零,再进入通关状态。碰撞则在点击时扣一次机会,反馈动画结束后判断是否失败。这样可以让计数和结果页面跟随动作变化。
动画按经过的时间更新。飞出动作持续 0.32 秒,碰撞反馈持续 0.26 秒,播放期间不使用休眠阻塞事件循环。重开会丢弃当前动作,再加载本关模板,旧动作不会继续修改新棋盘。
开发时还修复过一个事件顺序问题:动画即将结束时,队列里可能已经有重复点击。如果主循环先结束动画,再处理点击,旧点击就会被当成新操作。现在每帧先按当前状态处理事件,再推进仍然有效的旧动作。遇到重开或新动作时,也不把上一帧的耗时加到新动作上。
AIGC 使用过程
第一次:把作业要求整理成 PRD
我提供作业页面,要求 Codex 读取题目,把用于实现的 PRD 写入 homework2/PRD.md。
Codex 整理了游戏规则、界面、状态、数据结构、关卡和测试要求,并把题目规定与补充约定分开。例如,每关三次失误、动画期间忽略点击、最后一关结束后的按钮行为,都在文档中写明。它还给出三份关卡布局,并用脚本检查通关顺序。

这一轮的产出是一份可以直接交给实现任务的需求文档。关卡解序验证只能说明布局可解,当时还没有游戏程序。
第二次:让 Agent 按 PRD 实现和测试
我的要求是按照 PRD 实现游戏。

主 Agent 负责图形界面、资源和集成,子 Agent 负责关卡、路径检测、状态机和规则测试。之后又有子 Agent 补充鼠标事件和界面流程测试。各部分使用约定好的接口,例如用秒推进动作、动画结束后删除箭头、动画期间忽略棋盘点选。
开发先通过了 12 项关卡和路径测试,接入状态机后有 33 项规则测试,加入第一批界面测试后达到 52 项。代码按计划、路径、状态、界面等阶段提交。
这一轮中,我提供了实现范围、测试要求和提交要求,Agent 负责代码编写、运行命令和集成。
第三次:检查画面并修复遮挡
实际使用过程中,发现第二关和第三关的棋盘会遮住上方的剩余箭头数字。
Agent 调整了状态区,把数字改为横向排列,又加入回归测试:渲染各关的状态文本,检查文字矩形是否与棋盘相交。
发现原有测试能够验证程序绘制时不报错,却没有检查文字是否被遮住。于是让 Agent 补上这项检查,同类布局问题也有了自动化约束,不会再次发生。
第四次:审查动画结束帧的重复点击
使用过程中发现,碰撞动画接近结束时,如果队列里还有重复点击,主循环可能先结束动画,再把点击当作新动作,导致失误次数从 2 变成 1。于是让 Agent 修复。
Agent 先通过真实 run() 事件循环编写回归测试,在修复前复现问题。随后将主循环和截图脚本统一接入 step(events, dt),先处理输入,再推进仍然有效的旧动作。
修复后,飞出和碰撞两类结束帧测试都通过,并补充了首次点击、重开后新动作和退出后的计时检查。最终共有 57 项测试通过。
测试结果
运行环境是 macOS 15.7.9(Apple Silicon)、CPython 3.14.7、pygame-ce 2.5.8 和 SDL 2.32.10。
在 homework2/ 目录下执行:
python -m unittest discover -s tests -v
开发验收时共通过 57 项测试,其中规则与关卡测试 33 项,界面与事件循环测试 24 项。本次整理文章时再次运行,57 项全部通过。下面按 PRD 的测试项目归纳,同一项目可能对应多个测试方法。
| 编号 | 测试项目 | 预期结果 | 实际结果 | 是否通过 |
|---|---|---|---|---|
| T01 | 点击前方无阻挡的箭头 | 飞出后消除,失误次数不变 | 动画结束前保留箭头,结束后只删除一次,数量减一 | 通过 |
| T02 | 点击被阻挡的箭头 | 箭头保留,反馈碰撞,扣一次机会 | 箭头变色、晃动,位置不变,失误次数减一 | 通过 |
| T03 | 点击四条边朝外的箭头 | 正常飞出,不越界 | 四个方向均能消除,没有负索引误读 | 通过 |
| T04 | 清空非末关并进入下一关 | 显示通关,加载下一关初始状态 | 布局、箭头数和失误次数正确 | 通过 |
| T05 | 点击阻挡箭头直到机会用完 | 反馈结束后失败,锁定棋盘,可重开 | 次数降为零后显示失败,重开仍为当前关 | 通过 |
| T06 | 消除箭头、扣次后重开 | 恢复本关布局和计数,清除动作 | 布局、计数和动作均恢复 | 通过 |
| T07 | 四方向的相邻阻挡和隔空格阻挡 | 路径上任何箭头都构成阻挡 | 没有漏查远处箭头,阻挡箭头朝向不影响结果 | 通过 |
| T08 | 在身后或其他行列放置箭头 | 不构成阻挡 | 均未误判 | 通过 |
| T09 | 点击空格、外部、已消除格,或使用右键、长按 | 不改变棋盘和次数 | 无无效扣次或连续动作 | 通过 |
| T10 | 动画中及结束帧处理重复点击 | 只执行首次有效动作 | 修复事件顺序后,没有重复删除或扣次 | 通过 |
| T11 | 两种动画中重开,再点击新局箭头 | 旧动作及旧帧耗时不影响新局 | 新局布局完整,新动作计时正确 | 通过 |
| T12 | 消除阻挡后再点击原箭头 | 按当前棋盘重新判断 | 路径畅通后能够飞出 | 通过 |
| T13 | 验证三关解序、自动通关及本人试玩 | 三关可清空,并完成本人试玩 | 解序、求解检查和窗口自动通关通过;本人试玩也通过 | 自动化通过,本人试玩也通过 |
| T14 | 最后一关通关后重玩或回首页 | 不访问不存在的下一关 | 两个按钮分支均正常 | 通过 |
| T15 | 点击棋盘边界和格子分界线 | 左上边界包含,右下边界不包含,每次最多命中一格 | 命中结果符合约定 | 通过 |
| T16 | 重复重开和切换关卡 | 关卡模板不受运行过程影响 | 每次加载结果一致,模板未被修改 | 通过 |
| T17 | 新环境安装、异目录启动、检查中文和布局 | 依赖与资源完整,文字可见 | 仅安装运行依赖即可启动,字形齐全,状态文字未被棋盘遮挡 | 通过(macOS) |
| T18 | 在各状态和动画中关闭窗口 | 正常退出,不等待动画 | 事件循环停止,显示和字体资源释放 | 通过 |
界面自动化使用 SDL 虚拟显示器。开发验收还在 macOS 原生窗口中通过事件队列完成了三关、失败、重开和返回首页。
PSP 表格
本表沿用开发时的记录,统计 AI 协作经过的时间。预计时间在功能开发前填写,实际时间按主要活动划分并取整到分钟。主 Agent 和子 Agent 有并行工作,重叠时间只计一次。
| 阶段 | 预计耗时(分钟) | 实际耗时(分钟) | 差异(实际 − 预计) |
|---|---|---|---|
| 需求、环境和任务拆分 | 5 | 2 | -3 |
| 规则、关卡与测试;界面并行开发 | 20 | 6 | -14 |
| 集成、自动化验收与画面检查 | 15 | 6 | -9 |
| README、截图和测试记录 | 10 | 3 | -7 |
| 合计 | 50 | 17 | -33 |
记录区间为 2026 年 9 月 13 日 18:50—19:07(UTC+8)。前期 PRD 编写、本人学习、审阅、试玩,以及本篇博客的整理和复测均不在这 17 分钟内。
实际耗时低于预计,主要是规则和界面同时开发,测试使用标准库,画面使用 Pygame 基本图形绘制。集成期间仍花了时间处理计数遮挡和结束帧重复点击。由于没有记录人工独立实现同一项目的用时,这组数字不能直接换算成 AI 带来的效率倍数。
心得体会
这次开发中,我主要做的是给出目标和约束。先让 Codex 根据作业生成 PRD,再补充文字规范、提交格式和测试要求,之后把实现交给 Agent。拆分任务、编写代码、运行测试、修复问题和整理记录,都由 Agent 接着执行。我不需要把每个函数拆成问题,逐段拿到代码后再复制进项目。
这就是我在这次作业中采用的 Agent 驱动开发。人给出方向、范围和完成标准,Agent 组织实现过程。对这个项目来说,我可以把主要精力放在指导上,不必亲手完成每一行代码和每一次命令操作。
指导也需要具体内容。只有「做一个箭头游戏」,很多行为仍然没有确定:动画中能不能继续点击,机会什么时候扣除,重开是否保留当前关卡,最后一关结束后去哪里。这些约定写进 PRD 后,Agent 才有依据实现,测试也有依据判断。我的后续要求同样写回文档,避免只留在某一轮对话里。
并行 Agent 在这次开发中承担了不同工作。规则和关卡可以先实现,不必等待窗口完成;界面依照接口接入;测试再检查两者如何一起运行。主 Agent 负责协调接口和集成。分阶段提交则留下了需求拆分、路径检测、状态机、界面和修复的过程,方便查看某个改动从哪里开始。
AI 生成的实现也出现了问题。大棋盘遮住计数时,已有测试仍然通过,因为它们没有检查文字与棋盘的位置关系。动画结束帧的重复点击也没有被最初的测试覆盖。后来 Agent 继续检查画面和事件循环,才找到并修复这些问题。这让我看到,给 Agent 的任务需要包含运行、检查和修复,不能只停在代码生成。
这两次修复也说明了测试该怎么补。布局问题要检查实际绘制的文字矩形;事件队列问题要经过真实主循环。发现问题后,先让测试复现,再看修复是否使它通过,比单独增加几个正常点击案例更有用。测试数量能说明做了多少检查,具体检查了什么,仍要看测试内容。
人的工作还包括决定是否接受结果。Agent 可以给出三关的解序、自动通关记录和截图,我仍需要理解路径为什么这样判断,查看功能是否符合要求,并补上本人试玩。把代码实现交给 Agent 后,这些判断没有消失。
以后做类似项目,我会继续先明确需求和验收条件,让 Agent 连同实现、测试和问题修复一起完成。反馈时给出操作步骤、预期和实际结果;能写进文档的约定就写进去,能复现的问题就留下测试。这样我可以少花时间传递代码,把时间用在说明需求和判断结果上。

浙公网安备 33010602011771号