# -*- coding: utf-8 -*-
"""
KBO 팀-전용 시뮬레이터 (팀만, VAR 제거, 전역 SD, 용병 IMP, 분산 강화, 스탠딩 없음, 히스토리+Elo 누적)
- 팀 능력치: SP, RP, BAT, DEF, IMP (0~100)
- 시즌 편차: 전역 SD_*로 정규분포에서 '시즌당 1회' 샘플 → 시즌 내 고정(표시 안 함)
- 용병 IMP_eff가 SP/RP/BAT에 가산 영향 (DEF 기본 미반영)
- 분산 강화: DAY_SIG(공유), BATT_SIG(팀 컨디션), DISP_SP/RP(감마-포아송)
- 1–6회 SP, 7–9회 RP, KBO 12이닝 무승부
- 출력: 경기 로그(콘솔) + out/linescore_*.csv + (옵션) history.csv, elo_state.csv, elo_updates.csv
- myCompiler 호환: __name__ 체크 없이 자동 실행 (비활성화: SKIP_AUTORUN=1)

인라인/환경 옵션(선택):
  SEASON_SEED, ROUNDS, OFFENSE(숫자|auto)
  SD_SP, SD_RP, SD_BAT, SD_DEF, SD_IMP
  DAY_SIG, BATT_SIG, DISP_SP, DISP_RP
  SP_FOR_SHARE, RP_FOR_SHARE
  ELO(1/0), ELO_K, ELO_HOME_ADV, ELO_INIT
  DUMP_SEASON=1 → out/season_profile.csv 저장
"""

import os, sys, csv, math, time, random, hashlib
from typing import Dict, List, Tuple
from datetime import datetime

# ------------- 팀 기본치 (사용자 제안안 반영) -------------
TEAM_BASE = {
    "LG 트윈스":     {"SP":75, "RP":79, "BAT":82, "DEF":75, "IMP":75, "stadium":"잠실"},
    "두산 베어스":   {"SP":73, "RP":70, "BAT":88, "DEF":79, "IMP":75, "stadium":"잠실"},
    "키움 히어로즈": {"SP":70, "RP":68, "BAT":75, "DEF":73, "IMP":75, "stadium":"고척 스카이돔"},
    "SSG 랜더스":    {"SP":75, "RP":82, "BAT":73, "DEF":82, "IMP":75, "stadium":"인천SSG랜더스필드"},
    "KT 위즈":       {"SP":82, "RP":79, "BAT":73, "DEF":73, "IMP":75, "stadium":"수원 KT 위즈 파크"},
    "NC 다이노스":   {"SP":70, "RP":73, "BAT":79, "DEF":73, "IMP":75, "stadium":"창원 NC파크"},
    "KIA 타이거즈":  {"SP":75, "RP":65, "BAT":82, "DEF":68, "IMP":75, "stadium":"광주-기아 챔피언스 필드"},
    "삼성 라이온즈": {"SP":73, "RP":82, "BAT":75, "DEF":75, "IMP":75, "stadium":"대구 삼성 라이온즈 파크"},
    "롯데 자이언츠": {"SP":70, "RP":75, "BAT":73, "DEF":65, "IMP":75, "stadium":"사직"},
    "한화 이글스":   {"SP":70, "RP":70, "BAT":68, "DEF":65, "IMP":75, "stadium":"대전 한화 생명이글스파크"},
}

# ------------- 짧은 이름 매핑 -------------
ALIASES = {
 'lg':'LG 트윈스','엘지':'LG 트윈스','트윈스':'LG 트윈스',
 '두산':'두산 베어스','doosan':'두산 베어스','베어스':'두산 베어스',
 '키움':'키움 히어로즈','히어로즈':'키움 히어로즈','kiwoom':'키움 히어로즈',
 'ssg':'SSG 랜더스','랜더스':'SSG 랜더스',
 'kt':'KT 위즈','위즈':'KT 위즈','케이티':'KT 위즈',
 'nc':'NC 다이노스','다이노스':'NC 다이노스',
 'kia':'KIA 타이거즈','기아':'KIA 타이거즈','타이거즈':'KIA 타이거즈',
 '삼성':'삼성 라이온즈','samsung':'삼성 라이온즈','라이온즈':'삼성 라이온즈',
 '롯데':'롯데 자이언츠','lotte':'롯데 자이언츠','자이언츠':'롯데 자이언츠',
 '한화':'한화 이글스','hanwha':'한화 이글스','이글스':'한화 이글스',
}
def norm_team_name(name:str)->str:
    return ALIASES.get(''.join(name.lower().split()), name)

# ------------- 구장 득점 계수 (100=중립) -------------
PARK_RUNS = {
    "잠실":95, "고척 스카이돔":101, "인천SSG랜더스필드":103, "수원 KT 위즈 파크":102,
    "창원 NC파크":100, "광주-기아 챔피언스 필드":101, "대구 삼성 라이온즈 파크":102,
    "사직":98, "대전 한화 생명이글스파크":99
}

# ------------- 유틸 -------------
def print_scoreboard(g):
    away = g["away"]; home = g["home"]
    inn_n = max(len(g["lines_home"]), len(g["lines_away"]))
    hdr = "    " + " ".join(f"{i:>2}" for i in range(1, inn_n+1)) + " |  R"
    row_away = f"{away[:6]:<6}" + " ".join(f"{v:>2}" for v in g["lines_away"]) + f" | {g['R_away']:>2}"
    row_home = f"{home[:6]:<6}" + " ".join(f"{v:>2}" for v in g["lines_home"]) + f" | {g['R_home']:>2}"
    print(f"[BOX] {away} @ {home}  ({g['stadium']})")
    print(hdr)
    print(row_away)
    print(row_home)
    print(f"→ 결과: {g['result']}")

def to_float(s, default):
    try: return float(str(s))
    except: return default
def to_int(s, default):
    try: return int(str(s))
    except: return default

def stable_seed(*parts) -> int:
    m = hashlib.sha256()
    for p in parts:
        m.update(str(p).encode("utf-8")); m.update(b"|")
    return int.from_bytes(m.digest()[:8], "big", signed=False)

def sample_offense(season_seed,
                   mean=0.96, sd=0.06, lo=0.82, hi=1.15,
                   sticky=0.0,
                   cycle_amp=0.06, cycle_period=6.0,
                   base_year=2015):
    """시즌 득점 환경을 시드로부터 자동 샘플"""
    # 연도 추출
    try:
        y = int(str(season_seed)[:4])
    except:
        y = 2026
    # 앵커/스티키
    if sticky <= 0:
        rng = random.Random(stable_seed("OFFENSE", season_seed))
        val = rng.gauss(mean, sd)
    else:
        rng_a = random.Random(stable_seed("OFFENSE_anchor"))
        rng_s = random.Random(stable_seed("OFFENSE_season", season_seed))
        a = rng_a.gauss(0.0, sd)
        b = rng_s.gauss(0.0, sd)
        val = mean + sticky*a + math.sqrt(max(0.0, 1.0 - sticky*sticky))*b
    # 리그 주기(사이클)
    if cycle_amp and cycle_period > 0:
        phi = random.Random(stable_seed("OFFENSE_phase")).random()  # 고정 위상
        t = (y - int(base_year)) / float(cycle_period) + phi
        val += cycle_amp * math.sin(2.0*math.pi * t)
    return max(lo, min(hi, val))

# 외인 선발/불펜이 차지하는 비중(기본: 선발 40%, 불펜 5%)  ← to_float 정의 뒤로 이동(중요)
SP_FOR_SHARE = to_float(os.environ.get("SP_FOR_SHARE", "0.40"), 0.40)
RP_FOR_SHARE = to_float(os.environ.get("RP_FOR_SHARE", "0.05"), 0.05)

# ------------- 전역 환경 기본값 -------------
OFFENSE = max(0.5, min(1.5, to_float(os.environ.get("OFFENSE","0.90"), 0.90)))  # 득점 레벨
BASE_RPG = 4.3     # 팀당 9이닝 평균 득점(중립)
HOME_ADV = 1.04    # 홈 어드밴티지(~4%)

# 전역 SD (환경변수로 덮어쓰기 가능; 0이면 완전 고정)
def env_sd(key, default):
    v = os.environ.get(key)
    try: return float(v) if v is not None else default
    except: return default
SD_SP  = env_sd("SD_SP", 20.0)
SD_RP  = env_sd("SD_RP", 18.0)
SD_BAT = env_sd("SD_BAT",22.0)
SD_DEF = env_sd("SD_DEF",12.0)
SD_IMP = env_sd("SD_IMP",10.0)

# 용병 영향 가중치(필요시 조정)
IMP_WEIGHTS = {"SP": 0.75, "RP": 0.05, "BAT": 0.35}

# ---- 추가: 득점 분산/컨디션 파라미터(기본 온건) ----
DAY_SIG  = 0.20   # 구장-공유 일일 변동 (0.0~0.5 권장)
BATT_SIG = 0.15   # 팀별 일일 컨디션 (0.0~0.5 권장)
DISP_SP  = 3.0    # 선발쪽 감마 shape (∞=포아송, 2~4 추천)
DISP_RP  = 2.0    # 불펜쪽 감마 shape (작을수록 변동↑)

def clamp(x, lo, hi): 
    return lo if x<lo else (hi if x>hi else x)

def make_rng(seed_str: str) -> random.Random:
    if seed_str == "":
        return random.Random(time.time_ns())
    try:
        return random.Random(int(seed_str))
    except:
        return random.Random(sum(ord(c) for c in seed_str))

def park_index_runs(stadium: str) -> float:
    return PARK_RUNS.get(stadium, 100)/100.0

# --- 영향 계수(현대야구 밸런스) ---
def attack_index(BAT: float) -> float:
    # +10 BAT → +3.0% 득점
    return 1.0 + 0.030 * ((BAT - 70) / 10.0)

def pitch_index_sp(P: float) -> float:
    # +10 SP → −3.0% (국내 선발 중심)
    return 1.0 - 0.030 * ((P - 70) / 10.0)

def pitch_index_rp(P: float) -> float:
    # +10 RP → −2.0%
    return 1.0 - 0.020 * ((P - 70) / 10.0)

def defense_index(DEF: float) -> float:
    # +10 DEF → −0.8% 실점 (BAT의 ~27%)
    return 1.0 - 0.008 * ((DEF - 70) / 10.0)

def expected_runs_per9(offBAT, oppP, oppDEF, park_runs, home_boost, offense, which="SP"):
    base = BASE_RPG * offense
    p_idx = pitch_index_sp(oppP) if which=="SP" else pitch_index_rp(oppP)
    idx = attack_index(offBAT) * p_idx * defense_index(oppDEF) * park_runs * home_boost
    return max(0.4, base * idx)

def poisson(lmbd: float, rng: random.Random) -> int:
    # Knuth
    L = math.exp(-lmbd); k = 0; p = 1.0
    while p > L:
        k += 1; p *= rng.random()
    return k-1

def lnorm1(rng, sigma):
    """로그정규(평균 1.0) 스케일러"""
    if sigma <= 0: return 1.0
    return math.exp(rng.gauss(0.0, sigma))

def gamma_mult(rng, k):
    """감마-포아송 혼합용 스케일러 (shape=k, scale=1/k)"""
    if k <= 0:
        return 1.0
    return rng.gammavariate(k, 1.0 / k)

# ------------- 시즌 프로필(숨김) -------------
def build_season_profile(seed_str: str) -> Dict[str, Dict[str,float]]:
    rng = make_rng(seed_str)
    prof = {}
    for team, base in TEAM_BASE.items():
        # 1) 샘플링: 캡 없이 뽑기
        sp  = base["SP"]  + rng.gauss(0.0, SD_SP)
        rp  = base["RP"]  + rng.gauss(0.0, SD_RP)
        bat = base["BAT"] + rng.gauss(0.0, SD_BAT)
        dfn = base["DEF"] + rng.gauss(0.0, SD_DEF)

        # 2) IMP도 샘플 + 캡 (이건 그대로 괜찮음)
        imp_eff = clamp(base["IMP"] + rng.gauss(0.0, SD_IMP), 40, 97)

        # 3) IMP 가산
        sp  += SP_FOR_SHARE * IMP_WEIGHTS["SP"]  * (imp_eff - 70)
        rp  += RP_FOR_SHARE * IMP_WEIGHTS["RP"]  * (imp_eff - 70)
        bat +=              IMP_WEIGHTS["BAT"]   * (imp_eff - 70)

        # 4) 최종 한 번만 캡
        sp  = clamp(sp,  30, 97)
        rp  = clamp(rp,  30, 97)
        bat = clamp(bat, 30, 97)
        dfn = clamp(dfn, 30, 97)

        prof[team] = {"SP":sp,"RP":rp,"BAT":bat,"DEF":dfn,"IMP_eff":imp_eff,
                      "stadium":base["stadium"]}
    return prof

# ------------- 경기 시뮬 -------------
def sim_game(home: str, away: str, season: Dict[str,Dict[str,float]], offense: float, rng: random.Random):
    H, A = season[home], season[away]
    park = park_index_runs(TEAM_BASE[home]["stadium"])

    # 1) 기본 9이닝 기대득점
    mu9_away_sp = expected_runs_per9(A["BAT"], H["SP"], H["DEF"], park, 1.00, offense, "SP")
    mu9_away_rp = expected_runs_per9(A["BAT"], H["RP"], H["DEF"], park, 1.00, offense, "RP")
    mu9_home_sp = expected_runs_per9(H["BAT"], A["SP"], A["DEF"], park, HOME_ADV, offense, "SP")
    mu9_home_rp = expected_runs_per9(H["BAT"], A["RP"], A["DEF"], park, HOME_ADV, offense, "RP")

    # 2) 경기-공유 + 팀 컨디션 스케일
    day  = lnorm1(rng, DAY_SIG)
    hot_h = lnorm1(rng, BATT_SIG)
    hot_a = lnorm1(rng, BATT_SIG)

    # 3) 감마-포아송 혼합(오버디스퍼전)
    sp_h = gamma_mult(rng, DISP_SP); rp_h = gamma_mult(rng, DISP_RP)
    sp_a = gamma_mult(rng, DISP_SP); rp_a = gamma_mult(rng, DISP_RP)

    # 4) 이닝당 람다
    lam_away_sp = (mu9_away_sp/9.0) * day * hot_a * sp_a
    lam_away_rp = (mu9_away_rp/9.0) * day * hot_a * rp_a
    lam_home_sp = (mu9_home_sp/9.0) * day * hot_h * sp_h
    lam_home_rp = (mu9_home_rp/9.0) * day * hot_h * rp_h

    # 5) 이닝 시뮬
    lines_away=[]; lines_home=[]
    for inn in range(1,9):
        if inn<=6:
            lines_away.append(poisson(lam_away_sp, rng))
            lines_home.append(poisson(lam_home_sp, rng))
        else:
            lines_away.append(poisson(lam_away_rp, rng))
            lines_home.append(poisson(lam_home_rp, rng))

    # 9회: 원정 먼저, 홈이 지거나 동점이면 9회말 타석
    lines_away.append(poisson(lam_away_rp, rng))
    if sum(lines_home) <= sum(lines_away):
        lines_home.append(poisson(lam_home_rp, rng))
    else:
        lines_home.append(0)

    # 연장 10~12회 (둘 다 RP 람다)
    inn = 10
    while sum(lines_home)==sum(lines_away) and inn<=12:
        lines_away.append(poisson(lam_away_rp, rng))
        lines_home.append(poisson(lam_home_rp, rng))
        inn+=1

    R_away=sum(lines_away); R_home=sum(lines_home)
    res = "HOME" if R_home>R_away else ("AWAY" if R_home<R_away else "TIE")
    return {"home":home,"away":away,"stadium":TEAM_BASE[home]["stadium"],
            "lines_home":lines_home,"lines_away":lines_away,
            "R_home":R_home,"R_away":R_away,"result":res}

# ------------- 히스토리/Elo 누적 -------------
def paths(outdir):
    os.makedirs(outdir, exist_ok=True)
    return {
        "history": os.path.join(outdir, "history.csv"),
        "elo_state": os.path.join(outdir, "elo_state.csv"),
        "elo_log": os.path.join(outdir, "elo_updates.csv"),
    }

def append_history(p_history, r_idx, game, seed, offense):
    is_new = not os.path.exists(p_history)
    with open(p_history, "a", newline="", encoding="utf-8") as f:
        w = csv.writer(f)
        if is_new:
            w.writerow(["ts","round","away","home","R_away","R_home","result","stadium","SEASON_SEED","OFFENSE"])
        w.writerow([
            datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
            r_idx, game["away"], game["home"], game["R_away"], game["R_home"],
            game["result"], game["stadium"], seed, offense
        ])

def load_elo(p_state, teams, init_rating=1500.0):
    elo = {t: init_rating for t in teams}
    if os.path.exists(p_state):
        try:
            with open(p_state, "r", encoding="utf-8") as f:
                rd = csv.DictReader(f)
                for row in rd:
                    t = row.get("team")
                    r = row.get("rating")
                    if t in elo and r is not None:
                        elo[t] = float(r)
        except:
            pass
    return elo

def save_elo(p_state, elo):
    with open(p_state, "w", newline="", encoding="utf-8") as f:
        w=csv.DictWriter(f, fieldnames=["team","rating"])
        w.writeheader()
        for t,r in elo.items():
            w.writerow({"team":t,"rating":round(r,3)})

def elo_update(elo_home, elo_away, score_home, score_away, K=8.0, hfa=30.0):
    Eh = 1.0 / (1.0 + 10 ** (-(((elo_home + hfa) - elo_away)/400.0)))
    Ea = 1.0 - Eh
    Sh = 1.0 if score_home > score_away else (0.0 if score_home < score_away else 0.5)
    Sa = 1.0 - Sh
    new_h = elo_home + K*(Sh - Eh)
    new_a = elo_away + K*(Sa - Ea)
    return new_h, new_a, Eh, Ea, Sh, Sa

def append_elo_log(p_log, rnd, home, away, before_h, before_a, after_h, after_a, Eh, Ea, Sh, Sa):
    is_new = not os.path.exists(p_log)
    with open(p_log, "a", newline="", encoding="utf-8") as f:
        w = csv.writer(f)
        if is_new:
            w.writerow(["ts","round","home","away","home_before","away_before","home_after","away_after","Eh","Ea","Sh","Sa"])
        w.writerow([
            datetime.now().strftime("%Y-%m-%d %H:%M:%S"), rnd, home, away,
            round(before_h,3), round(before_a,3), round(after_h,3), round(after_a,3),
            round(Eh,3), round(Ea,3), Sh, Sa
        ])

# ------------- IO -------------
def parse_inline_config(line: str):
    cfg={}
    if not line or not line.strip().startswith("@"): return cfg
    parts = line.strip()[1:].strip().split()
    for tok in parts:
        if "=" in tok:
            k,v = tok.split("=",1)
            cfg[k.strip().upper()] = v.strip()
    return cfg

def matchups_from_env_or_stdin():
    raw = os.environ.get("MATCHUPS","").strip()
    inline_cfg={}
    if not raw:
        buf=[]
        try:
            while True:
                line = sys.stdin.readline()
                if not line: break
                if not buf and line.strip().startswith("@"):
                    inline_cfg = parse_inline_config(line); continue
                if line.strip()=="": break
                buf.append(line.rstrip("\n"))
        except:
            pass
        raw="\n".join(buf).strip()
    return inline_cfg, [ln for ln in raw.splitlines() if ln.strip()]

def parse_games(lines: List[str]) -> List[Tuple[str,str]]:
    games=[]
    for raw in lines:
        parts=[p.strip() for p in raw.replace("\t",",").split(",") if p.strip()]
        if len(parts)!=2:
            print(f"[SKIP] 형식: '홈, 원정'  예) 'lg, 두산'"); continue
        h,a = norm_team_name(parts[0]), norm_team_name(parts[1])
        if h not in TEAM_BASE or a not in TEAM_BASE:
            print(f"[SKIP] 팀 인식 실패: '{parts[0]}' 또는 '{parts[1]}'"); continue
        games.append((h,a))
    return games

def write_linescore_csv(outdir: str, tag: str, game):
    os.makedirs(outdir, exist_ok=True)
    rows=[]; max_inn=max(len(game["lines_home"]), len(game["lines_away"]))
    for i in range(max_inn):
        rows.append({"이닝":i+1, game["away"]:game["lines_away"][i] if i<len(game["lines_away"]) else 0,
                               game["home"]:game["lines_home"][i] if i<len(game["lines_home"]) else 0})
    path = os.path.join(outdir, f"linescore_{tag}.csv")
    with open(path,"w",newline="",encoding="utf-8") as f:
        w=csv.DictWriter(f, fieldnames=list(rows[0].keys()))
        w.writeheader(); w.writerows(rows)
    return path

# ------------- 실행 루프 -------------
def run():
    cfg = {"SEASON_SEED": os.environ.get("SEASON_SEED", os.environ.get("SEED","")),
           "ROUNDS": os.environ.get("ROUNDS","1"),
           "OFFENSE": os.environ.get("OFFENSE", str(OFFENSE)),
           "OUTDIR": os.environ.get("OUTDIR","out"),
           "ELO": os.environ.get("ELO","1"),
           "ELO_K": os.environ.get("ELO_K","8"),
           "ELO_HOME_ADV": os.environ.get("ELO_HOME_ADV","30"),
           "ELO_INIT": os.environ.get("ELO_INIT","1500")}
    inline_cfg, lines = matchups_from_env_or_stdin()
    cfg.update(inline_cfg)

    # 인라인/환경으로 SD/분산 파라미터 덮어쓰기 (있으면 반영)
    globals().update({
      "SD_SP":  to_float(cfg.get("SD_SP",  SD_SP),  SD_SP),
      "SD_RP":  to_float(cfg.get("SD_RP",  SD_RP),  SD_RP),
      "SD_BAT": to_float(cfg.get("SD_BAT", SD_BAT), SD_BAT),
      "SD_DEF": to_float(cfg.get("SD_DEF", SD_DEF), SD_DEF),
      "SD_IMP": to_float(cfg.get("SD_IMP", SD_IMP), SD_IMP),
      "DAY_SIG":  to_float(cfg.get("DAY_SIG",  DAY_SIG),  DAY_SIG),
      "BATT_SIG": to_float(cfg.get("BATT_SIG", BATT_SIG), BATT_SIG),
      "DISP_SP":  to_float(cfg.get("DISP_SP",  DISP_SP),  DISP_SP),
      "DISP_RP":  to_float(cfg.get("DISP_RP",  DISP_RP),  DISP_RP),
    })

    ROUNDS  = max(1, min(1000, to_int(cfg.get("ROUNDS",1), 1)))
    OUTDIR  = cfg.get("OUTDIR","out")
    SEED    = cfg.get("SEASON_SEED","")
    # run() 시작부에서, cfg.update(...) 직후 아무데나
    SHOW_BOARD = str(cfg.get("SCOREBOARD", os.environ.get("SCOREBOARD","0"))).lower() in ("1","true","y","on")
    SHOW_RESULT = str(cfg.get("RESULT_LINE", os.environ.get("RESULT_LINE","1"))).lower() in ("1","true","y","on")

   # OFFENSE 결정 (auto / fixed)
    off_mode = str(cfg.get("OFFENSE", "")).strip().lower()
    if off_mode in ("auto","rand","random"):
        OFFF = sample_offense(
            SEED,
            mean        = to_float(cfg.get("OFFENSE_MEAN",  0.96), 0.96),
            sd          = to_float(cfg.get("OFFENSE_SD",    0.06), 0.06),
            lo          = to_float(cfg.get("OFFENSE_MIN",   0.82), 0.82),
            hi          = to_float(cfg.get("OFFENSE_MAX",   1.15), 1.15),
            sticky      = to_float(cfg.get("OFFENSE_STICKY",0.0),  0.0),
            cycle_amp   = to_float(cfg.get("OFFENSE_CYCLE_AMP",    0.06), 0.06),
            cycle_period= to_float(cfg.get("OFFENSE_CYCLE_PERIOD", 6.0),  6.0),
            base_year   = to_int(cfg.get("BASE_YEAR", 2015), 2015),
        )
        off_label = "auto"
    else:
        OFFF = max(0.5, min(1.5, to_float(cfg.get("OFFENSE", OFFENSE), OFFENSE)))
        off_label = "fixed"

    # ↓ 옵션으로만 표시 (기본 안 찍힘)
    SHOW_OFFENSE = str(cfg.get("SHOW_OFFENSE", os.environ.get("SHOW_OFFENSE","0"))).lower() in ("1","true","y","on")
    if SHOW_OFFENSE:
        print(f"[SEASON] OFFENSE={OFFF:.3f} ({off_label})")

    season = build_season_profile(SEED)
    games  = parse_games(lines)
    if not games:
        print("실행된 경기가 없습니다.")
        return

    P = paths(OUTDIR)
    game_rng = random.Random(time.time_ns())

    # Elo 옵션
    use_elo = str(cfg.get("ELO","1")).strip().lower() in ("1","true","t","y","yes","on")
    if use_elo:
        global ELO_STATE
        ELO_STATE = load_elo(P["elo_state"], list(TEAM_BASE.keys()), init_rating=to_float(cfg.get("ELO_INIT","1500"),1500.0))
        K   = to_float(cfg.get("ELO_K","8"), 8.0)
        HFA = to_float(cfg.get("ELO_HOME_ADV","30"), 30.0)

    # 경기 실행
    for r in range(1, ROUNDS+1):
        for (h,a) in games:
            g = sim_game(h,a, season, OFFF, game_rng)
            if g is None: 
                continue
            tag = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
            write_linescore_csv(OUTDIR, tag, g)
            RH, RA = g["R_home"], g["R_away"]
            if SHOW_RESULT:
                print(f"[R{r}] {a} @ {h} ({g['stadium']}) → {RA}:{RH}  결과:{g['result']}")
            if SHOW_BOARD:
                print_scoreboard(g)

            # 히스토리 누적
            append_history(P["history"], r, g, SEED, OFFF)

            # Elo 업데이트
            if use_elo:
                before_h = ELO_STATE.get(h, 1500.0)
                before_a = ELO_STATE.get(a, 1500.0)
                after_h, after_a, Eh, Ea, Sh, Sa = elo_update(before_h, before_a, RH, RA, K=K, hfa=HFA)
                ELO_STATE[h], ELO_STATE[a] = after_h, after_a
                save_elo(P["elo_state"], ELO_STATE)
                append_elo_log(P["elo_log"], r, h, a, before_h, before_a, after_h, after_a, Eh, Ea, Sh, Sa)

# __name__ 체크 없이 자동 실행 (비활성화: SKIP_AUTORUN=1)
if os.environ.get("SKIP_AUTORUN","0") != "1":
    run()

Embed on website

To embed this project on your website, copy the following code and paste it into your website's HTML: