每日算法 — 使用java实现飞行棋:概率建模与路径规划最优策略

飞行棋是一款经典的棋盘竞技游戏,玩家通过掷骰子驱动棋子在环形轨道上前进,以全部棋子抵达终点为胜。看似简单规则的背后,隐藏着丰富的概率计算与策略博弈空间:何时出动新棋子、是否冒险穿越敌阵、怎样选择最优移动目标,都需要综合评估风险与收益。本文将用 Java 完整实现飞行棋的核心逻辑,并重点讲解概率建模、路径规划与蒙特卡洛模拟在最优策略决策中的应用。

一、游戏规则与核心算法挑战

标准飞行棋棋盘由 52 个公共格子与 4 条各 6 格的终点通道组成。每位玩家拥有 4 枚棋子,起始于自家停机坪。核心规则如下:

  • 掷出 6 点 可从停机坪出动一枚新棋子至起飞格,并获得额外一次掷骰机会
  • 棋子按顺时针方向在环形公共轨道上移动,移动步数等于骰子点数
  • 若棋子恰好落在敌方棋子所在格,敌方棋子被击落回停机坪(己方安全区与重叠格除外)
  • 棋子绕行一周后进入自家终点通道,需恰好掷到剩余步数才能抵达终点

算法挑战

  1. 概率建模:骰子结果的离散分布直接影响出动概率与期望步数
  2. 路径规划:环形轨道上的最短路径与多棋子协同移动策略
  3. 风险评估:穿越敌阵时的被击落概率量化
  4. 最优决策:多枚棋子可移动时,如何选择收益最大的走法

二、数据模型设计

首先建立棋盘、棋子与玩家的基础数据模型。

/**
 * 棋子状态
 */
public class Piece {
    public enum Status { HANGAR, BOARD, HOME } // 停机坪/轨道上/已到家

    public int id;              // 棋子编号 0-3
    public int playerId;        // 所属玩家 0-3
    public Status status;
    public int position;        // 在轨道上的位置 (0-51),仅在 BOARD 状态有效
    public int homePosition;    // 在终点通道中的位置 (0-5),进入 HOME 后使用

    public Piece(int id, int playerId) {
        this.id = id;
        this.playerId = playerId;
        this.status = Status.HANGAR;
        this.position = -1;
        this.homePosition = -1;
    }
}

/**
 * 玩家
 */
public class Player {
    public int id;
    public String name;
    public Piece[] pieces = new Piece[4];
    public int startOffset;     // 该玩家的起飞格在全局轨道上的偏移

    public Player(int id, String name, int startOffset) {
        this.id = id;
        this.name = name;
        this.startOffset = startOffset;
        for (int i = 0; i < 4; i++) {
            pieces[i] = new Piece(i, id);
        }
    }

    /**
     * 计算从当前位置移动到终点的剩余步数
     */
    public int stepsToHome(Piece piece) {
        if (piece.status == Piece.Status.HANGAR) return Integer.MAX_VALUE;
        if (piece.status == Piece.Status.HOME) return 6 - piece.homePosition;
        // 在公共轨道上,计算到进入终点通道的距离
        int enterPos = (startOffset + 50) % 52; // 终点通道入口前一格
        int dist = (enterPos - piece.position + 52) % 52;
        return dist + 6; // +6 为终点通道长度
    }
}

/**
 * 棋盘
 */
public class Board {
    public static final int TRACK_LENGTH = 52;  // 公共轨道长度
    public static final int HOME_LENGTH = 6;    // 终点通道长度
    public static final int PLAYER_COUNT = 4;

    // 安全区位置:每个玩家的起飞格及重叠格
    private final boolean[] safeZones = new boolean[TRACK_LENGTH];

    public Board() {
        // 四个玩家的起飞格为安全区
        for (int i = 0; i < PLAYER_COUNT; i++) {
            safeZones[i * 13] = true;
        }
    }

    public boolean isSafe(int position) {
        return safeZones[position % TRACK_LENGTH];
    }

    /**
     * 获取指定位置上的所有棋子(用于碰撞检测)
     */
    public List<Piece> getPiecesAt(int position, Player[] players) {
        List<Piece> list = new ArrayList<>();
        for (Player p : players) {
            for (Piece piece : p.pieces) {
                if (piece.status == Piece.Status.BOARD && piece.position == position) {
                    list.add(piece);
                }
            }
        }
        return list;
    }
}

三、概率建模:骰子分布与期望计算

飞行棋中,骰子结果直接决定行动空间。我们建立概率模型来量化不同决策的长期收益。

/**
 * 骰子概率模型
 */
public class DiceModel {
    public static final int SIDES = 6;

    /**
     * 单骰各点数的概率质量函数 (公平骰子)
     */
    public static double probability(int face) {
        if (face < 1 || face > SIDES) return 0.0;
        return 1.0 / SIDES;
    }

    /**
     * 掷出 6 点的概率(出动新棋子的关键事件)
     */
    public static double rollSixProbability() {
        return 1.0 / SIDES;
    }

    /**
     * 连续掷出 k 次 6 点的概率(理论上无上限,实际可截断)
     */
    public static double consecutiveSixes(int k) {
        return Math.pow(1.0 / SIDES, k);
    }

    /**
     * 单轮行动中至少出动一枚新棋子的概率
     * 即:至少掷出一个 6 点(考虑连续掷 6 的额外机会)
     * P = 1/6 + (1/6)^2 + (1/6)^3 + ... = (1/6) / (1 - 1/6) = 1/5
     */
    public static double launchProbabilityPerTurn() {
        double p = 1.0 / SIDES;
        return p / (1 - p); // 几何级数求和
    }

    /**
     * 从轨道某位置恰好走到目标位置的掷骰概率
     * 在 HOME 阶段需要精确步数
     */
    public static double exactRollProbability(int needed) {
        if (needed >= 1 && needed <= SIDES) return probability(needed);
        return 0.0;
    }
}

期望步数分析

当棋子在公共轨道上距离终点通道入口还有 d 步时,单轮期望前进步数为:

$$E[\text{步数}] = \sum_{i=1}^{6} i \cdot P(X=i) = 3.5$$

但由于存在被击落回停机坪的风险,有效期望步数需要结合存活概率修正。

四、路径规划与风险评估

在飞行棋中,最优路径不仅指最短几何路径,更意味着风险最小、收益最大的战略通道。

/**
 * 风险评估与路径规划
 */
public class RiskEvaluator {

    /**
     * 评估移动指定棋子后的局面得分
     * 得分越高,该走法越优
     */
    public static double evaluateMove(Board board, Player[] players,
                                       Player currentPlayer, Piece piece, int steps) {
        double score = 0.0;

        // 模拟移动后的位置
        MoveResult result = simulateMove(board, players, currentPlayer, piece, steps);
        Piece simulated = result.piece;

        // 1. 抵达终点的奖励(最高优先级)
        if (simulated.status == Piece.Status.HOME && simulated.homePosition == Board.HOME_LENGTH) {
            score += 10000.0;
        }
        // 2. 进入终点通道的奖励
        else if (simulated.status == Piece.Status.HOME) {
            score += 500.0 * simulated.homePosition;
        }

        // 3. 击落敌方棋子的奖励
        if (result.capturedPieces > 0) {
            score += 800.0 * result.capturedPieces;
        }

        // 4. 前进距离的线性奖励
        if (simulated.status == Piece.Status.BOARD) {
            score += 10.0 * steps;

            // 5. 位置安全性评估
            if (board.isSafe(simulated.position)) {
                score += 200.0; // 安全区加分
            } else {
                // 6. 被击落风险惩罚
                double risk = calculateCaptureRisk(board, players, currentPlayer, simulated.position);
                score -= 600.0 * risk;
            }

            // 7. 靠近终点通道入口的加速奖励
            int stepsToHome = currentPlayer.stepsToHome(simulated);
            if (stepsToHome < 20) {
                score += 50.0 * (20 - stepsToHome);
            }
        }

        // 8. 从停机坪出动的奖励(前期优先出兵)
        if (piece.status == Piece.Status.HANGAR && steps == 6) {
            long boardCount = Arrays.stream(currentPlayer.pieces)
                .filter(p -> p.status == Piece.Status.BOARD || p.status == Piece.Status.HOME)
                .count();
            if (boardCount < 2) {
                score += 300.0; // 前期优先多出兵
            }
        }

        return score;
    }

    /**
     * 计算某位置被敌方下一回合击落的概率
     */
    public static double calculateCaptureRisk(Board board, Player[] players,
                                               Player currentPlayer, int position) {
        double risk = 0.0;
        for (Player enemy : players) {
            if (enemy.id == currentPlayer.id) continue;
            for (Piece p : enemy.pieces) {
                if (p.status != Piece.Status.BOARD) continue;
                // 敌方需要掷出恰好 (position - enemyPos + 52) % 52 才能到达
                int needed = (position - p.position + Board.TRACK_LENGTH) % Board.TRACK_LENGTH;
                if (needed >= 1 && needed <= 6) {
                    risk += DiceModel.probability(needed);
                }
            }
        }
        return Math.min(risk, 1.0);
    }

    /**
     * 模拟移动结果
     */
    private static MoveResult simulateMove(Board board, Player[] players,
                                            Player player, Piece piece, int steps) {
        // 深拷贝棋子进行模拟
        Piece sim = new Piece(piece.id, piece.playerId);
        sim.status = piece.status;
        sim.position = piece.position;
        sim.homePosition = piece.homePosition;

        int captured = 0;

        if (sim.status == Piece.Status.HANGAR && steps == 6) {
            // 出动至起飞格
            sim.status = Piece.Status.BOARD;
            sim.position = player.startOffset;
        } else if (sim.status == Piece.Status.BOARD) {
            int newPos = (sim.position + steps) % Board.TRACK_LENGTH;
            int enterPos = (player.startOffset + 50) % Board.TRACK_LENGTH;

            // 判断是否进入终点通道
            int distToEnter = (enterPos - sim.position + Board.TRACK_LENGTH) % Board.TRACK_LENGTH;
            if (steps > distToEnter) {
                int homeSteps = steps - distToEnter - 1;
                if (homeSteps < Board.HOME_LENGTH) {
                    sim.status = Piece.Status.HOME;
                    sim.homePosition = homeSteps;
                }
                // 超出则不可移动(规则上通常不允许或停在入口前)
            } else {
                sim.position = newPos;
                // 碰撞检测与击落
                if (!board.isSafe(newPos)) {
                    for (Player other : players) {
                        if (other.id == player.id) continue;
                        for (Piece enemy : other.pieces) {
                            if (enemy.status == Piece.Status.BOARD && enemy.position == newPos) {
                                captured++;
                            }
                        }
                    }
                }
            }
        } else if (sim.status == Piece.Status.HOME) {
            int newHome = sim.homePosition + steps;
            if (newHome <= Board.HOME_LENGTH) {
                sim.homePosition = newHome;
                if (newHome == Board.HOME_LENGTH) {
                    sim.status = Piece.Status.HOME; // 已到家
                }
            }
        }

        return new MoveResult(sim, captured);
    }

    static class MoveResult {
        Piece piece;
        int capturedPieces;
        MoveResult(Piece p, int c) { this.piece = p; this.capturedPieces = c; }
    }
}

五、蒙特卡洛模拟:AI 最优决策

当存在多枚可移动的棋子时,我们需要选择最优走法。蒙特卡洛模拟通过大量随机推演来评估每个候选走法的期望胜率。

/**
 * 蒙特卡洛模拟决策引擎
 */
public class MonteCarloDecisionEngine {
    private static final int SIMULATION_COUNT = 5000; // 模拟次数
    private final Random random = new Random();

    /**
     * 为当前玩家选择最优走法
     * @return 最优决策 [pieceId, expectedWinRate]
     */
    public double[] selectBestMove(Board board, Player[] players, int currentPlayerId, int diceRoll) {
        Player current = players[currentPlayerId];
        List<Integer> movablePieces = getMovablePieces(current, diceRoll);

        if (movablePieces.isEmpty()) return new double[]{-1, 0.0};
        if (movablePieces.size() == 1) return new double[]{movablePieces.get(0), 1.0};

        double bestWinRate = -1.0;
        int bestPiece = -1;

        for (int pieceId : movablePieces) {
            double winRate = simulateWinRate(board, players, currentPlayerId, pieceId, diceRoll);
            if (winRate > bestWinRate) {
                bestWinRate = winRate;
                bestPiece = pieceId;
            }
        }

        return new double[]{bestPiece, bestWinRate};
    }

    /**
     * 对指定走法进行蒙特卡洛模拟,估算胜率
     */
    private double simulateWinRate(Board board, Player[] players, int currentPlayerId,
                                    int pieceId, int diceRoll) {
        int wins = 0;

        for (int i = 0; i < SIMULATION_COUNT; i++) {
            // 深拷贝当前局面
            GameState state = cloneState(board, players);
            // 执行候选走法
            executeMove(state, currentPlayerId, pieceId, diceRoll);
            // 快速模拟后续对局至结束
            if (fastSimulate(state, currentPlayerId)) {
                wins++;
            }
        }

        return (double) wins / SIMULATION_COUNT;
    }

    /**
     * 快速模拟:使用贪心策略快速推演至游戏结束
     */
    private boolean fastSimulate(GameState state, int targetPlayerId) {
        int turns = 0;
        int current = 0;

        while (turns < 500) { // 防止无限循环
            // 检查胜利条件
            if (hasWon(state.players[targetPlayerId])) return true;
            if ( Arrays.stream(state.players).anyMatch(p -> p.id != targetPlayerId && hasWon(p)) ) {
                return false;
            }

            // 掷骰子
            int roll = random.nextInt(6) + 1;
            Player player = state.players[current];
            List<Integer> movable = getMovablePieces(player, roll);

            if (!movable.isEmpty()) {
                // 快速模拟中使用启发式评估选择走法(非完整 MCTS)
                int best = movable.get(0);
                double bestScore = -Double.MAX_VALUE;
                for (int pid : movable) {
                    double s = RiskEvaluator.evaluateMove(
                        state.board, state.players, player, player.pieces[pid], roll);
                    if (s > bestScore) {
                        bestScore = s;
                        best = pid;
                    }
                }
                executeMove(state, current, best, roll);
            }

            // 6 点获得额外回合(简化处理:最多连续 3 次)
            if (roll != 6 || turns % 3 == 2) {
                current = (current + 1) % Board.PLAYER_COUNT;
            }
            turns++;
        }

        // 超时判定:比较各玩家已到家棋子数
        return countHomePieces(state.players[targetPlayerId]) >= 
               Arrays.stream(state.players).filter(p -> p.id != targetPlayerId)
                     .mapToInt(this::countHomePieces).max().orElse(0);
    }

    private List<Integer> getMovablePieces(Player player, int diceRoll) {
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < 4; i++) {
            Piece p = player.pieces[i];
            if (p.status == Piece.Status.HOME && p.homePosition == Board.HOME_LENGTH) continue;
            if (p.status == Piece.Status.HANGAR && diceRoll == 6) list.add(i);
            else if (p.status == Piece.Status.BOARD) list.add(i);
            else if (p.status == Piece.Status.HOME && p.homePosition + diceRoll <= Board.HOME_LENGTH) {
                list.add(i);
            }
        }
        return list;
    }

    private boolean hasWon(Player player) {
        return Arrays.stream(player.pieces)
            .allMatch(p -> p.status == Piece.Status.HOME && p.homePosition == Board.HOME_LENGTH);
    }

    private int countHomePieces(Player player) {
        return (int) Arrays.stream(player.pieces)
            .filter(p -> p.status == Piece.Status.HOME && p.homePosition == Board.HOME_LENGTH)
            .count();
    }

    // 深拷贝与执行走法的辅助方法(省略具体实现)
    private GameState cloneState(Board board, Player[] players) { /* ... */ return null; }
    private void executeMove(GameState state, int playerId, int pieceId, int steps) { /* ... */ }
    static class GameState { Board board; Player[] players; }
}

六、游戏主循环与控制台演示

/**
 * 飞行棋游戏引擎
 */
public class AeroplaneChess {
    private final Board board = new Board();
    private final Player[] players = new Player[Board.PLAYER_COUNT];
    private final Random dice = new Random();
    private final MonteCarloDecisionEngine ai = new MonteCarloDecisionEngine();

    public AeroplaneChess() {
        String[] names = {"红方", "黄方", "蓝方", "绿方"};
        for (int i = 0; i < Board.PLAYER_COUNT; i++) {
            players[i] = new Player(i, names[i], i * 13);
        }
    }

    public void play() {
        int current = 0;
        int round = 0;

        while (round < 1000) {
            Player player = players[current];
            System.out.println("\n===== " + player.name + " 的回合 =====");

            int consecutiveSixes = 0;
            while (consecutiveSixes < 3) {
                int roll = dice.nextInt(6) + 1;
                System.out.println("掷出: " + roll);

                double[] decision = ai.selectBestMove(board, players, current, roll);
                if (decision[0] < 0) {
                    System.out.println("无可用走法");
                    break;
                }

                int pieceId = (int) decision[0];
                System.out.printf("选择棋子 %d,预估胜率: %.2f%%\n", pieceId, decision[1] * 100);
                movePiece(player.pieces[pieceId], roll);
                printBoard();

                if (hasWon(player)) {
                    System.out.println("🎉 " + player.name + " 获胜!");
                    return;
                }

                if (roll == 6) {
                    consecutiveSixes++;
                    System.out.println("获得额外回合!");
                } else {
                    break;
                }
            }

            current = (current + 1) % Board.PLAYER_COUNT;
            round++;
        }
    }

    private void movePiece(Piece piece, int steps) {
        // 整合前面的移动逻辑(含碰撞检测)
    }

    private void printBoard() {
        // 简化控制台输出
    }

    private boolean hasWon(Player player) {
        return Arrays.stream(player.pieces)
            .allMatch(p -> p.status == Piece.Status.HOME && p.homePosition == Board.HOME_LENGTH);
    }

    public static void main(String[] args) {
        new AeroplaneChess().play();
    }
}

七、复杂度分析

模块 时间复杂度 空间复杂度 说明
单次局面评估 O(P) O(1) P 为场上棋子总数(≤16)
蒙特卡洛单次模拟 O(T · P) O(P) T 为模拟轮数上限
完整决策(5000 次模拟) O(S · T · P) O(P) S=5000 为模拟次数
棋盘上棋子移动 O(1) O(1) 哈希定位

优化方向

  1. 置换表(Transposition Table):缓存已评估的局面,避免重复计算
  2. 剪枝策略:当某走法胜率显著低于当前最优时提前终止模拟
  3. 并行化:利用多线程并行执行蒙特卡洛模拟,决策速度可提升 N 倍(N 为 CPU 核心数)

八、总结

飞行棋的魅力在于概率不确定性与策略确定性的交织。本文从工程实现角度,建立了完整的概率模型、风险评估体系与蒙特卡洛决策引擎:

  • 概率建模量化了出动期望与精确抵达概率,为战略节奏提供数学依据
  • 路径规划综合距离、安全性与击落收益,构建了多维度局面评估函数
  • 蒙特卡洛模拟通过大量随机推演收敛出最优走法,使 AI 具备接近人类高手的决策水平

读者可以在此基础上继续扩展:引入多线程并行模拟、设计更精细的评估函数权重、或加入在线学习机制让 AI 从历史对局中持续进化。