每日算法 — 使用java实现诈金花:不完全信息博弈与贝叶斯概率推理

引言

诈金花(又称”炸金花”、”三张牌”)是一款广泛流行的扑克博弈游戏。玩家仅持三张手牌,通过观察对手下注行为进行概率推理和虚张声势。其本质是不完全信息动态博弈的典型代表。本文用Java实现贝叶斯概率推理引擎,根据公共信息和对手行为动态更新牌型概率分布,并给出最优下注决策建议。

核心算法设计

1. 牌型判定与概率基线

诈金花牌型从大到小依次为:豹子(三张相同)> 同花顺 > 同花 > 顺子 > 对子 > 单张。由于每人仅三张牌,精确计算各类牌型的先验概率是贝叶斯推理的基础。

2. 贝叶斯概率推理

设事件H为”对手持有某类牌型”,观测E为”对手的下注行为”。根据贝叶斯定理:

$$P(H|E) = \frac{P(E|H) \times P(H)}{P(E)}$$

通过设定不同牌型对应的下注倾向(强牌更可能加注,弱牌更可能弃牌),我们可以根据对手行为反推其牌型分布。

3. 期望值决策模型

在每一轮下注时,计算”跟注”、”加注”、”弃牌”三种行动的期望收益,选择期望值最大的行动。

完整Java实现

import java.util.*;

/**
 * 诈金花博弈决策引擎
 * 包含牌型判定、贝叶斯概率推理与期望值决策
 */
public class ZhaJinHua {

    static class Card {
        int suit; // 0-3
        int rank; // 1-13
        Card(int s, int r) { suit = s; rank = r; }
        int value() { return rank; }
        public String toString() {
            String[] s = {"♠","♥","♣","♦"};
            String[] r = {"","A","2","3","4","5","6","7","8","9","10","J","Q","K"};
            return s[suit] + r[rank];
        }
    }

    enum HandRank {
        HIGH_CARD(1, "单张"), PAIR(2, "对子"), STRAIGHT(3, "顺子"),
        FLUSH(4, "同花"), STRAIGHT_FLUSH(5, "同花顺"), TRIPLE(6, "豹子");
        final int level; final String name;
        HandRank(int l, String n) { level = l; name = n; }
    }

    static class HandEval {
        HandRank rank;
        int[] kickers; // 用于同类型牌型的大小比较
        HandEval(HandRank r, int[] k) { this.rank = r; this.kickers = k; }
    }

    /**
     * 牌型判定:返回牌型等级和比较键
     */
    public static HandEval evaluate(Card[] hand) {
        if (hand.length != 3) return null;
        Arrays.sort(hand, (a, b) -> b.rank - a.rank); // 降序

        boolean isFlush = hand[0].suit == hand[1].suit && hand[1].suit == hand[2].suit;
        boolean isStraight = (hand[0].rank == hand[1].rank + 1 && hand[1].rank == hand[2].rank + 1) ||
                             (hand[0].rank == 13 && hand[1].rank == 2 && hand[2].rank == 1); // A23特殊顺子

        if (hand[0].rank == hand[1].rank && hand[1].rank == hand[2].rank) {
            return new HandEval(HandRank.TRIPLE, new int[]{hand[0].rank});
        }
        if (isFlush && isStraight) {
            return new HandEval(HandRank.STRAIGHT_FLUSH, new int[]{hand[0].rank});
        }
        if (isFlush) {
            return new HandEval(HandRank.FLUSH, new int[]{hand[0].rank, hand[1].rank, hand[2].rank});
        }
        if (isStraight) {
            int high = (hand[0].rank == 13 && hand[2].rank == 1) ? 3 : hand[0].rank;
            return new HandEval(HandRank.STRAIGHT, new int[]{high});
        }
        if (hand[0].rank == hand[1].rank) {
            return new HandEval(HandRank.PAIR, new int[]{hand[0].rank, hand[2].rank});
        }
        if (hand[1].rank == hand[2].rank) {
            return new HandEval(HandRank.PAIR, new int[]{hand[1].rank, hand[0].rank});
        }
        return new HandEval(HandRank.HIGH_CARD, new int[]{hand[0].rank, hand[1].rank, hand[2].rank});
    }

    /**
     * 计算先验概率:从52张牌中随机抽取3张的牌型分布
     */
    public static Map<HandRank, Double> priorProbabilities() {
        Map<HandRank, Long> counts = new HashMap<>();
        for (HandRank r : HandRank.values()) counts.put(r, 0L);

        List<Card> deck = new ArrayList<>();
        for (int s = 0; s < 4; s++) for (int r = 1; r <= 13; r++) deck.add(new Card(s, r));

        Card[] hand = new Card[3];
        long total = enumerate(deck, 0, 0, hand, counts);

        Map<HandRank, Double> probs = new HashMap<>();
        for (HandRank r : HandRank.values()) {
            probs.put(r, counts.get(r) / (double) total);
        }
        return probs;
    }

    private static long enumerate(List<Card> deck, int start, int depth, 
                                   Card[] hand, Map<HandRank, Long> counts) {
        if (depth == 3) {
            HandEval ev = evaluate(hand.clone());
            counts.put(ev.rank, counts.get(ev.rank) + 1);
            return 1;
        }
        long total = 0;
        for (int i = start; i <= deck.size() - (3 - depth); i++) {
            hand[depth] = deck.get(i);
            total += enumerate(deck, i + 1, depth + 1, hand, counts);
        }
        return total;
    }

    /**
     * 贝叶斯推理引擎
     * 根据对手下注行为更新其牌型概率分布
     */
    static class BayesianEngine {
        // 下注行为类型
        enum Action { FOLD, CHECK, CALL, RAISE, ALL_IN }

        // 似然矩阵:P(行为|牌型) —— 基于经验统计
        // 行:牌型,列:行为
        private double[][] likelihood;
        private Map<HandRank, Double> prior;

        BayesianEngine() {
            prior = priorProbabilities();
            initLikelihood();
        }

        private void initLikelihood() {
            // 假设:强牌更可能加注,弱牌更可能弃牌
            // 行顺序:HIGH_CARD, PAIR, STRAIGHT, FLUSH, STRAIGHT_FLUSH, TRIPLE
            // 列顺序:FOLD, CHECK, CALL, RAISE, ALL_IN
            likelihood = new double[][] {
                {0.40, 0.30, 0.20, 0.08, 0.02}, // 单张:较大概率弃牌或看牌
                {0.20, 0.25, 0.30, 0.20, 0.05}, // 对子:中等概率跟注
                {0.10, 0.15, 0.30, 0.35, 0.10}, // 顺子:大概率加注
                {0.08, 0.12, 0.25, 0.40, 0.15}, // 同花:更可能加注
                {0.05, 0.08, 0.20, 0.42, 0.25}, // 同花顺:高概率加注或全押
                {0.02, 0.05, 0.15, 0.38, 0.40}  // 豹子:极高概率全押
            };
        }

        /**
         * 根据观测到的行为,更新对手牌型后验概率
         */
        Map<HandRank, Double> updatePosterior(Action action) {
            int actionIdx = action.ordinal();
            Map<HandRank, Double> posterior = new HashMap<>();
            HandRank[] ranks = HandRank.values();

            double evidence = 0;
            for (int i = 0; i < ranks.length; i++) {
                evidence += likelihood[i][actionIdx] * prior.get(ranks[i]);
            }

            for (int i = 0; i < ranks.length; i++) {
                double post = (likelihood[i][actionIdx] * prior.get(ranks[i])) / evidence;
                posterior.put(ranks[i], post);
            }

            // 更新先验为后验(递归贝叶斯更新)
            prior = posterior;
            return posterior;
        }

        /**
         * 计算对手牌型强于给定牌型的概率
         */
        double probStrongerThan(HandRank myRank) {
            double prob = 0;
            for (Map.Entry<HandRank, Double> e : prior.entrySet()) {
                if (e.getKey().level > myRank.level) {
                    prob += e.getValue();
                } else if (e.getKey().level == myRank.level) {
                    // 同等级时假设50%概率对手更大(简化处理)
                    prob += e.getValue() * 0.5;
                }
            }
            return prob;
        }
    }

    /**
     * 期望值决策:计算跟注、加注、弃牌的期望收益
     */
    static class DecisionEngine {

        /**
         * 计算跟注的期望收益
         * @param pot 底池大小
         * @param callAmount 跟注金额
         * @param winProb 获胜概率
         */
        static double expectedValueCall(double pot, double callAmount, double winProb) {
            return winProb * pot - (1 - winProb) * callAmount;
        }

        /**
         * 计算加注的期望收益(简化模型)
         * @param pot 底池
         * @param raiseAmount 加注金额
         * @param winProb 获胜概率
         * @param foldProb 对手弃牌概率
         */
        static double expectedValueRaise(double pot, double raiseAmount, 
                                          double winProb, double foldProb) {
            double winShowdown = winProb * (pot + raiseAmount);
            double winFold = foldProb * pot;
            double lose = (1 - winProb - foldProb) * raiseAmount;
            return winShowdown + winFold - lose;
        }

        /**
         * 推荐最优行动
         */
        static String recommendAction(double pot, double callAmount, double winProb, 
                                       double foldProb) {
            double evCall = expectedValueCall(pot, callAmount, winProb);
            double evRaise = expectedValueRaise(pot, callAmount * 2, winProb, foldProb);
            double evFold = 0;

            System.out.printf("EV(跟注)=%.2f, EV(加注)=%.2f, EV(弃牌)=%.2f%n", 
                evCall, evRaise, evFold);

            if (evRaise > evCall && evRaise > evFold && evRaise > 0) return "加注";
            if (evCall > evFold && evCall > 0) return "跟注";
            return "弃牌";
        }
    }

    /**
     * 计算在已知自己手牌和若干公共信息下,获胜的精确概率
     * 使用蒙特卡洛模拟对手随机手牌
     */
    public static double monteCarloWinRate(Card[] myHand, List<Card> knownCards, int iterations) {
        HandEval myEval = evaluate(myHand);
        List<Card> deck = generateDeck();
        deck.removeAll(knownCards);

        int wins = 0, ties = 0, total = 0;
        Random rand = new Random();

        for (int i = 0; i < iterations; i++) {
            Collections.shuffle(deck, rand);
            Card[] oppHand = new Card[3];
            for (int j = 0; j < 3; j++) oppHand[j] = deck.get(j);
            HandEval oppEval = evaluate(oppHand);

            int cmp = compareHands(myEval, oppEval);
            if (cmp > 0) wins++;
            else if (cmp == 0) ties++;
            total++;
        }

        return (wins + ties * 0.5) / total;
    }

    private static int compareHands(HandEval a, HandEval b) {
        if (a.rank.level != b.rank.level) return a.rank.level - b.rank.level;
        for (int i = 0; i < a.kickers.length && i < b.kickers.length; i++) {
            if (a.kickers[i] != b.kickers[i]) return a.kickers[i] - b.kickers[i];
        }
        return 0;
    }

    private static List<Card> generateDeck() {
        List<Card> deck = new ArrayList<>();
        for (int s = 0; s < 4; s++) for (int r = 1; r <= 13; r++) deck.add(new Card(s, r));
        return deck;
    }

    // 演示主方法
    public static void main(String[] args) {
        // 先验概率
        System.out.println("=== 诈金花牌型先验概率 ===");
        Map<HandRank, Double> prior = priorProbabilities();
        for (HandRank r : HandRank.values()) {
            System.out.printf("%-12s: %.4f%% (%1.6f)%n", r.name, prior.get(r) * 100, prior.get(r));
        }

        // 贝叶斯推理演示
        System.out.println("\n=== 贝叶斯推理演示 ===");
        BayesianEngine bayes = new BayesianEngine();

        System.out.println("初始先验概率(对手):");
        printPosterior(bayes.prior);

        System.out.println("\n观测到对手'加注'后,更新后验概率:");
        bayes.updatePosterior(BayesianEngine.Action.RAISE);
        printPosterior(bayes.prior);

        System.out.println("\n观测到对手'全押'后,再次更新:");
        bayes.updatePosterior(BayesianEngine.Action.ALL_IN);
        printPosterior(bayes.prior);

        // 蒙特卡洛胜率计算
        System.out.println("\n=== 蒙特卡洛胜率模拟 ===");
        Card[] myHand = {new Card(0, 13), new Card(1, 12), new Card(2, 11)}; // AKQ
        List<Card> known = new ArrayList<>(Arrays.asList(myHand));
        double winRate = monteCarloWinRate(myHand, known, 50000);
        System.out.printf("手牌 %s 的胜率: %.2f%%%n", Arrays.toString(myHand), winRate * 100);

        // 决策建议
        System.out.println("\n=== 决策建议 ===");
        HandEval myEval = evaluate(myHand);
        double probOppStronger = bayes.probStrongerThan(myEval.rank);
        double myWinProb = 1 - probOppStronger;
        System.out.printf("对手牌型强于你的概率: %.2f%%%n", probOppStronger * 100);
        String action = DecisionEngine.recommendAction(100, 10, myWinProb, 0.1);
        System.out.println("推荐行动: " + action);
    }

    static void printPosterior(Map<HandRank, Double> post) {
        for (HandRank r : HandRank.values()) {
            System.out.printf("  %-12s: %.2f%%%n", r.name, post.get(r) * 100);
        }
    }
}

算法复杂度分析

算法模块 时间复杂度 空间复杂度
牌型判定 O(1) O(1)
先验概率精确计算 O(C(52,3)) = O(22100) O(1)
贝叶斯更新 O( HandRank
蒙特卡洛胜率 O(N) O(1)
期望值决策 O(1) O(1)

扩展思考

  1. 对手建模:不同玩家的下注模式差异巨大,可通过在线学习(如指数加权移动平均)动态调整似然矩阵。
  2. 多轮博弈树:构建扩展式博弈树,使用反事实遗憾最小化(CFR)算法求解近似纳什均衡策略。
  3. 心理博弈层:引入”虚张声势”概率模型,分析对手在弱牌时选择加注的 Bluff 频率。