
简介这份资源是一套用 C/C 编写的贪吃蛇游戏人工智能实现面向对游戏 AI、路径搜索与强化学习感兴趣的开发者与学习者帮助理解如何让蛇在自动寻路中尽可能吃满整张地图。压缩包共 57 个文件以 34 个 Python 源码为主辅以 gif 演示、png 截图、yml 配置、md 文档及许可证等整体约 1.52MB结构上分为 solver、gui、base、util、tests、docs 等模块便于按功能阅读与调试。项目综合运用最短路径、最长路径与人工智能算法核心目标是在保证生存的前提下最大化食物获取直至填满地图。已有 1635 人学习下载说明其在同类小项目中具备一定参考价值。读者可从中获得完整的算法实现思路、模块化目录组织方式、可视化运行界面以及测试与文档支持适合作为课程设计、算法练习或 AI 入门项目的参考素材。1. 贪吃蛇 AI 到底在做什么从“蛇撞墙”到“蛇自己找路”很多人第一次听到“贪吃蛇游戏的人工智能”脑子里浮现的是训练一个神经网络让蛇自己学会吃食物。但真动手做过一轮就会发现这件事的核心根本不是“让蛇变聪明”而是把游戏状态变成可计算的输入把移动方向变成可优化的输出。蛇每走一步你都要回答三个问题现在蛇头在哪、食物在哪、哪些方向会立刻撞死。这三个问题答清楚了剩下的才是选算法。我见过不少新手一上来就搭深度强化学习结果蛇在 20×20 的格子上连 10 分都拿不到。原因很简单状态表示没做对奖励函数没设计好蛇在“绕圈”和“追食物”之间反复横跳。贪吃蛇 AI 适合两类人一类是想学强化学习但找不到轻量级练手项目的人另一类是已经会写游戏逻辑、想看看 AI 怎么接管决策的人。它不需要 GPU 集群一台普通笔记本就能跑但里面的坑一个不少。这一篇不讲空泛的“AI 原理”而是按我实际做过的路径从状态编码、算法选型、训练循环到避坑排查把一条能复现的路子讲清楚。你跟着走至少能让蛇稳定吃到 30 分以上你想再往上推后面也有调参和进阶技巧。2. 状态怎么编码蛇才能“看见”世界2.1 用 11 维特征还是网格图像两种输入方案的取舍贪吃蛇 AI 的第一道分水岭是状态表示。常见做法有两类一类是手工提取低维特征另一类是把整个棋盘当成图像喂给卷积网络。我一般会先用手工特征跑通 baseline因为它的调试成本低得多。手工特征最经典的是 11 维向量蛇头四个方向是否危险、当前移动方向、食物相对蛇头的四个方位。这个表示在 20×20 以内的棋盘上足够用训练几百轮就能看到蛇开始追食物。缺点是蛇变长以后它“看不到”自己身体中段的分布容易把自己围死。网格图像方案是把棋盘转成 H×W×C 的张量C 可以是 3 通道蛇头、蛇身、食物或 4 通道再加墙。这种表示保留了全部空间信息但网络参数量大训练慢早期容易陷入“原地转圈”的局部最优。我的建议是先用 11 维特征把流程跑通再换网格图像做对比实验。import numpy as np def get_11d_state(snake, food, direction, board_size): snake: list of (x, y), snake[0] 是蛇头 food: (x, y) direction: 当前移动方向, 0上,1右,2下,3左 board_size: (w, h) head snake[0] # 四个方向是否立即危险 danger [] moves [(-1,0),(0,1),(1,0),(0,-1)] # 上右下左 for dx, dy in moves: nx, ny head[0]dx, head[1]dy if nx 0 or ny 0 or nx board_size[0] or ny board_size[1]: danger.append(1) elif (nx, ny) in snake[1:]: danger.append(1) else: danger.append(0) # 当前方向 one-hot dir_onehot [0,0,0,0] dir_onehot[direction] 1 # 食物相对方位 food_rel [0,0,0,0] if food[0] head[0]: food_rel[0] 1 elif food[0] head[0]: food_rel[2] 1 if food[1] head[1]: food_rel[3] 1 elif food[1] head[1]: food_rel[1] 1 return np.array(danger dir_onehot food_rel, dtypenp.float32)这段代码里danger的顺序必须和动作空间顺序严格一致否则蛇学到的策略会“左右颠倒”。food_rel用的是相对方位而不是绝对坐标这样蛇在棋盘任何位置都能泛化。board_size传(宽, 高)别传反传反了蛇会以为墙在错误的位置。2.2 动作空间与奖励函数为什么你的蛇总在绕圈动作空间通常设成 3 个直行、左转、右转。不要设成 4 个绝对方向因为蛇不能瞬间掉头绝对方向会让它产生“自杀式”动作。3 动作空间下蛇的转向是相对于当前方向的这样物理上更合理。奖励函数是第二个大坑。很多人只给“吃到食物 1撞死 -1”结果蛇学会的是“活着但不吃”因为绕圈不得分也不扣分。我一般会加一个小的步数惩罚比如每步 -0.01再给“靠近食物”一个微小正奖励。这样蛇有动力去追食物而不是原地转。def compute_reward(prev_dist, curr_dist, ate_food, dead): if dead: return -1.0 if ate_food: return 1.0 # 靠近食物给正奖励远离给负奖励 if curr_dist prev_dist: return 0.02 else: return -0.02注意这里的0.02不能太大否则蛇会为了“靠近”而反复横跳不真正吃食物。步数惩罚我一般放在环境 step 里不放在这个函数里方便单独调。2.3 棋盘尺寸与蛇的初始长度别一上来就 20×20新手最容易犯的错是直接上 20×20 棋盘、蛇长 3。这个配置下状态空间很大随机探索很难碰到食物。我建议从 8×8 或 10×10 开始蛇长 2食物随机放。等蛇能稳定吃 10 个食物再逐步扩大棋盘。扩大棋盘时不要只改尺寸还要同步调整网络隐藏层大小和训练轮数。8×8 下 128 隐藏单元够用20×20 下建议 256 起步。训练轮数也要从几千轮加到几万轮否则蛇还没探索到有效策略就停了。3. 选 DQN 还是 Q-table小棋盘和大棋盘的算法分界3.1 Q-table 在 10×10 以内还能打再大就爆内存Q-table 的思路是给每个状态-动作对存一个值。8×8 棋盘、蛇长 2 时状态数大约几千Q-table 完全放得下。我早期用 Q-table 跑 8×8蛇能稳定吃到 15 分左右。但棋盘一到 15×15状态数指数上升Q-table 内存直接爆掉而且大部分状态永远访问不到查表全是零。所以我的分界线是10×10 以内可以用 Q-table 做 baseline超过 10×10 直接上 DQN。DQN 用神经网络拟合 Q 值不需要存所有状态泛化能力也更强。import torch import torch.nn as nn class DQN(nn.Module): def __init__(self, state_dim11, action_dim3, hidden128): super().__init__() self.net nn.Sequential( nn.Linear(state_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, action_dim) ) def forward(self, x): return self.net(x)state_dim11对应前面 11 维特征action_dim3对应直行、左转、右转。隐藏层用 128 是 8×8 到 12×12 的通用值再大可以加到 256。激活函数用 ReLU 就够了别用 sigmoid容易梯度消失。3.2 经验回放与目标网络DQN 稳定的两个关键件DQN 直接训练会震荡因为前后两步的 Q 值互相影响。解决办法是经验回放池加目标网络。回放池存(state, action, reward, next_state, done)训练时随机采样一批打破时间相关性。目标网络每隔几百步同步一次参数让 Q 值目标稳定。from collections import deque import random class ReplayBuffer: def __init__(self, capacity10000): self.buffer deque(maxlencapacity) def push(self, state, action, reward, next_state, done): self.buffer.append((state, action, reward, next_state, done)) def sample(self, batch_size): batch random.sample(self.buffer, batch_size) states, actions, rewards, next_states, dones zip(*batch) return (np.array(states), np.array(actions), np.array(rewards), np.array(next_states), np.array(dones)) def __len__(self): return len(self.buffer)回放池容量我一般设 10000 到 50000。太小了样本不够太大了早期旧策略会拖后腿。目标网络同步步数设 200 到 500太小了不稳定太大了学得慢。3.3 训练循环从随机撞墙到稳定吃食物要多少轮训练循环的核心是用 epsilon-greedy 选动作存经验采样训练定期同步目标网络。epsilon 从 1.0 线性降到 0.1让蛇先探索再利用。def train_step(online_net, target_net, optimizer, buffer, batch_size64, gamma0.99): if len(buffer) batch_size: return states, actions, rewards, next_states, dones buffer.sample(batch_size) states torch.FloatTensor(states) next_states torch.FloatTensor(next_states) actions torch.LongTensor(actions) rewards torch.FloatTensor(rewards) dones torch.FloatTensor(dones) q_values online_net(states).gather(1, actions.unsqueeze(1)).squeeze(1) with torch.no_grad(): next_q target_net(next_states).max(1)[0] target rewards gamma * next_q * (1 - dones) loss nn.MSELoss()(q_values, target) optimizer.zero_grad() loss.backward() optimizer.step()gamma0.99表示蛇看重长期回报适合贪吃蛇这种需要规划路径的场景。batch_size64是通用起点显存不够可以降到 32。8×8 棋盘下我一般跑 3000 到 5000 轮能看到蛇稳定吃食物20×20 需要 20000 轮以上。4. 避坑排查蛇不吃饭、原地转、越训越傻的 5 个原因4.1 蛇一直绕圈不吃食物现象训练几千轮后蛇能活很久但分数停在 0 到 2 分。原因奖励函数里“活着”的隐性收益大于“吃食物”或者步数惩罚太小。解决把每步惩罚从 -0.01 调到 -0.05同时把吃食物奖励从 1.0 提到 2.0。如果还不行检查食物相对方位特征是不是写反了写反了蛇会往反方向追。4.2 蛇刚开局就撞墙现象每局不到 5 步就死epsilon 降到 0.1 后依然如此。原因危险检测特征里蛇头四个方向的顺序和动作空间顺序不一致。解决打印danger数组和实际移动结果逐方向核对。我一般会在环境里加一个断言如果蛇撞墙但danger对应位是 0直接报错。4.3 训练 loss 震荡不下降现象loss 在 0.5 到 2.0 之间反复跳Q 值越来越大。原因目标网络同步太慢或者 gamma 太接近 1。解决把目标网络同步步数从 1000 降到 200gamma 从 0.99 降到 0.95。如果还震荡检查奖励有没有做归一化奖励范围太大也会导致 Q 值爆炸。4.4 蛇越训越傻分数倒退现象前 2000 轮分数上升之后突然掉到 0 并再也上不去。原因回放池里旧策略样本太多新策略被带偏或者学习率太大把已学到的策略冲掉了。解决把回放池容量从 50000 降到 10000学习率从 1e-3 降到 1e-4。我一般还会保存历史最优模型分数倒退时回滚。4.5 换了棋盘尺寸后完全学不会现象8×8 能跑换成 16×16 后蛇连食物都找不到。原因状态特征没有归一化棋盘变大后食物相对距离的数值范围变了。解决把食物相对方位改成归一化坐标或者把棋盘尺寸作为额外特征输入。更简单的做法是扩大隐藏层并增加训练轮数别指望小网络直接泛化到大棋盘。5. 进阶技巧用优先经验回放和 n-step 回报把分数再推一截5.1 优先经验回放让蛇多复习“差点撞死”的样本普通回放池均匀采样但贪吃蛇里“撞墙前一步”的样本价值远高于“空旷处直行”。优先经验回放按 TD 误差给样本加权误差大的样本被采样概率高。实现上可以用 SumTree 或者简单的概率数组。class PrioritizedBuffer: def __init__(self, capacity10000, alpha0.6): self.capacity capacity self.alpha alpha self.buffer [] self.priorities np.zeros(capacity, dtypenp.float32) self.pos 0 def push(self, transition, td_error): priority (abs(td_error) 1e-5) ** self.alpha if len(self.buffer) self.capacity: self.buffer.append(transition) else: self.buffer[self.pos] transition self.priorities[self.pos] priority self.pos (self.pos 1) % self.capacity def sample(self, batch_size, beta0.4): probs self.priorities[:len(self.buffer)] / self.priorities[:len(self.buffer)].sum() indices np.random.choice(len(self.buffer), batch_size, pprobs) samples [self.buffer[i] for i in indices] weights (len(self.buffer) * probs[indices]) ** (-beta) weights / weights.max() return samples, indices, weightsalpha0.6控制优先级强度0 就是均匀采样。beta0.4是重要性采样修正训练后期可以升到 1.0。这个改动一般能让蛇在 20×20 上多拿 20% 到 30% 的分数。5.2 n-step 回报让蛇提前看到三步后的奖励单步 DQN 只看下一步奖励蛇在长路径上学习慢。n-step 回报把连续 n 步奖励累加让蛇更快传播“吃到食物”的价值。实现上可以在回放池里存 n 步的累积奖励。def compute_nstep_target(rewards, next_q, gamma0.99, n3): rewards: 连续 n 步奖励列表 next_q: 第 n 步后的 Q 值 target 0 for i, r in enumerate(rewards): target (gamma ** i) * r target (gamma ** n) * next_q return targetn 一般取 3 到 5。太小了没效果太大了方差大。我一般先用 n3 跑一轮如果分数波动大就降到 2。5.3 验证方法固定随机种子跑 100 局看均值和方差训练完别只看一局分数那可能是运气。我一般固定随机种子跑 100 局看平均分和标准差。平均分高但标准差大说明策略不稳定需要继续调 epsilon 或奖励。平均分低但标准差小说明策略保守需要加大探索。def evaluate(env, agent, episodes100, seed42): scores [] for i in range(episodes): state env.reset(seedseedi) done False while not done: action agent.act(state, epsilon0.0) state, reward, done, info env.step(action) scores.append(info[score]) return np.mean(scores), np.std(scores)epsilon0.0表示评估时完全不探索只用学到的策略。seedi保证每局初始状态不同但可复现。我一般要求平均分达到棋盘面积的 1/5 才算及格比如 20×20 棋盘平均分 80 以上。最后说个我自己的习惯每次改奖励函数或网络结构一定先跑 500 轮看趋势别一上来就几万轮。贪吃蛇 AI 的坑大多不在算法本身而在状态编码和奖励设计的细节里。把 11 维特征写对、把危险检测顺序对齐、把步数惩罚调好蛇自然就学会找路了。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。