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}")

Embed on website

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