feat: 添加石头剪刀布预测辅助系统

实现基于马尔可夫转移、频率统计和周期检测的融合预测算法,包含主程序、回测评估脚本、README 文档、MIT 许可证及 .gitignore 配置。
这个提交包含在:
2026-08-24 10:53:03 +08:00
当前提交 de44825e21
共修改 6 个文件,包含 436 行新增和 0 行删除
+106
查看文件
@@ -0,0 +1,106 @@
"""
回测 / 评估脚本:用多种模拟对手验证猜拳预测算法的有效性。
对比两组策略:
- 预测 AI:根据历史推荐克制对手的出手
- 随机基线:完全随机出手
胜率定义:(胜局数) / (总对局数)。平局不计入胜率分子。
理想情况下,面对「有模式」的对手,预测 AI 胜率应显著高于 33.3% 的随机基线。
"""
import random
import importlib.util
import numpy as np
from collections import deque
# 主文件名含连字符,无法直接 import,用 importlib 加载
_spec = importlib.util.spec_from_file_location("rps_main", "rock-paper-scissors.py")
_rps = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_rps)
RPSAssistant = _rps.RPSAssistant
win_map = _rps.win_map
moves = {1: "拳头", 2: "剪刀", 3: "布"}
# =============================
# 模拟对手生成器
# =============================
def opponent_random(n):
"""完全随机的对手(算法不应有优势)。"""
return [random.choice([1, 2, 3]) for _ in range(n)]
def opponent_bias(n, fav=3, p=0.6):
"""有固定偏好的对手:以概率 p 出 fav,否则随机。"""
return [fav if random.random() < p else random.choice([1, 2, 3]) for _ in range(n)]
def opponent_cycle(n, pattern=(1, 2, 3)):
"""按固定循环出拳的对手。"""
return [pattern[i % len(pattern)] for i in range(n)]
def opponent_markov(n, trans=None, start=1):
"""一阶马尔可夫依赖的对手:下一手受上一手影响。"""
if trans is None:
# 行 i:上一手为 i 时,下一手的转移概率(拳头/剪刀/布)
trans = {
1: [0.6, 0.2, 0.2],
2: [0.2, 0.6, 0.2],
3: [0.2, 0.2, 0.6],
}
seq = [start]
for _ in range(n - 1):
prev = seq[-1]
seq.append(int(np.random.choice([1, 2, 3], p=trans[prev])))
return seq
# =============================
# 评估逻辑
# =============================
def simulate(enemy_seq, use_predictor=True):
"""用给定对手序列跑一遍,返回 (胜局, 负局, 平局)。"""
assistant = RPSAssistant()
wins = losses = draws = 0
my_move = random.choice([1, 2, 3]) # 首局随机
for enemy in enemy_seq:
if use_predictor:
_, rec = assistant.record_opponent_move(enemy, my_move=my_move)
my_move = rec
else:
my_move = random.choice([1, 2, 3])
# 仍需推进 assistant 以统计,但 my_move 不参与预测
assistant.record_opponent_move(enemy, my_move=my_move)
# 判定本局
if win_map[my_move] == enemy:
wins += 1
elif win_map[enemy] == my_move:
losses += 1
else:
draws += 1
return wins, losses, draws
def report(name, enemy_seq):
n = len(enemy_seq)
w_pred, l_pred, d_pred = simulate(enemy_seq, use_predictor=True)
w_rand, l_rand, d_rand = simulate(enemy_seq, use_predictor=False)
pred_rate = w_pred / n * 100
rand_rate = w_rand / n * 100
print(f"对手类型: {name} (共 {n} 局)")
print(f" 预测AI -> 胜 {w_pred} / 负 {l_pred} / 平 {d_pred} | 胜率 {pred_rate:.1f}%")
print(f" 随机基线 -> 胜 {w_rand} / 负 {l_rand} / 平 {d_rand} | 胜率 {rand_rate:.1f}%")
print(f" 提升 -> {pred_rate - rand_rate:+.1f} 个百分点\n")
if __name__ == "__main__":
N = 2000
random.seed(42)
np.random.seed(42)
report("完全随机", opponent_random(N))
report("偏好出布(60%)", opponent_bias(N, fav=3, p=0.6))
report("固定循环(拳头->剪刀->布)", opponent_cycle(N))
report("一阶马尔可夫依赖", opponent_markov(N))