每日算法 — 使用java实现黑白棋:Minimax博弈树与Alpha-Beta剪枝局面评估

黑白棋(Reversi / Othello)是一款经典的二人零和博弈棋盘游戏,双方在8×8的棋盘上轮流落子,夹住对方棋子的直线(横、竖、斜)上的所有对方棋子将翻转为己方颜色。本文用Java实现一个带AI的黑白棋游戏,核心讲解Minimax博弈树搜索、Alpha-Beta剪枝优化,以及多维度启发式局面评估函数的设计。

博弈树搜索框架

黑白棋的AI决策采用深度优先的Minimax搜索:
– Max节点:AI试图最大化自己的评估值
– Min节点:对手试图最小化AI的评估值(即最大化自己的得分)
– Alpha-Beta剪枝:维护[alpha, beta]窗口,当某分支的值超出窗口时立即停止搜索该分支

启发式评估函数

黑白棋中棋子数不等于优势(角落被占后大量翻转常见),因此评估函数需综合多个维度:

  1. 棋子权重表:角落和边缘位置价值远高于中心,角落为最高价值(约+100),边缘次之,中心附近为负分。
  2. 行动力(Mobility):可落子位置越多,选择空间越大,通常赋予正权重。
  3. 稳定子(Stable Discs):无法被翻转的棋子,尤其是角落及其延伸线上的棋子。
  4. ** frontier(前沿子)**:与空格相邻的棋子容易被翻转,应尽量减少。

Java 实现

import java.util.*;

public class ReversiAI {
    static final int SIZE = 8;
    static final int EMPTY = 0, BLACK = 1, WHITE = 2;
    static final int[] DX = {-1,-1,-1,0,0,1,1,1};
    static final int[] DY = {-1,0,1,-1,1,-1,0,1};

    // 位置权重表:角落最高,边缘次之,中心附近为负
    static final int[][] POSITION_WEIGHT = {
        {100, -20,  10,   5,   5,  10, -20, 100},
        {-20, -30,  -5,  -5,  -5,  -5, -30, -20},
        { 10,  -5,  10,   2,   2,  10,  -5,  10},
        {  5,  -5,   2,   0,   0,   2,  -5,   5},
        {  5,  -5,   2,   0,   0,   2,  -5,   5},
        { 10,  -5,  10,   2,   2,  10,  -5,  10},
        {-20, -30,  -5,  -5,  -5,  -5, -30, -20},
        {100, -20,  10,   5,   5,  10, -20, 100}
    };

    int[][] board = new int[SIZE][SIZE];
    int currentPlayer = BLACK;

    public ReversiAI() {
        // 初始布局
        board[3][3] = board[4][4] = WHITE;
        board[3][4] = board[4][3] = BLACK;
    }

    // 判断落子是否合法,返回可翻转的坐标列表
    public List<int[]> getFlips(int x, int y, int player) {
        List<int[]> flips = new ArrayList<>();
        if (x < 0 || x >= SIZE || y < 0 || y >= SIZE || board[x][y] != EMPTY) return flips;
        int opponent = 3 - player;
        for (int d = 0; d < 8; d++) {
            List<int[]> line = new ArrayList<>();
            int nx = x + DX[d], ny = y + DY[d];
            while (nx >= 0 && nx < SIZE && ny >= 0 && ny < SIZE && board[nx][ny] == opponent) {
                line.add(new int[]{nx, ny});
                nx += DX[d]; ny += DY[d];
            }
            if (!line.isEmpty() && nx >= 0 && nx < SIZE && ny >= 0 && ny < SIZE && board[nx][ny] == player) {
                flips.addAll(line);
            }
        }
        return flips;
    }

    public List<int[]> getValidMoves(int player) {
        List<int[]> moves = new ArrayList<>();
        for (int i = 0; i < SIZE; i++)
            for (int j = 0; j < SIZE; j++)
                if (!getFlips(i, j, player).isEmpty())
                    moves.add(new int[]{i, j});
        return moves;
    }

    public void makeMove(int x, int y, int player) {
        List<int[]> flips = getFlips(x, y, player);
        board[x][y] = player;
        for (int[] f : flips) board[f[0]][f[1]] = player;
    }

    // 多维度启发式评估函数
    public int evaluate(int player) {
        int score = 0;
        int myFrontiers = 0, oppFrontiers = 0;
        int myMoves = getValidMoves(player).size();
        int oppMoves = getValidMoves(3 - player).size();

        for (int i = 0; i < SIZE; i++) {
            for (int j = 0; j < SIZE; j++) {
                if (board[i][j] == player) {
                    score += POSITION_WEIGHT[i][j];
                } else if (board[i][j] == 3 - player) {
                    score -= POSITION_WEIGHT[i][j];
                }
                // 前沿子统计
                if (board[i][j] != EMPTY) {
                    boolean isFrontier = false;
                    for (int d = 0; d < 8 && !isFrontier; d++) {
                        int ni = i + DX[d], nj = j + DY[d];
                        if (ni >= 0 && ni < SIZE && nj >= 0 && nj < SIZE && board[ni][nj] == EMPTY)
                            isFrontier = true;
                    }
                    if (isFrontier) {
                        if (board[i][j] == player) myFrontiers++;
                        else oppFrontiers++;
                    }
                }
            }
        }

        // 加权组合
        score += (myMoves - oppMoves) * 5;       // 行动力差异
        score -= (myFrontiers - oppFrontiers) * 3; // 前沿子差异(负权重)
        return score;
    }

    // Alpha-Beta剪枝的Minimax搜索
    public int[] minimax(int depth, int player, int alpha, int beta, boolean maximizing) {
        List<int[]> moves = getValidMoves(player);

        // 游戏结束或无合法移动时进行终局评估
        if (depth == 0 || moves.isEmpty()) {
            int eval = evaluate(BLACK); // 始终从BLACK视角评估
            return new int[]{-1, -1, eval};
        }

        int bestX = moves.get(0)[0], bestY = moves.get(0)[1];

        if (maximizing) {
            int maxEval = Integer.MIN_VALUE;
            for (int[] move : moves) {
                int[][] saved = saveBoard();
                makeMove(move[0], move[1], player);
                int eval = minimax(depth - 1, 3 - player, alpha, beta, false)[2];
                restoreBoard(saved);
                if (eval > maxEval) {
                    maxEval = eval;
                    bestX = move[0]; bestY = move[1];
                }
                alpha = Math.max(alpha, eval);
                if (beta <= alpha) break; // Beta剪枝
            }
            return new int[]{bestX, bestY, maxEval};
        } else {
            int minEval = Integer.MAX_VALUE;
            for (int[] move : moves) {
                int[][] saved = saveBoard();
                makeMove(move[0], move[1], player);
                int eval = minimax(depth - 1, 3 - player, alpha, beta, true)[2];
                restoreBoard(saved);
                if (eval < minEval) {
                    minEval = eval;
                    bestX = move[0]; bestY = move[1];
                }
                beta = Math.min(beta, eval);
                if (beta <= alpha) break; // Alpha剪枝
            }
            return new int[]{bestX, bestY, minEval};
        }
    }

    // 排序优化:优先搜索高价值位置(提升剪枝效率)
    public int[] minimaxSorted(int depth, int player, int alpha, int beta, boolean maximizing) {
        List<int[]> moves = getValidMoves(player);
        if (depth == 0 || moves.isEmpty()) {
            return new int[]{-1, -1, evaluate(BLACK)};
        }
        // 按位置权重排序
        moves.sort((a, b) -> Integer.compare(POSITION_WEIGHT[b[0]][b[1]], POSITION_WEIGHT[a[0]][a[1]]));
        return minimaxHelper(moves, depth, player, alpha, beta, maximizing);
    }

    private int[] minimaxHelper(List<int[]> moves, int depth, int player, int alpha, int beta, boolean maximizing) {
        int bestX = moves.get(0)[0], bestY = moves.get(0)[1];
        if (maximizing) {
            int maxEval = Integer.MIN_VALUE;
            for (int[] move : moves) {
                int[][] saved = saveBoard();
                makeMove(move[0], move[1], player);
                int eval = minimax(depth - 1, 3 - player, alpha, beta, false)[2];
                restoreBoard(saved);
                if (eval > maxEval) { maxEval = eval; bestX = move[0]; bestY = move[1]; }
                alpha = Math.max(alpha, eval);
                if (beta <= alpha) break;
            }
            return new int[]{bestX, bestY, maxEval};
        } else {
            int minEval = Integer.MAX_VALUE;
            for (int[] move : moves) {
                int[][] saved = saveBoard();
                makeMove(move[0], move[1], player);
                int eval = minimax(depth - 1, 3 - player, alpha, beta, true)[2];
                restoreBoard(saved);
                if (eval < minEval) { minEval = eval; bestX = move[0]; bestY = move[1]; }
                beta = Math.min(beta, eval);
                if (beta <= alpha) break;
            }
            return new int[]{bestX, bestY, minEval};
        }
    }

    private int[][] saveBoard() {
        int[][] copy = new int[SIZE][SIZE];
        for (int i = 0; i < SIZE; i++) copy[i] = board[i].clone();
        return copy;
    }

    private void restoreBoard(int[][] saved) {
        for (int i = 0; i < SIZE; i++) board[i] = saved[i].clone();
    }

    public void printBoard() {
        System.out.println("  0 1 2 3 4 5 6 7");
        for (int i = 0; i < SIZE; i++) {
            System.out.print(i + " ");
            for (int j = 0; j < SIZE; j++) {
                char c = board[i][j] == EMPTY ? '.' : (board[i][j] == BLACK ? 'X' : 'O');
                System.out.print(c + " ");
            }
            System.out.println();
        }
    }

    public static void main(String[] args) {
        ReversiAI game = new ReversiAI();
        game.printBoard();

        // AI执黑,深度6搜索
        int[] move = game.minimaxSorted(6, BLACK, Integer.MIN_VALUE, Integer.MAX_VALUE, true);
        System.out.println("\nAI suggests: (" + move[0] + "," + move[1] + ") eval=" + move[2]);
        game.makeMove(move[0], move[1], BLACK);
        game.printBoard();
    }
}

复杂度分析

  • 时间复杂度:Minimax搜索为O(b^d),b为分支因子(平均约8-10),d为搜索深度。Alpha-Beta剪枝在最优排序下可将复杂度降至O(b^(d/2))。深度6的搜索在实际运行中完全可接受。
  • 空间复杂度:O(d×SIZE²)用于递归栈和棋盘状态保存。

评估策略对比

评估维度 早期权重 晚期权重 说明
位置权重 高 极高 始终核心
行动力 高 低 开局重要
前沿子 中 低 避免被翻转
棋子数 忽略 极高 终局唯一标准

实战中通常根据剩余空格数动态调整权重:前30步重视位置和行动力,后34步逐渐转向稳定子和棋子数统计。深度搜索配合优质评估函数,AI可达到人类高手水平。