“一箭又一箭”小游戏
| 项目 | 内容 |
|---|---|
| 这个作业属于哪个课程 | H202601 软件工程与软件工程实践 |
| 这个作业要求在哪里 | 第二次个人作业 |
| 这个作业的目标 | 使用 Python 和 AIGC 完成“一箭又一箭”小游戏 |
| 姓名、学号 | 102401204 张楠 |
| GitHub 仓库 | Arrow_game |
一、项目展示
开始界面

游戏进行中

动态演示
完整游戏流程(这里从第四关开始,展示了正确通关后的结算画面,以及回到第一关后所展示的失误、重新开始、失误机会用光的画面。):

二、项目介绍
2.1 游戏规则
“一箭又一箭”是一款点击式箭头解谜小游戏。棋盘由若干带方向的箭头组成,方向分为上、下、左、右四种。玩家点击某个箭头后:
- 如果该箭头前进方向上没有其他箭头阻挡,它会飞出棋盘并消失;
- 如果前方有箭头阻挡,它不能飞出,会抖动变红,同时失误次数减一;
- 清空棋盘全部箭头即通关,进入下一关;
- 失误次数耗尽则本关失败,可重新开始。
关键判定:阻挡是否成立,只与前方路径上是否有箭头占据有关,与那个箭头自身的朝向无关。例如 → · · ↑ ·,第一个箭头朝右,但右侧有一个朝上的箭头,因此不能飞出。
2.2 界面设计
游戏共包含 5 个界面:
- 开始界面:标题、三条规则说明、开始游戏按钮;
- 游戏界面:棋盘居中,右侧面板显示当前关卡、剩余箭头数、剩余失误次数、本关用时、重新开始按钮;
- 关卡完成界面:显示“关卡完成”,可进入下一关或返回主菜单;
- 失败界面:显示“游戏失败”,可重试本关或返回主菜单;
- 全部通关界面:显示总用时,伴有烟花庆祝效果。
界面采用海洋主题:渐变蓝背景、动态波浪、上浮气泡、摇曳海草和游动的小鱼。棋盘底色为沙滩色,箭头为海蓝色。文字统一使用深色描边,确保在任何背景下都清晰可读。其实这里看github可知海洋主题是最后一个版本定下来的,之前是很枯燥的深蓝色画面(在文章的最后面可以看见),最初调试ai时想着可以玩就可以了,但越做越觉得太过无趣,我想要更具个人特色的界面,最起码会让人眼前一亮,看着生动有趣,最终调试出这个版本,个人很满意了。
2.3 主要功能与特色
- 四方向箭头齐全,路径检测正确,边界处理无误;
- 飞出有淡出动画,碰撞有抖动变红反馈;
- 每关 3 次失误机会,界面实时显示;
- 共 4 个关卡,第四关为 12 箭头高难度布局;
- 实时计时,结算时显示本关用时和总用时;
- 重新开始带确认框,避免误操作;
- 海洋主题背景,全部通关时绽放彩色烟花;
- 零外部素材:所有图形(箭头、气泡、海草、小鱼、烟花)均由 Pygame 原生绘制。
三、实现思路
3.1 箭头和方向的表示
每个箭头用一个 Arrow 对象表示,包含位置 (row, col) 和方向字符 dir。四个方向用字典统一映射,每个方向包含行增量、列增量和绘制旋转角度。
方向映射的 bug 与修复:在最初版本中,DIRS 字典的角度映射写错了导致箭头图形的指向与飞出方向不一致——例如朝右的箭头会向下飞,朝下的箭头会向左飞。
DIRS = {
'U': (0, -1, -math.pi / 2),
'D': (0, 1, math.pi / 2),
'L': (-1, 0, math.pi),
'R': (1, 0, 0)
}
修复方法是把角度改为与方向对应的正确值:朝上为 0,朝右为顺时针 90°(math.pi/2),朝下为 180°(math.pi),朝左为逆时针 90°(-math.pi/2)。修正后方向与图形完全一致
DIRS = {
'U': (0, -1, 0),
'D': (0, 1, math.pi),
'L': (-1, 0, -math.pi / 2),
'R': (1, 0, math.pi / 2)
}
3.2 关卡数据的表示
每个关卡用一个字符串列表表示,每个字符代表一个方向,. 表示空格。
字符串列表的方式便于手工设计关卡,每一行对应棋盘的一行,可读性强。
LEVELS = [
["R.D", "L..", ".U."],
[".R.D", "....", "L.U.", "...R"],
[".R..D", "...U.", "L....", "..D..", "...LR"],
[".U.U.", "L.R.D", ".U.D.", "D.L.R", ".R.R."]
]
3.3 路径检测(核心算法
判定箭头能否飞出,只需沿它的方向一直前进,看路径上是否有其他箭头。
def blocked(arrow):
r = arrow.r + arrow.dy
c = arrow.c + arrow.dx
while 0 <= r < rows and 0 <= c < cols:
if grid[r][c] is not None:
return True
r += arrow.dy
c += arrow.dx
return False
这个循环有两个作用:
- 判断是否越界:
r或c超出棋盘范围时,说明已到达边界,路径畅通; - 判断是否有阻挡:路径上任意一格
grid[r][c]不为空,说明有箭头阻挡。
循环从箭头的下一格开始(arrow.r + arrow.dy),而不是从箭头本身开始,避免误判自己。
3.4 飞行动画与碰撞动画
飞出动画:当玩家点击的箭头前方无阻挡时,程序调用 start_fly() 函数。该函数先计算出箭头从当前位置飞到棋盘边缘再飞出一格所需的距离,然后把箭头从 grid 中移除,并加入 flying_arrows 列表,交给动画系统逐帧处理。
def start_fly(arrow):
x, y = center(arrow.r, arrow.c)
if arrow.dx == 1:
dist = (board_x + board_w - x) + cell_size
elif arrow.dx == -1:
dist = (x - board_x) + cell_size
elif arrow.dy == 1:
dist = (board_y + board_h - y) + cell_size
else:
dist = (y - board_y) + cell_size
arrow.anim = 'fly'
arrow.t = 0
arrow.dur = 0.42
arrow.dist = dist
grid[arrow.r][arrow.c] = None
flying_arrows.append(arrow)
动画的每一帧都在 update() 函数中处理。进度 p 从 0 到 1 递增,通过缓动函数 e = 1 - (1-p)² 计算偏移量,使箭头呈现“先快后慢”的飞出效果。透明度 alpha 在飞出过程中逐渐降低,直到完全消失后从列表中移除。
碰撞动画每帧用 sin(p * π) 产生一个先增后减的偏移量 k,让箭头在前进方向上“弹一下再回来”。同时把颜色从蓝色逐渐变成红色,动画时长约 0.4 秒。这部分与飞出动画放在同一个 update() 函数里处理,
def update(dt):
global fail_delay, state, win_timer, elapsed_time, total_time
# 飞行动画
for i in range(len(flying_arrows)-1, -1, -1):
a = flying_arrows[i]
a.t += dt
p = min(1, a.t / a.dur)
e = 1 - (1-p)*(1-p)
a.offx = a.dx * a.dist * e
a.offy = a.dy * a.dist * e
a.alpha = max(0, 1 - p**1.6)
if p >= 1:
flying_arrows.pop(i)
# 碰撞抖动动画
for r in range(rows):
for c in range(cols):
a = grid[r][c]
if a and a.anim == 'bump':
a.t += dt
p = min(1, a.t / a.dur)
k = math.sin(math.pi * p) * cell_size * 0.2
a.offx = a.dx * k
a.offy = a.dy * k
a.color = (255, 96, 112) if p > 0.5 else (104, 196, 255)
if p >= 1:
a.anim = None
a.offx = a.offy = 0
a.color = None
碰撞动画:当玩家点击的箭头前方有阻挡时,程序不会移除箭头,而是给它打上 anim = 'bump' 的标记,并让失误次数减一。
if blocked(arrow):
arrow.anim = 'bump'
arrow.t = 0
arrow.dur = 0.4
mistakes_left -= 1
if mistakes_left <= 0:
fail_delay = 0.7
else:
start_fly(arrow)
3.5 关卡状态的切换
游戏使用一个有限状态机管理界面切换:menu、play、confirm、win、fail、allclear 六个状态。每个状态有对应的绘制函数和事件处理逻辑。状态之间的切换由通关检测、失败检测和按钮点击触发。
四、AIGC 使用过程
开发过程中使用 DeepSeek 作为 AIGC 辅助工具。下面记录 5 次具有代表性的协作过程。
4.1 生成基础游戏框架
| 项目 | 内容 |
|---|---|
| 我的提问 | 用 Python 和 Pygame 实现一个点击箭头飞出棋盘的小游戏,包含开始界面、游戏界面、通关失败界面,4 个关卡 |
| AI 提供 | 完整的 Pygame 框架代码,包括状态机、关卡数据、绘制函数、主循环 |
| 效果 | 可以直接运行,功能基本完整 |
| 人工修改 | 调整了窗口尺寸和棋盘布局,让右侧信息栏完整显示 |
4.2 修复箭头方向错误
| 项目 | 内容 |
|---|---|
| 我的提问 | 箭头向右的却向下移动,箭头向下的向左移动,怎么修? |
| AI 提供 | 指出是 DIRS 字典的角度映射错误,并给出修正方案 |
| 效果 | 修正后方向与图形完全一致 |
| 人工修改 | 只改了一处 DIRS 字典,其他代码不动 |
4.3 修复 Pygame 字体崩溃
| 项目 | 内容 |
|---|---|
| 我的提问 | 运行时报错 TypeError: expected str, bytes or os.PathLike object, not int,位置在 SysFont |
| AI 提供 | 指出这是 Pygame 2.6.1 在 Windows 下的已知 bug,建议改用 pygame.font.Font() 直接加载字体文件 |
| 效果 | 问题解决,中文正常显示 |
| 人工修改 | 新增 load_font() 函数,按优先级尝试微软雅黑、黑体、宋体等字体文件 |
4.4 增加计时和重新开始确认框
| 项目 | 内容 |
|---|---|
| 我的提问 | 希望在右侧显示本关用时,并且点击“重新开始本关”时弹出确认框 |
| AI 提供 | 计时变量、每帧更新逻辑,以及一个 STATE_CONFIRM 状态和确认遮罩界面 |
| 效果 | 直接可用,UI 与原有风格一致 |
| 人工修改 | 微调了计时器的更新时机,避免通关后仍继续计时 |
4.5 实现海洋主题背景和烟花
| 项目 | 内容 |
|---|---|
| 我的提问 | 想要一个海洋主题背景,有波浪、气泡、海草、小鱼;全部通关时要有烟花 |
| AI 提供 | 用纯 Pygame 绘图函数绘制动态海洋背景,以及基于粒子的烟花系统 |
| 效果 | 视觉效果好,不需要额外素材 |
| 人工修改 | 去掉了频闪的屏幕白光,减弱了光柱亮度,把鱼的方向改成朝左游,让文字更清晰 |
五、测试结果
在程序开发完成后,本人实际运行并逐项测试了以下内容:
| 编号 | 测试内容 | 预期结果 | 实际结果 | 是否通过 |
|---|---|---|---|---|
| T01 | 点击前方无阻挡的箭头 | 箭头飞出棋盘并消失 | 箭头飞出棋盘并消失,有淡出动画 | ✅ |
| T02 | 点击前方有阻挡的箭头 | 箭头不消失,失误次数减 1 | 箭头抖动变红,失误灯灭一个 | ✅ |
| T03 | 点击位于边缘且朝向棋盘外的箭头 | 箭头正常消失,不发生越界错误 | 箭头正常飞出,程序无报错 | ✅ |
| T04 | 消除本关全部箭头 | 显示通关并进入下一关 | 显示“关卡完成”,可进入下一关 | ✅ |
| T05 | 失误次数耗尽 | 显示失败并允许重新开始 | 显示“游戏失败”,可重试本关 | ✅ |
| T06 | 游戏进行中重新开始 | 箭头布局和失误次数恢复 | 弹出确认框,确认后重置本关 | ✅ |
所有测试用例均通过。
六、PSP 表格
| 任务 | 预估耗时(小时) | 实际耗时(小时) | 差异(小时) |
|---|---|---|---|
| 需求分析与游戏设计 | 1.0 | 1.0 | 0 |
| Python 与图形库学习 | 2.0 | 3.0 | +1.0 |
| 游戏界面实现 | 3.0 | 4.0 | +1.0 |
| 路径与碰撞逻辑实现 | 2.0 | 2.0 | 0 |
| 关卡设计 | 1.0 | 1.5 | +0.5 |
| AIGC 辅助开发 | 3.0 | 5.0 | +2.0 |
| 测试与修改 | 1.0 | 2.0 | +1.0 |
| README 与博客撰写 | 2.0 | 3.0 | +1.0 |
| 合计 | 15.0 | 21.5 | +6.5 |
七、心得体会
这次作业让我第一次完整地经历了“需求分析 → 设计 → 编码 → 测试 → 文档”的开发流程。
AI 带来的帮助很大。 作为一个 Python 和 Pygame 的新手,如果只靠查文档,我可能需要花两三倍的时间才能做出这个游戏。AI 帮我快速搭好了框架,还在我遇到方向错误、字体崩溃、动画不流畅等问题时,帮我定位并给出解决方案。可以说,没有 AI,这次作业不可能完成得这么顺利。
但 AI 也带来了一些问题。 比如它一开始生成的方向映射就是错的,导致箭头指向和飞出方向相反;它生成的海洋背景光柱太亮,把文字都盖住了。这说明 AI 的代码不能直接照搬,必须自己运行、观察、修改。AI 给的是方案,判断和取舍要靠自己。
我的收获是:
- 学会了用 Pygame 做 2D 游戏的基本方法:状态机、事件循环、逐帧渲染;
- 理解了路径检测这种“二维数组上的算法”如何实现,以及边界条件的重要性;
- 明白了 AIGC 是助手,不是替代品;
- 体会到“迭代”的价值,从 demo1 到 main.py,每一步都在改进。
这个游戏虽然简单,但它是我独立完成的第一个完整小游戏,收获很大。
八、GitHub 仓库
仓库地址:Arrow_game
仓库包含:
main.py:最终版程序demo1.py~demo4.py:4 个迭代版本README.md:项目说明screenshots/:游戏截图和 GIF
Commit 记录体现了完整的迭代过程:初始版本 → 修复方向 bug → 增加计时和确认框 → 增强第四关和烟花 → 海洋主题。
最初的界面是这样的


浙公网安备 33010602011771号