2026 秋软件工程个人作业(第二次)
2026秋软件工程个人作业(第二次)——利用AIGC完成"一箭又一箭"小游戏
| 项目 | 内容 |
|---|---|
| 这个作业属于哪个课程 | H202601软件工程与软件工程实践 |
| 这个作业要求在哪里 | 2026秋软件工程个人作业(第二次) |
| 这个作业的目标 | 使用 Python 和 AIGC 完成"一箭又一箭"小游戏 |
| 学号 | 102402144 |
| GitHub 仓库 | 仓库链接 |
目录
一、项目展示
开始界面

游戏过程

通关界面

失败界面

二、项目介绍
游戏规则
棋盘由若干格子组成,每个格子里有一个带方向的箭头(上 / 下 / 左 / 右),或者为空。 点击一个箭头,程序会检查它前进方向上的路径:
如果箭头与棋盘边界之间没有其他箭头阻挡,该箭头飞出棋盘并被消除;
如果路径上存在其他箭头,该箭头不能被消除,会晃动并变红作为碰撞提示, 同时消耗一次失误机会;
清除本关全部箭头即可进入下一关;
失误次数耗尽时本关失败,可以重新开始。
例如下面这一行,第一个箭头朝右,但右侧仍有其他箭头,因此不能飞出:
→ · · ↑ ·
而这一行最后一个箭头朝右,右侧没有其他箭头,因此可以飞出:
↑ · · · →
界面设计
界面包含开始界面、关卡选择、游戏界面和通关 / 失败界面。
- 开始界面:标题、开始按钮
- 游戏界面:当前关卡、棋盘、剩余箭头数、剩余失误次数、重新开始按钮
- 结果界面:通关/失败提示
具体效果已经在上方展示
主要功能
基础功能
- 具有可以正常显示和操作的图形化游戏界面;
- 棋盘中包含上、下、左、右四种方向的箭头;
- 能够通过鼠标点击选择箭头;
- 能够正确判断箭头前进方向上是否存在其他箭头;
- 前方无阻挡时,箭头能够飞出棋盘并消失;
- 前方有阻挡时,箭头不能消失,并给出明显的碰撞反馈;
- 设置失误次数,并在界面中显示剩余次数;
- 至少设计 3 个可以正常通关的关卡;
- 清空当前关卡全部箭头后,能够显示通关结果并进入下一关;
- 失误次数耗尽时,能够显示失败结果;
- 提供重新开始功能,使当前关卡恢复到初始状态。
附加功能
- 更多关卡或关卡选择;
- 得分、计时或星级评价;
- 提示功能;
- 撤销上一步;
- 保存游戏进度;
- 随机生成可通关的关卡;
- AI 自动求解当前关卡;
- 更流畅的动画和音效;
- 将程序打包为可执行文件;
三、实现思路
1. 箭头与方向的表示
方向
属性
- 四个反向的元组表示
方法
- 返回特定方向的行列增量
- 返回特定方向的字符表示
class Direction(Enum):
"""箭头的四个方向。value 为 (行增量, 列增量)。"""
UP = (-1, 0)
DOWN = (1, 0)
LEFT = (0, -1)
RIGHT = (0, 1)
@property
def delta(self) -> tuple[int, int]:
"""返回该方向的行、列增量。"""
return self.value
@property
def symbol(self) -> str:
"""返回该方向在关卡数据中的单字符符号,如 'U'。"""
return _DIRECTION_SYMBOLS[self.name]
箭头
属性
- 方向
- 在棋盘上位置
方法
- 返回方向的字符表示,用于储存关卡进度
@dataclass(frozen=True)
class Arrow:
"""棋盘上的一个箭头。"""
row: int
col: int
direction: Direction
@property
def symbol(self) -> str:
return self.direction.symbol
2. 棋盘的表示
棋盘
主要属性
- 自身的行列长度
- 储存箭头的二维数组
主要方法
- 根据储存的关卡数据构造出棋盘
- 判断某个箭头前方是否有箭头(即路径检测方法)
- 箭头被移除之后清除箭头
class Board:
"""游戏棋盘:一组带方向的箭头 + 路径检测。"""
def __init__(self, rows: int, cols: int) -> None:
self.rows = rows
self.cols = cols
self._grid: list[list[Arrow | None]] = [[None] * cols for _ in range(rows)]
@classmethod
def from_grid(cls, grid: list[str]) -> "Board":
"""从字符串列表构造棋盘。
每个字符串是一行,字符含义:
'U' / 'D' / 'L' / 'R' = 该方向箭头
'.' = 空格
所有行必须等长,否则抛出 ValueError。
"""
if not grid:
raise ValueError("关卡棋盘不能为空")
cols = len(grid[0])
for index, line in enumerate(grid):
if len(line) != cols:
raise ValueError(
f"第 {index} 行长度为 {len(line)},与第 0 行的 {cols} 不一致"
)
board = cls(len(grid), cols)
for r, line in enumerate(grid):
for c, char in enumerate(line):
if char == EMPTY_CELL:
continue
if char not in SYMBOL_TO_DIRECTION:
raise ValueError(
f"第 {r} 行第 {c} 列出现非法字符 {char!r},"
f"只允许 {EMPTY_CELL} 或 U/D/L/R"
)
board._grid[r][c] = Arrow(r, c, SYMBOL_TO_DIRECTION[char])
return board
def is_path_clear(self, arrow: Arrow) -> bool:
"""判断 arrow 沿其朝向到棋盘边界之间是否没有其他箭头。
从箭头前一个格子开始沿方向逐格前进,直到走出棋盘:
途中遇到任何箭头即视为被阻挡。
箭头位于边缘且朝向棋盘外时,扫描区间为空,直接判定为可飞出,
不会访问越界下标(对应测试 T03)。
若传入的箭头不属于本棋盘,直接抛出 ValueError —— 否则会基于
另一个棋盘的布局给出看似合理却错误的结果。
"""
if self.arrow_at(arrow.row, arrow.col) is not arrow:
raise ValueError(f"箭头 {arrow} 不属于当前棋盘")
dr, dc = arrow.direction.delta
r, c = arrow.row + dr, arrow.col + dc
while self.in_bounds(r, c):
if self._grid[r][c] is not None:
return False
r += dr
c += dc
return True
def remove(self, arrow: Arrow) -> None:
"""把箭头从棋盘上移除。"""
if self._grid[arrow.row][arrow.col] is not arrow:
raise ValueError(f"棋盘上不存在箭头 {arrow}")
self._grid[arrow.row][arrow.col] = None
关卡的表示
主要属性
- 当前关卡编号
- 当前棋盘(即上方的棋盘对象)
- 失误次数,剩余箭头,等分,用时等关卡状态信息
主要方法
- 重新开始,回复棋盘初始状态
- 撤销上一步操作
- 存档
- 关卡失败时弹窗
- 关卡成功时进入下一关
- 退出游戏时存档
四、AIGC使用过程
AI工具: cluade code + deepseek
一、让AI读取作业发布页面的html,根据要求作业要求生成PRD,作为项目指导

二、让AI根据PRD自行完成作业要求的基础功能的实现和测试

三、我亲自游玩游戏进行测试,发现问题并让AI修改
主要问题
- Bug:开始菜单不点按钮,随机点击任意处也会进入第一关
- 改进:使得游戏画面可缩放

四、让AI根据PRD自行完成作业要求的附加功能的实现和测试

五、我亲自游玩游戏后,向AI提出修改建议
建议
- 增加按钮反馈,使得鼠标放置在箭头上箭头会放大高亮,让用户知晓他选中了这个箭头
五、测试结果
AI自行编写自动化测试程序,进行共166项测试,全部通过,此处仅展示前20条,其中前6条为作业要求测试项
| 编号 | 测试内容 | 预期结果 | 实际结果 | 是否通过 |
|---|---|---|---|---|
| T01 | 点击前方无阻挡的箭头 | 箭头飞出棋盘并消失 | 点击后箭头从棋盘移除,剩余箭头数由 1 变 0,失误次数不变,并登记飞出动画 | ✅ |
| T02 | 点击前方有阻挡的箭头 | 箭头不消失,失误次数减 1 | 箭头仍在原格,剩余箭头数不变,失误次数由 3 变 2,并登记碰撞动画 | ✅ |
| T03 | 点击位于边缘且朝向棋盘外的箭头 | 箭头正常消失,不发生越界错误 | 边缘四个方向朝外的箭头均正常飞出,未出现 IndexError | ✅ |
| T04 | 消除本关全部箭头 | 显示通关并进入下一关 | 清空后显示通关界面;点击主按钮进入第 2 关,关卡号递增、布局与失误次数重置 | ✅ |
| T05 | 失误次数耗尽 | 显示失败并允许重新开始 | 失误归零后显示失败界面;点击主按钮可重新开始本关 | ✅ |
| T06 | 游戏进行中重新开始 | 箭头布局和失误次数恢复 | 点击"重新开始"按钮后,棋盘恢复初始布局,失误次数恢复为上限 | ✅ |
| T07 | 四方向在空旷棋盘上均可飞出 | 上下左右四个方向都判定为可飞出 | 四个方向全部返回可飞出 | ✅ |
| T08 | 四方向各放一个阻挡物 | 四个方向都判定为被阻挡 | 四个方向全部返回被阻挡 | ✅ |
| T09 | 方向的行列增量 | 四个方向的 (行, 列) 增量正确 | 与预期一致 | ✅ |
| T10 | 点击空格 | 不改变任何状态 | 盘面与失误次数均未变 | ✅ |
| T11 | 路径上只有空格不构成阻挡 | R.... 中的 R 可飞出 |
判定为可飞出 | ✅ |
| T12 | 不同行不同列的箭头不构成阻挡 | 斜向箭头不影响判定 | 判定为可飞出 | ✅ |
| T13 | 移除阻挡物后被挡箭头转为可飞出 | 阻挡消失后判定反转 | 移除后由"被阻挡"变为"可飞出" | ✅ |
| T14 | 传入不属于本棋盘的箭头 | 报错而非返回错误答案 | 抛出 ValueError,未静默返回错误结果 | ✅ |
| T15 | 4×5 棋盘逐格逐方向穷举检测 | 不访问越界下标 | 全部格子 × 四个方向均无异常 | ✅ |
| T16 | 1×1 棋盘四个方向 | 四个方向均可飞出且不越界 | 全部通过 | ✅ |
| T17 | 四角箭头朝外 | 均判定为可飞出 | 全部通过 | ✅ |
| T18 | 棋盘字符串与网格互转 | 转换后与原字符串一致 | 往返一致 | ✅ |
| T19 | 空格上没有箭头 | 空格返回 None | 符合预期 | ✅ |
| T20 | 剩余箭头计数 | 计数正确 | UDLR 行返回 4 |
✅ |
六、PSP表格
| 任务 | 预估耗时(分钟) | 实际耗时(分钟) | 差异(分钟) |
|---|---|---|---|
| agent自动化需求分析与游戏设计 | 10 | 2 | +8 |
| 游戏框架学习 | 120 | 60 | -60 |
| agent自动化基础功能代码实现(含测试与修改) | 5 | 20 | -15 |
| 人工测试 | 5 | 5 | 0 |
| agent自动化附加功能代码实现(含测试与修改) | 20 | 20 | 0 |
| 博客撰写 | 120 | 60 | -60 |
| 合计 | 280 | 167 | -127 |
七、心得体会
初次接触小型游戏的开发,我对框架一头雾水,通过学习AI生成的PRD以及自行查阅相关资料,我逐渐能理解各个模块的依赖关系和总体框架。我学习到,模块的具体实现其实不难,难的是如何组织代码框架,同时各个模块之间的耦合度不宜过高,否则后期维护和修改代码的时候会牵一发而动全身,极大的提高修改的难度。同时,一次性考虑所有的需求并实现代码在实际开发中是难以做到的,我们需要先搭建一个骨架,实现基本的对象和功能,再以此为基础慢慢往外扩展。我学习到,低风险的代码可以全权交给AI实现,高风险的代码,例如关乎用户的账号密码等,必须由人工审计和测试。
浙公网安备 33010602011771号