每日算法 — 使用java实现俄罗斯方块:Pierre Dellacherie算法与多特征线性评估

引言

俄罗斯方块(Tetris)是史上最经典的益智游戏之一。由于游戏状态空间巨大,传统的搜索算法难以应对。本文使用Java实现Pierre Dellacherie算法——这是目前已知最强的传统俄罗斯方块AI之一,通过6维特征向量的线性加权评估,在每一步选择最优的方块落点与旋转姿态。

算法核心思想

动作空间定义

俄罗斯方块的决策不仅包括移动方向,还包括旋转角度和最终落点。一个完整的动作定义为:
– 旋转次数(0~3)
– 水平偏移量(左移/右移格数)
– 硬降(直接落到底部)

Pierre Dellacherie评估函数

算法使用以下6个核心特征评估局面:

特征 符号 说明
Landing Height $LH$ 方块落点中心的高度
Eroded Piece Cells $EPC$ 消除行时属于当前方块的格子数
Row Transitions $RT$ 行内空→满/满→空的切换次数
Column Transitions $CT$ 列内空→满/满→空的切换次数
Holes $H$ 被方块覆盖的空洞数量
Board Wells $BW$ 井深之和(两侧有方块支撑的空列)

总评分:$Score = \sum_{i} w_i \cdot f_i$

Java实现

游戏板与方块定义

import java.util.*;

public class TetrisDellacherie {
    static final int ROWS = 20;
    static final int COLS = 10;

    // 7种经典方块的旋转形态(以4x4矩阵表示)
    static final int[][][][] PIECES = {
        // I-piece
        {{{0,0,0,0},{1,1,1,1},{0,0,0,0},{0,0,0,0}},
         {{0,0,1,0},{0,0,1,0},{0,0,1,0},{0,0,1,0}}},
        // O-piece
        {{{0,1,1,0},{0,1,1,0},{0,0,0,0},{0,0,0,0}}},
        // T-piece
        {{{0,1,0,0},{1,1,1,0},{0,0,0,0},{0,0,0,0}},
         {{0,1,0,0},{0,1,1,0},{0,1,0,0},{0,0,0,0}},
         {{0,0,0,0},{1,1,1,0},{0,1,0,0},{0,0,0,0}},
         {{0,1,0,0},{1,1,0,0},{0,1,0,0},{0,0,0,0}}},
        // S-piece
        {{{0,1,1,0},{1,1,0,0},{0,0,0,0},{0,0,0,0}},
         {{0,1,0,0},{0,1,1,0},{0,0,1,0},{0,0,0,0}}},
        // Z-piece
        {{{1,1,0,0},{0,1,1,0},{0,0,0,0},{0,0,0,0}},
         {{0,0,1,0},{0,1,1,0},{0,1,0,0},{0,0,0,0}}},
        // J-piece
        {{{1,0,0,0},{1,1,1,0},{0,0,0,0},{0,0,0,0}},
         {{0,1,1,0},{0,1,0,0},{0,1,0,0},{0,0,0,0}},
         {{0,0,0,0},{1,1,1,0},{0,0,1,0},{0,0,0,0}},
         {{0,1,0,0},{0,1,0,0},{1,1,0,0},{0,0,0,0}}},
        // L-piece
        {{{0,0,1,0},{1,1,1,0},{0,0,0,0},{0,0,0,0}},
         {{0,1,0,0},{0,1,0,0},{0,1,1,0},{0,0,0,0}},
         {{0,0,0,0},{1,1,1,0},{1,0,0,0},{0,0,0,0}},
         {{1,1,0,0},{0,1,0,0},{0,1,0,0},{0,0,0,0}}}
    };

    static final String[] PIECE_NAMES = {"I","O","T","S","Z","J","L"};

    // Dellacherie原始权重(经大量实验调优)
    static final double[] WEIGHTS = {
        -4.500158825082766,   // Landing Height
        3.4181268101392694,   // Eroded Piece Cells
        -3.2178882868487753,  // Row Transitions
        -9.348695305445199,   // Column Transitions
        -7.899265427351652,   // Holes
        -3.3855972247263626   // Board Wells
    };

    int[][] board = new int[ROWS][COLS];
    int totalLines = 0;
    int totalPieces = 0;
    Random rand = new Random();

特征计算

    /**
     * 计算Landing Height:当前方块落点中心到板底的距离
     */
    double computeLandingHeight(int pieceIdx, int rotation, int col, int landingRow) {
        int[][] shape = PIECES[pieceIdx][rotation];
        int minR = 3, maxR = 0;
        for (int r = 0; r < 4; r++) {
            for (int c = 0; c < 4; c++) {
                if (shape[r][c] == 1) {
                    minR = Math.min(minR, r);
                    maxR = Math.max(maxR, r);
                }
            }
        }
        // 中心高度 = 落点行 + 形态中心偏移
        return ROWS - (landingRow + minR) - (maxR - minR) / 2.0;
    }

    /**
     * 计算Eroded Piece Cells:当前方块参与消除的格子数
     */
    int computeErodedCells(int[][] tempBoard, int pieceIdx, int rotation, 
                           int landingRow, int col, int linesCleared) {
        if (linesCleared == 0) return 0;
        int[][] shape = PIECES[pieceIdx][rotation];
        int count = 0;
        for (int r = 0; r < 4; r++) {
            for (int c = 0; c < 4; c++) {
                if (shape[r][c] == 1) {
                    int br = landingRow + r;
                    int bc = col + c;
                    // 检查该格子所在行是否被消除
                    boolean rowFull = true;
                    for (int cc = 0; cc < COLS; cc++) {
                        if (tempBoard[br][cc] == 0) {
                            rowFull = false;
                            break;
                        }
                    }
                    if (rowFull) count++;
                }
            }
        }
        return count * linesCleared;
    }

    /**
     * 计算Row Transitions
     */
    int computeRowTransitions(int[][] b) {
        int transitions = 0;
        for (int r = 0; r < ROWS; r++) {
            int prev = 1; // 边界视为已填充
            for (int c = 0; c < COLS; c++) {
                int cell = b[r][c] != 0 ? 1 : 0;
                if (cell != prev) transitions++;
                prev = cell;
            }
            if (prev == 0) transitions++; // 右边界
        }
        return transitions;
    }

    /**
     * 计算Column Transitions
     */
    int computeColTransitions(int[][] b) {
        int transitions = 0;
        for (int c = 0; c < COLS; c++) {
            int prev = 1; // 顶部边界视为已填充
            for (int r = 0; r < ROWS; r++) {
                int cell = b[r][c] != 0 ? 1 : 0;
                if (cell != prev) transitions++;
                prev = cell;
            }
        }
        return transitions;
    }

    /**
     * 计算Holes:空洞是被方块覆盖的空单元格
     */
    int computeHoles(int[][] b) {
        int holes = 0;
        for (int c = 0; c < COLS; c++) {
            boolean blockFound = false;
            for (int r = 0; r < ROWS; r++) {
                if (b[r][c] != 0) blockFound = true;
                else if (blockFound) holes++;
            }
        }
        return holes;
    }

    /**
     * 计算Board Wells:井是两侧有方块支撑的空列段
     */
    int computeWells(int[][] b) {
        int wellSum = 0;
        for (int c = 0; c < COLS; c++) {
            for (int r = 0; r < ROWS; r++) {
                if (b[r][c] == 0) {
                    boolean leftBlocked = (c == 0) || (b[r][c-1] != 0);
                    boolean rightBlocked = (c == COLS-1) || (b[r][c+1] != 0);
                    if (leftBlocked && rightBlocked) {
                        // 计算井深
                        int depth = 1;
                        for (int rr = r + 1; rr < ROWS && b[rr][c] == 0; rr++) {
                            boolean lb = (c == 0) || (b[rr][c-1] != 0);
                            boolean rb = (c == COLS-1) || (b[rr][c+1] != 0);
                            if (lb && rb) depth++;
                            else break;
                        }
                        wellSum += depth;
                    }
                }
            }
        }
        return wellSum;
    }

动作模拟与最优决策

    /**
     * 模拟放置方块,返回(临时棋盘, 落点行, 消除行数)
     */
    Object[] simulatePlace(int pieceIdx, int rotation, int col) {
        int[][] temp = new int[ROWS][COLS];
        for (int r = 0; r < ROWS; r++)
            System.arraycopy(board[r], 0, temp[r], 0, COLS);

        int[][] shape = PIECES[pieceIdx][rotation];
        // 找到落点行
        int landingRow = -1;
        for (int startRow = 0; startRow <= ROWS - 4; startRow++) {
            boolean canPlace = true;
            for (int r = 0; r < 4 && canPlace; r++) {
                for (int c = 0; c < 4; c++) {
                    if (shape[r][c] == 1) {
                        int br = startRow + r, bc = col + c;
                        if (br < 0 || br >= ROWS || bc < 0 || bc >= COLS || temp[br][bc] != 0) {
                            canPlace = false;
                            break;
                        }
                    }
                }
            }
            if (canPlace) landingRow = startRow;
            else break;
        }
        if (landingRow < 0) return null; // 无法放置

        // 放置方块
        for (int r = 0; r < 4; r++) {
            for (int c = 0; c < 4; c++) {
                if (shape[r][c] == 1) {
                    temp[landingRow + r][col + c] = pieceIdx + 1;
                }
            }
        }

        // 消除满行
        int linesCleared = 0;
        for (int r = ROWS - 1; r >= 0; r--) {
            boolean full = true;
            for (int c = 0; c < COLS; c++) {
                if (temp[r][c] == 0) { full = false; break; }
            }
            if (full) {
                linesCleared++;
                // 下移行
                for (int rr = r; rr > 0; rr--)
                    System.arraycopy(temp[rr-1], 0, temp[rr], 0, COLS);
                Arrays.fill(temp[0], 0);
                r++; // 重新检查当前行
            }
        }

        return new Object[]{temp, landingRow, linesCleared};
    }

    /**
     * 评估一个动作,返回分数(越高越好)
     */
    double evaluateMove(int pieceIdx, int rotation, int col) {
        Object[] result = simulatePlace(pieceIdx, rotation, col);
        if (result == null) return Double.NEGATIVE_INFINITY;

        int[][] temp = (int[][]) result[0];
        int landingRow = (int) result[1];
        int linesCleared = (int) result[2];

        double lh = computeLandingHeight(pieceIdx, rotation, col, landingRow);
        double epc = computeErodedCells(temp, pieceIdx, rotation, landingRow, col, linesCleared);
        double rt = computeRowTransitions(temp);
        double ct = computeColTransitions(temp);
        double h = computeHoles(temp);
        double bw = computeWells(temp);

        return WEIGHTS[0] * lh + WEIGHTS[1] * epc + WEIGHTS[2] * rt 
             + WEIGHTS[3] * ct + WEIGHTS[4] * h + WEIGHTS[5] * bw;
    }

    /**
     * 寻找当前方块的最优放置方案
     */
    int[] findBestMove(int pieceIdx) {
        double bestScore = Double.NEGATIVE_INFINITY;
        int bestRot = 0, bestCol = 0;

        int rotations = PIECES[pieceIdx].length;
        for (int rot = 0; rot < rotations; rot++) {
            for (int col = -3; col < COLS; col++) {
                double score = evaluateMove(pieceIdx, rot, col);
                if (score > bestScore) {
                    bestScore = score;
                    bestRot = rot;
                    bestCol = col;
                }
            }
        }
        return new int[]{bestRot, bestCol};
    }

游戏主循环

    void playGame(int maxPieces) {
        System.out.println("=== Tetris Dellacherie AI ===");
        for (int i = 0; i < maxPieces; i++) {
            int piece = rand.nextInt(7);
            int[] move = findBestMove(piece);

            Object[] result = simulatePlace(piece, move[0], move[1]);
            if (result == null) {
                System.out.println("游戏结束! 总消除行数: " + totalLines + ", 总方块数: " + totalPieces);
                return;
            }

            board = (int[][]) result[0];
            int lines = (int) result[2];
            totalLines += lines;
            totalPieces++;

            if (lines > 0) {
                System.out.printf("放置 %s, 消除 %d 行, 累计 %d 行%n", 
                    PIECE_NAMES[piece], lines, totalLines);
            }
        }
        System.out.println("完成! 总消除行数: " + totalLines + ", 总方块数: " + totalPieces);
    }

    public static void main(String[] args) {
        TetrisDellacherie game = new TetrisDellacherie();
        game.playGame(1000);
    }

复杂度分析

操作 时间复杂度 说明
特征计算 $O(ROWS \times COLS)$ 遍历整个棋盘
单动作评估 $O(ROWS \times COLS)$ 模拟放置+特征提取
单方块决策 $O(4 \times COLS \times ROWS \times COLS)$ 最多4种旋转×10列
实际运行 约 $O(1)$ 棋盘尺寸固定为20×10

总结

Pierre Dellacherie算法的精髓在于将复杂的俄罗斯方块决策问题转化为6维线性回归问题。通过精心设计的特征工程(Landing Height、Row Transitions等),算法无需前瞻搜索即可获得顶尖水平。读者可以在此基础上引入特征权重自适应调整(如CEM交叉熵方法)或结合深度强化学习,进一步提升AI的上限。