# ===================== IDL-Basketball Full Simulation (2025-08-03) =====================
import random
from dataclasses import dataclass
from typing import List, Callable

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

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

# ─── Entities ─────────────────────────────────────────────────────────────────
@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
    two_att: int = 0        # 2점 시도
    two_made: int = 0       # 2점 성공
    three_att: int = 0      # 3점 시도
    three_made: int = 0     # 3점 성공
    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
        # 포지션 오타 시 무작위 반환 (디버그 메시지 추가해도 OK)
        return random.choice(self.players)

# ─── Core Handlers ────────────────────────────────────────────────────────────
def handle_success(team: Team, pts: int, log: List[str], opp: Team, scorer: Player = None):
    team.score += pts
    if scorer:
        scorer.points += pts
    log.append(f"득점 +{pts}! 현재 스코어: {team.name} {team.score} - {opp.score} {opp.name}")

def handle_failure(team: Team, opp: Team, log: List[str],
                   rebound_count: int = 1,
                   turnover: bool = False,
                   turnover_player: Player | None = None):
    if turnover and turnover_player:
        turnover_player.turnovers += 1
        log.append("턴오버 발생! 속공 기회!")
        try_fastbreak(turnover_player, opp, team, log)   # opp = 수비팀
        return

    # 리바운드 or 턴 종료
    off_prob = 0.30
    if random.random() < off_prob:
        log.append("공격 리바운드!")
        rebound(team, opp, log, rebound_count)
    else:
        log.append("수비 리바운드 → 턴 종료")

# ─── Shot / Pass 확률 함수 ─────────────────────────────────────────────────────
def pullup_2pt_prob(p: Player) -> float:
    return clamp(0.20 + stat(p.shoot)*0.25 + stat(p.handle)*0.05 + stat(p.agility)*0.02)

def pullup_3pt_prob(p: Player) -> float:
    return clamp(0.15 + stat(p.shoot)*0.30 + stat(p.handle)*0.05 + stat(p.agility)*0.02)

def layup_prob(p: Player) -> float:
    return clamp(0.30 + stat(p.physical)*0.10 + stat(p.agility)*0.10 + stat(p.sense)*0.05)

def dunk_prob(p: Player) -> float:
    return clamp(0.45 + stat(p.physical)*0.15 + stat(p.agility)*0.05)

def catch_and_shoot_prob(p: Player, pass_count: int = 0) -> float:
    base = 0.20 + stat(p.shoot)*0.30 + stat(p.agility)*0.02
    base += min(pass_count * 0.02, 0.06)
    return clamp(base)

def pass_prob(p: Player, pass_count: int = 0) -> float:
    base = 0.90  # 기본 성공률 ↑
    base += stat(p.sense) * 0.05  # 센스 강화
    base -= min(pass_count * 0.01, 0.03)  # 오픈 찬스 영향 ↓
    return clamp(base)

def stepback_3pt_prob(p: Player) -> float:
    return clamp(0.10 + stat(p.shoot)*0.28 + stat(p.handle)*0.12 + stat(p.sense)*0.05)

def cs_prob(pos: str) -> float:
    if pos == 'SG': return 0.50
    if pos == 'PG': return 0.35
    if pos == 'SF': return 0.30
    return 0.10  # PF, C는 거의 없음

def cs_points(pos: str) -> int:
    if pos in ('PG','SG'):
        return 3
    if pos == 'SF':
        return 3 if random.random() < 0.25 else 2
    return 2

# ─── Generic Actions ──────────────────────────────────────────────────────────
def action_pullup(team, opp, player, pts, prob_func, log, rebound_count):
    # 시도 기록
    if pts == 2:
        player.two_att += 1
    else:
        player.three_att += 1

    if random.random() < prob_func(player):
        # 성공 기록
        if pts == 2:
            player.two_made += 1
        else:
            player.three_made += 1

        log.append(f"{player.name} Pull-up 성공! (+{pts}점)")
        handle_success(team, pts, log, opp, scorer=player)
    else:
        log.append(f"{player.name} Pull-up 실패 → 리바운드")
        handle_failure(team, opp, log, rebound_count)

def action_drive_layup(team: Team, opp: Team, player: Player,
                       log: List[str], rebound_count: int):
    drive_prob = clamp(0.45 + stat(player.handle)*0.20
                              + stat(player.physical)*0.20
                              + stat(player.agility)*0.10)
    if random.random() < drive_prob:
        if random.random() < dunk_prob(player):
            log.append(f"{player.name} 드라이브 덩크 성공! (+2점)")
            handle_success(team, 2, log, opp, scorer=player)
        elif random.random() < layup_prob(player):
            log.append(f"{player.name} 드라이브 레이업 성공! (+2점)")
            handle_success(team, 2, log, opp, scorer=player)
        else:
            log.append(f"{player.name} 드라이브 마무리 실패 → 리바운드")
            handle_failure(team, opp, log, rebound_count)
    else:
        log.append(f"{player.name} 드라이브 실패 → 리바운드")
        handle_failure(team, opp, log, rebound_count)

def action_post_up(team: Team, opp: Team, player: Player,
                   log: List[str], rebound_count: int):
    if random.random() < clamp(0.40 + stat(player.physical)*0.15 + stat(player.sense)*0.05):
        log.append("Post-up 성공! (+2점)")
        handle_success(team, 2, log, opp, scorer=player)
    else:
        log.append("Post-up 실패 → 리바운드")
        handle_failure(team, opp, log, rebound_count)

def action_stepback_3pt(team: Team, opp: Team, player: Player,
                        log: List[str], rebound_count: int):
    if random.random() < stepback_3pt_prob(player):
        player.three_att += 1
        player.three_made += 1
        log.append(f"{player.name} 스텝백 3점 성공! (+3점)")
        handle_success(team, 3, log, opp, scorer=player)
    else:
        player.three_att += 1
        log.append(f"{player.name} 스텝백 3점 실패 → 리바운드")
        handle_failure(team, opp, log, rebound_count)

# ─── Fastbreak ────────────────────────────────────────────────────────────────
def try_fastbreak(turnover_player: Player, defense_team: Team, opponent_team: Team, log: List[str]):
    passer  = defense_team.get(turnover_player.pos)
    runner  = random.choice([p for p in defense_team.players if p != passer])
    prob = clamp(0.60 + stat(passer.sense)*0.10
                       + stat(runner.agility)*0.15
                       - stat(turnover_player.sense)*0.05)

    log.append(f"[Fastbreak] {passer.name} → {runner.name} (확률 {round(prob*100,1)}%)")
    if random.random() < prob:
        if random.random() < dunk_prob(runner):
            log.append(f"{runner.name} 속공 덩크 성공! (+2점)")
            handle_success(defense_team, 2, log, opponent_team, scorer=runner)
        elif random.random() < layup_prob(runner):
            log.append(f"{runner.name} 속공 레이업 성공! (+2점)")
            handle_success(defense_team, 2, log, opponent_team, scorer=runner)
        else:
            log.append(f"{runner.name} 속공 마무리 실패 → 수비 리바운드 (턴 종료)")
    else:
        log.append("속공 실패 → 수비 리바운드 (턴 종료)")

# ─── Additional Probabilities ────────────────────────────────────────────────
def screen_prob(p: Player)    -> float: return clamp(0.55 + stat(p.physical)*0.10 + stat(p.sense)*0.05)
def roll_pass_prob(p: Player) -> float: return clamp(0.65 + stat(p.handle)*0.05 + stat(p.sense)*0.05)

# ─── Drive & Kick / PnR ───────────────────────────────────────────────────────
def action_drive_kick(team: Team, opp: Team, player: Player,
                      log: List[str], rebound_count: int):
    drive_prob = clamp(0.25 + stat(player.handle)*0.20
                               + stat(player.agility)*0.15
                               + stat(player.sense)*0.05)
    if random.random() < drive_prob:
        sg = team.get('SG')
        if random.random() < roll_pass_prob(player):
            pts = 3 if sg.pos in ('PG','SG','SF') else 2
            action_pullup(team, opp, sg, pts,
                          lambda x: catch_and_shoot_prob(x, rebound_count+1),
                          log, rebound_count)
        else:
            log.append(f"{player.name} 킥아웃 패스 실패 → 턴오버")
            handle_failure(team, opp, log, rebound_count,
                           turnover=True, turnover_player=player)
    else:
        log.append(f"{player.name} 드라이브 실패 → 리바운드")
        handle_failure(team, opp, log, rebound_count)

def action_pnr(team: Team, opp: Team, player: Player,
               log: List[str], rebound_count: int):
    screener = team.get(random.choices(['PF','C'], [0.6,0.4])[0])
    if random.random() < screen_prob(screener):
        if random.random() < 0.40:
            action_drive_layup(team, opp, player, log, rebound_count)
        else:
            if random.random() < roll_pass_prob(player):
                if random.random() < dunk_prob(screener):
                    log.append(f"PnR 롤맨({screener.name}) 덩크 성공! (+2점)")
                    handle_success(team, 2, log, opp, scorer=screener)
                elif random.random() < layup_prob(screener):
                    log.append(f"PnR 롤맨({screener.name}) 레이업 성공! (+2점)")
                    handle_success(team, 2, log, opp, scorer=screener)
                else:
                    log.append(f"PnR 롤맨({screener.name}) 마무리 실패 → 리바운드")
                    handle_failure(team, opp, log, rebound_count)
            else:
                log.append("PnR 롤맨 패스 실패 → 턴오버")
                handle_failure(team, opp, log, rebound_count,
                               turnover=True, turnover_player=player)
    else:
        log.append("스크린 실패 → PG 드라이브")
        action_drive_layup(team, opp, player, log, rebound_count)

def action_pnp(team: Team, opp: Team, player: Player,
               log: List[str], rebound_count: int):
    screener = team.get(random.choices(['PF','C'], [0.6,0.4])[0])
    if random.random() < screen_prob(screener):  # 스크린 성공 확률
        if random.random() < roll_pass_prob(player):  # 패스 성공 확률
            pts = 3 if screener.pos == 'PF' else 2  # PF는 3점, C는 미드레인지 2점
            log.append(f"PnP 팝맨({screener.name}) 캐치 앤 슛 시도! (+{pts}점)")
            if random.random() < catch_and_shoot_prob(screener):  # 슛 성공 여부
                handle_success(team, pts, log, opp, scorer=screener)
            else:
                log.append(f"PnP 팝맨({screener.name}) 슛 실패 → 리바운드")
                handle_failure(team, opp, log, rebound_count)
        else:
            log.append("PnP 패스 실패 → 턴오버")
            handle_failure(team, opp, log, rebound_count,
                           turnover=True, turnover_player=player)
    else:
        log.append("스크린 실패 → PG 드라이브")
        action_drive_layup(team, opp, player, log, rebound_count)

# ─── Passing Action (정상 버전) ───────────────────────────────────────────────
def action_pass(team: Team, opp: Team, player: Player,
                log: List[str], pass_count: int, rebound_count: int):
    log.append(f"{player.name} 패스 시도 (카운트 {pass_count+1})")
    if pass_count > 0:
        log.append("▶ 오픈 찬스!")

    tgt_weights = {
        'PG': [('SG',0.36), ('C',0.27), ('PF',0.18), ('SF',0.19)],
        'SG': [('PG',0.60), ('SF',0.40)],
        'SF': [('PG',0.63), ('SG',0.37)],
        'PF': [('PG',0.67), ('SF',0.33)],
        'C' : [('PG',0.70), ('SF',0.30)]
    }
    choices, wts = zip(*tgt_weights.get(player.pos, [('PG',1.0)]))
    target_pos = random.choices(choices, wts)[0]

    receiver = team.get(target_pos)
    if receiver == player:
        receiver = random.choice([p for p in team.players if p != player])
    log.append(f"{player.name} → {receiver.name} ({receiver.pos})")
    if random.random() < pass_prob(player, pass_count):
        log.append(f"{receiver.name} 온볼로 결정")
        onball(team, opp, receiver.pos, log, pass_count+1, rebound_count)
    else:
        log.append("패스 실패 → 턴오버")
        handle_failure(team, opp, log, rebound_count,
                       turnover=True, turnover_player=player)

# ─── Rebound Logic ───────────────────────────────────────────────────────────
def select_rebounder(team: Team):
    weights = [(p.physical*0.6 + p.agility*0.2 + p.sense*0.2) + random.uniform(0,10)
               for p in team.players]
    return team.players[weights.index(max(weights))]

def offensive_rebound_prob(team: Team, rebound_count: int):
    if   rebound_count == 0: base = 0.50
    elif rebound_count == 1: base = 0.40
    elif rebound_count == 2: base = 0.30
    else:                    base = 0.12   # 3회째부터 급감
    return clamp(base)

def rebound(team: Team, opp: Team, log: List[str], rebound_count: int):
    if rebound_count >= 5:         # 안전장치: 무한 재귀 방지
        log.append("리바운드 난타전 끝 → 수비 리바운드")
        return
    if random.random() < offensive_rebound_prob(team, rebound_count):
        r = select_rebounder(team)
        r.off_reb += 1  
        log.append(f"공격 리바운드 by {r.name}")
        if random.random() < dunk_prob(r):
            log.append("Putback 덩크 성공! (+2점)")
            handle_success(team, 2, log, opp, scorer=r)
        else:
            log.append("Putback 실패 → 재리바운드")
            rebound(team, opp, log, rebound_count+1)
    else:
        # 수비 리바운드도 개별 기록
        d = select_rebounder(opp)
        d.def_reb += 1        # ← 수비 리바운드 기록
        log.append(f"수비 리바운드 by {d.name} → 턴 종료")

# ─── Position-specific On-ball ───────────────────────────────────────────────
def onball_sg(team, opp, log, pass_count, rebound_count):
    player = team.get('SG')

    if random.random() < 0.50:
        action_pass(team, opp, player, log, pass_count, rebound_count)
    else:
        log.append(f"{player.name} ISO 선택 (SG)")
        iso_choice = random.choices(
            ['pull2','pull3','stepback','drive_kick','drive_layup','pnr'],
            [0.25, 0.18, 0.09, 0.18, 0.20, 0.10]
        )[0]

        if   iso_choice == 'pull2':
            action_pullup(team, opp, player, 2, pullup_2pt_prob, log, rebound_count)
        elif iso_choice == 'pull3':
            action_pullup(team, opp, player, 3, pullup_3pt_prob, log, rebound_count)
        elif iso_choice == 'stepback':
            action_stepback_3pt(team, opp, player, log, rebound_count)
        elif iso_choice == 'drive_kick':
            action_drive_kick(team, opp, player, log, rebound_count)
        elif iso_choice == 'drive_layup':
            action_drive_layup(team, opp, player, log, rebound_count)
        else:  # 'pnr'
            action_pnr(team, opp, player, log, rebound_count)

def onball_sf(team, opp, log, pass_count, rebound_count):
    if random.random() < 0.40:
        action_pass(team, opp, team.get('SF'), log, pass_count, rebound_count)
    else:
        r = random.random()
        player = team.get('SF')
        if   r < 0.25: action_pullup(team, opp, player, 2, pullup_2pt_prob, log, rebound_count)
        elif r < 0.45: action_pullup(team, opp, player, 3, pullup_3pt_prob, log, rebound_count)
        elif r < 0.80: action_drive_layup(team, opp, player, log, rebound_count)
        else:          action_post_up(team, opp, player, log, rebound_count)

def onball_pf(team, opp, log, pass_count, rebound_count):
    if random.random() < 0.60:
        action_pass(team, opp, team.get('PF'), log, pass_count, rebound_count)
    else:
        r = random.random()
        player = team.get('PF')
        if   r < 0.65: action_post_up(team, opp, player, log, rebound_count)
        else:          action_pullup(team, opp, player, 2,
                                     lambda p: clamp(0.25 + stat(p.shoot)*0.30 + stat(p.agility)*0.02),
                                     log, rebound_count)

def onball_c(team, opp, log, pass_count, rebound_count):
    if random.random() < 0.50:
        action_pass(team, opp, team.get('C'), log, pass_count, rebound_count)
    else:
        action_post_up(team, opp, team.get('C'), log, rebound_count)

def onball_pg(team, opp, log, pass_count, rebound_count):
    player = team.get('PG')
    # 첫 번째 액션이면 패스 88%, 이후엔 낮춤
    pass_prob_pg = 0.75 if pass_count == 0 else 0.40
    if random.random() < pass_prob_pg:  # 여기서 적용해야 함   # 88% 패스
        log.append(f"{player.name} 패스 선택")
        targets  = ['SG','C','PF','SF']
        weights  = [0.25,0.30,0.25,0.20]
        target_pos = random.choices(targets, weights)[0]
        receiver   = team.get(target_pos)
        log.append(f"{player.name} → {receiver.name} ({target_pos})")

        if random.random() < pass_prob(player, pass_count):
            log.append(f"패스 성공! {receiver.name} 온볼로 결정")
            onball(team, opp, target_pos, log, pass_count+1, rebound_count)
        else:
            log.append("패스 실패 → 턴오버")
            handle_failure(team, opp, log, rebound_count,
                           turnover=True, turnover_player=player)
    else:                           # 12% 아이솔
        log.append(f"{player.name} 아이솔레이션 선택")
        iso_choice = random.choices(
            ['pull2','pull3','stepback','drive_kick','drive_layup','pnr','pnp'],
            [0.15,0.20,0.12,0.15,0.15,0.10,0.13]
        )[0]
        if   iso_choice == 'pull2':       action_pullup(team, opp, player, 2, pullup_2pt_prob, log, rebound_count)
        elif iso_choice == 'pull3':       action_pullup(team, opp, player, 3, pullup_3pt_prob, log, rebound_count)
        elif iso_choice == 'drive_kick':  action_drive_kick(team, opp, player, log, rebound_count)
        elif iso_choice == 'drive_layup': action_drive_layup(team, opp, player, log, rebound_count)
        elif iso_choice == 'stepback':    action_stepback_3pt(team, opp, player, log, rebound_count)
        elif iso_choice == 'pnp':         action_pnp(team, opp, player, log, rebound_count)
        else:                             action_pnr(team, opp, player, log, rebound_count)

# ─── Dispatcher ──────────────────────────────────────────────────────────────
def onball(team: Team, opp: Team, pos: str,
           log: List[str], pass_count: int = 0, rebound_count: int = 1):
    funcs = {
        'PG': onball_pg,
        'SG': onball_sg,
        'SF': onball_sf,
        'PF': onball_pf,
        'C' : onball_c
    }
    funcs.get(pos, onball_pg)(team, opp, log, pass_count, rebound_count)

# ─── Sample Teams ────────────────────────────────────────────────────────────
cavs = Team("Cavs", [
    Player("Kyrie","PG"), Player("JR","SG"),
    Player("LeBron","SF"), Player("Love","PF"),
    Player("Thompson","C")
])
warriors = Team("Warriors", [
    Player("Curry","PG"), Player("Klay","SG"),
    Player("Barnes","SF"), Player("Green","PF"),
    Player("Bogut","C")
])

# ─── Simulation Parameters ───────────────────────────────────────────────────
quarters = 4
possessions_per_q = 20   # per team

# ─── Reset scores ────────────────────────────────────────────────────────────
for t in (cavs, warriors):
    t.score = 0
    for pl in t.players: pl.points = 0

game_logs: List[str] = []

# ─── Run Simulation ──────────────────────────────────────────────────────────
positions = ['PG','SG','SF','PF','C']
starter_weights = [0.35,0.25,0.20,0.10,0.10]

for q in range(1, quarters+1):
    game_logs.append(f"\n=== {q}쿼터 시작 ===")
    start_scores = (cavs.score, warriors.score)

    for p in range(1, possessions_per_q+1):
        cav_log: List[str] = []
        starter_pos = random.choices(positions, starter_weights)[0]
        onball(cavs, warriors, starter_pos, cav_log)
        game_logs.append(f"[Cavs 포제션 #{p}]"); game_logs.extend(cav_log)

        war_log: List[str] = []
        starter_pos = random.choices(positions, starter_weights)[0]
        onball(warriors, cavs, starter_pos, war_log)
        game_logs.append(f"[Warriors 포제션 #{p}]"); game_logs.extend(war_log)

    c_delta, w_delta = cavs.score-start_scores[0], warriors.score-start_scores[1]
    print(f"{q}쿼터 결과: Cavs +{c_delta} / Warriors +{w_delta}")
    print(f"누적 스코어: Cavs {cavs.score} - Warriors {warriors.score}")

print(f"\n=== 최종 스코어: Cavs {cavs.score} - Warriors {warriors.score} ===\n")

# ─── Player Stats ───────────────────────────────────────────────────────────
# 5) 최종 출력부: 모든 스탯 보기
print("=== 선수 기록 ===")
for tm in (cavs, warriors):
    print(f"{tm.name}")
    for p in tm.players:
        print(
            f"{p.name} ({p.pos}) – "
            f"PTS:{p.points}  "
            f"2P {p.two_made}/{p.two_att}  "
            f"3P {p.three_made}/{p.three_att}  "
            f"ORB:{p.off_reb}  DRB:{p.def_reb}  "
            f"TO:{p.turnovers}"
        )
# --- PATCH END --------------------------------------------------------------

# ─── Full Log ────────────────────────────────────────────────────────────────
print("=== 경기 로그 ===")
for line in game_logs:
    print(line)

Embed on website

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