软工第二次作业_一箭又一箭

一箭又一箭 —— Python + AIGC 小游戏开发博客

作业基本信息

项目 内容
这个作业属于哪个课程 <https://edu.cnblogs.com/campus/fzu/202601SofwareEngineering/>
这个作业要求在哪里 <https://edu.cnblogs.com/campus/fzu/202601SofwareEngineering/homework/16717>
这个作业的目标 使用 Python 和 AIGC 完成「一箭又一箭」小游戏
学号 102401333
GitHub 仓库 https://github.com/me2nto/yijianyouyijian

一、项目展示

0. 一局演示(进阶模式通关)

demo

1. 开始界面与关卡选择

01_menu

开始界面提供了「经典模式(单格箭头)」和「进阶模式(线段箭头)」两种玩法入口。
02_select_classic
03_select_advanced

关卡选择页展示每个关卡的尺寸、箭头数/线段数、失误次数与已获星级,难度一目了然。

2. 游戏界面

04_game_classic

经典模式是题目要求的「单格箭头」,顶部显示当前关卡、剩余箭头、剩余失误与功能按钮,
中间是网格棋盘,箭头按方向着色(↑红 / ↓绿 / ←橙 / →蓝)。
05_game_advanced

进阶模式还原原版的「线段箭头」:每个箭头是一条可以拐弯的线,占据多个格子,
点击后整条线沿箭头方向滑出。
06_game_advanced_hard

进阶第 6 关「终极蛇形」是 8×8、10 条线段共 33 格的终极挑战,失误只有 2 次。

3. 提示与碰撞反馈

07_hint

点击「提示」会高亮一个当前可飞出的箭头(绿色脉冲描边)。

08_collision

点击被阻挡的箭头时,箭头会变红并晃动,同时弹出「碰撞 -1」提示并迸发粒子特效,失误次数减一。

4. 通关与失败界面

09_win

清空全部箭头后显示通关界面,并根据失误次数给出 1~3 星评价。

10_lose

失误次数耗尽后显示失败界面,可选择重新开始或返回菜单。


二、项目介绍

游戏规则

「一箭又一箭」是一款点击式箭头解谜游戏:

  1. 棋盘中摆放若干带方向的箭头,方向分为上、下、左、右四种;
  2. 点击箭头后,程序检查其前进方向(同一行或同一列)与棋盘边界之间是否存在其他箭头;
  3. 前方无阻挡:箭头飞出棋盘并被消除;
  4. 前方有阻挡:箭头不能消除,播放碰撞反馈(变红 + 晃动),消耗一次失误机会;
  5. 清除本关全部箭头即可通关,进入下一关;
  6. 失误次数耗尽则本关失败,可重新开始。

例如:

→  ·  ·  ↑  ·     第一个箭头朝右,右侧仍有其他箭头 → 不能飞出
↑  ·  ·  ·  →     最后一个箭头朝右,右侧无箭头 → 可以飞出

除题目要求的「单格箭头」基础版外,我还实现了原版的线段箭头:一个箭头是占据
多个格子、可以拐弯的线,点击后整条线沿箭头方向平移,撞到其它线段则退回并记失误,
全部移出边界则消除。单格箭头本质上是长度为 1 的线段,两种玩法统一在同一套判定逻辑里。

界面设计

  • 开始界面:游戏标题、规则说明、「经典模式 / 进阶模式 / 退出」入口;
  • 关卡选择界面:关卡卡片(尺寸、箭头数/线段数、失误次数、星级)+ 随机挑战;
  • 游戏界面:顶栏(关卡信息 + 状态芯片 + 功能按钮)+ 网格棋盘;
  • 通关 / 失败界面:结果展示 + 星级评价 + 下一步操作。

主要功能与特色

  • 两种模式:经典单格箭头(对应题目基础要求)+ 进阶线段箭头(还原原版);
  • 完整的四方向路径检测与边界处理,线段箭头支持「整条线平移扫过的区域」判定;
  • 清晰的飞出、碰撞动画与视觉反馈(拖尾、晃动、粒子);
  • 失误次数机制与胜负判定;
  • 14 个难度递增、经过可解性验证的关卡(经典 8 个 + 进阶 6 个);
  • 随机关卡生成器,两种模式均可一键生成「必定可解」的关卡;
  • 附加功能:关卡选择、撤销、提示、星级评价、键盘快捷键;
  • 现代扁平化界面:渐变背景、圆角卡片与柔和阴影;
  • 核心逻辑与界面解耦,可脱离图形界面进行单元测试。

三、实现思路

1. 箭头与方向的表示

方向用枚举字符串 "up" / "down" / "left" / "right" 表示,对应行列增量:

DIR_DELTA = {
    "up":    (-1, 0),
    "down":  (1, 0),
    "left":  (0, -1),
    "right": (0, 1),
}

箭头用不可变数据类 Arrow(row, col, direction, body=()) 表示:(row, col) 是箭头头部
body 是依次向尾部延伸的其它格子。body 为空即单格箭头,非空则为可弯曲的线段箭头。

2. 棋盘与路径检测(重点)

棋盘用「格子 → 箭头」的占用表 occupied 记录每个箭头占据的所有格子。路径检测核心是 is_blocked()

def is_blocked(self, arrow):
    dr, dc = DIR_DELTA[arrow.direction]
    for (r, c) in arrow.cells:           # 对线段的每一个格子
        rr, cc = r + dr, c + dc
        while self.in_bounds(rr, cc):    # 边界保护,避免数组越界
            other = self.occupied.get((rr, cc))
            if other is not None and other is not arrow:
                return True
            rr += dr
            cc += dc
    return False

对单格箭头,arrow.cells 只有一个格子,函数退化为「同一行/列、箭头与边界之间是否有
其它箭头」,正是题目要求的基础判定;对线段箭头,则对整条线的每个格子都沿方向扫描,
等价于检查整条线平移扫过的区域是否被占据。in_bounds() 从根本上避免数组越界。

3. 关卡表示

每关用一个字符串网格描述,U/D/L/R 表示四个方向,. 表示空格:

Level("初识方向", [
    ".R..",
    "LL..",
    "....",
    ".D.D",
], mistakes=3)

这种表示直观、易读、易改,便于手工设计关卡。进阶模式的线段箭头则用
seg(direction, head, body...) 直接构造,Level.from_arrows() 组装成关卡:

4. 可解性求解器

def find_solution(level):
    board = level.new_board()
    while board.remaining > 0:
        removable = board.removable()
        if not removable:       # 有剩余却无可飞出的箭头 → 死锁,无法通关
            return None
        board.remove(removable[0])
    return order

关键性质:消除只会解除阻挡、不会新增阻挡,因此贪心策略是完备的。
每一关都用求解器验证过存在通关顺序,保证玩家一定能通关。

5. 随机关卡生成(逆向构造法)

def generate_level(rows, cols, n_arrows, seed=None):
    board = Board(rows, cols, [])
    while len(placed) < n_arrows:
        r, c = 随机空格; d = 随机方向
        if not board.is_blocked(Arrow(r, c, d)):   # 前方无阻挡才加入
            board.add(Arrow(r, c, d))

从空棋盘开始,每次只加入「前进方向当前无阻挡」的箭头。这样按加入顺序的逆序
就是一条合法消除顺序,后加入的箭头会挡住先加入的箭头,自然形成层层解锁的链条,
从而保证生成的关卡一定可解。我用它生成经典模式第 4~8 关,并另写了一个
generate_segment_level()(同样的逆向构造,只是每次加入的是随机拐弯的线段)
来生成进阶关卡和「随机挑战」模式。

6. 界面与动画

  • 界面采用状态机(menu / select / game / win / lose)组织;
  • 飞出动画:Flyer 记录箭头像素位置,沿方向匀速移动直到飞出屏幕;
  • 碰撞动画:Hit 记录起始时间,箭头做衰减的正弦晃动并变红,同时显示浮动文字;
  • 所有图形均由 pygame 代码绘制,不依赖外部素材。

四、AIGC 使用过程

本项目全程借助 Claude Code 完成需求分析、代码编写、关卡设计、调试与测试。
以下是 6 次具有代表性的协作过程:

记录 1:路径检测

子任务 借助何种 AIGC 技术 AI 实现或提供了什么 效果如何 人工修改
路径检测 Claude Code 生成四个方向的路径检测代码,并内置边界保护 一次通过,正确覆盖边界情况 补充了边缘、角落朝外的越界测试用例,确认四种方向都不越界

我提出需求:「判断箭头前进方向、棋盘边界内是否存在其他箭头,注意不能越界」。
AI 给出了 DIR_DELTA 方向增量表和 is_blocked() 逐格检查的代码,并用 in_bounds()
做边界保护。我随后针对「位于边缘且朝外的箭头」单独写了测试用例(T03),
在单格棋盘、四个角上分别验证,确认不会发生数组越界。

记录 2:关卡设计

子任务 借助何种 AIGC 技术 AI 实现或提供了什么 效果如何 人工修改
关卡设计 Claude Code 生成字符串网格关卡 + 可解性求解器 初版某一关存在死锁,求解器返回 None 手工调整箭头方向与位置,重新验证 8 关均可通关

我让 AI 生成 8 个难度递增的关卡数据,并写一个 find_solution() 求解器来验证。
运行后发现初版某关存在「有剩余箭头却没有任何可飞出的箭头」的死锁(无法通关)。
我根据求解器提示,手工调整了该关中箭头的方向和位置,反复运行求解器,
最终 8 关全部通过验证,并亲自试玩确认每关都有合理的通关顺序。

记录 5:随机关卡生成器

子任务 借助何种 AIGC 技术 AI 实现或提供了什么 效果如何 人工修改
随机关卡生成 Claude Code 用逆向构造法生成「必定可解」的随机关卡 一次通过,生成的关卡均可解 调整了生成参数(棋盘尺寸、箭头密度、失误次数)以控制难度

为了让难度有上限(也对应作业的「随机生成可通关的关卡」附加项),我提出:
「有没有一种方法能保证随机生成的关卡一定可解?」AI 给出了「逆向构造法」——
每次只加入前进方向当前无阻挡的箭头,逆序即消除顺序。我验证了生成的关卡确实
都能被求解器给出完整顺序,并把它接入「随机挑战」按钮。

记录 6:线段箭头(还原原版)

子任务 借助何种 AIGC 技术 AI 实现或提供了什么 效果如何 人工修改
线段箭头玩法 Claude Code 调研原版玩法,把箭头扩展成可弯曲的线段并统一路径判定 一次通过,进阶关卡均可解 手工挑选/调整了若干关卡,保证既美观又有合理的难度

我提出:「能不能做成原版那种弯弯曲曲的线?」AI 先调研了原版《一箭又一箭》的
玩法,确认其箭头是「占据多个格子、可以拐弯的线段,点击后整条线沿箭头方向平移」,
然后建议把 Arrow 加上 body 字段、把 is_blocked() 从「只判断头部前方」推广到
「对整条线每个格子沿方向扫描」。这样单格箭头退化为长度 1 的线段,两种玩法共用
同一套判定。我用逆向构造法生成了 6 个进阶关卡并逐一验证可解。

记录 3:碰撞动画与界面

子任务 借助何种 AIGC 技术 AI 实现或提供了什么 效果如何 人工修改
碰撞动画 Claude Code 补全飞出/晃动动画、多界面状态机、中文 UI 基本可用 调整飞出速度(约 1000px/s)、晃动衰减幅度、配色与布局

我提出需求:「箭头飞出要有动画,碰撞要有晃动和变色反馈,界面用中文显示」。
AI 生成了 Flyer 飞出动画、Hit 碰撞晃动、浮动文字提示,以及开始/关卡选择/游戏/通关/失败
五个界面的状态机。初版动画速度偏快、晃动幅度偏大,我调整了速度与衰减参数,
并统一了配色(方向用红/绿/橙/蓝区分)和按钮布局,使反馈更清晰。

记录 4:测试用例

子任务 借助何种 AIGC 技术 AI 实现或提供了什么 效果如何 人工修改
编写测试用例 Claude Code 生成 T01~T06 的 unittest 测试 发现「撤销」测试场景设计问题 改用两个箭头的棋盘,把「撤销飞出」与「通关」两种场景分离

我让 AI 按作业要求编写 T01~T06 六项测试。运行时发现「撤销上一步」这条测试最初用了
只有一个箭头的棋盘——点击后立刻通关,导致撤销被禁止、断言失败。这其实是测试场景
设计不当而非游戏逻辑错误。我改用两个箭头的棋盘,先飞出一个(尚未通关)再撤销,
成功通过,也让测试更贴近真实玩法。


五、测试结果

除手动试玩外,我为核心逻辑编写了自动化单元测试(tests/test_game.py),
可在无显示环境下运行:

python -m unittest discover -s tests -v

20 项测试全部通过。对应作业要求的测试结果如下:

编号 测试内容 预期结果 实际结果 是否通过
T01 点击前方无阻挡的箭头 箭头飞出并消失,失误不变 箭头被消除,失误次数不变
T02 点击前方有阻挡的箭头 箭头不消失,失误减 1 箭头保留,失误次数减 1
T03 点击边缘且朝外的箭头 正常消失,不越界 正常消失,无越界错误
T04 消除本关全部箭头 显示通关并进入下一关 状态变为「通关」,可进入下一关
T05 失误次数耗尽 显示失败并允许重新开始 状态变为「失败」,可重新开始
T06 游戏进行中重新开始 箭头布局和失误次数恢复 布局与失误次数均恢复初始

此外,我还测试了经典 + 进阶共 14 个关卡的可解性(求解器都能给出完整消除顺序),
以及线段箭头、撤销、提示、随机关卡生成等扩展功能,均表现正常。


六、PSP 表格

任务 预估耗时(小时) 实际耗时(小时) 差异(小时)
需求分析与游戏设计 2 2 0
Python 与图形库学习 4 5 +1
游戏界面实现 6 6 0
路径与碰撞逻辑实现 5 4 -1
关卡设计 3 3 0
AIGC 辅助开发 4 5 +1
测试与修改 3 3 0
README 与博客撰写 3 3 0
合计 30 31 +1

注:请根据自己的真实情况调整上面的耗时数值。


七、心得体会

这次作业让我第一次完整地体验了「借助 AIGC 工具开发一个小游戏」的全流程,收获很大:

  1. AI 提升了效率,但不能替代理解。AI 很快就能给出路径检测、动画、界面等代码,
    但如果我不理解「消除只会解除阻挡」这一关键性质,就无法判断求解器为何正确,
    也就无法发现初版关卡的死锁问题。真正有价值的是理解之后再让 AI 帮忙实现。

  2. 测试暴露了意想不到的问题。最初我以为路径检测很简单,结果是在写 T03 的
    边界测试时才真正把「边缘箭头朝外」这种情况验证清楚。而「撤销」那条测试的失败
    更是提醒我:测试场景本身也要设计合理。

  3. 关卡设计比想象中更难。要让每一关「有合理的通关顺序」又不至于一眼看穿,
    需要反复试玩和用求解器验证。这次用字符串网格 + 求解器的方式,让设计迭代变得很快。

  4. 解耦设计的好处。把核心逻辑(game.py)和界面(main.py)分开,
    让我可以在不启动图形界面的情况下直接跑单元测试,调试效率高了很多。

总的来说,这次作业不仅让我学会了 pygame 的基本使用,更让我理解了「用 AI 辅助开发」
的正确姿势:AI 负责快速生成与查错,人负责理解、验证和做决策。这段经历对我今后的
学习和工作都很有帮助。

posted @ 2026-09-21 09:40  me2nto  阅读(10)  评论(0)    收藏  举报