每日算法 — 使用java实现飞行棋:概率模型与蒙特卡洛模拟最优决策

引言

飞行棋是一款经典的掷骰子棋类游戏,玩家通过掷骰子决定棋子前进的步数,在棋盘上绕行并争夺终点。虽然飞行棋看似纯靠运气,但其中蕴含丰富的概率决策问题:何时起飞、何时跳跃、何时撞击对手。本文将用Java实现飞行棋的概率模型与蒙特卡洛模拟,帮助玩家制定最优决策策略。

问题建模

标准飞行棋棋盘通常为52格环形轨道,外加4条各6格的终点通道。核心规则包括:

  1. 起飞规则:掷出6点可使停在基地的棋子起飞
  2. 跳跃规则:棋子恰好落在与自身颜色相同的格子上时,可向前跳跃4格
  3. 撞击规则:棋子落在对手棋子所在格子上时,对手棋子返回基地
  4. 连掷规则:掷出6点可获得额外一次掷骰机会
  5. 终点规则:必须恰好到达终点,超出则后退

核心算法设计

1. 概率模型

飞行棋的核心随机变量是骰子点数(1-6均匀分布)。我们需要计算:

  • 到达某格的期望步数:从当前位置到目标位置的期望掷骰次数
  • 被撞击概率:在下一回合中被对手撞击的概率
  • 连掷期望:由于掷出6可连掷,实际每次行动的期望步数大于3.5

2. 蒙特卡洛模拟

对于复杂的多人博弈场景,解析解难以求得。蒙特卡洛模拟通过大量随机采样来评估决策:

  • 模拟数万次对局,统计不同策略的胜率
  • 在当前状态下,评估每个可选行动的期望收益
  • 选择胜率最高的行动

3. 最优决策

每个回合,AI需要决策:
1. 如果有多个可移动的棋子,选择移动哪一个
2. 评估移动后的位置价值(考虑跳跃、撞击、被击风险)

Java完整实现

import java.util.*;

/**
 * 飞行棋概率模型与蒙特卡洛最优决策实现
 */
public class AeroplaneChessAI {

    // 棋盘参数
    private static final int BOARD_SIZE = 52;      // 环形轨道格数
    private static final int HOME_STRETCH = 6;     // 终点通道长度
    private static final int START_POS = 0;        // 起飞位置
    private static final int JUMP_BONUS = 4;       // 跳跃奖励格数
    private static final int REROLL_FACE = 6;      // 连掷点数

    private Random random;
    private int simulations; // 蒙特卡洛模拟次数

    public AeroplaneChessAI(int simulations) {
        this.random = new Random();
        this.simulations = simulations;
    }

    /**
     * 棋子状态
     */
    static class Piece {
        int id;
        int position;      // -1表示在基地,0-51表示轨道,52+表示终点通道
        boolean finished;  // 是否已到达终点
        int color;         // 棋子颜色(0-3)

        Piece(int id, int color) {
            this.id = id;
            this.color = color;
            this.position = -1; // 初始在基地
            this.finished = false;
        }

        boolean isAtBase() {
            return position == -1 && !finished;
        }

        boolean isOnTrack() {
            return position >= 0 && position < BOARD_SIZE;
        }

        boolean isInHomeStretch() {
            return position >= BOARD_SIZE;
        }
    }

    /**
     * 游戏状态
     */
    static class GameState {
        List<Piece>[] players; // 每个玩家的棋子
        int currentPlayer;

        @SuppressWarnings("unchecked")
        GameState(int numPlayers, int piecesPerPlayer) {
            players = new ArrayList[numPlayers];
            for (int i = 0; i < numPlayers; i++) {
                players[i] = new ArrayList<>();
                for (int j = 0; j < piecesPerPlayer; j++) {
                    players[i].add(new Piece(j, i));
                }
            }
            currentPlayer = 0;
        }

        GameState copy() {
            GameState copy = new GameState(players.length, 0);
            for (int i = 0; i < players.length; i++) {
                copy.players[i] = new ArrayList<>();
                for (Piece p : players[i]) {
                    Piece pc = new Piece(p.id, p.color);
                    pc.position = p.position;
                    pc.finished = p.finished;
                    copy.players[i].add(pc);
                }
            }
            copy.currentPlayer = this.currentPlayer;
            return copy;
        }
    }

    /**
     * 掷骰子,1-6均匀分布
     */
    int rollDice() {
        return random.nextInt(6) + 1;
    }

    /**
     * 计算移动后的新位置
     * 考虑终点通道和精确到达规则
     */
    int calculateNewPosition(int currentPos, int roll, int color) {
        if (currentPos == -1) {
            // 在基地,只有掷出6才能起飞
            return roll == REROLL_FACE ? START_POS : -1;
        }

        int newPos = currentPos + roll;

        // 检查是否进入终点通道
        int homeEntry = color * 13; // 每个玩家的终点通道入口不同
        if (currentPos <= homeEntry && newPos > homeEntry) {
            newPos = BOARD_SIZE + (newPos - homeEntry - 1);
        }

        // 检查是否超出终点
        if (newPos >= BOARD_SIZE + HOME_STRETCH) {
            int overshoot = newPos - (BOARD_SIZE + HOME_STRETCH - 1);
            newPos = BOARD_SIZE + HOME_STRETCH - 1 - overshoot;
        }

        return newPos;
    }

    /**
     * 检查是否触发跳跃(落在同色格子上)
     */
    boolean checkJump(int position, int color) {
        if (position < 0 || position >= BOARD_SIZE) return false;
        // 简化为:每13格一个同色区域
        return position % 13 == 0 && position != 0;
    }

    /**
     * 检查是否可以撞击对手
     */
    Piece checkCollision(GameState state, int newPos, int playerColor) {
        if (newPos < 0 || newPos >= BOARD_SIZE) return null;
        for (int i = 0; i < state.players.length; i++) {
            if (i == playerColor) continue;
            for (Piece p : state.players[i]) {
                if (p.position == newPos && !p.finished) {
                    return p;
                }
            }
        }
        return null;
    }

    /**
     * 执行一次移动
     * 返回是否获得连掷机会
     */
    boolean movePiece(GameState state, int playerIdx, int pieceIdx, int roll) {
        Piece piece = state.players[playerIdx].get(pieceIdx);
        boolean reroll = false;

        if (piece.isAtBase()) {
            if (roll == REROLL_FACE) {
                piece.position = START_POS;
                reroll = true;
            }
        } else if (!piece.finished) {
            int oldPos = piece.position;
            int newPos = calculateNewPosition(oldPos, roll, piece.color);

            if (newPos >= BOARD_SIZE + HOME_STRETCH - 1) {
                piece.finished = true;
                piece.position = BOARD_SIZE + HOME_STRETCH;
            } else {
                piece.position = newPos;

                // 检查跳跃
                if (checkJump(newPos, piece.color)) {
                    piece.position = Math.min(newPos + JUMP_BONUS, BOARD_SIZE - 1);
                }

                // 检查撞击
                Piece victim = checkCollision(state, piece.position, playerIdx);
                if (victim != null) {
                    victim.position = -1;
                    reroll = true; // 撞击奖励连掷
                }
            }

            if (roll == REROLL_FACE) reroll = true;
        }

        return reroll;
    }

    /**
     * 获取某玩家当前可移动的所有棋子
     */
    List<Integer> getMovablePieces(GameState state, int playerIdx, int roll) {
        List<Integer> movable = new ArrayList<>();
        List<Piece> pieces = state.players[playerIdx];

        for (int i = 0; i < pieces.size(); i++) {
            Piece p = pieces.get(i);
            if (p.finished) continue;
            if (p.isAtBase() && roll != REROLL_FACE) continue;
            // 检查移动后是否有效
            int newPos = calculateNewPosition(p.position, roll, p.color);
            if (newPos != p.position || p.isAtBase()) {
                movable.add(i);
            }
        }

        return movable;
    }

    /**
     * 蒙特卡洛模拟:评估在当前状态下选择移动某棋子的胜率
     */
    double monteCarloEvaluate(GameState initialState, int playerIdx, int pieceIdx, int roll) {
        int wins = 0;

        for (int sim = 0; sim < simulations; sim++) {
            GameState simState = initialState.copy();

            // 执行当前选择的移动
            boolean reroll = movePiece(simState, playerIdx, pieceIdx, roll);

            if (!reroll) {
                simState.currentPlayer = (simState.currentPlayer + 1) % simState.players.length;
            }

            // 随机模拟剩余对局
            int turns = 0;
            int maxTurns = 500;
            while (turns < maxTurns) {
                int cp = simState.currentPlayer;

                // 检查是否有人获胜
                if (hasWon(simState, playerIdx)) {
                    wins++;
                    break;
                }
                if (someoneElseWon(simState, playerIdx)) {
                    break;
                }

                int r = rollDice();
                List<Integer> moves = getMovablePieces(simState, cp, r);

                if (!moves.isEmpty()) {
                    // 随机选择移动(简化策略)
                    int chosen = moves.get(random.nextInt(moves.size()));
                    boolean rr = movePiece(simState, cp, chosen, r);
                    if (!rr) {
                        simState.currentPlayer = (cp + 1) % simState.players.length;
                    }
                } else {
                    simState.currentPlayer = (cp + 1) % simState.players.length;
                }

                turns++;
            }
        }

        return (double) wins / simulations;
    }

    /**
     * 贪心策略:不通过模拟,直接评估每个移动的即时收益
     */
    double greedyEvaluate(GameState state, int playerIdx, int pieceIdx, int roll) {
        Piece piece = state.players[playerIdx].get(pieceIdx);
        double score = 0;

        // 复制状态评估
        int oldPos = piece.position;
        int newPos = calculateNewPosition(oldPos, roll, piece.color);

        // 基础进度分
        if (piece.isAtBase() && roll == REROLL_FACE) {
            score += 50; // 起飞奖励
        } else if (!piece.finished) {
            score += newPos * 2;
        }

        // 跳跃奖励
        if (checkJump(newPos, piece.color)) {
            score += 30;
        }

        // 撞击对手奖励
        Piece victim = checkCollision(state, newPos, playerIdx);
        if (victim != null) {
            score += 40;
        }

        // 被击风险评估(模拟对手位置)
        for (int i = 0; i < state.players.length; i++) {
            if (i == playerIdx) continue;
            for (Piece op : state.players[i]) {
                if (op.position >= 0 && Math.abs(op.position - newPos) <= 6) {
                    score -= 10; // 附近有对手,存在被击风险
                }
            }
        }

        // 接近终点奖励
        if (newPos >= BOARD_SIZE) {
            score += 60;
        }

        // 完成奖励
        if (newPos >= BOARD_SIZE + HOME_STRETCH - 1) {
            score += 100;
        }

        return score;
    }

    /**
     * 选择最优棋子移动:结合贪心评估与蒙特卡洛验证
     */
    int selectBestPiece(GameState state, int playerIdx, int roll) {
        List<Integer> movable = getMovablePieces(state, playerIdx, roll);
        if (movable.isEmpty()) return -1;
        if (movable.size() == 1) return movable.get(0);

        // 先用贪心筛选出Top-2候选
        List<int[]> candidates = new ArrayList<>(); // [pieceIdx, greedyScore]
        for (int idx : movable) {
            candidates.add(new int[]{idx, (int) greedyEvaluate(state, playerIdx, idx, roll)});
        }
        candidates.sort((a, b) -> b[1] - a[1]);

        // 对Top候选进行蒙特卡洛验证
        int bestPiece = candidates.get(0)[0];
        double bestWinRate = -1;
        int topK = Math.min(2, candidates.size());

        for (int i = 0; i < topK; i++) {
            int idx = candidates.get(i)[0];
            double winRate = monteCarloEvaluate(state, playerIdx, idx, roll);
            if (winRate > bestWinRate) {
                bestWinRate = winRate;
                bestPiece = idx;
            }
        }

        return bestPiece;
    }

    boolean hasWon(GameState state, int playerIdx) {
        for (Piece p : state.players[playerIdx]) {
            if (!p.finished) return false;
        }
        return true;
    }

    boolean someoneElseWon(GameState state, int playerIdx) {
        for (int i = 0; i < state.players.length; i++) {
            if (i != playerIdx && hasWon(state, i)) return true;
        }
        return false;
    }

    public static void main(String[] args) {
        AeroplaneChessAI ai = new AeroplaneChessAI(1000);
        GameState state = new GameState(4, 4);

        // 设置一些棋子位置用于测试
        state.players[0].get(0).position = 10;
        state.players[0].get(1).position = 25;
        state.players[1].get(0).position = 12; // 靠近玩家0的棋子

        int roll = 4;
        System.out.println("当前玩家: 0, 掷出: " + roll);
        System.out.println("可移动棋子: " + ai.getMovablePieces(state, 0, roll));

        int best = ai.selectBestPiece(state, 0, roll);
        System.out.println("AI选择移动棋子: " + best);
        System.out.println("贪心评估值: " + ai.greedyEvaluate(state, 0, best, roll));
    }
}

概率分析

连掷期望

飞行棋中掷出6点可连掷,设每次掷骰的期望行动值为E:

$$E = \frac{1}{6}(6 + E) + \frac{1}{6}(5) + \frac{1}{6}(4) + \frac{1}{6}(3) + \frac{1}{6}(2) + \frac{1}{6}(1)$$

解得:$E = \frac{21}{5} = 4.2$,大于普通骰子期望3.5。

被击概率

假设对手棋子在你前方d格处,下一回合被击中的概率为:
– 若d ≤ 6:被击概率 = (7-d)/6(假设对手选择攻击)
– 若d > 6:被击概率 ≈ 0(单回合无法到达)

复杂度分析

操作 时间复杂度 空间复杂度 说明
位置计算 O(1) O(1) 简单算术运算
碰撞检测 O(P) O(1) P为总棋子数
贪心评估 O(P) O(1) 评估多个因素
蒙特卡洛模拟 O(S × T × P) O(P) S模拟次数,T最大回合数

算法优化方向

  1. 价值迭代:使用动态规划计算每个位置的价值函数
  2. 对手建模:预测对手行为,而非假设完全随机
  3. 多步前瞻:评估未来2-3步的联合行动效果
  4. 并行模拟:利用多线程加速蒙特卡洛采样

总结

飞行棋看似运气游戏,但通过概率模型和蒙特卡洛模拟,我们可以量化每个决策的期望收益。Java实现中,关键在于精确建模游戏规则(跳跃、撞击、连掷)和高效的状态评估函数。对于更复杂的变体规则,可扩展状态空间和策略网络,实现接近人类高手的AI水平。