《一箭又一箭》—— 用 Python + Pygame 做一个"要动脑"的解谜小游戏

《一箭又一箭》—— 用 Python + Pygame 做一个"要动脑"的解谜小游戏

一句话介绍:深空霓虹风的小游戏。点一支箭,如果它头部指向的那条线上没有被别的箭占住任何格子,
整支箭就会顺着自己的身子被"抽"出棋盘;被挡住则不能消除,并扣掉一次失误。
六关闯关制,棋盘从 12×12 递增到 22×22,上面密密麻麻铺满 6~20 格的折线箭矢

  • 技术栈:Python 3.13 + pygame 2.6.1(只用这一个第三方库
  • 代码量:核心逻辑 3 个模块 + 界面 6 个模块 + 6 个开发/测试工具
  • 运行方式:
pip install pygame
python main.py

一、项目展示

演示动画

演示:开始界面 → 闯关 → 悬停提示 → 消除 → 撞挡 → 通关

demo
是程序真实渲染的录屏,约 10 秒;同目录下还有体积更小的 demo

开始界面

[开始界面:闯关路线,已通关的关卡是绿色对勾,未解锁的是挂锁](
01-menu

顶部是标题与三行图文规则(三支小箭都是程序里真实渲染的),下面是一条闯关路线
6 个关卡节点按顺序用连接线串起来,已通关的是绿色 ✅(可以点进去重玩),当前关是高亮脉冲,
还没解锁的是灰色挂锁——点它只会提示"先通关第 N 关"。主按钮文案跟着进度走:
开始游戏继续闯关(第 N 关) → 六关全通后变成 重新闯关

游戏过程

悬停在能飞的箭上(停留约 0.45 秒才给提示,留出思考时间):

[悬停在畅通的箭上,出现绿色通道](
02-hover-free

)

悬停在被挡的箭上:红色通道 + 红框圈出挡住它的那一支(直接把整支挡路的箭都标红):

[悬停在被挡的箭上,标出挡路的那一支](
03-hover-blocked

)

点错时:整支箭向前顶一下再弹回、烧成红色,撞点扩散红圈,飘出"失误 -1",心形减少一个:

[点被挡的箭:变红、涟漪、飘字、扣一次失误](
04-collision

)

点对时:整支箭顺着自己的身子匀速滑出棋盘,翻过的路径留下光带,箭头出界处炸开冲击波和粒子。

通关与失败

[通关弹窗](
05-cleared

)

[失败弹窗](
06-failed

)

通关弹窗会显示点击次数、失误次数、剩余失误,并提示"第 N 关已解锁";失败弹窗会显示还差几支箭。


二、项目介绍

2.1 游戏规则

  1. 每支箭由一个箭头(头部)+ 一条 6~20 格的折线箭身组成,头部朝上/下/左/右。
  2. 紧挨头部的那一格箭身一定在头部正后方——箭头三角和箭身是一条直线接出去的,
    看起来像一支真正的箭,方向不会在头部突然拐弯。
  3. 点击一支箭(点头部或箭身任意一格都算):
    • 前方畅通 → 整支箭沿自己的轨迹滑出棋盘、永久消失;
    • 前方被挡 → 不能消除,扣一次失误,并圈出挡路的那一支。
  4. 箭身也算占位——它会挡住其他箭的路线,一支长箭常常同时卡住好几支箭;
    不同箭的箭身允许贴边交错,所以盘面会缠成一片。
  5. 箭自己的身子不挡自己。
  6. 清空棋盘上所有箭即过关;失误次数用光则本关失败,可以重新开始。
  7. 闯关模式:必须从第 1 关开始,通关当前关卡才解锁下一关,不能跳关。

2.2 界面设计

深空霓虹风格:靛蓝渐变背景 + 四团柔光 + 星点 + 暗角;棋盘是一块毛玻璃屏幕,格子是细边槽位;
箭矢是带渐变、描边、高光和外发光的霓虹折线,四种朝向四种基色(冰蓝 / 琥珀 / 薄荷 / 品红),
同朝向的不同箭再用细微亮度差区分,交错在一起也能看出是两支。

游戏界面顶部是信息栏:关卡徽章 + 关卡名、剩余箭矢数 + 进度条、剩余失误心形、重新开始 / 主菜单;
中间是棋盘;底部左下角常驻显示"本关特征",中间是提示,右边是快捷键。
棋盘最大 22×22(484 格),窗口按桌面自适应(目标 1920×1080)。

2.3 主要功能

功能 说明
四方向箭头 上 / 下 / 左 / 右,用方向 + 折线箭身表示
鼠标选箭 点头部或箭身任意一格都等效
路径检测 沿头部朝向逐格走到边界,判断路上有没有别的箭
消除飞出 整支箭沿自身轨迹滑出棋盘,带光带 / 冲击波 / 粒子
碰撞反馈 回弹 + 变红 + 涟漪 + 飘字 + 轻微抖屏,并扣一次失误
失误机制 界面显示剩余次数,用光判失败
重新开始 一键把本关恢复到初始状态
闯关进度 未通关不能跳关,主菜单有一条可视化关卡路线
路线提示 悬停 0.45 秒显示这条线通不通(绿 / 红通道)
6 个关卡 各有一套"配方",盘面特征各不相同,均验证可通关

2.4 特色

  • 关卡是"构造出来必定可通关"的,不是试出来的(见 3.5)。
  • 每关一个特征,从盘面一眼能区分:短箭多弯 → 蛇身缠成一片 → 长箭横贯全图 →
    八成的箭朝同一方向 → 依赖链最深 → 最多最密、长短混杂。
  • 难度是可量化的:每关都统计"开局可消支数 / 平均每步可选数 / 平均被挡支数 / 依赖链深度",
    现在的六关分别是 24 支、1.4~2.1 个、2.13.4 支、14~20 层

三、实现思路

3.1 箭头与方向怎么表示

方向就是 0~3 四个整数,配一张位移表,所有"往前走一格""反向"之类的操作都查表:

UP, RIGHT, DOWN, LEFT = 0, 1, 2, 3
DELTA    = {UP: (-1, 0), RIGHT: (0, 1), DOWN: (1, 0), LEFT: (0, -1)}
OPPOSITE = {UP: DOWN, DOWN: UP, LEFT: RIGHT, RIGHT: LEFT}

@dataclass(frozen=True)
class Arrow:
    cells: Tuple[Cell, ...]   # cells[0] 是头部(带方向),后面依次是箭身,尾部在最后
    direction: int            # 头部朝向

    @property
    def head(self):  return self.cells[0]
    @property
    def length(self): return len(self.cells)
    def body(self):  return self.cells[1:]

一支箭同时是"一个点"和"一条占位的折线":头部(cells[0])决定它能往哪飞,
整条 cells 决定它占了哪些格子、会挡住谁。把顺序写死(头在最前、尾在最后)省掉了很多
"谁是头"的歧义——渲染动画、颜色渐变、判定路径都直接按这个顺序来。

两条硬规则(都写在 Board.structural_errors() 里,关卡数据非法会直接报错):

# 规则 1:头部朝向上不能压着自己的身子,否则这支箭永远飞不出去
blocked = self.self_blocked_cells(arrow)

# 规则 2:紧挨头部的那一格必须在头部正后方(箭头方向不突变)
if arrow.neck_direction is not None and arrow.neck_direction != OPPOSITE[arrow.direction]:
    errors.append("箭头 {} 的方向与紧贴头部的那一格箭身不一致".format(arrow.head))

规则 2 是我在试玩时觉得"看着别扭"提出来的:原来箭头三角可能插在一条横着的柱子上,
方向看着像"拐了个弯",从解谜变成了考眼力。把它变成硬规则之后有个连锁反应——
朝向从此由几何唯一决定,不能再事后改朝向来修关卡,所以关卡生成方式也必须换(见 3.5)。

3.2 一关怎么表示

关卡数据是一串显式路径描述,一行一支箭:

Level(name="短兵相接", rows=12, cols=12, lives=3, tag="短箭 · 多弯", paths=(
    "3 4 R DDLD",   # 头部在第 3 行第 4 列(从 0 开始),朝右;
                    # 箭身依次往下、往下、往左、往下走 4 格
    ...
))

最初用的是字符画(^ v < > 表示箭头、# 表示箭身),但后来需求变成"允许不同箭的箭身贴边交错",
字符画就废了——相邻两支箭的 # 会连成一片,无法判断哪一段属于哪一支。
换成显式路径后,既不依赖连通性判断,也允许贴边,还能一眼看出每支箭的形状。
想看盘面可以用 python -m arrowout.levels,它会打印"每支箭一个符号"的棋盘图和参考解法。

3.3 路径检测(重点)

判定规则一句话:从头部格子出发,沿朝向每次走一格,一直走到棋盘边界;路上遇到的第一格若属于别的箭,就是被挡住了。

代码只有十几行:

def blocker_cell_of(self, index: int) -> Optional[Cell]:
    """头部射线撞到的第一格(别的箭);畅通返回 None。"""
    arrow = self.arrows[index]
    dr, dc = DELTA[arrow.direction]
    r, c = arrow.head
    while True:
        r += dr
        c += dc
        if not self.in_bounds((r, c)):
            return None                    # 一路走到边界都没有东西 → 可以飞出去
        owner = self.owner.get((r, c))
        if owner is not None and owner != index:
            return (r, c)                  # 撞到别的箭,返回挡路的位置

几个关键点:

  • 只走一条直线。规则限定"同一行或同一列",所以不需要做几何求交、也不用扫整个棋盘,
    逐格步进就够了,复杂度是 O(到边界的距离)(最多 22 步)。
  • owner != index 是"自己的身子不挡自己"。为了做到 O(1) 判断"这格属于谁",
    棋盘维护了一张 owner: 格子 → 第几支箭 的索引表,任何增删箭之后调 _reindex() 重建:
def _reindex(self) -> None:
    self.owner = {}
    for index, arrow in enumerate(self.arrows):
        for cell in arrow.cells:
            self.owner[cell] = index
  • 顺带收集"整条射线上都有谁",得到的就是箭与箭之间的依赖关系,
    用它算难度指标(一支箭被几支挡、依赖链有多深),也是关卡生成器的核心数据:
def blocker_indices_of(self, index: int) -> Set[int]:
    """挡住这支箭的所有箭(射线穿过的每一支都算)。"""
  • 可解性判定用"单调性 + 贪心"。消除某支箭只会让别的箭更自由,不会让原本能动的箭变成不能动,
    所以随便挑一支当前可消除的箭消掉,不会破坏可解性;一旦还有箭却一支都动不了,就说明无解:
def solve(self) -> Optional[List[Cell]]:
    board = self.copy(); order = []
    while board.arrows:
        free = board.free_heads()
        if not free:
            return None                 # 卡住了 → 这盘无解
        board.remove_at(free[0]); order.append(free[0])
    return order                        # 返回一条完整通关顺序

3.4 分层结构:逻辑与渲染彻底分开

arrowout/
├── board.py     纯逻辑:棋格、箭矢、路径判定、求解器   ← 不 import pygame
├── game.py      LevelSession:一关的状态机(点击 / 失误 / 胜负)
├── levels.py    6 关数据 + 结构校验                    ← 不 import pygame
├── theme.py     尺寸、深色配色、字体、渐变/发光素材
├── ui.py        毛玻璃面板、霓虹按钮、发光文字
├── render.py    布局换算、箭矢贴图(超采样)、平滑路径与渐隐遮罩
├── effects.py   飞出 / 回弹 / 涟漪 / 冲击波 / 粒子 / 飘字
├── scenes.py    开始、游戏、结算界面
└── app.py       窗口、主循环、闯关进度、淡入切换

这么分的收益很直接:一个没有显示器的环境里就能验证关卡对不对、规则有没有写歪,
所有自动测试都只需 from arrowout.board import Board

3.5 关卡是怎么生成的(构造上必定可通关)

这是整个项目里最花心思的部分。先说结论:关卡的消除顺序是"倒着"构造出来的。

设一关的消除顺序是 a₁, a₂, …, aₙ(a₁ 最先被消除)。那么"这一关可通关"等价于:

对每个 k,ray(aₖ)(aₖ 头部到边界的射线)不能穿过 aₖ…aₙ 里任何一支箭的格子。

于是按消除顺序逆序来放(先放 aₙ,最后放 a₁),约束就只剩一条:
新箭的射线不能被任何已经放好的箭挡住(同时它自己的身子不能压在自己的射线上)。
放完把顺序倒过来,就是一条合法的消除顺序——构造上必定可通关,不需要事后修补

在此基础上又加了三样东西,才有现在这个密度和难度:

  1. 主动挡人:新箭的箭身优先去穿过"还畅通着的那几支箭"的射线,
    等于每放一支就顺手堵住上一支,开局可选数能从 912 支压到 24 支。
  2. 定向堵路:万一这一轮所有候选都挡不住别人(棋盘已经很挤了),就退一步——
    专门挑一支还畅通的箭,在它的射线上找一段"到边界全是空"的走廊,
    把新箭的头放进走廊里,那支箭立刻被挡住;新箭自己可能是畅通的,
    但"畅通总数"不会增加。这一步是难度能不能上去的关键。
  3. 补空 + 翻转加难:剩下的零散空格接到相邻箭的尾部(只要不挡住"比它先消除"的箭,
    长度不超过 20),占格率从 60% 拉到 88~91%;再把还能点掉的箭"翻个身"
    (头换到路径另一端,格子不变,只改自己的射线),仍可通关且可点数变少就保留。

每一关还有一份自己的配方:长短分布(短 / 中 / 长三段加权)、整体转弯倾向、
"射线要长"的权重、方向偏置。这就是"六关特征各不相同"的来源:

关卡 特征 配方要点 棋盘 箭数 身长 平均每步可选 依赖深度
1 短兵相接 短箭 · 多弯 身长偏短、转弯率高 12×12 16 6~13 2.1 14
2 缠丝成网 蛇身缠成一片 中等身长、彼此穿射线 14×14 17 6~18 1.9 15
3 长驱直入 长箭横贯全图 长身长、几乎不转弯 16×16 17 6~20 1.4 14
4 四方来朝 八成的箭朝同一方向 方向偏置(上为主) 17×17 23 7~20 1.7 17
5 层层套锁 依赖链最深 长射线权重最高 19×19 28 6~20 1.8 18
6 天罗地网 最多最密 · 长短混杂 长短三段均衡 22×22 31 7~20 2.0 16

生成器一次会跑上百盘候选,按"关联强度 / 依赖深度 / 短身折弯 / 长身跨度 / 覆盖率 / 平均可选数"
打分择优,最后固化进 levels.py(随机种子固定,可复现)。

3.6 渲染与动画的两点小技巧

(1)飞行动画:整支箭沿自己的轨迹"流"出去。
把"尾 → 头 → 出界点"连成一条圆角路径(每个拐角用二次贝塞尔替换,并记下每个原始格心的弧长),
箭身每一格都是路径上的一个采样点,所有采样点以同一速度前进——于是整支箭像被拉着的链子一样
滑出去,转弯自然走圆弧、形状不散。头部越过棋盘边缘后用一张"边界内不透明、越界后线性降到 0"
的渐隐遮罩按距离淡出,就有"到了边界就消失"的效果。

(2)清晰度与性能:超采样,但只给"锐利的部分"。
贴图按 2.8~4 倍超采样再平滑缩回,边缘干净很多;但投影和外发光这类模糊层放到 1 倍分辨率上算——
它们本来就是低频图形,放大回来看不出差别,却省掉一个数量级的时间。再加上所有贴图按
(箭身格子, 朝向, 颜色, 格宽) 缓存、动画里的箭走"整条身子用一个多边形一次填出"的快速路径,
实测(1920×1080):贴图构建约 150ms、静止约 4ms/帧、飞出中约 10ms/帧,稳在 60fps 以内。


四、AIGC 使用过程

整个项目是在 AI 辅助下做的。我负责定需求、定验收标准、逐项核对结果;AI 负责拆解、实现、自测并给出证据。
下面记 4 次比较有代表性的协作过程(包括 AI 出错、我把它纠正过来的部分)。

4.1 把"看着别扭"翻译成可校验的硬规则

内容
我的需求 "箭头的方向不可以突变,靠近箭头的一格箭身的方向一定要和箭头的方向一致,否则玩起来观感很差,从益智游戏变成了考眼力游戏。"
AI 的产出 没有只改渲染,而是把它写成了结构不变量Board.structural_errors() 里检查"紧挨头部的那一格必须在头部正后方",违规的关卡数据直接报错。同时提醒我一个连锁反应:朝向从此由几何唯一决定,原来那套"密铺 + 事后改朝向修可解性"的生成方式失效了,得换一种生成方式。
我的验证 python -m arrowout.levels 对六关做结构与可通关校验;② 把截图放大看箭头是不是"一支箭"的样子;③ 顺手发现开始界面第三支示例箭也是方向突变,一起修了。
结果 这条规则进了不变量,以后手改关卡数据也会被拦住;生成方式在下一轮换代。

体会:把模糊的感受("看着别扭")说成可判定的条件("紧挨头部的一格必须在正后方"),
AI 才能真正把它固化下来,而不是"这次改对了、下次又歪了"。

4.2 生成方式换代:从"密铺 + 修复"改成"逆序构造"

内容
我的需求 "难度还是不够高……尽量能错综复杂的填满地图。"
AI 的产出 因为在 4.1 里失去了"改朝向"这个修复手段,原来的生成器基本报废。AI 重新推导了可通关的等价条件,改成按消除顺序的逆序构造,并诚实指出:它早先写进自己笔记里的一条结论("逆序构造最多只能铺到 1/3 密")是错的——当年铺不密是因为额外加了"箭身要避开别人射线"这条不必要的约束。去掉之后,同样一条规则能把占格率做到 88~91%。
我的验证 求解器逐关验证 + tools/verify_gameplay.py 用界面同一套接口把六关各点一遍(384 项断言),确认"每一关都真的能通关"。
结果 占格率 60% → 88~91%;而且生成器本身不用做"修复",复杂度反而下降了。

体会:AI 会犯错,而且会把错误结论当经验沉淀下来。所以关键结论必须自己复算一遍——
我这边的手段就是"让求解器来说话"。

4.3 "每一步的选择还是太多":从模糊反馈到可测量指标

内容
我的需求 "箭身彼此之间的联系不够多,每一步的选择还是太多了,导致难度骤减。"
AI 的产出 先把"选择太多"翻译成三个可测量指标(开局可消支数 / 平均每步可选数 / 依赖链深度),插桩定位到真实原因:构造时一旦"没有候选能挡住别人",程序就随便放一支,可选数立刻反弹。于是补了一条定向堵路机制。这里 AI 踩了个真 bug——判断"新箭挡住了谁"时方向写反了(应该看新箭自己的格子落在谁的射线上,写成了"新箭的射线穿过了谁"),导致这条兜底永远不生效;修掉之后开局可选从 45 支降到 23 支。
我的验证 直接看 -m arrowout.levels 打印的"逐步可选数 min / avg",确认从 2.12.7 降到 **1.42.1**;再实际试玩第 3 关,11 步里只有一种选择。
结果 开局可消 24 支、平均每步 1.42.1 个选择、平均每支被 2.13.4 支挡、依赖链 1420 层。

体会:模糊的"太难/太简单"没法直接优化,必须先把它变成一个数字,AI 才有目标可用。

4.4 UI 互相挡住:AI 也会"看不见",需要我的截图

内容
我的需求 圈了一张信息栏的截图:"这里的 UI 互相挡住了。"
AI 的产出 按坐标核对出根因:剩余箭矢 的数字框是 64104px,而关卡进度条画在 8895px,两者正好叠在一起。信息栏高度从 96 加到 110,把数字和进度条拆成两行。AI 同时承认:这个错在它上一版自己出的截图里就存在,只是当时只看了整图、没把信息栏放大检查,之后把"HUD 局部放大核对"补进了流程。
我的验证 让它把 HUD 单独截成局部大图,确认数字、进度条、心形各自不重叠;后来又发现开场标题条里"特征 / 箭数"那行被卡片底部的进度线压到,一并修掉(卡片加高 + 底色加深,保证压在密集箭身上也能看清字)。
结果 信息栏与标题条都不再重叠。

体会:视觉问题只有真的看图才能发现。AI 会跑通逻辑、跑通测试,但它对"看着舒服不舒服"这件事并不可靠,
所以"逐张读截图"这一步不能省。

4.5(附带)性能:从 22.4ms/帧 降到 9.6ms/帧

飞行动画最初是"每一段画一个矩形 + 每个拐点画一个圆",一支 20 格的箭有 40 个点、两百多次绘制调用,
实测飞行时 22.4ms/帧(已经掉到 45fps)。AI 把它改成整条身子扩展成一个闭合多边形一次填出
(两侧按半宽偏移 + 两端半圆),降到 9.6ms/帧,稳回 60fps。这类"性能热点定位 + 换算法"是 AI 比较擅长的地方。


五、测试结果

测试分四层:纯逻辑(命令行)→ 关卡体检 → 真实事件驱动的界面流程 → 截图核对

5.1 自动试玩:tools/verify_gameplay.py(384 项断言)

用界面同一套接口(LevelSession)把六关各真实点一遍:

测试项目 预期结果 实际结果 是否通过
每关存在通关顺序 求解器给出完整顺序 6 关分别 16 / 17 / 17 / 23 / 28 / 31 步
按顺序点击 全部判为通关、剩余 0 支 一致
点箭身任意一格 等效于点这支箭(同一 head) 一致
点被挡的箭 不消除、扣 1 次失误、返回挡路坐标 一致
先错一次再通关 依然能通关 一致
点空地 无效,且不扣失误 一致
失误耗尽 判失败,剩余失误 0 一致
重新开始 棋盘与失误次数复原初始状态 一致
失败后重开 仍能正常通关 一致
结构校验 不越界 / 不重叠 / 不压自己射线 / 箭头方向不突变 六关 0 错误

5.2 界面端到端:tools/smoke_test.py(35 项检查)

真实的鼠标 / 键盘事件塞进事件队列,跑和主循环一样的 event → update → draw

测试项目 预期结果 实际结果 是否通过
启动落在开始界面 只有第 1 关解锁 一致
点击未解锁的关卡节点 不进关,只给提示 停在开始界面 + 提示文案
强行跳关(直接调 start_level) 被拒绝 仍停在开始界面
开始游戏 进入第 1 关,箭数正确 一致
悬停累积 0.45 秒 出现路线提示 hover_time > 0.45 且提示就绪
点畅通的箭 箭数 -1 一致
点被挡的箭 扣 1 次失误 3 → 2
按 R 棋盘与失误复原 一致
Esc 回主菜单 进度不变,「继续闯关」回本关 一致
通关第 1 关 弹窗显示 + 第 2 关解锁 一致
点「下一关」 进入第 2 关 一致
失误用光 失败弹窗 + 重新开始可用 一致
全通关 结算界面 + 按钮变「重新闯关」 一致

5.3 截图核对:tools/screenshots.py(13 张)

用 SDL dummy 驱动无窗口渲染每个界面,逐张放大检查。这一层抓到的问题最多:

发现的问题 处理 是否通过
信息栏里进度条压住"剩余箭矢"的数字 信息栏加高到 110,数字与进度条拆成两行 ✅(修复后通过)
开场标题条里"特征 / 箭数"被卡片底线压到 卡片加高到 132、底色加深 ✅(修复后通过)
开始界面第三支示例箭方向突变(头压着自己箭身) 换成合规的格子序列
悬停光带戳出棋盘外 路径终点收到面板边缘内
通关文案把箭头总数显示成 0 修文案取数

5.4 关卡体检与性能

python -m arrowout.levels        # 结构 + 可通关性 + 难度指标 + 符号棋盘 + 参考解法
测试项目 预期结果 实际结果 是否通过
关卡结构与可通关性 六关全部合法且可通关 一致
贴图构建耗时 进关不卡顿 26~31 支箭共约 150ms
静止帧耗时(1920×1080) < 16.6ms 约 4ms/帧
飞行动画帧耗时 < 16.6ms 约 10ms/帧
真实窗口启动 正常跑满 8 秒无异常 退出码 124

5.5 六关实测数据

关卡 棋盘 箭数 占格率 开局可消 平均每步可选 平均被挡 依赖深度 平均身长 身长范围
1 短兵相接 12×12 16 86% 3 2.1 2.12 14 7.8 6~13
2 缠丝成网 14×14 17 88% 3 1.9 2.29 15 10.2 6~18
3 长驱直入 16×16 17 88% 3 1.4 3.18 14 13.3 6~20
4 四方来朝 17×17 23 90% 4 1.7 3.00 17 11.3 7~20
5 层层套锁 19×19 28 91% 3 1.8 2.79 18 11.8 6~20
6 天罗地网 22×22 31 90% 3 2.0 2.32 16 14.1 7~20

六、PSP 表格

(下表按本次实际的开发节奏填写,可以直接改成你自己的真实投入时间。)

阶段 主要工作 预计耗时(分钟) 实际耗时(分钟)
需求与玩法梳理 拆解作业要求,定"最少要有什么" 30 20
程序结构设计 逻辑/渲染分层、模块划分、数据结构选型 40 35
核心逻辑 棋盘、箭矢、路径判定、求解器 90 110
图形界面与渲染 深空霓虹美术、贴图缓存、超采样、界面布局 120 180
关卡生成器 密铺/逆序构造、定向堵路、补空、翻转加难 90 240
动画与特效 飞行圆角路径、回弹、涟漪、冲击波、粒子 60 90
自动测试与截图核对 自动试玩、冒烟测试、13 张截图逐张核对 60 130
文档与博客 README、博客正文、演示 GIF 50 70
合计 540 875

七、心得体会

7.1 AI 帮了我什么

  • 把想法直接变成能跑的东西。我说"箭身要 6~20 格、允许交错、箭头方向不许突变",
    它能落到数据结构、渲染、关卡数据格式一整套改动上,而且每改一处都会跑一遍自测。
  • 替我做了不可能手工做到的事。要在 22×22 的棋盘上铺 31 支互相咬住的箭、还得保证一定能通关——
    这种盘面手工摆根本摆不出来。AI 用"逆序构造"把它变成了一个可证明的问题。
  • 找出我肉眼看不到的问题。性能热点(22.4ms/帧)、像素级的 UI 重叠,都是它先量出数字我才信。
  • 让"验收"变得可执行。384 项断言、35 项界面流程、13 张截图——我不用凭感觉说"应该没问题"。

7.2 出现的问题

  • AI 会把错误结论当成经验。它一度笃定"逆序构造最多只能铺到 1/3 密",害得我们绕了一大圈改用另一种生成方式,
    后来才发现是当年多加了一条没必要的约束。所以关键结论我都会让它再复算一遍,用求解器说话。
  • AI 会写反逻辑。"新箭挡住了谁"判定写反,导致一条兜底机制永远不生效——表面看不出来,
    是插桩打日志才暴露的。
  • AI 对"好不好看"不可靠。信息栏重叠这种问题,它自己出的截图里就有,但它只看了整图。
    视觉的事必须人来把关。
  • 模糊的需求会喂出模糊的结果。我说"难度还是不够高",它可以给我十几版改动但都不对路;
    一旦我说清"每一步的选择太多",它就能定位到具体机制。

7.3 我的收获

  1. "可验证"应该是最先设计的东西,而不是最后补的。 一开始就把逻辑层和渲染层分开、
    board.py 不依赖 pygame,后面才可能用命令行验证六关是否可通关——这个决定省了后面大量的返工。
  2. 模糊反馈要翻译成数字。 "难度不够" → 开局可消支数 / 平均每步可选数 / 平均被挡支数 / 依赖链深度;
    "看着别扭" → 箭头方向不突变;"铺满地图" → 占格率。有了数字,改进才有方向,也才能验收。
  3. 不能只看 AI 说"做好了"。 要它给证据:跑测试、出截图、报数字。
    这次的每一个版本我都是靠"读截图 + 看指标表 + 自己试玩"来确认的。
  4. 约束越硬,越不容易退化。 把"箭头方向不许突变"写成 structural_errors() 里的硬规则之后,
    后面任何一次改动都逃不过它,这比写在文档里靠谱得多。

附:怎么运行与自测

pip install pygame
python main.py                      # 开始游戏

# 自测(都不需要显示器)
python tools/verify_gameplay.py     # 384 项断言:可通关 / 点箭身等效 / 失误机制 / 重开复原
python tools/smoke_test.py          # 35 项真实事件级界面流程(含闯关解锁逻辑)
python tools/screenshots.py         # 无窗口渲染 13 张界面截图
python -m arrowout.levels           # 关卡体检:结构、可通关性、难度指标、参考解法
python tools/generate_levels.py     # 重新生成六关(固定种子,可复现)
python tools/make_demo.py           # 重新录制演示 GIF 与配图
posted @ 2026-09-20 18:39  zkk321  阅读(7)  评论(0)    收藏  举报