每日算法 — 使用java实现俄罗斯方块:贪心策略与状态评估

游戏介绍

俄罗斯方块(Tetris)是1984年诞生的经典益智游戏,七种不同形状的方块(I、O、T、S、Z、J、L)从顶部下落,玩家可以旋转和左右移动方块,目标是填满整行以消除得分。本文用Java实现俄罗斯方块的AI自动玩家,核心是使用贪心策略评估每个落点的得分。

核心算法

状态评估函数

AI决策的关键是评估每个可能的落点状态,选取评分最高的动作。评估函数综合考虑以下因素:
1. ** Landing Height:方块落点高度(越低越好)
2.
Eroded Piece Cells:消行时贡献的方块数(越多越好)
3.
Row Transitions:行中空到填的转换次数(越少越好)
4.
Column Transitions:列中空到填的转换次数(越少越好)
5.
Holes:被覆盖的空洞数(越少越好)
6.
Well Sums**:井的深度之和(越少越好)

贪心搜索

对每个新方块,枚举所有旋转和水平位置,模拟下落到底部,计算评估分数,选择最优落点。

Java实现

import java.util.*;

public class TetrisAI {
    // 游戏板:宽10高20
    static final int BOARD_WIDTH = 10;
    static final int BOARD_HEIGHT = 20;

    // 七种方块形状(以4x4矩阵表示)
    static final int[][][][] SHAPES = {
        // I
        {{{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
        {{{0,0,0,0}, {0,1,1,0}, {0,1,1,0}, {0,0,0,0}}},
        // T
        {{{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}},
         {{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}}},
        // S
        {{{0,0,0,0}, {0,1,1,0}, {1,1,0,0}, {0,0,0,0}},
         {{1,0,0,0}, {1,1,0,0}, {0,1,0,0}, {0,0,0,0}}},
        // Z
        {{{0,0,0,0}, {1,1,0,0}, {0,1,1,0}, {0,0,0,0}},
         {{0,1,0,0}, {1,1,0,0}, {1,0,0,0}, {0,0,0,0}}},
        // J
        {{{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}},
         {{1,0,0,0}, {1,1,1,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}}},
        // L
        {{{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}},
         {{0,0,1,0}, {1,1,1,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}}}
    };

    // 评估权重(基于Pierre Dellacherie算法)
    static final double W_LANDING_HEIGHT = -4.500158825082766;
    static final double W_ERODED_CELLS = 3.4181268101392694;
    static final double W_ROW_TRANSITIONS = -3.2178882868487753;
    static final double W_COL_TRANSITIONS = -9.348695305445199;
    static final double W_HOLES = -7.899265427351652;
    static final double W_WELL_SUMS = -3.3855972247263626;

    Random rand = new Random();
    boolean[][] board = new boolean[BOARD_HEIGHT][BOARD_WIDTH];
    int score = 0;
    int lines = 0;

    // 获取方块的某旋转状态下的有效单元格
    List<int[]> getCells(int shapeIdx, int rot) {
        List<int[]> cells = new ArrayList<>();
        int[][] shape = SHAPES[shapeIdx][rot];
        for (int y = 0; y < 4; y++) {
            for (int x = 0; x < 4; x++) {
                if (shape[y][x] == 1) cells.add(new int[]{x, y});
            }
        }
        return cells;
    }

    // 模拟放置方块,返回(新棋盘,消除行数,落点高度)
    Result simulatePlace(boolean[][] currentBoard, int shapeIdx, int rot, int offsetX) {
        List<int[]> cells = getCells(shapeIdx, rot);
        boolean[][] newBoard = copyBoard(currentBoard);

        // 找到能放置的最低y位置
        int minY = BOARD_HEIGHT;
        for (int[] c : cells) {
            int x = c[0] + offsetX;
            if (x < 0 || x >= BOARD_WIDTH) return null; // 越界
            // 从顶部开始向下找碰撞
            for (int y = 0; y <= BOARD_HEIGHT; y++) {
                int by = c[1] + y;
                if (by >= BOARD_HEIGHT || currentBoard[by][x]) {
                    minY = Math.min(minY, y - 1);
                    break;
                }
            }
        }

        if (minY < 0) return null; // 无法放置

        // 放置方块
        int landingHeight = 0;
        for (int[] c : cells) {
            int x = c[0] + offsetX;
            int y = c[1] + minY;
            if (y < 0) return null;
            newBoard[y][x] = true;
            landingHeight = Math.max(landingHeight, BOARD_HEIGHT - y);
        }

        // 消除满行
        int cleared = 0;
        int erodedCells = 0;
        for (int y = BOARD_HEIGHT - 1; y >= 0; y--) {
            boolean full = true;
            for (int x = 0; x < BOARD_WIDTH; x++) {
                if (!newBoard[y][x]) { full = false; break; }
            }
            if (full) {
                // 统计这行中被刚放置的方块数(简化:不精确追踪,用总消除行数估算)
                erodedCells += 4; // 估算
                cleared++;
                // 下移所有行
                for (int dy = y; dy > 0; dy--) {
                    System.arraycopy(newBoard[dy - 1], 0, newBoard[dy], 0, BOARD_WIDTH);
                }
                Arrays.fill(newBoard[0], false);
                y++; // 重新检查当前行
            }
        }

        return new Result(newBoard, cleared, landingHeight, erodedCells);
    }

    // 评估棋盘状态
    double evaluate(boolean[][] b) {
        // 1. Row Transitions
        int rowTrans = 0;
        for (int y = 0; y < BOARD_HEIGHT; y++) {
            boolean prev = true; // 左边界外视为已填充
            for (int x = 0; x < BOARD_WIDTH; x++) {
                boolean curr = b[y][x];
                if (curr != prev) rowTrans++;
                prev = curr;
            }
            if (!prev) rowTrans++; // 右边界
        }

        // 2. Column Transitions
        int colTrans = 0;
        for (int x = 0; x < BOARD_WIDTH; x++) {
            boolean prev = true; // 上边界外视为已填充
            for (int y = 0; y < BOARD_HEIGHT; y++) {
                boolean curr = b[y][x];
                if (curr != prev) colTrans++;
                prev = curr;
            }
            if (!prev) colTrans++; // 下边界
        }

        // 3. Holes(空洞:上方有填充但当前为空)
        int holes = 0;
        for (int x = 0; x < BOARD_WIDTH; x++) {
            boolean filledAbove = false;
            for (int y = 0; y < BOARD_HEIGHT; y++) {
                if (b[y][x]) filledAbove = true;
                else if (filledAbove) holes++;
            }
        }

        // 4. Well Sums(井:两侧都被填充的连续空列段)
        int wellSums = 0;
        for (int x = 0; x < BOARD_WIDTH; x++) {
            for (int y = 0; y < BOARD_HEIGHT; y++) {
                if (!b[y][x]) {
                    boolean left = (x == 0) || b[y][x - 1];
                    boolean right = (x == BOARD_WIDTH - 1) || b[y][x + 1];
                    if (left && right) {
                        int depth = 1;
                        for (int dy = y + 1; dy < BOARD_HEIGHT && !b[dy][x]; dy++) {
                            boolean l = (x == 0) || b[dy][x - 1];
                            boolean r = (x == BOARD_WIDTH - 1) || b[dy][x + 1];
                            if (l && r) depth++;
                            else break;
                        }
                        wellSums += depth;
                    }
                }
            }
        }

        // 组合评分
        return W_LANDING_HEIGHT * 0 +
               W_ROW_TRANSITIONS * rowTrans +
               W_COL_TRANSITIONS * colTrans +
               W_HOLES * holes +
               W_WELL_SUMS * wellSums;
    }

    // AI选择最佳动作
    Move findBestMove(int shapeIdx) {
        Move best = null;
        double bestScore = Double.NEGATIVE_INFINITY;

        int rotations = SHAPES[shapeIdx].length;
        for (int rot = 0; rot < rotations; rot++) {
            for (int x = -3; x < BOARD_WIDTH; x++) {
                Result r = simulatePlace(board, shapeIdx, rot, x);
                if (r == null) continue;

                double score = evaluate(r.board);
                // 奖励消行
                score += W_ERODED_CELLS * r.cleared * 4;
                score += W_LANDING_HEIGHT * (BOARD_HEIGHT - r.landingHeight);

                if (score > bestScore) {
                    bestScore = score;
                    best = new Move(rot, x, r);
                }
            }
        }
        return best;
    }

    // 执行一步
    boolean playStep() {
        int shapeIdx = rand.nextInt(7);
        Move move = findBestMove(shapeIdx);
        if (move == null) return false; // 游戏结束

        board = move.result.board;
        lines += move.result.cleared;
        score += move.result.cleared * move.result.cleared * 100;
        return true;
    }

    // 打印棋盘
    void printBoard() {
        for (int y = 0; y < BOARD_HEIGHT; y++) {
            for (int x = 0; x < BOARD_WIDTH; x++) {
                System.out.print(board[y][x] ? "[]" : " .");
            }
            System.out.println();
        }
        System.out.println("Score: " + score + " Lines: " + lines);
        System.out.println();
    }

    boolean[][] copyBoard(boolean[][] src) {
        boolean[][] c = new boolean[BOARD_HEIGHT][BOARD_WIDTH];
        for (int y = 0; y < BOARD_HEIGHT; y++) {
            System.arraycopy(src[y], 0, c[y], 0, BOARD_WIDTH);
        }
        return c;
    }

    // 数据结构
    static class Result {
        boolean[][] board;
        int cleared;
        int landingHeight;
        int erodedCells;
        Result(boolean[][] b, int c, int h, int e) {
            board = b; cleared = c; landingHeight = h; erodedCells = e;
        }
    }

    static class Move {
        int rotation, offsetX;
        Result result;
        Move(int r, int x, Result res) {
            rotation = r; offsetX = x; result = res;
        }
    }

    public static void main(String[] args) {
        TetrisAI ai = new TetrisAI();
        int steps = 0;
        while (ai.playStep() && steps < 10000) {
            steps++;
            if (steps % 100 == 0) {
                System.out.println("Step " + steps + " Lines: " + ai.lines);
            }
        }
        System.out.println("Game Over! Steps: " + steps + " Lines: " + ai.lines + " Score: " + ai.score);
    }
}

复杂度分析

  • 单次评估时间:$O(WIDTH \times HEIGHT)$,即 $O(200)$
  • 每方块搜索空间:最多4种旋转 $\times$ 约13个水平位置 = 52种放置方式
  • 每步总复杂度:$O(52 \times WH) \approx O(10^4)$,现代计算机可轻松实时计算
  • 整体游戏复杂度:随高度增加,搜索空间不变,评估效率恒定

总结

本文实现了基于Pierre Dellacherie评估函数的俄罗斯方块AI,通过六维特征评分选择最优落点。读者可以尝试调整权重参数、加入搜索深度(如lookahead多步)或使用遗传算法优化权重,进一步提升AI表现。