feat: 添加石头剪刀布预测辅助系统
实现基于马尔可夫转移、频率统计和周期检测的融合预测算法,包含主程序、回测评估脚本、README 文档、MIT 许可证及 .gitignore 配置。
这个提交包含在:
+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))
|
||||
在新工单中引用
屏蔽一个用户