2026秋软件工程个人作业(第二次)

2026秋软件工程个人作业(第二次)

运行环境:Python 3.13 + pygame 2.6.1(只依赖 pygame 一个第三方库)

pip install pygame
python main.py                # 菜单:开始游戏(固定 5 关)/ 更多关卡(关卡选项)
python main.py --level 3      # 直接从第 3 关开始
python main.py --random       # 随机布局:每次开局现场生成 5 关
python main.py --random --seed 20260919   # 指定种子,复现同一批关卡
这个作业属于哪个课程 H202601软件工程与软件工程实践
这个作业要求在哪里 2026秋软件工程个人作业(第2次)
这个作业的目标 使用 Python 和 AIGC 完成“一箭又一箭”小游戏
学号 102401114
GitHub仓库 仓库链接

1.项目展示

游戏开始界面
游戏开始界面

游戏过程、通关
游戏过程、通关

失败界面和更多关卡
失败界面和更多关卡

2.项目介绍

游戏规则

棋盘上有若干带方向的单格箭头(上 / 下 / 左 / 右)。

  • 点击一个箭头:如果它前进方向到棋盘边界之间没有其他箭头,它就沿该方向飞出棋盘并消失;
  • 如果路径上有其他箭头挡路:箭头不能消除,会晃动变红、挡路者闪红、弹出提示,并扣 1 次失误
  • 清空本关全部箭头 → 通关;失误次数耗尽 → 本关失败,可重新挑战。

例如下面这行箭头,第一支朝右但右侧还有别的箭头,点不动;第三支朝右且右侧到边界为空,可以飞出:

→  ·  ·  ↑  ·        第一支朝右,右侧有箭头 → 被挡
↑  ·  ·  ·  →        最后一支朝右,右侧到边界无箭头 → 飞出

界面设计

  • 四个界面:开始界面、关卡选项、对局界面、结算界面(通关弹窗 / 失败弹窗 / 全部通关);
  • 布局统一:卡片(圆角 + 浅阴影 + 细边框)、按钮(400×62 / 400×52 / 200×58 三档尺寸)、
    字号(64 / 44 / 28 / 24 / 20 / 17 六档)全部复用同一套常量,四个界面视觉一致;
  • 配色:浅色底 + 蓝色箭头 + 红色失误,底纹用 AI 生成的浅色面板图(低透明度铺底);
  • 反馈:反馈分三层——动画(晃动 / 变色 / 飞出 / 涟漪 / 星星回弹)、文字(漂浮提示)、
    声音(掌声 / 失败 / 烟花),保证任何一次点击都有明确回应。

主要功能清单

  1. 鼠标点击选择箭头,上下左右四方向;
  2. 路径检测(同行 / 同列到边界),点对飞出、点错扣失误;
  3. 飞出动画、碰撞晃动变红、挡路箭头闪红、漂浮文字提示;
  4. 失误次数显示(爱心图片,满 / 空两态);
  5. 固定 5 关,难度递进(4×4/5 支 → 6×6/15 支,失误上限 3~4);
  6. 随机布局模式:每局现场生成 5 关,逐关用求解器验证「必定可通关」,同一种子可复现;
  7. 关卡选项界面:任选一关直接挑战,卡片显示历史最佳星级与最高分;
  8. 计分与星级:按用时与失误评 1~3 星,最高分存本地 records.json
  9. 提示功能:一键高亮一个当前能飞的箭头;
  10. 重新开始:恢复初始状态、失误复原、计时归零,且不改变本关布局;
  11. 音效:正确选择鼓掌、错误选择失败音、通关烟花;M 键静音;
  12. 鼠标悬停预览前进光路(绿=通,红=被挡);
  13. 键盘快捷键:R 重新开始、Esc 返回菜单、M 静音。

特色

  • 关卡不是手编的,是求解器验证出来的:固定 5 关由「随机生成 + DFS 求解器验证」产出,
    随机关卡在开局时现场生成并即时验证,不存在死局;
  • 发现并利用了游戏的一条数学性质(见 3.4):箭头减少只会解除阻挡 ⇒ 贪心必通、
    没有顺序陷阱。这条性质同时保证了随机关卡的安全性和菜单自动演示的合理性;
  • AIGC 全链路:图形素材由 AI 生图 + 自写脚本做白底抠图、裁水印;音效由 Python 标准库
    现场合成(不下载第三方音频,无版权风险);
  • 可验证:100 项断言的自动试玩测试 + 帧率实测脚本,所有结论都有数据支撑。

3.实现思路:

3.1 箭头与方向怎么表示

方向用四个字符常量表示,配套一张增量表 DELTA,值是 (行增量, 列增量)

UP, DOWN, LEFT, RIGHT = "U", "D", "L", "R"
DELTA = {
    UP:    (-1, 0),
    DOWN:  ( 1, 0),
    LEFT:  ( 0, -1),
    RIGHT: ( 0,  1),
}
# 关卡用 ASCII 书写,人读人改都方便
SYMBOL_2_DIR = {"^": UP, "v": DOWN, "<": LEFT, ">": RIGHT}

箭头对象只存三个信息 + 一个编号:

class Arrow:
    __slots__ = ("r", "c", "d", "uid")   # uid 供动画跟踪同一个箭头

关卡用 ASCII 网格书写,比坐标数组直观得多,也方便人工检查:

Level(
    name="第3关 · 十字路口",
    grid=[
        "^>.>.",
        ".^v.>",
        ">>...",
        "..v..",
        ".....",
    ],
    mistakes=3,
)

难度曲线(棋盘尺寸 / 箭头数 / 失误上限 / 开局至少被挡数)集中在一张常量表里,
固定关卡与随机关卡共用同一套配置:

LEVEL_CONFIGS = [
    (4, 4,  5, 3, 1, "初次上手"),
    (4, 4,  7, 3, 2, "两两相望"),
    (5, 5,  9, 3, 3, "十字路口"),
    (5, 5, 12, 4, 4, "箭阵"),
    (6, 6, 15, 4, 5, "一箭又一箭"),
]

3.2 一个必须小心的坑:行列坐标 vs 屏幕坐标

DELTA 是「(行增量, 列增量)」,而行号增大是向下、列号增大是向右
画箭头、算速度时必须换成屏幕的 (x, y)

dr, dc = DELTA[direction]
ux, uy = dc, dr      # 列增量 -> 屏幕 x,行增量 -> 屏幕 y

我第一版就是在这里栽了跟头:箭头整体画反了(上变成左、右变成下)。
逻辑是对的,只有渲染错了,所以单元测试全绿、肉眼一看全错。后面用一张
「四方向对照图」(tools/check_sprites.py)逐个方向肉眼确认,才彻底修掉。
教训很直白:能跑的测试不代表看得对,涉及坐标系的改动一定要有可视化验证

3.3 路径检测(重点)

判定规则只有一句:同一行或同一列、箭头与边界之间是否还有其他箭头
实现就是从箭头的下一格出发,沿方向一格一格走到棋盘外,路上遇到谁就返回谁:

def scan(self, arrow):
    """返回 (是否通畅, 挡路的箭头 or None)"""
    dr, dc = DELTA[arrow.d]
    r, c = arrow.r + dr, arrow.c + dc
    while 0 <= r < self.rows and 0 <= c < self.cols:   # 走出边界 = 通畅
        other = self.arrow_at(r, c)
        if other is not None:
            return False, other                        # 撞到第一个箭头
        r += dr
        c += dc
    return True, None

几个实现细节:

  • 为什么不用预计算:棋盘最大 6×6,最坏 5 步就能走完,O(行+列) 的线性扫描完全够用;
    点击是低频事件,没必要为了理论性能引入额外数据结构。
  • "谁是挡路的"顺便返回:碰撞反馈要把挡路的那支箭头也闪红,所以扫描时返回第一个
    挡路的箭头,而不是只返回一个布尔值——这点让界面层的反馈实现变得很简单。
  • 一处实现,两处使用:点击判定用 scan();鼠标悬停时的光路预览(绿线/红线、
    以及被挡格子的红框)也调用同一个 scan(),规则只有一份,不存在"预览和实际判定不一致"。
  • 失误判定放在核心层Board.click() 返回结构化的结果
    {'ok': True, 'result': 'fly'}{'ok': False, 'result': 'blocked', 'blocker': …}),
    失误次数在核心层扣减。界面层只负责"播动画 / 播音效",规则与表现彻底分开。

3.4 关卡怎么生成:DFS 求解器 + 一条有趣的数学性质

求解器:状态就是「剩余箭头的集合」,用 frozenset 表示,DFS + 记忆化搜一条清空顺序:

def solve_state(state, rows, cols, memo=None):
    """state: {(r, c, 方向), ...};返回一条清空顺序,无解返回 None"""
    ...
    def dfs(st):
        if not st:
            return []
        if st in memo:
            return memo[st]
        for a in _removable_in(st, rows, cols):   # 当前能飞的箭头
            sub = dfs(st - {a})
            if sub is not None:
                memo[st] = [a] + sub
                return memo[st]
        memo[st] = None          # 死局,记住它,避免重复搜索
        return None
    return dfs(state)

随机关卡:随机撒箭头 → 立刻用求解器验证 → 只接受「可解、且开局至少有若干支箭头被挡」
的布局,否则重撒。整局 5 关平均生成耗时 8.8ms(实测区间 1~25ms),进游戏前没有任何可感知的等待。

顺手发现的数学性质:我让求解器统计每个关卡「点错一步就会死局的首步」有多少个,
在 20 局共 100 个关卡上统计,结果 不可解 0 个、陷阱首步 0 个。想了一下原因,其实是个单调性结论:

阻挡只取决于「剩余箭头集合」:多一支箭只可能多一个障碍,不会少。
所以移除箭头后,任何原本能飞的箭头仍然能飞(可飞集合单调不减)。
于是若关卡可解(存在某个清空顺序),把其中任意一个"当前可飞"的箭头提前到最前,
剩余部分依然可解 —— 贪心必通,不存在顺序陷阱

由此得到三个直接结论,全都用在了程序里:

  1. 玩家不会因为"点错顺序"而死局,失败只可能来自点了被挡的箭头浪费失误次数 →
    关卡难度必须靠密度 / 阻挡数来做(LEVEL_CONFIGS 的第 5 个字段就是这个门槛);
  2. 随机关卡只要被求解器判定"可解",玩家必定能通关 → 随机生成是安全的;
  3. 菜单里的自动演示可以随便挑一个可飞的箭头飞出,永远不会演示到死局。

3.5 界面架构与流畅度

  • 逻辑与界面分离game_core.py 是纯逻辑(棋盘、路径、失误、求解器、计分,不 import pygame),
    main.py 只管画和收鼠标事件。好处是自动测试可以不开窗口、直接调 Game.on_click()
    走真实点击路径
    跑完 5 关;
  • 动画用 dt 驱动:所有动画(飞出、晃动、闪烁、涟漪、星星回弹)都按帧间隔推进,
    不依赖具体帧率;
  • 图片按需缓存:箭头按 (方向, 像素尺寸) 缓存"旋转 + 缩放"的结果,碰撞用的红色版本
    预生成,每帧只做 blit;
  • 界面静态层预渲染:菜单和关卡选项里不动的部分(底纹、标题、卡片、文字)渲染成一张
    离屏图缓存起来,每帧只 blit 一次 —— 菜单帧耗时从 7.28ms 降到 0.21ms
  • 实测数据tools/perf_check.py,dummy 驱动纯软件渲染,60 FPS 的预算是 16.7ms/帧):
场景 平均单帧 余量
菜单(自动演示在动) 0.21 ms 约 80 倍
关卡选项(5 张卡片) 0.15 ms 约 110 倍
对局(6×6 / 15 支箭头) 2.60 ms 约 6 倍
连续飞出动画 3.24 ms 约 5 倍

4.AIGC 使用过程:

过程一:提出游戏需实现的基本功能要去,让ai生成代码

image

使用deepseek ai给了我一个游戏的基本demo 基本满足作业所需要去 ui界面有点问题,改了字体大小

过程二:让ai添加更多关卡的功能,提高游戏可玩性

image

使用deepseek ai新增了难度曲线和即使验证功能 基本可用

过程三:让ai生成箭头图形和心图形

image

使用deepseek ai生成了具体图像 动画流程

过程四:性能与音效——把"感觉"换成数字

  • 我的需求:「换成图片素材后动画要保证流畅」+「点对鼓掌、点错失败音、通关烟花音效」。
  • AI 的产出:给出两个方向——①素材缓存 + 界面静态层预渲染;
    ②用 Python 标准库(wave/array/math)现场合成音效,避免下载第三方音频的版权问题。
  • 我的验证与修改
    • 写了 tools/perf_check.py 实测三段最费时间的场景,发现菜单逐帧重绘大量中文是最贵的
      (7.28ms/帧),改成静态层预渲染后降到 0.21ms;对局 2.60ms、连续飞出 3.24ms,
      都在 16.7ms 预算内(余量 5 倍以上),"流畅"就从感觉变成了数字;
    • 音效做完后我听不出问题,就改用可量化的检查:统计过零率、强瞬态个数、RMS,
      发现错误音效的过零率没有随之下滑(说明"往下掉"的听感没做出来),
      定位到是最后加的高通滤波把下行基频滤掉了,改为一阶低通后过零率正常下降;
      掌声则统计出 13 个明显的强瞬态(对应一下下"巴掌")。
  • 结果:4 个音效(鼓掌 / 失败 / 通关琶音 / 烟花)全部接入,
    M 键静音,没有声卡或素材缺失时自动静默降级;
    自动测试用 sound.last_played 断言"点对=鼓掌、点错=失败、通关=烟花",不用听声音也能回归。

5.测试结果:

测试方法

  • 核心逻辑单测 + 全流程自动试玩tools/autoplay_test.py,不弹窗口(SDL 用 dummy 驱动),
    通过 Game.on_click()真实点击路径(和玩家点击完全同一条代码路径),
    求解顺序由求解器给出,每关都真的"打完";
  • 关卡可解性复核tools/verify_levels.py(逐关打印一条通关顺序);
  • 性能实测tools/perf_check.py

运行命令:

python tools/autoplay_test.py     # 106 项断言
python tools/verify_levels.py     # 5 关可解性复核
python tools/perf_check.py        # 帧率实测

测试结果表(摘自真实运行输出)

# 测试项目 预期结果 实际结果 结论
1 零失误且快通关的星级与得分 3 星 / 1000 分 3 星 / 1000 分 通过
2 失误 1 次且略超时 2 星 2 星 通过
3 失误 2 次且严重超时 1 星 1 星 通过
4 分数随失误 / 超时递减 单调递减 1000 > 751 > 350 通过
5 极端情况分数保底 ≥100 分 100 分 通过
6 目标用时是 3 星 / 2 星的分界 正好 par 算 3 星 符合 通过
7 音效加载 4 个音效全部加载 4 个 通过
8 点错箭头的音效接线 播放 pick_bad pick_bad 通过
9 点对箭头的音效接线 播放 pick_ok(鼓掌) pick_ok 通过
10 静音开关 静音后不出声 静音后 play 返回 False 通过
11 本关通关音效 播放 fireworks(烟花) fireworks 通过
12 菜单「更多关卡」 进入关卡选项界面 进入 通过
13 关卡选项点第 4 关卡片 直接进入第 4 关(箭阵) 第 4 关 通过
14 关卡选项「随机挑战」 现场生成 5 关 5 关(种子每次不同,本次 446330394) 通过
15 菜单「开始游戏」 进入固定 5 关的第 1 关 第 1 关 通过
16 点被挡箭头 失误数 -1,箭头仍在,播失败音 失误 3→2,箭头未消失 通过
17 碰撞动画 箭头处于晃动状态 晃动动画中 通过
18 点被挡箭头后箭头总数 不减少 未减少 通过
19 重新开始按钮 失误复原、计时归零 失误复原、计时 0.0 通过
20 重新开始是否换布局 保持同一布局 布局不变 通过
21 飞出动画方向 与箭头朝向一致 一致(D) 通过
22 清空全部箭头 进入通关结算界面 进入结算 通过
23 零失误通关成绩 3 星 / 1000 分 3 星 / 1000 分(用时 2.4s / 目标 15s) 通过
24 本局总分累计 正确累计 1000 通过
25 最高分写入存档 记录 score/stars/best_time 写入成功 通过
26 更差成绩的覆盖行为 不覆盖最高分 仍为 1000 通过
27 失误耗尽 进入失败界面 进入失败界面 通过
28 失败是否计分 不计分、不写记录 result 为空、无记录 通过
29 失败界面「关卡选项」 返回关卡选项界面 返回 通过
30 随机关卡可复现性 同种子布局完全相同 完全相同 通过
31 随机关卡随机性 不同种子布局不同 不同 通过
32 随机关卡可解性 全部可解 100% 可解 通过
33 随机关卡每关开局有阻挡 至少 1 支被挡 满足(均值 1.6~7.6) 通过
34 随机模式 6 局 × 5 关自动通关 30 关全部清空 全部清空 通过
35 随机模式不写固定关卡存档 记录为空 为空 通过
36 经典 5 关连打 5 关全部清空并进入全部通关 通过 通过
37 5 关成绩全部入档 5 条记录 5 条 通过
38 随机模式不误报"刷新最高分" 不显示(随机布局不入档) new_record=False 通过

汇总:106 项断言,0 项失败,脚本退出码 0。

其它验证

测试项目 预期结果 实际结果 结论
5 关可解性(verify_levels.py 全部存在通关顺序 第 1~5 关全部可解 通过
帧率(菜单 / 关卡选项 / 对局 / 飞出动画) 单帧远低于 16.7ms 0.21 / 0.15 / 2.60 / 3.24 ms 通过
素材缺失容错 能退回程序绘制不崩溃 程序绘制兜底生效 通过
无声卡环境 静默降级不影响游戏 正常运行 通过

6.PSP 表格:

任务 预估耗时(分钟) 实际耗时(分钟) 差异(分钟)
需求分析与玩法设计 5 5 0
箭头 / 方向 / 关卡表示 15 20 +5
路径检测与失误判定 15 15 0
图形界面与动画 5 10 +5
关卡选项 + 计分星级 + 存档 20 15 -5
AIGC 素材与音效 15 15 0
测试与性能调优 10 10 0
文档与博客 30 50 +20
合计 115 140 +25

7.心得体会:

AI帮了我什么

  • 把重复劳动压缩掉:四界面 UI、按钮 / 卡片组件、动画框架、四五十行的样例数据,
    这些"知道要做什么但很费手"的部分,AI 基本一次就能给到可用的程度;
  • 它提的方案往往比我最初想的多一层:比如我本来只想"随机生成关卡",
    它建议"生成后立刻用求解器验证",这才让随机关卡敢直接上线;也是顺着它的建议做统计,
    我才发现"陷阱步恒为 0"这条性质,反过来指导了难度设计;

学到了什么

  • 使用ai开发的具体流程: 确实之前没什么开发经验,拿到作业的第一时间我想的是,直接复制
    具体的任务要求给ai,直接让他生成代码就好了,但这样做出来的游戏有很多缺陷(可能就是我个人
    思考问题不够全面吧),然后去b站和博客上学习如何使用ai开发,学会先让ai从小的做起,先完成MVP
    ,可以在完成基础内容后生成个demo试看一下,加功能的时候不要连着做多个不同的功能,一方面一方面
    地完成,提高效率。
  • markdown的撰写:也是花了好多的时间在写博客上了说是,确实还用不太熟,对效果不大满意,
    使用ai生成了部分markdown代码,学到了挺多的hhh:D。
posted @ 2026-09-20 20:17  JunYu_101  阅读(8)  评论(0)    收藏  举报