import random
from dataclasses import dataclass, field
from typing import List, Dict, Callable, Optional
# ======================== 엔진 v7 (Player/Team/Logger/weighted_choice/핵심 로직) ========================
def clamp(p: float) -> float:
return max(0.05, min(p, 0.95))
def stat(v: int) -> float:
return v / 100.0
@dataclass
class Player:
name: str
pos: str
shoot: int = 70
handle: int = 70
agility: int = 70
physical: int = 70
sense: int = 70
points: int = 0
assists: int = 0
two_att: int = 0
two_made: int = 0
three_att: int = 0
three_made: int = 0
off_reb: int = 0
def_reb: int = 0
turnovers: int = 0
@dataclass
class Team:
name: str
players: List[Player]
score: int = 0
def get(self, pos: str) -> Player:
for p in self.players:
if p.pos == pos:
return p
return random.choice(self.players)
class Logger:
def __init__(self, verbose=False):
self.lines: List[str] = []
self.verbose = verbose
def log(self, text: str):
if self.verbose:
self.lines.append(text)
def weighted_choice(players):
weights = [p.physical * 0.6 + p.agility * 0.3 + p.sense * 0.1 for p in players]
return random.choices(players, weights)[0]
SHOT_PROB: Dict[str, Callable[[Player], float]] = {
'2pull': lambda p: clamp(0.20 + stat(p.shoot) * 0.25 + stat(p.handle) * 0.05),
'3pull': lambda p: clamp(0.10 + stat(p.shoot) * 0.25 + stat(p.handle) * 0.20 + stat(p.agility) * 0.05),
'layup': lambda p: clamp(0.25 + stat(p.physical) * 0.08 + stat(p.agility) * 0.08 + stat(p.sense) * 0.04),
'dunk': lambda p: clamp(0.35 + stat(p.physical) * 0.10),
'stepback3': lambda p: clamp(0.10 + stat(p.shoot) * 0.28 + stat(p.handle) * 0.12 + stat(p.sense) * 0.05),
}
SHOT_POINTS = {'2pull': 2, 'layup': 2, 'dunk': 2, '3pull': 3, 'stepback3': 3}
PASS_TARGETS = {
'PG': [('SG', 0.36), ('C', 0.27), ('PF', 0.18), ('SF', 0.19)],
'SG': [('PG', 0.55), ('SF', 0.45)],
'SF': [('PG', 0.50), ('SG', 0.50)],
'PF': [('PG', 0.60), ('SF', 0.40)],
'C': [('PG', 0.65), ('SF', 0.35)]
}
POSITION_DECISION = {
'PG': [('pass', 0.50), ('2pull', 0.06), ('3pull', 0.15), ('stepback3', 0.04), ('layup', 0.07), ('pnr', 0.12), ('handoff', 0.06)],
'SG': [('pass', 0.50), ('2pull', 0.12), ('3pull', 0.25), ('stepback3', 0.08), ('layup', 0.05)],
'SF': [('pass', 0.45), ('2pull', 0.20), ('3pull', 0.20), ('layup', 0.15)],
'PF': [('pass', 0.55), ('post', 0.15), ('2pull', 0.15), ('3pull', 0.15)],
'C': [('pass', 0.55), ('post', 0.35), ('2pull', 0.10)]
}
def handle_success(team, opp, player, pts, lg, last_passer: Optional[Player] = None):
team.score += pts
player.points += pts
if last_passer and last_passer != player:
last_passer.assists += 1
lg.log(f"어시스트: {last_passer.name}")
lg.log(f"{player.name} 득점 +{pts}! → {team.name} {team.score} - {opp.score} {opp.name}")
def handle_miss(team, opp, lg, rebound_count=0):
off_prob = clamp(0.28 - rebound_count * 0.05)
if random.random() < off_prob:
rb = weighted_choice(team.players)
rb.off_reb += 1
lg.log(f"공격 리바운드 {rb.name}")
return True
rb = weighted_choice(opp.players)
rb.def_reb += 1
lg.log(f"수비 리바운드 {rb.name} → 턴 종료")
return False
def _record_shot(player: Player, pts: int, made: bool):
if pts == 2:
player.two_att += 1
if made:
player.two_made += 1
else:
player.three_att += 1
if made:
player.three_made += 1
def attempt_shot(team, opp, player, kind, lg, last_passer=None):
pts = SHOT_POINTS[kind]
prob = SHOT_PROB[kind](player)
made = random.random() < prob
_record_shot(player, pts, made)
if made:
lg.log(f"{player.name} {kind} 성공 ({prob:.2f})")
handle_success(team, opp, player, pts, lg, last_passer)
return False
lg.log(f"{player.name} {kind} 실패 ({prob:.2f})")
return handle_miss(team, opp, lg)
def attempt_shot_open3(team, opp, shooter, lg, last_passer):
pts = 3
base_prob = SHOT_PROB['3pull'](shooter) + 0.05
prob = clamp(base_prob)
made = random.random() < prob
_record_shot(shooter, pts, made)
if made:
lg.log(f"{shooter.name} 코너 오픈 3점 성공 ({prob:.2f})")
handle_success(team, opp, shooter, pts, lg, last_passer)
return False
lg.log(f"{shooter.name} 코너 오픈 3점 실패 ({prob:.2f})")
return handle_miss(team, opp, lg)
def attempt_post(team, opp, player, lg, last_passer=None):
prob = clamp(0.40 + stat(player.physical) * 0.15 + stat(player.sense) * 0.05)
lg.log(f"{player.name} 포스트업 ({prob:.2f})")
if random.random() < prob:
handle_success(team, opp, player, 2, lg, last_passer)
return False
if random.random() < 0.40:
targets = [p for p in team.players if p.pos in ('PG', 'SG', 'SF') and p != player]
shooter = random.choice(targets) if targets else player
lg.log("포스트 킥아웃!")
return attempt_shot_open3(team, opp, shooter, lg, last_passer=player)
lg.log("포스트업 실패")
return handle_miss(team, opp, lg)
def attempt_pass(team, opp, player, lg, depth=0, last_passer=None):
if depth >= 1:
lg.log("패스 제한 → 슛 선택")
return attempt_shot(team, opp, player, '2pull', lg, last_passer)
targets, wts = zip(*PASS_TARGETS.get(player.pos, [('PG', 1.0)]))
pos = random.choices(targets, wts)[0]
recv = team.get(pos)
prob = clamp(0.85 + stat(player.sense)*0.05 + stat(player.handle)*0.03 - depth*0.02)
lg.log(f"{player.name}→{recv.name} 패스 ({prob:.2f})")
if random.random() < prob:
return onball(team, opp, recv.pos, lg, depth+1, last_passer=player)
player.turnovers += 1
lg.log("패스 실패 → 턴오버")
return False
def attempt_handoff(team, opp, player, lg, last_passer=None):
receiver = team.get('SG')
lg.log(f"{player.name} 핸드오프 → {receiver.name}")
prob = clamp(0.78 + stat(player.sense)*0.05)
if random.random() < prob:
lg.log("핸드오프 성공 → 슛 기회")
return attempt_shot(team, opp, receiver, '2pull', lg, last_passer=player)
player.turnovers += 1
lg.log("핸드오프 실패 → 턴오버")
return False
def attempt_pnr(team, opp, player, lg, last_passer=None):
screener = team.get(random.choice(['PF', 'C']))
lg.log(f"PnR (스크리너 {screener.name}) 시도")
success = clamp(0.58 + stat(screener.sense)*0.05)
if random.random() < success:
if random.random() < 0.30:
return attempt_shot(team, opp, player, 'layup', lg, last_passer)
lg.log("롤맨에게 패스")
return attempt_shot(team, opp, screener, 'layup', lg, last_passer=player)
lg.log("PnR 실패 → 2pull")
return attempt_shot(team, opp, player, '2pull', lg, last_passer)
def onball(team, opp, pos, lg, depth=0, last_passer=None):
player = team.get(pos)
decision = POSITION_DECISION[pos]
action = random.choices([d[0] for d in decision], [d[1] for d in decision])[0]
if action == 'pass':
return attempt_pass(team, opp, player, lg, depth, last_passer)
if action in SHOT_PROB:
keep = attempt_shot(team, opp, player, action, lg, last_passer)
elif action == 'post':
keep = attempt_post(team, opp, player, lg, last_passer)
elif action == 'pnr':
keep = attempt_pnr(team, opp, player, lg, last_passer)
elif action == 'handoff':
keep = attempt_handoff(team, opp, player, lg, last_passer)
else:
keep = False
if keep:
return onball(team, opp, pos, lg, depth, last_passer)
return False
def simulate_game(team_a, team_b, quarters=4, poss_per_q=20):
lg = Logger(verbose=False)
for q in range(1, quarters + 1):
for _ in range(poss_per_q):
onball(team_a, team_b, random.choices(['PG', 'SG', 'SF', 'PF', 'C'], [0.28, 0.27, 0.25, 0.10, 0.10])[0], lg)
onball(team_b, team_a, random.choices(['PG', 'SG', 'SF', 'PF', 'C'], [0.28, 0.27, 0.25, 0.10, 0.10])[0], lg)
return lg
# ================== v9 랜덤 30팀 생성 유틸/리그 클래스 ==================
east_teams = [
Team("IVE",[
Player("가을", "PG", shoot=80, handle=87, agility=78, physical=75, sense=67),
Player("리즈", "SG", shoot=85, handle=78, agility=79, physical=76, sense=90),
Player("장원영", "SF", shoot=96, handle=76, agility=86, physical=90, sense=70),
Player("안유진", "PF", shoot=90, handle=85, agility=85, physical=92, sense=87),
Player("이서", "PG", shoot=83, handle=84, agility=90, physical=85, sense=77),
])
]
@dataclass
class League:
teams: List[Team]
games_per_matchup: int = 2
verbose: bool = False
standings: Dict[str, int] = field(default_factory=dict)
def _gen_schedule(self):
schedule = []
n = len(self.teams)
for i in range(n):
for j in range(i+1, n):
for _ in range(self.games_per_matchup):
schedule.append((i, j))
random.shuffle(schedule)
return schedule
def simulate_season(self):
self.standings = {t.name: 0 for t in self.teams}
schedule = self._gen_schedule()
for home_idx, away_idx in schedule:
home = self.teams[home_idx]
away = self.teams[away_idx]
for tm in (home, away):
tm.score = 0 # <-- **여기도 반드시 4칸 들여쓰기!**
# 선수 스탯 누적 유지!
log = simulate_game(home, away)
winner = home if home.score > away.score else away
self.standings[winner.name] += 1
if self.verbose:
print(f"{home.name} {home.score} - {away.score} {away.name} -> {winner.name} 승")
return self.standings
def create_random_league(num_teams: int = 30, games_per_matchup: int = 2, seed: int = 42):
random.seed(seed)
cities = random.sample(_CITIES, num_teams)
nicknames = random.sample(_NICKNAMES, num_teams)
teams = [generate_team(c, n) for c, n in zip(cities, nicknames)]
return League(teams, games_per_matchup=games_per_matchup, verbose=False)
# -------------------------------- Main : season sim + stat dump --------------------
nba = create_random_league()
standings = nba.simulate_season()
print("=== Final Standings (wins) ===")
for team, wins in sorted(standings.items(), key=lambda x: -x[1]):
print(f"{team}: {wins}")
num_teams = len(nba.teams)
games_per_team = (num_teams - 1) * nba.games_per_matchup
print(f"\n=== Team Standings with Wins, Win%, Avg Points ===")
for team in nba.teams:
team_total_points = sum(p.points for p in team.players)
wins = nba.standings[team.name]
win_pct = wins / games_per_team
avg_pts = team_total_points / games_per_team
print(f"{team.name:24s} W:{wins:3d} Win%:{win_pct:.3f} AvgPts:{avg_pts:.2f}")
print("\n=== Top 10 Scorers (PPG) ===")
all_players = []
for team in nba.teams:
for p in team.players:
ppg = p.points / games_per_team
all_players.append((ppg, p.name, team.name, p.assists / games_per_team, (p.off_reb + p.def_reb) / games_per_team))
for ppg, name, tname, apg, rpg in sorted(all_players, reverse=True)[:10]:
print(f"{name} [{tname}] PPG:{ppg:.2f} APG:{apg:.2f} RPG:{rpg:.2f}")
print("\n=== Player Per-Game Stats ===")
for team in nba.teams:
print(f"[{team.name}]")
for p in team.players:
ppg = p.points / games_per_team
apg = p.assists / games_per_team
rpg = (p.off_reb + p.def_reb) / games_per_team
tpg = p.turnovers / games_per_team
two_pct = (p.two_made / p.two_att * 100) if p.two_att else 0
three_pct = (p.three_made / p.three_att * 100) if p.three_att else 0
print(f"{p.name:18s} PPG:{ppg:.2f} APG:{apg:.2f} RPG:{rpg:.2f} TPG:{tpg:.2f} "
f"2P%:{two_pct:.1f} 3P%:{three_pct:.1f}")
To embed this project on your website, copy the following code and paste it into your website's HTML: