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

2. 游戏界面

3. 碰撞反馈

4. 通关与失败界面(游戏通关新增星级评分模块)


5.扩展提示模块

二、项目介绍
“一箭又一箭”是一款点击式箭头解谜游戏。棋盘中放置了朝上、下、左、右四个方向的箭头。玩家需要观察箭头的朝向和相互阻挡关系,选择正确的消除顺序,最终清空棋盘。
- 前方没有其他箭头时,被点击的箭头会沿自身方向飞出棋盘。
- 前方存在其他箭头时,箭头会变红并回弹,同时扣除一次失误机会。
- 正确消除使用箭头飞出的短促“嗖”声,错误碰撞使用低沉警示音,玩家无需看文字也能区分操作结果。
- 清空当前关卡即可通关;失误机会耗尽则挑战失败。
- 游戏包含四个难度递增且经过求解器验证的关卡。
- 游戏添加星级评分以及提示模块以提高可玩性。
- 支持重新开始、返回主页以及
R、Esc快捷键。
游戏使用 960×720 窗口和米白色暖色主题,所有图形由 Pygame 直接绘制,两种音效由代码合成,不依赖外部图片或音频素材;音频设备不可用时会静默降级。
三、实现思路
1. 方向与关卡的数据表示
关卡使用字符串元组保存,每个字符对应一个格子:U、D、L、R 分别代表上、下、左、右,. 代表空格。方向枚举同时提供行列增量,四个方向可以共用同一套路径算法。
class Direction(str, Enum):
UP = "U"
DOWN = "D"
LEFT = "L"
RIGHT = "R"
@property
def delta(self) -> tuple[int, int]:
return {
Direction.UP: (-1, 0),
Direction.DOWN: (1, 0),
Direction.LEFT: (0, -1),
Direction.RIGHT: (0, 1),
}[self]
@dataclass(frozen=True)
class Level:
name: str
grid: tuple[str, ...]
max_mistakes: int
例如第一关的布局如下。R 与 U 在第一行,D 与 L 在第三行,中间五格为空:
Level(
name="第一关 · 初窥门径",
grid=(
"R.U",
"...",
"D.L",
),
max_mistakes=3,
)
程序加载关卡时会把不可变字符串复制成二维列表,游戏过程只修改副本,因此重新开始时可以从原始模板完整恢复棋盘。
2. 路径检测算法
路径检测是游戏的核心。程序先取得箭头方向,从相邻的下一格开始,沿 (dr, dc) 一直扫描到边界。只要途中发现非空格子,就说明路径被挡住;循环自然结束则表示箭头可以飞出。
def has_clear_path(self, row: int, col: int) -> bool:
direction = self.direction_at(row, col)
if direction is None:
return False
dr, dc = direction.delta
row += dr
col += dc
while self._inside(row, col):
if self.board[row][col] != ".":
return False
row += dr
col += dc
return True
这里有三个重要细节:
- 检查的是直到边界的整条路径,而不只是相邻格;隔着多个空格的箭头同样会形成阻挡。
- 每次访问棋盘前先调用
_inside判断坐标,因此不会出现数组越界。 - 位于边缘且朝向棋盘外的箭头,第一步就会离开棋盘,循环体不会执行,会直接返回
True。
单次检测最多扫描一行或一列,时间复杂度为 O(max(rows, cols)),空间复杂度为 O(1)。
3. 点击处理、失误和状态切换
规则层将一次点击归纳为三种结果:成功消除 REMOVED、发生碰撞 BLOCKED、无效点击 IGNORED。界面层只需要根据返回结果选择动画和音效,不需要重复实现游戏规则。
def attempt_move(self, row: int, col: int) -> MoveResult:
if self.phase is not GamePhase.PLAYING:
return MoveResult.IGNORED
direction = self.direction_at(row, col)
if direction is None:
return MoveResult.IGNORED
if not self.has_clear_path(row, col):
self.mistakes_remaining -= 1
if self.mistakes_remaining <= 0:
self.mistakes_remaining = 0
self._freeze_timer()
self.phase = GamePhase.FAILED
return MoveResult.BLOCKED
self.board[row][col] = "."
if self.remaining_arrows == 0:
self._freeze_timer()
self.phase = GamePhase.LEVEL_COMPLETE
return MoveResult.REMOVED
成功时直接把对应格子改为 .,剩余箭头数通过扫描棋盘实时计算,避免单独维护计数器造成状态不同步。失败或通关时会冻结计时,使结果界面的时间不再继续增加。
游戏流程由状态枚举统一管理:
class GamePhase(str, Enum):
MENU = "menu"
PLAYING = "playing"
LEVEL_COMPLETE = "level_complete"
FAILED = "failed"
ALL_COMPLETE = "all_complete"
4.动画与连续点击优化
界面层将每个视觉效果保存为独立的 Animation 对象,而不是用一个全局动画锁。成功消除后,规则层已经移除了箭头,飞出动画则使用保存的坐标和方向继续绘制,因此玩家可以立刻点击下一个箭头。每一帧只过滤已经播放完的动画,其余动画可以同时存在,飞出动画使用二次缓动 progress²,使箭头逐渐加速离开;碰撞动画使用正弦函数完成前冲与回弹,并叠加高频小幅抖动。正常箭头统一为绿色,碰撞动画中的箭头临时绘制为红色。
5.DFS 关卡求解器与难度验证
手工设计的箭头布局可能出现互相阻挡的死局,因此项目实现了 DFS 求解器。每一步枚举当前能够飞出的箭头,模拟移除后递归搜索;如果棋盘清空就返回点击序列,如果某个局面无解则记录到 failed 集合,避免重复搜索。
6.扩展模块创建
关卡完成后,根据游戏状态中保存的 stars_earned 星级结果,在结算界面绘制最多 3 颗星。界面层通过循环依次生成三个星形,并判断当前序号是否小于获得的星数:满足条件时绘制为金色实心星,否则绘制为空心星。
界面为每关设置提示功能,通过 hinted_cell 保存当前被提示的格子。玩家点击“提示”按钮后调用 use_hint(),再由游戏逻辑层的 request_hint() 返回一个当前可以正确点击的箭头位置。获得位置后,界面使用黄色矩形和周期变化的亮度对该格子进行动态高亮,使玩家能够快速识别提示目标。
四、AIGC 使用过程
| 子任务 | 使用的 AIGC | 提出的要求 | AI 提供的内容 | 当前效果 | 人工修改 |
|---|---|---|---|---|---|
| 架构设计 | Codex | 使用 Python 3.13 + Pygame,规则层和界面层分离 | 设计 GameState、关卡模型、移动结果、状态机、动画和求解器接口 |
便于独立测试规则 | 对界面层进行了人工优化 |
| 游戏实现 | Codex | 实现三关、飞出/碰撞动画、失误、重开与完整流程 | 生成核心逻辑、Pygame UI、系统字体回退和键盘操作 | 实现了这三个主要逻辑功能 | 对界面颜色以及箭头数量进行了优化 |
| 区分操作音效 | Codex | 为正确操作和错误操作增加不同声音 | 使用代码合成箭头“嗖”声与低沉错误音,并增加静默降级和调用测试 | 根据试听反馈,将最初的上扬音改为更符合飞出动画的气流音 | 对呈现的音效效果进行了调整 |
| 界面与棋盘调整 | Codex | 将界面改为米白色,并让三关分别为 9、16、25 格且留出更多空格 | 重设背景、面板、文字、按钮和箭头配色;增加棋盘格数与空格数量测试 | 三关分别保留 5、10、17 个空格 | 将颜色调整至理想效果 |
| 连续点击优化 | Codex | 消除箭头后应能立刻点击另一个箭头 | 将单一动画和全局输入锁改为多动画并行;仅防止同一碰撞箭头重复扣分 | 可连续快速消除,飞行动画不会相互中断 | 对并行实现进行了理解优化 |
| 箭头颜色统一 | Codex | 所有箭头改为绿色,碰撞时变红 | 四个方向统一使用绿色;碰撞箭头临时切换为红色并前冲抖动、回弹 | 正常与碰撞状态通过绿/红直接区分 | 删除AI加入的冗余黑黄冲突的可视化 |
| 高难度扩展关 | Codex | 在三个基础关之后增加一个更难的关卡 | 生成并筛选 8*8布局,使用 DFS 验证可解,并限制开局只有两个合法选择 | 求解序列包含多个唯一选择步骤,难度高于前三关 | 进行了试玩并评定难度系数 |
| 添加星级评分和提示模块 | Chatgpt | 为游戏增加关卡星级评分和提示功能,提高反馈性与可玩性 | 根据通关用时和失误情况设计星级评价,并生成三星显示界面;同时增加提示按钮,通过当前棋盘状态寻找一个可正确点击的箭头并进行黄色高亮 | 通关后可显示对应星级,提示按钮可显示剩余次数并高亮可行箭头 | 调整星级评分策略 |
五、测试结果
测试环境:Windows 11、Python 3.13、Pygame 2.6.1、pytest 8.4.2。
| 编号 | 测试内容 | 预期结果 | 实际结果 | 是否通过 |
|---|---|---|---|---|
| T01 | 点击前方无阻挡的箭头 | 飞出棋盘并消失 | 与预期一致 | 通过 |
| T02 | 点击前方有阻挡的箭头 | 保持原位,失误减一 | 与预期一致 | 通过 |
| T03 | 点击边缘且朝外的箭头 | 正常消除,无越界异常 | 与预期一致 | 通过 |
| T04 | 消除本关全部箭头 | 显示通关并可进入下一关 | 与预期一致 | 通过 |
| T05 | 失误次数耗尽 | 显示失败并允许重开 | 与预期一致 | 通过 |
| T06 | 游戏中重新开始 | 布局、失误和计时恢复 | 与预期一致 | 通过 |
六、PSP 表格
| 任务 | 预估耗时(小时) | 实际耗时(小时) | 差异(小时) |
|---|---|---|---|
| 需求分析与游戏设计 | 0.5 | 0.5 | 0 |
| Python 与 Pygame 学习 | 1.5 | 2 | +0.5 |
| 游戏界面实现 | 0.5 | 0.5 | 0 |
| 路径与碰撞逻辑实现 | 2 | 1 | -1 |
| 关卡设计 | 1.5 | 1.5 | 0 |
| AIGC 辅助开发 | 1 | 1 | 0 |
| 测试与修改 | 2 | 2 | 0 |
| README 与博客撰写 | 1 | 1 | 0 |
| 合计 | 10 | 9.5 | 0 |
七、心得体会
这次作业是我第一次比较完整地使用 AIGC 参与一个小游戏从需求分析到测试交付的全过程。刚开始拿到题目时,我对需要实现哪些界面、怎样表示箭头、如何判断阻挡并没有完整思路。AIGC 能在我完全没有头绪时为我理出思路。
AIGC 生成的结果并不是一次就完全符合要求,实际试玩检验仍然非常重要。最初箭头飞出动画播放时会锁定全部输入,导致我无法立即点击下一个箭头。发现这个问题后,我要求将单一动画改为动画列表,使多个飞出动画能够并行播放,同时只限制同一个正在碰撞的箭头,防止重复扣除失误次数。音效也经过了调整:最初正确操作使用普通上扬提示音,但和箭头飞出的动作不够贴合,后来改成由滤波噪声和扫频合成的“嗖”声。界面颜色同样经历了多次修改,最终使用米白色背景、绿色正常箭头和红色碰撞箭头,使正常状态和错误状态更加直观。这个过程让我认识到,AI 给出的方案只能作为初稿,最终效果必须由人实际体验和判断。
在这个过程中我也深刻体会到了git作为管理工具的重要性,它可以让我在项目之外开拓一个分支,当我对这些分支功能进行测试优化之后确定上线之后再"push",保证版本处于我们能控制的范围内。这也为我对后面的小组作业提供了思路和最基本的方法论。
完成这次作业后,我对软件工程的认识不再只是“把代码写出来”。一个可以提交的项目还需要需求拆分、结构设计、版本管理、自动化测试、文档和真实的运行验证。AIGC 的优势是可以快速提供思路、代码框架和测试用例,提高开发效率;但我们仍然需要理解代码、发现实际体验中的问题,并判断修改是否真正满足需求。今后使用 AIGC 时,我会继续把它作为协作和检查工具,而不是直接接受所有生成结果,并通过测试、试玩和有意义的 Git Commit 保证每一次修改都可验证、可追踪。
八、运行方式
py -3.13 -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r requirements.txt
.\.venv\Scripts\python.exe main.py
运行自动化测试:
.\.venv\Scripts\python.exe -m pytest -v
九、commit记录


浙公网安备 33010602011771号