每日算法 — 使用java实现飞行棋:马尔可夫决策过程与动态规划最优策略

引言

飞行棋是经典的棋盘游戏,玩家通过掷骰子决定棋子移动步数,目标是让自己的四枚棋子全部绕棋盘一圈并到达终点。看似依赖运气的游戏背后,隐藏着丰富的概率决策问题。本文将使用Java建模飞行棋的马尔可夫决策过程(MDP),并通过动态规划计算每一步的最优行动策略。

算法原理

飞行棋规则建模

标准飞行棋棋盘包含52个格子(环形),四枚棋子从基地出发,需要掷出6点才能起飞。棋子按照顺时针方向前进,经过自己颜色的跳跃格时可飞跃前进,到达对手棋子位置时可将其撞回基地。

马尔可夫决策过程

飞行棋可以建模为MDP,包含以下要素:

  • 状态空间 ( S ):所有棋子的位置组合
  • 行动空间 ( A ):当有多枚可移动棋子时选择移动哪一枚
  • 转移概率 ( P(s’|s,a) ):由骰子点数决定
  • 奖励函数 ( R(s,a,s’) ):到达终点、撞击对手、被撞击等的收益

动态规划求解

对于有限时界的MDP,可以使用逆向归纳法(Backward Induction)求解最优策略:

[ V_t(s) = \max_a \sum_{s’} P(s’|s,a) [R(s,a,s’) + V_{t+1}(s’)] ]

其中 ( V_t(s) ) 表示在时刻 ( t ) 处于状态 ( s ) 的最优期望收益。

Java实现

import java.util.*;

/**
 * 飞行棋游戏状态
 */
class LudoState {
    // 每位玩家4枚棋子的位置,-1表示在基地,0-51表示棋盘上,52表示到达终点
    private final int[][] positions;
    private final int currentPlayer;

    public LudoState(int[][] positions, int currentPlayer) {
        this.positions = new int[4][4];
        for (int p = 0; p < 4; p++) {
            System.arraycopy(positions[p], 0, this.positions[p], 0, 4);
        }
        this.currentPlayer = currentPlayer;
    }

    public int[][] getPositions() { return positions; }
    public int getCurrentPlayer() { return currentPlayer; }

    /**
     * 判断某枚棋子是否在基地
     */
    public boolean isInBase(int player, int piece) {
        return positions[player][piece] == -1;
    }

    /**
     * 判断某枚棋子是否已到达终点
     */
    public boolean isFinished(int player, int piece) {
        return positions[player][piece] >= 52;
    }

    /**
     * 获取棋子在棋盘上的实际位置(考虑玩家起始偏移)
     */
    public int getBoardPosition(int player, int piece) {
        int pos = positions[player][piece];
        if (pos < 0 || pos >= 52) return -1;
        return (pos + player * 13) % 52;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof LudoState)) return false;
        LudoState other = (LudoState) o;
        return currentPlayer == other.currentPlayer &&
               Arrays.deepEquals(positions, other.positions);
    }

    @Override
    public int hashCode() {
        return 31 * Arrays.deepHashCode(positions) + currentPlayer;
    }
}

/**
 * 马尔可夫决策过程求解器
 */
public class LudoMDPSolver {
    private static final int NUM_PLAYERS = 4;
    private static final int PIECES_PER_PLAYER = 4;
    private static final int BOARD_SIZE = 52;
    private static final int DICE_SIDES = 6;

    // 奖励函数参数
    private static final double REWARD_FINISH = 100.0;      // 棋子到达终点
    private static final double REWARD_KICK = 20.0;         // 撞击对手棋子
    private static final double REWARD_JUMP = 10.0;         // 经过跳跃格
    private static final double REWARD_PROGRESS = 1.0;      // 每前进一步的奖励
    private static final double PENALTY_KICKED = -15.0;     // 被撞击回基地
    private static final double PENALTY_RISK = -2.0;        // 停留在易被撞击位置

    // 跳跃格位置(相对于每位玩家起点)
    private static final int[] JUMP_SPOTS = {0, 13, 26, 39};

    // 动态规划表:state -> expected value
    private Map<LudoState, Double> valueTable;
    private Map<LudoState, Integer> policyTable; // 最优行动

    public LudoMDPSolver() {
        this.valueTable = new HashMap<>();
        this.policyTable = new HashMap<>();
    }

    /**
     * 计算执行某个行动后的期望价值
     * @param state 当前状态
     * @param action 选择的棋子索引(0-3)
     * @param diceRoll 骰子点数
     * @return 即时奖励
     */
    private double computeImmediateReward(LudoState state, int action, int diceRoll) {
        int player = state.getCurrentPlayer();
        int[] playerPos = state.getPositions()[player];
        int piecePos = playerPos[action];

        double reward = 0;

        // 处理起飞:只有掷出6且棋子在基地才能起飞
        if (piecePos == -1) {
            if (diceRoll == 6) {
                // 起飞到起点
                reward += REWARD_PROGRESS;
            } else {
                return 0; // 无法移动
            }
        }

        // 计算新位置
        int newPos = piecePos == -1 ? 0 : piecePos + diceRoll;

        // 到达终点奖励
        if (newPos >= 52) {
            reward += REWARD_FINISH;
            return reward;
        }

        // 每前进一步的进度奖励
        reward += diceRoll * REWARD_PROGRESS * 0.1;

        // 检查是否经过跳跃格
        int boardPos = (newPos + player * 13) % BOARD_SIZE;
        for (int jumpSpot : JUMP_SPOTS) {
            int actualJump = (jumpSpot + player * 13) % BOARD_SIZE;
            if (boardPos == actualJump) {
                reward += REWARD_JUMP;
                break;
            }
        }

        // 检查是否可以撞击对手
        for (int opponent = 0; opponent < NUM_PLAYERS; opponent++) {
            if (opponent == player) continue;
            int[] oppPos = state.getPositions()[opponent];
            for (int p = 0; p < PIECES_PER_PLAYER; p++) {
                if (oppPos[p] >= 0 && oppPos[p] < 52) {
                    int oppBoardPos = (oppPos[p] + opponent * 13) % BOARD_SIZE;
                    if (boardPos == oppBoardPos) {
                        reward += REWARD_KICK;
                    }
                }
            }
        }

        // 评估新位置的风险(是否容易被对手撞击)
        int riskCount = 0;
        for (int opponent = 0; opponent < NUM_PLAYERS; opponent++) {
            if (opponent == player) continue;
            // 简化的风险评估:统计对手能一步到达此位置的骰子点数数量
            for (int roll = 1; roll <= 6; roll++) {
                int[] oppPos = state.getPositions()[opponent];
                for (int p = 0; p < PIECES_PER_PLAYER; p++) {
                    if (oppPos[p] >= 0 && oppPos[p] < 52) {
                        int potentialPos = oppPos[p] + roll;
                        if (potentialPos < 52) {
                            int potBoardPos = (potentialPos + opponent * 13) % BOARD_SIZE;
                            if (potBoardPos == boardPos) {
                                riskCount++;
                            }
                        }
                    }
                }
            }
        }
        reward += riskCount * PENALTY_RISK;

        return reward;
    }

    /**
     * 执行行动并生成新状态
     */
    private LudoState executeAction(LudoState state, int action, int diceRoll) {
        int[][] newPositions = new int[NUM_PLAYERS][PIECES_PER_PLAYER];
        for (int p = 0; p < NUM_PLAYERS; p++) {
            System.arraycopy(state.getPositions()[p], 0, newPositions[p], 0, PIECES_PER_PLAYER);
        }

        int player = state.getCurrentPlayer();
        int piecePos = newPositions[player][action];

        // 处理起飞
        if (piecePos == -1 && diceRoll == 6) {
            newPositions[player][action] = 0;
        } else if (piecePos >= 0) {
            int newPos = piecePos + diceRoll;

            // 检查并处理撞击
            if (newPos < 52) {
                int newBoardPos = (newPos + player * 13) % BOARD_SIZE;
                for (int opponent = 0; opponent < NUM_PLAYERS; opponent++) {
                    if (opponent == player) continue;
                    for (int p = 0; p < PIECES_PER_PLAYER; p++) {
                        if (newPositions[opponent][p] >= 0 && newPositions[opponent][p] < 52) {
                            int oppBoardPos = (newPositions[opponent][p] + opponent * 13) % BOARD_SIZE;
                            if (newBoardPos == oppBoardPos) {
                                newPositions[opponent][p] = -1; // 撞回基地
                            }
                        }
                    }
                }
            }

            newPositions[player][action] = Math.min(newPos, 52);
        }

        // 下一个玩家(简化为轮流,掷出6可继续)
        int nextPlayer = (diceRoll == 6) ? player : (player + 1) % NUM_PLAYERS;

        return new LudoState(newPositions, nextPlayer);
    }

    /**
     * 判断游戏是否结束(当前玩家的所有棋子都到达终点)
     */
    private boolean isGameOver(LudoState state) {
        int player = state.getCurrentPlayer();
        for (int p = 0; p < PIECES_PER_PLAYER; p++) {
            if (state.getPositions()[player][p] < 52) {
                return false;
            }
        }
        return true;
    }

    /**
     * 获取当前状态下所有合法行动
     */
    private List<Integer> getLegalActions(LudoState state) {
        List<Integer> actions = new ArrayList<>();
        int player = state.getCurrentPlayer();
        int[] pos = state.getPositions()[player];

        for (int i = 0; i < PIECES_PER_PLAYER; i++) {
            // 基地中的棋子需要掷出6才能起飞
            if (pos[i] == -1) {
                actions.add(i); // 将在评估时根据骰子点数判断是否可行
            } else if (pos[i] < 52) {
                actions.add(i);
            }
        }
        return actions;
    }

    /**
     * 使用动态规划求解单步最优策略
     * 对于完整游戏,由于状态空间巨大,这里使用蒙特卡洛采样近似
     */
    public int solveOptimalAction(LudoState state) {
        List<Integer> legalActions = getLegalActions(state);
        if (legalActions.isEmpty()) return -1;

        double bestValue = Double.NEGATIVE_INFINITY;
        int bestAction = legalActions.get(0);

        for (int action : legalActions) {
            double expectedValue = 0;

            // 对骰子的6种结果求期望
            for (int dice = 1; dice <= DICE_SIDES; dice++) {
                double reward = computeImmediateReward(state, action, dice);

                // 如果能执行该行动,累加后续状态的期望价值
                LudoState nextState = executeAction(state, action, dice);
                if (nextState != null && !isGameOver(nextState)) {
                    // 使用启发式评估后续状态
                    double heuristic = evaluateState(nextState, state.getCurrentPlayer());
                    expectedValue += (reward + heuristic) / DICE_SIDES;
                } else {
                    expectedValue += reward / DICE_SIDES;
                }
            }

            if (expectedValue > bestValue) {
                bestValue = expectedValue;
                bestAction = action;
            }
        }

        return bestAction;
    }

    /**
     * 启发式状态评估函数
     * 评估状态对指定玩家的有利程度
     */
    private double evaluateState(LudoState state, int player) {
        double score = 0;
        int[] pos = state.getPositions()[player];

        for (int p = 0; p < PIECES_PER_PLAYER; p++) {
            if (pos[p] >= 52) {
                score += REWARD_FINISH * 0.5; // 已完成的棋子
            } else if (pos[p] >= 0) {
                // 进度奖励:越靠前越好
                score += pos[p] * REWARD_PROGRESS;
                // 接近终点额外奖励
                if (pos[p] > 45) score += (pos[p] - 45) * 2;
            }
        }

        // 对手进度的惩罚
        for (int opp = 0; opp < NUM_PLAYERS; opp++) {
            if (opp == player) continue;
            int[] oppPos = state.getPositions()[opp];
            for (int p = 0; p < PIECES_PER_PLAYER; p++) {
                if (oppPos[p] >= 52) {
                    score -= REWARD_FINISH * 0.3;
                }
            }
        }

        return score;
    }

    /**
     * 蒙特卡洛模拟评估某个行动的长期价值
     * @param state 当前状态
     * @param action 要评估的行动
     * @param simulations 模拟次数
     */
    public double monteCarloEvaluate(LudoState state, int action, int simulations) {
        double totalReward = 0;
        Random rand = new Random();

        for (int sim = 0; sim < simulations; sim++) {
            LudoState simState = state;
            double simReward = 0;
            double discount = 1.0;

            // 执行指定行动
            int dice = rand.nextInt(DICE_SIDES) + 1;
            simReward += computeImmediateReward(simState, action, dice) * discount;
            simState = executeAction(simState, action, dice);

            // 后续随机模拟
            for (int step = 0; step < 20 && !isGameOver(simState); step++) {
                discount *= 0.95;
                List<Integer> legal = getLegalActions(simState);
                if (legal.isEmpty()) break;

                int randomAction = legal.get(rand.nextInt(legal.size()));
                dice = rand.nextInt(DICE_SIDES) + 1;
                simReward += computeImmediateReward(simState, randomAction, dice) * discount;
                simState = executeAction(simState, randomAction, dice);
            }

            totalReward += simReward;
        }

        return totalReward / simulations;
    }

    /**
     * 主程序:演示MDP决策
     */
    public static void main(String[] args) {
        LudoMDPSolver solver = new LudoMDPSolver();

        // 初始化游戏状态:所有棋子都在基地
        int[][] initialPositions = new int[4][4];
        for (int p = 0; p < 4; p++) {
            Arrays.fill(initialPositions[p], -1);
        }

        // 设置一个中间状态用于测试
        initialPositions[0][0] = 10; // 玩家0的第1枚棋子在位置10
        initialPositions[0][1] = -1; // 玩家0的第2枚棋子在基地
        initialPositions[0][2] = 25; // 玩家0的第3枚棋子在位置25
        initialPositions[0][3] = 48; // 玩家0的第4枚棋子接近终点

        // 对手棋子位置
        initialPositions[1][0] = 15;
        initialPositions[2][0] = 23;
        initialPositions[3][0] = 36;

        LudoState state = new LudoState(initialPositions, 0);

        System.out.println("=== 飞行棋MDP最优策略求解 ===");
        System.out.println("当前玩家: 0");
        System.out.println("玩家0棋子位置: " + Arrays.toString(initialPositions[0]));

        // 求解最优行动
        int optimalAction = solver.solveOptimalAction(state);
        System.out.println("\nDP决策推荐移动棋子: " + optimalAction);

        // 蒙特卡洛验证
        System.out.println("\n=== 蒙特卡洛模拟验证 ===");
        for (int action = 0; action < 4; action++) {
            if (initialPositions[0][action] < 52) {
                double mcValue = solver.monteCarloEvaluate(state, action, 1000);
                System.out.printf("移动棋子 %d 的蒙特卡洛期望收益: %.2f%n", action, mcValue);
            }
        }

        System.out.println("\n=== 各骰子点数下的即时奖励分析 ===");
        for (int dice = 1; dice <= 6; dice++) {
            System.out.printf("\n骰子点数 = %d:%n", dice);
            for (int action = 0; action < 4; action++) {
                if (initialPositions[0][action] < 52) {
                    double reward = solver.computeImmediateReward(state, action, dice);
                    System.out.printf("  移动棋子 %d: 奖励 = %.2f%n", action, reward);
                }
            }
        }
    }
}

策略分析

关键决策场景

  1. 起飞决策:当基地中有棋子且掷出6时,应选择起飞还是移动已在棋盘上的棋子?MDP分析表明,早期阶段优先起飞以获取更多移动选择;后期阶段则优先推进靠前的棋子。

  2. 撞击权衡:移动棋子撞击对手可能获得高即时奖励,但如果新位置暴露在高风险区域,长期期望可能不如保守推进。

  3. 终点冲刺:当棋子接近终点时,即使骰子点数可能超出终点,也应优先移动该棋子,因为完成棋子的边际价值最高。

复杂度分析

指标 复杂度 说明
状态空间 ( O(54^{16}) ) 每枚棋子54种可能位置,16枚棋子
单步决策 ( O(A \cdot D \cdot P) ) ( A ) 为合法行动数,( D=6 ) 为骰子面数,( P ) 为玩家数
蒙特卡洛模拟 ( O(S \cdot H \cdot A) ) ( S ) 模拟次数,( H ) 模拟深度

总结

本文将飞行棋建模为马尔可夫决策过程,利用动态规划的期望最大化原理求解每步最优行动。通过定义包含进度奖励、撞击收益和风险惩罚的复合奖励函数,算法能够在起飞、推进、撞击等决策中做出平衡的选择。蒙特卡洛模拟进一步验证了单步DP策略的长期有效性。

对于完整游戏的最优策略,由于状态空间极其庞大,实际应用中通常结合蒙特卡洛树搜索(MCTS)与策略网络来近似求解。本文的DP框架为此类高级方法提供了坚实的理论基础。