引言
狼人杀是一款经典的社交推理游戏,玩家分为狼人阵营与好人阵营,通过发言和投票淘汰对方。对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");
}
}
贝叶斯更新的关键设计
- 对数域计算:多个概率连乘会导致下溢,本文转为对数域加法,最后用softmax归一化
- 全局约束:单纯独立更新会导致期望狼人数偏离实际,IPF校正保证
E[狼人数] = 剩余狼人数 - 似然函数建模:投票行为的似然是核心,需根据游戏经验设定参数(如狼人投好人概率75%)
信息增益策略的意义
传统AI只投概率最高的狼人,但信息增益策略还考虑:
– 若某位玩家的身份能解锁大量其他玩家的身份推断,优先投他以获取信息
– 在置信度高时(残局),信息增益趋近于0,策略退化为纯概率最大化
– 这一框架可自然扩展到夜间刀人、女巫救药等决策
复杂度分析
| 操作 | 复杂度 | 说明 |
|---|---|---|
| 单条证据更新 | O(n·e) | n玩家数,e证据数 |
| 全局约束校正 | O(n) | 迭代比例拟合 |
| 信息增益计算 | O(n²) | 对每个候选模拟两种结果 |
| 最优投票选择 | O(n²) | n个候选各计算信息增益 |
对于12人局,单次决策在毫秒级,完全满足实时需求。
总结
本文用Java实现了狼人杀AI的核心推理引擎:贝叶斯更新将离散的发言和投票行为转化为连续的身份概率,信息增益策略在”投狼人”与”获取信息”之间取得平衡。这套框架不仅适用于狼人杀,也可推广到阿瓦隆、密室逃脱等不完全信息推理游戏。