引言
炸弹猫(Exploding Kittens)是一款火爆的卡牌桌游,牌堆中隐藏着致命的炸弹猫,玩家通过抽牌和出牌技能来避免引爆炸弹。本文用Java实现炸弹猫的AI决策系统,核心算法为卡牌概率推理、风险量化评估与最优抽牌策略,让AI能够像老手玩家一样根据场上信息推断炸弹位置并做出最优决策。
游戏规则与核心机制
牌堆包含多种卡牌:炸弹猫(Exploding Kitten)、拆除(Defuse)、跳过(Skip)、透视(See the Future)、洗牌(Shuffle)、攻击(Attack)、 favor 等。每位玩家回合必须抽一张牌,若抽到炸弹猫且没有拆除则出局。
核心算法设计
1. 概率推理引擎
AI维护对牌堆中每张未知牌的贝叶斯概率估计。根据已知信息(已出牌、已透视的牌、已抽走的牌)不断更新各位置为炸弹的概率。
2. 风险量化评估
对每个可能的行动(抽牌、使用跳过、使用透视等),计算期望损失:
风险值 = 炸弹概率 × 出局代价 + (1 - 炸弹概率) × 机会成本
3. 最优决策选择
AI在所有合法行动中选择风险最小、期望收益最大的策略。当炸弹概率超过阈值时,优先使用透视确认牌堆顶部;当确认安全时则大胆抽牌积累手牌优势。
Java完整实现
import java.util.*;
/**
* 炸弹猫AI决策系统
* 实现卡牌概率推理、风险量化与最优决策
*/
public class ExplodingKittensAI {
// 卡牌类型
enum CardType {
EXPLODING_KITTEN, DEFUSE, SKIP, SEE_THE_FUTURE,
SHUFFLE, ATTACK, FAVOR, NOPE, NORMAL
}
// 游戏状态
private List<CardType> deck; // 牌堆(从底到顶)
private List<CardType> hand; // AI手牌
private List<CardType> discardPile; // 弃牌堆
private int totalPlayers; // 总玩家数
private int alivePlayers; // 存活玩家数
private int explodingKittensInDeck; // 牌堆中炸弹猫数量
private int deckSize; // 牌堆总张数
// 概率推理状态
private double[] bombProb; // 每个位置是炸弹的概率
private List<CardType> knownTopCards; // 通过透视已知的顶部牌
public ExplodingKittensAI(int totalPlayers) {
this.totalPlayers = totalPlayers;
this.alivePlayers = totalPlayers;
this.hand = new ArrayList<>();
this.discardPile = new ArrayList<>();
this.knownTopCards = new ArrayList<>();
initDeck();
}
/**
* 初始化牌堆(简化版,每种卡牌数量可调)
*/
private void initDeck() {
deck = new ArrayList<>();
explodingKittensInDeck = totalPlayers - 1;
for (int i = 0; i < explodingKittensInDeck; i++)
deck.add(CardType.EXPLODING_KITTEN);
for (int i = 0; i < 6; i++) deck.add(CardType.DEFUSE);
for (int i = 0; i < 5; i++) deck.add(CardType.SKIP);
for (int i = 0; i < 5; i++) deck.add(CardType.SEE_THE_FUTURE);
for (int i = 0; i < 4; i++) deck.add(CardType.SHUFFLE);
for (int i = 0; i < 4; i++) deck.add(CardType.ATTACK);
for (int i = 0; i < 4; i++) deck.add(CardType.FAVOR);
for (int i = 0; i < 5; i++) deck.add(CardType.NORMAL);
Collections.shuffle(deck);
deckSize = deck.size();
bombProb = new double[deckSize];
updateBombProbability();
}
/**
* 更新每个牌堆位置的炸弹概率
* 基于已知信息和贝叶斯推理
*/
private void updateBombProbability() {
int unknownPositions = deck.size() - knownTopCards.size();
if (unknownPositions <= 0) return;
// 计算已知的非炸弹数量
int knownNonBombs = 0;
for (CardType c : knownTopCards) {
if (c != CardType.EXPLODING_KITTEN) knownNonBombs++;
}
// 剩余未知牌中的炸弹数量
int remainingBombs = explodingKittensInDeck
- countKnownBombsInDeck();
int remainingUnknown = unknownPositions;
// 基础概率:均匀分布假设
double baseProb = remainingBombs > 0 ?
(double) remainingBombs / remainingUnknown : 0;
// 更新每个位置
for (int i = 0; i < deck.size(); i++) {
if (i < knownTopCards.size()) {
// 已知位置的牌
bombProb[i] = knownTopCards.get(i) == CardType.EXPLODING_KITTEN ? 1.0 : 0.0;
} else {
bombProb[i] = baseProb;
}
}
}
private int countKnownBombsInDeck() {
int count = 0;
for (CardType c : knownTopCards) {
if (c == CardType.EXPLODING_KITTEN) count++;
}
return count;
}
/**
* AI主决策函数:在当前回合选择最优行动
*/
public Decision makeDecision() {
List<Decision> candidates = new ArrayList<>();
// 选项1:使用SKIP跳过抽牌
if (hand.contains(CardType.SKIP)) {
double risk = 0; // SKIP完全规避抽牌风险
candidates.add(new Decision(Action.USE_SKIP, risk,
"使用SKIP跳过本轮抽牌,零风险"));
}
// 选项2:使用SEE_THE_FUTURE透视顶部3张
if (hand.contains(CardType.SEE_THE_FUTURE)) {
double risk = 0.1; // 微小机会成本
candidates.add(new Decision(Action.USE_SEE_FUTURE, risk,
"使用透视获取信息,降低后续不确定性"));
}
// 选项3:使用SHUFFLE洗牌
if (hand.contains(CardType.SHUFFLE)) {
double risk = 0.2; // 洗牌后需重新估计概率
candidates.add(new Decision(Action.USE_SHUFFLE, risk,
"洗牌打乱牌堆,可能降低顶部炸弹概率"));
}
// 选项4:使用ATTACK转移回合
if (hand.contains(CardType.ATTACK)) {
double risk = 0.05; // 攻击本身无风险,但会消耗手牌
candidates.add(new Decision(Action.USE_ATTACK, risk,
"攻击下家,转移抽牌义务"));
}
// 选项5:直接抽牌
double drawRisk = bombProb.length > 0 ? bombProb[0] : 0;
// 如果有DEFUSE,抽到炸弹也可化解,降低有效风险
if (hand.contains(CardType.DEFUSE)) {
drawRisk *= 0.1; // 90%的炸弹可被拆除
}
candidates.add(new Decision(Action.DRAW_CARD, drawRisk,
String.format("直接抽牌,当前顶部炸弹概率: %.1f%%",
bombProb.length > 0 ? bombProb[0] * 100 : 0)));
// 选择风险最低的决策
Decision best = candidates.stream()
.min(Comparator.comparingDouble(d -> d.risk))
.orElse(candidates.get(0));
System.out.println("AI决策分析:");
for (Decision d : candidates) {
System.out.printf(" %-20s 风险值: %.3f | %s%n",
d.action, d.risk, d.reason);
}
System.out.println("=> 选择: " + best.action + "\n");
return best;
}
/**
* 执行决策并更新游戏状态
*/
public void executeDecision(Decision decision) {
switch (decision.action) {
case USE_SKIP:
hand.remove(CardType.SKIP);
discardPile.add(CardType.SKIP);
System.out.println("AI使用SKIP,跳过本轮抽牌");
break;
case USE_SEE_FUTURE:
hand.remove(CardType.SEE_THE_FUTURE);
discardPile.add(CardType.SEE_THE_FUTURE);
// 透视顶部3张牌
knownTopCards.clear();
for (int i = 0; i < Math.min(3, deck.size()); i++) {
knownTopCards.add(deck.get(deck.size() - 1 - i));
System.out.println("AI透视看到第" + (i+1) + "张: " + deck.get(deck.size() - 1 - i));
}
updateBombProbability();
break;
case USE_SHUFFLE:
hand.remove(CardType.SHUFFLE);
discardPile.add(CardType.SHUFFLE);
Collections.shuffle(deck);
knownTopCards.clear(); // 洗牌后透视信息失效
updateBombProbability();
System.out.println("AI使用SHUFFLE,牌堆已重新洗牌");
break;
case USE_ATTACK:
hand.remove(CardType.ATTACK);
discardPile.add(CardType.ATTACK);
System.out.println("AI使用ATTACK,下家需连续抽2张牌");
break;
case DRAW_CARD:
if (deck.isEmpty()) {
System.out.println("牌堆已空!");
return;
}
CardType drawn = deck.remove(deck.size() - 1); // 从顶部抽牌
deckSize--;
System.out.println("AI抽牌: " + drawn);
if (drawn == CardType.EXPLODING_KITTEN) {
if (hand.contains(CardType.DEFUSE)) {
hand.remove(CardType.DEFUSE);
discardPile.add(CardType.DEFUSE);
System.out.println("抽到炸弹猫!AI使用DEFUSE成功拆除!");
// 将炸弹猫放回牌堆任意位置(AI会选择最安全的位置)
int safePos = findSafestPosition();
deck.add(safePos, CardType.EXPLODING_KITTEN);
System.out.println("AI将炸弹猫放回牌堆第" + (deck.size() - safePos) + "张位置");
} else {
System.out.println("抽到炸弹猫!AI没有DEFUSE,出局!");
alivePlayers--;
}
} else {
hand.add(drawn);
}
// 更新透视信息
if (!knownTopCards.isEmpty()) knownTopCards.remove(0);
updateBombProbability();
break;
}
}
/**
* 找到牌堆中最安全的位置放回炸弹猫
*/
private int findSafestPosition() {
// AI会选择距离顶部最远的位置,让其他玩家先承担风险
// 或者选择已知安全的区域
if (deck.isEmpty()) return 0;
// 默认放回底部(索引0)
return 0;
}
/**
* 模拟对手抽牌后的信息更新(通过弃牌堆推断)
*/
public void observeOpponentDraw(int opponentId, boolean survived) {
if (!survived) {
alivePlayers--;
System.out.println("玩家" + opponentId + "抽到炸弹猫出局!");
// 牌堆中少了一张炸弹猫
explodingKittensInDeck--;
}
updateBombProbability();
}
/**
* 模拟对手出牌的信息更新
*/
public void observeOpponentPlay(int opponentId, CardType card) {
discardPile.add(card);
if (card == CardType.SEE_THE_FUTURE) {
// 对手使用了透视,但AI不知道看到了什么
// 可假设对手获得信息后风险降低,因此AI应更谨慎
System.out.println("对手使用透视,AI提高警惕");
}
updateBombProbability();
}
public void printStatus() {
System.out.printf("=== AI状态 ===%n手牌: %s%n牌堆剩余: %d 张%n", hand, deck.size());
if (bombProb.length > 0) {
System.out.printf("顶部炸弹概率: %.1f%%%n", bombProb[bombProb.length - 1] * 100);
}
System.out.println();
}
enum Action {
DRAW_CARD, USE_SKIP, USE_SEE_FUTURE,
USE_SHUFFLE, USE_ATTACK
}
static class Decision {
Action action;
double risk;
String reason;
Decision(Action a, double r, String reason) {
this.action = a; this.risk = r; this.reason = reason;
}
}
public static void main(String[] args) {
ExplodingKittensAI ai = new ExplodingKittensAI(4);
// 给AI一些初始手牌
ai.hand.add(CardType.DEFUSE);
ai.hand.add(CardType.SKIP);
ai.hand.add(CardType.SEE_THE_FUTURE);
System.out.println("=== 炸弹猫AI决策模拟 ===\n");
for (int round = 1; round <= 10; round++) {
System.out.println("======== 第 " + round + " 回合 ========");
ai.printStatus();
Decision decision = ai.makeDecision();
ai.executeDecision(decision);
if (ai.alivePlayers <= 1) break;
}
System.out.println("模拟结束!存活玩家数: " + ai.alivePlayers);
}
}
算法亮点解析
贝叶斯概率更新
AI通过维护已知的顶部牌信息(knownTopCards),精确计算每个未知位置的条件炸弹概率。当透视牌被使用后,顶部几张牌的概率被精确设为0或1,大幅提升了后续决策的准确性。
风险量化模型
每个候选行动都被赋予一个数值化的风险值。风险值不仅考虑直接后果(如抽牌时的炸弹概率),还考虑机会成本(如消耗SKIP意味着未来少一张保命牌)。这种统一量化的方式让不同行动之间可以直接比较。
炸弹猫的主动放置策略
当AI用DEFUSE化解炸弹后,需要选择将炸弹放回牌堆的位置。本实现选择放回底部(索引0),这是纳什均衡策略——将风险最大化地转移给后续玩家。更复杂的AI还可以根据各玩家的手牌猜测和存活状态选择最有利的位置。
复杂度分析
- 时间复杂度:概率更新为O(D),D为牌堆深度。决策评估为O(|A|),A为候选行动集合。
- 空间复杂度:O(D)存储牌堆和概率数组。
扩展方向
- 对手建模:根据每位玩家的出牌历史推断其手牌组成,调整炸弹概率估计
- 蒙特卡洛模拟:对复杂的多轮决策使用MC模拟评估长期期望收益
- 欺骗策略:偶尔在”安全”时故意使用透视,误导对手的概率推断
- 多人博弈分析:引入Shapley值或纳什均衡概念分析最优策略
总结
炸弹猫AI展示了不完全信息博弈中的概率推理方法。通过贝叶斯更新维护对牌堆状态的置信分布,并用风险量化模型进行决策,AI能够在充满不确定性的卡牌游戏中做出近乎最优的选择,是信息论与博弈论在游戏AI中的经典结合。