# -*- coding: utf-8 -*-
"""
KBO 플레이어 기반 한 경기 시뮬레이터 (myCompiler OK, 표준 라이브러리만)
- 인라인/환경 헤더 예:
  @ SEASON_SEED=2029 OFFENSE=auto SCOREBOARD=1 BOXSCORE=1 SHOW_OFFENSE=1
  lg, 두산
"""

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

# ------------------------- 유틸 -------------------------
def clamp(x, lo, hi):
    # BUGFIX: 원래 구현은 범위 내에서도 hi를 반환하는 버그가 있었음
    return lo if x < lo else (hi if x > hi else x)

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

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

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 str(seed_str)))

# ------------------------- 리그/구장/옵션 -------------------------
BASE_RPG = 4.3   # 팀당 9이닝 기준 베이스 득점
HOME_ADV = 1.04  # 홈 이득
OFFENSE_DEFAULT = to_float(os.environ.get("OFFENSE","0.96"), 0.96)

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

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

# ------------------------- 팀 베이스(스카우팅 앵커) -------------------------
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)

# ------------------------- OFFENSE 자동 샘플 -------------------------
def sample_offense(seed, mean=0.96, sd=0.06, lo=0.82, hi=1.15):
    rng = make_rng(f"OFFENSE::{seed}")
    val = rng.gauss(mean, sd)
    return clamp(val, lo, hi)

# ------------------------- 선수 모델 -------------------------
POS_ORDER = ["C","1B","2B","3B","SS","LF","CF","RF","DH"]

class Batter:
    __slots__=("name","hand","pos","con","pow","eye","spd","defense","stats")
    def __init__(self, name, hand, pos, con,powr,eye,spd,defense):
        self.name=name; self.hand=hand; self.pos=pos
        self.con=con; self.pow=powr; self.eye=eye; self.spd=spd; self.defense=defense
        self.stats={"AB":0,"H":0,"2B":0,"3B":0,"HR":0,"BB":0,"SO":0,"RBI":0,"R":0}

class Pitcher:
    __slots__=("name","hand","role","stuff","ctrl","stam","gb","stats","pitches")
    def __init__(self, name, hand, role, stuff, ctrl, stam, gb):
        self.name=name; self.hand=hand; self.role=role
        self.stuff=stuff; self.ctrl=ctrl; self.stam=stam; self.gb=gb
        self.stats={"IPo":0,"H":0,"R":0,"ER":0,"BB":0,"SO":0,"HR":0}
        self.pitches=0

class Team:
    __slots__=("name","stadium","park_idx","batters","rotation","bullpen","lineup","def_avg")
    def __init__(self, name, stadium):
        self.name=name; self.stadium=stadium; self.park_idx=park_index_runs(stadium)
        self.batters: List[Batter]=[]
        self.rotation: List[Pitcher]=[]
        self.bullpen:  List[Pitcher]=[]
        self.lineup:   List[Batter]=[]
        self.def_avg=70.0

# ------------------------- 인라인 로스터 (여기에 선수 직접 입력) -------------------------
# 필드:
#   BAT: name, hand(L/R), pos(C/1B/2B/3B/SS/LF/CF/RF/DH), con,pow,eye,spd,defense, order(1~9 선택)
#   PIT: name, hand(L/R), role(SP/CL/SU/MR/LR), stuff,ctrl,stam,gb
INLINE_ROSTERS: Dict[str, Dict[str, list]] = {
    "LG 트윈스": {
        "batters": [
            {"name":"홍창기","hand":"L","pos":"RF","con":82,"pow":62,"eye":88,"spd":72,"defense":75,"order":1},
            {"name":"문보경","hand":"L","pos":"3B","con":72,"pow":76,"eye":60,"spd":60,"defense":70,"order":2},
            {"name":"오스틴","hand":"R","pos":"1B","con":66,"pow":84,"eye":56,"spd":48,"defense":62,"order":3},
            {"name":"박동원","hand":"R","pos":"C" ,"con":60,"pow":72,"eye":56,"spd":45,"defense":72,"order":4},
            {"name":"김현수","hand":"L","pos":"LF","con":74,"pow":78,"eye":58,"spd":38,"defense":58,"order":5},
            {"name":"문성주","hand":"L","pos":"CF","con":76,"pow":50,"eye":66,"spd":76,"defense":76,"order":6},
            {"name":"신민재","hand":"R","pos":"2B","con":66,"pow":40,"eye":56,"spd":84,"defense":78,"order":7},
            {"name":"오지환","hand":"L","pos":"SS","con":68,"pow":58,"eye":58,"spd":68,"defense":82,"order":8},
            {"name":"김범석","hand":"R","pos":"DH","con":60,"pow":82,"eye":46,"spd":34,"defense":40,"order":9},
        ],
        "pitchers": [
            {"name":"켈리","hand":"R","role":"SP","stuff":78,"ctrl":75,"stam":86,"gb":50},
            {"name":"임찬규","hand":"R","role":"SP","stuff":74,"ctrl":73,"stam":80,"gb":52},
            {"name":"정우영","hand":"R","role":"SU","stuff":76,"ctrl":74,"stam":60,"gb":65},
            {"name":"고우석","hand":"R","role":"CL","stuff":84,"ctrl":80,"stam":60,"gb":60},
            {"name":"김진성","hand":"R","role":"MR","stuff":72,"ctrl":72,"stam":60,"gb":60},
            {"name":"김윤식","hand":"L","role":"LR","stuff":70,"ctrl":71,"stam":72,"gb":62},
        ],
    },
    "두산 베어스": {
        "batters": [
            {"name":"정수빈","hand":"L","pos":"CF","con":72,"pow":44,"eye":60,"spd":82,"defense":82,"order":1},
            {"name":"허경민","hand":"R","pos":"3B","con":70,"pow":50,"eye":58,"spd":68,"defense":78,"order":2},
            {"name":"양석환","hand":"R","pos":"1B","con":64,"pow":80,"eye":54,"spd":46,"defense":60,"order":3},
            {"name":"양의지","hand":"R","pos":"C" ,"con":72,"pow":82,"eye":58,"spd":36,"defense":72,"order":4},
            {"name":"김재환","hand":"L","pos":"LF","con":66,"pow":84,"eye":54,"spd":40,"defense":56,"order":5},
            {"name":"강승호","hand":"R","pos":"2B","con":64,"pow":64,"eye":52,"spd":62,"defense":68,"order":6},
            {"name":"김대한","hand":"R","pos":"RF","con":58,"pow":66,"eye":48,"spd":60,"defense":62,"order":7},
            {"name":"김재호","hand":"R","pos":"SS","con":64,"pow":50,"eye":56,"spd":52,"defense":78,"order":8},
            {"name":"페르난데스","hand":"L","pos":"DH","con":76,"pow":70,"eye":58,"spd":28,"defense":35,"order":9},
        ],
        "pitchers": [
            {"name":"곽빈","hand":"R","role":"SP","stuff":82,"ctrl":78,"stam":86,"gb":52},
            {"name":"장원준","hand":"L","role":"SP","stuff":70,"ctrl":72,"stam":78,"gb":55},
            {"name":"이승진","hand":"R","role":"SU","stuff":75,"ctrl":72,"stam":60,"gb":60},
            {"name":"홍건희","hand":"R","role":"CL","stuff":78,"ctrl":76,"stam":60,"gb":60},
            {"name":"최원준","hand":"R","role":"MR","stuff":72,"ctrl":70,"stam":60,"gb":60},
            {"name":"최승용","hand":"L","role":"LR","stuff":68,"ctrl":68,"stam":70,"gb":62},
        ],
    },
}

# ------------------------- 로스터 생성 (랜덤용 기존 함수) -------------------------
def gen_name(rng, team, tag, i):
    short = {"LG 트윈스":"LG","두산 베어스":"DOO","키움 히어로즈":"KIW","SSG 랜더스":"SSG","KT 위즈":"KT",
             "NC 다이노스":"NC","KIA 타이거즈":"KIA","삼성 라이온즈":"SAM","롯데 자이언츠":"LOT","한화 이글스":"HAN"}.get(team, "T")
    return f"{short}_{tag}{i:02d}"

def make_team_with_players(team_name: str, seed: str, sd=10.0) -> Team:
    base = TEAM_BASE[team_name]
    rng = make_rng(f"ROSTER::{seed}::{team_name}")
    t = Team(team_name, base["stadium"])

    pos_bias = {
        "C":  {"pow":-2, "def":+6},
        "1B": {"pow":+6, "def":-3},
        "2B": {"pow":-2, "def":+2},
        "3B": {"pow":+2, "def":+1},
        "SS": {"pow":-3, "def":+6},
        "LF": {"pow":+2, "def":-1},
        "CF": {"pow":-1, "def":+4},
        "RF": {"pow":+3, "def":-1},
        "DH": {"pow":+5, "def":-6},
    }
    for pos in POS_ORDER + ["UTIL","UTIL","UTIL"]:
        pow_bias = pos_bias.get(pos,{}).get("pow",0)
        def_bias = pos_bias.get(pos,{}).get("def",0)
        con = clamp(rng.gauss(base["BAT"], sd), 30, 95)
        powr= clamp(rng.gauss(base["BAT"]+pow_bias, sd), 30, 95)
        eye = clamp(rng.gauss(base["BAT"], sd), 30, 95)
        spd = clamp(rng.gauss(70, 10), 30, 90)
        dfn = clamp(rng.gauss(base["DEF"]+def_bias, sd), 30, 95)
        hand= "R" if rng.random()<0.7 else "L"
        name= gen_name(rng, team_name, "BAT", len(t.batters)+1)
        real_pos = pos if pos in POS_ORDER else rng.choice(POS_ORDER[:-1])
        t.batters.append(Batter(name, hand, real_pos, con,powr,eye,spd,dfn))

    for i in range(5):
        stf = clamp(rng.gauss(base["SP"], sd), 30, 95)
        ctrl= clamp(rng.gauss(base["SP"], sd), 30, 95)
        stam= clamp(rng.gauss(base["SP"], sd), 30, 95)
        gb  = clamp(rng.gauss(70, 10), 30, 90)
        name= gen_name(rng, team_name, "SP", i+1)
        hand= "R" if rng.random()<0.7 else "L"
        t.rotation.append(Pitcher(name, hand, "SP", stf, ctrl, stam, gb))

    roles = [("CL", base["RP"]+8), ("SU", base["RP"]+4), ("SU", base["RP"]+2),
             ("MR", base["RP"]), ("MR", base["RP"]-2), ("LR", base["RP"]-4)]
    for role, mu in roles:
        stf = clamp(rng.gauss(mu, sd), 30, 95)
        ctrl= clamp(rng.gauss(mu, sd), 30, 95)
        stam= clamp(rng.gauss(60 if role!="LR" else 70, 8), 30, 95)
        gb  = clamp(rng.gauss(70, 10), 30, 90)
        name= gen_name(rng, team_name, role, len(t.bullpen)+1)
        hand= "R" if rng.random()<0.72 else "L"
        t.bullpen.append(Pitcher(name, hand, role, stf, ctrl, stam, gb))

    imp = TEAM_BASE[team_name]["IMP"]
    boost_sp  = (imp-70)*0.6
    boost_bat = (imp-70)*0.4
    best_sp = max(t.rotation, key=lambda p: p.stuff + p.ctrl + p.stam)
    best_sp.stuff = clamp(best_sp.stuff + boost_sp, 30, 99)
    best_sp.ctrl  = clamp(best_sp.ctrl  + boost_sp*0.6, 30, 99)
    best_sp.stam  = clamp(best_sp.stam  + boost_sp*0.5, 30, 99)
    best_bat = max(t.batters, key=lambda b: b.con*0.6 + b.pow*0.8 + b.eye*0.4)
    best_bat.con  = clamp(best_bat.con + boost_bat*0.6, 30, 99)
    best_bat.pow  = clamp(best_bat.pow + boost_bat, 30, 99)
    best_bat.eye  = clamp(best_bat.eye + boost_bat*0.5, 30, 99)

    starters=[]
    used=set()
    for pos in POS_ORDER[:-1]:
        cand=[b for b in t.batters if b.pos==pos and b not in used]
        if not cand: cand=[b for b in t.batters if b not in used]
        pick=max(cand, key=lambda b:b.con*0.6+b.pow*0.8+b.eye*0.4)
        starters.append(pick); used.add(pick)
    rest=[b for b in t.batters if b not in used]
    dh=max(rest, key=lambda b:b.con*0.5+b.pow*0.9+b.eye*0.3)
    dh.pos="DH"; starters.append(dh); used.add(dh)
    t.lineup = starters
    t.def_avg = sum(b.defense for b in starters if b.pos!="DH")/8.0
    return t

# ------------------------- 인라인 로스터 빌더 -------------------------
def _bat_score(b: Batter) -> float:
    return b.con*0.6 + b.pow*0.8 + b.eye*0.4

def make_team_from_inline(team_name: str) -> Team:
    if team_name not in INLINE_ROSTERS:
        raise KeyError(f"{team_name} 인라인 로스터가 없습니다.")
    base = TEAM_BASE[team_name]
    T = Team(team_name, base["stadium"])
    src = INLINE_ROSTERS[team_name]

    # 타자
    for row in src.get("batters", []):
        b = Batter(
            row["name"], str(row.get("hand","R")).upper()[0],
            row.get("pos","DH").upper(),
            to_int(row.get("con",60),60), to_int(row.get("pow",60),60),
            to_int(row.get("eye",60),60), to_int(row.get("spd",60),60),
            to_int(row.get("defense",60),60)
        )
        try:
            b.stats["_order"] = int(row.get("order")) if row.get("order") not in (None,"") else None
        except:
            b.stats["_order"] = None
        T.batters.append(b)

    # 라인업
    ordered = [b for b in T.batters if b.stats.get("_order")]
    unordered = [b for b in T.batters if not b.stats.get("_order")]
    lineup=[]
    if ordered:
        ordered.sort(key=lambda x: x.stats["_order"])
        seen=set()
        for b in ordered:
            if b.stats["_order"]>=1 and b.stats["_order"]<=9 and b not in seen:
                lineup.append(b); seen.add(b)
            if len(lineup)==9: break
        if len(lineup)<9:
            # 포지션 미스럽거나 부족하면 타격 상위로 채움
            rest=[b for b in unordered if b not in lineup]
            lineup += sorted(rest, key=_bat_score, reverse=True)[:9-len(lineup)]
    else:
        starters=[]; used=set()
        for pos in POS_ORDER[:-1]:
            cand=[b for b in T.batters if b.pos==pos and b not in used]
            if cand:
                pick = max(cand, key=_bat_score)
            else:
                remain=[b for b in T.batters if b not in used]
                pick = max(remain, key=_bat_score) if remain else None
            if pick:
                starters.append(pick); used.add(pick)
        rest=[b for b in T.batters if b not in used]
        if rest:
            dh=max(rest, key=_bat_score); dh.pos="DH"; starters.append(dh); used.add(dh)
        lineup = starters[:9]
    T.lineup = lineup

    # 수비 평균 (DH 제외)
    fielders = [b for b in T.lineup if b.pos != "DH"]
    T.def_avg = (sum(b.defense for b in fielders)/len(fielders)) if fielders else 70.0

    # 투수
    for row in src.get("pitchers", []):
        p = Pitcher(
            row["name"], str(row.get("hand","R")).upper()[0],
            row.get("role","SP").upper(),
            to_int(row.get("stuff",70),70), to_int(row.get("ctrl",70),70),
            to_int(row.get("stam",75),75), to_int(row.get("gb",70),70)
        )
        if p.role=="SP":
            T.rotation.append(p)
        else:
            T.bullpen.append(p)
    # 최소 보정
    if not T.rotation:
        T.rotation.append(Pitcher("FAKE_SP","R","SP",70,70,75,70))
    if not T.bullpen:
        T.bullpen.extend([
            Pitcher("FAKE_CL","R","CL",75,75,60,70),
            Pitcher("FAKE_SU","R","SU",73,72,60,70),
            Pitcher("FAKE_MR","R","MR",70,70,60,70),
            Pitcher("FAKE_LR","R","LR",68,68,70,70),
        ])
    return T

# ------------------------- 확률 모델(간단) -------------------------
def prob_event(b: Batter, p: Pitcher, team_def: float, park_idx: float, offense: float, home_boost: float):
    k  = 0.18 + 0.0025*(p.stuff-50) - 0.0018*(b.con-50)
    bb = 0.08 + 0.0020*(b.eye-50)   - 0.0015*(p.ctrl-50)
    hr = 0.03 + 0.0020*(b.pow-50)   - 0.0010*(p.stuff-50)
    adj = offense * park_idx * home_boost
    k  = clamp(k,  0.08, 0.35)
    bb = clamp(bb, 0.04, 0.16)
    hr = clamp(hr * (0.9 + 0.2*adj), 0.005, 0.09)
    stay = clamp(1.0 - k - bb, 0.50, 0.92)
    babip = 0.300 + 0.0015*(b.con-50) - 0.0012*(team_def-50)
    babip = clamp(babip * (0.95 + 0.10*adj), 0.250, 0.360)
    p2b = clamp(0.055 + 0.0007*(b.pow-50), 0.03, 0.10)
    p3b = clamp(0.006 + 0.0002*(b.spd-50), 0.002, 0.020)
    pK = k
    pBB= bb
    pHR= hr
    pBIP = stay - hr
    inplay_hit = babip
    p2 = pBIP * (p2b / (p2b + p3b + 1e-9)) * inplay_hit
    p3 = pBIP * (p3b / (p2b + p3b + 1e-9)) * inplay_hit
    p1 = pBIP * (1.0 - (p2b + p3b)) * inplay_hit
    pOUT = max(0.0, 1.0 - (pK+pBB+pHR+p1+p2+p3))
    return (pK,pBB,pHR,p2,p3,p1,pOUT)

def pick_event(rng: random.Random, probs: Tuple[float,...]) -> int:
    x = rng.random()
    acc=0.0
    for i,p in enumerate(probs):
        acc+=p
        if x<=acc: return i
    return len(probs)-1

# ------------------------- 주자/득점 처리 -------------------------
def advance_bases(state: List[int], n: int) -> int:
    runs=0
    for i in range(2, -1, -1):
        if state[i]:
            t = i + n
            if t >= 3:
                runs += 1
            else:
                state[t] = 1
            state[i] = 0
    if n <= 3:
        if n == 1:
            if state[0]==1:
                pass
            state[0]=1
        elif n == 2:
            state[1]=1
        elif n == 3:
            state[2]=1
    return runs

def walk_bases(state: List[int]) -> int:
    runs=0
    if state[0] and state[1] and state[2]:
        runs+=1
    if state[1] and state[0]:
        state[2]=1
    if state[0]:
        state[1]=1
    state[0]=1
    return runs

# ------------------------- 경기 시뮬 -------------------------
def sim_game(home: Team, away: Team, offense: float, rng: random.Random, scoreboard=False, boxscore=False):
    p_home = home.rotation[0]
    p_away = away.rotation[0]
    sp_pitch_cap = 90 + int((p_home.stam + p_away.stam)/2 - 70)
    rp_pitch_cap = 25

    def pick_reliever(team: Team, inning: int, diff: int):
        cand = []
        if inning>=8 and abs(diff)<=1:
            role_order = ["CL","SU","SU","MR","MR","LR"]
        else:
            role_order = ["MR","MR","LR","SU","SU","CL"]
        for rl in role_order:
            for p in team.bullpen:
                if p.role==rl and p.pitches < rp_pitch_cap:
                    cand.append(p)
        return cand[0] if cand else team.bullpen[0]

    idx_home=0; idx_away=0
    bases=[0,0,0]
    lines_h=[]; lines_a=[]
    R_home=0; R_away=0

    def pitch_stat(pit, t):
        pit.stats["IPo"] += t

    for inn in range(1, 10+20):
        # --- 원정 ---
        outs=0; bases=[0,0,0]; runs=0
        pitcher = p_home if inn<=6 else (p_home if p_home.pitches<sp_pitch_cap else pick_reliever(home, inn, R_home-R_away))
        while outs<3:
            bat = away.lineup[idx_away]; idx_away=(idx_away+1)%9
            probs = prob_event(bat, pitcher, home.def_avg, home.park_idx, offense, 1.00)
            ev = pick_event(rng, probs)
            if ev==0:
                outs += 1; bat.stats["SO"]+=1; pitcher.stats["SO"]+=1; pitcher.pitches += 4
            elif ev==1:
                bb_runs = walk_bases(bases); runs+=bb_runs; bat.stats["BB"]+=1; pitcher.stats["BB"]+=1; pitcher.pitches += 5
            elif ev==2:
                scored = sum(bases)+1; runs+=scored; bat.stats["HR"]+=1; bat.stats["H"]+=1; bat.stats["RBI"]+=scored; bat.stats["R"]+=1
                pitcher.stats["HR"]+=1; pitcher.stats["H"]+=1; bases=[0,0,0]; pitcher.pitches += 4
            elif ev==3:
                r = advance_bases(bases,2); runs+=r; bat.stats["2B"]+=1; bat.stats["H"]+=1; pitcher.stats["H"]+=1; pitcher.pitches += 3
            elif ev==4:
                r = advance_bases(bases,3); runs+=r; bat.stats["3B"]+=1; bat.stats["H"]+=1; pitcher.stats["H"]+=1; pitcher.pitches += 3
            elif ev==5:
                r = advance_bases(bases,1); runs+=r; bat.stats["H"]+=1; pitcher.stats["H"]+=1; pitcher.pitches += 3
            else:
                outs += 1; pitcher.pitches += 3
            if ev not in (1,):
                bat.stats["AB"]+=1
            pitcher.stats["R"] += runs - (pitcher.stats["R"])
            pitcher.stats["ER"]= pitcher.stats["R"]
        lines_a.append(runs); R_away+=runs
        pitch_stat(pitcher, 3)

        # --- 홈 ---
        outs=0; bases=[0,0,0]; runs=0
        pitcher = p_away if inn<=6 else (p_away if p_away.pitches<sp_pitch_cap else pick_reliever(away, inn, R_away-R_home))
        while outs<3:
            if inn>=9 and R_home>R_away:
                break
            bat = home.lineup[idx_home]; idx_home=(idx_home+1)%9
            probs = prob_event(bat, pitcher, away.def_avg, home.park_idx, offense, HOME_ADV)
            ev = pick_event(rng, probs)
            if ev==0:
                outs += 1; bat.stats["SO"]+=1; pitcher.stats["SO"]+=1; pitcher.pitches+=4
            elif ev==1:
                bb_runs = walk_bases(bases); runs+=bb_runs; bat.stats["BB"]+=1; pitcher.stats["BB"]+=1; pitcher.pitches+=5
            elif ev==2:
                scored = sum(bases)+1; runs+=scored; bat.stats["HR"]+=1; bat.stats["H"]+=1; bat.stats["RBI"]+=scored; bat.stats["R"]+=1
                pitcher.stats["HR"]+=1; pitcher.stats["H"]+=1; bases=[0,0,0]; pitcher.pitches+=4
            elif ev==3:
                r = advance_bases(bases,2); runs+=r; bat.stats["2B"]+=1; bat.stats["H"]+=1; pitcher.stats["H"]+=1; pitcher.pitches+=3
            elif ev==4:
                r = advance_bases(bases,3); runs+=r; bat.stats["3B"]+=1; bat.stats["H"]+=1; pitcher.stats["H"]+=1; pitcher.pitches+=3
            elif ev==5:
                r = advance_bases(bases,1); runs+=r; bat.stats["H"]+=1; pitcher.stats["H"]+=1; pitcher.pitches+=3
            else:
                outs += 1; pitcher.pitches+=3
            if ev not in (1,):
                bat.stats["AB"]+=1
            pitcher.stats["R"] += runs - (pitcher.stats["R"])
            pitcher.stats["ER"]= pitcher.stats["R"]
        lines_h.append(runs); R_home+=runs
        pitch_stat(pitcher, 3)

        if inn>=9 and R_home!=R_away:
            break
        if inn>=12 and R_home==R_away:
            break

    result = "HOME" if R_home>R_away else ("AWAY" if R_home<R_away else "TIE")
    if scoreboard:
        print(f"[BOX] {away.name} @ {home.name}  ({home.stadium})")
        inn_n = max(len(lines_h), len(lines_a))
        hdr = "    " + " ".join(f"{i:>2}" for i in range(1, inn_n+1)) + " |  R"
        print(hdr)
        print(f"{away.name[:6]:<6}" + " ".join(f"{v:>2}" for v in lines_a) + f" | {R_away:>2}")
        print(f"{home.name[:6]:<6}" + " ".join(f"{v:>2}" for v in lines_h) + f" | {R_home:>2}")
        print(f"→ 결과: {result}")

    if boxscore:
        print("\n[타자 박스] AWAY")
        for b in away.lineup:
            print(f"{b.name:<10} {b.pos:>2}  AB {b.stats['AB']:>2} H {b.stats['H']:>2} 2B {b.stats['2B']:>2} 3B {b.stats['3B']:>2} HR {b.stats['HR']:>2} BB {b.stats['BB']:>2} SO {b.stats['SO']:>2} R {b.stats['R']:>2} RBI {b.stats['RBI']:>2}")
        print("[타자 박스] HOME")
        for b in home.lineup:
            print(f"{b.name:<10} {b.pos:>2}  AB {b.stats['AB']:>2} H {b.stats['H']:>2} 2B {b.stats['2B']:>2} 3B {b.stats['3B']:>2} HR {b.stats['HR']:>2} BB {b.stats['BB']:>2} SO {b.stats['SO']:>2} R {b.stats['R']:>2} RBI {b.stats['RBI']:>2}")
        print("\n[투수 박스] AWAY")
        pits = [*away.rotation[:1], *away.bullpen]
        for p in pits:
            if p.stats["IPo"] or p.pitches:
                ip = f"{p.stats['IPo']//3}.{p.stats['IPo']%3}"
                print(f"{p.name:<10} {p.role:>2} IP {ip:>4} H {p.stats['H']:>2} R {p.stats['R']:>2} ER {p.stats['ER']:>2} BB {p.stats['BB']:>2} SO {p.stats['SO']:>2} HR {p.stats['HR']:>2} PC {p.pitches:>3}")
        print("[투수 박스] HOME")
        pits = [*home.rotation[:1], *home.bullpen]
        for p in pits:
            if p.stats["IPo"] or p.pitches:
                ip = f"{p.stats['IPo']//3}.{p.stats['IPo']%3}"
                print(f"{p.name:<10} {p.role:>2} IP {ip:>4} H {p.stats['H']:>2} R {p.stats['R']:>2} ER {p.stats['ER']:>2} BB {p.stats['BB']:>2} SO {p.stats['SO']:>2} HR {p.stats['HR']:>2} PC {p.pitches:>3}")

    return {
        "home":home.name,"away":away.name,"stadium":home.stadium,
        "lines_home":lines_h,"lines_away":lines_a,
        "R_home":R_home,"R_away":R_away,"result":result
    }

# ------------------------- CSV -------------------------
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

# ------------------------- RUN -------------------------
def run():
    cfg, games = read_matchups()
    if not games:
        print("실행된 경기가 없습니다."); 
        return

    SEED = cfg.get("SEASON_SEED", os.environ.get("SEASON_SEED",""))
    off_mode = str(cfg.get("OFFENSE", os.environ.get("OFFENSE","auto"))).lower()
    if off_mode in ("auto","rand","random"):
        OFFF = sample_offense(SEED)
        off_label = "auto"
    else:
        OFFF = clamp(to_float(cfg.get("OFFENSE", OFFENSE_DEFAULT), OFFENSE_DEFAULT), 0.5, 1.5)
        off_label = "fixed"

    if str(cfg.get("SHOW_OFFENSE", os.environ.get("SHOW_OFFENSE","0"))).lower() in ("1","y","true","on"):
        print(f"[SEASON] OFFENSE={OFFF:.3f} ({off_label})")

    OUTDIR = cfg.get("OUTDIR", os.environ.get("OUTDIR","out"))
    SCOREBOARD = str(cfg.get("SCOREBOARD", os.environ.get("SCOREBOARD","0"))).lower() in ("1","y","true","on")
    BOXSCORE   = str(cfg.get("BOXSCORE",   os.environ.get("BOXSCORE","0"))).lower()   in ("1","y","true","on")

    rng = make_rng(f"GAME::{SEED}")

    # 팀/선수 생성: 인라인 로스터 우선, 없으면 랜덤 생성
    teams: Dict[str,Team]={}
    requested = list({h for h,_ in games} | {a for _,a in games})
    for t in requested:
        if t in INLINE_ROSTERS and (INLINE_ROSTERS[t].get("batters") or INLINE_ROSTERS[t].get("pitchers")):
            teams[t] = make_team_from_inline(t)
        else:
            teams[t] = make_team_with_players(t, SEED, sd=10.0)

    # 경기 실행
    for (h,a) in games:
        g = sim_game(teams[h], teams[a], OFFF, rng, scoreboard=SCOREBOARD, boxscore=BOXSCORE)
        tag = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
        write_linescore_csv(OUTDIR, tag, g)
        print(f"{a} @ {h} ({TEAM_BASE[h]['stadium']}) → {g['R_away']}:{g['R_home']}  결과:{g['result']}")

# ------------------------- 입력 파서 (원본과 동일) -------------------------
def parse_inline_config(line: str):
    cfg={}
    if not line or not line.strip().startswith("@"):
        return cfg
    sanitized = re.sub(r'\s*=\s*','=', line.strip()[1:].strip())
    for tok in sanitized.split():
        if "=" in tok:
            k,v = tok.split("=",1)
            cfg[k.strip().upper()] = v.strip()
    return cfg

def read_matchups():
    raw = os.environ.get("MATCHUPS","").strip()
    cfg={}
    if not raw:
        buf=[]
        while True:
            line=sys.stdin.readline()
            if not line: break
            if not buf and line.strip().startswith("@"):
                cfg = parse_inline_config(line); continue
            if line.strip()=="": break
            buf.append(line.rstrip("\n"))
        raw="\n".join(buf).strip()
    games=[]
    for ln in raw.splitlines():
        if not ln.strip(): continue
        parts=[p.strip() for p in ln.replace("\t",",").split(",") if p.strip()]
        if len(parts)!=2: 
            print("[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("[SKIP] 팀 인식 실패:", parts); 
            continue
        games.append((h,a))
    return cfg, games

# myCompiler: __name__ 체크 없이 자동 실행
run()

Embed on website

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