import random
from collections import defaultdict, Counter
import csv

# =========================
# 기본 설정
# =========================

TEAMS = [
    "두산 베어스", "한화 이글스", "KIA 타이거즈", "키움 히어로즈", "KT 위즈",
    "LG 트윈스", "롯데 자이언츠", "NC 다이노스", "삼성 라이온즈", "SSG 랜더스",
]

ABBR = {
    "두산 베어스": "두산",
    "한화 이글스": "한화",
    "KIA 타이거즈": "KIA",
    "키움 히어로즈": "키움",
    "KT 위즈": "KT",
    "LG 트윈스": "LG",
    "롯데 자이언츠": "롯데",
    "NC 다이노스": "NC",
    "삼성 라이온즈": "삼성",
    "SSG 랜더스": "SSG",
}

N = len(TEAMS)
TEAM_IDX = {t: i for i, t in enumerate(TEAMS)}
IDX_TEAM = {i: t for t, i in TEAM_IDX.items()}


# =========================
# 1. 원형 방식으로 기본 완전매칭 9개 생성
# =========================

def circle_method_pairings(n: int):
    arr = list(range(n))
    rounds = []
    for _ in range(n - 1):
        pairs = []
        for i in range(n // 2):
            a = arr[i]
            b = arr[-(i + 1)]
            pairs.append(tuple(sorted((a, b))))
        rounds.append(sorted(pairs))
        # 0번 고정 나머지 회전
        arr = [arr[0]] + [arr[-1]] + arr[1:-1]
    return rounds  # 9개의 "리그 전체 매치업 세트"


def apply_perm_to_round(round_pairs, perm):
    return sorted([tuple(sorted((perm[a], perm[b]))) for a, b in round_pairs])


def apply_perm_to_rounds(rounds, perm):
    return [apply_perm_to_round(r, perm) for r in rounds]


# =========================
# 2. 중복 없는 완전매칭 풀 생성
# =========================

def generate_unique_cycles(base_rounds, num_cycles, avoid=None, attempts=50000):
    """
    base_rounds: circle_method_pairings 결과 (9개 완전매칭)
    num_cycles:  몇 번 '라운드 전체'를 만들지 (4 → 36개 등)
    avoid:       이미 사용된 완전매칭(tuple) 집합
    """
    if avoid is None:
        avoid = set()

    out = []
    seen = set(avoid)
    need = num_cycles * len(base_rounds)
    tries = 0

    while len(out) < need and tries < attempts:
        tries += 1
        perm_arr = list(range(N))
        random.shuffle(perm_arr)
        perm = {i: perm_arr[i] for i in range(N)}
        rounds = apply_perm_to_rounds(base_rounds, perm)
        random.shuffle(rounds)

        # 완전매칭 중복 체크
        if any(tuple(r) in seen for r in rounds):
            continue

        out.extend(rounds)
        for r in rounds:
            seen.add(tuple(r))

    if len(out) != need:
        raise RuntimeError(
            f"유일한 완전매칭 수집 실패: 필요 {need}, 확보 {len(out)}"
        )
    return out


# =========================
# 3. 클러스터 방지 + 연속 같은 상대 금지 스케줄러
# =========================

def schedule_with_min_gap_fixed(pool, min_gap, prev_last_opp=None, max_restarts=800):
    """
    pool: 완전매칭 리스트 (각 원소 = [(a,b), ...] 길이 5)
    min_gap: 같은 맞대결 간 최소 간격 (슬롯 단위)
    prev_last_opp: 앞 블록(3G->2G 경계)에서 마지막 상대 정보 (dict)
    """
    base = [list(m) for m in pool]
    gap = min_gap
    restarts = 0

    while True:
        remaining = base[:]
        random.shuffle(remaining)
        res = []
        cooldown = defaultdict(int)  # pair -> 남은 쿨다운 슬롯 수
        last_opp = {i: None for i in range(N)} if prev_last_opp is None else prev_last_opp.copy()
        success = True

        while remaining:
            feasible = []
            for r in remaining:
                ok = True
                # 1) 같은 맞대결 쿨다운 체크
                for a, b in r:
                    key = (a, b) if a < b else (b, a)
                    if cooldown[key] > 0:
                        ok = False
                        break
                if not ok:
                    continue
                # 2) 직전 슬롯에서 같은 상대와 연속 경기 금지
                for a, b in r:
                    if last_opp[a] == b or last_opp[b] == a:
                        ok = False
                        break
                if ok:
                    feasible.append(r)

            if not feasible:
                success = False
                break

            r = random.choice(feasible)
            res.append(r)

            # 쿨다운 감소
            for k in list(cooldown.keys()):
                if cooldown[k] > 0:
                    cooldown[k] -= 1

            # 이번 라운드 쌍들에 새 쿨다운 부여
            for a, b in r:
                key = (a, b) if a < b else (b, a)
                cooldown[key] = gap

            # 마지막 상대 업데이트
            for a, b in r:
                last_opp[a] = b
                last_opp[b] = a

            remaining.remove(r)

        if success:
            return res, last_opp

        # 실패 → 리스타트 & gap 살짝 완화
        restarts += 1
        if restarts > max_restarts:
            gap = max(1, gap - 1)
            restarts = 0


# =========================
# 4. 시즌 스케줄 생성
# =========================

def generate_season_2034(seed=2034_777):
    random.seed(seed)

    base_rounds = circle_method_pairings(N)

    # 3연전용 36 슬롯, 2연전용 18 슬롯 (완전매칭 중복 없음)
    three_pool = generate_unique_cycles(base_rounds, 4, avoid=set())
    two_pool   = generate_unique_cycles(base_rounds, 2, avoid=set(map(tuple, three_pool)))

    # 클러스터 방지: 3G = min_gap 6, 2G = min_gap 4
    three_slots, last_after_3 = schedule_with_min_gap_fixed(three_pool, 6, prev_last_opp=None)
    two_slots,  _             = schedule_with_min_gap_fixed(two_pool,   4, prev_last_opp=last_after_3)

    slot_types = ["3G"] * len(three_slots) + ["2G"] * len(two_slots)
    series_len = [3] * len(three_slots)    + [2] * len(two_slots)
    all_slots  = three_slots + two_slots

    # 홈/원정 8/8을 위해 시리즈 단위 쿼터 관리
    pair_q3 = {}
    pair_q2 = {}
    for a in range(N):
        for b in range(a + 1, N):
            pair_q3[(a, b)] = {a: 2, b: 2}  # 3연전 시리즈 4번 → 각 2번씩 홈
            pair_q2[(a, b)] = {a: 1, b: 1}  # 2연전 시리즈 2번 → 각 1번씩 홈

    series_by_slot = defaultdict(list)  # slot_idx -> [(home_idx, away_idx), ...]
    series_rows = []

    for slot_idx, (pairs, ptype, slen) in enumerate(zip(all_slots, slot_types, series_len), start=1):
        for a, b in pairs:
            key = (min(a, b), max(a, b))
            if ptype == "3G":
                if pair_q3[key][a] > pair_q3[key][b]:
                    host = a
                elif pair_q3[key][b] > pair_q3[key][a]:
                    host = b
                else:
                    host = random.choice([a, b])
                pair_q3[key][host] -= 1
            else:
                if pair_q2[key][a] > pair_q2[key][b]:
                    host = a
                elif pair_q2[key][b] > pair_q2[key][a]:
                    host = b
                else:
                    host = random.choice([a, b])
                pair_q2[key][host] -= 1

            away = b if host == a else a
            series_by_slot[slot_idx].append((host, away))
            series_rows.append({
                "slot": slot_idx,
                "ptype": ptype,
                "length": slen,
                "home_idx": host,
                "away_idx": away,
                "home": IDX_TEAM[host],
                "away": IDX_TEAM[away],
            })

    # 팀별 홈/원정 검증 (총 72/72)
    home_tot = Counter()
    away_tot = Counter()
    for s in series_rows:
        slen = s["length"]
        home_tot[s["home"]] += slen
        away_tot[s["away"]] += slen
    for t in TEAMS:
        if not (home_tot[t] == 72 and away_tot[t] == 72):
            raise RuntimeError(f"{t} 홈/원정 합 72/72 아님: home={home_tot[t]}, away={away_tot[t]}")

    # 시리즈 → 일(day) 단위로 확장
    days = []  # days[d] = [(home_idx, away_idx), ...]  하루 5경기
    for slot_idx, (pairs, ptype, slen) in enumerate(zip(all_slots, slot_types, series_len), start=1):
        games = series_by_slot[slot_idx]  # 이 슬롯의 5매치업
        for _ in range(slen):  # 3연전이면 3일, 2연전이면 2일
            days.append(list(games))

    # 3연전 블록: 36 슬롯 * 3일 = 108일 → 6일 × 18주
    # 2연전 블록: 18 슬롯 * 2일 = 36일 → 4일 × 9주
    days_3g = days[:36 * 3]
    days_2g = days[36 * 3:]

    weeks = []  # 27주

    # WEEK 1~18 (3G, 6일)
    for w in range(18):
        week_days = days_3g[w * 6:(w + 1) * 6]
        weeks.append({
            "week": w + 1,
            "type": "3G",
            "days": week_days,
        })

    # WEEK 19~27 (2G, 4일)
    for j in range(9):
        week_days = days_2g[j * 4:(j + 1) * 4]
        weeks.append({
            "week": 19 + j,
            "type": "2G",
            "days": week_days,
        })

    return weeks


# =========================
# 5. 출력 유틸 (Markdown / CSV)
# =========================

def export_markdown(weeks, season, filename=None):
    if filename is None:
        filename = f"KBO_{season}_vs_27weeks_SPREAD.md"

    lines = ["#### 정규시즌"]
    for w in weeks:
        lines.append(f"##### WEEK {w['week']}")
        for day_games in w["days"]:
            for home_idx, away_idx in day_games:
                home = IDX_TEAM[home_idx]
                away = IDX_TEAM[away_idx]
                lines.append(f"{ABBR[home]} vs {ABBR[away]}")
            lines.append("")  # 일자 사이 빈 줄

    text = "\n".join(lines).strip() + "\n"
    with open(filename, "w", encoding="utf-8") as f:
        f.write(text)
    return filename


def export_csv(weeks, season, filename=None):
    if filename is None:
        filename = f"KBO_{season}_vs_27weeks_SPREAD.csv"

    with open(filename, "w", newline="", encoding="utf-8-sig") as f:
        writer = csv.writer(f)
        writer.writerow(["Season", "Week", "DayInWeek", "Type", "Home", "Away"])

        for w in weeks:
            week_no = w["week"]
            wtype = "3G week" if w["type"] == "3G" else "2G week"
            for d, day_games in enumerate(w["days"], start=1):
                for home_idx, away_idx in day_games:
                    home = IDX_TEAM[home_idx]
                    away = IDX_TEAM[away_idx]
                    writer.writerow([season, week_no, d, wtype, home, away])
    return filename


def print_week_preview(weeks, week_no):
    print(f"##### WEEK {week_no}")
    w = next(w for w in weeks if w["week"] == week_no)
    for day_games in w["days"]:
        for home_idx, away_idx in day_games:
            home = IDX_TEAM[home_idx]
            away = IDX_TEAM[away_idx]
            print(f"{ABBR[home]} vs {ABBR[away]}")
        print("")


# =========================
# Main: 2034 시즌 생성 & 저장
# =========================

if __name__ == "__main__":
    SEASON = 2034
    weeks = generate_season_2034(seed=2034_777)

    # 마크다운 / CSV 파일로 저장
    md_file = export_markdown(weeks, SEASON)
    csv_file = export_csv(weeks, SEASON)

    print(f"생성 완료! markdown: {md_file}, csv: {csv_file}")
    print()
    print_week_preview(weeks, 1)   # WEEK 1 프리뷰
    print_week_preview(weeks, 19)  # WEEK 19 프리뷰 (2연전 블록 시작)

Embed on website

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