Serpent / AI strategy study / mathematical notes

Serpent AI 策略研究:从最短路到压力控制

这不是一篇普通的游戏攻略。它记录的是一个小游戏如何被拆成路径搜索、资源经济、几何压力和长期控制问题。

← 文章目录 · 玩 Serpent · 查看全部策略回放 · English version

总判断:Serpent 的核心已经不再是普通贪吃蛇的“路径最短问题”,而是一个带随机资源、几何压力、时间约束的控制问题。

最早的问题是:怎样吃到更多食物。最后的问题变成:如何在 30 秒进食约束下,用食物、大奖励、墙和危险物调节几何压力 P=Nq²,同时让分数保持正漂移。

0. 问题重定义:从小游戏到动力系统

我最开始以为 Serpent 是一个“贪吃蛇变体”:吃东西,避障碍,尽量活久一点。这个理解没有错,但它太浅。

当 Greedy、A*、Lookahead、NoGrow、TailGuard、Depth-2、MinLength 一层层跑出来之后,这个游戏真正暴露出来的不是“哪个 AI 分数高”,而是它的状态空间里有一套非常清楚的动力结构:分数在增长,长度在增长,size 在扰动几何压力,墙和危险物在提供修剪,timeout 在强迫系统周期性进食。

所以本文的核心不是“怎么打高分”,而是:Serpent 的长期最优控制对象到底是什么?

早期目标:maximize score 中期目标:maximize score subject to survival 后期目标:maximize E[dS/dt] subject to E[dP/dt] ≈ 0, τ < 30s, death risk low

最终答案不是 length,也不是 size,而是:

pressure = length × size² P(t) = N(t)q(t)²

1. 游戏规则的数学抽象

1.1 状态变量

设当前游戏状态为:

X(t) = {N(t), q(t), S(t), τ(t), M(t)}
符号含义
N(t)蛇的节数,也就是 length。
q(t)蛇的粗细 size,取值 1、2、3。
S(t)当前分数。
τ(t)距离上次吃普通食物或大奖励的时间。
M(t)地图状态,包括食物、大奖励、危险物、障碍物和蛇身形状。

代码里的关键常量可以直接翻译成数学约束:MAX_SIZE = 3,无进食死亡时间是 NO_PROGRESS_TIMEOUT = 30000ms,普通食物 +10,大奖励 +50,危险物 -20,障碍物扣分为 -5/-10/-15,砍节为 1/1/2

1.2 行为变量

每个 tick,AI 直接控制的是方向:

u(t) ∈ {上, 下, 左, 右}

但从策略层面看,它选择的是行为类型:

行为记号效果
吃普通食物F+10 分, +1 节, τ 重置
吃大奖励B+50 分, +1 节, size 可能 +1, τ 重置
撞脆墙W1-5 分, -1 节,至少保留 1 节
撞砖墙W2-10 分, -1 节,至少保留 1 节
撞铁墙W3-15 分, -2 节,至少保留 1 节
吃危险物H-20 分, -2 节,至少保留 1 节,不重置 τ
等待/绕行Ø分数不变,长度不变,但消耗 timeout 余量。
实现细节:危险物可以砍节,但它不刷新 lastFoodEaten。所以危险物不是续命资源,只是减压资源。这一点会直接影响长期控制策略。

2. 基础资源方程

2.1 长度方程

设某段时间内吃普通食物 F 次,吃大奖励 B 次,撞三类墙分别为 W1/W2/W3 次,吃危险物 H 次。名义长度变化是:

ΔN = F + B - W1 - W2 - 2W3 - 2H

但游戏里有一个非常关键的下限:

N ≥ 1

所以更准确地说:

实际砍节 = min(惩罚砍节数, N - 1)

这就是为什么一节蛇撞墙不会继续被砍死。它不是一个小实现细节,而是长期小长度稳态能成立的重要原因之一。

2.2 分数方程

ΔS = 10F + 50B - 5W1 - 10W2 - 15W3 - 20H

如果把障碍物类型看作随机变量,障碍生成概率为:

P(W1)=0.55 P(W2)=0.30 P(W3)=0.15

那么一次随机撞墙的期望扣分是:

E[C_wall] = 0.55×5 + 0.30×10 + 0.15×15 = 2.75 + 3 + 2.25 = 8

一次随机撞墙的期望砍节是:

E[L_wall] = 0.55×1 + 0.30×1 + 0.15×2 = 0.55 + 0.30 + 0.30 = 1.15

所以每修掉 1 节的期望成本约为:

8 / 1.15 ≈ 6.96 分 / 节

于是:

普通食物净收益 ≈ 10 - 6.96 = +3.04 大奖励净收益 ≈ 50 - 6.96 = +43.04

关键结论:只要能持续把增长出来的 length 修掉,普通食物和大奖励都是正收益资源。

这一步把 Serpent 从“避障游戏”变成了“资源账游戏”。墙不是失败;墙是付费修剪。

2.3 timeout 约束

30 秒无进食死亡,所以必须满足:

τ(t) < 30s

长期策略里,食物不是单纯得分源,它也是续命时钟。如果平均进食周期是 T_food,必须有:

T_food < 30s

实际策略要留安全余量,所以一般要把目标压到:

T_food ≤ 22s ~ 25s

危险物和撞墙不重置 τ,所以长期主循环必须包含普通食物或大奖励。墙和危险物只能修剪,不能续命。

3. 几何压力:真正该控制的是 pressure

早期我们只看 N = length。这是必要的,但不够。因为 Serpent 里每节蛇的实际占地会随 size 增长。

size每节占格
11×1 = 1
22×2 = 4
33×3 = 9

所以定义近似几何压力:

P = N × q²

更准确的版本是:

P_real = |OccupiedCells(snake, q)|

因为粗蛇相邻节会重叠,Nq² 是一个上界或近似指标。但它非常有用,因为它揭示了:同样的 length,在不同 size 下完全不是同一个状态。

3.1 墙在不同 size 下的几何修剪效率

撞一次随机墙,期望砍节是 1.15。如果 size 为 q,期望清理几何占地约为:

E[A_trim | q] ≈ 1.15 × q²

期望成本仍约 8 分,所以每清理 1 个几何格的成本:

Cost_per_cell(q) = 8 / (1.15q²)
size期望清理占地每占地格成本
11.158 / 1.15 ≈ 6.96
24.608 / 4.60 ≈ 1.74
310.358 / 10.35 ≈ 0.77

反直觉结论:变粗后,墙作为几何减压阀反而更高效。

但同时,变粗后更容易误撞、更难寻路、可达空间下降。这就是 Serpent 的核心张力:size 提高了修剪效率,也放大了几何风险。

3.2 稳态条件

成熟的长期目标不是 N ≤ 常数,而是:

P = Nq² ≤ P_max

例如可以设:

size建议 length 上限pressure
1N ≤ 5P ≤ 5
2N ≤ 3P ≤ 12
3N ≤ 2 或 3P ≤ 18 或 27

控制器目标变成:

Minimize pressure drift: E[ΔP] ≤ ε Subject to: E[ΔS] > 0 τ < 30s

4. L1–L17:策略演进与算法实现

下面每个策略都不是孤立版本号,而是一次问题重定义。Serpent AI 的进步不是“代码越来越复杂”,而是目标函数越来越接近游戏真实结构。

L1–L2

Greedy 与 A*:最短路问题

baseline / pathfinding

L1 Greedy 直接追最近或最高收益目标:

argmin_target distance(head, target) 或 argmax_target reward(target) / distance(head, target)

它只优化眼前收益,没有考虑 future reachability、self-collision、pressure,也没有考虑 timeout margin。所以它是 baseline,不是可持续策略。

L2 A* 把问题升级成真正的路径搜索:

path* = argmin_path cost(path) cost(path) = path length f(n)=g(n)+h(n)
Implementation notes
open_set: priority queue ordered by f(n)
g(n): current path length
h(n): Manhattan distance to target
blocked: snake body + hard obstacles
neighbors: four directions, with wrap-around if game rules allow

A* 比 Greedy 强,因为它不会只看曼哈顿距离,而是真正避开静态阻挡。但它仍然只解决“当前蛇头到目标是否有路径”,不解决“吃完以后是否还有出口”。

A* replay
seed0:460 分。适合作为 baseline,不是最优录像。
L3a

Lookahead / Chase Tail:安全寻路

reachability-constrained planning

L3a 的关键变化是:吃食物前先模拟路径,吃完以后检查能不能追到尾巴。

simulate(snake, π) → snake' 安全条件:Reachable(head(snake'), tail(snake')) = true

尾巴是一个移动的出口。如果新蛇头还能到达新蛇尾,说明这条路径至少没有立即把自己封死。

Algorithm
def safe_to_eat(game, food):
    path = astar(game.head, food.position, game.blocked_cells)
    if not path:
        return False

    future = simulate(game, path)
    return bfs_reachable(future.head, future.tail, future.blocked_cells)

它的局限也很清楚:它只看一颗食物,不看第二颗食物;它检查尾巴可达,但不最大化剩余空间。

L3a Lookahead replay
seed0:460 分。这个 seed 不够理想,但能展示“吃完检查出口”的思想。
L4

Economic Decision:墙不是障碍,是价格

resource-cost optimization

这是第一个真正的认知跳跃。普通玩家把墙当作不可通行障碍;L4 把墙当作带价格的通道。

Net(target) = Reward(target) - ObstacleCost(path_to_target)

普通食物撞脆墙:+10 - 5 = +5。大奖励撞铁墙:+50 - 15 = +35。从资源账看,墙不是失败,而是修剪成本。

Implementation notes
def cell_cost(cell):
    if cell.is_empty:
        return 1
    if cell.is_fragile_wall:
        return 1 + wall_weight * 5
    if cell.is_brick_wall:
        return 1 + wall_weight * 10
    if cell.is_iron_wall:
        return 1 + wall_weight * 15
    return INF

这一步把 Serpent 从“避障寻路”变成了“资源经济”。但它没有成为最强,因为它低估了变粗后的几何风险。

L4 Economic replay
seed0:600 分。用于展示“墙可通行”的策略转向。
L4-NoGrow

大奖励为什么会变成负债

score-rush / q=1 meta

从纯分数账看,大奖励是高收益资源。但从几何账看,大奖励可能是压力冲击。

如果 q 从 1 变 2: P_after - P_before = (N+1)×2² - N×1² 如果 q 从 2 变 3: P_after - P_before = (N+1)×3² - N×2²

所以 NoGrow-Strict 的目标是固定 q(t)=1。在这个约束下,几何压力退化成 P=N,寻路和生存难度大幅下降。

Implementation notes
def food_score(food, game):
    if food.type == "big_reward":
        return -INF   # NoGrow-Strict: treat bonus as forbidden
    return 10 - distance_penalty(food)

短期刷分中,保持 size=1 的灵活性大于大奖励的 +50。这不是因为大奖励数学上亏,而是因为短期 score-rush 没有足够强的 pressure recovery。

NoGrow-Strict replay
seed0:1500 分。正文核心证据之一。
L5

Space:吃完之后,剩多少空间

future feasible volume

L5 不再只问“我能不能吃到这个食物”,而是问:“吃完以后,我还剩多少可达空间?”

Space(snake') = FloodFillReachableCells(head(snake'), blocked(snake')) score(path) = reward - obstacle_cost - λ·path_length + μ·Space(after_eat)
Flood fill implementation
def reachable_area(game, start):
    queue = [start]
    visited = {start}
    while queue:
        cell = queue.pop()
        for nxt in neighbors(cell):
            if nxt not in visited and not blocked(nxt):
                visited.add(nxt)
                queue.append(nxt)
    return len(visited)

数学上,这一步接近“保持可控域”:不只是拿分,还要最大化未来可行状态空间。

L5 Space replay
seed0:1500 分。适合展示空间指标如何抬高上限。
L6–L7

TailGuard 与 Depth-2:score-rush 顶点

hard safety constraint + shallow planning

L6 TailGuard 把“能追到尾巴”从加分项变成硬约束:

maximize Space(after_eat) subject to TailReachable(after_eat) = true

这是从软偏好到安全约束的变化。L6 seed0 跑到 2190,是当前 score-rush 线的视觉高潮。

L6 pseudo-code
for food in visible_foods(game):
    path = astar(game.head, food.position, game)
    future = simulate_path(game, path)
    if can_reach_tail(future):
        candidates.append(score(food, path, future))
return best(candidates)

L7 Depth-2 再往前看一步:当前食物安全之后,下一颗可见食物是否仍然有机会继续吃。

V(s) ≈ R(s,a) + γ max R(s',a')

它不是完整动态规划,因为新食物随机补充,不能完全预知;但它补上了 L6 的一个盲点。

L6 TailGuard replay
seed0:2190 分。正文最重要回放之一。
L7 Depth-2 replay
seed0:1660 分。展示二层前瞻。
L11–L12

失败分支:TailStall 与 CycleCruise

negative evidence

失败策略必须保留。它们说明:局部安全不等于长期稳定,理论巡航也可能被 timeout 杀死。

L11 TailStall

假设是:如果 stall 时追尾巴,头跟着移动尾巴走,就能形成安全循环。

head follows moving tail → local safety

失败原因是:尾巴可达不是充分条件。追尾路径仍然可能把身体折叠成坏拓扑,最后 self collision。

L12 CycleCruise / Hamiltonian

空棋盘贪吃蛇里,Hamiltonian cycle 可以保证不撞自己。但 Serpent 有 30 秒 timeout、动态障碍、危险物、大奖励和随机食物。固定巡航的进食周期可能是:

T_food ≈ O(GRID²)

而游戏要求 T_food < 30s。所以它理论优雅,但不适合当前规则。

L11 TailStall replay
seed0:1010 分,最后 self。失败证据。
L12 Cycle replay
seed0:30 分,timeout。失败证据。
L8–L17

长期控制线:从长蛇控速到小长度稳态

endurance-control

长期控制线的起点是一个简单但强的等式:

吃 1 食物 → +10, +1 节 撞 1 脆墙 → -5, -1 节 净:+5 分, length 不变

所以理论上存在:

E[ΔN]=0 E[ΔS]>0

L8 尝试固定目标长度;L9 把严格稳态放宽成低正增长;L10/L13 做 hybrid;L14 以后出现真正的转折:不要在危险长蛇里求稳,而是让系统始终待在小长度安全集。

A = {N small} Keep X(t) ∈ A

L16/L17 进一步推翻了早期 NoGrow 的强假设:不需要永远 size=1。只要 pressure 小,size=3 也可以继续稳态。

A = {q ∈ {1,2,3}, P=Nq² ≤ P_max}
Long-control state machine
if timeout_soon:
    seek_safe_food_or_bonus()
elif pressure_high:
    trim_by_wall_or_hazard()
elif pressure_low:
    take_profitable_food()
else:
    safe_wander_or_follow_tail()
L16 AllSize replay
5000 tick 截断片段。展示小胖蛇稳态。
L17 AllSize A* replay
长跑策略片段。不是完整 106 分钟录像,而是稳态证据。

5. Hazard 纳入后的修正

危险物的效果是:

H = -20 分, -2 节

所以每节成本是:

20 / 2 = 10 分/节
修剪资源每节成本
脆墙5
随机障碍6.96
铁墙7.5
砖墙10
Hazard10

所以 hazard 不是优质修剪资源。但它有两个价值:一次砍 2 节,适合高压应急;如果附近没有墙,它是备用减压阀。

策略边界:hazard 不刷新进食计时,所以它不能替代 food。L18 里它应该是 emergency pressure relief,而不是常规循环资源。

6. L18:PressureControl-Profit 的理论设计

现在 L18 不该叫 MinLength-Profit,更准确应该叫:

L18-PressureControl-Profit

目标函数:

maximize E[dS/dt] subject to: P(t) ≤ P_max most of the time τ(t) < 30s death probability minimized

6.1 状态机

LOW_PRESSURE:
  可以吃普通食物
  如果大奖励收益很高且路径安全,也可以吃

MEDIUM_PRESSURE:
  优先吃普通食物
  避免大奖励,除非 timeout 压力很高

HIGH_PRESSURE:
  停止吃奖励
  优先找脆墙/铁墙修剪
  hazard 可作为应急修剪

TIMEOUT_PRESSURE:
  不管 pressure,先吃最近安全食物/大奖励续命

RECOVERY_AFTER_BONUS:
  如果 size 刚变大,立刻降低 pressure

6.2 每步评分函数

Value(action) = immediate_score - λ · P_after - μ · timeout_risk - ν · death_risk - ρ · path_cost + κ · future_reachability

其中 λ 应该随 pressure 增大:

λ = λ0 + λ1·max(0, P - P_target)

pressure 越高,越重视修剪;pressure 低时,允许多吃、多赚钱。

6.3 动作估值

吃普通食物

V(F) = +10 - λ·((N+1)q²)

吃大奖励

q<3: V(B) = +50 - λ·((N+1)(q+1)²) q=3: V(B) = +50 - λ·((N+1)q²)

撞墙

V(Wi) = -cost_i - path_cost + λ·pressure_reduction_i pressure_reduction_i ≈ min(loss_i, N-1)q²

吃 hazard

V(H) = -20 + λ·min(2,N-1)q² - timeout_penalty

hazard 只有在 λ 很大,也就是 pressure 很高时才值得吃。

7. 思路演进:九次问题重定义

阶段问题看法对应模型关键变化
1贪吃蛇 AI 问题minimize distance to foodGreedy / A*
2安全寻路问题Reachable(head', tail') = true吃完后仍要有出口
3经济问题Net = reward - cost墙是付费修剪
4NoGrow metaq=1 fixed短期刷分拒绝大奖励
5稳态控制问题E[ΔN]=0, E[ΔS]>0撞墙抵消进食增长
6低正增长准稳态0≤E[ΔN]≤ε不必完全抵消
7小长度安全集A={N small}不要在长蛇里求稳
8pressure 控制P=Nq²控制几何压力
9吸收大奖励q∈{1,2,3}, P≤Pmax不怕变胖,只怕 pressure 失控

这轮研究的真正价值在于:我没有停在“哪个 AI 分数高”,而是不断追问“这个游戏的长期动力系统是什么”。

8. 当前能说的结论,和不能说的强结论

8.1 弱命题:Serpent 存在长期正收益压力受控策略

在以下条件下:

  1. AI 能在 τ<30s 内找到普通食物或大奖励;
  2. AI 能以有限代价找到障碍物或 hazard 作为修剪资源;
  3. AI 使用 pressure 控制,使 P=Nq² 保持在某个上界附近;
  4. q≤3,即 size 有上限;
  5. 障碍物/危险物不会形成永久不可达死局。

则有模拟证据和正期望资源账支持存在策略,使得:

E[ΔS] > 0 E[ΔP] ≈ 0 或 E[ΔP] ≤ ε survival time very long in simulation

更直观地说:只要能持续把几何压力修回安全区,分数就可以长期正增长。

8.2 现在不能声称的强定理

不能严格声称:

所有随机种子都无限不死。

原因很硬:地图随机生成;食物/大奖励/危险物位置可能短期极差;AI 寻路不是完美动态规划;timeout 是硬约束;size=3 后可达性下降;真实 occupied cells 比 Nq² 更复杂;撞墙结算一次只处理一个障碍,规划估值仍需校准。

准确说法:我们已经有强模拟证据和正期望资源账,支持长期正收益压力受控策略存在。但还没有形式化证明“全随机环境几乎必然无限存活”。这个边界必须守住。

9. 录像引用校正

我重新整理了正文里的回放引用:不是把 22 个录像全部塞进正文,而是按证据功能筛选。

正文用途回放判断
baselineastar_seed0.jsonA* seed0 分数不高,但适合做基线。
墙经济economic_seed0.json用于展示墙可通行,不强调最高分。
NoGrow 阶段性顶点no_grow_strict_seed0.json1500,正文核心证据。
空间评分l5_space_seed0.json1500,适合正文。
score-rush 顶点l6_tail_guard_seed0.json2190,正文最重要回放。
Depth-2 前瞻l7_depth2_seed0.json1660,能展示策略逻辑。
失败分支l11_tail_stall_seed0.json / l12_cycle_seed0.json对应准确,适合说明失败原因。
pressure 稳态l16_min_length_all_size_seed0.json / l17_endurance_seed1.json作为稳态片段,不当成完整局。

其余录像保留在回放库作为附录。像 L8/L9/L10/L13 这类长跑或截断录像,不适合在正文里当“最终分数证据”,更适合作为策略库补充。