第二次作业-一箭又一箭
利用 AIGC 完成“一箭又一箭”网格箭头小游戏
一、作业信息
| 项目 | 内容 |
|---|---|
| 这个作业属于哪个课程 | 2026 秋软件工程与软件工程实践 |
| 这个作业要求在哪里 | 第二次个人作业 |
| 这个作业的目标 | 使用 Python 和 AIGC 完成“一箭又一箭”小游戏 |
| 学号 | 102402141 |
| GitHub 仓库 | 项目仓库 |
二、项目展示
1. 开始界面

2. 游戏界面

3. 碰撞反馈
点击被阻挡箭头后的,箭头震动、阻挡箭头变红且生命值减少。

4. 通关或失败界面


三、项目介绍
“一箭又一箭”是一款基于 Python Tkinter 开发的网格箭头解谜小游戏。
玩家需要观察每支箭头的方向,判断箭头前方是否存在其他箭头。当前方没有阻挡时,点击箭头后它会飞出棋盘;如果前方存在阻挡,箭头不会消失,同时会触发碰撞反馈并扣除一次生命。
游戏包含四个难度逐步增加的关卡。现在每次进入关卡或重新开始时,都会随机生成新的箭头位置和方向,但棋盘仍然保证存在可行的通关顺序。
四、主要功能
- 支持上、下、左、右四种箭头方向;
- 支持鼠标点击箭头;
- 自动检测箭头前方的阻挡关系;
- 无阻挡时播放飞出棋盘动画;
- 被阻挡时播放震动和红色错误反馈;
- 每个关卡有三次失误机会;
- 支持重新开始、通关、失败和下一关;
- 支持随机生成保证可解的棋盘;
- 使用自动化测试验证核心规则。
五、实现思路
1. 数据结构
项目主要使用以下数据结构:
Level:保存关卡名称、棋盘布局和失误次数;Arrow:保存箭头所在行、列和方向;Board:保存当前仍在棋盘上的箭头;GameSession:管理当前关卡、生命值和游戏状态。
游戏状态包括:
START
PLAYING
WON
FAILED
ALL_CLEARED
2. 路径检测
点击箭头后,从箭头相邻的下一格开始,沿着箭头方向逐格检查,直到棋盘边界。
如果检测到其他箭头,则说明当前箭头被阻挡;如果一直检测到边界仍没有发现箭头,则当前箭头可以飞出。
路径检测只检查箭头前方,不检查后方和斜线方向。
核心逻辑可以概括为:
while 当前坐标没有超出棋盘:
if 当前格存在其他箭头:
说明被阻挡
沿箭头方向继续移动
3. 随机棋盘生成
随机生成器继承每个关卡模板的棋盘大小、箭头数量和失误次数。
生成时采用逆向构造:
- 随机选择箭头位置;
- 从最终消除顺序的反方向逐步放置箭头;
- 每次只选择前方没有已放置箭头的方向;
- 因此构造出的棋盘天然保留一条合法消除顺序;
- 测试中再使用求解器验证棋盘确实可解。
核心代码讲解
1. 使用统一的数据结构表示方向
项目使用字符表示四个方向:
DIRECTION_DELTAS = {
"^": (-1, 0), # 向上
"v": (1, 0), # 向下
"<": (0, -1), # 向左
">": (0, 1), # 向右
}
每个方向都被转换成行列坐标的增量。这样四个方向可以使用同一套路径检测逻辑,而不需要为上、下、左、右分别编写代码。
例如:
(-1, 0)表示行号减一,箭头向上;(1, 0)表示行号加一,箭头向下;(0, -1)表示列号减一,箭头向左;(0, 1)表示列号加一,箭头向右。
箭头使用 Arrow 数据类表示:
@dataclass(frozen=True)
class Arrow:
row: int
col: int
direction: str
@property
def position(self):
return self.row, self.col
Arrow 保存箭头所在的行、列和方向。箭头的位置使用 (row, col) 表示,左上角棋盘格为 (0, 0)。
2. 检测箭头前方是否被阻挡
棋盘使用字典保存当前仍然存在的箭头:
self._arrows: Dict[Position, Arrow] = {}
字典的键是箭头位置,值是对应的 Arrow 对象。当箭头飞出棋盘后,直接从字典中删除,因此后续路径检测不会再把已经消除的箭头当作阻挡物。
路径检测代码如下:
def blocking_arrow(self, arrow: Arrow) -> Optional[Arrow]:
for position in self.path_cells(arrow):
blocker = self._arrows.get(position)
if blocker is not None:
return blocker
return None
path_cells() 从箭头相邻的下一格开始,沿着箭头方向逐格检查,直到棋盘边界:
def path_cells(self, arrow: Arrow):
row_step, col_step = DIRECTION_DELTAS[arrow.direction]
row = arrow.row + row_step
col = arrow.col + col_step
cells = []
while self.in_bounds(row, col):
cells.append((row, col))
row += row_step
col += col_step
return tuple(cells)
这里有两个重要细节:
- 检测从箭头前方的下一格开始,不能从箭头自身开始,否则会把自己误判为阻挡;
- 每次循环先检查是否越界,再读取棋盘位置,可以避免上边界和左边界出现负索引问题。
路径上只要出现一支其他箭头,就判定为阻挡。箭头的朝向不会影响阻挡结果,只要它位于当前箭头的前进路径上即可。
一次路径检测最多扫描一整行或一整列,因此时间复杂度为:
O(max(行数, 列数))
额外空间复杂度为:
O(1)
3. 处理玩家点击
玩家点击棋盘后,首先判断当前游戏是否处于 PLAYING 状态,然后查找点击位置是否存在箭头:
def click(self, row: int, col: int) -> ActionResult:
if self.status is not GameStatus.PLAYING:
return ActionResult("ignored", status=self.status)
arrow = self.board.arrow_at(row, col)
if arrow is None:
return ActionResult("invalid", status=self.status)
点击空格、棋盘外或已经消除的位置时,不改变棋盘,也不会扣除失误次数。
如果箭头前方存在阻挡,则保留箭头并扣除一次失误机会:
blocker = self.board.blocking_arrow(arrow)
if blocker is not None:
self.mistakes_left -= 1
if self.mistakes_left <= 0:
self.mistakes_left = 0
self.status = GameStatus.FAILED
return ActionResult(
"blocked",
arrow,
blocker,
self.board.remaining,
self.mistakes_left,
self.status,
)
界面收到 blocked 结果后,会让被点击的箭头震动,同时把真正的阻挡箭头短暂显示为红色,并播放生命值爆炸反馈。
如果前方没有阻挡,则移除箭头:
self.board.remove(arrow)
if self.board.remaining == 0:
self.status = GameStatus.WON
return ActionResult(
"fly",
arrow,
None,
self.board.remaining,
self.mistakes_left,
self.status,
)
箭头的状态变化顺序是:
点击箭头
↓
检测前方路径
↓
有阻挡:扣除生命并播放碰撞反馈
↓
无阻挡:播放飞出动画
↓
动画结束后移除箭头
↓
判断当前关卡是否通关
游戏界面在播放动画期间会暂时忽略后续点击,避免一次操作重复扣除生命或重复删除箭头。
4. 随机生成保证可解的棋盘
为了避免固定布局过于规律,项目增加了随机棋盘生成器。
随机棋盘会继承关卡模板的以下属性:
- 棋盘行数和列数;
- 箭头数量;
- 每关的失误次数。
生成器采用逆向构造的方法保证棋盘存在解法:
for _ in range(count):
candidates = []
for position in remaining_positions:
available = [
direction
for direction in directions
if not _has_arrow_ahead(
position,
direction,
occupied,
rows,
cols,
)
]
if available:
candidates.append((position, available))
position, available = randomizer.choice(candidates)
direction = randomizer.choice(available)
symbols[row][col] = direction
occupied.add(position)
remaining_positions.remove(position)
生成过程中的 occupied 表示已经放置的箭头。每次放置新箭头时,只从前方没有已放置箭头的位置和方向中选择。
假设箭头的最终点击顺序为:
A → B → C → D
生成器会按照反方向:
D → C → B → A
进行构造。这样在最终棋盘中,先点击 A 时,A 的路径不会被后续箭头阻挡;点击 A 后,再点击 B,以此类推。因此构造过程天然保留了一条合法的消除顺序。
生成完成后,项目还会使用求解器对棋盘进行验证:
def is_solvable(level: Level) -> bool:
return find_solution(level) is not None
这样既通过生成算法保证可解,又通过自动化测试进行二次确认。
六、AIGC 使用过程
第一次协作:需求分析和代码结构


- 提出的要求:根据作业要求创建
PRD.md,明确游戏规则、界面、测试和交付要求。 - AI 提供的内容:整理了产品目标、游戏状态、关卡要求、测试要求和博客要求。
- 实际效果:后续开发有了明确的功能边界。
- 人工检查:结合老师作业页面重新检查需求,并补充了随机棋盘、动画和博客记录要求。
第二次协作:界面和 Bug 修复

- 遇到的问题:Tkinter 启动时报错,首页和游戏界面视觉效果也不够像游戏。
- 提出的要求:修复启动错误,优化浅色主题、首页布局、箭头颜色和碰撞反馈。
- AI 提供的内容:定位并删除了 Tkinter 不支持的
spacing3参数,并重新设计了首页和游戏界面。 - 实际效果:程序可以正常进入界面,首页和游戏页更加清晰。
- 人工检查:实际运行程序,并测试飞出动画、阻挡反馈和生命值变化。
第三次协作:随机生成可解棋盘
- 遇到的问题:固定关卡的箭头方向仍然存在明显规律。
- 提出的要求:在保证棋盘有解的情况下随机生成箭头位置和方向。
- AI 提供的内容:实现了基于逆向构造的随机关卡生成器。
- 实际效果:每次开始或重新开始都会得到不同的棋盘。
- 人工检查:运行自动化测试,并使用多个随机种子验证棋盘可解。
七、自动化测试
运行命令:
python -m unittest discover -s homework2/tests -v
本项目共运行 15 个自动化测试,全部通过。
| 编号 | 测试内容 | 预期结果 | 实际结果 |
|---|---|---|---|
| T01 | 点击前方无阻挡的箭头 | 箭头飞出并消失 | 通过 |
| T02 | 点击前方有阻挡的箭头 | 箭头保留并扣除一次生命 | 通过 |
| T03 | 点击边缘且朝向棋盘外的箭头 | 正常飞出,不发生越界 | 通过 |
| T04 | 清空当前关卡 | 显示通关并进入下一关 | 通过 |
| T05 | 生命值耗尽 | 显示失败界面 | 通过 |
| T06 | 重新开始当前关卡 | 棋盘重新生成,生命值恢复 | 通过 |
| T07 | 点击空格 | 游戏状态不变 | 通过 |
| T08 | 随机棋盘生成 | 每张棋盘都可以求解 | 通过 |
八、PSP 表格
| 阶段 | 预计耗时 | 实际耗时 | 差异说明 |
|---|---|---|---|
| 需求分析和 PRD 整理 | 30min | 25min | 无 |
| 核心规则实现 | 30min | 30min | 无 |
| Tkinter 界面开发 | 30min | 40min | 无 |
| 动画和视觉优化 | 30min | 37min | 无 |
| 随机棋盘生成 | 30min | 28min | 无 |
| 自动化测试 | 30min | 25min | 无 |
| 文档和博客整理 | 1h | 1.5h | 无 |
九、Git 开发过程
本项目按照功能阶段进行了多次提交:
2218e47 feat: implement arrow game core rules
514a4ba feat: add tkinter game interface and feedback
55dff4 test: expand regression coverage and add usage docs
7466b4b fix: remove unsupported tkinter label option
7d9865c feat: polish home screen and feedback animations
7fbefa8 feat: increase level depth and speed up fly animation
cf86675 style: randomize fixed level direction layouts
8ec79db feat: generate solvable random boards
这些提交分别对应核心规则、界面、测试、Bug 修复、动画优化和随机关卡等开发阶段。
十、运行方式
在项目根目录执行:
python -m homework2
运行测试:
python -m unittest discover -s homework2/tests -v
十一、心得体会
这次开发中,AIGC 工具在需求整理、代码结构设计、Bug 定位和测试编写方面提供了帮助。但 AI 生成的代码不能直接视为正确代码,仍然需要自己运行程序、查看报错、设计测试并检查实际效果。
本项目中比较重要的人工修改包括:
- 修复 Tkinter 不支持的控件参数;
- 调整首页和游戏界面的颜色与布局;
- 修改箭头飞出和碰撞反馈动画;
- 重新设计随机棋盘生成逻辑;
- 使用自动化测试确认随机棋盘不会出现无解情况。

浙公网安备 33010602011771号