《一箭又一箭》—— 用 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
一、项目展示
演示动画
(
是程序真实渲染的录屏,约 10 秒;同目录下还有体积更小的 
开始界面
[开始界面:闯关路线,已通关的关卡是绿色对勾,未解锁的是挂锁](

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

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

)
点错时:整支箭向前顶一下再弹回、烧成红色,撞点扩散红圈,飘出"失误 -1",心形减少一个:
[点被挡的箭:变红、涟漪、飘字、扣一次失误](

)
点对时:整支箭顺着自己的身子匀速滑出棋盘,翻过的路径留下光带,箭头出界处炸开冲击波和粒子。
通关与失败
[通关弹窗](

)
[失败弹窗](

)
通关弹窗会显示点击次数、失误次数、剩余失误,并提示"第 N 关已解锁";失败弹窗会显示还差几支箭。
二、项目介绍
2.1 游戏规则
- 每支箭由一个箭头(头部)+ 一条 6~20 格的折线箭身组成,头部朝上/下/左/右。
- 紧挨头部的那一格箭身一定在头部正后方——箭头三角和箭身是一条直线接出去的,
看起来像一支真正的箭,方向不会在头部突然拐弯。 - 点击一支箭(点头部或箭身任意一格都算):
- 前方畅通 → 整支箭沿自己的轨迹滑出棋盘、永久消失;
- 前方被挡 → 不能消除,扣一次失误,并圈出挡路的那一支。
- 箭身也算占位——它会挡住其他箭的路线,一支长箭常常同时卡住好几支箭;
不同箭的箭身允许贴边交错,所以盘面会缠成一片。 - 箭自己的身子不挡自己。
- 清空棋盘上所有箭即过关;失误次数用光则本关失败,可以重新开始。
- 闯关模式:必须从第 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₁),约束就只剩一条:
新箭的射线不能被任何已经放好的箭挡住(同时它自己的身子不能压在自己的射线上)。
放完把顺序倒过来,就是一条合法的消除顺序——构造上必定可通关,不需要事后修补。
在此基础上又加了三样东西,才有现在这个密度和难度:
- 主动挡人:新箭的箭身优先去穿过"还畅通着的那几支箭"的射线,
等于每放一支就顺手堵住上一支,开局可选数能从 912 支压到 24 支。 - 定向堵路:万一这一轮所有候选都挡不住别人(棋盘已经很挤了),就退一步——
专门挑一支还畅通的箭,在它的射线上找一段"到边界全是空"的走廊,
把新箭的头放进走廊里,那支箭立刻被挡住;新箭自己可能是畅通的,
但"畅通总数"不会增加。这一步是难度能不能上去的关键。 - 补空 + 翻转加难:剩下的零散空格接到相邻箭的尾部(只要不挡住"比它先消除"的箭,
长度不超过 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——判断"新箭挡住了谁"时方向写反了(应该看新箭自己的格子落在谁的射线上,写成了"新箭的射线穿过了谁"),导致这条兜底永远不生效;修掉之后开局可选从 4 |
| 我的验证 | 直接看 -m arrowout.levels 打印的"逐步可选数 min / avg",确认从 2.1 |
| 结果 | 开局可消 2 |
体会:模糊的"太难/太简单"没法直接优化,必须先把它变成一个数字,AI 才有目标可用。
4.4 UI 互相挡住:AI 也会"看不见",需要我的截图
| 内容 | |
|---|---|
| 我的需求 | 圈了一张信息栏的截图:"这里的 UI 互相挡住了。" |
| AI 的产出 | 按坐标核对出根因:剩余箭矢 的数字框是 64 |
| 我的验证 | 让它把 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 我的收获
- "可验证"应该是最先设计的东西,而不是最后补的。 一开始就把逻辑层和渲染层分开、
让board.py不依赖 pygame,后面才可能用命令行验证六关是否可通关——这个决定省了后面大量的返工。 - 模糊反馈要翻译成数字。 "难度不够" → 开局可消支数 / 平均每步可选数 / 平均被挡支数 / 依赖链深度;
"看着别扭" → 箭头方向不突变;"铺满地图" → 占格率。有了数字,改进才有方向,也才能验收。 - 不能只看 AI 说"做好了"。 要它给证据:跑测试、出截图、报数字。
这次的每一个版本我都是靠"读截图 + 看指标表 + 自己试玩"来确认的。 - 约束越硬,越不容易退化。 把"箭头方向不许突变"写成
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 与配图

浙公网安备 33010602011771号