Skip to content

Commit 22e2b2e

Browse files
committed
250715
1 parent 41e0d48 commit 22e2b2e

2 files changed

Lines changed: 566 additions & 0 deletions

File tree

game_theory/minimax_RSP.py

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import random
2+
import matplotlib.pyplot as plt
3+
import numpy as np
4+
import pandas as pd
5+
6+
random.seed(42)
7+
8+
def random_strategy(_, __):
9+
# 완전히 균등한 랜덤 전략 (33.33% 가위, 33.33% 바위, 33.33% 보)
10+
return random.randint(0, 2)
11+
12+
def scissors_biased_10(_, __):
13+
# 가위가 10% 더 많이 나오는 전략 (40% 가위, 30% 바위, 30% 보)
14+
return random.choices([0,1,2], weights=[0.4,0.3,0.3])[0]
15+
16+
def rock_biased_10(_, __):
17+
# 바위가 10% 더 많이 나오는 전략 (30% 가위, 40% 바위, 30% 보)
18+
return random.choices([0,1,2], weights=[0.3,0.4,0.3])[0]
19+
20+
def paper_biased_10(_, __):
21+
# 보가 10% 더 많이 나오는 전략 (30% 가위, 30% 바위, 40% 보)
22+
return random.choices([0,1,2], weights=[0.3,0.3,0.4])[0]
23+
24+
def scissors_biased_heavy(_, __):
25+
# 가위가 매우 많이 나오는 전략 (70% 가위, 15% 바위, 15% 보)
26+
return random.choices([0,1,2], weights=[0.7,0.15,0.15])[0]
27+
28+
def rock_biased_heavy(_, __):
29+
# 바위가 매우 많이 나오는 전략 (15% 가위, 70% 바위, 15% 보)
30+
return random.choices([0,1,2], weights=[0.15,0.7,0.15])[0]
31+
32+
def paper_biased_heavy(_, __):
33+
# 보가 매우 많이 나오는 전략 (15% 가위, 15% 바위, 70% 보)
34+
return random.choices([0,1,2], weights=[0.15,0.15,0.7])[0]
35+
36+
def cycle_strategy(history_self, _):
37+
# 순서: Scissors(0) -> Rock(1) -> Paper(2) -> Scissors...
38+
if not history_self:
39+
return 0
40+
else:
41+
return (history_self[-1] + 1) % 3
42+
43+
def copy_opponent_strategy(history_self, history_opponent):
44+
# 상대방이 이전에 낸 것을 따라하는 전략 (1라운드는 무작위)
45+
if not history_opponent:
46+
return random.randint(0,2)
47+
else:
48+
return history_opponent[-1]
49+
50+
def minimax_frequency_analyzer(history_self, history_opponent):
51+
# 상대방의 빈도를 분석해서 가장 많이 나오는 것을 카운터하는 전략
52+
if len(history_opponent) < 10: # 충분한 데이터가 없으면 랜덤
53+
return random.randint(0, 2)
54+
55+
# 최근 50게임의 빈도 분석
56+
recent_history = history_opponent[-50:]
57+
counts = [recent_history.count(i) for i in range(3)]
58+
59+
# 가장 많이 나온 것을 카운터
60+
most_frequent = counts.index(max(counts))
61+
counter_move = (most_frequent + 1) % 3 # 가위(0)->바위(1), 바위(1)->보(2), 보(2)->가위(0)
62+
return counter_move
63+
64+
def minimax_pattern_predictor(history_self, history_opponent):
65+
# 상대방의 패턴을 예측해서 미리 카운터하는 전략
66+
if len(history_opponent) < 3:
67+
return random.randint(0, 2)
68+
69+
# 최근 패턴 분석 (3연속 패턴)
70+
if len(history_opponent) >= 6:
71+
# 마지막 3개 패턴
72+
last_pattern = tuple(history_opponent[-3:])
73+
74+
# 과거에서 같은 패턴 이후에 무엇이 나왔는지 찾기
75+
pattern_predictions = []
76+
for i in range(len(history_opponent) - 3):
77+
if tuple(history_opponent[i:i+3]) == last_pattern:
78+
if i + 3 < len(history_opponent):
79+
pattern_predictions.append(history_opponent[i+3])
80+
81+
if pattern_predictions:
82+
# 가장 자주 나온 다음 수를 예측
83+
from collections import Counter
84+
predicted_move = Counter(pattern_predictions).most_common(1)[0][0]
85+
return (predicted_move + 1) % 3 # 카운터
86+
87+
# 패턴이 없으면 빈도 기반으로 fallback
88+
return minimax_frequency_analyzer(history_self, history_opponent)
89+
90+
def anti_cycle_strategy(history_self, history_opponent):
91+
# 순환 패턴을 감지하고 카운터하는 전략
92+
if len(history_opponent) < 6:
93+
return random.randint(0, 2)
94+
95+
# 최근 6게임에서 순환 패턴 확인
96+
recent = history_opponent[-6:]
97+
98+
# 3-cycle 확인: [a,b,c,a,b,c] 패턴
99+
if recent[:3] == recent[3:]:
100+
next_in_cycle = recent[0] # 다음에 올 것 예측
101+
return (next_in_cycle + 1) % 3 # 카운터
102+
103+
# 2-cycle 확인: [a,b,a,b,a,b] 패턴
104+
if len(set(recent[::2])) == 1 and len(set(recent[1::2])) == 1:
105+
if len(history_opponent) % 2 == 0:
106+
next_in_cycle = recent[0]
107+
else:
108+
next_in_cycle = recent[1]
109+
return (next_in_cycle + 1) % 3
110+
111+
return minimax_frequency_analyzer(history_self, history_opponent)
112+
113+
def rps_result(a, b):
114+
if a == b:
115+
return 0
116+
elif (a == 0 and b == 2) or (a == 1 and b == 0) or (a == 2 and b == 1):
117+
return 1
118+
else:
119+
return -1
120+
121+
def match_matrix(strategyA, strategyB, n_rounds):
122+
histA, histB = [], []
123+
results = np.zeros(n_rounds, dtype=int)
124+
for i in range(n_rounds):
125+
moveA = strategyA(histA, histB)
126+
moveB = strategyB(histB, histA)
127+
histA.append(moveA)
128+
histB.append(moveB)
129+
results[i] = rps_result(moveA, moveB)
130+
return np.mean(results)
131+
132+
strategies = {
133+
"Random": random_strategy,
134+
"Scissors-10%": scissors_biased_10,
135+
"Rock-10%": rock_biased_10,
136+
"Paper-10%": paper_biased_10,
137+
"Scissors-Heavy": scissors_biased_heavy,
138+
"Rock-Heavy": rock_biased_heavy,
139+
"Paper-Heavy": paper_biased_heavy,
140+
"Cycle": cycle_strategy,
141+
"Copy-Opponent": copy_opponent_strategy,
142+
"Minimax-Frequency": minimax_frequency_analyzer,
143+
"Minimax-Pattern": minimax_pattern_predictor,
144+
"Anti-Cycle": anti_cycle_strategy
145+
}
146+
147+
N_rounds = 100000
148+
n = len(strategies)
149+
strategy_names = list(strategies.keys())
150+
matrix = np.zeros((n, n))
151+
152+
for i, nameA in enumerate(strategy_names):
153+
for j, nameB in enumerate(strategy_names):
154+
matrix[i, j] = match_matrix(strategies[nameA], strategies[nameB], N_rounds)
155+
156+
# DataFrame으로 보기 좋게
157+
results_df = pd.DataFrame(matrix, index=strategy_names, columns=strategy_names)
158+
print("\nPayoff matrix (row strategy vs column strategy, average per game, A's perspective):\n")
159+
print(results_df.round(3))
160+
161+
# 결과 분석 추가
162+
print("\n" + "="*60)
163+
print("폰 노이만(Von Neumann) 미니맥스 정리 시각화")
164+
print("="*60)
165+
166+
# 1. 순수전략들의 페이오프 매트릭스 (이론값)
167+
print("\n1. 순수전략 페이오프 매트릭스 (Player A 관점):")
168+
print(" 가위 바위 보")
169+
print("가위 0 -1 +1")
170+
print("바위 +1 0 -1")
171+
print("보 -1 +1 0")
172+
173+
# 2. 미니맥스 값 계산
174+
print("\n2. 미니맥스 분석:")
175+
print("각 순수전략의 최악의 경우 (minimax):")
176+
print("- 가위만: min(0, -1, +1) = -1")
177+
print("- 바위만: min(+1, 0, -1) = -1")
178+
print("- 보만: min(-1, +1, 0) = -1")
179+
print("→ 순수전략의 minimax value = -1")
180+
181+
print("\n혼합전략 (1/3, 1/3, 1/3)의 기댓값:")
182+
print("모든 상대 전략에 대해 기댓값 = 0")
183+
print("→ 혼합전략의 minimax value = 0")
184+
185+
print(f"\n폰 노이만 정리: max(minimax) = min(maximin) = 0")
186+
print("Nash 균형: 양 플레이어 모두 (1/3, 1/3, 1/3)")
187+
188+
# 3. 실제 시뮬레이션 결과와 비교
189+
print("\n3. 시뮬레이션 결과 검증:")
190+
random_vs_all = results_df.loc["Random"]
191+
print(f"Random 전략 vs 모든 전략의 평균: {random_vs_all.mean():.6f}")
192+
print(f"Random 전략 vs 모든 전략의 표준편차: {random_vs_all.std():.6f}")
193+
print(f"이론값 0과의 차이: {abs(random_vs_all.mean()):.6f}")
194+
195+
# 4. 편향된 전략들이 Random에게 지는 이유
196+
print("\n4. 편향 전략의 취약성:")
197+
biased_strategies = ["Scissors-Heavy", "Rock-Heavy", "Paper-Heavy"]
198+
for strategy in biased_strategies:
199+
if strategy in results_df.index:
200+
value_vs_random = results_df.loc[strategy, "Random"]
201+
print(f"{strategy} vs Random: {value_vs_random:.3f}")
202+
203+
print("\n편향된 전략은 예측 가능하므로 카운터 당할 수 있습니다.")
204+
print("Random은 어떤 전략도 이용할 수 없으므로 최적입니다.")
205+
206+
# 히트맵으로 시각화
207+
plt.figure(figsize=(14,12))
208+
plt.imshow(results_df, cmap='RdYlGn', interpolation='nearest')
209+
plt.colorbar(label="Average payoff (row vs col)")
210+
plt.xticks(np.arange(n), strategy_names, rotation=45, ha='right')
211+
plt.yticks(np.arange(n), strategy_names)
212+
plt.title("Von Neumann Minimax Theorem: Rock-Paper-Scissors (100,000 games)")
213+
for i in range(n):
214+
for j in range(n):
215+
plt.text(j, i, f"{results_df.iloc[i,j]:+.2f}", ha='center', va='center', color='black', fontsize=7)
216+
plt.xlabel("Column: Opponent strategy")
217+
plt.ylabel("Row: Player strategy")
218+
plt.tight_layout()
219+
plt.show()

0 commit comments

Comments
 (0)