208 行
8.1 KiB
Python
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)
|