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