Super Gonzalo

ROK Rage simulation

ROK Rage simulation

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)
    python

    분노감소 처리 함수

    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
    
    python

    심판의 반지 발동 처리 함수

    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
    
    python

    연격 처리 함수

    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
    
    python

    메인 분노 계산 시스템 함수

    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
    
    python

    통합 시뮬레이션 함수

    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
    
    python

    시뮬레이션 실행 함수

    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
    
    python

    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
    
    python

    발동 턴 분포 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()
    python

    첫 스킬 발동 턴 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()    
    
    python

    시뮬레이션 실행

    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}")
    python

    시각화

    # 그래프 시각화
    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)
    python