每日算法 — 使用java实现Threes:贪心策略与状态空间评估

引言

Threes! 是2014年发布的极简数字合并游戏,玩家在4×4棋盘上滑动数字方块,将相同数字(或1与2)合并为更大数字。与2048不同,Threes的合并规则更精细(1+2=3,3+3=6,6+6=12…),且新方块从边缘生成。本文用Java完整实现Threes游戏引擎,重点讲解两个算法:一是基于贪心策略的AI决策系统,二是利用启发式状态评估函数(单调性+空格数+大数字位置)指导滑动方向选择。

核心算法设计

1. 游戏规则与合并逻辑

  • 棋盘为4×4网格,每格为空或含1/2/3/6/12/24/48/96/192/384/768/1536/3072/6144
  • 滑动方向时,所有方块向该方向靠拢
  • 合并规则:1+2=3,其他相同数字合并翻倍(3+3=6,6+6=12…)
  • 每次有效移动后,在滑动方向的反方向边缘随机生成新方块(1/2/3的概率分别为40%/40%/20%)
  • 当棋盘满且无法合并时游戏结束

2. 启发式评估函数

AI评估棋盘状态的综合得分:
– 空格数权重:空格越多,未来操作空间越大
– 单调性权重:大数字集中在角落,形成单调递减序列(如2048的蛇形策略)
– 大数字位置权重:最大数字放在角落得分最高
– 平滑度权重:相邻数字差值越小越好,便于后续合并

3. 贪心决策 + 期望最大化

对每个方向模拟执行,计算移动后的期望评估值(考虑新方块生成的概率分布),选择期望值最高的方向。更高级的变体可采用Minimax或Expectimax搜索多层。

完整Java实现

import java.util.*;

/**
 * Threes! 游戏引擎:贪心策略 + 启发式状态评估
 */
public class ThreesGame {
    static final int SIZE = 4;
    static final int EMPTY = 0;
    Random rand = new Random(42);
    int[][] board;
    int score;
    boolean gameOver;

    // 评估权重(可调的启发式参数)
    static final double W_EMPTY = 270.0;
    static final double W_MONO = 47.0;
    static final double W_SMOOTH = 11.0;
    static final double W_MAX_CORNER = 100.0;
    static final double W_MERGE = 70.0;

    public ThreesGame() {
        board = new int[SIZE][SIZE];
        spawnTile(); spawnTile(); // 初始两格
    }

    /**
     * 核心合并逻辑:向指定方向滑动
     * 返回是否发生了有效移动
     */
    boolean move(int dir) {
        // dir: 0=上, 1=下, 2=左, 3=右
        int[][] prev = copyBoard();
        boolean moved = false;

        if (dir == 0) moved = moveUp();
        else if (dir == 1) moved = moveDown();
        else if (dir == 2) moved = moveLeft();
        else if (dir == 3) moved = moveRight();

        if (moved) {
            spawnTileAtEdge(dir);
            score += calculateMergeScore(prev, board);
        }
        gameOver = !hasValidMove();
        return moved;
    }

    boolean moveLeft() {
        boolean moved = false;
        for (int r=0; r<SIZE; r++) {
            int[] row = new int[SIZE];
            int idx = 0;
            for (int c=0; c<SIZE; c++) if (board[r][c] != EMPTY) row[idx++] = board[r][c];

            int[] merged = mergeLine(Arrays.copyOf(row, idx));
            Arrays.fill(board[r], EMPTY);
            for (int i=0; i<merged.length; i++) board[r][i] = merged[i];
            if (merged.length != idx || !Arrays.equals(merged, Arrays.copyOf(row, idx))) moved = true;
        }
        return moved;
    }

    boolean moveRight() {
        reverseBoardHorizontally();
        boolean moved = moveLeft();
        reverseBoardHorizontally();
        return moved;
    }

    boolean moveUp() {
        transpose();
        boolean moved = moveLeft();
        transpose();
        return moved;
    }

    boolean moveDown() {
        transpose();
        boolean moved = moveRight();
        transpose();
        return moved;
    }

    /**
     * 单行合并逻辑(从左到右)
     * Threes规则:1+2=3,相同数字(>=3)合并翻倍
     */
    int[] mergeLine(int[] line) {
        List<Integer> result = new ArrayList<>();
        boolean[] merged = new boolean[line.length];

        for (int i=0; i<line.length; i++) {
            if (merged[i]) continue;
            if (i+1 < line.length && !merged[i+1]) {
                // 检查是否可以合并
                int a = line[i], b = line[i+1];
                if ((a==1 && b==2) || (a==2 && b==1)) {
                    result.add(3); merged[i]=merged[i+1]=true; continue;
                }
                if (a >= 3 && a == b) {
                    result.add(a*2); merged[i]=merged[i+1]=true; continue;
                }
            }
            result.add(line[i]);
        }
        return result.stream().mapToInt(Integer::intValue).toArray();
    }

    /** 在滑动方向的反方向边缘生成新方块 */
    void spawnTileAtEdge(int dir) {
        List<int[]> candidates = new ArrayList<>();
        if (dir == 0) { // 上滑 -> 底部生成
            for (int c=0; c<SIZE; c++) if (board[SIZE-1][c]==EMPTY) candidates.add(new int[]{SIZE-1,c});
        } else if (dir == 1) { // 下滑 -> 顶部生成
            for (int c=0; c<SIZE; c++) if (board[0][c]==EMPTY) candidates.add(new int[]{0,c});
        } else if (dir == 2) { // 左滑 -> 右侧生成
            for (int r=0; r<SIZE; r++) if (board[r][SIZE-1]==EMPTY) candidates.add(new int[]{r,SIZE-1});
        } else if (dir == 3) { // 右滑 -> 左侧生成
            for (int r=0; r<SIZE; r++) if (board[r][0]==EMPTY) candidates.add(new int[]{r,0});
        }

        if (candidates.isEmpty()) return;
        int[] pos = candidates.get(rand.nextInt(candidates.size()));
        double p = rand.nextDouble();
        board[pos[0]][pos[1]] = p < 0.4 ? 1 : (p < 0.8 ? 2 : 3);
    }

    /** 随机生成初始方块 */
    void spawnTile() {
        List<int[]> empty = new ArrayList<>();
        for (int r=0; r<SIZE; r++) for (int c=0; c<SIZE; c++) if (board[r][c]==EMPTY) empty.add(new int[]{r,c});
        if (empty.isEmpty()) return;
        int[] pos = empty.get(rand.nextInt(empty.size()));
        board[pos[0]][pos[1]] = rand.nextDouble() < 0.7 ? 1 : 2;
    }

    /**
     * 启发式状态评估函数
     */
    double evaluateBoard(int[][] b) {
        double score = 0;

        // 1. 空格数奖励
        int emptyCount = countEmpty(b);
        score += W_EMPTY * emptyCount;

        // 2. 单调性(Snake Pattern):大数字在角落形成单调序列
        score += W_MONO * calculateMonotonicity(b);

        // 3. 平滑度:相邻格子差值倒数和
        score += W_SMOOTH * calculateSmoothness(b);

        // 4. 最大数字在角落奖励
        score += W_MAX_CORNER * maxInCorner(b);

        // 5. 可合并对数奖励
        score += W_MERGE * countMergeable(b);

        return score;
    }

    int countEmpty(int[][] b) {
        int cnt = 0;
        for (int[] row : b) for (int v : row) if (v == EMPTY) cnt++;
        return cnt;
    }

    /**
     * 计算单调性:分别评估四个方向的单调性,取最大值
     * 目标是大数字集中在某一角落,形成递减序列
     */
    double calculateMonotonicity(int[][] b) {
        double[] totals = new double[4]; // 左->右, 右->左, 上->下, 下->上

        // 行方向
        for (int r=0; r<SIZE; r++) {
            int current = 0;
            while (current < SIZE-1 && b[r][current] != EMPTY) {
                int next = current + 1;
                while (next < SIZE && b[r][next] == EMPTY) next++;
                if (next >= SIZE) break;
                if (b[r][current] >= b[r][next]) totals[0] += Math.log(b[r][next]+1) / Math.log(2);
                else totals[1] += Math.log(b[r][current]+1) / Math.log(2);
                current = next;
            }
        }

        // 列方向
        for (int c=0; c<SIZE; c++) {
            int current = 0;
            while (current < SIZE-1 && b[current][c] != EMPTY) {
                int next = current + 1;
                while (next < SIZE && b[next][c] == EMPTY) next++;
                if (next >= SIZE) break;
                if (b[current][c] >= b[next][c]) totals[2] += Math.log(b[next][c]+1) / Math.log(2);
                else totals[3] += Math.log(b[current][c]+1) / Math.log(2);
                current = next;
            }
        }

        return Math.max(totals[0], totals[1]) + Math.max(totals[2], totals[3]);
    }

    double calculateSmoothness(int[][] b) {
        double smooth = 0;
        for (int r=0; r<SIZE; r++) {
            for (int c=0; c<SIZE-1; c++) {
                if (b[r][c] != EMPTY && b[r][c+1] != EMPTY)
                    smooth -= Math.abs(valueLog(b[r][c]) - valueLog(b[r][c+1]));
            }
        }
        for (int r=0; r<SIZE-1; r++) {
            for (int c=0; c<SIZE; c++) {
                if (b[r][c] != EMPTY && b[r+1][c] != EMPTY)
                    smooth -= Math.abs(valueLog(b[r][c]) - valueLog(b[r+1][c]));
            }
        }
        return smooth;
    }

    double valueLog(int v) {
        if (v <= 2) return v;
        return Math.log(v/3.0) / Math.log(2) + 1;
    }

    double maxInCorner(int[][] b) {
        int max = 0;
        for (int[] row : b) for (int v : row) max = Math.max(max, v);
        if (b[0][0] == max || b[0][SIZE-1] == max || b[SIZE-1][0] == max || b[SIZE-1][SIZE-1] == max)
            return 1.0;
        return 0.0;
    }

    int countMergeable(int[][] b) {
        int count = 0;
        for (int r=0; r<SIZE; r++) {
            for (int c=0; c<SIZE; c++) {
                if (b[r][c] == EMPTY) continue;
                if (c+1<SIZE && canMerge(b[r][c], b[r][c+1])) count++;
                if (r+1<SIZE && canMerge(b[r][c], b[r+1][c])) count++;
            }
        }
        return count;
    }

    boolean canMerge(int a, int b) {
        if (a == EMPTY || b == EMPTY) return false;
        if ((a==1 && b==2) || (a==2 && b==1)) return true;
        return a >= 3 && a == b;
    }

    /**
     * AI决策:对四个方向计算期望评估值,选择最优
     */
    int getBestMove() {
        double bestScore = Double.NEGATIVE_INFINITY;
        int bestDir = -1;

        for (int dir=0; dir<4; dir++) {
            int[][] sim = simulateMove(board, dir);
            if (sim == null) continue; // 无效移动

            // 计算该方向的期望评估值(考虑不同新方块的概率)
            double expectedScore = evaluateBoard(sim);

            if (expectedScore > bestScore) {
                bestScore = expectedScore;
                bestDir = dir;
            }
        }
        return bestDir;
    }

    /** 模拟执行某方向移动,不实际修改board */
    int[][] simulateMove(int[][] src, int dir) {
        ThreesGame temp = new ThreesGame();
        temp.board = copyBoard(src);
        boolean moved = temp.move(dir);
        return moved ? temp.board : null;
    }

    boolean hasValidMove() {
        for (int dir=0; dir<4; dir++) {
            ThreesGame temp = new ThreesGame();
            temp.board = copyBoard(board);
            if (temp.move(dir)) return true;
        }
        return false;
    }

    int calculateMergeScore(int[][] before, int[][] after) {
        int sumBefore = 0, sumAfter = 0;
        for (int r=0; r<SIZE; r++) {
            for (int c=0; c<SIZE; c++) {
                sumBefore += before[r][c];
                sumAfter += after[r][c];
            }
        }
        return sumAfter - sumBefore;
    }

    int[][] copyBoard(int[][] src) {
        int[][] dst = new int[SIZE][SIZE];
        for (int i=0; i<SIZE; i++) System.arraycopy(src[i], 0, dst[i], 0, SIZE);
        return dst;
    }

    int[][] copyBoard() { return copyBoard(board); }

    void transpose() {
        for (int r=0; r<SIZE; r++)
            for (int c=r+1; c<SIZE; c++) {
                int t = board[r][c]; board[r][c] = board[c][r]; board[c][r] = t;
            }
    }

    void reverseBoardHorizontally() {
        for (int r=0; r<SIZE; r++)
            for (int c=0; c<SIZE/2; c++) {
                int t = board[r][c]; board[r][c] = board[r][SIZE-1-c]; board[r][SIZE-1-c] = t;
            }
    }

    void printBoard() {
        System.out.println("Score: " + score);
        for (int[] row : board) {
            for (int v : row) System.out.printf(v==0?"   . ":"%4d ", v);
            System.out.println();
        }
        System.out.println();
    }

    void runAI(int maxMoves) {
        System.out.println("=== Threes! AI 开始 ===");
        printBoard();
        for (int i=0; i<maxMoves && !gameOver; i++) {
            int dir = getBestMove();
            if (dir < 0) break;
            String[] names = {"上","下","左","右"};
            System.out.println("第"+(i+1)+"步: " + names[dir]);
            move(dir);
            printBoard();
        }
        System.out.println("Game Over! Final Score: " + score);
    }

    public static void main(String[] args) {
        new ThreesGame().runAI(1000);
    }
}

复杂度分析

  • 单次移动:遍历4行/列,每行合并 O(SIZE),总时间 O(SIZE²) = O(16),空间 O(SIZE)。
  • 启发式评估:计算单调性、平滑度等需遍历整个棋盘,时间 O(SIZE²),空间 O(1)。
  • AI决策:需模拟4个方向,每个方向一次评估 O(SIZE²),总时间 O(4×SIZE²) = O(64),每步可在微秒级完成。若扩展为两层Expectimax搜索,复杂度变为 O(4² × SIZE²)。

总结

本文实现了Threes!的完整游戏引擎与AI决策系统:独特的1+2=3合并规则、边缘生成机制、以及基于空格数+单调性+平滑度+角落最大值的启发式评估。该AI采用贪心策略每步选择评估值最高的方向,在Threes中可达到较高分数。读者可进一步实现Expectimax多层搜索,或引入神经网络学习更优的评估函数权重。