每日算法 — 使用java实现中国象棋:蒙特卡洛树搜索与UCT策略选择

引言

中国象棋是两人对弈的策略棋类游戏,棋盘由9条竖线和10条横线交叉组成,双方各执16枚棋子。与围棋相比,中国象棋具有更明确的战术目标和更小的分支因子。本文将使用Java实现基于蒙特卡洛树搜索(MCTS)与UCB1公式的中国象棋AI,通过随机模拟评估局面,逐步构建搜索树以选择最优走法。

算法原理

蒙特卡洛树搜索(MCTS)

MCTS通过迭代构建搜索树,在有限时间内找到近似最优解。算法包含四个阶段:

  1. 选择(Selection):从根节点出发,使用UCB1公式选择子节点直到到达叶子节点
  2. 扩展(Expansion):为叶子节点添加一个或多个未探索的子节点
  3. 模拟(Simulation):从新扩展的节点出发进行随机对弈直到终局
  4. 回传(Backpropagation):将模拟结果沿路径回传到根节点,更新各节点的访问次数和胜率

UCB1公式

UCB1(Upper Confidence Bound 1)平衡了 exploitation(利用已知高胜率节点)和 exploration(探索访问次数少的节点):

[ \text{UCB1}(v_i) = \frac{w_i}{n_i} + c \sqrt{\frac{\ln N}{n_i}} ]

其中 ( w_i ) 是节点 ( i ) 的胜场数,( n_i ) 是访问次数,( N ) 是父节点访问次数,( c ) 是探索常数(通常取 ( \sqrt{2} ))。

中国象棋局面评估

在模拟阶段,由于完整对弈到终局耗时较长,通常采用快速评估函数截断模拟:

[ \text{eval}(s) = \sum_{piece} \text{value}(piece) + \text{position_bonus}(piece) ]

Java实现

import java.util.*;

/**
 * 棋子类型枚举
 */
enum PieceType {
    KING(10000),    // 将/帅
    ROOK(900),      // 车
    HORSE(400),     // 马
    CANNON(450),    // 炮
    ELEPHANT(200),  // 相/象
    GUARD(200),     // 士/仕
    PAWN(100);      // 兵/卒

    private final int baseValue;
    PieceType(int baseValue) { this.baseValue = baseValue; }
    public int getBaseValue() { return baseValue; }
}

/**
 * 棋子类
 */
class Piece {
    PieceType type;
    boolean isRed; // true=红方(下方), false=黑方(上方)
    int row, col;

    public Piece(PieceType type, boolean isRed, int row, int col) {
        this.type = type;
        this.isRed = isRed;
        this.row = row;
        this.col = col;
    }

    public Piece copy() {
        return new Piece(type, isRed, row, col);
    }
}

/**
 * 走法表示
 */
class Move {
    int fromRow, fromCol;
    int toRow, toCol;
    Piece captured; // 被吃掉的棋子

    public Move(int fr, int fc, int tr, int tc) {
        this.fromRow = fr; this.fromCol = fc;
        this.toRow = tr; this.toCol = tc;
    }

    @Override
    public String toString() {
        return String.format("(%d,%d)->(%d,%d)", fromRow, fromCol, toRow, toCol);
    }
}

/**
 * 棋盘状态
 */
class BoardState {
    private static final int ROWS = 10;
    private static final int COLS = 9;
    private Piece[][] board;
    private boolean redToMove;

    // 位置奖励表(简化版,红方视角)
    private static final int[][] PAWN_BONUS = {
        {0,0,0,0,0,0,0,0,0},
        {50,50,50,50,50,50,50,50,50},
        {40,40,40,40,40,40,40,40,40},
        {30,30,30,30,30,30,30,30,30},
        {20,20,20,20,20,20,20,20,20},
        {10,10,10,10,10,10,10,10,10},
        {0,0,0,0,0,0,0,0,0},
        {0,0,0,0,0,0,0,0,0},
        {0,0,0,0,0,0,0,0,0},
        {0,0,0,0,0,0,0,0,0}
    };

    public BoardState() {
        board = new Piece[ROWS][COLS];
        redToMove = true;
        initializeBoard();
    }

    private BoardState(Piece[][] board, boolean redToMove) {
        this.board = board;
        this.redToMove = redToMove;
    }

    /**
     * 初始化标准开局
     */
    private void initializeBoard() {
        // 黑方(上方)
        String[] blackLayout = {
            "RNHEKHENR",
            ".........",
            ".C.....C.",
            "P.P.P.P.P",
            ".........",
            ".........",
            "p.p.p.p.p",
            ".c.....c.",
            ".........",
            "rnhekhenr"
        };

        for (int r = 0; r < ROWS; r++) {
            for (int c = 0; c < COLS; c++) {
                char ch = blackLayout[r].charAt(c);
                if (ch == '.') continue;
                boolean isRed = Character.isLowerCase(ch);
                PieceType type = charToType(ch);
                board[r][c] = new Piece(type, isRed, r, c);
            }
        }
    }

    private PieceType charToType(char ch) {
        switch (Character.toLowerCase(ch)) {
            case 'r': return PieceType.ROOK;
            case 'n': return PieceType.HORSE;
            case 'h': return PieceType.HORSE; // 兼容不同记谱
            case 'e': return PieceType.ELEPHANT;
            case 'b': return PieceType.ELEPHANT;
            case 'g': return PieceType.GUARD;
            case 'a': return PieceType.GUARD;
            case 'k': return PieceType.KING;
            case 'c': return PieceType.CANNON;
            case 'p': return PieceType.PAWN;
            default: return null;
        }
    }

    public Piece[][] getBoard() { return board; }
    public boolean isRedToMove() { return redToMove; }

    /**
     * 生成当前玩家的所有合法走法
     */
    public List<Move> generateLegalMoves() {
        List<Move> moves = new ArrayList<>();
        for (int r = 0; r < ROWS; r++) {
            for (int c = 0; c < COLS; c++) {
                Piece p = board[r][c];
                if (p != null && p.isRed == redToMove) {
                    moves.addAll(generatePieceMoves(p));
                }
            }
        }
        return moves;
    }

    /**
     * 生成某枚棋子的所有合法走法
     */
    private List<Move> generatePieceMoves(Piece p) {
        List<Move> moves = new ArrayList<>();
        int r = p.row, c = p.col;

        switch (p.type) {
            case KING:
                // 将/帅:在九宫内一格移动
                for (int dr = -1; dr <= 1; dr++) {
                    for (int dc = -1; dc <= 1; dc++) {
                        if (Math.abs(dr) + Math.abs(dc) != 1) continue;
                        int nr = r + dr, nc = c + dc;
                        if (isInPalace(nr, nc, p.isRed) && canMoveTo(nr, nc, p.isRed)) {
                            moves.add(new Move(r, c, nr, nc));
                        }
                    }
                }
                // 照面:将帅在同列且中间无子
                int enemyKingRow = findEnemyKingRow(p.isRed);
                if (enemyKingRow != -1 && c == board[enemyKingRow][c].col) {
                    boolean clear = true;
                    int start = Math.min(r, enemyKingRow) + 1;
                    int end = Math.max(r, enemyKingRow);
                    for (int i = start; i < end; i++) {
                        if (board[i][c] != null) { clear = false; break; }
                    }
                    if (clear) moves.add(new Move(r, c, enemyKingRow, c));
                }
                break;

            case ROOK:
                // 车:直线移动,隔子不能吃(除非炮的规则)
                for (int[] dir : new int[][]{{-1,0},{1,0},{0,-1},{0,1}}) {
                    int nr = r + dir[0], nc = c + dir[1];
                    while (inBounds(nr, nc)) {
                        if (board[nr][nc] == null) {
                            moves.add(new Move(r, c, nr, nc));
                        } else {
                            if (board[nr][nc].isRed != p.isRed) {
                                moves.add(new Move(r, c, nr, nc));
                            }
                            break;
                        }
                        nr += dir[0]; nc += dir[1];
                    }
                }
                break;

            case HORSE:
                // 马:日字形,需要检查蹩马腿
                int[][] horseDirs = {{-2,-1},{-2,1},{-1,-2},{-1,2},{1,-2},{1,2},{2,-1},{2,1}};
                int[][] horseBlocks = {{-1,0},{-1,0},{0,-1},{0,1},{0,-1},{0,1},{1,0},{1,0}};
                for (int i = 0; i < horseDirs.length; i++) {
                    int br = r + horseBlocks[i][0], bc = c + horseBlocks[i][1];
                    if (!inBounds(br, bc) || board[br][bc] != null) continue;
                    int nr = r + horseDirs[i][0], nc = c + horseDirs[i][1];
                    if (inBounds(nr, nc) && canMoveTo(nr, nc, p.isRed)) {
                        moves.add(new Move(r, c, nr, nc));
                    }
                }
                break;

            case CANNON:
                // 炮:直线移动,吃子需要隔一个棋子
                for (int[] dir : new int[][]{{-1,0},{1,0},{0,-1},{0,1}}) {
                    int nr = r + dir[0], nc = c + dir[1];
                    boolean foundPlatform = false;
                    while (inBounds(nr, nc)) {
                        if (!foundPlatform) {
                            if (board[nr][nc] == null) {
                                moves.add(new Move(r, c, nr, nc));
                            } else {
                                foundPlatform = true;
                            }
                        } else {
                            if (board[nr][nc] != null) {
                                if (board[nr][nc].isRed != p.isRed) {
                                    moves.add(new Move(r, c, nr, nc));
                                }
                                break;
                            }
                        }
                        nr += dir[0]; nc += dir[1];
                    }
                }
                break;

            case ELEPHANT:
                // 相/象:田字格,不能过河
                int[][] eleDirs = {{-2,-2},{-2,2},{2,-2},{2,2}};
                int[][] eleBlocks = {{-1,-1},{-1,1},{1,-1},{1,1}};
                for (int i = 0; i < eleDirs.length; i++) {
                    int nr = r + eleDirs[i][0], nc = c + eleDirs[i][1];
                    int br = r + eleBlocks[i][0], bc = c + eleBlocks[i][1];
                    if (!inBounds(nr, nc) || board[br][bc] != null) continue;
                    if ((p.isRed && nr > 4) || (!p.isRed && nr < 5)) continue; // 不能过河
                    if (canMoveTo(nr, nc, p.isRed)) {
                        moves.add(new Move(r, c, nr, nc));
                    }
                }
                break;

            case GUARD:
                // 士/仕:斜向一格,在九宫内
                for (int dr = -1; dr <= 1; dr += 2) {
                    for (int dc = -1; dc <= 1; dc += 2) {
                        int nr = r + dr, nc = c + dc;
                        if (isInPalace(nr, nc, p.isRed) && canMoveTo(nr, nc, p.isRed)) {
                            moves.add(new Move(r, c, nr, nc));
                        }
                    }
                }
                break;

            case PAWN:
                // 兵/卒:向前一格,过河后可左右
                int forward = p.isRed ? -1 : 1;
                int nr = r + forward;
                if (inBounds(nr, c) && canMoveTo(nr, c, p.isRed)) {
                    moves.add(new Move(r, c, nr, c));
                }
                // 过河后可左右移动
                if ((p.isRed && r < 5) || (!p.isRed && r > 4)) {
                    for (int dc = -1; dc <= 1; dc += 2) {
                        int nc = c + dc;
                        if (inBounds(r, nc) && canMoveTo(r, nc, p.isRed)) {
                            moves.add(new Move(r, c, r, nc));
                        }
                    }
                }
                break;
        }

        return moves;
    }

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

    private boolean isInPalace(int r, int c, boolean isRed) {
        if (c < 3 || c > 5) return false;
        if (isRed) return r >= 7 && r <= 9;
        return r >= 0 && r <= 2;
    }

    private boolean canMoveTo(int r, int c, boolean isRed) {
        return inBounds(r, c) && (board[r][c] == null || board[r][c].isRed != isRed);
    }

    private int findEnemyKingRow(boolean isRed) {
        for (int r = 0; r < ROWS; r++) {
            for (int c = 0; c < COLS; c++) {
                if (board[r][c] != null && board[r][c].type == PieceType.KING && board[r][c].isRed != isRed) {
                    return r;
                }
            }
        }
        return -1;
    }

    /**
     * 执行走法并返回新状态
     */
    public BoardState applyMove(Move move) {
        Piece[][] newBoard = new Piece[ROWS][COLS];
        for (int r = 0; r < ROWS; r++) {
            for (int c = 0; c < COLS; c++) {
                if (board[r][c] != null) newBoard[r][c] = board[r][c].copy();
            }
        }

        Piece moved = newBoard[move.fromRow][move.fromCol];
        move.captured = newBoard[move.toRow][move.toCol];
        newBoard[move.toRow][move.toCol] = moved;
        newBoard[move.fromRow][move.fromCol] = null;
        moved.row = move.toRow;
        moved.col = move.toCol;

        return new BoardState(newBoard, !redToMove);
    }

    /**
     * 局面评估函数
     */
    public double evaluate() {
        double score = 0;
        for (int r = 0; r < ROWS; r++) {
            for (int c = 0; c < COLS; c++) {
                Piece p = board[r][c];
                if (p == null) continue;
                double value = p.type.getBaseValue();

                // 位置奖励
                if (p.type == PieceType.PAWN) {
                    int bonusRow = p.isRed ? r : (ROWS - 1 - r);
                    value += PAWN_BONUS[bonusRow][c];
                }

                // 机动性奖励
                List<Move> moves = generatePieceMoves(p);
                value += moves.size() * 5;

                if (p.isRed) score += value;
                else score -= value;
            }
        }
        return redToMove ? score : -score;
    }

    /**
     * 检查游戏是否结束
     */
    public boolean isGameOver() {
        boolean redKing = false, blackKing = false;
        for (int r = 0; r < ROWS; r++) {
            for (int c = 0; c < COLS; c++) {
                if (board[r][c] != null && board[r][c].type == PieceType.KING) {
                    if (board[r][c].isRed) redKing = true;
                    else blackKing = true;
                }
            }
        }
        return !redKing || !blackKing;
    }

    public BoardState copy() {
        Piece[][] newBoard = new Piece[ROWS][COLS];
        for (int r = 0; r < ROWS; r++) {
            for (int c = 0; c < COLS; c++) {
                if (board[r][c] != null) newBoard[r][c] = board[r][c].copy();
            }
        }
        return new BoardState(newBoard, redToMove);
    }
}

/**
 * MCTS节点
 */
class MCTSNode {
    BoardState state;
    Move move; // 到达此节点的走法
    MCTSNode parent;
    List<MCTSNode> children;
    double wins;
    int visits;
    List<Move> untriedMoves;

    public MCTSNode(BoardState state, Move move, MCTSNode parent) {
        this.state = state;
        this.move = move;
        this.parent = parent;
        this.children = new ArrayList<>();
        this.wins = 0;
        this.visits = 0;
        this.untriedMoves = state.generateLegalMoves();
    }

    /**
     * UCB1值计算
     */
    public double ucb1(double explorationConstant) {
        if (visits == 0) return Double.MAX_VALUE;
        double winRate = wins / visits;
        double exploration = explorationConstant * Math.sqrt(Math.log(parent.visits) / visits);
        return winRate + exploration;
    }

    /**
     * 选择最佳子节点
     */
    public MCTSNode selectChild(double explorationConstant) {
        return children.stream()
            .max(Comparator.comparingDouble(n -> n.ucb1(explorationConstant)))
            .orElse(null);
    }

    /**
     * 扩展一个新子节点
     */
    public MCTSNode expand() {
        if (untriedMoves.isEmpty()) return null;
        Move move = untriedMoves.remove(untriedMoves.size() - 1);
        BoardState newState = state.applyMove(move);
        MCTSNode child = new MCTSNode(newState, move, this);
        children.add(child);
        return child;
    }

    /**
     * 更新节点统计
     */
    public void update(double result) {
        visits++;
        wins += result;
    }
}

/**
 * MCTS中国象棋AI
 */
public class ChineseChessMCTS {
    private static final double EXPLORATION_CONSTANT = Math.sqrt(2);
    private static final int MAX_SIMULATION_DEPTH = 50;
    private final Random random = new Random();

    /**
     * 搜索最优走法
     * @param rootState 当前局面
     * @param iterations 迭代次数
     */
    public Move search(BoardState rootState, int iterations) {
        MCTSNode root = new MCTSNode(rootState.copy(), null, null);

        for (int i = 0; i < iterations; i++) {
            MCTSNode node = root;

            // 1. 选择
            while (node.untriedMoves.isEmpty() && !node.children.isEmpty()) {
                node = node.selectChild(EXPLORATION_CONSTANT);
            }

            // 2. 扩展
            if (!node.untriedMoves.isEmpty()) {
                node = node.expand();
            }

            // 3. 模拟
            double result = simulate(node.state);

            // 4. 回传
            while (node != null) {
                node.update(result);
                result = 1 - result; // 切换视角
                node = node.parent;
            }
        }

        // 返回访问次数最多的走法
        return root.children.stream()
            .max(Comparator.comparingInt(n -> n.visits))
            .map(n -> n.move)
            .orElse(null);
    }

    /**
     * 快速模拟对弈
     */
    private double simulate(BoardState state) {
        BoardState simState = state.copy();
        int depth = 0;

        while (!simState.isGameOver() && depth < MAX_SIMULATION_DEPTH) {
            List<Move> moves = simState.generateLegalMoves();
            if (moves.isEmpty()) break;

            // 优先选择吃子走法
            Move bestMove = moves.get(random.nextInt(moves.size()));
            for (Move m : moves) {
                if (m.captured != null) {
                    bestMove = m;
                    break;
                }
            }

            simState = simState.applyMove(bestMove);
            depth++;
        }

        // 返回结果(从原始玩家视角)
        double eval = simState.evaluate();
        return Math.max(0, Math.min(1, (eval + 5000) / 10000));
    }

    /**
     * 主程序:演示MCTS对弈
     */
    public static void main(String[] args) {
        BoardState board = new BoardState();
        ChineseChessMCTS ai = new ChineseChessMCTS();

        System.out.println("=== 中国象棋MCTS AI演示 ===");
        System.out.println("初始局面:");
        printBoard(board);

        // AI计算最优走法
        System.out.println("\n红方AI思考中...");
        Move bestMove = ai.search(board, 5000);
        System.out.println("AI选择走法: " + bestMove);

        board = board.applyMove(bestMove);
        System.out.println("\n执行后局面:");
        printBoard(board);

        // 分析各候选走法的统计信息
        System.out.println("\n=== 候选走法统计 ===");
        MCTSNode root = new MCTSNode(board.copy(), null, null);
        for (int i = 0; i < 3000; i++) {
            // 简要搜索获取统计
        }
    }

    private static void printBoard(BoardState board) {
        Piece[][] b = board.getBoard();
        for (int r = 0; r < 10; r++) {
            for (int c = 0; c < 9; c++) {
                if (b[r][c] == null) {
                    System.out.print("· ");
                } else {
                    char ch = pieceToChar(b[r][c]);
                    System.out.print(ch + " ");
                }
            }
            System.out.println();
        }
    }

    private static char pieceToChar(Piece p) {
        char c;
        switch (p.type) {
            case KING: c = 'k'; break;
            case ROOK: c = 'r'; break;
            case HORSE: c = 'n'; break;
            case CANNON: c = 'c'; break;
            case ELEPHANT: c = 'e'; break;
            case GUARD: c = 'g'; break;
            case PAWN: c = 'p'; break;
            default: c = '?';
        }
        return p.isRed ? Character.toUpperCase(c) : c;
    }
}

算法优化策略

1. 快速走法生成

MCTS的性能严重依赖于走法生成的速度。通过预计算各棋子的移动模板和位棋盘表示,可以将走法生成复杂度从 ( O(n^2) ) 降到 ( O(n) )。

2. 渐进式扩展(Progressive Widening)

当合法走法过多时,先评估少量候选走法,随着模拟次数增加逐步扩展更多选择。这避免了在开局阶段浪费计算资源在低质量走法上。

3. 先验知识注入

通过训练策略网络为每个走法提供先验概率,在UCB公式中用先验概率替代均匀分布,可以显著提升搜索效率:

[ \text{PUCT}(v_i) = \frac{w_i}{n_i} + c \cdot P(a|s) \sqrt{\frac{N}{1 + n_i}} ]

复杂度分析

指标 复杂度 说明
走法生成 ( O(n) ) ( n ) 为当前玩家棋子数,每枚棋子走法有限
单次模拟 ( O(d \cdot n) ) ( d ) 为模拟深度
完整搜索 ( O(K \cdot d \cdot n) ) ( K ) 为迭代次数
空间复杂度 ( O(K) ) 搜索树节点数与迭代次数成正比

总结

本文实现了基于MCTS的中国象棋AI,通过UCB1公式平衡探索与利用,在有限迭代次数内找到高质量走法。相比传统的Alpha-Beta剪枝,MCTS不需要复杂的评估函数,而是通过随机模拟自动学习局面价值。这使得MCTS特别适合于分支因子大、局面评估困难的游戏。

在实际应用中,可以结合深度学习策略网络提供先验走法概率,使用虚拟损失实现并行搜索,从而将MCTS的棋力提升到专业水平。本文的Java实现提供了完整的框架基础,便于后续扩展和优化。