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