2026软件工程个人作业第二次
2026软件工程个人作业第二次
| 项目 | 内容 |
|---|---|
| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/fzu/2026-01SoftwareEngineeringandSoftwareEngineeringPractice |
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/fzu/2026-01SoftwareEngineeringandSoftwareEngineeringPractice/homework/16718 |
| 这个作业的目标 | 使用 Python 和 AIGC 完成“一箭又一箭”小游戏 |
| 学号 | 102401428 |
| GitHub 仓库 | <Winksong/arrow-again-arrow> |
一、项目展示
- 开始界面

- 效果展示



-
完整演示
链接: https://pan.baidu.com/s/1vMhoezOGk4zlqW1Q0DHWCA?pwd=t8n2
提取码: t8n2
二、项目介绍
游戏棋盘中包含若干带方向的箭头,箭头方向分为上、下、左、右四种。玩家点击某个箭头后,程序检查该箭头前进方向上的路径:
- 如果箭头与棋盘边界之间没有其他箭头阻挡,该箭头飞出棋盘并被消除;
- 如果路径上存在其他箭头,该箭头不能被消除,并通过移动后返回、晃动、变色或文字等方式提示碰撞;
- 点击被阻挡的箭头会消耗一次失误机会;
- 清除本关全部箭头后即可进入下一关;
- 失误次数耗尽时,本关失败,可重新开始。
三、实现思路
1. 数据表示
-
方向:字典映射到单位位移,遍历时直接累加。
DIRS = {"up": (-1, 0), "down": (1, 0), "left": (0, -1), "right": (0, 1)} -
箭头:
Arrow(row, col, direction, state, anim_t),朝向用字符串,与关卡字符画直接对应。 -
棋盘:
dict[(row, col) → Arrow],按坐标取箭头 O(1),不用为空格填占位符。 -
关卡:字符串布局,
↑↓←→为箭头、·为空格,方便手工调整。{ "name": "第 1 关 · 入门", "rows": 3, "cols": 3, "mistakes": 3, "layout": """ →·↑ ·↓· ↑·· """, }
2. 路径检测(核心判定)
点击箭头后,沿其方向逐格前进直到边界,途中遇到任何箭头即被阻挡。用生成器实现:
def iter_path(self, arrow: Arrow) -> Iterator[tuple[int, int]]:
dr, dc = arrow.delta
r, c = arrow.row + dr, arrow.col + dc
while self.in_bounds(r, c): # ✅ 先判后取
yield r, c
r += dr
c += dc
def path_clear(self, arrow: Arrow) -> bool:
return not any(
self.arrow_at(r, c) is not None for r, c in self.iter_path(arrow)
)
坑一:边界判断必须放在 while 条件里。 若写成「先取下一格坐标、再判越界」,向上/向左到边界时 r = -1,Python 负下标不报错,会静默绕到棋盘另一端,判定出错却无异常,人工试玩几乎发现不了(对应 T03)。
坑二:flying 状态的箭头不算阻挡。 arrow_at() 里过滤了它,否则会出现「箭头已飞出还挡着后面」的情况:
def arrow_at(self, r, c):
a = self.cells.get((r, c))
if a is None or a.state == FLYING:
return None
return a
3. 逻辑与界面分离
规则全部放在不依赖 pygame 的 model.py,界面只负责绘制与转发点击。
| 文件 | 职责 | 依赖 pygame |
|---|---|---|
model.py |
规则与数据(Arrow / Board / Session) | ❌ |
solver.py |
可解性校验与求解器 | ❌ |
levels.py |
关卡数据 | ❌ |
view.py |
绘制 | ✅ |
app.py |
主循环、状态机、事件 | ✅ |
好处是核心逻辑可直接用 pytest 测试,不用开窗口(SDL_VIDEODRIVER=dummy,配在 tests/conftest.py)。Session.click() 返回三态字符串,把判定与界面表现解耦:
if self.board.path_clear(arrow):
arrow.state = FLYING
return CLICK_FLY # 界面播飞出动画
arrow.state = BLOCKED
self.mistakes_left -= 1 # 界面播碰撞反馈
return CLICK_BLOCKED
4. 关卡可通关性
solver.solve() 用贪心求可行解:每轮扫一遍剩余箭头,挑一个前方无阻挡的消除,重复到清空;某轮找不到就说明互相挡死了。
坑三:求解器循环上限。 早期 solvable() 写成 len(remaining),而它每轮都在变小,导致大棋盘提前退出、把有解误判成无解。正确做法是固定循环「箭头总数」次。
坑四:手工摆关卡极易摆出无解死局,肉眼看不出来。 本项目手工设计了 9 个候选布局,7 个被求解器判定为无解:
↑·↓·
·→·← ← 看似正常,实际箭头互相指着
↓···
·←↑·
因此关卡一律先用反向构造(solver.generate)产出候选,再人工挑选:
按「拆除顺序的逆序」放置箭头。放第 k 个时,棋盘上只有「会在它之后才被拆掉」的箭头,只要新箭头路径上没有它们即可。
构造时即保证有解,省掉了暴力验证。最后用 tools/playtest.py 跑 5000 局随机乱点,量化验证难度递增。
5. 状态与动画
- 箭头状态机:
idle → flying → 移出或idle → blocked → idle。 - 动画锁输入:动画期间忽略点击,防止连点刷失误。
- 胜负判定延后:通关放在飞出动画播完的回调里,避免动画没播完就弹卡片。
- 失败路径统一:失误耗尽走碰撞反馈而非飞出动画,故状态同步抽成
_sync_result_state()统一处理。
四、AIGC 使用过程
本次开发全程使用 AIGC 辅助,以下为几次有代表性的记录:
| 子任务 | 借助何种 AIGC 技术 | AI 实现或提供了什么 | 效果如何 | 人工修改 |
|---|---|---|---|---|
| 路径检测与核心模型 | Deepseek | 生成方向映射、棋盘类与四方向路径检测代码 | 基本可用 | 首版把边界判断写在取下一位之后,向上/向左会触发 Python 负下标静默绕回。 |
| 阻挡判定 | Deepseek | 生成按同行同列扫描阻挡箭头的逻辑 | 有缺陷 | 首版把 flying 状态也当作阻挡,出现「箭头已飞出仍挡着后面」。人工在 arrow_at() 中过滤该状态 |
| 关卡生成 | Deepseek | 初版用「随机摆盘 + 暴力验证」,7×7 棋盘生成一次约 15 秒 | 不符合要求 | 改为「按消除顺序逆序放置」的反向构造法,构造时即保证有解,生成变为瞬时 |
| 求解器 | Deepseek | 生成可解性校验函数 | 有缺陷 | 循环上限误写为 len(remaining),而该值每轮递减,会把有解误判为无解。人工改为固定循环箭头总数次 |
| 碰撞与飞出动画 | Deepseek | 生成晃动 + 闪红 + 浮动文字的碰撞反馈,以及加速飞出动画 | 基本可用 | 人工调整晃动时长、飞出加速度等参数,使手感更干脆 |
| 自动化测试 | Deepseek | 按 T01~T06 生成 pytest 测试 | 首次运行有 3 个用例失败 | 分析发现是测试里的布局数据本身设计有误(如把 ↓ 放在底边其实能直接飞出),修正测试数据后全部通过 |
| 界面布局 | Deepseek | 生成开始界面与标题绘制 | 标题旁装饰箭头与文字重叠 | 人工把装饰箭头移到标题两侧 |
五、测试结果
| 编号 | 测试内容 | 预期结果 | 实际结果 | 是否通过 |
|---|---|---|---|---|
| T01 | 点击前方无阻挡的箭头 | 箭头飞出棋盘并消失 | 与预期一致 | 通过 |
| T02 | 点击前方有阻挡的箭头 | 箭头不消失,失误次数减 1 | 与预期一致 | 通过 |
| T03 | 点击位于边缘且朝向棋盘外的箭头 | 箭头正常消失,不发生越界错误 | 与预期一致 | 通过 |
| T04 | 消除本关全部箭头 | 显示通关并进入下一关 | 与预期一致 | 通过 |
| T05 | 失误次数耗尽 | 显示失败并允许重新开始 | 与预期一致 | 通过 |
| T06 | 游戏进行中重新开始 | 箭头布局和失误次数恢复 | 与预期一致 | 通过 |
| T07 | 点击空格或棋盘外 | 不计入失误次数 | 与预期一致 | 通过 |
| T08 | 斜线方向不算阻挡 | 箭头正常飞出 | 与预期一致 | 通过 |
| T09 | 阻挡箭头的朝向不影响判定 | 仍被判定为阻挡 | 与预期一致 | 通过 |
| T10 | 动画播放期间点击 | 忽略点击,不重复扣失误 | 与预期一致 | 通过 |
六、PSP 表格
| 任务 | 预估耗时(小时) | 实际耗时(小时) | 差异(小时) |
|---|---|---|---|
| 需求分析与游戏设计 | 0.5 | 1 | 0.5 |
| Python 与图形库学习 | 1.5 | 2 | 0.5 |
| 游戏界面实现 | 1 | 0.5 | -0.5 |
| 路径与碰撞逻辑实现 | 1 | 0.5 | -0.5 |
| 关卡设计 | 0.5 | 1 | 0.5 |
| AIGC 辅助开发 | 1 | 1.5 | 0.5 |
| 测试与修改 | 1 | 2 | -1 |
| README 与博客撰写 | 0.5 | 0.5 | 0 |
| 合计 | 7.5 | 10 | 0 |
七、心得体会
- AI 最大的帮助是把会做的事变快:界面绘制、动画、测试骨架这些模式化的代码,描述清楚需求后几秒钟就能拿到能跑的版本,把精力留给了规则设计。
- AI 也会犯错,而且错得很自信:这次测试用例里的布局数据就有三处自相矛盾,如果自己不真正理解路径检测规则,根本看不出是测试错了还是代码错了。
- 逻辑与界面分离后,核心规则可以脱离窗口直接测试,这为后续其他项目的开发起到了启示作用。
浙公网安备 33010602011771号