软件工程第二次作业
软件工程第二次作业——《一箭又一箭》小游戏
| 项目 | 内容 |
|---|---|
| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/fzu/202601SofwareEngineering |
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/fzu/202601SofwareEngineering/homework/16717 |
| 这个作业的目标 | 使用 Python 和 AIGC 完成“一箭又一箭”小游戏 |
| 学号 | 102401135 |
| GitHub 仓库 | https://github.com/Victoria029/arrow_escape |
一、项目展示
1. 开始界面
开始界面

2. 游戏进行中
游戏过程演示

3. 碰撞反馈

4. 通关界面

5. 失败界面

6. 全部通关界面

二、项目介绍
2.1 游戏规则
“一箭又一箭”是一款点击式箭头解谜小游戏。棋盘为 5×5 的网格,上面分布着若干带方向的箭头(上、下、左、右四种)。
玩家用鼠标左键点击某个箭头后,程序会检查该箭头前进方向上是否有其他箭头阻挡:
- 无阻挡:箭头沿自身方向飞出棋盘并消失;
- 有阻挡:箭头不能消除,会向前撞击阻挡的箭头,然后弹回原位,同时失误次数减 1;
- 清空当前关卡的全部箭头即通关,可进入下一关;
- 失误次数达到 3 次时,本关失败,可以重新开始。
关键判定:阻挡是否成立,只与前方路径上是否有箭头占据有关,与被阻挡箭头自身的朝向无关。例如棋盘第一行是 → · · ↑ ·,第一个箭头朝右,但它的右侧有另一个箭头,因此不能飞出。
2.2 界面设计
游戏共包含 6 个界面:
- 开始界面:显示游戏标题、副标题、四个方向装饰箭头,以及“开始游戏”“选择关卡”“退出游戏”三个按钮;
- 选关界面:以列表形式列出全部 5 个关卡,每行显示关卡序号、名称和箭头数量,点击即可进入对应关卡;
- 游戏界面:顶部显示当前关卡、失误次数和剩余箭头数;中部为 5×5 棋盘;网格下方有“撤销”和“提示”按钮,最底部有“重新开始”和“主菜单”按钮;
- 通关结算界面:半透明遮罩层上方显示白色卡片,包含“恭喜过关!”、用时/失误/提示次数统计,以及“主菜单”“重玩本关”“下一关”三个按钮;
- 失败结算界面:显示“失败”和“失误次数用尽了”,附统计信息,提供“主菜单”和“重试本关”两个按钮;
- 全部通关界面:显示“恭喜通过所有关卡!”的最终庆祝画面。
界面采用浅灰色背景、浅蓝色箭头、白色网格线的配色方案。箭头由箭杆和箭头两部分组成,成功飞出和碰撞回弹都有平滑的动画反馈。
2.3 主要功能
- 方向系统:支持上、下、左、右四种箭头,用
Direction枚举统一表示; - 路径检测:沿箭头方向逐格检查同一行或同一列是否有其他箭头阻挡;
- 飞出动画:箭头以每帧 20 像素的速度平滑飞出窗口,飞出屏幕后自动清理;
- 碰撞动画:箭头根据与阻挡物的实际距离,使用正弦函数实现“前进→弹回”的平滑往复位移,同时箭头变红;
- 失误管理:每关最多 3 次失误,界面顶部实时显示;
- 撤销功能:记录操作历史快照,支持撤销上一步操作,失误次数同步回退;
- 提示功能:自动搜索一个当前可飞出的箭头并用黄色圆圈高亮;
- 关卡系统:共 5 关,每关使用逆向生成算法随机生成箭头布局,保证 100% 有解;
- 统计系统:记录每关用时、失误次数和提示次数,在结算界面展示。
2.4 特色
- 随机生成保证有解:使用“逆向生成算法”,从空棋盘开始,每次放置箭头时只选择路径畅通的方向。这样按放置顺序的倒序点击必定能全部消除,从根本上杜绝了死锁;
- 拟真碰撞反馈:箭头不是简单地原地晃动,而是根据与阻挡物的实际距离,精准地撞上去再弹回;
- 动画期间防连点:箭头正在播放碰撞动画时,禁止对该箭头重复点击,避免失误次数被重复扣除;
- 胜利结算时机优化:最后一个箭头飞出后,等待飞行动画完全播放完毕才弹出结算界面,让玩家能看到完整的消除过程;
- 分层架构:引擎逻辑(
core/)、界面渲染(ui/)、数据模型(models/)、关卡数据(levels/)相互独立。
三、实现思路
3.1 箭头、方向和关卡的表示
方向的表示
四种方向使用 Python 的 Enum 枚举类型统一表示:
class Direction(Enum):
UP = 1
DOWN = 2
LEFT = 3
RIGHT = 4
使用枚举而不是字符串或整数,好处是类型安全、可读性强,在 if-elif 分支判断时不会因为拼写错误导致 bug。
箭头的表示
每个箭头由一个 Arrow 类表示,包含位置(行列坐标)、方向、可见状态,以及用于动画的像素坐标:
class Arrow:
def __init__(self, row, col, direction):
self.row = row
self.col = col
self.direction = direction
self.is_visible = True
self.is_colliding = False
self.collide_timer = 0
self.collide_offset = 15
# 动画像素坐标
self.base_px = GRID_OFFSET_X + col * CELL_SIZE + CELL_SIZE // 2
self.base_py = GRID_OFFSET_Y + row * CELL_SIZE + CELL_SIZE // 2
self.px = self.base_px
self.py = self.base_py
关键设计是将逻辑坐标和像素坐标分离:row/col 用于路径检测和游戏逻辑,px/py 用于界面渲染和动画。这样逻辑层完全不需要关心箭头在屏幕上的具体位置。
棋盘的表示
棋盘使用 5×5 的二维列表表示,每个元素要么是 None(空格),要么是一个 Arrow 对象:
self.grid = [[None for _ in range(GRID_SIZE)] for _ in range(GRID_SIZE)]
关卡的表示
关卡数据由 levels/level_data.py 中的 generate_level() 函数动态生成:
def generate_level(num_arrows):
grid = [[None for _ in range(5)] for _ in range(5)]
positions = [(r, c) for r in range(5) for c in range(5)]
random.shuffle(positions)
arrows = []
for r, c in positions:
if len(arrows) >= num_arrows:
break
random.shuffle(directions)
for d in directions:
# 检查该方向路径上是否有其他箭头
if is_path_clear(grid, r, c, d):
grid[r][c] = d
arrows.append((r, c, d))
break
random.shuffle(arrows)
return arrows
3.2 路径检测方法
路径检测是整个游戏最核心的逻辑。给定一个箭头,需要判断它沿自身方向前进到棋盘边界之间,是否存在其他箭头。
实现方式是将阻挡判定封装为 get_blocking_arrow() 方法,返回路径上第一个阻挡的 Arrow 对象,如果没有阻挡则返回 None:
def get_blocking_arrow(self, arrow):
r, c = arrow.row, arrow.col
if arrow.direction == Direction.UP:
for i in range(r - 1, -1, -1):
if self.grid[i][c] is not None:
return self.grid[i][c]
elif arrow.direction == Direction.DOWN:
for i in range(r + 1, GRID_SIZE):
if self.grid[i][c] is not None:
return self.grid[i][c]
elif arrow.direction == Direction.LEFT:
for i in range(c - 1, -1, -1):
if self.grid[r][i] is not None:
return self.grid[r][i]
elif arrow.direction == Direction.RIGHT:
for i in range(c + 1, GRID_SIZE):
if self.grid[r][i] is not None:
return self.grid[r][i]
return None
这个方法返回的不是布尔值而是具体的 Arrow 对象,使得调用方既能判断“是否被阻挡”,又能获取“被谁阻挡”的信息——碰撞动画的距离计算正是依赖这一点。
- 边界安全:使用 range(r - 1, -1, -1) 这样的写法,当箭头位于棋盘边缘(r = 0)时,range 会直接为空,循环体不会执行,天然避免了数组越界问题。
3.3 碰撞动画的实现
当箭头被阻挡时,我希望它能根据与阻挡物的实际距离撞过去再弹回来,而不是简单地原地晃动。实现方式是使用正弦函数产生平滑的往复位移:
distance = abs(arrow.row - blocking_arrow.row) + abs(arrow.col - blocking_arrow.col)
arrow.collide_offset = max(15, distance * CELL_SIZE - 50)
然后在 update() 中逐帧计算偏移:
progress = (30 - arrow.collide_timer) / 30
offset = math.sin(progress * math.pi) * arrow.collide_offset
sin(0) = 0,sin(π/2) = 1,sin(π) = 0。当 progress 从 0 变到 1 时,offset 先增大到最大值再回到 0,形成一次完整的“前进→弹回”运动。最大偏移量由 collide_offset 控制,保证了箭头会刚好碰到阻挡物边缘再返回。
3.4 本项目的文件结构
ruangong2/
├── main.py # 程序入口
├── config.py # 全局配置(窗口尺寸、颜色、网格大小等)
├── levels/
│ └── level_data.py # 关卡的逆向生成算法
├── models/
│ └── data_model.py # Arrow 类和 Direction 枚举
├── core/
│ └── game_engine.py # 核心逻辑:路径检测、状态管理、动画更新
├── ui/
│ └── game_ui.py # 界面绘制:棋盘、箭头、按钮、结算画面
└── utils/
└── logger.py # 日志工具
四、AIGC 使用过程
以下记录来自本次项目的实际协作过程。
| 子任务 | 借助何种 AIGC 技术 | AI 实现或提供了什么 | 效果如何 | 人工修改 |
|---|---|---|---|---|
| 路径检测与核心逻辑 | DeepSeek | 生成四方向路径检测代码和游戏状态机框架 | 逻辑基本正确,但坐标计算存在偏差 | 手动修正了 handle_click 中的像素坐标到网格坐标的映射,补充了边界安全处理 |
| 箭头绘制与动画 | DeepSeek | 生成箭头绘制代码和碰撞动画方案 | 初始版本箭头尺寸过大超出格子,弹跳幅度也不合理 | 调整为箭杆+箭头分离绘制,根据阻挡距离动态计算碰撞偏移量,使用正弦函数实现平滑弹回 |
| 关卡的逆向生成算法 | DeepSeek | 提出“从空棋盘开始,每次只放置路径畅通的箭头”的逆向生成思路 | 完美解决了随机关卡出现死锁的问题 | 补充了最终打乱顺序的逻辑,增加解谜难度;调整了每关的箭头数量梯度(8→12→14→16→18) |
| 界面布局与结算画面 | DeepSeek | 生成开始界面、选关界面、通关/失败结算界面的完整代码 | 结算界面出现频闪,按钮位置遮挡了信息栏 | 将 pygame.display.flip() 统一移到主循环末尾解决频闪;重新规划了所有按钮的坐标布局 |
AIGC 使用中的典型问题与解决过程:
-
路径检测的越界问题:DeepSeek 最初生成的代码使用
board[row - 1][col]直接访问数组,当箭头位于棋盘边缘时会产生负索引。我改用了range(r - 1, -1, -1)的写法,让range自然处理边界情况。 -
关卡死锁问题:在使用固定关卡数据时,多次出现箭头互相阻挡导致无法通关的情况(例如 A 朝右挡住 B,B 朝左挡住 A)。最终通过与 DeepSeek 讨论,确定了“逆向生成算法”方案——每次放置箭头时只选择当前路径畅通的方向,从根本上保证了可解性。
-
碰撞动画的真实感:DeepSeek 最初实现的碰撞动画只是原地左右晃动,与视觉预期差距很大。我提出“箭头应该先碰到阻挡物再弹回”的需求后,DeepSeek 给出了使用正弦函数和动态偏移量的方案,效果非常自然。
五、测试结果
| 编号 | 测试内容 | 预期结果 | 实际结果 | 是否通过 |
|---|---|---|---|---|
| T01 | 点击前方无阻挡的箭头 | 箭头飞出棋盘并消失,剩余箭头数减 1 | 箭头平滑飞出屏幕,计数正确更新 | ✅ |
| T02 | 点击前方有阻挡的箭头 | 箭头不消失,失误次数减 1 | 箭头变红并向前撞击阻挡物后弹回,失误次数加 1 | ✅ |
| T03 | 点击位于边缘且朝向棋盘外的箭头 | 箭头正常消失,不发生越界错误 | 箭头顺利飞出,无报错 | ✅ |
| T04 | 消除本关全部箭头 | 显示通关结算并进入下一关 | 最后一个箭头飞出的动画播放完毕后弹出结算界面 | ✅ |
| T05 | 失误次数耗尽 | 显示失败界面并允许重新开始 | 显示“失败”和“失误次数用尽了”,提供重试按钮 | ✅ |
| T06 | 游戏进行中重新开始 | 箭头布局和失误次数恢复初始状态 | 棋盘恢复到当前关卡的初始布局,失误次数归零 | ✅ |
六、PSP 表格
| 任务 | 预估耗时(小时) | 实际耗时(小时) | 差异(小时) |
|---|---|---|---|
| 需求分析与游戏设计 | 1 | 2 | +1 |
| Python 与图形库学习 | 3 | 4 | +1 |
| 游戏界面实现 | 3 | 5 | +2 |
| 路径与碰撞逻辑实现 | 2 | 4 | +2 |
| 关卡设计 | 2 | 3 | +1 |
| AIGC 辅助开发 | 4 | 5 | +1 |
| 测试与修改 | 3 | 4 | +1 |
| README 与博客撰写 | 2 | 3 | +1 |
| 合计 | 20 | 30 | +10 |
七、心得体会
这次作业是我第一次系统性地将 AIGC 工具引入到完整的软件开发流程中,收获远超预期。
AI 带来的帮助:最明显的效率提升体现在“从 0 到 1”的阶段。以前遇到一个不熟悉的图形库(比如 Pygame),我需要花大量时间查文档、看教程。但这次,我只需要用自然语言描述需求,AI 就能快速给出可运行的代码框架,让我把精力集中在逻辑设计和体验优化上,而不是纠结 API 的用法。
遇到的问题:但 AI 生成的代码从来不是“拿来即用”的。路径检测的边界条件、碰撞动画的真实感、界面按钮的坐标布局——这些问题 AI 给出的第一版几乎都有瑕疵。更重要的是,AI 在设计固定关卡时,多次生成了互相阻挡的死锁关卡,导致游戏根本无法通关。这让我意识到,AI 擅长“写代码”,但不擅长“验证逻辑”,最终的把关必须由人来完成。
我的收获:最大的成长是学会了“带着验证的心态使用 AI”。每一段 AI 生成的代码,我都会实际运行、观察效果,发现问题后不只是说“有 bug”,而是精确描述现象和预期,让 AI 理解问题本质后再给出修复方案。这个“提出需求 → 验证结果 → 精准反馈 → 迭代改进”的循环,本身就是一种高效的开发方法论。
浙公网安备 33010602011771号