软件工程第二次个人作业
| 这个作业属于哪个课程 | 福州大学 202601 软件工程 |
|---|---|
| 这个作业要求在哪里 | 软件工程课程第二次个人作业 |
| 这个作业的目标 | 借助 AIGC 完成 Python 单格箭头小游戏,理解路径检测与状态管理,通过本人试玩和本机测试验证功能,并记录开发、交付和时间投入。 |
| 学号 | 102401318 |
| GitHub 仓库 | Mortal810 / arrow-exit-lab |
一箭又一箭·向外:让箭头飞出屏幕
这次作业看起来只需要完成一件事:点击箭头,让它飞出棋盘。但真正做起来,箭头前面有没有障碍、动画没结束又点了一次怎么办、重开会不会留下旧状态,都是绕不过去的问题。把程序传到 GitHub,也不是复制一条命令就一定顺利。
我把游戏命名为 《一箭又一箭·向外》。没有先给它塞满新玩法,而是保留单格、四方向、三次机会的规则,把“能运行”继续推进到“能验证”。基础实现由 ChatGPT 辅助完成;Windows 运行、T01—T06 手工测试、四关试玩、本机自动化测试和截图录屏由我完成,下面分别展示。
证据说明:游戏展示图全部取自本人实验过程截图,动态片段来自本人录屏。原项目的 AI 自动化演示图没有用作本人试玩证据。自动化测试报告另列一节,不和手工测试混在一起。
一、小程序总览
| 开始界面 | 第一关游戏界面 |
|---|---|
![]() |
![]() |
图 1—2:左侧是开始页,右侧是第一关。棋盘和状态区分开,当前关卡、剩余箭头、失误机会、计时、提示与重开操作都能直接找到。
下面这段动图包含碰撞、重开和随后完成第一关的过程:

图 3:由本人 测试1.mp4 原速转成 GIF,仅等比例缩小并降低显示帧率,没有重新生成游戏画面。最终数值以原始录像与截图为准。
完整录像也保留在材料中,点击可以查看或下载:
| 本人原始录像 | 时长(约) | 主要记录 |
|---|---|---|
| 测试1 | 8.896 秒 | 非初始局面的碰撞、重开,以及随后第一关通关 |
| 碰撞动画 | 5.781 秒 | 连续触发碰撞、机会耗尽、失败后重开 |
| 进度保存与窗口缩放 | 16.064 秒 | 退出、重新启动、历史成绩与窗口变化;也包含仓库页面 |
视频时长是文件时长,不是开发工时。GIF 是便于阅读的预览,原始 MP4 没有替换或覆盖。
二、边界明确
2.1 基础玩法与功能范围
棋盘中每支箭头只占一格,方向是上、下、左、右。左键点击后,从箭头前方一格开始,沿同一行或同一列一直检查到边界:路径为空就飞出并消除;路径上存在箭头就保留原位、给出碰撞反馈,并扣一次机会。隔着空格仍可能被挡,阻挡者朝哪边不影响它占据这格。
每关有 3 次失误机会。清空后进入通关页,再点击下一关;机会耗尽则失败,可以重开。空格、棋盘外区域与右键不是有效消除动作。动画进行中不接受新的棋盘动作,但仍可以重开或返回。
| 关卡 | 棋盘 | 初始箭头数 | 设计重点 |
|---|---|---|---|
| 1:向外一步 | 5×5 | 10 | 四方向、边界与基本阻挡 |
| 2:隔空相遇 | 6×6 | 18 | 不只看相邻格,要看完整路径 |
| 3:层层解围 | 7×7 | 28 | 逐步解除交错方向的依赖 |
| 4:有序远行 | 8×8 | 40 | 更密的布局,不额外改变基础规则 |
在基础玩法上,项目保留了四组扩展:第四关与解锁选关、解释性提示、计时与星级、本地历史成绩。
2.2 开发环境与运行入口
本人两轮测试报告记录的环境均为 Windows 11、Python 3.13.13、Tk 8.6。游戏使用 Python 标准库和 Tkinter,运行时不需要安装第三方运行库。
cd "D:\软件工程作业\arrow_lab"
py -3.13 -m tkinter
# 关闭 Tk 检查窗口后启动游戏
py -3.13 app.py
H 请求提示,R 重开,Esc 返回选关;默认进度保存在 %LOCALAPPDATA%\ArrowExitLab\progress.json,也可以用 --save 指定独立验收存档。
环境记录截图

Tk 检查截图

保留了实际执行过程。运行说明的重点不是要求所有人使用同一个磁盘,而是先进入包含 app.py 和 resources 的项目根目录。
三、代码解释
3.1 先分开规则和画面
app.py 程序入口
arrowgame/core.py 棋盘、方向、路径判断与游戏状态
arrowgame/levels.py 关卡加载及合法性、可解性检查
arrowgame/ui.py Tk 界面、输入、动画与坐标换算
arrowgame/storage.py 解锁进度和历史成绩
resources/levels.json 四个固定关卡及验证数据
tests/ 核心规则、关卡、存档和界面测试
这里的关键是 core.py 不依赖 Tk。界面把鼠标操作交给规则层,再绘制结果;测试也调用同一套规则,而不是另外写一份“专门让测试通过”的游戏逻辑。
3.2 箭头如何表示:位置和方向分开
第一关的实际模板是:
rows = [
".RR.D",
"R..D.",
"....L",
"...L.",
"R.UR."
]
DELTAS = {"U": (-1, 0), "D": (1, 0), "L": (0, -1), "R": (0, 1)}
. 表示空格。运行时,Board 把非空位置保存成 (row, col) → 方向 的字典。行号向下增加,列号向右增加,四个方向就能统一为坐标增量。界面从第 1 行、第 1 列开始标号,代码内部则从 0 开始。
剩余箭头数直接取当前占用字典的长度,避免在删除箭头之外再维护一份容易不同步的计数。
3.3 路径检测:相邻格为空,真的不代表能出去
当前源码的关键部分如下:
def ray(self, cell):
direction = self._cells.get(cell)
if direction is None:
return
dr, dc = DELTAS[direction]
r, c = cell[0] + dr, cell[1] + dc
while self.inside(r, c):
yield r, c
r, c = r + dr, c + dc
def first_blocker(self, cell):
return next((pos for pos in self.ray(cell)
if pos in self._cells), None)
这里我抓住三个要点:不检查自己、逐格走到边界、只看是否占用。先判断坐标是否在棋盘内,再处理该格;边缘朝外箭头的扫描路径可以为空,这时自然没有阻挡。
例如第一关第 1 行第 2 列朝右,下一格第 1 行第 3 列已经有箭头,因此不能消除。第 3 行第 5 列朝左时,左边的路径为空,就可以飞出。这两种情况分别对应我的 T02 和 T01。
如果换成 → · · ↑ ·,只检查紧挨着箭头的一格就会漏判;沿路径一直扫描才能发现远处的占用。单次判断只扫描一条行或列,不需要把全棋盘每一格都检查一遍。
3.4 动画不是装饰,它也影响状态正确性
Game.click() 在处理动作之前有一道检查:
if self.screen is not Screen.PLAYING or self.pending is not None:
return "ignored"
pending 表示尚未完成的飞出或碰撞动作。成功点击先建立飞出动作,动画完成后才删除箭头;碰撞则在点击时扣一次机会,动画结束后保留箭头。最后一次机会用完,也是在碰撞反馈结束后进入失败页。
有效点击 → 检查路径 → 建立 pending → 播放动画
↓
飞出:删除一次;碰撞:保留箭头
↓
清空 pending,检查通关/失败
因此,同一段动画内的连点不会重复扣机会或删除箭头。这里不能扩大成“任何连续点击都只算一次”:动画结束后,下一次点击本来就应该作为新动作处理。
3.5 重开、求解和缩放,分别解决什么问题
重开不是往旧棋盘里补箭头。restart() 调用 start(self.level_index),从关卡模板创建新棋盘,同时重置机会、提示、计时和 pending。旧动画失去动作对象,就不会继续污染新局面。
提示不是联网 AI。它从 board.available() 中选择当前可飞出的箭头,画出高亮和到边界的虚线路径,不替玩家点击。相同提示仍显示时不会重复扣数;每局最多使用 3 次。
关卡求解利用的是当前规则的单调性。箭头只会被删除,不会新增障碍,所以删除一个合法箭头不会破坏其他可用动作。复制棋盘后不断移除可飞出的箭头,能给出解序;棋盘非空但没有候选动作时,则可判定当前布局无解。这是静态单格规则下的结论,不直接适用于会移动障碍的复杂玩法。
缩放必须兼顾点击。画面缩放后,鼠标坐标也需要通过 (实际坐标-偏移) / 缩放比例 转回逻辑坐标,再映射成行列,否则会出现“看着点中,实际选错格子”。
存档只保存解锁和历史成绩。最高星级与最短用时分别更新,可能来自不同局次;半途棋盘不会保存。这一点在判断保存功能是否正常时要先说明白。
对应源码:core.py · ui.py · storage.py。以上解释以本次被测代码为准。
四、AIGC 协作记录
我使用的是 ChatGPT。最初的要求是完成基础功能、在正确性有保障后适当扩展,并提供测试与记录;随后围绕本机运行、GitHub 发布和实验采证继续提问。
| 子任务 | 我提出的要求或问题 | AI 完成了什么 | 实际效果 | 本人判断与操作 |
|---|---|---|---|---|
| 基础规则与状态机 | 四方向、完整路径、碰撞扣机会、重开必须先正确 | 拆分 Board/Game,实现射线扫描、动作管理和核心测试 |
开发阶段首轮 35 项测试通过 | 在 Windows 手工操作 T01、T02、T05、T06,并保存数值变化与录像;未把 AI 源码说成独立手写 |
| 关卡与递进检查 | 至少 3 个可通关关卡,扩展不能牺牲基础规则 | 构造 4 关、保存解序并验证 | 首轮 40 项中 1 项递进断言失败,修正后 40 项通过 | 本人完成四关;代码中的修复由 AI 完成,不写成我手工重新设计四关 |
| 界面与反馈 | 开始、游戏、结果界面完整,飞出和碰撞要清楚 | 实现 Tk 界面、动画、提示、星级及存档,加入界面回归测试 | 开发阶段修正列号重叠和失败页文案 | 用本人截图与三段录屏检查实际显示和交互,接受这套较简洁的布局 |
| 本机与 GitHub 排错 | 目录命令、远程地址和首次 Push 出错 | 区分路径、Git 配置、身份验证与网络连接 | 本地基线提交和仓库上传完成 | 本人执行命令、反馈截图;不把连接失败写成游戏算法 Bug |
| 实验记录与文档整理 | 哪些证据属于本人,如何组织测试与时间投入 | 提供验收方法、证据分类、代码说明和 Markdown 整理 | 形成手工证据、两轮 Windows 报告及本篇说明 | 保留原始截图、视频和报告;明确 PSP 为估计,不伪造工时 |
4.1 一次有实际失败记录的改动:箭头多,不等于依赖更深
关卡开发记录里,最初四关的依赖层数是 5、9、14、13。第四关虽然箭头更多,但没有满足设计时的递进条件,因此对应测试失败。AI 将下一关的最低依赖层数约束改成:
required_depth = max(depth, levels[-1]["layers"] + 2 if levels else depth)
复测后层数为 5、9、14、25。这次修复针对的是递进指标,不是把一个无解关卡改成有解关卡。层数也不是玩家主观难度的分数,不能仅凭它得出“第四关一定更有趣”。
这组材料让我更容易分清“程序在验证什么”,而不是只看末尾有没有一个 OK。
4.2 一次界面修正:规则正确,不代表画面已经合格
开发记录还保存了列号与关卡说明重叠的问题。AI 将棋盘边长由 500 调整为 480,中心纵坐标由 414 调整为 434,并增加布局回归测试。失败页也把含义容易混淆的文字改为“剩余机会 0 / 3”。
这些修正由 AI 在开发阶段完成,我后续做的是本机验证。
4.3 Git 排错带来的另一个提醒
发布过程里出现过错误的 /tree/ 远程地址、重复添加 origin,以及连接 GitHub 443 端口失败。这些问题属于不同层次:有的是配置,有的是网络,不能用“重装一遍”统一解决。
这次也让我注意到,AI 给出的操作方案需要评估影响。例如删除 .git 会移除本地历史,不能当作修正远程地址的常规动作;处理局部配置时,应优先考虑局部修改。遇到报错,先保留输出、判断原因,比盲目重复执行一整串命令更有用。
五、测试结果:本人操作和自动化报告分成独立两个部分
5.1 T01—T06:本人手工验收
下表使用本人实际截图中的数值,不继续套用操作模板里的示例。单张截图只证明当时的状态,连续变化结合原始录像判断;不同重复实验的画面没有拼成一条虚假的时间线。
| 编号 | 实际操作 | 预期结果 | 本人实际结果 | 结论与证据 |
|---|---|---|---|---|
| T01 | 第一关点击第 3 行第 5 列向左箭头 | 飞出消失,不扣机会 | 箭头 10→9,机会保持 3 | 通过;前 / 后![]() |
| T02 | 重开后点击第 1 行第 2 列向右箭头 | 被第 1 行第 3 列阻挡,保留箭头并扣一次机会 | 箭头保持 10,机会 3→2,出现碰撞提示 | 通过;前 / 后![]() |
| T03 | 第二关点击第 6 行第 1 列向左箭头 | 边缘朝外正常飞出,不越界 | 箭头 18→17,机会保持 3,程序继续运行 | 通过;前 / 后![]() |
| T04 | 本人清空第一关,点击进入下一关 | 显示通关并初始化第二关 | 第一关箭头为 0;第二关显示 18 支箭头、3 次机会 | 通过;通关 下一关![]() |
| T05 | 重开后分别触发三次碰撞,每次等待反馈结束 | 机会耗尽后失败,可以重开 | 失败页机会 0、箭头仍为 10;重开恢复 3 次机会 | 通过;失败 / 重开 另有碰撞录像 |
| T06 | 在已改变的第一关局面点击重开 | 恢复布局、机会和计时 | 箭头 8→10,机会 1→3;画面时间 3.5→0.3 秒,提示保持初始 3 次 | 通过;前 / 后![]() |
T03 的这组手工图验证的是左边缘,四方向边界由自动化测试另行覆盖。T06 重开前后提示均为 3,不能据此单独声称“手工验证了已经消耗的提示恢复”;相关重置逻辑还有状态测试补充。
| 碰撞反馈 | 机会耗尽后的失败界面 |
|---|---|
![]() |
![]() |
图 4—5:被点击箭头变红,阻挡位置被框出;机会用完后可以重开。
| 重开前:8 支箭头、1 次机会 | 重开后:10 支箭头、3 次机会 |
|---|---|
| T06重开前 | |
![]() |
T06重开后![]() |
图 6—7:先把局面改变,再检查重开,比在初始状态反复按按钮更有说明力。截图中的系统通知按原图保留,没有改写游戏数据。
5.2 四关都由本人实际完成
| 关卡 | 对应结算图中的用时 | 剩余机会 | 提示使用 | 剩余箭头 | 结果 |
|---|---|---|---|---|---|
| 向外一步 | 5.7 秒 | 3 | 0 | 0 | 通关,3 星 |
| 隔空相遇 | 9.9 秒 | 3 | 0 | 0 | 通关,3 星 |
| 层层解围 | 12.4 秒 | 3 | 0 | 0 | 通关,3 星 |
| 有序远行 | 21.0 秒 | 3 | 0 | 0 | 全部完成,3 星 |
| 第一关 | 第二关 |
|---|---|
![]() |
![]() |
| 第三关 | 第四关 |
![]() |
![]() |
图 8—11:四关结算记录。表内数字仅对应这些截图中的局次,不代表首次尝试、所有重试或平均水平,也不据此判断程序性能。提示测试与通关截图可以来自不同局次。
游戏计时会包含动画与切换到其他窗口的时间,因此它不能替代个人 PSP。
5.3 两轮 Windows 自动化测试
我在自己的 Windows 上运行:
py -3.13 tools\run_tests.py --output evidence\windows_verified
第二轮使用独立的 evidence/student_test_runs/20260922_193137 目录,避免把两轮结果混为一份。表中时间由报告的 UTC 时间换算为北京时间(UTC+8)。
| 轮次 | 报告生成时间 | 运行范围 | 通过/失败/错误/跳过 | 脚本用时 |
|---|---|---|---|---|
| 第一轮 | 2026-09-21 20:22:49 | core + Tk GUI | 66 / 0 / 0 / 0 | 4.942 秒 |
| 第二轮 | 2026-09-22 19:32:17 | core + Tk GUI | 66 / 0 / 0 / 0 | 11.425 秒 |
Windows第二轮自动化测试终端:

图 12:本人在 Windows 执行测试脚本的终端。它是自动化测试的运行记录,不是 66 次手工点击记录。
测试构成为:核心规则与状态机 35 项、关卡 5 项、存档 10 项、Tk 界面 16 项,共 66 项。625 个小棋盘与 500 个矩形棋盘属于部分测试方法内部的子场景,没有重复计成额外测试项。两轮执行时间不同,也不能在没有控制条件的情况下解读成性能退化或优化效果。
第二轮记录的被测提交为 1e8ed48d6aafe249c5b95709dfe05fc534bb809a。本次整理时,报告列出的 12 个源码、测试及关卡文件的 SHA-256 均与附件一致;新增博客和文档不改变这些被测文件。这个核对只能支持相应文件版本的一致性,不是额外生成的一轮游戏测试。
六、附加功能
| 功能 | 当前实现 | 材料与验证范围 |
|---|---|---|
| 第四关与选关 | 通关逐步解锁,已解锁关卡可再次挑战 | 四关结算图与选关页显示四关成绩;初始锁定规则另有自动化测试 |
| 解释性提示 | 高亮一个当前可消除箭头,画出出口路径,不自动消除 | 提示截图显示虚线路径与剩余次数;不把两张不同局面当作连续动作 |
| 计时与星级 | 通关 1 星、零失误再 1 星、未用提示再 1 星,无速度惩罚 | 四张结果图显示时间、机会、提示和星级;具体规则见 Game.stars |
| 本地历史 | 保存解锁、最高星级及最短时间,不保存半途棋盘 | 本人录像包含关闭、重新启动、再次查看历史成绩 |
| 窗口缩放 | 绘制和点击使用统一逻辑坐标 | 本人录像展示窗口变化;缩放后坐标映射另由自动化测试补充 |
| 提示画面 | 四关成绩与再次进入入口 |
|---|---|
![]() |
![]() |
图 13—14:提示只说明出口,不替我完成操作。选关页保留的是历史最佳值,不要求与下一次游玩的实时值相同。
我更愿意把这些扩展理解为对基础体验的补充:卡住时知道原因,通关后有反馈,重新打开还有记录。没有为了列出更多功能而改掉作业的核心规则。
七、GitHub 与交付
仓库地址是 Mortal810/arrow-exit-lab,源码、关卡和运行说明都围绕这个项目组织。
GitHub仓库页面:

附件中可核对的历史提交是 1e8ed48:chore: import AI-assisted baseline for personal review。截图、录屏和第二轮报告中有一部分是在这个提交之后取得的,因此本次将它们与完成的说明一起纳入一次真正新增的本地资料提交。新增资料的提交由 ChatGPT 在附件副本上执行,属于本轮 AI 辅助整理,不冒充本人过去手工编码。
README 同步改为本人 Windows 实测环境、真实截图、测试结果与代码入口,并保留 AI 辅助来源。GitHub 在线内容以实际推送结果为准。
八、PSP表格
下面保留原记录的参考预计,总计 11.50 小时。原预计在过程中保存,不证明每一阶段都做过严格的事前预测。差异只用于讨论投入分配。
| 任务 | 参考预计/小时 | 已完成耗时/小时(理论估计) | 差异/小时 |
|---|---|---|---|
| 需求分析与游戏设计 | 1.00 | 1.00 | 0.00 |
| Python 与图形库学习 | 1.00 | 1.25 | +0.25 |
| 游戏界面实现 | 2.00 | 1.00 | -1.00 |
| 路径与碰撞逻辑实现 | 2.00 | 1.00 | -1.00 |
| 关卡设计 | 1.00 | 1.25 | +0.25 |
| AIGC 辅助开发 | 1.00 | 1.75 | +0.75 |
| 测试与修改 | 2.00 | 3.50 | +1.50 |
| README 与博客撰写 | 1.50 | 1.00 | -0.50 |
| 合计 | 11.50 | 11.75 | +0.25 |
因此,已完成工作估计 11.75 小时;后续发布计划 0.50 小时;预计结项 12.25 小时。
这张表最值得讨论的不是少了零点几小时,而是投入结构:AI 缩短了从零搭建的工作,测试、采证和排错却不会自动消失。自动化脚本只运行数秒,准备条件、观察结果和整理证据的过程远不止数秒。
九、心得体会
第一,不要把“前面一格是空的”当成“前面没有障碍”。它既是路径检测的问题,也像做项目时容易出现的错觉:窗口打开不等于流程完整,代码提交也不等于资料交付完毕。
第二,AI 的价值不只在于生成代码。模块拆分、边界案例、错误定位和测试方法,同样能减少试错。但生成结果需要被检查;AI 完成的修复、本人执行的验证、后来整理的文字,应当分别说清楚。
第三,证据最好跟着实验走。这次保留的前后截图、失败页、通关页、原始录像和两轮测试报告,让最终总结可以回到具体事实。比起一句“全部正常”,我现在更愿意写清楚“做了什么、看到了什么、证据在哪里”。
《向外》没有很多复杂机制,但从规则到界面、从本机到仓库、从结果到记录,已经让我看到一份小软件作业应该怎样被解释和核对。下一步值得继续学习的,是更早记录时间、在真正的增量修改后提交,以及让文档与代码版本同步,而不是单纯追求功能数量。
参考与说明
源码基线、开发阶段记录由 ChatGPT 辅助完成;操作证据来自本次提交的截图、录屏和 Windows 报告。本篇不使用原商业游戏的代码、关卡、美术或音效,也不把自动化演示标记去掉后作为本人证据。


/ 后
/ 后
下一关
/ 重开
另有碰撞录像
/ 后






浙公网安备 33010602011771号