软工第二次作业
软工第二次个人作业:使用 Python 与 AIGC 实现“一箭又一箭”
| 项目内容 | 内容 |
|---|---|
| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/fzu/202601SofwareEngineering |
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/fzu/202601SofwareEngineering/homework/16717 |
| 这个作业的目标 | 使用 Python 和 AIGC 完成“一箭又一箭”小游戏 |
| 学号 | 102401234 |
| GitHub 仓库 | cht-hyp/arrow-puzzle-game |
一、项目展示
游戏演示视频
开始界面:

游戏过程:

提示高亮:

单关通关:

全部通关:

失败界面:

二、项目介绍
项目名为“一箭一箭又一箭”,使用 Python 与 Pygame 开发。每个箭头占据一个网格,方向为上、下、左或右。点击后检查箭头前方到边界之间是否有其他箭头;没有阻挡就沿方向飞出,有阻挡则回弹、变红并消耗一次失误机会。
目标是清空当前关卡。三个经典关卡分别有 9、16、25 个箭头,棋盘为 3×3、4×4、5×5;同时提供进阶折线模式和可解随机挑战。每关初始有 3 次机会,耗尽后失败;可随时重新开始当前关。清空后显示通关,点击“下一关”继续,第三关结束显示全部通关。
安装和启动命令见 GitHub 仓库的
README。环境为 Windows、Python 3.12.14、Pygame 2.6.1。
三、实现思路
本地项目主要结构
arrow-puzzle-game/
├─ main.py # 游戏启动入口
│
├─ game/
│ ├─ game.py # 主循环、页面、输入与动画调度
│ ├─ arrow.py # 箭头数据结构与方向定义
│ ├─ board.py # 基础棋盘和路径检测
│ ├─ session.py # 基础模式操作与胜负状态
│ ├─ levels.py # 基础模式固定关卡
│ ├─ solver.py # 基础模式求解器
│ │
│ ├─ lines.py # 进阶线条及棋盘结构
│ ├─ advanced_session.py # 进阶模式操作逻辑
│ ├─ advanced_levels.py # 进阶模式固定关卡
│ ├─ advanced_solver.py # 进阶模式求解器
│ ├─ snake.py # 进阶线条飞出动画
│ │
│ ├─ random_levels.py # 可解随机关卡生成
│ ├─ history.py # 撤销快照与历史记录
│ ├─ campaign.py # 关卡、模式及重开流程
│ ├─ animation.py # 基础箭头飞出与受阻动画
│ ├─ progress.py # 游戏成绩与进度保存
│ ├─ modes.py # 游戏模式定义
│ ├─ constants.py # 窗口、状态等常量
│ ├─ theme.py # 界面配色与样式
│ └─ ui.py # 图标和通用界面绘制
│
├─ tests/
│ ├─ test_logic.py # 基础规则与路径检测测试
│ ├─ test_solver.py # 基础求解器测试
│ ├─ test_levels.py # 固定关卡可解性测试
│ ├─ test_random_levels.py # 随机关卡生成测试
│ ├─ test_advanced_play.py # 进阶模式操作测试
│ ├─ test_advanced_solver.py # 进阶求解器测试
│ ├─ test_animation.py # 飞出与受阻动画测试
│ ├─ test_ext_hint.py # 提示功能测试
│ ├─ test_ext_undo.py # 手动、自动操作撤销测试
│ ├─ test_auto_solve.py # 自动演示测试
│ └─ test_regression.py # 游戏流程回归测试
│
├─ tools/
│ ├─ final_check.py # 最终交付检查脚本
│ └─ capture_ui.py # 游戏界面截图工具
│
├─ README.md # 项目介绍与运行说明
├─ BLOG.md # 课程作业博客
└─ requirements.txt # Python 运行依赖
四方向路径检测
先确认箭头仍在棋盘上,再根据方向取步长 (dr, dc)。从箭头前方的下一格开始,沿当前方向逐格检查;遇到任何箭头即判定为被阻挡,阻挡箭头自身的方向不影响判断。
以下为 game/board.py 中选定方向步长后的核心循环:
row, col = arrow.row + dr, arrow.col + dc
while 0 <= row < self.rows and 0 <= col < self.cols:
if self.get_arrow(row, col) is not None:
return False
row += dr
col += dc
return True
边缘朝外时下一格已经越界,循环不执行,直接允许消除。检测过程不修改棋盘,实际移除和扣机会由会话负责。
关卡与求解
三个固定模板每次加载都生成独立棋盘,重开不会改变模板。求解器复制棋盘,按坐标顺序查找当前可以飞出的箭头并删除,直到清空;若仍有箭头但无合法操作则返回 None。
由于删除箭头只会减少阻挡,不会破坏其他合法删除,所以逐个合法消除即可,没有使用 DFS 或回溯。求解器既用于验证关卡可解性,也被提示功能和自动演示功能复用。程序实际返回的固定关卡和随机关卡均经过求解器验证可解。
动画与状态
逻辑选择会立即更新棋盘状态,动画保存视觉副本,不改变逻辑坐标。飞出速度会随窗口尺寸等比调整,箭头完全离开窗口后动画才结束;被阻挡时,箭头会前移、回弹、晃动并变红约 320ms,不显示额外的文字提示。
成功飞行时允许继续点击其他箭头;同一正在晃动的箭头短暂防连点,防止重复扣机会。最后一箭或最后一次失误发生后,逻辑已结束,界面等待现存动画播放完成再显示结果。重新开始会清理动画和提示。
扩展功能:提示与撤销
提示没有另写一套规则,而是直接复用已有的求解器,对当前剩余棋盘调用 solve(board),取解序列的第一步,用黄色描边加浅黄底色高亮该箭头,1 秒后自动恢复。
solution = solve(self.campaign.session.board) # 求解当前棋盘,不是关卡初始布局
if not solution: # 空棋盘为 [],死锁为 None
return False
self.hinted_position = solution[0] # 只取第一步
self.hint_started_at = time.monotonic() # 主循环计时,不用 time.sleep
求解器原本就接受任意 Board 并在内部复制,因此不需要改动求解器本身。提示是纯提示:调用前后棋盘和失误次数完全不变,也不写入撤销历史。动画进行中或非游戏状态不响应,棋盘一旦变化就清除旧高亮。
撤销使用轻量快照实现。每次成功消除箭头前保存当前棋盘状态;手动消除和自动消除都会记录,被阻挡的点击不写入撤销历史。
@dataclass(frozen=True)
class GameSnapshot:
board: Board
remaining_mistakes: int
快照必须和正在变化的棋盘解耦,否则所有历史都会变成当前状态。由于箭头是不可变对象,这里只需要复制棋盘字典并深拷贝一次:
def capture(session): # 保存
return GameSnapshot(session.board.copy(), session.remaining_mistakes)
def restore(session, snapshot): # 撤销逻辑的简化示意
session.board.arrows = deepcopy(snapshot.board.arrows)
if session.remaining_mistakes == 0:
session.state = GameState.LOSE
elif session.remaining_arrows == 0:
session.state = GameState.WIN
else:
session.state = GameState.PLAYING
撤销支持连续多步撤销,直到历史为空。Restart、Next Level、Retry 都调用同一个 start_level(),在那里统一清空历史,确保新关卡不会撤销回上一关。动画进行中不执行撤销,避免撤销半完成的动画;空历史时按钮置灰。每次撤销只恢复最近一次成功消除,手动操作和自动操作都包含在内。被阻挡的点击不计入撤销步骤,已经扣除的失误机会也不会返还。自动演示中途停止并等待当前动画结束后,可以逐步撤销已经执行的自动操作。
界面布局
窗口默认 504×896,并且可以自由拖动缩放;拖动时锁定 9:16 比例,棋盘、按钮、字号与飞行速度都从当前窗口尺寸实时算出,所以放大缩小后布局比例不变。棋盘放在 HUD 与按钮行之间居中。
界面尺寸一开始是写成常量的,窗口一动就对不上;改成“以 800×600 为基准、每次绘制按当前窗口重算”之后,同一个缩放比例同时服务棋盘、按钮和文字。飞行速度也必须随窗口等比缩放:窗口变小后如果仍用固定像素速度,箭头会在还没飞出棋盘时动画就结束。棋盘位置则从“紧贴 HUD 下方”改为在 HUD 与按钮之间的区域居中,视觉上更平衡。
四、AIGC 使用过程
| 子任务 | 我的要求 | AIGC 工具及完成内容 | 实际效果 | 人工参与与修改 |
|---|---|---|---|---|
| 核心路径检测 | 根据作业要求,将箭头限定为单格对象,点击后沿箭头方向检查同一行或同一列的阻挡情况,并将核心规则与 GUI 分离 | Codex 实现四方向路径检查、GameSession 和对应自动化测试 | 测试从 5 项增至 20 项并全部通过,覆盖边界、跨空格阻挡、重复点击和终局保护 | 我先明确了游戏规则和模块分工,要求 AI 不在鼠标事件中重复实现路径检测,并重点检查四方向、边界和重复点击等情况。代码由 AI 编写,我通过阶段报告和测试结果检查实现是否符合原定规则。 |
| 关卡设计与求解 | 至少设计三个可通关关卡,并增加自动求解器,避免仅凭直觉设计关卡 | Codex 创建三个固定关卡、Campaign 和逐步合法消除的求解器 | 33 项测试通过,三关答案均可回放;第一版布局即通过可解性验证 | 我确定了关卡数量、难度递增和可解性要求,提出利用求解器验证完整通关顺序,并要求保存解序列供后续测试。 |
| 连续点击与碰撞反馈改进 | 实际运行游戏后,希望飞出动画期间可以继续点击其他箭头,同时避免同一箭头重复触发;被阻挡时应有足够明显的反馈 | Codex 将动画改为列表管理,保留同箭头防连点,并将文字提示改为独立计时 2 秒 | 测试从 46 项增至 51 项并全部通过,覆盖并行动画、提示刷新和重开清理 | 我通过实际操作发现原有输入锁限制了连续点击游玩,因此主动调整交互要求。后续观察到碰撞文字与 HUD 重叠,又要求删除文字提示,改用箭头或线条变红、晃动反馈。 |
| 项目回归测试 | 基础功能完成后,要求对完整游戏进行测试,检查关卡、状态切换、动画及异常操作 | Codex 补充答案回放、三关反复重开、HUD 和七类场景退出测试 | 55 项全部通过,三关可解,依赖检查正常;本轮没有修改游戏核心实现 | 我将测试独立划分为一个开发阶段,要求覆盖作业规定的 T01~T06,并增加连续点击、动画期间操作和关卡切换等检查。通过测试报告确认基础功能后,再决定进入扩展开发,而不是继续堆叠新功能。 |
| 扩展:提示与撤销 | 在基础版之上增加 提示 和 撤销,提示 必须基于当前剩余棋盘,撤销 应撤销最近一次引起棋盘变化的操作,支持手动和自动消除,被阻挡的点击不计入,同时不破坏原有玩法 | DeepSeek Harness 复用现有 Solver 实现一步提示,利用独立快照实现多步撤销,并调整窗口布局,codex对撤销过程做了进一步规范 | 测试从 55 项增至 87 项并全部通过;后续 UI 美化与扩展回归后,211 项测试、199 个子测试全部通过。原有 Solver 接口未改动 | 我明确了 提示 只提示、不自动操作,以及 撤销 应恢复哪些状态;要求 AI 复用已有模块,不重新设计规则;我实测发现撤销功能存在的问题,并要求AI进行相应修正 |
| 进阶模式与动画改进 | 在经典模式之外增加带折线的进阶模式,箭头后可连接任意长度、允许多次转折的线条,并尽量铺满棋盘 | Codex 实现 LinePiece、AdvancedBoard、进阶路径检测、退出动画及对应求解功能 | 进阶模式能够显示直线和折线,并沿箭头方向退出;早期版本存在线条过粗、运动不自然和闪烁等问题,后续进行了针对性修改 | 我参考原游戏规则,纠正了 AI 最初将整条折线作为刚体平移的错误理解,明确要求箭头向前移动、后续线段依次跟随。实际观察动画后,又提出更简单的“头部增长一格、尾部缩短一格”方案,要求中间折线保持稳定,并将线条改为细线、小箭头,减少闪烁和复杂变形。 |
| 游戏界面与交互优化 | 在功能完整的基础上改善界面布局,增加暂停和页面导航,使整体风格简洁、Q 版、色彩柔和且统一 | Codex 实现暂停菜单、返回选关与主菜单等交互;ChatGPT 根据运行截图分析布局问题,提供 UI 设计建议和美术素材参考(生成素材图片) | 游戏能够显示暂停覆盖层并提供继续、重开和返回操作;原界面存在按钮过长、留白过多、文字拥挤以及暂停后底层仍有 hover 反馈等问题 | 我多次提供实际运行截图,指出具体视觉和交互问题。随后明确了暂停时底层不能响应鼠标、选关页应使用独立关卡卡片、游戏界面应划分顶部信息区、中央棋盘区和底部操作区等要求,并借助 ChatGPT 探索统一的 Q 版视觉风格;通过实际界面截图检查提示高亮、按钮状态和布局,并提出窗口尺寸、棋盘位置等调整意见。 |
示例一
对箭头点击流畅度的优化、对提示优化

示例二
对飞行动画的调整

示例三
对UI美术的调整

五、测试结果
| 编号 | 测试内容 | 预期结果 | 实际结果 | 是否通过 |
|---|---|---|---|---|
| T01 | 点击前方无阻挡箭头 | 飞出并消失 | 逻辑移除且不扣机会;四方向动画与出窗像素测试通过 | 通过 |
| T02 | 点击前方有阻挡箭头 | 保留箭头,机会减 1 | 保留数量,扣一次机会;同箭头碰撞期间防连点 | 通过 |
| T03 | 边缘朝外箭头 | 正常消除,不越界 | 四边朝外检查通过 | 通过 |
| T04 | 清空当前关 | 显示通关,点击进入下一关 | 最后动画结束显示通关,按钮切换下一关 | 通过 |
| T05 | 机会耗尽 | 显示失败并允许重开 | 延后显示失败,结束后操作保护与重试通过 | 通过 |
| T06 | 游戏过程中重开 | 布局和机会恢复 | 三关各反复重开,恢复布局及机会,清空动画和提示 | 通过 |
| T07 | 使用提示功能 | 高亮当前可消除对象且不改变游戏状态 | 提示正确且不写入撤销历史 | 通过 |
| T08 | 撤销成功消除,忽略受阻点击 | 恢复最近消除的对象,受阻扣除的失误次数不返还 | 手动与自动操作均可逐步撤销 | 通过 |
| T09 | 回放三个正式关卡 | 所有关卡均能按求解序列清空 | 三关全部通过 | 通过 |
| T10 | 操作进阶折线模式 | 线条消除与受阻规则正确 | 点击线身、消除和碰撞测试通过 | 通过 |
| T11 | 生成随机挑战 | 经典与进阶随机棋盘均可解 | 多种尺寸生成及求解验证通过 | 通过 |
| T12 | 中途停止自动演示 | 停止后可以继续手动操作 | 棋盘、提示和撤销恢复正常 | 通过 |
测试脚本结果:

测试已由本人手动试玩,检验无误。
六、PSP 表格
| 任务 | 预估耗时(小时) | 实际耗时(小时) | 差异(小时) |
|---|---|---|---|
| 需求分析与游戏设计 | 0.5 | 0.5 | 0 |
| Python 与图形库学习 | 3 | 4 | 1 |
| 游戏界面实现 | 2 | 5 | 3 |
| 路径与碰撞逻辑实现 | 2 | 1 | -1 |
| 关卡设计 | 2 | 1 | -1 |
| AIGC 辅助开发 | 3 | 5 | 2 |
| 测试与修改 | 2 | 3 | 1 |
| 扩展功能(提示与撤销) | 1 | 3 | 2 |
| README 与博客撰写 | 3 | 4 | 1 |
| 合计 | 18.5 | 26 | 8 |
七、心得体会
一开始我去微信玩了一下原游戏,觉得拆分开来看,玩法似乎没有那么复杂,实际上做的时候才发现比想象中麻烦得多。不说 UI 的布局和设计,前几个固定关卡的设定还算容易,进阶关卡涉及更长的线条、更多的转折和相互阻挡,感觉就没有那么简单了。当前的只是 5*5 棋盘的版本,进阶感觉比基础版简单。后续有机会的话,我也希望能在 GitHub 上继续更新更大棋盘、更多格子的版本。
而等到真正实现逻辑的时候,会发现 AI 自己理解的和我们想要的往往是两码事。AI 会认为棋盘边界就是边界了,于是飞出动画只绘制到棋盘边界;AI 觉得进阶模式中带着长线条的箭头直接平移飞出就好了,却没有考虑后面的线条应该沿着原来的路径依次跟随;提示 “蛇形飞出” 后,AI又想要绘制很复杂且不美观的动画出来;不特别告诉 AI 的话,游戏界面都没有返回主菜单的按键。很多布局和按键不协调的问题,也只有自己测试了才知道,AI只会说测试通过。AI 能帮助建立数据结构、编写界面和生成测试,但 AI 不会,或者说不能直接生成你想要的游戏。即使提示词已经描述得比较详细,AI 解读出的游戏规则、交互方式和视觉效果也不一定与自己一致。
本次作业让我学到了不少。首先是需求分析的重要性。最开始觉得只需要告诉 AI “做一个这样那样的游戏” 就足够了,后来才发现,箭头怎样移动、什么时候触发消除、动画期间能否继续点击、暂停后计时是否停止、撤销是否恢复失误次数,这些看似细小的问题,实际上都需要提前确定。需求越模糊,后续反复修改的成本就越高。
还有模块化开发和版本管理。我把整个项目分成基础架构、核心逻辑、关卡、界面、动画、测试等阶段,尽量让每一阶段只负责一部分内容。这样出现问题时,可以更容易地判断是游戏规则、状态管理还是界面绘制造成的,便于修改和调整。同时,后面一些新的模式也能复用先前的底层。学习使用 Git 则让我能够保留各阶段的成果,不必担心后续扩展破坏已经完成的基础版本。

浙公网安备 33010602011771号