软件工程第二次个人作业
| 项目 | 内容 |
|---|---|
| 这个作业属于哪个课程 | H202601 软件工程与软件工程实践 |
| 这个作业要求在哪里 | 软件工程课程第二次个人作业 |
| 这个作业的目标 | 使用 Python 和 AIGC 完成“一箭又一箭”小游戏 |
| 学号 | 102401633 |
| GitHub 仓库 | https://github.com/Clu3y/Arr0w-voyage |
一、项目概览
项目包含开始界面、三个普通关卡、自定义配置页、游戏主界面、通关与失败结算页,以及提示、增加失误次数、移出三类道具。开发过程中还加入了箭头飞出动画、碰撞反馈、红心失误显示、背景音乐与音效。
| 功能模块 | 实现内容 |
|---|---|
| 核心玩法 | 点击箭头,判断其前方是否有阻挡;无阻挡时飞出,有阻挡时消耗失误次数 |
| 普通关卡 | 三个可解关卡,失误次数和道具状态跨关保留 |
| 道具系统 | 提示、增加失误次数、移出,普通模式下各可使用一次 |
| 自定义模式 | 支持 1~10 的 N×N 棋盘、三类道具各 0~9 次和空格比例配置 |
| 随机棋盘 | 使用反向放置法生成棋盘,保证生成结果一定可解 |
| 视觉反馈 | 飞出动画、碰撞晃动、变色、提示高亮和 Toast 消息 |
| 工程实践 | 模块拆分、pytest 自动化测试、SDL 冒烟测试和完整文档 |
关卡设计
| 关卡 | 棋盘 | 箭头数量 | 设计目的 |
|---|---|---|---|
| 第一关 | 5×5 | 8 | 熟悉点击、方向和阻挡判断 |
| 第二关 | 5×5 | 20 | 增加箭头密度和多层依赖 |
| 第三关 | 6×6 | 36 | 使用满棋盘检验完整规划能力 |
二、项目展示

开始界面 |
游戏界面 |
本关通过 |
全部通关 |
本关失败 |
箭头飞出动画 |
三、玩法与界面设计
1. 游戏规则
棋盘由若干方格组成,每个箭头格包含上、下、左、右四个方向之一。点击箭头后,程序沿着箭头方向检查它到棋盘边缘之间的格子:
- 如果路径上没有其他箭头,箭头飞出棋盘并消失;
- 如果路径上存在其他箭头,箭头保留,并消耗一次失误机会;
- 清空当前关卡全部箭头后进入下一关;
- 普通模式共有三颗红心,失误次数在三个关卡之间保留;
- 失误次数耗尽后显示失败结果,可重新开始。
2. 界面与视觉风格
为了避免明显的模板化游戏风格,界面没有采用高饱和渐变、厚重阴影和悬浮卡片,而是使用暖色背景、细线、清晰的箭头素材和少量强调色。按钮、方格、道具框、提示框和鼠标素材保持统一风格,并通过九宫格缩放避免圆角在拉伸后变形。
界面中的箭头飞出、碰撞晃动、红心变化、道具禁用和确认弹窗都提供了明确反馈,让玩家能够理解“为什么这次操作成功或失败”。
3. 图片、音效与背景音乐来源
项目中的图片、音效和背景音乐均使用 CC0 素材,没有使用原商业游戏的美术、代码或关卡资源。
| 素材类型 | 来源 | 项目中的用途 | 许可证 |
|---|---|---|---|
| 界面与棋盘图片 | Kenney | 箭头、道具、方格、鼠标、按钮、提示框和部分界面背景 | CC0 / Public Domain |
| 操作音效 | Kenney | 按钮悬停、箭头飞出、碰撞失误、通关和失败 | CC0 / Public Domain |
| 背景音乐 | OpenGameArt | 开始界面和游戏中循环播放的 bgm.ogg |
CC0 / Public Domain |
| 音效文件 | 使用位置 |
|---|---|
click.ogg |
鼠标进入可交互按钮时播放 |
fly.ogg |
箭头成功飞出时播放 |
miss.ogg |
箭头被阻挡并消耗失误次数时播放 |
win.ogg |
本关通过或全部通关时播放 |
fail.ogg |
失误次数耗尽时播放 |
四、核心实现思路
1. 使用二维数组表示棋盘
棋盘使用二维列表表示:
# 每个格子要么为空(None),要么保存 "UP"、"DOWN"、"LEFT"、"RIGHT"
# 二维列表使路径判断可以脱离 Pygame 单独测试
Board = list[list[str | None]]
其中 None 表示空位置,"UP"、"DOWN"、"LEFT"、"RIGHT" 表示四种箭头方向。路径判断等纯逻辑与 Pygame 绘制分离,因此测试时不需要打开游戏窗口,既降低了调试成本,也方便覆盖边界情况。
2. 路径阻挡判断
每个方向先映射为行列偏移量,例如 UP = (-1, 0)、RIGHT = (0, 1)。然后从箭头所在格开始逐格扫描,遇到任意非空单元格即判定为受阻:
def can_fly_out(board: Board, row: int, col: int) -> bool:
# 1. 读取当前箭头方向,并转换成行、列的移动步长
direction = board[row][col]
delta_row, delta_col = DIRECTIONS[direction]
# 2. 从箭头前方的第一格开始扫描
row += delta_row
col += delta_col
while 0 <= row < len(board) and 0 <= col < len(board[0]):
# 3. 前方只要出现任意箭头,就说明路径被阻挡
if board[row][col] is not None:
return False
# 4. 没有阻挡则继续沿当前方向前进
row += delta_row
col += delta_col
# 5. 未遇到阻挡并走出棋盘,说明该箭头可以飞出
return True
3. 状态管理与动画
游戏使用枚举维护 START、CUSTOM_CONFIG、PLAYING、LEVEL_COMPLETE、GAME_COMPLETE、CUSTOM_COMPLETE 和 LOSE 等状态。点击事件、界面绘制与结算逻辑根据当前状态分派,避免不同页面的按钮互相误触。
动画状态没有直接混在绘制代码中,而是集中记录飞出箭头、碰撞格、提示格和 Toast 计时。最后一支箭头飞出时,程序会等待动画结束再显示结果页,避免画面突然切换。
4. 生成保证可解的随机棋盘
内置关卡和自定义棋盘最终都使用同一种二维结构。区别在于,内置关卡使用可读的字符布局描述棋盘,自定义模式则先选择箭头位置,再动态计算方向并把箭头真正写入 board[row][col]。
4.1 内置关卡:把字符布局转换成箭头
为了便于阅读和维护,关卡文件先使用 ↑、↓、←、→ 表示方向。读取关卡时,再把字符逐个转换成程序内部使用的方向名称:
DIRECTION_SYMBOLS = {
"↑": "UP",
"↓": "DOWN",
"←": "LEFT",
"→": "RIGHT",
}
def _parse_level(rows: tuple[str, ...]) -> Board:
# 每一行字符串按字符拆分;"." 不在映射表中,因此会转换为 None
return [
[DIRECTION_SYMBOLS.get(symbol) for symbol in row]
for row in rows
]
例如,字符布局 "....↑" 和 ".↑..." 会被转换成:
[
[None, None, None, None, "UP"],
[None, "UP", None, None, None],
]
4.2 自定义棋盘:随机选择位置并放置箭头
自定义棋盘的核心目标有两个:第一,箭头位置和方向要保持随机;第二,生成结果必须存在通关顺序。项目采用“反向放置”思路,即每次放置箭头时都确保它当前可以飞向棋盘边缘,这样按放置顺序的逆序点击就一定可以清空棋盘。
# 1. 从所有格子中选出需要放置箭头的位置
selected = generator.sample(positions, arrow_count)
# 2. 从中心向外放置,保证箭头写入时到某一边缘的路径仍然为空
selected.sort(
key=lambda cell: min(
cell[0],
cell[1],
board_size - 1 - cell[0],
board_size - 1 - cell[1],
),
reverse=True,
)
# 3. 创建空棋盘,再逐个把箭头写入对应位置
board = [[None for _ in range(board_size)] for _ in range(board_size)]
previous_direction = None
for row, col in selected:
clear_directions = []
# 临时写入候选方向,复用 can_fly_out 判断该方向当前是否畅通
for direction in DIRECTIONS:
board[row][col] = direction
if can_fly_out(board, row, col):
clear_directions.append(direction)
board[row][col] = None
if not clear_directions:
continue
# 只有一个畅通方向时必须选择它,否则该位置可能无法完成放置
if len(clear_directions) == 1:
chosen_direction = clear_directions[0]
else:
# 重复方向权重为 1,其他畅通方向权重为 4,减少连续同方向
weights = [
1.0 if direction == previous_direction else 4.0
for direction in clear_directions
]
chosen_direction = generator.choices(
clear_directions,
weights=weights,
k=1,
)[0]
# 将最终选择的方向真正写入二维棋盘
board[row][col] = chosen_direction
previous_direction = chosen_direction
return board
这里的 selected 决定箭头出现在哪些格子,循环中的 board[row][col] = direction 用于临时测试方向,而 board[row][col] = chosen_direction 负责把最终箭头写入棋盘。由于每次放置时都能飞出,最终棋盘按照放置顺序的逆序操作一定可以清空。
方向选择没有简单禁用上一个方向,而是采用 1:4 的权重降低连续重复概率。如果只有两个畅通方向,重复概率约为 1/5;有三个畅通方向时约为 1/9。如果只有一个畅通方向,仍然直接选择它,以保证棋盘可解性不受影响。
五、AIGC 使用过程
| 子任务 | 借助何种 AIGC 技术 | 我提出的要求 | AI 实现或提供了什么 | 实际效果如何 | 是否进行人工修改 |
|---|---|---|---|---|---|
| 需求分析与项目骨架 | Codex | 分析第二次作业要求,拆解评分点,给出开发路线,并把路径逻辑设计成可以独立测试的结构 | 拆解游戏功能;生成项目骨架、路径检测函数和初始测试 | 项目目录和模块边界清晰,路径逻辑首批 8 项测试通过 | 是。自行创建虚拟环境并安装 Pygame、pytest;冒烟测试发现字体枚举异常后,改用 Pygame 自带字体回退 |
| 开始界面与视觉重构 | Codex | 实现开始、游戏和结算界面,让鼠标交互有清晰反馈 | 生成 HUD、棋盘、箭头、按钮、状态切换和动画代码,初始版本偏常见渐变卡片风格 | 功能可以运行,但视觉模板感较强,部分控件和文案位置不够协调 | 是。删除蓝色渐变和厚重阴影;重新调整标题、按钮、道具和提示框位置 |
| 道具与模块化拆分 | Codex | 让提示、增加失误次数和移出道具可靠工作,并拆出独立模块方便后续扩展 | 实现三种道具、动画状态、音频接口以及 tools.py、animations.py、audio.py 等模块 |
道具逻辑和界面模块可以独立导入 | 是。逐项回归道具消耗、跨关保留、重新开始和失败流程;将移出操作改为先选道具、再点目标箭头,避免依赖悬停状态 |
| 自定义模式与可解随机棋盘 | Codex | 增加自定义配置页,支持棋盘大小、道具次数和难度设置,并要求随机棋盘始终可解 | 新增 custom_mode.py、选项滑块和反向放置式棋盘生成器,并接入成功、失败和重开流程 |
自定义模式主体可用;批量验证时发现空格曾被错误传给路径判断函数,滑块右侧也出现数值重叠 | 是。临时放入候选方向完成判断后再恢复空格;把圆形旋钮改为五行长条并调整布局;验证不同尺寸和难度均能清空 |
| 连续同方向优化 | Codex | 观察到随机棋盘连续同方向偏多,要求分析如何在不破坏可解性的前提下降低重复概率 | 提供加权随机的初步思路和可解性风险提示 | 初步思路可行,但具体权重仍需要结合实际棋盘效果调整 | 是,将重复方向权重设为 1、其他畅通方向设为 4;保留唯一畅通方向回退 |
六、测试结果
| 编号 | 测试内容 | 预期结果 | 实际结果 | 是否通过 |
|---|---|---|---|---|
| T01 | 点击无阻挡箭头 | 箭头飞出并消失 | 箭头被移除,剩余数量减少 | 是 |
| T02 | 点击有阻挡箭头 | 箭头保留,失误增加 | 箭头保留,失误次数正确减少 | 是 |
| T03 | 测试四个方向路径 | 阻挡判断全部正确 | 四个方向及边缘测试通过 | 是 |
| T04 | 清空本关箭头 | 显示通关并进入下一关 | 状态和关卡切换正确 | 是 |
| T05 | 失误次数耗尽 | 显示失败并允许重开 | 失败页与恢复流程正常 | 是 |
| U01 | 道具跨关使用 | 已用道具不跨关恢复 | 下一关保持已使用状态 | 是 |
| U02 | 随机棋盘可解性 | 不同尺寸和难度均可清空 | 批量验证通过 | 是 |
| U03 | 长条滑块交互 | 最小值与最大值映射正确 | 左右边界映射正确 | 是 |
| U04 | 方向重复抑制 | 上一方向权重为 1,其他方向为 4 | 受控随机测试通过 | 是 |
主要问题与修复
| 问题 | 原因 | 修复方式 | 回归结果 |
|---|---|---|---|
| 系统字体枚举异常 | 运行环境中的字体枚举行为不一致 | 改用 Pygame 自带字体并增加回退 | 冒烟测试通过 |
| 清空第一关后无法继续 | 只更新了本关完成标志,没有进入下一关 | 增加结果状态和关卡切换逻辑 | 通关流程通过 |
| 失败后没有结果页 | 仅提示失误,没有切换界面状态 | 增加失败状态、重试和返回首页 | 失败流程通过 |
| 随机棋盘无法生成 | 空格曾被传入路径判断函数 | 临时放置候选方向,判断后再恢复空格 | 批量可解性通过 |
| 滑块右侧数值重叠 | 滑块和值域位置过度靠右 | 控件整体左移并检查布局边界 | 交互测试通过 |
七、PSP 时间记录
| 任务 | 预估耗时(小时) | 实际耗时(小时) | 差异(小时) |
|---|---|---|---|
| 需求分析与游戏设计 | 1.0 | 1.0 | 0.0 |
| Python 与图形库学习 | 2.0 | 2.5 | +0.5 |
| 游戏界面实现 | 1.0 | 1.5 | +0.5 |
| 路径与碰撞逻辑实现 | 2.0 | 2.5 | +0.5 |
| 关卡设计 | 0.5 | 0.5 | 0.0 |
| AIGC 辅助开发 | 2.0 | 2.0 | 0.0 |
| 测试与修改 | 1.5 | 1.5 | 0.0 |
| README 与博客撰写 | 2.0 | 2.0 | 0.0 |
| 合计 | 12.0 | 13.5 | +1.5 |
总耗时比预估多 1.5 小时,超支约 12.5%。主要偏差来自 Python 与 Pygame 环境学习、界面细节调整和路径逻辑的边界验证。核心逻辑的预估相对准确,但环境配置、实际视觉体验和回归测试仍需要额外时间。
八、心得体会
通过本次作业,我深刻感受到,写出可用功能和完成完整软件工程任务,是两件完全不同的事。一开始我认为项目重点仅仅是实现箭头路径判断与游戏界面。可真正动手开发才发现,模糊的需求必须重新梳理。就像随机棋盘生成功能,看似简单,不加限制就会产生无解棋盘。于是我把它拆成随机生成、尺寸校验、空格比例控制、保证可解等多个可检查条件,并编写对应的测试。我也由此明白:只有将抽象想法变成可验证的标准,开发工作才有明确方向。
在使用 Codex 辅助开发的过程中,我最大的感受是它可以显著加快探索速度,但不能代替人的判断。它能够快速生成界面结构、路径逻辑、模块代码和测试,也确实帮我节省了大量重复劳动。不过,AI 不一定了解真实视觉质量和隐藏边界。生成的滑块在功能上没有问题,放到实际页面中却显得拥挤;随机棋盘生成器最初也存在空格被错误传入路径判断函数的问题;方向重复优化如果直接禁用上一个方向,还会破坏原本的可解性。这些问题让我明白,人的任务不是简单接受生成的代码,而是定义标准、运行程序、发现问题,并决定哪些地方应该修改。
总体来说,这次作业让我经历了从需求分析、界面设计、逻辑实现、AIGC 协作到测试和文档整理的完整过程。相比最后完成的小游戏,我更看重的是学会了如何把模糊想法拆成可执行、可检查、可修改的工程任务,也逐渐理解了 AI 在开发中应该扮演的角色:它是一个高效的协作者,而不是最终决定的替代者。
Arr0w Voyage · 一箭又一箭
Made with Python & Pygame · 2026
浙公网安备 33010602011771号