每日算法 — 使用java实现Craps掷骰子游戏:概率建模与期望值决策

Craps是一种经典的赌场骰子游戏,玩家通过投掷两颗六面骰子进行博弈。游戏的第一轮称为”出场掷”(Come Out Roll),根据点数总和决定后续流程:掷出7或11直接获胜,掷出2、3或12直接落败,掷出4、5、6、8、9、10则确立”点数”(Point),进入追逐阶段继续投掷,直到再次掷出点数获胜或掷出7落败。本文将用Java完整实现Craps游戏的核心逻辑,并深入分析其概率模型与期望值计算。

游戏规则与状态机建模

Craps的核心在于清晰的状态流转。游戏可抽象为有限状态机:初始状态为COME_OUT,掷骰后根据点数转移到WIN、LOSE或POINT_ESTABLISHED。进入点数状态后,持续掷骰直至命中目标点数或7。这种状态机设计为算法实现提供了清晰的框架。

不同点数组合的概率是分析关键。两颗骰子共有36种等可能结果:7的出现概率最高(6/36 = 16.67%),其次是6和8(各5/36 = 13.89%),2和12最低(各1/36 = 2.78%)。理解这些概率是制定投注策略的基础。

核心算法:概率计算与期望值分析

胜率计算

Craps中”通过线”(Pass Line)投注的胜率计算需要分情况讨论:

  • 出场掷直接获胜:P(7)+P(11) = 6/36 + 2/36 = 8/36
  • 出场掷直接落败:P(2)+P(3)+P(12) = 1/36 + 2/36 + 1/36 = 4/36
  • 确立点数后的条件概率:若点数为4,则需在4和7之间竞争,P(获胜|点数=4) = 3/(3+6) = 3/9

综合胜率 = 8/36 + Σ[P(确立点数=k) × P(命中k | k或7)]

蒙特卡洛模拟验证

对于复杂投注组合的精确计算,蒙特卡洛模拟是有效的验证手段。通过大量随机实验统计胜率,可与理论值相互印证。

Java实现

import java.util.Random;
import java.util.Map;
import java.util.HashMap;

/**
 * Craps掷骰子游戏完整实现
 * 包含游戏状态机、概率计算与蒙特卡洛模拟
 */
public class CrapsGame {

    // 游戏状态枚举
    enum GameState {
        COME_OUT,      // 出场掷阶段
        POINT_ESTABLISHED, // 已确立点数
        WIN,           // 获胜
        LOSE           // 落败
    }

    private final Random random;
    private GameState state;
    private int point; // 确立的点数
    private int rollCount;

    public CrapsGame() {
        this.random = new Random();
        reset();
    }

    /**
     * 重置游戏状态
     */
    public void reset() {
        this.state = GameState.COME_OUT;
        this.point = 0;
        this.rollCount = 0;
    }

    /**
     * 投掷两颗骰子,返回点数总和
     */
    public int rollDice() {
        int die1 = random.nextInt(6) + 1;
        int die2 = random.nextInt(6) + 1;
        rollCount++;
        return die1 + die2;
    }

    /**
     * 执行一次投掷并根据当前状态更新游戏状态
     * @return 当前回合的点数总和
     */
    public int playRound() {
        if (state == GameState.WIN || state == GameState.LOSE) {
            throw new IllegalStateException("游戏已结束,请先调用reset()");
        }

        int roll = rollDice();

        if (state == GameState.COME_OUT) {
            // 出场掷阶段
            switch (roll) {
                case 7: case 11:
                    state = GameState.WIN;
                    break;
                case 2: case 3: case 12:
                    state = GameState.LOSE;
                    break;
                default:
                    point = roll;
                    state = GameState.POINT_ESTABLISHED;
                    break;
            }
        } else if (state == GameState.POINT_ESTABLISHED) {
            // 追逐阶段
            if (roll == point) {
                state = GameState.WIN;
            } else if (roll == 7) {
                state = GameState.LOSE;
            }
            // 其他点数继续游戏
        }

        return roll;
    }

    /**
     * 完整运行一局游戏直到结束
     * @return true表示获胜,false表示落败
     */
    public boolean playGame() {
        reset();
        while (state != GameState.WIN && state != GameState.LOSE) {
            playRound();
        }
        return state == GameState.WIN;
    }

    /**
     * 计算理论胜率(Pass Line投注)
     */
    public static double calculateTheoreticalWinRate() {
        // 出场掷直接获胜:7或11
        double instantWin = 8.0 / 36.0;
        // 出场掷直接落败:2、3或12
        double instantLose = 4.0 / 36.0;

        // 确立点数后的条件胜率
        Map<Integer, Double> pointProb = new HashMap<>();
        pointProb.put(4, 3.0 / 9.0);
        pointProb.put(5, 4.0 / 10.0);
        pointProb.put(6, 5.0 / 11.0);
        pointProb.put(8, 5.0 / 11.0);
        pointProb.put(9, 4.0 / 10.0);
        pointProb.put(10, 3.0 / 9.0);

        // 各点数确立概率
        Map<Integer, Double> establishProb = new HashMap<>();
        establishProb.put(4, 3.0 / 36.0);
        establishProb.put(5, 4.0 / 36.0);
        establishProb.put(6, 5.0 / 36.0);
        establishProb.put(8, 5.0 / 36.0);
        establishProb.put(9, 4.0 / 36.0);
        establishProb.put(10, 3.0 / 36.0);

        double pointWin = 0.0;
        for (int p : pointProb.keySet()) {
            pointWin += establishProb.get(p) * pointProb.get(p);
        }

        return instantWin + pointWin;
    }

    /**
     * 蒙特卡洛模拟估算胜率
     * @param trials 模拟次数
     */
    public static double monteCarloSimulation(int trials) {
        CrapsGame game = new CrapsGame();
        int wins = 0;
        for (int i = 0; i < trials; i++) {
            if (game.playGame()) {
                wins++;
            }
        }
        return (double) wins / trials;
    }

    /**
     * 计算两颗骰子各点数总和的概率分布
     */
    public static Map<Integer, Double> calculateDiceDistribution() {
        Map<Integer, Integer> counts = new HashMap<>();
        for (int i = 1; i <= 6; i++) {
            for (int j = 1; j <= 6; j++) {
                int sum = i + j;
                counts.put(sum, counts.getOrDefault(sum, 0) + 1);
            }
        }
        Map<Integer, Double> distribution = new HashMap<>();
        for (int sum : counts.keySet()) {
            distribution.put(sum, counts.get(sum) / 36.0);
        }
        return distribution;
    }

    public GameState getState() { return state; }
    public int getPoint() { return point; }
    public int getRollCount() { return rollCount; }

    public static void main(String[] args) {
        // 输出概率分布
        System.out.println("=== 两颗骰子点数概率分布 ===");
        Map<Integer, Double> dist = calculateDiceDistribution();
        for (int i = 2; i <= 12; i++) {
            System.out.printf("点数 %2d: %.4f (%.2f%%)%n", i, dist.get(i), dist.get(i) * 100);
        }

        // 理论胜率
        double theoreticalRate = calculateTheoreticalWinRate();
        System.out.printf("%n理论胜率(Pass Line): %.6f (%.4f%%)%n", theoreticalRate, theoreticalRate * 100);

        // 蒙特卡洛模拟
        int[] trialCounts = {1000, 10000, 100000, 1000000};
        System.out.println("\n=== 蒙特卡洛模拟结果 ===");
        for (int trials : trialCounts) {
            double mcRate = monteCarloSimulation(trials);
            System.out.printf("模拟 %,d 次: 胜率 = %.6f, 误差 = %.6f%n", 
                trials, mcRate, Math.abs(mcRate - theoreticalRate));
        }

        // 演示一局完整游戏
        System.out.println("\n=== 演示一局游戏 ===");
        CrapsGame demo = new CrapsGame();
        while (demo.getState() != GameState.WIN && demo.getState() != GameState.LOSE) {
            int roll = demo.playRound();
            if (demo.getRollCount() == 1) {
                System.out.println("出场掷: " + roll);
            } else {
                System.out.println("第" + demo.getRollCount() + "掷: " + roll + " (目标点数: " + demo.getPoint() + ")");
            }
        }
        System.out.println("结果: " + (demo.getState() == GameState.WIN ? "获胜!" : "落败!") + " 共投掷" + demo.getRollCount() + "次");
    }
}

复杂度分析

  • 单次投掷:O(1)时间复杂度,O(1)空间复杂度
  • 完整游戏:期望投掷次数约为3.38次(通过精确概率计算可得),故期望时间复杂度为O(1)
  • 蒙特卡洛模拟:N次试验的时间复杂度为O(N),空间复杂度为O(1)
  • 概率计算:通过枚举36种骰子组合,时间复杂度O(1)

扩展思路

基于本文的实现框架,可进一步探索:多种投注方式(不通过线、来注、现场注)的期望值差异;利用大数定律验证长期盈亏趋势;引入玩家资金管理策略(如凯利公式)优化投注比例。Craps虽然规则简单,但其概率结构蕴含丰富的数学内涵,是理解条件概率与期望值的绝佳案例。