每日算法 — 使用java实现狼人杀:贝叶斯网络身份推理与信息增益最优投票

引言

狼人杀是一款经典的社交推理游戏,玩家分为狼人阵营与好人阵营,通过发言和投票淘汰对方。对AI而言,核心挑战是不完全信息下的概率推理:每个玩家的真实身份隐藏,只能根据公开行为(投票、发言、被查验结果)不断更新信念。本文用Java实现一个基于贝叶斯更新的身份推理引擎,并结合信息增益计算最优投票策略。

贝叶斯身份推理模型

设有N名玩家,其中W个狼人,N-W个好人(含预言家、女巫等特殊角色,简化模型中统一为平民与神职)。每个玩家i有一个隐藏身份变量R_i ∈ {狼人, 好人}。

贝叶斯推理的核心公式:

P(R_i=狼人 | 证据E) = P(E | R_i=狼人) · P(R_i=狼人) / P(E)

证据E包括:
– 玩家j投票给玩家k → 若j是狼人,k更可能是好人(狼人倾向于抗推好人)
– 玩家j被预言家查验为好人/狼人 → 直接观测
– 玩家j在白天被投出 → 公布身份,更新全局信念

信息增益投票策略

AI需要决定投票给哪位玩家。信息增益(Information Gain)策略不仅考虑”谁最可能是狼人”,还考虑”投出谁能带来最多信息”:

IG(vote=j) = H(当前) – E[H(投出j后)]

其中H为身份分布的熵。若某位玩家身份已明确,投出他信息增益低;若某位玩家的死活能大幅改变其他玩家的身份分布,则信息增益高。

import java.util.*;

/**
 * 狼人杀AI核心:贝叶斯身份推理 + 信息增益最优投票
 * 简化模型:N玩家,W狼人,其余为好人(含预言家等)
 */
public class WerewolfBayesianAI {

    public static final int WEREWOLF = 1;
    public static final int VILLAGER = 0; // 包含所有好人阵营

    private final int nPlayers;
    private final int nWerewolves;
    // beliefs[i] = P(玩家i是狼人 | 当前证据)
    private final double[] beliefs;
    // 存活状态
    private final boolean[] alive;
    // 已确认身份(-1未知,0好人,1狼人)
    private final int[] confirmed;
    // 历史证据列表
    private final List<Evidence> evidenceList;

    // 转移概率参数(经验值)
    private static final double P_WW_VOTE_VILLAGER = 0.75; // 狼人投好人的概率
    private static final double P_WW_VOTE_WEREWOLF = 0.10; // 狼人投狼队友的概率(卖队友)
    private static final double P_VILLAGER_RANDOM_VOTE = 0.60; // 平民随机投票

    public WerewolfBayesianAI(int nPlayers, int nWerewolves) {
        this.nPlayers = nPlayers;
        this.nWerewolves = nWerewolves;
        this.beliefs = new double[nPlayers];
        this.alive = new boolean[nPlayers];
        this.confirmed = new int[nPlayers];
        this.evidenceList = new ArrayList<>();

        // 初始化:先验概率
        double prior = (double) nWerewolves / nPlayers;
        for (int i = 0; i < nPlayers; i++) {
            beliefs[i] = prior;
            alive[i] = true;
            confirmed[i] = -1;
        }
    }

    /**
     * 证据类型枚举
     */
    enum EvidenceType {
        VOTE,           // 玩家A投票给玩家B
        CHECK_RESULT,   // 预言家查验结果
        DEATH_SPEECH,   // 遗言(被投出或夜间死亡)
        BEHAVIOR_SCORE  // 行为评分(正数偏狼,负数偏好人)
    }

    static class Evidence {
        EvidenceType type;
        int from, to;     // 相关玩家
        double value;     // 数值证据(如查验结果1=狼,0=好人;行为分数)
        int day;          // 第几天

        Evidence(EvidenceType type, int from, int to, double value, int day) {
            this.type = type; this.from = from; this.to = to;
            this.value = value; this.day = day;
        }
    }

    /**
     * 添加证据并更新信念
     */
    public void addEvidence(Evidence e) {
        evidenceList.add(e);
        updateBeliefs();
    }

    /**
     * 执行一轮完整的贝叶斯更新
     * 对每个未确认身份的玩家,基于所有证据重新计算后验概率
     */
    private void updateBeliefs() {
        for (int i = 0; i < nPlayers; i++) {
            if (confirmed[i] != -1 || !alive[i]) continue;

            double prior = beliefs[i];
            double logLikelihoodWolf = Math.log(prior + 1e-10);
            double logLikelihoodVillager = Math.log(1 - prior + 1e-10);

            for (Evidence e : evidenceList) {
                if (e.type == EvidenceType.VOTE && e.from == i) {
                    // 玩家i投票给e.to
                    double[] lh = voteLikelihood(i, e.to);
                    logLikelihoodWolf += Math.log(lh[1] + 1e-10);
                    logLikelihoodVillager += Math.log(lh[0] + 1e-10);
                } else if (e.type == EvidenceType.VOTE && e.to == i) {
                    // 其他玩家投票给i,视为对i的间接评价
                    double[] lh = receivedVoteLikelihood(e.from, i);
                    logLikelihoodWolf += Math.log(lh[1] + 1e-10);
                    logLikelihoodVillager += Math.log(lh[0] + 1e-10);
                } else if (e.type == EvidenceType.CHECK_RESULT && e.to == i) {
                    // 直接查验结果
                    if (e.value > 0.5) {
                        logLikelihoodWolf += 2.0; // 强证据
                        logLikelihoodVillager -= 2.0;
                    } else {
                        logLikelihoodWolf -= 2.0;
                        logLikelihoodVillager += 2.0;
                    }
                } else if (e.type == EvidenceType.BEHAVIOR_SCORE && e.from == i) {
                    // 行为评分:正数偏狼
                    logLikelihoodWolf += e.value * 0.5;
                    logLikelihoodVillager -= e.value * 0.5;
                }
            }

            // 归一化(softmax近似)
            double odds = Math.exp(logLikelihoodWolf - logLikelihoodVillager);
            beliefs[i] = odds / (1 + odds);

            // 约束:全局狼人数量期望守恒
            // E[狼人数] = sum(beliefs) 应近似等于剩余狼人数
        }

        // 后处理:应用全局约束(剩余狼人数期望)
        applyGlobalConstraint();
    }

    /**
     * 投票行为的似然函数
     * 返回 [P(投票|好人), P(投票|狼人)]
     */
    private double[] voteLikelihood(int voter, int target) {
        // 好人倾向于随机投票或跟票
        double pVillagerVote = P_VILLAGER_RANDOM_VOTE / (nPlayers - 1);
        // 狼人倾向于投好人,少投队友
        double pWolfVote;
        if (beliefs[target] < 0.3) {
            pWolfVote = P_WW_VOTE_VILLAGER; // 目标像好人,狼人积极投
        } else if (beliefs[target] > 0.7) {
            pWolfVote = P_WW_VOTE_WEREWOLF; // 目标像狼队友,可能卖队友
        } else {
            pWolfVote = 0.30; // 不确定时中等概率
        }
        return new double[]{pVillagerVote, pWolfVote};
    }

    /**
     * 被投票的似然(从投票者角度分析被投者身份)
     * 返回 [P(被投|被投者是好人), P(被投|被投者是狼人)]
     */
    private double[] receivedVoteLikelihood(int voter, int target) {
        // 如果voter是狼人且投target,target更可能是好人
        double pVoterWolf = beliefs[voter];
        double pTargetIsVillagerGivenWolfVote = P_WW_VOTE_VILLAGER;
        double pTargetIsWolfGivenWolfVote = P_WW_VOTE_WEREWOLF;

        double pTargetVillager = pVoterWolf * pTargetIsVillagerGivenWolfVote
                               + (1 - pVoterWolf) * (1.0 / (nPlayers - 1));
        double pTargetWolf = pVoterWolf * pTargetIsWolfGivenWolfVote
                           + (1 - pVoterWolf) * (1.0 / (nPlayers - 1));
        return new double[]{pTargetVillager, pTargetWolf};
    }

    /**
     * 应用全局约束:所有存活未确认玩家的狼人概率之和 ≈ 剩余狼人数
     * 使用迭代比例拟合(IPF)进行校正
     */
    private void applyGlobalConstraint() {
        int remainingWolves = nWerewolves;
        int remainingPlayers = 0;
        double sumBeliefs = 0;
        for (int i = 0; i < nPlayers; i++) {
            if (confirmed[i] == WEREWOLF) remainingWolves--;
            if (alive[i] && confirmed[i] == -1) {
                remainingPlayers++;
                sumBeliefs += beliefs[i];
            }
        }
        if (remainingPlayers == 0) return;
        // 期望守恒:若sum > remainingWolves,整体下调;反之上调
        double scale = remainingWolves / Math.max(sumBeliefs, 0.01);
        // 限制缩放范围,避免极端值
        scale = Math.max(0.3, Math.min(3.0, scale));
        for (int i = 0; i < nPlayers; i++) {
            if (alive[i] && confirmed[i] == -1) {
                beliefs[i] = Math.max(0.01, Math.min(0.99, beliefs[i] * scale));
            }
        }
    }

    /**
     * 确认某位玩家身份(被投出后公布,或查验确认)
     */
    public void confirmIdentity(int player, int identity) {
        confirmed[player] = identity;
        beliefs[player] = (identity == WEREWOLF) ? 1.0 : 0.0;
        if (identity == WEREWOLF) {
            alive[player] = false;
        }
    }

    /**
     * 淘汰玩家(白天被投出或夜间死亡)
     */
    public void eliminate(int player) {
        alive[player] = false;
        // 若死亡时身份已确认,更新全局
        if (confirmed[player] != -1) {
            updateBeliefs();
        }
    }

    /**
     * 计算当前身份分布的熵
     */
    public double calculateEntropy() {
        double entropy = 0;
        for (int i = 0; i < nPlayers; i++) {
            if (!alive[i] || confirmed[i] != -1) continue;
            double p = beliefs[i];
            double q = 1 - p;
            if (p > 0.01) entropy -= p * Math.log(p);
            if (q > 0.01) entropy -= q * Math.log(q);
        }
        return entropy;
    }

    /**
     * 计算投出某位玩家后的期望熵(信息增益核心)
     */
    public double expectedEntropyAfterEliminate(int target) {
        if (!alive[target]) return Double.MAX_VALUE;
        double pWolf = beliefs[target];
        double pVillager = 1 - pWolf;

        // 模拟:若target是狼人
        double entropyIfWolf = 0;
        if (pWolf > 0.01) {
            // 临时状态保存
            double[] saved = beliefs.clone();
            int savedConfirmed = confirmed[target];
            confirmed[target] = WEREWOLF;
            alive[target] = false;
            updateBeliefs();
            entropyIfWolf = calculateEntropy();
            // 恢复
            System.arraycopy(saved, 0, beliefs, 0, nPlayers);
            confirmed[target] = savedConfirmed;
            alive[target] = true;
        }

        // 模拟:若target是好人
        double entropyIfVillager = 0;
        if (pVillager > 0.01) {
            double[] saved = beliefs.clone();
            int savedConfirmed = confirmed[target];
            confirmed[target] = VILLAGER;
            alive[target] = false;
            updateBeliefs();
            entropyIfVillager = calculateEntropy();
            System.arraycopy(saved, 0, beliefs, 0, nPlayers);
            confirmed[target] = savedConfirmed;
            alive[target] = true;
        }

        return pWolf * entropyIfWolf + pVillager * entropyIfVillager;
    }

    /**
     * 选择最优投票目标:综合"最可能是狼人"和"信息增益最大"
     */
    public int chooseVoteTarget() {
        double bestScore = -Double.MAX_VALUE;
        int bestTarget = -1;
        double currentEntropy = calculateEntropy();

        for (int i = 0; i < nPlayers; i++) {
            if (!alive[i] || confirmed[i] != -1) continue;

            double pWolf = beliefs[i];
            double expEntropy = expectedEntropyAfterEliminate(i);
            double infoGain = currentEntropy - expEntropy;

            // 综合评分:狼人概率权重 + 信息增益权重
            double score = 0.6 * pWolf + 0.4 * (infoGain / Math.max(currentEntropy, 0.1));

            if (score > bestScore) {
                bestScore = score;
                bestTarget = i;
            }
        }
        return bestTarget;
    }

    /**
     * 获取当前信念分布字符串
     */
    public String getBeliefString() {
        StringBuilder sb = new StringBuilder("信念分布(狼人概率):\n");
        for (int i = 0; i < nPlayers; i++) {
            sb.append(String.format("  P%d: %.3f %s %s\n", i, beliefs[i],
                alive[i] ? "" : "[死亡]",
                confirmed[i] == -1 ? "" : (confirmed[i] == WEREWOLF ? "[狼人]" : "[好人]")));
        }
        return sb.toString();
    }

    public static void main(String[] args) {
        // 12人局:4狼人,8好人
        WerewolfBayesianAI ai = new WerewolfBayesianAI(12, 4);
        System.out.println("=== 初始状态 ===");
        System.out.println(ai.getBeliefString());
        System.out.println("当前熵: " + ai.calculateEntropy());

        // 第1天:预言家查验P3为狼人
        System.out.println("\n=== 证据:预言家查验 P3=狼人 ===");
        ai.addEvidence(new Evidence(EvidenceType.CHECK_RESULT, 0, 3, 1.0, 1));
        System.out.println(ai.getBeliefString());

        // P3被投出,确认狼人
        ai.confirmIdentity(3, WEREWOLF);
        ai.eliminate(3);
        System.out.println("P3被投出确认为狼人。更新后:");
        System.out.println(ai.getBeliefString());

        // 第1天投票记录:P1投P5, P2投P5, P4投P5, P5投P6
        System.out.println("\n=== 证据:投票记录 ===");
        ai.addEvidence(new Evidence(EvidenceType.VOTE, 1, 5, 0, 1));
        ai.addEvidence(new Evidence(EvidenceType.VOTE, 2, 5, 0, 1));
        ai.addEvidence(new Evidence(EvidenceType.VOTE, 4, 5, 0, 1));
        ai.addEvidence(new Evidence(EvidenceType.VOTE, 5, 6, 0, 1));
        System.out.println(ai.getBeliefString());

        // 行为评分:P7表现激进(偏狼)
        System.out.println("\n=== 证据:P7行为评分+1.5(偏狼) ===");
        ai.addEvidence(new Evidence(EvidenceType.BEHAVIOR_SCORE, 7, -1, 1.5, 1));
        System.out.println(ai.getBeliefString());

        // 最优投票决策
        int target = ai.chooseVoteTarget();
        System.out.println("\n=== AI投票决策 ===");
        System.out.println("推荐投票目标: P" + target);
        System.out.println("P" + target + "的狼人概率: " + String.format("%.3f", ai.beliefs[target]));
        System.out.println("当前熵: " + String.format("%.3f", ai.calculateEntropy()));

        // 计算每位存活玩家的信息增益
        System.out.println("\n=== 各目标信息增益分析 ===");
        double curEnt = ai.calculateEntropy();
        for (int i = 0; i < 12; i++) {
            if (!ai.alive[i] || ai.confirmed[i] != -1) continue;
            double ee = ai.expectedEntropyAfterEliminate(i);
            double ig = curEnt - ee;
            System.out.printf("P%d: 狼人概率=%.3f, 期望熵=%.3f, 信息增益=%.3f%n",
                i, ai.beliefs[i], ee, ig);
        }

        // 性能测试
        System.out.println("\n=== 性能测试 ===");
        long start = System.nanoTime();
        for (int iter = 0; iter < 10000; iter++) {
            WerewolfBayesianAI test = new WerewolfBayesianAI(12, 4);
            for (int d = 0; d < 5; d++) {
                test.addEvidence(new Evidence(EvidenceType.VOTE, d, d + 1, 0, d));
                test.addEvidence(new Evidence(EvidenceType.BEHAVIOR_SCORE, d + 5, -1, 0.5, d));
            }
            test.chooseVoteTarget();
        }
        long ns = System.nanoTime() - start;
        System.out.println("10000轮完整推理平均耗时: " + (ns / 10000.0 / 1000) + " μs");
    }
}

贝叶斯更新的关键设计

  1. 对数域计算:多个概率连乘会导致下溢,本文转为对数域加法,最后用softmax归一化
  2. 全局约束:单纯独立更新会导致期望狼人数偏离实际,IPF校正保证E[狼人数] = 剩余狼人数
  3. 似然函数建模:投票行为的似然是核心,需根据游戏经验设定参数(如狼人投好人概率75%)

信息增益策略的意义

传统AI只投概率最高的狼人,但信息增益策略还考虑:
– 若某位玩家的身份能解锁大量其他玩家的身份推断,优先投他以获取信息
– 在置信度高时(残局),信息增益趋近于0,策略退化为纯概率最大化
– 这一框架可自然扩展到夜间刀人、女巫救药等决策

复杂度分析

操作 复杂度 说明
单条证据更新 O(n·e) n玩家数,e证据数
全局约束校正 O(n) 迭代比例拟合
信息增益计算 O(n²) 对每个候选模拟两种结果
最优投票选择 O(n²) n个候选各计算信息增益

对于12人局,单次决策在毫秒级,完全满足实时需求。

总结

本文用Java实现了狼人杀AI的核心推理引擎:贝叶斯更新将离散的发言和投票行为转化为连续的身份概率,信息增益策略在”投狼人”与”获取信息”之间取得平衡。这套框架不仅适用于狼人杀,也可推广到阿瓦隆、密室逃脱等不完全信息推理游戏。