张馨恬软工第二次作业
| 项目 | 内容 |
|---|---|
| 这个作业属于哪个课程 | <202601软件工程> |
| 这个作业要求在哪里 | <2026秋软件工程个人作业(第二次)> |
| 这个作业的目标 | 使用 Python 和 AIGC 完成“一箭又一箭”小游戏 |
| 学号 | <102401603> |
| GitHub 仓库 | https://github.com/daezxt/arrow-game/tree/main |
一、项目展示
1. 开始界面

2. 游戏过程


3. 通关界面

4. 失败界面

6. 完整游戏展示
二、项目介绍
1. 游戏简介
本项目实现了一个“一箭又一箭”风格的点击式解谜小游戏。玩家需要观察棋盘中火箭的方向和相互阻挡关系,按照正确顺序点击火箭,使火箭依次飞出棋盘。若火箭前方没有其他火箭阻挡,则飞出并被消除;若前方存在其他火箭,则无法飞出,并产生碰撞反馈,同时消耗一次失误次数。
2. 游戏规则
- 棋盘中包含若干火箭,方向分为上、下、左、右四种。
- 玩家通过鼠标点击选择火箭。
- 点击火箭后,程序检查该火箭前进方向上是否被其他火箭阻挡。
- 若前方无阻挡,火箭飞出棋盘并消失。
- 若前方有阻挡,火箭不能消失,并给出明显碰撞反馈。
- 点击被阻挡的火箭会消耗一次失误机会。
- 清除本关全部火箭后,进入下一关。
- 失误次数耗尽时,本关失败,可以重新开始。
- 游戏提供重新开始按钮,可将当前关卡恢复到初始状态。
- 全部关卡通关后,从头开始。
3. 界面设计
游戏界面主要包括:
- 开始界面:显示游戏标题、开始按钮、操作说明。
- 游戏界面:显示当前关卡、棋盘、剩余火箭数量、剩余失误次数、右下角重新开始按钮。
- 结果界面:显示通关或失败信息,并提供进入下一关或重新开始按钮。
界面采用渐变背景、圆角卡片、阴影和按钮悬停效果,整体风格统一。
4. 主要功能
- 图形化游戏界面,包含开始界面、游戏界面、通关界面和失败界面;
- 棋盘中显示上、下、左、右四种方向的火箭;
- 鼠标点击火箭进行交互,飞出或者碰撞;
- 实现路径阻挡检测,正确判断火箭前方是否有其他火箭;
- 火箭飞出动画:可飞出的火箭沿方向飞出屏幕后消失;
- 碰撞反馈动画:被阻挡的火箭会左右晃动;
- 失误次数统计与显示,失误次数耗尽后本关失败;
- 设计 5 个可正常通关的关卡,棋盘为 5×5;
- 支持通关后进入下一关、失败后重新开始、游戏中重新开始;
- 全部通关后从头开始。
5. 项目特色
- 使用 5×5 棋盘,5 个关卡,遮挡层次逐渐加深;
- 每个火箭随机分配一种颜色,整局固定,界面更活泼;
- 火箭带描边和细节(机身、机头、尾翼、尾焰、舷窗);
- 四个方向的火箭由同一个朝上的火箭旋转得到,形状完全一致;
- 界面美化:渐变背景、圆角卡片、阴影、按钮悬停变色;
- 飞出动画:火箭沿方向飞出屏幕后消失,而不是瞬间消失;
- 碰撞反馈:被阻挡的火箭左右晃动。
三、实现思路
1. 开发环境
- 操作系统:Windows 11
- 编程语言:Python 3.13
- 图形库:Pygame 2.6.1
- 开发工具:VS Code
- AIGC 工具:DeepSeek
2. 项目结构
arrow-game/
├── main.py
├── levels.py
└── README.md
3. 火箭与方向的表示
本项目使用二维数组表示整个棋盘,每个格子存储一个方向字符串或 None。None 表示该格子为空,没有火箭。
方向使用字符串表示:
'U' # 上
'D' # 下
'L' # 左
'R' # 右
例如,一关的棋盘数据如下:
LEVELS = [
[
['U', 'U', None, 'U', 'U'],
['L', None, 'R', None, 'R'],
[None, 'D', 'R', 'U', None],
['L', None, 'L', None, 'R'],
['D', 'D', None, 'D', 'D'],
],
...
]
棋盘为 5×5,board[r][c] 表示第 r 行第 c 列的格子:
- 如果
board[r][c]是'U'、'D'、'L'、'R',说明该格有对应方向的火箭; - 如果
board[r][c]是None,说明该格为空。
绘制时,根据 board[r][c] 的方向,把同一个朝上的火箭 Surface 旋转到对应方向,再画到格子中心。
4. 路径检测方法
路径检测是游戏的核心逻辑。点击某个火箭后,根据火箭方向判断其前方是否存在其他火箭。
基本思路:
- 获取当前火箭的行、列、方向;
- 根据方向确定检测路径:
- 向上:同一列,行号减小;
- 向下:同一列,行号增大;
- 向左:同一行,列号减小;
- 向右:同一行,列号增大;
- 从当前火箭位置出发,沿对应方向逐格检查;
- 如果遇到其他火箭,则说明被阻挡,返回
False; - 如果直到边界都没有遇到其他火箭,则说明可以飞出,返回
True。
实际代码如下:
def can_fly(self, r, c):
arrow = self.board[r][c]
if arrow is None:
return False
if arrow == 'U':
for i in range(r - 1, -1, -1):
if self.board[i][c] is not None:
return False
elif arrow == 'D':
for i in range(r + 1, self.rows):
if self.board[i][c] is not None:
return False
elif arrow == 'L':
for j in range(c - 1, -1, -1):
if self.board[r][j] is not None:
return False
elif arrow == 'R':
for j in range(c + 1, self.cols):
if self.board[r][j] is not None:
return False
return True
说明:
- 向上检测时,使用
range(r - 1, -1, -1),从当前行的上一行开始,一直检查到第 0 行; - 向下检测时,使用
range(r + 1, self.rows),从当前行的下一行开始,一直检查到最后一行; - 向左检测时,使用
range(c - 1, -1, -1),从当前列的左边一列开始,一直检查到第 0 列; - 向右检测时,使用
range(c + 1, self.cols),从当前列的右边一列开始,一直检查到最后一列。
边界处理:
range的结束位置使用-1或self.rows/self.cols,保证不会越界;- 如果路径上全是
None,循环结束后返回True,表示可以飞出; - 如果中途遇到非
None,立即返回False,表示被阻挡。
5. 碰撞反馈实现
当火箭被阻挡时,本项目通过晃动给出反馈,同时失误次数减 1。
实际实现方式:
- 在
animations列表里记录一个shake动画,包含格子位置和计时; - 点击被阻挡的火箭时,把该火箭的
(r, c)和"shake": True加入animations; - 每帧在
update_animations里让anim["t"]增加,超过 0.4 秒后移除; - 绘制时,如果发现当前格子是晃动动画,就根据
anim["t"]的奇偶左右偏移,形成晃动效果。
相关代码:
def update_animations(game, dt):
for anim in game.animations[:]:
anim["t"] += dt
if anim.get("shake"):
if anim["t"] > 0.4:
game.animations.remove(anim)
else:
if anim["t"] > 1.5:
game.animations.remove(anim)
绘制时:
if anim.get("shake"):
arrow = game.board[r][c]
if arrow is None:
continue
offset = int(6 * (1 if int(anim["t"] * 20) % 2 == 0 else -1))
cx += offset
说明:
anim["t"]控制晃动持续时间,数值越大晃动越久;int(anim["t"] * 20) % 2决定当前帧向左还是向右偏移,形成左右晃动;- 偏移量 6 像素,幅度不大但能明显看到;
- 触发碰撞时失误次数减 1,达到 0 时本关失败。
6. 飞出动画实现
原来火箭飞出时是瞬间消失,后来改成沿方向飞出屏幕后再消失。
实现方式:
- 点击可飞出的火箭时,从
board里移除该火箭,同时把(r, c, dir)加入animations,"shake"为False; - 绘制时,棋盘只画没有动画的火箭;动画层单独画所有动画中的火箭;
- 飞出动画按
speed = 1200像素/秒沿方向移动,anim["t"]超过 1.5 秒后移除。
相关代码:
else:
arrow = anim["dir"]
speed = 1200
dist = speed * anim["t"]
if arrow == 'U':
cy -= dist
elif arrow == 'D':
cy += dist
elif arrow == 'L':
cx -= dist
elif arrow == 'R':
cx += dist
说明:
- 飞出动画独立于
board单独绘制,因为火箭已经从board里移除了; - 棋盘绘制时用
anim_cells跳过正在动画的格子,避免重复画; - 飞出速度
1200像素/秒,1.5 秒足够飞出屏幕。
7. 关卡表示与切换
本项目使用二维数组表示关卡。每个关卡是一个 5×5 的二维列表,格子中存储方向字符串或 None。所有关卡统一使用 3 次失误机会。
关卡数据示例:
LEVELS = [
[
['U', 'U', None, 'U', 'U'],
['L', None, 'R', None, 'R'],
[None, 'D', 'R', 'U', None],
['L', None, 'L', None, 'R'],
['D', 'D', None, 'D', 'D'],
],
...
]
关卡加载逻辑:
def load_level(self, index):
raw = LEVELS[index]
self.board = [row[:] for row in raw]
self.rows = len(self.board)
self.cols = len(self.board[0])
self.mistakes_left = 3
self.remaining = sum(1 for row in self.board for cell in row if cell)
palette = [...]
self.colors = {}
for r in range(self.rows):
for c in range(self.cols):
if self.board[r][c] is not None:
self.colors[(r, c)] = random.choice(palette)
说明:
LEVELS[index]取出对应关卡;- 使用
row[:]复制每一行,避免直接修改原始关卡数据; - 加载时重置失误次数、剩余火箭数、火箭颜色和动画状态;
- 游戏状态切换到
"playing"。
通关与切换:
- 每次成功消除一个火箭后,
remaining减 1; - 如果
remaining为 0,把状态切换为"win"; - 通关界面显示“下一关”按钮,点击后
level_index加一,加载下一关; - 如果是最后一关,状态切换为
"all_win",显示“恭喜通关”界面。
失败与重新开始:
- 每次被阻挡时,失误次数减 1;
- 如果失误次数达到 0,把状态切换为
"lose"; - 失败界面显示“重新开始”按钮,点击后重新加载当前关卡。
游戏中也可以点击右下角“重新开始”按钮,将当前关卡恢复到初始状态。
8. 游戏状态管理
本项目使用一个全局变量 scene 来表示当前游戏状态,通过判断 scene 的值来决定绘制哪个界面、响应哪些点击事件。
状态定义:
scene = "start" # 开始界面
scene = "playing" # 游戏中
scene = "result" # 通关或失败
scene = "all_win" # 全部通关
四个状态分别对应:
"start":开始界面;"playing":游戏中;"result":通关或失败;"all_win":全部通关。
状态切换逻辑:
- 点击“开始游戏”按钮时,
level_index重置为 0,创建Game(0),状态切换为"playing"; - 在游戏中,如果
remaining为 0,状态切换为"result",result_win = True; - 在游戏中,如果
mistakes_left为 0,状态切换为"result",result_win = False; - 点击“下一关”按钮时,如果还有下一关,
level_index加一,进入下一关;否则状态切换为"all_win"; - 点击“重新开始”按钮时,调用
restart()重新加载当前关卡; - 点击“从头开始”按钮时,重新创建
Game(0),状态切换为"playing"。
主循环里根据状态决定绘制内容:
if scene == "start":
start_btn = draw_start_screen(screen)
elif scene == "playing":
draw_gradient_background(screen)
update_animations(game, dt)
draw_hud(screen, game)
restart_btn = draw_board(screen, game)
elif scene == "result":
result_btn = draw_result_screen(screen, game, result_win)
elif scene == "all_win":
all_win_btn = draw_all_win_screen(screen)
说明:
- 开始界面和游戏界面分开绘制;
- 通关、失败和全部通关界面独立绘制;
- 点击事件也按状态分发,避免在开始界面误触棋盘逻辑。
四、AIGC 使用过程
AIGC 使用记录 1
| 项目 | 内容 |
|---|---|
| 子任务 | 环境配置与 Pygame 兼容性问题 |
| 借助何种 AIGC 技术 | DeepSeek |
| 我的提问 / 要求 | 运行 python main.py 时报 ModuleNotFoundError: No module named 'pygame',以及 SysFont 在 Windows 上读字体注册表报 TypeError |
| AI 实现或提供了什么 | 建议先激活 .venv 再运行,或者直接用 .venv\Scripts\python.exe main.py;对于 SysFont 报错,建议改用 pygame.font.Font("C:/Windows/Fonts/simhei.ttf", size) |
| 效果如何 | 虚拟环境用对后 pygame 能正常导入;字体改用文件路径后不再报错 |
| 人工修改 | 把 FONT_PATH 固定为 C:/Windows/Fonts/simhei.ttf,并确认 Windows 上存在该字体 |
AIGC 使用记录 2
| 项目 | 内容 |
|---|---|
| 子任务 | 火箭绘制与方向一致性 |
| 借助何种 AIGC 技术 | DeepSeek |
| 我的提问 / 要求 | 左右方向的火箭和上下方向的火箭长得不一样,机头和机身分离 |
| AI 实现或提供了什么 | 建议只画一个朝上的火箭 Surface,然后用 pygame.transform.rotate 旋转到对应方向,四个方向形状完全一致 |
| 效果如何 | 四个方向火箭形状统一,不再出现机头机身分离 |
| 人工修改 | 加入 _rocket_cache 缓存,同一颜色只生成一次,避免每帧重复旋转 |
AIGC 使用记录 3
| 项目 | 内容 |
|---|---|
| 子任务 | 飞出动画 |
| 借助何种 AIGC 技术 | DeepSeek |
| 我的提问 / 要求 | 火箭飞出棋盘时,不想让它瞬间消失,想做成沿方向飞出的动画 |
| AI 实现或提供了什么 | 建议用 animations 列表记录正在飞出的火箭,每帧更新坐标,飞出屏幕后删除;并提醒棋盘绘制要跳过正在动画的格子 |
| 效果如何 | 动画比较自然,火箭沿方向飞出屏幕后消失 |
| 人工修改 | 调整 speed = 1200,动画时长 1.5 秒,并加入 anim_cells 避免重复绘制 |
AIGC 使用记录 4
| 项目 | 内容 |
|---|---|
| 子任务 | 界面美化 |
| 借助何种 AIGC 技术 | DeepSeek |
| 我的提问 / 要求 | 界面太单调,想加渐变背景、圆角卡片、阴影和按钮悬停效果 |
| AI 实现或提供了什么 | 给出了渐变背景逐行绘制、圆角卡片用 border_radius、阴影用偏移矩形、按钮悬停用 mouse.get_pos() 判断的方案 |
| 效果如何 | 整体风格统一,视觉上比纯色界面好很多 |
| 人工修改 | 调整了配色(淡蓝渐变、白色卡片、蓝色/绿色按钮),并统一了字体和间距 |
AIGC 使用记录 5
| 项目 | 内容 |
|---|---|
| 子任务 | 关卡设计 |
| 借助何种 AIGC 技术 | DeepSeek |
| 我的提问 / 要求 | 第 2 关有两个火箭互相卡住,无法通关;想把关卡改复杂但不能有无法出去的 |
| AI 实现或提供了什么 | 分析了互锁原因(同一行/列两个火箭面对面),给出了避免互锁的设计原则,并提供了 5 个可通关关卡 |
| 效果如何 | 5 个关卡都能正常通关,遮挡层次逐渐加深 |
| 人工修改 | 逐关试玩验证,确认每关都有合理通关顺序 |
五、测试结果
1. 测试环境
- 操作系统:Windows 11
- Python 版本:Python 3.13
- 图形库版本:Pygame 2.6.1
- 运行方式:在项目目录下执行
python main.py
2. 测试用例与结果
| 编号 | 测试内容 | 预期结果 | 实际结果 | 是否通过 |
|---|---|---|---|---|
| T01 | 点击前方无阻挡的火箭 | 火箭飞出棋盘并消失 | 火箭沿方向飞出,飞出屏幕后消失 | 通过 |
| T02 | 点击前方有阻挡的火箭 | 火箭不消失,失误次数减 1 | 火箭左右晃动,失误次数减 1 | 通过 |
| T03 | 点击位于边缘且朝向棋盘外的火箭 | 火箭正常消失,不发生越界错误 | 火箭正常飞出,无越界报错 | 通过 |
| T04 | 消除本关全部火箭 | 显示通关并进入下一关 | 显示“通关!”,点击“下一关”进入下一关 | 通过 |
| T05 | 失误次数耗尽 | 显示失败并允许重新开始 | 显示“失败!”,点击“重新开始”恢复 | 通过 |
| T06 | 游戏进行中重新开始 | 火箭布局和失误次数恢复 | 点击“重新开始”后布局和失误次数都恢复 | 通过 |
3. 补充测试
除 T01–T06 外,还进行了以下补充测试:
- 连续点击同一个能飞出的火箭:第一次飞出后,该格子变为空,再次点击不再有反应;
- 点击空白格子:不触发任何操作;
- 点击棋盘外区域:不触发任何操作,游戏状态不受影响;
- 多次重新开始:关卡布局和失误次数都能正确恢复;
- 窗口关闭:点击右上角关闭按钮,程序正常退出。
4. 测试结论
经过测试,本项目的基础功能均能正常运行:
- 开始界面、游戏界面、通关界面、失败界面和全部通关界面显示正常;
- 四种方向的火箭都能正确显示和响应点击;
- 路径判断正确,能区分前方是否有火箭阻挡;
- 位于边缘、朝向棋盘外的火箭能正常飞出,没有出现越界错误;
- 被阻挡的火箭会晃动,并消耗一次失误机会;
- 5 个关卡都经过实际试玩,均能正常通关;
- 失误次数耗尽后显示“失败”,可以重新开始;
- 游戏中可以随时重新开始,状态能正确恢复。
测试过程中发现并修复的问题:
- 火箭飞出动画最初没有显示,原因是
board里已经清空、绘制循环找不到火箭;改为动画层独立绘制后修复; - 左右方向火箭机头机身分离,改为统一朝上绘制再旋转后修复;
- 第 2 关曾有两个火箭互锁,重新设计关卡后修复。
六、PSP 表格
| 任务 | 预估耗时(小时) | 实际耗时(小时) | 差异(小时) |
|---|---|---|---|
| 需求分析与游戏设计 | 2 | 1.5 | -0.5 |
| Python 与图形库学习 | 2 | 2 | 0 |
| 游戏界面实现 | 2 | 2.5 | +0.5 |
| 路径与碰撞逻辑实现 | 3 | 2 | -1 |
| 关卡设计 | 2 | 2.5 | +0.5 |
| AIGC 辅助开发 | 4 | 3 | -1 |
| 测试与修改 | 3 | 3 | 0 |
| README 与博客撰写 | 1 | 1 | 0 |
| 合计 | 19 | 17.5 | -1.5 |
七、心得体会
1. AIGC 带来的帮助
- 帮我快速了解 Pygame 的基本用法,包括窗口创建、事件循环、绘制图形和文字;
- 帮我生成路径检测的初版代码,让我先有一个可运行的版本,再在此基础上修改;
- 帮我设计关卡的数据结构,确定用二维数组存储方向和空位;
- 帮我定位路径检测中的越界问题、字体加载问题、虚拟环境问题;
- 帮我实现火箭绘制、碰撞晃动、飞出动画和界面美化;
- 帮我想界面美化的方向,给出渐变背景、圆角卡片、按钮悬停等建议;
- 帮我分析关卡互锁原因,给出可通关的关卡设计。
2. 遇到的问题
- 虚拟环境没激活,导致
ModuleNotFoundError: No module named 'pygame'; pygame.font.SysFont在 Windows 上读字体注册表报TypeError,改用字体文件路径解决;- 左右方向火箭机头机身分离,改为统一朝上绘制再旋转后解决;
- 飞出动画最初没有显示,改为动画层独立绘制后解决;
- 第 2 关曾有两个火箭互锁,重新设计关卡后解决;
- 通关后闪退,排查后确认是场景切换逻辑问题,调整后解决。
3. 我的收获
这是我第一次使用 Python 和 Pygame 开发一个完整的小游戏。从环境配置、虚拟环境激活,到路径检测、动画实现、界面美化,每一步都遇到了不同的问题。AI 帮我快速搭起了可运行的框架,但真正让程序稳定、好玩、好看的,还是自己反复试玩、调试和修改。
我学会了:
- 如何用二维数组表示棋盘和关卡;
- 如何用方向字符串和循环实现路径检测;
- 如何用
animations列表管理飞出和晃动动画; - 如何用
pygame.transform.rotate保持四个方向形状一致; - 如何用渐变、圆角、阴影、悬停美化界面;
- 如何设计可通关、有层次、不互锁的关卡。
这次作业让我对 Pygame 和游戏开发流程有了完整的认识,也让我更清楚 AI 能帮什么、自己必须做什么。
浙公网安备 33010602011771号