# ===================== IDL‑Basketball Simulation v7 (Rebound Weights · 3P Boost · Post Kick‑out · Lay/Dunk Nerf) =====================
import random
from dataclasses import dataclass
from typing import List, Dict, Callable, Optional

# ---------- Utility ----------

def clamp(p: float) -> float:
    return max(0.05, min(p, 0.95))

def stat(v: int) -> float:
    return v / 100.0

# weighted choice for rebounders

POSITION_REB_WEIGHT = {
    'C': 1.25,
    'PF': 1.10,
    'SF': 1.0,
    'SG': 0.9,
    'PG': 0.75
}

def weighted_rebound_choice(players):
    weights = []
    for p in players:
        base = (p.physical * 0.6 + p.agility * 0.3 + p.sense * 0.1)
        pos_weight = POSITION_REB_WEIGHT.get(p.pos, 1.0)
        weights.append(base * pos_weight)
    return random.choices(players, weights)[0]

# ---------- Data Models ----------

@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
    pass_boost: float = 0.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)

# ---------- Logger ----------

class Logger:
    def __init__(self):
        self.lines: List[str] = []

    def log(self, text: str):
        self.lines.append(text)

# ---------- Probability Maps ----------

SHOT_PROB: Dict[str, Callable[[Player], float]] = {
    '2pull': lambda p: clamp(0.10 + stat(p.shoot) * 0.20 + stat(p.handle) * 0.05),
    '3pull': lambda p: clamp(0.08 + stat(p.shoot) * 0.20 + stat(p.handle) * 0.04),
    '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.22 + 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.57), ('2pull', 0.08), ('3pull', 0.06), ('stepback3', 0.02), ('layup', 0.07), ('pnr', 0.12), ('handoff', 0.08)],
    'SG': [('pass', 0.58), ('2pull', 0.10), ('3pull', 0.18), ('stepback3', 0.06), ('layup', 0.08)],
    'SF': [('pass', 0.45), ('2pull', 0.20), ('3pull', 0.20), ('layup', 0.15)],
    'PF': [('pass', 0.20), ('post', 0.45), ('2pull', 0.25), ('3pull', 0.10)],
    'C':  [('pass', 0.20), ('post', 0.55), ('2pull', 0.25)]
}

# ---------- Core Handlers ----------

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.18 - rebound_count * 0.06)
    if rebound_count >= 2:
        lg.log("공격 기회 소멸 (24초 바이올레이션)")
        return False
        
    if random.random() < off_prob:
        rb = weighted_rebound_choice(team.players)
        rb.off_reb += 1
        lg.log(f"공격 리바운드 {rb.name}")
        return True
    rb = weighted_rebound_choice(opp.players)
    rb.def_reb += 1
    lg.log(f"수비 리바운드 {rb.name} → 턴 종료")
    return False


# ---------- Shooting Helpers ----------

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)
    if player.pass_boost > 0:
        prob += player.pass_boost
        prob = clamp(prob)
        player.pass_boost = 0.0 # 한 번만 적용
    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):
    # 오픈 찬스 3점 – 성공률 +5%
    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)


# ---------- Actions ----------

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
    # 실패 – 30% 확률 킥아웃 3점 찬스
    if random.random() < 0.30:
        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):
    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:
        recv.pass_boost = 0.03 + stat(player.sense) * 0.015 # 기본 5% + 패서 센스 비중
        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(random.choice(['SG', 'SF', 'PF']))
    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)

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

# ---------- Simulation ----------
def simulate_game(team_a, team_b, quarters=4, poss_per_q=25):
    lg = Logger()
    for q in range(1, quarters + 1):
        lg.log(f"\n=== {q}쿼터 ===")
        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)
        lg.log(f"{q}Q 종료: {team_a.name} {team_a.score} - {team_b.score} {team_b.name}")
    lg.log(f"\n=== 최종 스코어: {team_a.name} {team_a.score} - {team_b.score} {team_b.name} ===")
    return lg

# …위쪽은 그대로 두시고…

# 팀 풀 정의

TEAM_POOL = {
    'IVE': Team('IVE', [
        Player('리즈', 'PG', 81, 88, 84, 72, 89), Player('가을', 'SG', 77, 74, 81, 66, 70),
        Player('장원영', 'SF', 94, 86, 87, 89, 84), Player('이서', 'PF', 74, 68, 77, 83, 72),
        Player('안유진', 'C', 75, 70, 78, 92, 85)
    ]),
    'aespa' : Team('aespa', [
        Player('윈터', 'PG', 81, 86, 82, 66, 83), Player('닝닝', 'SG', 86, 78, 79, 76, 80),
        Player('카리나', 'SF', 75, 76, 78, 90, 77), Player('지젤', 'PF', 65, 66, 70, 85, 72),
        Player('Bogut', 'C', 70, 70, 70, 70, 70)
    ])
}

# ─── Main 함수 정의 ──────────────────
def main():
    IVE = TEAM_POOL['IVE']
    aespa = TEAM_POOL['aespa']
    
    log = simulate_game(IVE, aespa)
    for line in log.lines:
        print(line)

    print("\n=== 선수 기록 ===")
    for tm in (IVE, aespa):
        print(tm.name)
        for p in tm.players:
            print(f"{p.name}({p.pos}) PTS:{p.points} AST:{p.assists} 2P {p.two_made}/{p.two_att} 3P {p.three_made}/{p.three_att} ORB:{p.off_reb} DRB:{p.def_reb} TO:{p.turnovers}")

# ─── myCompiler 에서는 guard 없이 바로 호출 ──────────────────
main()

Embed on website

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