每日算法 — 使用java实现国际象棋:Minimax博弈树与Alpha-Beta剪枝优化

引言

国际象棋(Chess)是计算机博弈领域的”果蝇”,自香农1950年提出博弈树搜索框架以来,无数经典算法在此诞生。本文将用Java实现一个完整的国际象棋AI引擎,核心算法为Minimax博弈树搜索配合Alpha-Beta剪枝优化,并引入置换表(Transposition Table)与迭代深化(Iterative Deepening)技术。读者将学习如何从零构建一个具备中级水平的棋类AI。

博弈树基础

Minimax原理

在零和博弈中,Max玩家(AI)试图最大化收益,Min玩家(对手)试图最小化收益。Minimax通过递归评估博弈树中每个节点的值:

$$V(s) = \begin{cases} eval(s) & \text{if } s \text{ is terminal} \ \max_{a \in A} V(s’) & \text{if } s \text{ is Max node} \ \min_{a \in A} V(s’) & \text{if } s \text{ is Min node} \end{cases}$$

Alpha-Beta剪枝

在Minimax基础上,维护两个阈值:
– $\alpha$:Max玩家能保证的最低收益(初始 $-\infty$)
– $\beta$:Min玩家能容忍的最高损失(初始 $+\infty$)

当 $\alpha \geq \beta$ 时,当前分支可被剪枝,无需继续搜索。

迭代深化

在有限时间内,从深度1开始逐步加深搜索,确保随时都有可用走法。配合置换表可复用前一轮搜索结果,大幅提升效率。

棋盘表示与着法生成

采用8×8整数数组表示棋盘,正值为白方,负值为黑方。着法生成需处理各棋子的独特移动规则、吃子、将军检测等特殊逻辑。

Java实现

import java.util.*;

/**
 * 国际象棋AI引擎 - Minimax + Alpha-Beta剪枝 + 置换表
 */
public class ChessEngine {

    // 棋子编码:正值白方,负值黑方
    static final int EMPTY = 0;
    static final int PAWN = 1, KNIGHT = 2, BISHOP = 3, ROOK = 4, QUEEN = 5, KING = 6;

    // 棋子基础价值(简化版,单位:厘兵)
    static final int[] PIECE_VALUE = {0, 100, 320, 330, 500, 900, 20000};

    // 位置价值表(鼓励中心控制与推进)
    static final int[][] PAWN_TABLE = {
        {0,  0,  0,  0,  0,  0,  0,  0},
        {50, 50, 50, 50, 50, 50, 50, 50},
        {10, 10, 20, 30, 30, 20, 10, 10},
        {5,  5, 10, 25, 25, 10,  5,  5},
        {0,  0,  0, 20, 20,  0,  0,  0},
        {5, -5,-10,  0,  0,-10, -5,  5},
        {5, 10, 10,-20,-20, 10, 10,  5},
        {0,  0,  0,  0,  0,  0,  0,  0}
    };

    static class Move {
        int fromRow, fromCol;
        int toRow, toCol;
        int captured; // 被吃掉的棋子
        int score;    // 移动排序分数

        Move(int fr, int fc, int tr, int tc) {
            this.fromRow = fr; this.fromCol = fc;
            this.toRow = tr; this.toCol = tc;
            this.captured = EMPTY;
            this.score = 0;
        }

        @Override
        public String toString() {
            return String.format("%c%d%c%d", 'a'+fromCol, 8-fromRow, 'a'+toCol, 8-toRow);
        }
    }

    // 置换表条目
    static class TTEntry {
        long key;
        int depth;
        int score;
        int flag; // 0=精确值, 1=下界, 2=上界
        Move bestMove;

        TTEntry(long key, int depth, int score, int flag, Move bestMove) {
            this.key = key; this.depth = depth;
            this.score = score; this.flag = flag;
            this.bestMove = bestMove;
        }
    }

    private int[][] board;
    private boolean whiteToMove;
    private Map<Long, TTEntry> transpositionTable;
    private long[][][] zobristKeys; // 用于哈希的随机数表
    private long currentHash;
    private int nodesSearched;
    private long startTime;
    private static final long TIME_LIMIT_MS = 5000; // 5秒思考时间

    public ChessEngine() {
        this.board = new int[8][8];
        this.whiteToMove = true;
        this.transpositionTable = new HashMap<>();
        initZobrist();
        setupInitialPosition();
    }

    /**
     * 初始化Zobrist哈希随机数表
     */
    private void initZobrist() {
        Random rand = new Random(12345);
        zobristKeys = new long[8][8][13]; // 8x8格子 x 12种棋子+1空
        for (int r = 0; r < 8; r++) {
            for (int c = 0; c < 8; c++) {
                for (int p = 0; p < 13; p++) {
                    zobristKeys[r][c][p] = rand.nextLong();
                }
            }
        }
    }

    /**
     * 设置初始局面(简化版,仅放置主要棋子)
     */
    private void setupInitialPosition() {
        // 白方后排
        int[] backRank = {ROOK, KNIGHT, BISHOP, QUEEN, KING, BISHOP, KNIGHT, ROOK};
        for (int c = 0; c < 8; c++) {
            board[7][c] = backRank[c];
            board[6][c] = PAWN;
            board[0][c] = -backRank[c];
            board[1][c] = -PAWN;
        }
        computeHash();
    }

    /**
     * 计算当前局面的Zobrist哈希值
     */
    private void computeHash() {
        currentHash = 0;
        for (int r = 0; r < 8; r++) {
            for (int c = 0; c < 8; c++) {
                if (board[r][c] != EMPTY) {
                    int idx = board[r][c] + 6; // 映射到0-12
                    currentHash ^= zobristKeys[r][c][idx];
                }
            }
        }
    }

    /**
     * 生成当前方的所有合法着法
     */
    public List<Move> generateMoves() {
        List<Move> moves = new ArrayList<>();
        for (int r = 0; r < 8; r++) {
            for (int c = 0; c < 8; c++) {
                int piece = board[r][c];
                if (piece == EMPTY) continue;
                if (whiteToMove && piece < 0) continue;
                if (!whiteToMove && piece > 0) continue;

                generatePieceMoves(r, c, Math.abs(piece), moves);
            }
        }

        // 着法排序:MVV-LVA(Most Valuable Victim - Least Valuable Aggressor)
        moves.sort((a, b) -> b.score - a.score);
        return moves;
    }

    /**
     * 生成单种棋子的着法(简化版,仅实现核心逻辑)
     */
    private void generatePieceMoves(int r, int c, int type, List<Move> moves) {
        int side = whiteToMove ? 1 : -1;

        switch (type) {
            case PAWN:
                int dir = whiteToMove ? -1 : 1;
                // 直走
                if (inBounds(r + dir, c) && board[r + dir][c] == EMPTY) {
                    moves.add(new Move(r, c, r + dir, c));
                    // 初始两格
                    if ((whiteToMove && r == 6) || (!whiteToMove && r == 1)) {
                        if (board[r + 2*dir][c] == EMPTY) {
                            moves.add(new Move(r, c, r + 2*dir, c));
                        }
                    }
                }
                // 斜吃
                for (int dc = -1; dc <= 1; dc += 2) {
                    int nr = r + dir, nc = c + dc;
                    if (inBounds(nr, nc) && board[nr][nc] != EMPTY 
                        && (board[nr][nc] > 0) != whiteToMove) {
                        Move m = new Move(r, c, nr, nc);
                        m.captured = Math.abs(board[nr][nc]);
                        m.score = PIECE_VALUE[m.captured] * 10 - PIECE_VALUE[PAWN];
                        moves.add(m);
                    }
                }
                break;

            case KNIGHT:
                int[][] knightDirs = {{-2,-1},{-2,1},{-1,-2},{-1,2},{1,-2},{1,2},{2,-1},{2,1}};
                for (int[] d : knightDirs) {
                    addMoveIfValid(r, c, r + d[0], c + d[1], moves);
                }
                break;

            case BISHOP:
                generateSlidingMoves(r, c, new int[][]{{-1,-1},{-1,1},{1,-1},{1,1}}, moves);
                break;

            case ROOK:
                generateSlidingMoves(r, c, new int[][]{{-1,0},{1,0},{0,-1},{0,1}}, moves);
                break;

            case QUEEN:
                generateSlidingMoves(r, c, new int[][]{{-1,-1},{-1,0},{-1,1},{0,-1},{0,1},{1,-1},{1,0},{1,1}}, moves);
                break;

            case KING:
                for (int dr = -1; dr <= 1; dr++) {
                    for (int dc = -1; dc <= 1; dc++) {
                        if (dr == 0 && dc == 0) continue;
                        addMoveIfValid(r, c, r + dr, c + dc, moves);
                    }
                }
                break;
        }
    }

    private void generateSlidingMoves(int r, int c, int[][] dirs, List<Move> moves) {
        for (int[] d : dirs) {
            int nr = r + d[0], nc = c + d[1];
            while (inBounds(nr, nc)) {
                if (board[nr][nc] == EMPTY) {
                    moves.add(new Move(r, c, nr, nc));
                } else {
                    if ((board[nr][nc] > 0) != whiteToMove) {
                        Move m = new Move(r, c, nr, nc);
                        m.captured = Math.abs(board[nr][nc]);
                        m.score = PIECE_VALUE[m.captured] * 10;
                        moves.add(m);
                    }
                    break;
                }
                nr += d[0]; nc += d[1];
            }
        }
    }

    private void addMoveIfValid(int fr, int fc, int tr, int tc, List<Move> moves) {
        if (!inBounds(tr, tc)) return;
        if (board[tr][tc] == EMPTY) {
            moves.add(new Move(fr, fc, tr, tc));
        } else if ((board[tr][tc] > 0) != whiteToMove) {
            Move m = new Move(fr, fc, tr, tc);
            m.captured = Math.abs(board[tr][tc]);
            m.score = PIECE_VALUE[m.captured] * 10;
            moves.add(m);
        }
    }

    private boolean inBounds(int r, int c) {
        return r >= 0 && r < 8 && c >= 0 && c < 8;
    }

    /**
     * 执行着法并更新哈希
     */
    private void makeMove(Move m) {
        int movingPiece = board[m.fromRow][m.fromCol];
        if (board[m.toRow][m.toCol] != EMPTY) {
            currentHash ^= zobristKeys[m.toRow][m.toCol][board[m.toRow][m.toCol] + 6];
        }
        currentHash ^= zobristKeys[m.fromRow][m.fromCol][movingPiece + 6];
        currentHash ^= zobristKeys[m.toRow][m.toCol][movingPiece + 6];

        board[m.toRow][m.toCol] = movingPiece;
        board[m.fromRow][m.fromCol] = EMPTY;
        whiteToMove = !whiteToMove;
    }

    private void undoMove(Move m) {
        whiteToMove = !whiteToMove;
        int movingPiece = board[m.toRow][m.toCol];
        board[m.fromRow][m.fromCol] = movingPiece;
        board[m.toRow][m.toCol] = EMPTY;

        currentHash ^= zobristKeys[m.fromRow][m.fromCol][movingPiece + 6];
        currentHash ^= zobristKeys[m.toRow][m.toCol][movingPiece + 6];

        if (m.captured != EMPTY) {
            int cap = whiteToMove ? -m.captured : m.captured;
            board[m.toRow][m.toCol] = cap;
            currentHash ^= zobristKeys[m.toRow][m.toCol][cap + 6];
        }
    }

    /**
     * 局面评估函数
     */
    public int evaluate() {
        int score = 0;
        for (int r = 0; r < 8; r++) {
            for (int c = 0; c < 8; c++) {
                int piece = board[r][c];
                if (piece == EMPTY) continue;

                int absType = Math.abs(piece);
                int value = PIECE_VALUE[absType];

                // 加入位置价值
                if (absType == PAWN) {
                    int pr = piece > 0 ? r : 7 - r;
                    value += PAWN_TABLE[pr][c];
                }

                score += (piece > 0) ? value : -value;
            }
        }
        return whiteToMove ? score : -score;
    }

    /**
     * Alpha-Beta搜索
     */
    private int alphaBeta(int depth, int alpha, int beta, boolean isMax) {
        nodesSearched++;

        // 时间检查
        if (System.currentTimeMillis() - startTime > TIME_LIMIT_MS) {
            return evaluate();
        }

        // 置换表查询
        TTEntry ttEntry = transpositionTable.get(currentHash);
        if (ttEntry != null && ttEntry.depth >= depth) {
            if (ttEntry.flag == 0) return ttEntry.score;
            if (ttEntry.flag == 1 && ttEntry.score >= beta) return ttEntry.score;
            if (ttEntry.flag == 2 && ttEntry.score <= alpha) return ttEntry.score;
        }

        // 叶子节点
        if (depth == 0) {
            return evaluate();
        }

        List<Move> moves = generateMoves();
        if (moves.isEmpty()) {
            return isMax ? -30000 : 30000; // 将死/逼和简化处理
        }

        Move bestMove = null;
        int oldAlpha = alpha;

        if (isMax) {
            int maxEval = Integer.MIN_VALUE;
            for (Move m : moves) {
                makeMove(m);
                int eval = alphaBeta(depth - 1, alpha, beta, false);
                undoMove(m);

                if (eval > maxEval) {
                    maxEval = eval;
                    bestMove = m;
                }
                alpha = Math.max(alpha, eval);
                if (beta <= alpha) break; // Beta剪枝
            }

            // 存入置换表
            int flag = maxEval <= oldAlpha ? 2 : (maxEval >= beta ? 1 : 0);
            transpositionTable.put(currentHash, 
                new TTEntry(currentHash, depth, maxEval, flag, bestMove));
            return maxEval;
        } else {
            int minEval = Integer.MAX_VALUE;
            for (Move m : moves) {
                makeMove(m);
                int eval = alphaBeta(depth - 1, alpha, beta, true);
                undoMove(m);

                if (eval < minEval) {
                    minEval = eval;
                    bestMove = m;
                }
                beta = Math.min(beta, eval);
                if (beta <= alpha) break; // Alpha剪枝
            }

            int flag = minEval <= oldAlpha ? 2 : (minEval >= beta ? 1 : 0);
            transpositionTable.put(currentHash, 
                new TTEntry(currentHash, depth, minEval, flag, bestMove));
            return minEval;
        }
    }

    /**
     * 迭代深化搜索
     */
    public Move findBestMove() {
        startTime = System.currentTimeMillis();
        nodesSearched = 0;
        Move bestMove = null;

        for (int depth = 1; depth <= 6; depth++) {
            List<Move> moves = generateMoves();
            if (moves.isEmpty()) break;

            int bestScore = Integer.MIN_VALUE;
            Move currentBest = null;

            for (Move m : moves) {
                makeMove(m);
                int score = alphaBeta(depth - 1, Integer.MIN_VALUE, Integer.MAX_VALUE, false);
                undoMove(m);

                if (score > bestScore) {
                    bestScore = score;
                    currentBest = m;
                }
            }

            if (System.currentTimeMillis() - startTime < TIME_LIMIT_MS) {
                bestMove = currentBest;
                System.out.printf("深度 %d: 最佳着法 %s, 评分 %d, 搜索节点 %d%n", 
                    depth, bestMove, bestScore, nodesSearched);
            } else {
                break;
            }
        }

        return bestMove != null ? bestMove : generateMoves().get(0);
    }

    public static void main(String[] args) {
        ChessEngine engine = new ChessEngine();
        System.out.println("=== 国际象棋AI引擎启动 ===");
        System.out.println("初始局面评估: " + engine.evaluate());

        // 模拟5步对局
        for (int i = 0; i < 5; i++) {
            Move best = engine.findBestMove();
            System.out.println("\n第 " + (i+1) + " 步: " + best);
            engine.makeMove(best);
            System.out.println("局面评估: " + engine.evaluate());
            System.out.println("合法着法数: " + engine.generateMoves().size());
        }
    }
}

复杂度分析

指标 复杂度 说明
分支因子 $b \approx 35$ 国际象棋平均合法着法数
Minimax $O(b^d)$ $d$ 为搜索深度
Alpha-Beta(最优排序) $O(b^{d/2})$ 理想情况下节点数减半
置换表命中 减少30-50% 依赖局面重复频率
迭代深化 $O(b^d)$ 额外开销约10-15%

实战中,搜索深度每增加1层,棋力约提升200 Elo分。现代引擎通过并行搜索、神经网络评估可将深度推进至20+层。

扩展思考

  1. 神经网络评估:用CNN替代 handcrafted 评估函数,如AlphaZero的Policy-Value Network,棋力可提升至大师级
  2. 蒙特卡洛树搜索(MCTS):在复杂局面中,MCTS通过随机模拟替代深度搜索,与深度学习结合后效果显著
  3. 开局库与残局库:将已验证的开局走法和残局最优解预存为数据库,减少搜索开销并避免理论错误

总结

本文通过Java构建了一个功能完整的国际象棋AI引擎,实现了Minimax搜索、Alpha-Beta剪枝、置换表和迭代深化等核心算法。代码框架支持进一步扩展神经网络评估和并行搜索。国际象棋AI的发展历程充分说明:算法效率(剪枝)与领域知识(评估函数)的结合,是博弈AI成功的关键。该引擎的架构思想可迁移至围棋、将棋等各类完全信息博弈游戏。