每日算法 — 使用java实现跳棋:Minimax博弈树与Alpha-Beta剪枝

跳棋(Checkers)是一款流传数百年的经典策略棋类游戏,规则简洁却蕴含深厚的博弈智慧。本文将用Java从零实现一个带AI的跳棋程序,核心围绕Minimax博弈树搜索Alpha-Beta剪枝两大经典算法,并设计一套完整的局面评估体系,让AI能够做出有策略深度的人类级对弈决策。

一、跳棋规则与算法建模

跳棋在8×8棋盘上进行,双方各12枚棋子分列于深色格中。核心规则如下:

  • 移动:普通棋子只能向对角线前方移动一格
  • 吃子:若敌方棋子相邻且其后方为空,可跃过该子并将其移除(强制吃子优先)
  • 升王:棋子抵达对方底线后升级为”王”,可前后自由移动与吃子
  • 连跳:一次吃子后若仍可继续吃,必须继续(多重吃子)

算法层面,我们需要建模三个核心模块:棋盘状态表示合法移动生成器AI决策引擎

二、棋盘表示与数据结构

采用一维数组索引0-63表示64个格子,辅以位掩码快速判断深色格子。每个格子的状态用枚举表示。

public enum Piece {
    EMPTY,      // 空格
    RED,        // 红方普通棋子
    RED_KING,   // 红方王
    BLACK,      // 黑方普通棋子
    BLACK_KING  // 黑方王
}

public class Board {
    private static final int SIZE = 8;
    private Piece[] squares = new Piece[64];

    // 深色格子掩码:仅奇数行之奇数列 + 偶数行之偶数列(0索引)
    public static boolean isDarkSquare(int row, int col) {
        return (row + col) % 2 == 1;
    }

    public Board() {
        // 初始化:黑方在0-2行,红方在5-7行,均只在深色格子上
        for (int r = 0; r < 3; r++) {
            for (int c = 0; c < SIZE; c++) {
                if (isDarkSquare(r, c)) squares[r * SIZE + c] = Piece.BLACK;
            }
        }
        for (int r = 5; r < 8; r++) {
            for (int c = 0; c < SIZE; c++) {
                if (isDarkSquare(r, c)) squares[r * SIZE + c] = Piece.RED;
            }
        }
        // 其余为空
        for (int i = 0; i < 64; i++) {
            if (squares[i] == null) squares[i] = Piece.EMPTY;
        }
    }

    public Piece get(int row, int col) {
        return squares[row * SIZE + col];
    }

    public void set(int row, int col, Piece p) {
        squares[row * SIZE + col] = p;
    }

    public Board copy() {
        Board b = new Board();
        b.squares = this.squares.clone();
        return b;
    }
}

三、合法移动生成器

跳棋的合法移动生成是AI的基石,需处理普通移动、吃子移动以及强制吃子规则。若存在吃子移动,则普通移动被禁止。

public class Move {
    public int fromRow, fromCol;
    public int toRow, toCol;
    public boolean isCapture;       // 是否为吃子
    public List<int[]> captures;    // 被吃的棋子坐标序列(支持连跳)

    public Move(int fr, int fc, int tr, int tc) {
        this.fromRow = fr; this.fromCol = fc;
        this.toRow = tr; this.toCol = tc;
        this.captures = new ArrayList<>();
        this.isCapture = false;
    }
}

public class MoveGenerator {

    // 为指定颜色生成所有合法移动
    public static List<Move> generateMoves(Board board, boolean isRed) {
        List<Move> captures = new ArrayList<>();
        List<Move> normals = new ArrayList<>();

        for (int r = 0; r < 8; r++) {
            for (int c = 0; c < 8; c++) {
                Piece p = board.get(r, c);
                if (p == Piece.EMPTY) continue;
                boolean pieceIsRed = (p == Piece.RED || p == Piece.RED_KING);
                if (pieceIsRed != isRed) continue;

                boolean isKing = (p == Piece.RED_KING || p == Piece.BLACK_KING);
                generateForPiece(board, r, c, isRed, isKing, captures, normals);
            }
        }

        // 跳棋规则:若有吃子移动,必须执行吃子
        return captures.isEmpty() ? normals : captures;
    }

    private static void generateForPiece(Board b, int r, int c, 
                                         boolean isRed, boolean isKing,
                                         List<Move> caps, List<Move> norms) {
        // 普通棋子只能向前(红方向下,黑方向上)
        int[] directions = isKing ? new int[]{-1, 1} : 
                           (isRed ? new int[]{1} : new int[]{-1});

        for (int dr : directions) {
            for (int dc : new int[]{-1, 1}) {
                int nr = r + dr, nc = c + dc;
                // 普通移动
                if (inBounds(nr, nc) && b.get(nr, nc) == Piece.EMPTY) {
                    norms.add(new Move(r, c, nr, nc));
                }
                // 吃子移动:目标格隔一格
                int jr = r + 2 * dr, jc = c + 2 * dc;
                if (inBounds(jr, jc) && b.get(jr, jc) == Piece.EMPTY) {
                    Piece mid = b.get(nr, nc);
                    boolean midIsRed = (mid == Piece.RED || mid == Piece.RED_KING);
                    boolean midIsBlack = (mid == Piece.BLACK || mid == Piece.BLACK_KING);
                    if ((isRed && midIsBlack) || (!isRed && midIsRed)) {
                        Move m = new Move(r, c, jr, jc);
                        m.isCapture = true;
                        m.captures.add(new int[]{nr, nc});
                        caps.add(m);
                    }
                }
            }
        }
    }

    // 连跳递归扩展:在一次吃子后继续搜索可吃的路径
    public static List<Move> expandCaptures(Board board, Move move, boolean isRed) {
        List<Move> results = new ArrayList<>();
        Board simulated = simulateMove(board, move);
        int r = move.toRow, c = move.toCol;
        Piece p = simulated.get(r, c);
        boolean isKing = (p == Piece.RED_KING || p == Piece.BLACK_KING);

        boolean hasMore = false;
        int[] directions = isKing ? new int[]{-1, 1} : 
                           (isRed ? new int[]{1} : new int[]{-1});

        for (int dr : directions) {
            for (int dc : new int[]{-1, 1}) {
                int nr = r + dr, nc = c + dc;
                int jr = r + 2 * dr, jc = c + 2 * dc;
                if (inBounds(jr, jc) && simulated.get(jr, jc) == Piece.EMPTY) {
                    Piece mid = simulated.get(nr, nc);
                    boolean midIsRed = (mid == Piece.RED || mid == Piece.RED_KING);
                    boolean midIsBlack = (mid == Piece.BLACK || mid == Piece.BLACK_KING);
                    if ((isRed && midIsBlack) || (!isRed && midIsRed)) {
                        hasMore = true;
                        Move next = new Move(move.fromRow, move.fromCol, jr, jc);
                        next.isCapture = true;
                        next.captures.addAll(move.captures);
                        next.captures.add(new int[]{nr, nc});
                        results.addAll(expandCaptures(simulated, next, isRed));
                    }
                }
            }
        }

        if (!hasMore) {
            results.add(move);
        }
        return results;
    }

    public static Board simulateMove(Board b, Move m) {
        Board nb = b.copy();
        Piece p = nb.get(m.fromRow, m.fromCol);
        nb.set(m.fromRow, m.fromCol, Piece.EMPTY);

        // 升王判定
        if (p == Piece.RED && m.toRow == 7) p = Piece.RED_KING;
        if (p == Piece.BLACK && m.toRow == 0) p = Piece.BLACK_KING;
        nb.set(m.toRow, m.toCol, p);

        // 移除被吃棋子
        for (int[] cap : m.captures) {
            nb.set(cap[0], cap[1], Piece.EMPTY);
        }
        return nb;
    }

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

四、局面评估函数

评估函数是跳棋AI的灵魂,决定了AI对”好局面”的理解。我们采用加权多特征模型:

public class Evaluator {
    // 各特征权重(经大量对弈调优)
    private static final int PIECE_VALUE = 100;      // 普通棋子基础分
    private static final int KING_VALUE = 250;       // 王的价值更高
    private static final int BACK_RANK = 10;         // 底线保护 bonus
    private static final int CENTER_BONUS = 5;       // 中心控制 bonus
    private static final int MOBILITY_WEIGHT = 3;    // 行动力权重
    private static final int CAPTURE_THREAT = 15;    // 威胁吃子 bonus

    // 中心位置权重表(4x4对称,映射到8x8深色格)
    private static final int[][] CENTER_WEIGHT = {
        {0, 1, 1, 0},
        {1, 2, 2, 1},
        {1, 2, 2, 1},
        {0, 1, 1, 0}
    };

    public static int evaluate(Board board) {
        int redScore = 0, blackScore = 0;
        int redMobility = 0, blackMobility = 0;

        for (int r = 0; r < 8; r++) {
            for (int c = 0; c < 8; c++) {
                Piece p = board.get(r, c);
                if (p == Piece.EMPTY) continue;

                int value = (p == Piece.RED_KING || p == Piece.BLACK_KING) 
                             ? KING_VALUE : PIECE_VALUE;
                int centerBonus = CENTER_WEIGHT[r / 2][c / 2] * CENTER_BONUS;
                int backRank = (p == Piece.RED && r == 0) || (p == Piece.BLACK && r == 7) 
                                ? BACK_RANK : 0;

                if (p == Piece.RED || p == Piece.RED_KING) {
                    redScore += value + centerBonus + backRank;
                } else {
                    blackScore += value + centerBonus + backRank;
                }
            }
        }

        // 行动力评估:可行走法数量
        redMobility = MoveGenerator.generateMoves(board, true).size();
        blackMobility = MoveGenerator.generateMoves(board, false).size();

        int mobilityScore = (redMobility - blackMobility) * MOBILITY_WEIGHT;

        // 红方为正,黑方为负(AI作为黑方时取反)
        return (redScore - blackScore) + mobilityScore;
    }

    // 终局判定:若某方无合法移动,则判负
    public static boolean isGameOver(Board board) {
        return MoveGenerator.generateMoves(board, true).isEmpty() 
            || MoveGenerator.generateMoves(board, false).isEmpty();
    }
}

五、Minimax博弈树搜索

Minimax是双人零和博弈的经典决策算法。AI模拟双方轮流走子,在搜索树深处用评估函数给出局面分值,再反向推导当前最佳走法。

public class MinimaxAI {
    private int maxDepth;
    private boolean isRed; // AI执哪一方

    public MinimaxAI(int depth, boolean playsRed) {
        this.maxDepth = depth;
        this.isRed = playsRed;
    }

    public Move findBestMove(Board board) {
        List<Move> moves = MoveGenerator.generateMoves(board, isRed);
        if (moves.isEmpty()) return null;

        Move bestMove = null;
        int bestValue = isRed ? Integer.MIN_VALUE : Integer.MAX_VALUE;

        for (Move m : moves) {
            Board next = MoveGenerator.simulateMove(board, m);
            int value = minimax(next, maxDepth - 1, !isRed);

            if (isRed && value > bestValue) {
                bestValue = value;
                bestMove = m;
            } else if (!isRed && value < bestValue) {
                bestValue = value;
                bestMove = m;
            }
        }
        return bestMove;
    }

    private int minimax(Board board, int depth, boolean currentRed) {
        if (depth == 0 || Evaluator.isGameOver(board)) {
            return Evaluator.evaluate(board);
        }

        List<Move> moves = MoveGenerator.generateMoves(board, currentRed);
        if (moves.isEmpty()) {
            // 无合法移动 = 判负
            return currentRed ? Integer.MIN_VALUE + 1 : Integer.MAX_VALUE - 1;
        }

        if (currentRed) {
            int maxEval = Integer.MIN_VALUE;
            for (Move m : moves) {
                Board next = MoveGenerator.simulateMove(board, m);
                int eval = minimax(next, depth - 1, false);
                maxEval = Math.max(maxEval, eval);
            }
            return maxEval;
        } else {
            int minEval = Integer.MAX_VALUE;
            for (Move m : moves) {
                Board next = MoveGenerator.simulateMove(board, m);
                int eval = minimax(next, depth - 1, true);
                minEval = Math.min(minEval, eval);
            }
            return minEval;
        }
    }
}

六、Alpha-Beta剪枝优化

Minimax的搜索量随深度呈指数增长。Alpha-Beta剪枝通过维护上下界αβ,剪掉不可能影响最终决策的分支,在理想情况下将复杂度从O(b^d)降至O(b^(d/2))。

public class AlphaBetaAI {
    private int maxDepth;
    private boolean isRed;
    private int nodesExplored; // 统计搜索节点数

    public AlphaBetaAI(int depth, boolean playsRed) {
        this.maxDepth = depth;
        this.isRed = playsRed;
    }

    public Move findBestMove(Board board) {
        nodesExplored = 0;
        List<Move> moves = MoveGenerator.generateMoves(board, isRed);
        if (moves.isEmpty()) return null;

        Move bestMove = null;
        int bestValue = isRed ? Integer.MIN_VALUE : Integer.MAX_VALUE;

        for (Move m : moves) {
            Board next = MoveGenerator.simulateMove(board, m);
            int value = alphaBeta(next, maxDepth - 1, 
                                  Integer.MIN_VALUE, Integer.MAX_VALUE, !isRed);
            if (isRed && value > bestValue) {
                bestValue = value; bestMove = m;
            } else if (!isRed && value < bestValue) {
                bestValue = value; bestMove = m;
            }
        }
        System.out.println("搜索节点数: " + nodesExplored);
        return bestMove;
    }

    private int alphaBeta(Board board, int depth, int alpha, int beta, boolean currentRed) {
        nodesExplored++;
        if (depth == 0 || Evaluator.isGameOver(board)) {
            return Evaluator.evaluate(board);
        }

        List<Move> moves = MoveGenerator.generateMoves(board, currentRed);
        if (moves.isEmpty()) {
            return currentRed ? Integer.MIN_VALUE + 1 : Integer.MAX_VALUE - 1;
        }

        if (currentRed) {
            int maxEval = Integer.MIN_VALUE;
            for (Move m : moves) {
                Board next = MoveGenerator.simulateMove(board, m);
                int eval = alphaBeta(next, depth - 1, alpha, beta, false);
                maxEval = Math.max(maxEval, eval);
                alpha = Math.max(alpha, eval);
                if (beta <= alpha) break; // β剪枝
            }
            return maxEval;
        } else {
            int minEval = Integer.MAX_VALUE;
            for (Move m : moves) {
                Board next = MoveGenerator.simulateMove(board, m);
                int eval = alphaBeta(next, depth - 1, alpha, beta, true);
                minEval = Math.min(minEval, eval);
                beta = Math.min(beta, eval);
                if (beta <= alpha) break; // α剪枝
            }
            return minEval;
        }
    }
}

七、主程序与对弈循环

public class CheckersGame {
    public static void main(String[] args) {
        Board board = new Board();
        AlphaBetaAI ai = new AlphaBetaAI(8, false); // AI执黑,搜索深度8
        Scanner sc = new Scanner(System.in);
        boolean redTurn = true; // 红方先手

        while (!Evaluator.isGameOver(board)) {
            printBoard(board);
            List<Move> moves = MoveGenerator.generateMoves(board, redTurn);
            if (moves.isEmpty()) break;

            if (redTurn) {
                // 人类玩家(简化版:随机选一步,实际可扩展为点击输入)
                System.out.println("红方可选移动:");
                for (int i = 0; i < moves.size(); i++) {
                    Move m = moves.get(i);
                    System.out.printf("%d: (%d,%d)->(%d,%d)%n", 
                                      i, m.fromRow, m.fromCol, m.toRow, m.toCol);
                }
                System.out.print("选择移动编号: ");
                int choice = sc.nextInt();
                board = MoveGenerator.simulateMove(board, moves.get(choice));
            } else {
                System.out.println("黑方(AI)思考中...");
                Move aiMove = ai.findBestMove(board);
                if (aiMove != null) {
                    System.out.printf("AI走法: (%d,%d)->(%d,%d)%n",
                                      aiMove.fromRow, aiMove.fromCol,
                                      aiMove.toRow, aiMove.toCol);
                    board = MoveGenerator.simulateMove(board, aiMove);
                }
            }
            redTurn = !redTurn;
        }

        printBoard(board);
        int eval = Evaluator.evaluate(board);
        System.out.println(eval > 0 ? "红方胜!" : eval < 0 ? "黑方胜!" : "平局!");
    }

    static void printBoard(Board b) {
        System.out.println("  0 1 2 3 4 5 6 7");
        for (int r = 0; r < 8; r++) {
            System.out.print(r + " ");
            for (int c = 0; c < 8; c++) {
                Piece p = b.get(r, c);
                char ch = switch (p) {
                    case RED -> 'r';
                    case RED_KING -> 'R';
                    case BLACK -> 'b';
                    case BLACK_KING -> 'B';
                    default -> Board.isDarkSquare(r, c) ? '.' : ' ';
                };
                System.out.print(ch + " ");
            }
            System.out.println();
        }
    }
}

八、复杂度分析

维度 复杂度 说明
分支因子 b 4-10 平均每步约4-8个合法移动,吃子分支更多
Minimax O(b^d) 深度d=8时约10^8节点,纯Minimax难以承受
Alpha-Beta O(b^(d/2)) 理想排序下节点数降至约10^4量级
评估函数 O(1) 64格扫描,常数时间
移动生成 O(b) 与分支因子同阶

关键优化点
1. 走法排序:优先搜索吃子走法与中心移动,可大幅提升剪枝效率
2. 迭代加深:从浅层到深层逐步搜索,配合时间限制随时返回最优解
3. 置换表:用Zobrist哈希缓存已搜索局面,避免重复计算
4. 杀手启发:记录近期引发剪枝的走法,优先尝试

九、总结

本文完整实现了一个Java跳棋AI,涵盖棋盘建模、移动生成、Minimax搜索、Alpha-Beta剪枝与多特征局面评估。核心要点回顾:

  • 强制吃子规则要求移动生成器先枚举所有吃子路径,再考虑普通移动
  • Minimax提供了博弈决策的理论框架,但裸搜索深度受限
  • Alpha-Beta剪枝在不损失最优性的前提下,将有效搜索深度翻倍
  • 评估函数的权重设计直接影响AI风格:调高KING_VALUE偏向稳健推进,调高MOBILITY_WEIGHT偏向积极兑子

读者可在此基础上扩展置换表迭代加深开局库等高级模块,打造更强的跳棋引擎。跳棋作为完备信息博弈的典型代表,其算法框架同样适用于国际象棋、围棋等更复杂的棋类游戏。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注