每日算法 — 使用java实现四国军棋:不完全信息博弈与蒙特卡洛树搜索

引言

四国军棋是中国经典的四人棋盘游戏,两队玩家(上下家为一队,左右家为一队)在暗棋规则下对弈——每个玩家只能看到自己棋子的等级,无法看到对手和队友的棋子。本文用Java实现四国军棋的AI决策系统,讲解不完全信息博弈的核心挑战,以及信息集蒙特卡洛树搜索(ISMCTS)算法如何在信息缺失的情况下做出近似最优决策。

问题分析

四国军棋的核心规则

  • 棋盘:17×12的公路/铁路/行营/大本营网格
  • 棋子等级:司令 > 军长 > 师长 > 旅长 > 团长 > 营长 > 连长 > 排长 > 工兵 > 炸弹
  • 暗棋规则:除己方棋子外,所有其他棋子以背面朝上显示
  • 合作对抗:对家为队友,可互相掩护和配合

不完全信息博弈的挑战

与象棋、围棋等完全信息博弈不同,四国军棋的AI面临三大核心难题:
1. 信息隐藏:无法直接观察对手棋子的真实等级
2. 信念更新:需根据对弈历史不断修正对隐藏信息的概率估计
3. 团队协作:需在不暴露信息的前提下与队友协调策略

核心算法一:信息集与信念状态

在不完全信息博弈中,信息集(Information Set)定义为玩家无法区分的所有可能游戏状态的集合。对于四国军棋,每个信息集包含:
– 已观察到的棋盘布局
– 己方棋子的确切位置与等级
– 对手棋子的所有可能排列(需满足约束:每种等级的数量固定)

信念状态用概率分布表示:对每一个隐藏的棋子,维护其属于各等级的概率。

核心算法二:信息集蒙特卡洛树搜索(ISMCTS)

ISMCTS是MCTS在不完全信息博弈中的扩展,核心思想:
1. 确定化(Determinization):从当前信息集中随机采样一个具体的完全信息状态
2. 标准MCTS:在该确定化实例上执行UCT搜索
3. 聚合更新:将搜索结果聚合回信息集节点,更新动作价值

完整Java实现

package sijunqi.ai;

import java.util.*;

/**
 * 四国军棋AI决策系统
 * 
 * 核心功能:
 * 1. 信息集建模:维护对隐藏棋子等级的概率信念
 * 2. 确定化采样:从信念状态生成合法的完全信息对局实例
 * 3. ISMCTS搜索:在不完全信息下进行蒙特卡洛树搜索
 * 4. 规则约束:确保采样符合四国军棋的棋子数量约束
 */
public class SijunqiAI {

    // 棋子等级定义
    public enum Rank {
        GONG(0), PAI(1), LIAN(2), YING(3), TUAN(4),
        LV(5), SHI(6), JUN(7), SI(8), ZHA(9), DI(10), QI(11);

        public final int value;
        Rank(int v) { this.value = v; }

        public boolean beats(Rank other) {
            if (this == ZHA || other == ZHA) return true; // 炸弹同归于尽
            if (this == GONG && other == DI) return true; // 工兵挖地雷
            if (this == QI) return false; // 军旗不能移动和吃子
            return this.value > other.value;
        }
    }

    // 棋盘位置
    public static class Pos {
        public final int row, col;
        public Pos(int r, int c) { this.row = r; this.col = c; }
        @Override public boolean equals(Object o) {
            if (o instanceof Pos) { Pos p = (Pos)o; return row==p.row && col==p.col; }
            return false;
        }
        @Override public int hashCode() { return row * 100 + col; }
        @Override public String toString() { return "(" + row + "," + col + ")"; }
    }

    // 棋子
    public static class Piece {
        public final int owner; // 0=自己, 1=队友, 2=下家, 3=上家
        public Rank rank;       // 己方可见,对手为null
        public boolean revealed; // 是否已暴露等级
        public Pos pos;

        public Piece(int owner, Rank rank, Pos pos) {
            this.owner = owner;
            this.rank = rank;
            this.pos = pos;
            this.revealed = owner == 0 || owner == 1; // 己方和队友默认可见
        }
    }

    // 棋盘状态
    public static class Board {
        public static final int ROWS = 17, COLS = 12;
        private Piece[][] grid;

        public Board() {
            grid = new Piece[ROWS][COLS];
        }

        public Piece get(Pos p) { return grid[p.row][p.col]; }
        public void set(Pos p, Piece piece) { grid[p.row][p.col] = piece; }

        /**
         * 获取某玩家的所有棋子
         */
        public List<Piece> getPiecesByOwner(int owner) {
            List<Piece> list = new ArrayList<>();
            for (int r = 0; r < ROWS; r++) {
                for (int c = 0; c < COLS; c++) {
                    Piece p = grid[r][c];
                    if (p != null && p.owner == owner) list.add(p);
                }
            }
            return list;
        }
    }

    /**
     * 信念状态:对每个隐藏棋子维护等级概率分布
     */
    public static class BeliefState {
        // 每个未暴露棋子位置 -> 各等级的概率
        public Map<Pos, double[]> beliefMap;
        // 全局约束:各等级剩余数量
        public Map<Rank, Integer> remainingCount;

        public BeliefState() {
            beliefMap = new HashMap<>();
            remainingCount = new HashMap<>();
            // 标准四国军棋:每方各有1司令、1军长、2师长、2旅长、2团长、2营长、3连长、3排长、3工兵、2炸弹、3地雷、1军旗
            remainingCount.put(Rank.SI, 1);
            remainingCount.put(Rank.JUN, 1);
            remainingCount.put(Rank.SHI, 2);
            remainingCount.put(Rank.LV, 2);
            remainingCount.put(Rank.TUAN, 2);
            remainingCount.put(Rank.YING, 2);
            remainingCount.put(Rank.LIAN, 3);
            remainingCount.put(Rank.PAI, 3);
            remainingCount.put(Rank.GONG, 3);
            remainingCount.put(Rank.ZHA, 2);
            remainingCount.put(Rank.DI, 3);
            remainingCount.put(Rank.QI, 1);
        }

        /**
         * 根据观察到的对弈历史更新信念
         * 例如:若某对手棋子吃掉了己方的师长,则该棋子至少为军长
         */
        public void updateBelief(Pos opponentPos, Rank minRank) {
            double[] probs = beliefMap.get(opponentPos);
            if (probs == null) return;

            // 将该位置所有低于 minRank 的概率设为0,然后重归一化
            double sum = 0;
            for (int i = 0; i < probs.length; i++) {
                if (Rank.values()[i].value < minRank.value) {
                    probs[i] = 0;
                }
                sum += probs[i];
            }
            if (sum > 0) {
                for (int i = 0; i < probs.length; i++) probs[i] /= sum;
            }
        }
    }

    /**
     * MCTS树节点
     */
    public static class MCTSNode {
        public Pos from, to;           // 该节点代表的动作
        public MCTSNode parent;
        public List<MCTSNode> children;
        public int visits;
        public double wins;
        public boolean fullyExpanded;

        public MCTSNode(Pos from, Pos to, MCTSNode parent) {
            this.from = from;
            this.to = to;
            this.parent = parent;
            this.children = new ArrayList<>();
            this.visits = 0;
            this.wins = 0;
        }

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

    private final int playerId;
    private final double explorationConstant;
    private final Random random;

    public SijunqiAI(int playerId, double exploration) {
        this.playerId = playerId;
        this.explorationConstant = exploration;
        this.random = new Random();
    }

    /**
     * ISMCTS主算法
     * 执行多次迭代:确定化 -> MCTS选择/扩展/模拟/回传
     */
    public Pos[] decideMove(Board board, BeliefState belief, int iterations) {
        MCTSNode root = new MCTSNode(null, null, null);

        for (int i = 0; i < iterations; i++) {
            // 步骤1:确定化 - 从信念状态采样一个具体的棋盘
            Board determinized = sampleDeterminization(board, belief);

            // 步骤2:MCTS选择
            MCTSNode selected = select(root, determinized);

            // 步骤3:扩展
            MCTSNode expanded = expand(selected, determinized);

            // 步骤4:模拟(随机 rollout)
            double reward = simulate(expanded, determinized);

            // 步骤5:回传
            backpropagate(expanded, reward);
        }

        // 选择访问次数最多的动作
        MCTSNode best = root.children.stream()
            .max(Comparator.comparingInt(n -> n.visits))
            .orElse(null);

        return best != null ? new Pos[]{best.from, best.to} : null;
    }

    /**
     * 从信念状态采样一个确定化实例
     * 为每个隐藏棋子随机分配一个符合约束的等级
     */
    public Board sampleDeterminization(Board board, BeliefState belief) {
        Board sampled = new Board();

        // 复制已知棋子
        for (int r = 0; r < Board.ROWS; r++) {
            for (int c = 0; c < Board.COLS; c++) {
                Pos p = new Pos(r, c);
                Piece piece = board.get(p);
                if (piece != null) {
                    if (piece.owner == playerId || piece.owner == (playerId + 2) % 4) {
                        // 己方和队友的棋子完全已知
                        sampled.set(p, new Piece(piece.owner, piece.rank, p));
                    } else {
                        // 对手棋子:根据信念采样等级
                        Rank sampledRank = sampleRankFromBelief(p, belief);
                        sampled.set(p, new Piece(piece.owner, sampledRank, p));
                    }
                }
            }
        }
        return sampled;
    }

    /**
     * 根据信念分布采样一个等级
     */
    private Rank sampleRankFromBelief(Pos pos, BeliefState belief) {
        double[] probs = belief.beliefMap.getOrDefault(pos, 
            Arrays.stream(Rank.values()).mapToDouble(r -> 1.0 / Rank.values().length).toArray());

        double roll = random.nextDouble();
        double cumsum = 0;
        for (int i = 0; i < probs.length; i++) {
            cumsum += probs[i];
            if (roll <= cumsum) return Rank.values()[i];
        }
        return Rank.GONG;
    }

    /**
     * MCTS选择阶段:递归选择UCB1值最高的子节点
     */
    private MCTSNode select(MCTSNode node, Board board) {
        while (!node.children.isEmpty() && node.fullyExpanded) {
            node = node.children.stream()
                .max(Comparator.comparingDouble(n -> n.ucb1(explorationConstant)))
                .orElse(node);
        }
        return node;
    }

    /**
     * MCTS扩展阶段:为当前节点添加一个未探索的子节点
     */
    private MCTSNode expand(MCTSNode node, Board board) {
        List<Pos[]> legalMoves = generateLegalMoves(board, playerId);

        // 过滤已探索的动作
        List<Pos[]> unexplored = new ArrayList<>();
        for (Pos[] move : legalMoves) {
            boolean found = false;
            for (MCTSNode child : node.children) {
                if (child.from.equals(move[0]) && child.to.equals(move[1])) {
                    found = true; break;
                }
            }
            if (!found) unexplored.add(move);
        }

        if (unexplored.isEmpty()) {
            node.fullyExpanded = true;
            return node;
        }

        Pos[] move = unexplored.get(random.nextInt(unexplored.size()));
        MCTSNode child = new MCTSNode(move[0], move[1], node);
        node.children.add(child);
        return child;
    }

    /**
     * 生成某玩家的所有合法移动
     */
    private List<Pos[]> generateLegalMoves(Board board, int owner) {
        List<Pos[]> moves = new ArrayList<>();
        List<Piece> pieces = board.getPiecesByOwner(owner);

        for (Piece p : pieces) {
            if (p.rank == Rank.DI || p.rank == Rank.QI) continue; // 地雷和军旗不能移动

            // 公路移动(一步一格)
            int[][] dirs = {{-1,0}, {1,0}, {0,-1}, {0,1}};
            for (int[] d : dirs) {
                int nr = p.pos.row + d[0], nc = p.pos.col + d[1];
                if (isValidMove(board, p.pos, new Pos(nr, nc), owner)) {
                    moves.add(new Pos[]{p.pos, new Pos(nr, nc)});
                }
            }
        }
        return moves;
    }

    /**
     * 判定移动是否合法
     */
    private boolean isValidMove(Board board, Pos from, Pos to, int owner) {
        if (to.row < 0 || to.row >= Board.ROWS || to.col < 0 || to.col >= Board.COLS) return false;
        Piece target = board.get(to);
        if (target != null && target.owner == owner) return false; // 不能吃己方棋子
        return true;
    }

    /**
     * 模拟阶段:从当前节点随机 rollout 到终局
     */
    private double simulate(MCTSNode node, Board board) {
        // 简化模拟:执行若干步随机走法后评估局面
        int currentPlayer = playerId;
        Board simBoard = cloneBoard(board);

        for (int step = 0; step < 20; step++) {
            List<Pos[]> moves = generateLegalMoves(simBoard, currentPlayer);
            if (moves.isEmpty()) break;
            Pos[] move = moves.get(random.nextInt(moves.size()));
            applyMove(simBoard, move[0], move[1]);
            currentPlayer = (currentPlayer + 1) % 4;
        }

        return evaluateBoard(simBoard);
    }

    /**
     * 局面评估函数
     */
    private double evaluateBoard(Board board) {
        double score = 0;
        List<Piece> myPieces = board.getPiecesByOwner(playerId);
        List<Piece> teamPieces = board.getPiecesByOwner((playerId + 2) % 4);

        for (Piece p : myPieces) {
            if (p.rank != null) score += p.rank.value * 10;
        }
        for (Piece p : teamPieces) {
            if (p.rank != null) score += p.rank.value * 5; // 队友权重较低
        }
        return score;
    }

    private Board cloneBoard(Board board) {
        Board clone = new Board();
        for (int r = 0; r < Board.ROWS; r++) {
            for (int c = 0; c < Board.COLS; c++) {
                Pos p = new Pos(r, c);
                Piece piece = board.get(p);
                if (piece != null) {
                    clone.set(p, new Piece(piece.owner, piece.rank, p));
                }
            }
        }
        return clone;
    }

    private void applyMove(Board board, Pos from, Pos to) {
        Piece attacker = board.get(from);
        Piece defender = board.get(to);

        if (defender == null) {
            // 移动到空格
            board.set(to, new Piece(attacker.owner, attacker.rank, to));
            board.set(from, null);
        } else if (attacker.rank.beats(defender.rank)) {
            // 吃子成功
            board.set(to, new Piece(attacker.owner, attacker.rank, to));
            board.set(from, null);
        } else {
            // 同归于尽或失败
            board.set(from, null);
            if (attacker.rank == Rank.ZHA || defender.rank == Rank.ZHA) {
                board.set(to, null); // 炸弹同归于尽
            }
        }
    }

    /**
     * 回传阶段:将模拟结果更新到路径上的所有节点
     */
    private void backpropagate(MCTSNode node, double reward) {
        while (node != null) {
            node.visits++;
            node.wins += reward;
            node = node.parent;
        }
    }

    // ==================== 测试入口 ====================
    public static void main(String[] args) {
        System.out.println("===== 四国军棋AI决策系统 =====\n");

        // 初始化棋盘(简化布局)
        Board board = new Board();

        // 放置己方棋子
        board.set(new Pos(13, 1), new Piece(0, Rank.SI, new Pos(13, 1)));
        board.set(new Pos(13, 3), new Piece(0, Rank.JUN, new Pos(13, 3)));
        board.set(new Pos(13, 5), new Piece(0, Rank.ZHA, new Pos(13, 5)));
        board.set(new Pos(14, 2), new Piece(0, Rank.GONG, new Pos(14, 2)));

        // 放置隐藏的对手棋子
        board.set(new Pos(3, 1), new Piece(2, null, new Pos(3, 1)));
        board.set(new Pos(3, 3), new Piece(2, null, new Pos(3, 3)));
        board.set(new Pos(4, 2), new Piece(2, null, new Pos(4, 2)));

        // 初始化信念状态
        BeliefState belief = new BeliefState();
        belief.beliefMap.put(new Pos(3, 1), new double[]{0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.1,0.1,0.3,0.15});
        belief.beliefMap.put(new Pos(3, 3), new double[]{0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.1,0.1,0.3,0.15});
        belief.beliefMap.put(new Pos(4, 2), new double[]{0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.1,0.1,0.3,0.15});

        // 创建AI并决策
        SijunqiAI ai = new SijunqiAI(0, 1.414);
        System.out.println("AI思考中(1000次迭代)...");
        Pos[] move = ai.decideMove(board, belief, 1000);

        if (move != null) {
            System.out.println("AI决策结果:从 " + move[0] + " 移动到 " + move[1]);
        } else {
            System.out.println("未找到合法移动");
        }

        // 测试信念更新
        System.out.println("\n【测试】信念更新");
        System.out.println("假设对手 (3,1) 吃掉了己方的军长");
        belief.updateBelief(new Pos(3, 1), Rank.JUN);
        double[] updated = belief.beliefMap.get(new Pos(3, 1));
        System.out.print("更新后概率分布: ");
        for (int i = 0; i < updated.length; i++) {
            if (updated[i] > 0) System.out.print(Rank.values()[i] + "=" + String.format("%.2f", updated[i]) + " ");
        }
    }
}

复杂度分析

指标 复杂度 说明
确定化采样 O(N) N为隐藏棋子数
MCTS单次迭代 O(treeDepth + simSteps) 树搜索深度 + 模拟步数
总搜索时间 O(iterations × (N + simSteps)) 迭代次数控制总体复杂度
空间复杂度 O(iterations × treeNodes) 树节点存储
信念更新 O(R) R为等级种类数(固定12种)

扩展与优化

  1. 神经网络估值:用策略网络和价值网络替代随机模拟和手工评估函数
  2. 对手建模:维护对手风格的概率模型(激进/保守),调整belief更新策略
  3. 通信协议:队友间通过走法编码传递有限的加密信息
  4. 历史嵌入:将多轮对弈历史编码为特征向量,输入神经网络
  5. 并行确定化:利用多线程同时评估多个确定化实例,加速ISMCTS

总结

本文用Java实现了四国军棋的AI决策系统,将不完全信息博弈中的核心难题——信息隐藏、信念更新和团队协作——通过信息集蒙特卡洛树搜索进行了系统化建模。ISMCTS的核心洞察在于:虽然无法直接处理无穷多的可能世界,但通过大量随机确定化和统计聚合,AI仍能在信息缺失的环境中做出稳健的近似最优决策。这一方法论广泛应用于扑克、桥牌和其他暗牌类游戏AI的设计中。