2026 秋软件工程个人作业(第二次)
作业信息
| 项目内容 | 内容 |
|---|---|
| 这个作业属于哪个课程 | H202601软件工程与软件工程实践 |
| 这个作业要求在哪里 | 2026秋软件工程个人作业(第二次) |
| 这个作业的目标 | 使用 Python 和 AIGC 完成“一箭又一箭”小游戏 |
| 学号 | 052402234 |
| GitHub 仓库 | https://github.com/monkery-king-nice/one-arrow-after-another |
1.项目展示
1.1 游戏运行视频
为了更加直观地展示游戏的实际运行效果,我录制了游戏运行过程,包括开始游戏、箭头移动、碰撞反馈、关卡切换以及最终通关等过程。
1.2 开始界面

1.3 游戏过程

1.4 通关界面

1.5 失败界面

2. 项目介绍
2.1 游戏规则
“一箭又一箭”是一款基于 Python 和 Pygame 开发的点击式箭头解谜小游戏。游戏采用 8×8 的棋盘作为主要游戏区域,棋盘中会放置不同方向的箭头。
玩家需要观察每个箭头的方向,并点击箭头进行移动:
- 箭头方向为上、下、左、右四种方向。
- 点击箭头后,程序会检测箭头沿自身方向到棋盘边缘的路径上是否存在其他箭头。
- 如果路径上没有其他箭头,当前箭头可以顺利飞出棋盘,并从棋盘中消失。
- 如果路径上存在其他箭头,则当前箭头会被阻挡,触发碰撞动画,并扣除 1 次失误次数。
- 玩家每一关初始拥有 3 次失误机会,当失误次数降为 0 后,完成当前碰撞动画即可进入失败界面。
- 当棋盘上的所有箭头都被成功消除后,即可完成当前关卡并进入下一关。
- 游戏共设计了 3 个关卡,全部完成后进入最终通关界面。
2.2 界面设计
游戏主要由开始界面、游戏界面和结果界面组成。
开始界面
开始界面用于展示游戏名称、基本玩法说明以及“开始游戏”按钮,玩家点击按钮后进入第 1 关。
游戏界面
游戏界面以 8×8 棋盘为核心区域,棋盘中显示不同方向的箭头,同时在界面上显示当前关卡、分数、剩余失误次数、计时等信息。玩家通过鼠标点击箭头进行解谜。
结果界面
当玩家完成全部关卡后,会进入通关结果界面,显示最终游戏结果、得分和星级评价,并提供重新开始游戏的操作。
2.3 主要功能
本游戏主要实现了以下功能:
- 箭头点击与方向判断:支持上、下、左、右四个方向的箭头。
- 路径检测:点击箭头后,检测其前进方向上是否存在其他箭头。
- 箭头飞出动画:没有障碍时,箭头沿自身方向移动并飞出棋盘。
- 碰撞反馈动画:箭头被阻挡时,会向障碍箭头移动、发生碰撞并震动,然后沿原路径返回。
- 失误次数管理:每次点击被阻挡的箭头会消耗 1 次失误机会。
- 多关卡系统:游戏包含 3 个可通关关卡,完成当前关卡后进入下一关。
- 计分系统:成功消除箭头可以获得分数,发生错误会扣分,完成关卡还可以获得额外奖励。
- 计时系统:记录玩家完成关卡所使用的时间,并参与最终评价。
- 星级评价:根据游戏过程中的表现进行星级评价。
- 通关与失败判定:根据箭头是否全部消除以及失误次数是否耗尽判断游戏结果。
- 重新开始功能:在通关或失败后可以重新开始游戏。
2.4 游戏特色
本游戏的主要特色在于规则简单,但需要玩家进行观察和思考。玩家不能只根据箭头本身的方向进行点击,而需要提前判断箭头前方是否存在其他箭头,因此具有一定的解谜和策略性。
此外,游戏加入了箭头飞出、碰撞、返回等动画效果,使操作结果更加直观。针对关卡中可能出现的相互阻挡问题,还增加了关卡可解性验证机制,在开发阶段通过程序模拟箭头消除过程,确保设计的 3 个关卡均能够完成。
在此基础上,游戏进一步加入了分数、计时和星级评价,使玩家除了完成关卡之外,还可以尝试用更少的失误和更短的时间获得更高评价,从而增加游戏的可玩性。
3.实现思路
3.1 箭头和棋盘的表示
本游戏采用 8×8 的二维数组表示游戏棋盘。棋盘中的每一个位置对应一个格子,通过行和列确定箭头的位置。
使用字符串表示当前格子中的箭头方向:
"up"
"down"
"left"
"right"
如果当前格子没有箭头,则使用空字符串 "" 表示。
例如第 1 关的部分数据如下:
LEVEL_1 = [
["right", "", "", "", "", "", "", ""],
["", "", "", "", "", "", "", "left"],
["down", "", "", "", "", "", "", ""],
...
]
其中:
LEVEL_1[0][0] = "right"
表示第 1 行第 1 列存在一个向右的箭头;
LEVEL_1[1][7] = "left"
表示第 2 行第 8 列存在一个向左的箭头;
LEVEL_1[2][0] = "down"
表示第 3 行第 1 列存在一个向下的箭头。
因此,一个箭头可以由以下两个信息确定:
箭头位置:行号 + 列号
箭头方向:up / down / left / right
当前实际进行游戏的棋盘使用 BOARD 保存。进入某个关卡时,将原始关卡数据复制到 BOARD:
BOARD = copy.deepcopy(LEVELS[level_index])
这样玩家消除箭头时只修改当前棋盘,不会影响原始关卡数据。
3.2 方向的表示
游戏中共有上、下、左、右四个方向。程序使用 DIRECTIONS 字典统一表示方向:
DIRECTIONS = {
"up": (0, -1),
"down": (0, 1),
"left": (-1, 0),
"right": (1, 0),
}
在棋盘的行列坐标中,四个方向对应的变化为:
up :行 - 1
down :行 + 1
left :列 - 1
right :列 + 1
这种表示方式可以将方向转换成统一的坐标变化量,使程序在进行路径检测和箭头移动时都可以使用相同的数据。
例如,当箭头方向为 right 时:
dx, dy = DIRECTIONS["right"]
可以获得对应的方向信息,然后在动画中按照该方向更新箭头的位置。
需要注意的是,棋盘的二维数组坐标和屏幕像素坐标存在区别。棋盘中主要使用“行、列”确定位置,而箭头飞行动画使用 x、y 像素坐标。因此,程序在绘制和动画过程中会将棋盘中的行列位置转换成屏幕坐标。
3.3 关卡的表示
游戏共设计了三个关卡:
LEVEL_1 = [...]
LEVEL_2 = [...]
LEVEL_3 = [...]
LEVELS = [LEVEL_1, LEVEL_2, LEVEL_3]
每一个关卡本质上都是一个 8×8 的二维数组。
程序通过:
current_level
记录当前关卡编号。其中:
0 → 第 1 关
1 → 第 2 关
2 → 第 3 关
进入新关卡时,通过:
load_level(level_index)
加载对应的棋盘数据。
def load_level(level_index):
global BOARD, current_level
current_level = level_index
BOARD = copy.deepcopy(LEVELS[level_index])
同时,进入新关卡还会重新设置本关的失误次数、得分和计时等数据,而整个游戏的累计总分不会被清空。
3.4路径检测算法设计
路径检测的基本思想
本游戏最核心的算法是判断当前点击的箭头能否直接飞出棋盘。
箭头只能沿自身指向的方向移动,因此不需要进行复杂的寻路。程序只需要从当前箭头所在的位置开始,沿箭头指向的方向逐格检查。
基本规则为:
从当前箭头开始
↓
按照箭头方向逐格检查
↓
检查路径上是否存在其他箭头
↙ ↘
存在 不存在
↓ ↓
被其他箭头阻挡 可以飞出棋盘
如果路径上存在其他箭头,则当前箭头不能飞出;如果一直检查到棋盘边界都没有发现其他箭头,则当前箭头可以飞出。
因此,本游戏的路径检测实际上是一个沿行或列进行的线性搜索。
核心路径检测函数
程序使用 can_arrow_exit() 判断当前棋盘中的箭头能否飞出:
def can_arrow_exit(row, col):
return can_arrow_exit_on_board(BOARD, row, col)
真正执行检测的是 can_arrow_exit_on_board()。
对于向右的箭头,程序从当前箭头右侧的第一个格子开始,一直检查到棋盘边界:
if direction == "right":
for c in range(col + 1, GRID_SIZE):
if board[row][c]:
return False
return True
其含义是:
- 当前箭头位于
(row, col); - 从
col + 1开始检查右侧格子; - 如果发现某个格子不为空,说明该位置存在其他箭头;
- 当前箭头的路径被阻挡,返回
False; - 如果一直检查到棋盘边界都没有发现箭头,则返回
True。
其他三个方向采用相同的思想。
向左:
elif direction == "left":
for c in range(col - 1, -1, -1):
if board[row][c]:
return False
return True
向下:
elif direction == "down":
for r in range(row + 1, GRID_SIZE):
if board[r][col]:
return False
return True
向上:
elif direction == "up":
for r in range(row - 1, -1, -1):
if board[r][col]:
return False
return True
因此,路径检测的关键并不是寻找一条新的路径,而是判断:
当前箭头 → 棋盘边界
这一条直线路径上是否存在其他箭头。
路径检测示例
例如当前棋盘某一行存在:
→ → 空 空
A B
假设 A 是向右的箭头。
程序会从 A 右侧的格子开始检查:
A → B
因为 B 是一个箭头,所以检测到障碍后直接返回:
False
此时 A 不能飞出。
如果玩家先点击 B,并且 B 可以飞出,那么程序会将 B 从 BOARD 中删除:
BOARD[fa["row"]][fa["col"]] = ""
此时棋盘变成:
→ 空 空 空
A
玩家再次点击 A 时,程序重新进行路径检测。由于 A 到棋盘边界之间已经没有其他箭头,因此返回:
True
于是 A 就可以飞出。
这也是本游戏解谜玩法的核心:通过不断消除当前能够飞出的箭头,逐渐解除其他箭头的阻挡。
3.5阻挡箭头的检测
当 can_arrow_exit() 返回 False 时,程序还需要知道究竟是哪一个箭头挡住了当前箭头。
因此设计了:
find_blocking_arrow(row, col)
该函数同样沿当前箭头的方向逐格搜索,但与路径检测不同,它的目的不是判断能否飞出,而是寻找路径上的第一个障碍箭头。
例如向右搜索:
if direction == "right":
for c in range(col + 1, GRID_SIZE):
if BOARD[row][c]:
return row, c
找到第一个箭头后立即返回其行列位置。
例如:
→ 空 ↓ → 空
A B
如果 A 向右,那么程序从 A 右侧开始检查,首先遇到 B,于是返回:
B 的行号和列号
而不是继续寻找后面的箭头。
这样程序就能够计算当前箭头应该向前移动到什么位置,然后播放:
向前移动
↓
接近障碍箭头
↓
碰撞震动
↓
原路返回
的动画效果。
3.6玩家点击后的完整处理流程
玩家点击棋盘后,程序首先根据鼠标坐标计算出对应的行和列:
cell = get_clicked_cell(board_x, board_y, event.pos)
如果点击的位置存在箭头,则读取:
direction = BOARD[row][col]
然后进行路径检测:
if can_arrow_exit(row, col):
此时有两种情况。
第一种情况是路径畅通:
点击箭头
↓
路径检测
↓
没有其他箭头阻挡
↓
启动飞出动画
↓
箭头离开棋盘
↓
从 BOARD 删除
↓
得分 +100
第二种情况是路径被阻挡:
点击箭头
↓
路径检测
↓
发现其他箭头
↓
寻找第一个障碍箭头
↓
播放碰撞动画
↓
失误次数 -1
↓
得分 -50
其中,飞出棋盘的箭头并不会在点击的瞬间立即从 BOARD 删除,而是等箭头完成飞出动画后再删除:
if is_flying_arrow_off_board(fa, board_x, board_y):
BOARD[fa["row"]][fa["col"]] = ""
add_arrow_score()
这样可以保证玩家能够看到完整的箭头飞出动画,同时棋盘数据也能正确反映箭头是否已经被消除。
3.7关卡可解性检测
为了避免设计出无法通关的关卡,程序还增加了 is_level_solvable() 函数,对关卡进行自动验证。
其基本思路是复制一份关卡数据,然后模拟玩家不断消除可以直接飞出的箭头:
board = copy.deepcopy(level)
每一轮程序寻找当前所有能够飞出的箭头:
exitable = []
for r in range(GRID_SIZE):
for c in range(GRID_SIZE):
if board[r][c] and can_arrow_exit_on_board(board, r, c):
exitable.append((r, c))
如果找到可以飞出的箭头,就模拟将其中一个删除:
r, c = exitable[0]
order.append((r, c, board[r][c]))
board[r][c] = ""
然后重新进行路径检测。
例如:
A → B → C
如果 C 可以首先飞出:
A → B
接下来 B 可以飞出:
A
最后 A 也可以飞出:
空棋盘
如果某一轮已经没有任何可以飞出的箭头,但棋盘上仍然存在箭头,则说明当前关卡出现死锁:
if not exitable:
break
最后程序检查棋盘是否还存在箭头:
for r in range(GRID_SIZE):
for c in range(GRID_SIZE):
if board[r][c]:
return False, order
如果仍有箭头,则返回 False,表示该关卡不可解;如果所有箭头都被模拟删除,则返回 True。
程序启动时通过:
verify_levels()
对三个关卡进行验证,并在控制台输出验证结果和一种可行的通关顺序。
4. AIGC 开发记录
在“一箭又一箭”的开发过程中,我使用 TRAE 和 ChatGPT 对部分功能进行辅助开发。通过 AI 生成代码、分析问题和辅助修改,再结合实际运行结果进行测试和调整,逐步完善游戏功能。
| 子任务 | 借助何种 AIGC 技术 | AI 实现或提供了什么 | 实际效果如何 | 人工修改 |
|---|---|---|---|---|
| 游戏基础框架 | TRAE | 生成 Pygame 游戏窗口、8×8 棋盘、四种方向箭头以及鼠标点击检测代码 | 游戏可以正常启动,棋盘和箭头能够正常显示,鼠标可以识别点击位置 | 运行程序进行测试,检查棋盘内外点击是否正常 |
| 箭头路径检测 | TRAE | 实现 can_arrow_exit() 函数,对 up、down、left、right 四个方向进行路径检测 |
能够判断箭头前方是否存在其他箭头,并确定当前箭头能否飞出棋盘 | 对四个方向和棋盘边界进行实际测试,并检查边界条件 |
| 关卡设计与可解性验证 | TRAE + ChatGPT | 生成关卡数组,并设计 is_level_solvable() 函数模拟箭头依次飞出 |
初始关卡中出现过箭头相互阻挡、无法继续的情况 | 根据实际试玩调整关卡布局,并通过程序验证三个关卡均可以完成 |
| 失误次数与游戏失败机制 | TRAE + ChatGPT | 增加 MAX_MISTAKES、mistakes_left 等变量,并完善 game_over 状态和重新开始流程 |
点击被阻挡箭头可以扣除失误次数,次数归零后进入游戏失败状态 | 实际测试错误点击、次数归零、失败界面和重新开始功能 |
| 被阻挡箭头碰撞动画 | TRAE + ChatGPT | 实现 find_blocking_arrow(),并增加箭头前进、碰撞震动、返回等动画状态 |
被阻挡箭头可以先向障碍箭头移动,碰撞后震动,再沿原路径返回 | 根据实际动画效果调整移动速度、碰撞距离和动画过程,并保证箭头返回后保持原来的指向 |
| 碰撞动画 Bug 修复 | TRAE + ChatGPT | 分析 draw_board() 和碰撞动画绘制逻辑,定位障碍箭头在部分动画阶段没有被绘制的问题 |
找到了障碍箭头在 moving_forward 和 moving_back 阶段短暂消失的原因 |
修改绘制逻辑并重新运行测试,确认障碍箭头能够持续显示 |
5.测试结果
| 编号 | 测试内容 | 预期结果 | 实际结果 | 测试结论 |
|---|---|---|---|---|
| T01 | 点击前方无阻挡的箭头 | 箭头沿自身方向飞出棋盘并消失 | 箭头正常飞出棋盘,完全离开后消失 | 通过 |
| T02 | 点击前方有阻挡的箭头 | 箭头不消失,失误次数减 1 | 箭头向前移动后与障碍箭头碰撞、震动并返回,失误次数减 1 | 通过 |
| T03 | 点击位于边缘且朝向棋盘外的箭头 | 箭头正常消失,不发生越界错误 | 箭头正常飞出棋盘并消失,程序运行正常 | 通过 |
| T04 | 消除本关全部箭头 | 显示通关并进入下一关 | 本关所有箭头消除后进入关卡完成状态,可以继续进入下一关 | 通过 |
| T05 | 失误次数耗尽 | 显示失败并允许重新开始 | 失误次数降至 0 后进入游戏失败状态,可以重新开始 | 通过 |
| T06 | 游戏进行中重新开始 | 箭头布局和失误次数恢复 | 重新开始后恢复第 1 关初始布局,失误次数恢复为 3 次 | 通过 |
6.PSP表格
| 任务 | 预估耗时(分钟) | 实际耗时(分钟) | 差异(分钟) |
|---|---|---|---|
| 需求分析与游戏设计 | 30 | 25 | -5 |
| Python 与图形库学习 | 60 | 75 | +15 |
| 游戏界面实现 | 30 | 20 | -10 |
| 路径与碰撞逻辑实现 | 60 | 55 | -5 |
| 关卡设计 | 30 | 35 | +5 |
| AIGC 辅助开发 | 40 | 30 | -10 |
| 测试与修改 | 20 | 50 | +30 |
| README 与博客撰写 | 30 | 60 | +30 |
| 合计 | 300 | 350 | +50 |
7.心得体会
通过本次“一箭又一箭”小游戏的开发,我对 AI 辅助软件开发有了更加直观的认识。AI 可以在代码编写、功能实现和问题分析等方面提供很大的帮助,尤其是在一些重复性较高的代码编写以及功能实现过程中,可以明显提高开发效率。例如,本项目中的箭头路径检测、关卡可解性验证、失误次数、碰撞动画等功能,都得到了 AI 的辅助。
但在实际使用过程中,我也发现 AI 生成的代码并不能完全保证正确。有些代码虽然能够运行,但实际效果与预期存在差异。例如在碰撞动画开发过程中,曾经出现障碍箭头短暂消失的问题,需要通过实际运行和观察发现 Bug,再让 AI 分析具体原因并进行修改。关卡设计过程中也发现,AI 生成的部分布局可能存在无法通关的情况,因此不能完全依赖 AI 的结果,还需要自己进行测试和验证。
通过这次实践,我认识到 AIGC 更适合作为软件开发过程中的辅助工具,而不是完全代替程序员。开发者仍然需要负责分析需求、判断代码是否合理、运行程序、发现问题并进行修改。只有将 AI 生成、人工分析和实际测试结合起来,才能得到比较可靠的最终结果。
在本次项目中,我不仅进一步熟悉了 Python 和 Pygame 的使用,也加深了对程序状态管理、路径检测、动画处理、关卡设计和软件测试的理解。同时,我也学会了如何更加准确地向 AI 描述问题,以及如何根据程序实际运行结果不断调整和完善代码。这些经验对以后使用 AI 辅助完成更复杂的软件开发任务具有一定帮助。
8.扩展功能说明
得分系统
在基础箭头消除玩法的基础上,程序增加了得分机制,使玩家的操作结果能够通过分数进行体现。
程序主要设置了以下几种得分情况:
- 成功消除一个箭头可以获得基础分数;
- 点击被阻挡的箭头会产生扣分;
- 完成本关后可以获得额外的关卡奖励;
- 如果玩家在规定时间内完成关卡,还可以根据剩余时间获得时间奖励。
程序中的主要得分参数如下:
SCORE_ARROW = 100
SCORE_PENALTY = 50
SCORE_LEVEL_BONUS = 500
TIME_BONUS_LIMIT = 30.0
TIME_BONUS_PER_SEC = 20
通过这些设置,可以鼓励玩家尽量减少错误操作,并在较短时间内完成关卡。
计时功能
游戏增加了关卡计时功能,用于记录玩家完成当前关卡所使用的时间。
每进入一个新的关卡,都会重新开始计时。玩家完成关卡后,根据本关的完成时间计算相应的时间奖励。
计时功能不仅可以作为星级评价的依据,也可以增加游戏的挑战性,使玩家不仅需要考虑箭头的消除顺序,还需要尽量提高完成速度。
星级评价
程序根据玩家完成关卡所使用的时间进行星级评价。
当前程序设置了三个时间等级:
STAR3_TIME_LIMIT = 15.0
STAR2_TIME_LIMIT = 30.0
评价规则可以概括为:
| 完成时间 | 星级评价 |
|---|---|
| 不超过 15 秒 | 三星 |
| 超过 15 秒但不超过 30 秒 | 二星 |
| 超过 30 秒 | 一星 |
因此,玩家在完成同一个关卡时,所用时间不同会得到不同的星级评价。
可执行文件打包与 GitHub 发布
为了方便其他用户直接运行游戏,我在完成 Python + Pygame 程序开发后,进一步将项目打包为 Windows 可执行文件,并发布到 GitHub Releases。
首先使用 PyInstaller 对游戏程序进行打包。通过编写 一箭又一箭.spec 配置文件,设置程序入口、程序名称以及单文件模式和无控制台窗口模式,然后执行打包命令:
pyinstaller 一箭又一箭.spec
打包完成后,在 dist/ 目录下生成了游戏的可执行文件。打包后的程序约 14 MB,可以直接在 Windows 系统中运行。经过实际启动测试,确认打包后的游戏能够正常打开并运行。
为了避免较大的二进制文件影响源代码仓库,我没有将 build/ 和 dist/ 目录提交到 Git 仓库,而是将可执行文件作为正式发行版本上传到 GitHub Releases。
具体发布过程如下:
- 在 GitHub 仓库中进入 Releases,创建新的 Release。
- 创建版本标签
v1.0。 - 填写版本标题和游戏功能说明。
- 将打包得到的
.exe文件上传到 Release 附件。 - 发布 Release。
发布完成后,其他用户可以直接通过 GitHub 仓库的 Releases 页面获取游戏的可执行文件,不需要下载源代码,也不需要提前安装 Python 和 Pygame 环境。
在发布过程中,上传中文名称的 一箭又一箭.exe 时曾出现 GitHub 将文件名显示为 default.exe 的问题。经过排查后,将可执行文件复制并重命名为英文名称 OneArrowAfterAnother_v1.0.exe 后重新上传,文件名显示恢复正常,最终成功完成发布。
通过这一过程,我不仅完成了游戏功能开发,还进一步完成了程序的打包和发布,使“一箭又一箭”从只能在开发环境中运行的 Python 程序,变成了可以直接提供给其他用户运行的 Windows 可执行程序。
浙公网安备 33010602011771号