# -*- 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()
To embed this project on your website, copy the following code and paste it into your website's HTML: