引言
2048是一款风靡全球的数字滑动合并游戏,玩家通过上下左右四个方向的操作,使相同数字的方块碰撞合并,最终目标是合成2048甚至更高的数字。游戏的难点在于每一步操作不仅影响当前局面,还会随机在空白格生成新方块(通常是2或4),使得未来的状态具有高度的不确定性。传统解法多依赖贪心启发式评估或Expectimax搜索,本文将探索一种生物启发式算法——蚁群优化(Ant Colony Optimization, ACO),通过模拟蚂蚁在棋盘上”释放信息素”的行为,引导AI在庞大的状态空间中找到高价值的合并路径。
算法核心思想
信息素建模
将2048棋盘视为一张”图”,每个棋盘状态是一个节点,每次合法操作(上/下/左/右)构成一条边。蚂蚁在图上移动时,会在走过的边上沉积信息素,信息素浓度反映该操作路径的历史收益。
信息素更新规则包含两个部分:
1. 正反馈:高得分的操作路径会积累更多信息素
2. 挥发机制:旧信息素以固定速率衰减,避免算法过早收敛到局部最优
启发式能见度
除信息素外,每只蚂蚁在选择下一步时还会参考启发式能见度,由以下因素加权构成:
– 合并奖励:本次操作产生的合并总得分
– 单调性惩罚:棋盘非单调程度的惩罚项(鼓励大数字集中在角落)
– 空格奖励:操作后剩余空白格的数量
– 平滑度:相邻格子数值差异的倒数(鼓励平滑过渡)
蚂蚁选择方向的概率由信息素浓度与启发式能见度的乘积决定:
$$P(direction) = \frac{\tau_{dir}^\alpha \cdot \eta_{dir}^\beta}{\sum_{d \in D} \tau_d^\alpha \cdot \eta_d^\beta}$$
其中 $\tau$ 为信息素,$\eta$ 为能见度,$\alpha$ 和 $\beta$ 为调节参数。
Java实现
项目结构
src/
├── game/
│ ├── Board.java // 2048棋盘核心逻辑
│ ├── Direction.java // 方向枚举
│ └── MoveResult.java // 移动结果封装
├── aco/
│ ├── Ant.java // 单只蚂蚁
│ ├── ACOEngine.java // 蚁群优化引擎
│ └── PheromoneMap.java // 信息素映射表
├── heuristic/
│ └── Evaluator.java // 启发式评估函数
└── Main.java
Direction.java & MoveResult.java
package game;
/**
* 2048游戏四个操作方向
*/
public enum Direction {
UP(0, -1), DOWN(0, 1), LEFT(-1, 0), RIGHT(1, 0);
public final int dx, dy;
Direction(int dx, int dy) {
this.dx = dx;
this.dy = dy;
}
}
/**
* 封装一次移动操作的结果
*/
public class MoveResult {
public final int[][] board; // 移动后的棋盘状态
public final int scoreGain; // 本次合并获得的总分数
public final boolean moved; // 是否发生了实际移动
public final int emptyCells; // 移动后的空格数量
public final Direction direction; // 操作方向
public MoveResult(int[][] board, int scoreGain, boolean moved,
int emptyCells, Direction direction) {
this.board = board;
this.scoreGain = scoreGain;
this.moved = moved;
this.emptyCells = emptyCells;
this.direction = direction;
}
}
Board.java — 2048棋盘核心逻辑
package game;
import java.util.*;
/**
* 2048游戏棋盘,支持滑动合并、随机生成和局面评估
*/
public class Board {
public static final int SIZE = 4;
private int[][] grid;
private Random rand;
public Board(Random rand) {
this.rand = rand;
this.grid = new int[SIZE][SIZE];
}
public Board(int[][] grid, Random rand) {
this.rand = rand;
this.grid = deepCopy(grid);
}
/**
* 深拷贝棋盘
*/
public int[][] deepCopy(int[][] src) {
int[][] dst = new int[src.length][src[0].length];
for (int i = 0; i < src.length; i++) {
System.arraycopy(src[i], 0, dst[i], 0, src[i].length);
}
return dst;
}
public int[][] getGrid() { return grid; }
public void setGrid(int[][] g) { this.grid = deepCopy(g); }
/**
* 执行指定方向的滑动合并操作
* 核心算法:按方向压缩非零元素,相邻相等则合并
*/
public MoveResult move(Direction dir) {
int[][] newGrid = deepCopy(grid);
int scoreGain = 0;
boolean moved = false;
if (dir == Direction.LEFT || dir == Direction.RIGHT) {
for (int r = 0; r < SIZE; r++) {
int[] row = new int[SIZE];
for (int c = 0; c < SIZE; c++) row[c] = newGrid[r][c];
int[] merged = dir == Direction.LEFT ? mergeLine(row) : mergeLineReverse(row);
for (int c = 0; c < SIZE; c++) {
if (newGrid[r][c] != merged[c]) moved = true;
newGrid[r][c] = merged[c];
}
scoreGain += extractScore(row, merged);
}
} else {
for (int c = 0; c < SIZE; c++) {
int[] col = new int[SIZE];
for (int r = 0; r < SIZE; r++) col[r] = newGrid[r][c];
int[] merged = dir == Direction.UP ? mergeLine(col) : mergeLineReverse(col);
for (int r = 0; r < SIZE; r++) {
if (newGrid[r][c] != merged[r]) moved = true;
newGrid[r][c] = merged[r];
}
scoreGain += extractScore(col, merged);
}
}
int empty = countEmpty(newGrid);
return new MoveResult(newGrid, scoreGain, moved, empty, dir);
}
/**
* 单行/单列向左合并的核心算法
* 步骤:1)压缩非零元素到左侧 2)从左向右相邻相等则合并
*/
private int[] mergeLine(int[] line) {
int[] result = new int[line.length];
int pos = 0;
for (int i = 0; i < line.length; i++) {
if (line[i] != 0) {
result[pos++] = line[i];
}
}
for (int i = 0; i < pos - 1; i++) {
if (result[i] == result[i + 1] && result[i] != 0) {
result[i] *= 2;
result[i + 1] = 0;
}
}
// 再次压缩
int[] finalResult = new int[line.length];
int fp = 0;
for (int v : result) {
if (v != 0) finalResult[fp++] = v;
}
return finalResult;
}
private int[] mergeLineReverse(int[] line) {
int[] reversed = new int[line.length];
for (int i = 0; i < line.length; i++) reversed[i] = line[line.length - 1 - i];
int[] merged = mergeLine(reversed);
int[] result = new int[line.length];
for (int i = 0; i < line.length; i++) result[i] = merged[line.length - 1 - i];
return result;
}
private int extractScore(int[] before, int[] after) {
int score = 0;
for (int i = 0; i < after.length; i++) {
if (after[i] != 0 && after[i] != before[i]) {
// 检测是否是合并产生的新值
for (int j = 0; j < before.length; j++) {
if (before[j] == after[i] / 2) {
score += after[i];
break;
}
}
}
}
return score;
}
public int countEmpty(int[][] g) {
int count = 0;
for (int[] row : g) {
for (int v : row) {
if (v == 0) count++;
}
}
return count;
}
/**
* 在随机空白格生成新方块(90%概率为2,10%概率为4)
*/
public void spawnRandom() {
List<int[]> empty = new ArrayList<>();
for (int r = 0; r < SIZE; r++) {
for (int c = 0; c < SIZE; c++) {
if (grid[r][c] == 0) empty.add(new int[]{r, c});
}
}
if (!empty.isEmpty()) {
int[] pos = empty.get(rand.nextInt(empty.size()));
grid[pos[0]][pos[1]] = rand.nextDouble() < 0.9 ? 2 : 4;
}
}
/**
* 获取所有合法移动方向
*/
public List<Direction> getValidMoves() {
List<Direction> valid = new ArrayList<>();
for (Direction d : Direction.values()) {
MoveResult mr = move(d);
if (mr.moved) valid.add(d);
// 恢复棋盘(这里简化处理,实际应保存恢复)
}
return valid;
}
/**
* 检查是否游戏结束(无合法移动)
*/
public boolean isGameOver() {
for (Direction d : Direction.values()) {
MoveResult mr = move(d);
if (mr.moved) {
// 恢复棋盘状态
return false;
}
}
return true;
}
public void printBoard() {
for (int[] row : grid) {
for (int v : row) {
System.out.printf("%5d", v);
}
System.out.println();
}
System.out.println();
}
}
Evaluator.java — 启发式评估函数
package heuristic;
import game.Direction;
import game.MoveResult;
/**
* 2048局面启发式评估器
* 综合评分 = 单调性权重 + 空格权重 + 平滑度权重 + 最大数位置权重
*/
public class Evaluator {
private static final double W_MONOTONICITY = 1.0;
private static final double W_EMPTY = 2.7;
private static final double W_SMOOTHNESS = 1.0;
private static final double W_MAX_CORNER = 1.5;
/**
* 计算启发式能见度 eta(值越大表示该操作越优)
*/
public double evaluateMove(MoveResult result) {
if (!result.moved) return 0.0;
int[][] board = result.board;
double score = 0.0;
// 1. 合并得分奖励(直接收益)
score += result.scoreGain * 0.5;
// 2. 空格数量奖励(未来操作空间)
score += result.emptyCells * W_EMPTY;
// 3. 单调性评估(大数字应集中在角落)
score += calculateMonotonicity(board) * W_MONOTONICITY;
// 4. 平滑度评估(相邻格子数值差异小)
score += calculateSmoothness(board) * W_SMOOTHNESS;
// 5. 最大数在角落奖励
score += maxInCornerBonus(board) * W_MAX_CORNER;
return Math.max(score, 0.01); // 避免除零
}
private double calculateMonotonicity(int[][] board) {
double monoScore = 0.0;
// 检查每行是否单调递减(从左到右)
for (int[] row : board) {
for (int i = 0; i < row.length - 1; i++) {
if (row[i] >= row[i + 1]) monoScore += 1.0;
else monoScore -= 1.5;
}
}
// 检查每列是否单调递减(从上到下)
for (int c = 0; c < board[0].length; c++) {
for (int r = 0; r < board.length - 1; r++) {
if (board[r][c] >= board[r + 1][c]) monoScore += 1.0;
else monoScore -= 1.5;
}
}
return Math.max(monoScore, 0);
}
private double calculateSmoothness(int[][] board) {
double smooth = 0.0;
for (int r = 0; r < board.length; r++) {
for (int c = 0; c < board[0].length; c++) {
if (c < board[0].length - 1 && board[r][c] > 0 && board[r][c + 1] > 0) {
smooth += 1.0 / (1.0 + Math.abs(board[r][c] - board[r][c + 1]));
}
if (r < board.length - 1 && board[r][c] > 0 && board[r + 1][c] > 0) {
smooth += 1.0 / (1.0 + Math.abs(board[r][c] - board[r + 1][c]));
}
}
}
return smooth;
}
private double maxInCornerBonus(int[][] board) {
int max = 0;
for (int[] row : board) {
for (int v : row) max = Math.max(max, v);
}
// 检查四个角落是否有最大值
int corners = board[0][0];
corners = Math.max(corners, board[0][board[0].length - 1]);
corners = Math.max(corners, board[board.length - 1][0]);
corners = Math.max(corners, board[board.length - 1][board[0].length - 1]);
return corners == max ? 10.0 : 0.0;
}
}
PheromoneMap.java — 信息素映射表
package aco;
import game.Direction;
import java.util.*;
/**
* 信息素映射表
* 使用"棋盘哈希 -> 方向 -> 信息素浓度"的三级结构
* 由于状态空间巨大,采用局部信息素(基于最近几步的历史)
*/
public class PheromoneMap {
private Map<String, Map<Direction, Double>> pheromones;
private final double evaporationRate; // 挥发率
private final double initialPheromone;
public PheromoneMap(double evaporationRate, double initialPheromone) {
this.pheromones = new HashMap<>();
this.evaporationRate = evaporationRate;
this.initialPheromone = initialPheromone;
}
/**
* 获取指定状态下某方向的信息素浓度
*/
public double getPheromone(String stateHash, Direction dir) {
Map<Direction, Double> map = pheromones.getOrDefault(stateHash, new HashMap<>());
return map.getOrDefault(dir, initialPheromone);
}
/**
* 更新信息素:增加新沉积,并全局挥发
*/
public void updatePheromone(String stateHash, Direction dir, double deposit) {
// 全局挥发
for (Map<Direction, Double> map : pheromones.values()) {
for (Direction d : map.keySet()) {
map.put(d, map.get(d) * (1.0 - evaporationRate));
}
}
// 当前路径增加信息素
Map<Direction, Double> map = pheromones.computeIfAbsent(stateHash, k -> new HashMap<>());
double current = map.getOrDefault(dir, initialPheromone);
map.put(dir, current + deposit);
}
/**
* 计算选择概率(信息素^alpha * 能见度^beta)
*/
public Map<Direction, Double> calculateProbabilities(String stateHash,
Map<Direction, Double> visibilities,
double alpha, double beta) {
Map<Direction, Double> probs = new HashMap<>();
double sum = 0.0;
for (Direction dir : visibilities.keySet()) {
double tau = getPheromone(stateHash, dir);
double eta = visibilities.get(dir);
double value = Math.pow(tau, alpha) * Math.pow(eta, beta);
probs.put(dir, value);
sum += value;
}
// 归一化
if (sum > 0) {
for (Direction dir : probs.keySet()) {
probs.put(dir, probs.get(dir) / sum);
}
}
return probs;
}
/**
* 生成棋盘状态的紧凑哈希表示
*/
public static String hashBoard(int[][] board) {
StringBuilder sb = new StringBuilder();
for (int[] row : board) {
for (int v : row) {
sb.append(v).append(',');
}
}
return sb.toString();
}
}
Ant.java — 单只蚂蚁模拟
package aco;
import game.*;
import heuristic.Evaluator;
import java.util.*;
/**
* 单只蚂蚁的2048游戏模拟
* 蚂蚁根据信息素和启发式能见度选择操作方向,直到游戏结束
*/
public class Ant {
private final Random rand;
private final Evaluator evaluator;
private final PheromoneMap pheromoneMap;
private final double alpha;
private final double beta;
private List<MoveRecord> path; // 记录走过的路径
private int finalScore;
private int maxTile;
public Ant(Random rand, Evaluator evaluator, PheromoneMap pheromoneMap,
double alpha, double beta) {
this.rand = rand;
this.evaluator = evaluator;
this.pheromoneMap = pheromoneMap;
this.alpha = alpha;
this.beta = beta;
this.path = new ArrayList<>();
}
/**
* 蚂蚁从初始棋盘开始模拟一局游戏
* @param initialBoard 初始棋盘状态
* @return 本局最终得分
*/
public int simulate(Board initialBoard) {
Board board = new Board(initialBoard.getGrid(), rand);
path.clear();
int totalScore = 0;
maxTile = 0;
// 如果初始棋盘为空,生成两个初始方块
if (board.countEmpty(board.getGrid()) == 16) {
board.spawnRandom();
board.spawnRandom();
}
while (true) {
// 计算每个合法移动的信息素和能见度
Map<Direction, Double> visibilities = new HashMap<>();
Map<Direction, MoveResult> moveResults = new HashMap<>();
for (Direction dir : Direction.values()) {
Board testBoard = new Board(board.getGrid(), rand);
MoveResult mr = testBoard.move(dir);
if (mr.moved) {
double eta = evaluator.evaluateMove(mr);
visibilities.put(dir, eta);
moveResults.put(dir, mr);
}
}
if (visibilities.isEmpty()) break; // 无合法移动,游戏结束
// 根据信息素概率选择方向
String stateHash = PheromoneMap.hashBoard(board.getGrid());
Map<Direction, Double> probs = pheromoneMap.calculateProbabilities(
stateHash, visibilities, alpha, beta);
Direction chosen = selectByProbability(probs);
// 执行移动
MoveResult result = board.move(chosen);
totalScore += result.scoreGain;
board.setGrid(result.board);
board.spawnRandom();
// 记录路径
path.add(new MoveRecord(stateHash, chosen, result.scoreGain));
// 更新最大方块
for (int[] row : board.getGrid()) {
for (int v : row) maxTile = Math.max(maxTile, v);
}
}
finalScore = totalScore;
return finalScore;
}
/**
* 根据概率轮盘选择方向
*/
private Direction selectByProbability(Map<Direction, Double> probs) {
double r = rand.nextDouble();
double cumulative = 0.0;
for (Map.Entry<Direction, Double> entry : probs.entrySet()) {
cumulative += entry.getValue();
if (r <= cumulative) return entry.getKey();
}
return probs.keySet().iterator().next();
}
/**
* 蚂蚁返回路径信息,用于全局信息素更新
*/
public List<MoveRecord> getPath() { return path; }
public int getFinalScore() { return finalScore; }
public int getMaxTile() { return maxTile; }
public static class MoveRecord {
public final String stateHash;
public final Direction direction;
public final int scoreGain;
public MoveRecord(String stateHash, Direction direction, int scoreGain) {
this.stateHash = stateHash;
this.direction = direction;
this.scoreGain = scoreGain;
}
}
}
ACOEngine.java — 蚁群优化引擎
package aco;
import game.Board;
import heuristic.Evaluator;
import java.util.*;
/**
* 蚁群优化引擎
* 协调多只蚂蚁的模拟、信息素更新和最优解追踪
*/
public class ACOEngine {
private final int numAnts; // 蚂蚁数量
private final int maxIterations; // 最大迭代次数
private final double alpha; // 信息素权重
private final double beta; // 启发式权重
private final double evaporation; // 信息素挥发率
private final double depositRate; // 信息素沉积系数
private final PheromoneMap pheromoneMap;
private final Evaluator evaluator;
private final Random rand;
private int bestScore;
private int bestMaxTile;
private List<Ant.MoveRecord> bestPath;
public ACOEngine(int numAnts, int maxIterations, double alpha, double beta,
double evaporation, double depositRate, Random rand) {
this.numAnts = numAnts;
this.maxIterations = maxIterations;
this.alpha = alpha;
this.beta = beta;
this.evaporation = evaporation;
this.depositRate = depositRate;
this.rand = rand;
this.pheromoneMap = new PheromoneMap(evaporation, 1.0);
this.evaluator = new Evaluator();
this.bestScore = 0;
this.bestMaxTile = 0;
}
/**
* 运行蚁群优化,返回最优策略的推荐方向
*/
public Direction optimize(Board currentBoard) {
System.out.println("=== 启动蚁群优化 ===");
for (int iter = 0; iter < maxIterations; iter++) {
List<Ant> ants = new ArrayList<>();
int iterBestScore = 0;
Ant iterBestAnt = null;
// 每只蚂蚁模拟一局游戏
for (int i = 0; i < numAnts; i++) {
Ant ant = new Ant(rand, evaluator, pheromoneMap, alpha, beta);
int score = ant.simulate(currentBoard);
ants.add(ant);
if (score > iterBestScore) {
iterBestScore = score;
iterBestAnt = ant;
}
}
// 更新全局最优
if (iterBestScore > bestScore) {
bestScore = iterBestScore;
bestPath = new ArrayList<>(iterBestAnt.getPath());
bestMaxTile = iterBestAnt.getMaxTile();
}
// 全局信息素更新(仅最优蚂蚁的路径)
if (iterBestAnt != null) {
for (Ant.MoveRecord record : iterBestAnt.getPath()) {
double deposit = depositRate * record.scoreGain;
pheromoneMap.updatePheromone(record.stateHash, record.direction, deposit);
}
}
if (iter % 10 == 0) {
System.out.printf(" 迭代%d: 本轮最优=%d, 全局最优=%d, 最大方块=%d%n",
iter, iterBestScore, bestScore, bestMaxTile);
}
}
// 返回当前状态下信息素最高的方向作为推荐
String currentHash = PheromoneMap.hashBoard(currentBoard.getGrid());
Direction bestDir = null;
double maxPheromone = -1;
for (Direction dir : game.Direction.values()) {
double p = pheromoneMap.getPheromone(currentHash, dir);
if (p > maxPheromone) {
maxPheromone = p;
bestDir = dir;
}
}
System.out.printf("=== 优化完成,全局最优得分=%d,最大方块=%d ===%n", bestScore, bestMaxTile);
return bestDir;
}
public int getBestScore() { return bestScore; }
public int getBestMaxTile() { return bestMaxTile; }
}
Main.java — 运行演示
import game.*;
import aco.*;
import java.util.*;
public class Main {
public static void main(String[] args) {
Random rand = new Random(42);
// 创建初始棋盘
Board board = new Board(rand);
board.spawnRandom();
board.spawnRandom();
System.out.println("初始棋盘:");
board.printBoard();
// 配置蚁群优化参数
ACOEngine engine = new ACOEngine(
20, // 蚂蚁数量
50, // 迭代次数
1.0, // alpha: 信息素权重
2.0, // beta: 启发式权重
0.1, // 挥发率
0.5, // 沉积系数
rand
);
// 获取当前最优操作方向
Direction bestMove = engine.optimize(board);
System.out.println("推荐操作方向: " + bestMove);
// 执行推荐操作并展示结果
MoveResult result = board.move(bestMove);
board.setGrid(result.board);
System.out.println("操作后棋盘(得分+" + result.scoreGain + "):");
board.printBoard();
}
}
算法复杂度分析
| 模块 | 时间复杂度 | 说明 |
|---|---|---|
| 单次移动合并 | O(n²) | n为棋盘边长(2048中固定为4,实际为O(1)) |
| 启发式评估 | O(n²) | 遍历棋盘计算单调性、平滑度等 |
| 单只蚂蚁模拟 | O(steps × n²) | steps为一局游戏的平均步数 |
| 完整ACO迭代 | O(ants × iterations × steps × n²) | 蚂蚁数×迭代数×步数 |
| 信息素更新 | O(paths) | 与蚂蚁走过的路径长度成正比 |
总结
本文将蚁群优化算法应用于2048游戏,通过信息素正反馈机制和启发式能见度引导,使AI能够在有限计算预算内探索高价值的操作序列。ACO的核心优势在于:
- 群体智能:多只蚂蚁并行探索不同策略,避免单一路径的局部最优
- 自适应学习:信息素沉积机制让算法自动聚焦于高收益操作模式
- 可扩展性:通过调整蚂蚁数量和迭代次数,可灵活平衡计算开销与决策质量
该方法展示了生物启发式算法在组合优化问题中的独特价值,特别是在状态空间巨大、存在随机因素的游戏场景中,ACO提供了一种与传统Minimax/Expectimax截然不同的求解思路。