引言
还记得小时候在诺基亚上玩得不亦乐乎的贪吃蛇吗?那个简单却又魔性的游戏,一不小心就撞墙或者咬到自己
现在,我将用 10 分钟时间让你学会 AI 自学习玩贪吃蛇代码,而且越玩越6,是不是很帅

代码直接发,不搞花里胡哨的
"机器学习"和"强化学习"是啥?听起来好高级!
别怕,咱们用大白话聊聊。
- 人工智能 (AI):这个大家应该都听过,就是让机器模仿人的智能,能思考、能学习。比如 Siri、小爱同学,或者下棋的 AlphaGo。
- 机器学习 (Machine Learning, ML) :这是 AI 的一种实现方式。不是我们写死规则告诉机器"如果这样就这样",而是让机器自己从数据中"学习"规律。就像你看多了猫猫狗狗的照片,下次就能认出新的猫狗一样。
- 强化学习 (Reinforcement Learning, RL) :这是机器学习的一个分支,特别有意思。它模仿的是我们(或者小动物)学习新技能的方式------试错!
想象一下你在训练一只小狗学握手。
- 当它碰巧抬起爪子,你就给它一块零食(狠狠地奖励 Reward)。
- 如果它乱动或者咬你,你可能会说"不行"(狠狠地惩罚 Penalty,或者说负奖励)。
- 小狗不知道"握手"指令一开始是啥意思,它只是瞎尝试 (Action) 各种动作。
- 慢慢地,它发现在某个情况 (State) 下(比如你伸出手说"握手"),做出"抬爪子"这个动作 (Action) 能得到好吃的(奖励 Reward),它就会更倾向于做这个动作。
强化学习就是这个原理。
- 试试看:随便走一步,看看会怎么样。
- 得反馈:如果吃到食物,就很开心(得奖励);如果撞墙了,就很疼(受惩罚)。
- 记下来:把这次经验记在小本本上,下次遇到类似情况就知道该怎么办。
AI 在一个环境 (Environment) (比如贪吃蛇游戏)中,观察当前的状态 (State) (蛇头在哪?食物在哪?周围有没有危险?),尝试做出一个动作 (Action) (向上、向下、向左、或向右),然后环境会给它一个奖励 (Reward)(吃到食物 +10分!撞墙 -10分!离食物近了 +0.1分!)。
AI 的目标就是学会一套策略 (Policy) ,在不同的状态下选择能获得最大总奖励的动作。
我们今天的主角------Q-Learning------就是强化学习中的一种著名算法。它厉害的地方在于,它会学习一个叫做 "Q-table" 的东西,其实就是一个映射表罢了。
Q-Learning 和 "Q 表":AI 的小抄秘籍
Q-Learning 的核心思想是学习一个"动作价值函数 "(Action-Value Function),我们通常叫它 Q 函数。
它特别适合贪吃蛇这种游戏。Q-learning的核心就是那个Q表,里面存了每个状态下每个动作的"价值"。比如,蛇头前面有墙,往左转可能是0分(不好),往右转可能是10分(很好),AI就会选右转。
听起来是不是很牛逼?其实很简单
你可以把 Q 函数想象成一张巨大的备忘录 或者小抄 ,我们叫它 Q 表 (Q-Table)。
- 这张表的行 代表了所有可能遇到的状态 (State)。在贪吃蛇里,一个状态可能就是:"蛇头上方是墙,右边是身体,食物在左下方,当前蛇向右走"。(当然,为了让电脑理解,我们会把它数字化,后面代码里会看到)。
- 这张表的列 代表了所有可以采取的动作 (Action):上、下、左、右。
- 表里的每个格子 Q(s, a) 里的数值 ,就代表在状态
s
下,采取动作a
,预计未来能获得的总奖励有多好。这个数值越高,说明这个动作在这个状态下越"划算"。
(一个极其简化的 Q 表示意图)
状态 (State) | 向上 (Q值) | 向下 (Q值) | 向左 (Q值) | 向右 (Q值) |
---|---|---|---|---|
"前面是墙,食物在右边" | -100 | 5 | 3 | 15 |
"前面没路,食物在上面" | 20 | -5 | -100 | -100 |
... | ... | ... | ... | ... |
Q-learning有个公式,用来更新Q表:
- Q(s,a) = Q(s,a) + α [r + γ max(Q(s',a')) - Q(s,a)]
- 新 Q 值 = 旧 Q 值 + 学习率 * ( 当前奖励 + 折扣因子 * (下一个状态最好的 Q 值) - 旧 Q 值 )
- 学习率 (Learning Rate, α):表示 AI 对新学到的知识有多"信任"。值越大,越容易被新经验改变;值越小,越"固执",更相信过去的经验。
- 折扣因子 (Discount Factor, γ):表示 AI 对"未来奖励"的重视程度。值越接近 1,说明 AI 越有"远见",会为了未来的大奖励而放弃眼前的小奖励;值越接近 0,说明 AI 越"短视",只关心眼前的奖励。
别怕这个公式,我来解释得简单点:
- 现在状态:比如蛇头在哪儿,食物在哪儿。
- 动作:往上走、往下走之类。
- 奖励:吃到食物得 +10,撞墙得 -10。
- 未来最大价值:AI想象下一步能拿到的最好成绩。
- 学习速度:控制AI学得多快,像调音量一样。
一开始 Q 表是空的(或者随机值),AI 像个无头苍蝇一样乱撞。 但每次行动后,它都会根据得到的奖励和看到的新状态,用上面那个类似的思想去更新 Q 表里对应格子的数值。 玩的游戏次数越多(我们叫 episodes 或 训练轮数),Q 表就记录得越准确,AI 就知道在什么情况下该做什么动作了。
贪吃蛇AI的"眼睛"和"脑子"
状态:AI的"眼睛"
AI要玩游戏,得先看清周围的情况。我在代码里设计了getState
方法,让AI知道:
- 危险在哪儿:蛇头往上、右、下、左走会不会撞墙或自己。
- 食物在哪儿:食物在蛇头的上、右、下、左哪个方向。
- 现在朝哪儿走:蛇当前的方向是上、右、下、左。
这些信息组合起来就是一个"状态",有点像AI的"眼睛",帮它看清世界。
简化逻辑如下
ts
export type Dir = 'up' | 'down' | 'left' | 'right'
/**
* 获取当前游戏状态,用于 AI 决策
* @returns 游戏状态对象
*/
getState(): {
dangers: { [K in Dir]: boolean }
foodDirection: { [K in Dir]: boolean }
currentDirection: { [K in Dir]: boolean }
} {
const head = this.snake[0]
return {
/** 蛇头在四个方向是否会撞到墙壁或自身 */
dangers: {
up: this.isDanger(head.x, head.y - 1),
right: this.isDanger(head.x + 1, head.y),
down: this.isDanger(head.x, head.y + 1),
left: this.isDanger(head.x - 1, head.y),
},
/** 食物相对于蛇头的位置 */
foodDirection: {
up: this.food.y < head.y,
right: this.food.x > head.x,
down: this.food.y > head.y,
left: this.food.x < head.x,
},
/** 蛇当前的移动方向 */
currentDirection: {
up: this.direction === 'up',
right: this.direction === 'right',
down: this.direction === 'down',
left: this.direction === 'left',
},
}
}
/**
* 检查指定位置是否危险
* @param x x坐标
* @param y y坐标
* @returns 是否危险
*/
private isDanger(x: number, y: number): boolean {
return (
x < 0 ||
x >= this.config.gridSize ||
y < 0 ||
y >= this.config.gridSize ||
this.isOnSnake({ x, y })
)
}
动作和奖励:AI的"脑子"
AI能做的动作很简单,就是选一个方向走。我在getAction
方法里让它有时随机试试(探索),有时挑Q表里最好的(利用)。奖励是这样的:
- 吃到食物:+10分,太棒了!
- 撞墙或自己:-10分,太惨了!
- 每走一步:离食物近了+0.1,远了-0.1,鼓励它快点找吃的。
简化逻辑如下
ts
/**
* 获取 AI 决策动作
* @param state 当前游戏状态
* @returns 动作方向
*/
getAction(state: ReturnType<Snake['getState']>): Dir {
const stateKey = this.getStateKey(state)
/** 探索:随机选择动作 */
if (Math.random() < this.config.explorationRate) {
const actions: (Dir)[] = ['up', 'right', 'down', 'left']
return actions[Math.floor(Math.random() * actions.length)]
}
/** 利用:选择Q值最高的动作 */
if (!this.qTable[stateKey]) {
this.qTable[stateKey] = { up: 0, right: 0, down: 0, left: 0 }
}
const qValues = this.qTable[stateKey]
let bestAction: Dir = 'right'
let bestValue = -Infinity
/** 选择期望值最高的动作 */
for (const action in qValues) {
const a = action as keyof typeof qValues
if (qValues[a] > bestValue) {
bestValue = qValues[a]
bestAction = action as Dir
}
}
return bestAction
}
记录环境:让 AI 学会根据环境判断
-
一开始没有 qTable,AI 会随机一个方向
-
根据这个方向对于目标的影响,计算是奖励还是惩罚
-
updateQValue:记录当前环境下,各个动作(上下左右)的最佳决策(计算奖励)
-
比如当前环境信息如下(这时最佳方案是向左):
json{ "dangerUp": false, "dangerRight": false, "dangerDown": false, "dangerLeft": false, "foodUp": false, "foodRight": false, "foodDown": false, "foodLeft": true, "dirUp": false, "dirRight": false, "dirDown": false, "dirLeft": true }
同时计算每个方向的奖励值
json{ "up": 7.811, "right": 12.154, "down": 7.376, "left": 12.981 }
随着环境信息与奖励值的不断迭代,AI 就能学会如何赢取高分
为了减少内存占用,我用 01 来记录状态

ts
/**
* 更新 Q 值
* ### Q(s,a) = Q(s,a) + α [r + γ max(Q(s',a')) - Q(s,a)]
* ### 新 Q 值 = 旧 Q 值 + 学习率 * ( 当前奖励 + 折扣因子 * (下一个状态最好的 Q 值) - 旧 Q 值 )
*
* @param state 当前状态
* @param action 执行的动作
* @param reward 获得的奖励
* @param nextState 下一状态
*/
updateQValue(
state: ReturnType<Snake['getState']>,
action: Dir,
reward: number,
nextState: ReturnType<Snake['getState']>,
): void {
const stateKey = this.getStateKey(state)
const nextStateKey = this.getStateKey(nextState)
if (!this.qTable[stateKey]) {
this.qTable[stateKey] = { up: 0, right: 0, down: 0, left: 0 }
}
if (!this.qTable[nextStateKey]) {
this.qTable[nextStateKey] = { up: 0, right: 0, down: 0, left: 0 }
}
const maxNextQ = Math.max(...Object.values(this.qTable[nextStateKey]))
/**
* 键为当前的环境情况,如:
* ```json
* {
* "dangerUp": false,
* "dangerRight": false,
* "dangerDown": false,
* "dangerLeft": false,
* "foodUp": false,
* "foodRight": false,
* "foodDown": false,
* "foodLeft": true,
* "dirUp": false,
* "dirRight": false,
* "dirDown": false,
* "dirLeft": true
* }
* ```
*
* 值为当前环境情况下,四个方向的 Q 值,如:
* ```json
* {
* "up": 7.811,
* "right": 12.154,
* "down": 7.376,
* "left": 12.981
* }
* ```
*
* 通过不断更新某个环境下的 Q 值,可以使 AI 学会更好的决策
*/
this.qTable[stateKey][action] += this.config.learningRate * (
reward + this.config.discountFactor *
maxNextQ - this.qTable[stateKey][action]
)
}
/**
* 计算奖励值
* @param snake 蛇实例
* @param prevScore 前一次得分
* @param didMove 是否成功移动 (没撞墙/自己)
* @returns 奖励值
*/
calculateReward(snake: Snake, prevScore: number, didMove: boolean): number {
/** 游戏结束,狠狠地惩罚 */
if (!didMove)
return -10
/** 吃到食物,狠狠地奖励 */
if (snake.score > prevScore)
return 10
/** 计算蛇头与食物的距离 */
const head = snake.snake[0]
const food = snake.food
const distance = Math.abs(head.x - food.x) + Math.abs(head.y - food.y)
const prevDistance = this.prevDistance
/** 如果蛇头距离食物更近,返回微小正奖励(+0.1) */
if (prevDistance && distance < prevDistance) {
this.prevDistance = distance
return 0.1
}
/** 如果距离变远,则返回微小负奖励(-0.1) */
this.prevDistance = distance
return -0.1
}
/**
* 训练 AI
* @param snake 蛇实例
* @param episodes 训练轮数
**/
async train(snake: Snake, episodes: number, cb?: VoidFunction): Promise<void> {
console.log(`开始训练 ${episodes} 次...`)
const initialExplorationRate = this.config.explorationRate
for (let i = 0; i < episodes; i++) {
snake.reset()
/** 重置距离比较基准 */
this.prevDistance = 0
/** 记录当前局的步数 */
let step = 0
while (!snake.gameOver) {
const state = snake.getState()
const action = this.getAction(state)
const prevScore = snake.score
snake.changeDirection(action)
const didMove = snake.move()
const reward = this.calculateReward(snake, prevScore, didMove)
const nextState = snake.getState()
this.updateQValue(state, action, reward, nextState)
snake.draw()
cb?.()
step++
/** 可以加一个最大步数限制,防止蛇陷入死循环或无意义的绕圈 */
if (step > snake.config.gridSize * snake.config.gridSize * 2) {
console.log(`Episode ${this.trainCount + 1} reached max steps, ending.`)
snake.gameOver = true // 强制结束本局
}
/** 每 10 局,每 100 步延时一次,避免完全卡死 */
if (i % 10 === 0 && step % 100 === 0) {
await new Promise(resolve => setTimeout(resolve, 10))
}
}
// --- 每局结束后的处理 ---
this.trainCount++
this.totalScore += snake.score
/** 每隔一定局数保存一次,避免频繁 IO 操作,也减少数据丢失风险 */
if (i % 10 === 0 || i === episodes - 1) {
this.saveQTable()
this.saveTrainCount()
this.saveTotalScore()
}
/** 随着训练进行,减少探索率 */
this.config.explorationRate = Math.max(
this.config.minExplorationRate,
initialExplorationRate * this.config.explorationDecay ** i,
)
}
}
总结流程
markdown
开始训练
│
▼
重置游戏(snake.reset())
│
▼
获取当前状态 state = snake.getState()
│
▼
判断是否探索:如果随机值 < 探索率,则随机选择动作,否则选择 Q 值最高的动作
│
▼
执行动作:snake.changeDirection(action) -> snake.move()
│
▼
判断是否游戏结束
├── 是 → 游戏结束,给予负奖励,更新 Q 表,结束当前回合
└── 否 → 计算奖励 reward(根据是否吃到食物、是否接近食物等)
│
▼
获取下一状态 nextState = snake.getState()
│
▼
更新 Q 值:Q(s, a) = Q(s, a) + α (reward + γ max(Q(nextState)) - Q(s, a))
│
▼
绘制游戏(snake.draw())
│
▼
继续循环
我的代码分两部分:Snake
类管游戏规则,QLearnSnakeAI
类管AI学习。
Snake
类:游戏的地盘
Snake
类负责画蛇、移动蛇、生成食物。比如:
move()
:让蛇走一步,吃到食物就变长,撞墙就游戏结束。draw()
:把蛇和食物画在屏幕上,游戏结束还显示得分。
QLearnSnakeAI
类:AI的大脑
QLearnSnakeAI
类是AI的核心,里面有几个关键方法:
getAction
:根据状态选动作,像AI的"决策中心"。updateQValue
:用Q-learning公式更新Q表,像AI的"记笔记"。calculateReward
:算奖励,像AI的"老师"。train
:让AI玩很多轮游戏,每次都学一点。
训练时,AI会玩好多次游戏(比如1000轮),每轮都记下经验,更新Q表。慢慢地,它就从"乱撞"变成"高手"了。