引言
猜拳(石头剪刀布)是人类最古老的游戏之一,看似简单却蕴含丰富的博弈论与概率学原理。当对手不是完全随机出拳时,其行为往往存在可预测的模式。本文将用Java实现一个智能猜拳AI,通过马尔可夫链建模对手出拳序列的状态转移规律,并结合频率统计进行自适应对抗,让AI在有限轮次内逐步占据优势。
核心算法思路
马尔可夫链状态转移
马尔可夫链的核心假设是:下一时刻的状态仅依赖于当前时刻的状态。在猜拳场景中,我们可以将对手上一次出的拳作为当前状态,预测其下一次最可能出的拳。具体实现时,维护一个 3×3 的转移计数矩阵,记录从每一种拳到另一种拳的转移次数。
频率自适应策略
单独使用马尔可夫链可能在对手出拳模式变化时反应滞后。因此引入频率统计作为辅助:记录对手历史出拳中石头、剪刀、布各自出现的次数,选择出现频率最高的拳的克制拳作为备选策略。最终决策通过加权融合马尔可夫预测与频率统计的结果。
反制逻辑
一旦预测出对手最可能出的拳,AI选择能够克制该拳的选项:石头克剪刀、剪刀克布、布克石头。
完整Java实现
import java.util.*;
/**
* 智能猜拳AI —— 基于马尔可夫链与频率统计的自适应对抗
*/
public class SmartRPS {
// 出拳枚举:0=石头, 1=剪刀, 2=布
public enum Move { ROCK, SCISSORS, PAPER }
// 马尔可夫转移计数矩阵:transCount[prev][next]
private final int[][] transCount = new int[3][3];
// 频率统计
private final int[] freqCount = new int[3];
// 历史出拳记录
private final List<Move> history = new ArrayList<>();
// 权重参数
private static final double MARKOV_WEIGHT = 0.7;
private static final double FREQ_WEIGHT = 0.3;
/**
* 将Move转为索引
*/
private int idx(Move m) {
return m.ordinal();
}
/**
* 获取克制指定出拳的选项
*/
private Move counter(Move m) {
return switch (m) {
case ROCK -> Move.PAPER; // 布克石头
case SCISSORS -> Move.ROCK; // 石头克剪刀
case PAPER -> Move.SCISSORS; // 剪刀克布
};
}
/**
* 记录对手的出拳,更新马尔可夫矩阵与频率统计
*/
public void recordOpponentMove(Move move) {
int curIdx = idx(move);
freqCount[curIdx]++;
if (!history.isEmpty()) {
int prevIdx = idx(history.get(history.size() - 1));
transCount[prevIdx][curIdx]++;
}
history.add(move);
}
/**
* 基于马尔可夫链预测对手下一次出拳
*/
private double[] markovPredict() {
double[] prob = new double[3];
if (history.isEmpty()) {
Arrays.fill(prob, 1.0 / 3);
return prob;
}
int lastIdx = idx(history.get(history.size() - 1));
int total = Arrays.stream(transCount[lastIdx]).sum();
if (total == 0) {
Arrays.fill(prob, 1.0 / 3);
return prob;
}
for (int i = 0; i < 3; i++) {
prob[i] = (double) transCount[lastIdx][i] / total;
}
return prob;
}
/**
* 基于频率统计预测对手下一次出拳
*/
private double[] freqPredict() {
double[] prob = new double[3];
int total = Arrays.stream(freqCount).sum();
if (total == 0) {
Arrays.fill(prob, 1.0 / 3);
return prob;
}
for (int i = 0; i < 3; i++) {
prob[i] = (double) freqCount[i] / total;
}
return prob;
}
/**
* 综合预测并选择AI出拳
*/
public Move chooseMove() {
double[] markov = markovPredict();
double[] freq = freqPredict();
double[] combined = new double[3];
for (int i = 0; i < 3; i++) {
combined[i] = MARKOV_WEIGHT * markov[i] + FREQ_WEIGHT * freq[i];
}
// 选择概率最高的对手出拳,并反制
int bestOpponent = 0;
for (int i = 1; i < 3; i++) {
if (combined[i] > combined[bestOpponent]) {
bestOpponent = i;
}
}
return counter(Move.values()[bestOpponent]);
}
/**
* 判定单轮胜负:1=AI胜, -1=对手胜, 0=平局
*/
public int judge(Move ai, Move opponent) {
if (ai == opponent) return 0;
return (idx(ai) - idx(opponent) + 3) % 3 == 1 ? 1 : -1;
}
/**
* 运行模拟对战
*/
public static void main(String[] args) {
SmartRPS ai = new SmartRPS();
Random rand = new Random(42);
int aiWins = 0, oppWins = 0, draws = 0;
// 模拟对手:前30轮偏好石头,之后偏好剪刀
for (int round = 1; round <= 100; round++) {
Move opponentMove;
if (round <= 30) {
opponentMove = rand.nextDouble() < 0.6 ? Move.ROCK :
(rand.nextBoolean() ? Move.SCISSORS : Move.PAPER);
} else {
opponentMove = rand.nextDouble() < 0.6 ? Move.SCISSORS :
(rand.nextBoolean() ? Move.ROCK : Move.PAPER);
}
Move aiMove = ai.chooseMove();
int result = ai.judge(aiMove, opponentMove);
if (result > 0) aiWins++;
else if (result < 0) oppWins++;
else draws++;
ai.recordOpponentMove(opponentMove);
}
System.out.println("=== 100轮对战结果 ===");
System.out.println("AI胜: " + aiWins);
System.out.println("对手胜: " + oppWins);
System.out.println("平局: " + draws);
System.out.println("AI胜率: " + (aiWins * 100.0 / (aiWins + oppWins)) + "%");
}
}
复杂度分析
- 时间复杂度:每轮预测和记录操作均为 $O(1)$,因为马尔可夫矩阵和频率数组大小固定为常数。
- 空间复杂度:$O(n)$,其中 $n$ 为历史记录长度(主要用于存储历史出拳序列,核心矩阵仅占用常数空间)。
扩展方向
- 高阶马尔可夫链:使用前两次出拳作为状态(3×3×3 转移矩阵),捕捉更长的序列模式。
- 动态权重调整:根据近期预测准确率自动调整
MARKOV_WEIGHT与FREQ_WEIGHT。 - 对手模型切换检测:当预测准确率连续下降时,触发模型重置或切换为随机策略。
总结
本文通过马尔可夫链与频率统计的双通道预测,为猜拳游戏构建了一个简单却有效的自适应AI。核心思想在于:任何非完全随机的行为序列都蕴含可挖掘的统计规律,而马尔可夫链正是捕捉这类短时序依赖的利器。读者可将此框架迁移到其他序列预测场景,如用户行为预测、股票趋势分析等。