文件
guessing-prediction-system/rock-paper-scissors.py
T
wangchuanli de44825e21 feat: 添加石头剪刀布预测辅助系统
实现基于马尔可夫转移、频率统计和周期检测的融合预测算法,包含主程序、回测评估脚本、README 文档、MIT 许可证及 .gitignore 配置。
2026-08-24 10:53:03 +08:00

208 行
8.1 KiB
Python

import random
from collections import deque, Counter
import numpy as np
import json
import os
moves = {1: "拳头", 2: "剪刀", 3: "布"}
win_map = {1: 2, 2: 3, 3: 1} # key克制value
HISTORY_FILE = "rps_history.json"
class RPSAssistant:
def __init__(self, memory_size=15, cycle_maxlen=5):
self.enemy_history = deque(maxlen=memory_size)
self.recent_results = deque(maxlen=5)
# 马尔可夫一阶转移矩阵,加1做拉普拉斯平滑,避免零概率
self.transition_matrix = np.ones((3, 3))
self.cycle_maxlen = cycle_maxlen
self.W_count = 0 # 当前连胜
self.total_wins = 0
self.total_losses = 0
self.history_records = []
if os.path.exists(HISTORY_FILE):
with open(HISTORY_FILE, "r") as f:
self.history_records = json.load(f)
# 初始化统计与转移矩阵
for r in self.history_records:
if r.get("本局结果") == "胜利":
self.total_wins += 1
elif r.get("本局结果") == "失败":
self.total_losses += 1
self._rebuild_transition_matrix()
def _rebuild_transition_matrix(self):
"""根据存档中的对手出手序列重建转移矩阵(用于从存档恢复学习状态)。"""
self.transition_matrix = np.ones((3, 3))
hist = [r["对手出手"] for r in self.history_records]
self.enemy_history = deque(hist, maxlen=self.enemy_history.maxlen)
for i in range(len(hist) - 1):
prev, nxt = hist[i], hist[i + 1]
self.transition_matrix[prev - 1][nxt - 1] += 1
def save_history(self):
for record in self.history_records:
for k, v in record.items():
if isinstance(v, np.integer):
record[k] = int(v)
with open(HISTORY_FILE, "w") as f:
json.dump(self.history_records, f, ensure_ascii=False, indent=2)
def markov_predict(self):
"""基于上一手的一阶马尔可夫预测,返回 (预测手, 置信度)。
置信度 = 最大转移概率(0~1),用于融合时衡量该预测的可信程度。
置信度过低时返回 None,表示「无明显依赖」。
"""
if len(self.enemy_history) < 2:
return None, 0.0
last = self.enemy_history[-1]
row = self.transition_matrix[last - 1]
probs = row / row.sum()
best = int(np.argmax(probs)) + 1
confidence = float(probs.max())
# 至少需要一定样本且明显偏向某一手才认为可信
if confidence < 0.45:
return None, confidence
return best, confidence
def frequency_predict(self):
"""基于频率统计的预测,返回 (预测手, 置信度)。
置信度 = 最高频手占比。当各手频率接近(如对手均衡出拳)时,
置信度低,不应强行给出虚假偏好信号。
"""
if len(self.enemy_history) < 3:
return None, 0.0
count = Counter(self.enemy_history)
total = sum(count.values())
most, cnt = count.most_common(1)[0]
confidence = cnt / total
if confidence < 0.45:
return None, confidence
return most, confidence
def cycle_predict(self):
"""基于周期/循环模式的预测,返回 (预测手, 置信度) 或 (None, 0.0)。
在末尾历史中寻找重复出现的固定长度片段,据其推断下一手。
"""
hist = list(self.enemy_history)
max_L = min(self.cycle_maxlen, len(hist) // 2)
for L in range(2, max_L + 1):
pattern = hist[-L:]
for start in range(0, len(hist) - 2 * L + 1):
if hist[start:start + L] == pattern:
return hist[start + L], 1.0
return None, 0.0
def predict_enemy(self):
"""融合多种带置信度的预测。
策略:收集所有「有效(置信度达标)」的预测,按置信度加权随机选取;
若没有任何有效预测,则退化为均匀随机(避免虚假信号)。
"""
candidates = []
for pred_fn in (self.markov_predict, self.frequency_predict, self.cycle_predict):
move, conf = pred_fn()
if move is not None:
candidates.append((move, conf))
if not candidates:
return random.choice([1, 2, 3])
moves_list, weights = zip(*candidates)
# 归一化权重
total_w = sum(weights)
norm_weights = [w / total_w for w in weights]
return int(np.random.choice(moves_list, p=norm_weights))
def recommend_move(self, predicted_enemy):
"""根据预测对手出手,推荐能克制它的一手。"""
if self.W_count >= 1:
move = [k for k, v in win_map.items() if v == predicted_enemy][0]
else:
lose_count = sum([1 for r in self.recent_results if r == "失败"])
if lose_count >= 2 and random.random() < 0.3:
move = random.choice([1, 2, 3])
else:
move = [k for k, v in win_map.items() if v == predicted_enemy][0]
return move
def record_opponent_move(self, enemy_move, my_move):
# 基于「已知历史」预测对手的下一手(此时 enemy_history 尚未包含当前手)
predicted_next = self.predict_enemy()
recommended_move = self.recommend_move(predicted_next)
# 用「上一手 -> 当前手」更新转移矩阵,并写入历史
if self.enemy_history:
prev = self.enemy_history[-1]
self.transition_matrix[prev - 1][enemy_move - 1] += 1
self.enemy_history.append(enemy_move)
# 判断本局胜负
if win_map[my_move] == enemy_move:
result = "胜利"
self.W_count += 1
self.total_wins += 1
elif win_map[enemy_move] == my_move:
result = "失败"
self.W_count = 0
self.total_losses += 1
else:
result = "平局"
self.W_count = 0
self.recent_results.append(result)
# 保存本局记录
record = {
"局数": len(self.history_records) + 1,
"对手出手": int(enemy_move),
"AI预测对手下一手": int(predicted_next),
"推荐出手": int(recommended_move),
"我的出手": int(my_move),
"本局结果": result
}
self.history_records.append(record)
self.save_history()
# 输出格式化信息
print(f"\n局数:{record['局数']}")
print(f"你输入的对手实际出手:{moves[enemy_move]}")
print(f"本局胜负结果:{result}")
print(f"AI预测对手下一手可能出:{moves[predicted_next]}")
print(f"AI推荐你出手:{moves[recommended_move]}")
print(f"历史记录:总局数={len(self.history_records)}, 累积胜局={self.total_wins}, 累积负局={self.total_losses}, 当前连胜={self.W_count}\n")
return predicted_next, recommended_move
# =============================
# 主程序
# =============================
if __name__ == "__main__":
assistant = RPSAssistant()
print("🎮 猜拳辅助AI启动")
print("每局输入对手出手:1=拳头,2=剪刀,3=布, q退出")
# 首局推荐出手
first_move = random.choice([1, 2, 3])
print(f"首局推荐你出手:{moves[first_move]}")
while True:
inp = input("输入对手出手(1/2/3, q退出): ")
if inp.lower() == 'q':
print("比赛结束")
break
try:
enemy_move = int(inp)
if enemy_move not in [1, 2, 3]:
print("请输入1/2/3或q退出")
continue
assistant.record_opponent_move(enemy_move, my_move=first_move)
# 后续局首局推荐更新为上一局记录的推荐出手
first_move = assistant.history_records[-1]["推荐出手"]
except Exception as e:
print("输入有误,请重新输入:", e)