last updated - 16차
기본 패키지 및 디버그 세팅
import numpy as np
import random
from collections import defaultdict
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from multiprocessing import Pool
pd.options.display.float_format = "{:.2f}".format
import matplotlib.font_manager as fm
# 한글 폰트 설정
font_path = 'C:/Windows/Fonts/malgun.ttf'
font_prop = fm.FontProperties(fname=font_path)
plt.rc('font', family=font_prop.get_name())
# 디버그 모드 설정 (True면 각 턴의 로그를 상세 출력)
DEBUG_MODE = False
def debug_log(msg: str):
"""디버그 모드일 때만 로그를 출력"""
if DEBUG_MODE:
print(msg)
분노감소 처리 함수
def apply_rage_decrease(rage: float, 분노감소: str, cooltime: int) -> (float, int):
"""
분노감소 기전을 적용하는 헬퍼 함수.
쿨타임이 0이고 확률 10%로 발동.
'백발백중'은 분노 -50,
'파죽지세'는 분노 -100,
발동 시 5턴 쿨타임.
"""
if 분노감소 != '없음' and cooltime == 0:
if random.random() < 0.1: # 10% 확률
if 분노감소 == '백발백중':
rage = max(rage - 50, 0)
debug_log("분노감소(백발백중) 발동: -50")
cooltime = 5
elif 분노감소 == '파죽지세':
rage = max(rage - 100, 0)
debug_log("분노감소(파죽지세) 발동: -100")
cooltime = 5
return rage, cooltime
심판의 반지 발동 처리 함수
def apply_ring_activation(turn: int, 악세2: str,
ring_cooltime: int,
ring_active_until: int) -> (int, int):
"""
'반지'/'반지(특)' 발동 로직.
- 10% 확률로 발동
- 발동 시 2턴 (현재 턴 + 다음 턴) 지속
- 5턴 쿨타임
"""
# 쿨타임 감소
if ring_cooltime > 0:
ring_cooltime -= 1
# 현재 활성화 상태 확인
ring_active = (turn <= ring_active_until)
# 발동 시도
if ring_cooltime == 0 and not ring_active:
if random.random() < 0.1: # 10% 확률
# 2턴 유지
ring_active_until = turn + 1
ring_cooltime = 5
debug_log(f"[반지 발동] {악세2}가 {turn}턴에 발동! 2턴간 유지, 쿨타임 5턴")
return ring_cooltime, ring_active_until
연격 처리 함수
def apply_extra_rage_if_necessary(
rage_this_turn: float,
연격: str,
분노수급: int,
영향력: str,
악세: str,
악세2: str,
치자: str,
치자_분노량: int,
전투력_비축: str,
분노감소: str,
cooltime: int
) -> (float, int):
"""
연격('y')일 경우 25% 확률로 추가 분노를 계산.
연격=='n'이면 0
"""
if 연격 != 'y':
return 0.0, cooltime
# 연격='y'인 경우 25% 확률
if random.random() < 0.25:
extra_rage, new_cooltime = calculate_rage_with_decrease(
분노수급,
영향력,
악세,
악세2,
치자,
치자_분노량,
전투력_비축,
분노감소,
cooltime
)
# 220 제한
if rage_this_turn + extra_rage > 220:
extra_rage = 220 - rage_this_turn
return extra_rage, new_cooltime
return 0.0, cooltime
메인 분노 계산 시스템 함수
def calculate_rage_with_decrease(
분노수급: int,
영향력: str,
악세: str,
악세2: str,
치자: str,
치자_분노량: int,
전투력_비축: str,
분노감소: str,
cooltime: int
) -> (float, int):
"""
분노 계산 + 분노감소 적용
최종 220으로 제한
"""
rage = 86 + 분노수급
# 영향력
if 영향력 == 'y':
rage += 5.16
# 악세 (분뿔)
if 악세 == '분뿔':
if random.random() < 0.3:
rage += 50
elif 악세 == '분뿔(특)':
if random.random() < 0.3:
rage += 65
# 치자
if 치자 == 'y' and random.random() < 0.1:
rage += 치자_분노량
# 전투력 비축
if 전투력_비축 == 'y' and random.random() < 0.1:
rage += 25
# 분노감소 처리
rage, cooltime = apply_rage_decrease(rage, 분노감소, cooltime)
# 220 제한
rage = min(rage, 220)
return rage, cooltime
통합 시뮬레이션 함수
def simulate_activation_turns_distribution(
simulations: int,
발동분노: int,
분노수급: int,
악세: str, # '분뿔','분뿔(특)','없음'
악세2: str, # '반지','반지(특)','없음'
치자: str,
치자_분노량: int,
영향력: str,
전투력_비축: str,
분노감소: str,
연격: str
):
"""
첫 스킬 발동 턴 + '반지'(악세2) 활성화 확률을 리턴
"""
activation_turn_counts = defaultdict(int)
ring_active_on_skill_turn_counts = 0
for _ in range(simulations):
total_rage = 0
turn = 0
cooltime = 0 # 분노감소
# 반지
ring_cooltime = 0
ring_active_until = 0
ring_active_on_skill_turn = False
while True:
turn += 1
# 1) 기본 분노 계산
rage_this_turn, cooltime = calculate_rage_with_decrease(
분노수급,
영향력,
악세,
악세2,
치자,
치자_분노량,
전투력_비축,
분노감소,
cooltime
)
# 2) 연격 처리
extra_rage, new_cooltime = apply_extra_rage_if_necessary(
rage_this_turn,
연격,
분노수급,
영향력,
악세,
악세2,
치자,
치자_분노량,
전투력_비축,
분노감소,
cooltime
)
cooltime = new_cooltime
# 3) 반지 처리
if 악세2 in ['반지','반지(특)']:
ring_cooltime, ring_active_until = apply_ring_activation(
turn, 악세2, ring_cooltime, ring_active_until
)
# 4) 턴 분노 합산
total_rage += (rage_this_turn + extra_rage)
# 5) 스킬 발동 조건 검사
if total_rage >= 발동분노:
# 반지 활성화 여부 (이 턴에 발동했는지)
if 악세2 in ['반지','반지(특)']:
# 이 턴에 활성화중이면
if turn <= ring_active_until:
ring_active_on_skill_turn = True
break
# 6) 쿨타임들 감소
if cooltime > 0:
cooltime -= 1
if ring_cooltime > 0:
ring_cooltime -= 1
# 첫 스킬 발동 턴
activation_turn = turn + 1
activation_turn_counts[activation_turn] += 1
if ring_active_on_skill_turn:
ring_active_on_skill_turn_counts += 1
# 분포 계산
distribution = {}
if activation_turn_counts:
min_turn = min(activation_turn_counts.keys())
max_turn = max(activation_turn_counts.keys())
for t in range(min_turn, max_turn+1):
distribution[t] = (activation_turn_counts[t]/simulations)*100
# 기대 발동 턴
expected_turn = sum(
t * activation_turn_counts[t]
for t in range(min_turn, max_turn+1)
)/simulations
else:
distribution = {}
expected_turn = 0
# 반지 활성화 확률
ring_prob = (ring_active_on_skill_turn_counts / simulations)*100
return distribution, expected_turn, ring_prob
시뮬레이션 실행 함수
def run_simulations_with_conditions(conditions, simulations=100_000_000):
results = []
for condition in conditions:
distribution, expected_turn, ring_prob = simulate_activation_turns_distribution(
simulations=simulations,
발동분노=condition['발동분노'],
분노수급=condition['분노수급'],
악세=condition.get('악세','없음'),
악세2=condition.get('악세2','없음'),
치자=condition.get('치자','n'),
치자_분노량=condition.get('치자_분노량',0),
영향력=condition.get('영향력','n'),
전투력_비축=condition.get('전투력_비축','n'),
분노감소=condition.get('분노감소','없음'),
연격=condition.get('연격','n')
)
results.append({
'조건': condition,
'분포': distribution,
'기대 발동턴': expected_turn,
'반지 활성화 확률 (%)': ring_prob
})
return results
DataFrame 변환 함수
def results_to_dataframe(results):
data = []
for result in results:
condition = result['조건']
dist = result['분포']
expected_turn = result['기대 발동턴']
ring_prob = result.get('반지 활성화 확률 (%)', 0)
발동분노 = condition.get('발동분노', None)
분노수급 = condition.get('분노수급', None)
악세 = condition.get('악세','없음')
악세2 = condition.get('악세2','없음')
치자 = condition.get('치자','n')
치자_분노량 = condition.get('치자_분노량',0)
영향력 = condition.get('영향력','n')
전투력_비축 = condition.get('전투력_비축','n')
연격 = condition.get('연격','n')
분노감소 = condition.get('분노감소','없음')
row = {
'발동분노': 발동분노,
'분노수급': 분노수급,
'악세': 악세,
'악세2': 악세2,
'치자': 치자,
'치자_분노량': 치자_분노량,
'영향력': 영향력,
'전투력_비축': 전투력_비축,
'연격': 연격,
'분노감소': 분노감소,
'기대 발동턴': expected_turn,
'반지 활성화 확률 (%)': ring_prob
}
data.append(row)
df = pd.DataFrame(data)
return df
발동 턴 분포 Subplot 함수
def plot_distributions_subplot(results, condition_format_func=None, color_map=None):
num_conditions = len(results)
cols = 4
rows = (num_conditions + cols - 1) // cols
fig, axes = plt.subplots(rows, cols, figsize=(24, 5 * rows))
axes = axes.flatten()
if num_conditions == 0:
print("No results to plot.")
return
all_turns = []
for result in results:
if result['분포']:
all_turns.extend(result['분포'].keys())
if not all_turns:
print("No distribution data to plot.")
return
x_min, x_max = min(all_turns), max(all_turns)
all_probabilities = []
for result in results:
if result['분포']:
all_probabilities.extend(result['분포'].values())
if not all_probabilities:
print("No probability data to plot.")
return
if color_map is None:
color_map = {
'없음': 'gray',
'분뿔': 'skyblue',
'분뿔(특)': 'orange',
'반지': 'green',
'반지(특)': 'purple'
}
y_max = max(all_probabilities)*1.1
for i, result in enumerate(results):
distribution = result['분포']
condition = result['조건']
turns = list(distribution.keys())
probabilities = [distribution[t] for t in turns]
if condition_format_func:
condition_str = condition_format_func(condition)
else:
condition_str = str(condition)
if not turns:
ax = axes[i]
ax.set_title(condition_str + " (No Data)", fontsize=10)
ax.axis('off')
continue
악세 = condition.get('악세','없음')
bar_color = color_map.get(악세,'lightgray')
ax = axes[i]
bars = ax.bar(turns, probabilities, color=bar_color, edgecolor='black')
ax.set_title(condition_str, fontsize=10)
ax.set_xlabel('발동턴')
ax.set_ylabel('확률 (%)')
ax.set_xlim(x_min - 0.5, x_max + 0.5)
ax.set_ylim(0, y_max)
ax.set_xticks(range(x_min, x_max+1, 1))
ax.grid(axis='y', linestyle='--', alpha=0.7)
for bar, prob in zip(bars, probabilities):
ax.text(bar.get_x() + bar.get_width()/2,
bar.get_height()+0.01,
f'{prob:.2f}%',
ha='center', va='bottom',
fontsize=9)
for j in range(i+1, len(axes)):
fig.delaxes(axes[j])
plt.tight_layout()
plt.show()
첫 스킬 발동 턴 line chart
def plot_expected_activation_turn(df):
plt.figure(figsize=(10,8))
sns.lineplot(data=df, x='분노수급', y='기대 발동턴', hue='악세', marker=True, style='분노감소')
for i in range(len(df)):
plt.text(
df['분노수급'][i],
df['기대 발동턴'][i],
f"{df['기대 발동턴'][i]:.2f}",
fontsize=9,
ha='center',
va='bottom'
)
# 라벨링 및 제목
plt.title('조건에 따른 기대 발동턴 변화')
plt.xlabel('분노수급량', fontsize=12, fontweight='bold')
plt.ylabel('기대 발동턴', fontsize=12)
# x축 범위와 간격 설정
x_min, x_max = 9, 18 # x축 범위 설정
margin = 1 # 여유 공간 마진 값
plt.xlim(x_min - margin, x_max)
# x축 간격 설정
tick_interval = 3
ticks = np.arange(x_min, x_max + tick_interval, tick_interval)
plt.xticks(ticks, fontsize=10)
plt.legend(title='악세', loc='upper right')
plt.grid(True)
plt.show()
시뮬레이션 실행
if __name__ == "__main__":
import time
# 디버그 모드 끄고 실행
# DEBUG_MODE = True # 켜면 턴별 로그를 볼 수 있음
DEBUG_MODE = False
conditions = [
{'발동분노': 1000, '분노수급': 9, '악세': '분뿔', '연격': 'n', '분노감소': '없음'},
{'발동분노': 1000, '분노수급': 9, '악세': '분뿔(특)', '연격': 'n', '분노감소': '없음'},
{'발동분노': 1000, '분노수급': 9, '악세': '분뿔', '연격': 'n', '분노감소': '파죽지세'},
{'발동분노': 1000, '분노수급': 9, '악세': '분뿔(특)', '연격': 'n', '분노감소': '파죽지세'},
# {'발동분노': 1000, '분노수급': 12, '악세': '분뿔', '연격': 'n', '분노감소': '없음'},
# {'발동분노': 1000, '분노수급': 12, '악세': '분뿔(특)', '연격': 'n', '분노감소': '없음'},
# {'발동분노': 1000, '분노수급': 12, '악세': '분뿔', '연격': 'n', '분노감소': '파죽지세'},
# {'발동분노': 1000, '분노수급': 12, '악세': '분뿔(특)', '연격': 'n', '분노감소': '파죽지세'},
# {'발동분노': 1000, '분노수급': 15, '악세': '분뿔', '연격': 'n', '분노감소': '없음'},
# {'발동분노': 1000, '분노수급': 15, '악세': '분뿔(특)', '연격': 'n', '분노감소': '없음'},
# {'발동분노': 1000, '분노수급': 15, '악세': '분뿔', '연격': 'n', '분노감소': '파죽지세'},
# {'발동분노': 1000, '분노수급': 15, '악세': '분뿔(특)', '연격': 'n', '분노감소': '파죽지세'},
# {'발동분노': 1000, '분노수급': 18, '악세': '분뿔', '연격': 'n', '분노감소': '없음'},
# {'발동분노': 1000, '분노수급': 18, '악세': '분뿔(특)', '연격': 'n', '분노감소': '없음'},
# {'발동분노': 1000, '분노수급': 18, '악세': '분뿔', '연격': 'n', '분노감소': '파죽지세'},
# {'발동분노': 1000, '분노수급': 18, '악세': '분뿔(특)', '연격': 'n', '분노감소': '파죽지세'},
]
print(f"[INFO] {len(conditions)}개 조건에 대해 시뮬레이션을 시작합니다.")
start_time = time.time()
results = run_simulations_with_conditions(conditions, simulations=50000)
print(f"[INFO] 시뮬레이션 완료. 소요시간: {time.time() - start_time:.2f}초")
df = results_to_dataframe(results)
print(df)
# CSV 저장
base_path = r"C:\Users\st016\OneDrive\game Archive\라이즈\시뮬레이션"
file_name = "파죽지세분석_refactor.csv"
full_path = f"{base_path}\\{file_name}"
df.to_csv(full_path, index=False, encoding="utf-8-sig")
print(f"[INFO] CSV 저장 완료: {full_path}")
시각화
# 그래프 시각화
def my_condition_formatter(condition):
발동분노 = condition.get('발동분노')
분노수급 = condition.get('분노수급')
악세 = condition.get('악세','없음')
악세2 = condition.get('악세2','없음')
연격 = condition.get('연격','n')
분감 = condition.get('분노감소','없음')
return f"발동분노:{발동분노}, 수급:{분노수급}, 악세:{악세}, 악세2:{악세2}, 연격:{연격}, 분감:{분감}"
plot_distributions_subplot(results, my_condition_formatter)
plot_expected_activation_turn(df)