1. 简介
之前我们探讨过一篇关于 2048 游戏求解算法 的文章,但那篇更多是从理论角度分析,没有给出实际代码实现。
本文我们将用 Java 实现一个完整的 2048 求解器。这个求解器会同时模拟“人类”和“计算机”两个角色的走法,展示如何通过算法实现更优的游戏策略。最终目标是让程序自己玩出高分,而不是靠手速或运气。
✅ 重点:这不是一个图形界面游戏,而是一个基于控制台的 AI 求解框架。
⚠️ 踩坑提示:别一上来就想搞 GUI,先把核心逻辑跑通才是正道。
2. 初始框架搭建
要实现求解器,首先得有个能跑起来的游戏环境。我们需要构建游戏板、计算机玩家、人类玩家的基本结构,确保游戏能正常进行。
2.1. 游戏板(Board)设计
游戏的核心是 4x4 的网格。我们先定义一个 Cell
类来表示坐标位置:
public class Cell {
private final int x;
private final int y;
public Cell(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
@Override
public String toString() {
return "(" + x + "," + y + ")";
}
}
接着是 Board
类,用于表示当前游戏状态。它是一个不可变对象(immutable),每次操作都返回新实例,便于后续算法回溯。
public class Board {
private final int[][] board;
private final int score;
public Board(int size) {
this.board = new int[size][];
this.score = 0;
for (int x = 0; x < size; ++x) {
this.board[x] = new int[size];
for (int y = 0; y < size; ++y) {
board[x][y] = 0;
}
}
}
public int getSize() {
return board.length;
}
public int getScore() {
return score;
}
public int getCell(Cell cell) {
return board[cell.getX()][cell.getY()];
}
public boolean isEmpty(Cell cell) {
return getCell(cell) == 0;
}
public List<Cell> emptyCells() {
List<Cell> result = new ArrayList<>();
for (int x = 0; x < board.length; ++x) {
for (int y = 0; y < board[x].length; ++y) {
Cell cell = new Cell(x, y);
if (isEmpty(cell)) {
result.add(cell);
}
}
}
return result;
}
}
📌 说明:
- 使用二维数组存储格子值,0 表示空。
emptyCells()
返回所有空格位置,供后续随机放置新数字使用。- 不可变设计 ✅:每次操作生成新 Board,避免状态污染。
2.2. 计算机玩家与放置方块
计算机玩家的任务很简单:在空格中随机放一个 2 或 4(90% 概率为 2)。
为了支持不可变性,我们添加一个私有构造函数用于复制状态:
private Board(int[][] board, int score) {
this.score = score;
this.board = new int[board.length][];
for (int x = 0; x < board.length; ++x) {
this.board[x] = Arrays.copyOf(board[x], board[x].length);
}
}
然后实现 placeTile
方法:
public Board placeTile(Cell cell, int number) {
if (!isEmpty(cell)) {
throw new IllegalArgumentException("该格子非空");
}
Board result = new Board(this.board, this.score);
result.board[cell.getX()][cell.getY()] = number;
return result;
}
最后是 Computer
类:
public class Computer {
private final SecureRandom rng = new SecureRandom();
public Board makeMove(Board input) {
List<Cell> emptyCells = input.emptyCells();
if (emptyCells.isEmpty()) return input;
double numberToPlace = rng.nextDouble();
int indexToPlace = rng.nextInt(emptyCells.size());
Cell cellToPlace = emptyCells.get(indexToPlace);
return input.placeTile(cellToPlace, numberToPlace >= 0.9 ? 4 : 2);
}
}
📌 要点:
- 使用
SecureRandom
更安全(虽然这里无所谓)。 - 放置失败直接抛异常,防止逻辑错乱。
2.3. 人类玩家与滑动逻辑
这里的“人类”玩家初期只是一个随机走法的占位符,后续会被 AI 替代。
先定义方向枚举:
public enum Move {
UP,
DOWN,
LEFT,
RIGHT
}
为了让滑动逻辑统一处理,我们引入两个辅助方法:转置(transpose)和翻转(reverse):
private static int[][] transpose(int[][] input) {
int[][] result = new int[input.length][];
for (int x = 0; x < input.length; ++x) {
result[x] = new int[input[0].length];
for (int y = 0; y < input[0].length; ++y) {
result[x][y] = input[y][x];
}
}
return result;
}
private static int[][] reverse(int[][] input) {
int[][] result = new int[input.length][];
for (int x = 0; x < input.length; ++x) {
result[x] = new int[input[0].length];
for (int y = 0; y < input[0].length; ++y) {
result[x][y] = input[x][input.length - y - 1];
}
}
return result;
}
核心 move
方法如下:
public Board move(Move move) {
int newScore = 0;
int[][] tiles = new int[this.board.length][];
for (int x = 0; x < this.board.length; ++x) {
tiles[x] = Arrays.copyOf(this.board[x], this.board[x].length);
}
// 统一转换为向上滑动
if (move == Move.LEFT || move == Move.RIGHT) {
tiles = transpose(tiles);
}
if (move == Move.DOWN || move == Move.RIGHT) {
tiles = reverse(tiles);
}
int[][] result = new int[tiles.length][];
for (int x = 0; x < tiles.length; ++x) {
LinkedList<Integer> thisRow = new LinkedList<>();
for (int y = 0; y < tiles[0].length; ++y) {
if (tiles[x][y] > 0) {
thisRow.add(tiles[x][y]);
}
}
// 合并逻辑
LinkedList<Integer> newRow = new LinkedList<>();
while (thisRow.size() >= 2) {
int first = thisRow.pop();
int second = thisRow.peek();
if (second == first) {
int newNumber = first * 2;
newRow.add(newNumber);
newScore += newNumber;
thisRow.pop();
} else {
newRow.add(first);
}
}
newRow.addAll(thisRow);
// 填充结果
result[x] = new int[tiles[0].length];
for (int y = 0; y < tiles[0].length; ++y) {
result[x][y] = newRow.isEmpty() ? 0 : newRow.pop();
}
}
// 恢复原始方向
if (move == Move.DOWN || move == Move.RIGHT) {
result = reverse(result);
}
if (move == Move.LEFT || move == Move.RIGHT) {
result = transpose(result);
}
return new Board(result, this.score + newScore);
}
📌 滑动合并要点:
- 先压缩非零元素 ✅
- 再合并相邻相同数字 ✅
- 每次合并只触发一次 ❌(避免
2 2 2 2
变成8
) - 分数累加正确 ✅
最后是随机“人类”玩家:
public class Human {
private final SecureRandom rng = new SecureRandom();
public Board makeMove(Board input) {
Move move = Move.values()[rng.nextInt(4)];
return input.move(move);
}
}
2.4. 运行游戏循环
加个打印方法方便调试:
private static void printBoard(Board board) {
StringBuilder topLines = new StringBuilder();
StringBuilder midLines = new StringBuilder();
for (int x = 0; x < board.getSize(); ++x) {
topLines.append("+--------");
midLines.append("| ");
}
topLines.append("+");
midLines.append("|");
for (int y = 0; y < board.getSize(); ++y) {
System.out.println(topLines);
System.out.println(midLines);
for (int x = 0; x < board.getSize(); ++x) {
Cell cell = new Cell(x, y);
System.out.print("|");
if (board.isEmpty(cell)) {
System.out.print(" ");
} else {
StringBuilder output = new StringBuilder(Integer.toString(board.getCell(cell)));
while (output.length() < 8) {
output.append(" ");
if (output.length() < 8) {
output.insert(0, " ");
}
}
System.out.print(output);
}
}
System.out.println("|");
System.out.println(midLines);
}
System.out.println(topLines);
System.out.println("Score: " + board.getScore());
}
主循环:
Board board = new Board(4);
Computer computer = new Computer();
Human human = new Human();
// 初始放置两个数字
for (int i = 0; i < 2; ++i) {
board = computer.makeMove(board);
}
printBoard(board);
do {
System.out.println("Human move");
System.out.println("==========");
board = human.makeMove(board);
printBoard(board);
System.out.println("Computer move");
System.out.println("=============");
board = computer.makeMove(board);
printBoard(board);
} while (!board.emptyCells().isEmpty());
System.out.println("Final Score: " + board.getScore());
运行后可以看到一个随机玩家的游戏过程。
3. 实现智能求解器
现在我们把 Human
类升级为 AI 玩家,使用 Expectimax 算法(类似 Minimax,但考虑概率分支)。
3.1. 模拟走法与评分机制
核心思想:尝试每个方向,模拟后续几步,选择期望得分最高的路径。
重写 makeMove
:
public Board makeMove(Board input) {
return Arrays.stream(Move.values())
.parallel()
.map(input::move)
.filter(moved -> !moved.equals(input)) // 剪枝:跳过无效移动
.max(Comparator.comparingInt(board -> generateScore(board, 0)))
.orElse(input);
}
递归生成评分:
private int generateScore(Board board, int depth) {
if (depth >= 3) {
return calculateFinalScore(board);
}
return board.emptyCells().stream()
.flatMap(cell -> Stream.of(new Pair<>(cell, 2), new Pair<>(cell, 4)))
.mapToInt(move -> {
Board newBoard = board.placeTile(move.getFirst(), move.getSecond());
int boardScore = calculateScore(newBoard, depth + 1);
return (int) (boardScore * (move.getSecond() == 2 ? 0.9 : 0.1));
})
.sum();
}
private int calculateScore(Board board, int depth) {
return Arrays.stream(Move.values())
.parallel()
.map(board::move)
.filter(moved -> !moved.equals(board))
.mapToInt(newBoard -> generateScore(newBoard, depth))
.max()
.orElse(0);
}
📌 关键点:
depth >= 3
时停止递归,调用最终评分。- 使用
flatMap
模拟计算机放置 2 或 4。 - 权重:2 占 90%,4 占 10%。
3.2. 终局评分策略
对最终状态进行打分,综合多个因素:
private int calculateFinalScore(Board board) {
List<List<Integer>> rowsToScore = new ArrayList<>();
for (int i = 0; i < board.getSize(); ++i) {
List<Integer> row = new ArrayList<>();
List<Integer> col = new ArrayList<>();
for (int j = 0; j < board.getSize(); ++j) {
row.add(board.getCell(new Cell(i, j)));
col.add(board.getCell(new Cell(j, i)));
}
rowsToScore.add(row);
rowsToScore.add(col);
}
return rowsToScore.stream()
.mapToInt(row -> {
List<Integer> preMerged = row.stream()
.filter(value -> value != 0)
.collect(Collectors.toList());
int numMerges = 0;
int monotonicityLeft = 0;
int monotonicityRight = 0;
for (int i = 0; i < preMerged.size() - 1; ++i) {
Integer first = preMerged.get(i);
Integer second = preMerged.get(i + 1);
if (first.equals(second)) {
++numMerges;
} else if (first > second) {
monotonicityLeft += first - second;
} else {
monotonicityRight += second - first;
}
}
int score = 1000;
score += 250 * row.stream().filter(value -> value == 0).count(); // 空格奖励
score += 750 * numMerges; // 合并奖励
score -= 10 * row.stream().mapToInt(Integer::intValue).sum(); // 数值惩罚(鼓励小数)
score -= 50 * Math.min(monotonicityLeft, monotonicityRight); // 单调性惩罚
return score;
})
.sum();
}
📌 评分因子说明:
- ✅ 空格越多越好(保留操作空间)
- ✅ 合并机会多加分
- ✅ 行列单调有序加分(如 2-4-8-16)
- ❌ 大数过多扣分(防过早合并)
⚠️ 参数可调!不同权重组合会影响 AI 风格(激进 or 保守)
4. 性能优化
原始版本每步耗时约 1 分钟,太慢了。我们来优化。
4.1. 并行流加速
利用 Java 8 Stream 的 .parallel()
:
Arrays.stream(Move.values()).parallel()
✅ 效果:从 60s → 20s 左右。
4.2. 剪枝无效分支
添加 Board.equals()
:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Board board1 = (Board) o;
return Arrays.deepEquals(board, board1.board);
}
然后在流中过滤:
.filter(moved -> !moved.equals(board))
✅ 效果:后期节省大量无效计算,最终耗时降至 3~5 秒/步。
5. 总结
我们实现了一个基于 Expectimax 算法的 2048 求解器,包含:
- 完整游戏逻辑 ✅
- AI 决策框架 ✅
- 多因子评分系统 ✅
- 并行与剪枝优化 ✅
🎯 后续可尝试:
- 调整评分权重,观察 AI 行为变化
- 增加搜索深度(需进一步优化性能)
- 引入启发式规则(如最大数靠角)