引言
贪吃蛇是一款经典的街机游戏,玩家控制蛇在网格中移动并吃掉食物。本文将使用Java实现一个基于蒙特卡洛树搜索(MCTS)的贪吃蛇AI,通过UCT(Upper Confidence Bound applied to Trees)公式在探索未知路径与利用已知优势之间取得平衡,让蛇在复杂局面下做出长远决策。
算法核心思想
蒙特卡洛树搜索四阶段
MCTS通过迭代执行以下四个阶段来构建搜索树:
- 选择(Selection):从根节点出发,使用UCT公式选择子节点,直到到达未完全展开的节点
- 扩展(Expansion):为选中的节点添加一个或多个子节点
- 模拟(Simulation):从新节点出发进行随机模拟( rollout )直到游戏结束
- 回溯(Backpropagation):将模拟结果反向传播,更新路径上所有节点的统计值
UCT公式
$$UCT = \frac{w_i}{n_i} + c \sqrt{\frac{\ln N}{n_i}}$$
其中 $w_i$ 是胜利次数,$n_i$ 是访问次数,$N$ 是父节点访问次数,$c$ 是探索常数。
Java实现
核心数据结构
import java.util.*;
public class MCTSSnakeAI {
// 方向定义:上、右、下、左
private static final int[][] DIRS = {{-1,0}, {0,1}, {1,0}, {0,-1}};
private static final int GRID_SIZE = 10;
private static final double EXPLORATION_CONSTANT = 1.414;
// 游戏状态
static class GameState {
Set<String> snakeBody = new LinkedHashSet<>(); // 保持顺序
int headRow, headCol;
int foodRow, foodCol;
int score;
boolean gameOver;
GameState() {
snakeBody.add("5,5");
headRow = 5; headCol = 5;
spawnFood();
score = 0;
}
GameState(GameState other) {
this.snakeBody = new LinkedHashSet<>(other.snakeBody);
this.headRow = other.headRow;
this.headCol = other.headCol;
this.foodRow = other.foodRow;
this.foodCol = other.foodCol;
this.score = other.score;
this.gameOver = other.gameOver;
}
void spawnFood() {
Random rand = new Random();
do {
foodRow = rand.nextInt(GRID_SIZE);
foodCol = rand.nextInt(GRID_SIZE);
} while (snakeBody.contains(foodRow + "," + foodCol));
}
boolean isValid(int r, int c) {
return r >= 0 && r < GRID_SIZE && c >= 0 && c < GRID_SIZE
&& !snakeBody.contains(r + "," + c);
}
// 执行动作,返回是否吃到食物
boolean step(int dirIdx) {
if (gameOver) return false;
int nr = headRow + DIRS[dirIdx][0];
int nc = headCol + DIRS[dirIdx][1];
if (!isValid(nr, nc)) {
gameOver = true;
return false;
}
String newPos = nr + "," + nc;
snakeBody.add(newPos);
headRow = nr; headCol = nc;
boolean ate = (nr == foodRow && nc == foodCol);
if (!ate) {
// 移除尾部
Iterator<String> it = snakeBody.iterator();
it.next();
it.remove();
} else {
score += 10;
spawnFood();
}
return ate;
}
List<Integer> getLegalActions() {
List<Integer> acts = new ArrayList<>();
for (int i = 0; i < 4; i++) {
int nr = headRow + DIRS[i][0];
int nc = headCol + DIRS[i][1];
if (nr >= 0 && nr < GRID_SIZE && nc >= 0 && nc < GRID_SIZE
&& !snakeBody.contains(nr + "," + nc)) {
acts.add(i);
}
}
return acts;
}
}
// MCTS节点
static class MCTSNode {
GameState state;
MCTSNode parent;
Map<Integer, MCTSNode> children = new HashMap<>();
int visits = 0;
double totalReward = 0;
int action; // 到达该节点的动作
List<Integer> untriedActions;
MCTSNode(GameState state, MCTSNode parent, int action) {
this.state = new GameState(state);
this.parent = parent;
this.action = action;
this.untriedActions = state.getLegalActions();
}
boolean isFullyExpanded() {
return untriedActions.isEmpty();
}
double uctValue() {
if (visits == 0) return Double.MAX_VALUE;
double exploitation = totalReward / visits;
double exploration = EXPLORATION_CONSTANT * Math.sqrt(Math.log(parent.visits) / visits);
return exploitation + exploration;
}
MCTSNode bestChild() {
MCTSNode best = null;
double bestVal = -Double.MAX_VALUE;
for (MCTSNode child : children.values()) {
double val = child.uctValue();
if (val > bestVal) {
bestVal = val;
best = child;
}
}
return best;
}
}
}
MCTS搜索主逻辑
/**
* 执行MCTS搜索,返回最优动作
*/
public int search(GameState rootState, int iterations) {
MCTSNode root = new MCTSNode(rootState, null, -1);
for (int i = 0; i < iterations; i++) {
// 阶段1&2:选择与扩展
MCTSNode node = treePolicy(root);
// 阶段3:模拟
double reward = defaultPolicy(node.state);
// 阶段4:回溯
backup(node, reward);
}
// 选择访问次数最多的动作(更稳健)
int bestAction = -1;
int maxVisits = -1;
for (Map.Entry<Integer, MCTSNode> e : root.children.entrySet()) {
if (e.getValue().visits > maxVisits) {
maxVisits = e.getValue().visits;
bestAction = e.getKey();
}
}
return bestAction;
}
/**
* 树策略:选择+扩展
*/
private MCTSNode treePolicy(MCTSNode node) {
while (!node.state.gameOver) {
if (!node.isFullyExpanded()) {
return expand(node);
} else if (node.children.isEmpty()) {
break;
} else {
node = node.bestChild();
}
}
return node;
}
/**
* 扩展节点:随机选择一个未尝试的动作
*/
private MCTSNode expand(MCTSNode node) {
int action = node.untriedActions.remove(
new Random().nextInt(node.untriedActions.size()));
GameState nextState = new GameState(node.state);
nextState.step(action);
MCTSNode child = new MCTSNode(nextState, node, action);
node.children.put(action, child);
return child;
}
/**
* 默认策略:随机模拟直到游戏结束
* 返回值归一化到 [0,1]
*/
private double defaultPolicy(GameState state) {
GameState sim = new GameState(state);
Random rand = new Random();
int steps = 0;
final int MAX_STEPS = 500;
while (!sim.gameOver && steps < MAX_STEPS) {
List<Integer> legal = sim.getLegalActions();
if (legal.isEmpty()) break;
// 优先向食物方向移动的贪婪随机策略
int bestDir = legal.get(rand.nextInt(legal.size()));
double bestDist = Double.MAX_VALUE;
for (int d : legal) {
int nr = sim.headRow + DIRS[d][0];
int nc = sim.headCol + DIRS[d][1];
double dist = Math.abs(nr - sim.foodRow) + Math.abs(nc - sim.foodCol);
if (dist < bestDist && rand.nextDouble() < 0.7) {
bestDist = dist;
bestDir = d;
}
}
sim.step(bestDir);
steps++;
}
// 奖励函数:得分 + 生存步数归一化 + 剩余空间奖励
double spaceReward = sim.snakeBody.size() / (double)(GRID_SIZE * GRID_SIZE);
return (sim.score * 0.1 + steps / 500.0 + spaceReward) / 3.0;
}
/**
* 回溯更新统计值
*/
private void backup(MCTSNode node, double reward) {
while (node != null) {
node.visits++;
node.totalReward += reward;
node = node.parent;
}
}
运行入口
public static void main(String[] args) {
MCTSSnakeAI ai = new MCTSSnakeAI();
GameState state = new GameState();
int totalMoves = 0;
while (!state.gameOver && totalMoves < 1000) {
int action = ai.search(state, 800); // 每次决策800次迭代
if (action < 0) break;
String[] dirNames = {"上", "右", "下", "左"};
System.out.printf("第%d步: 向%s移动 | 得分:%d | 长度:%d%n",
totalMoves + 1, dirNames[action], state.score, state.snakeBody.size());
state.step(action);
totalMoves++;
}
System.out.println("游戏结束! 最终得分: " + state.score + ", 总步数: " + totalMoves);
}
复杂度分析
| 阶段 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 选择 | $O(d)$,$d$为树深度 | $O(N)$,$N$为节点数 |
| 扩展 | $O(1)$ | $O(1)$每节点 |
| 模拟 | $O(k)$,$k$为模拟步数 | $O(1)$ |
| 总搜索 | $O(I \cdot (d + k))$,$I$为迭代次数 | $O(I)$ |
算法优化建议
- 渐进式拓宽(Progressive Widening):当节点访问次数超过阈值时,才允许扩展更多子节点
- RAVE(Rapid Action Value Estimation):利用兄弟节点的统计信息加速收敛
- 虚拟损失(Virtual Loss):并行化时防止多个线程同时探索同一路径
- 策略网络引导:用轻量级神经网络替代随机模拟,提升模拟质量
总结
本文通过Java实现了基于MCTS的贪吃蛇AI,核心亮点包括:完整的UCT搜索树构建、贪婪偏向的随机模拟策略、以及综合考虑得分与生存空间的奖励函数。MCTS无需显式评估函数,仅通过自我对弈即可学习策略,非常适合状态空间复杂、难以设计精确评估函数的游戏场景。