아침이 피크보다 험했다 — 밴드 폭의 역설
Peak Load는 낙하 블록 퍼즐 위에 발전소를 얹은 게임입니다. 완성된 줄은 저절로 지워지지 않고 금색으로 «장전»된 채 보드에 남고, 언제 쏠지는 플레이어가 정합니다. MVP 이후 계측으로 결함 4건을 잡았는데, 그중 하나는 스펙 4절의 전제 자체가 거꾸로 되어 있었습니다.
규칙 — 많이 지우는 게 아니라 제때 지운다
보드는 정통 그대로입니다. 10×20, 7-bag, SRS 회전과 월킥, 홀드 1칸, NEXT 5개, 고스트. 배울 게 없어야 해서 일부러 하나도 안 바꿨습니다. 새 규칙은 오직 전력 레이어입니다.
- 줄을 방출하면 출력이 즉시 오릅니다 — 1/2/3/4줄 = +34 / +62 / +88 / +120 MW. 그리고 반감기 5.5초로 계속 식습니다.
- 수요 그래프 위 초록 리본이 안정 밴드 = 수요의 85~130%입니다. 안에 있으면 초당 12점.
- 밴드 아래로 새면 정전 게이지, 위로 넘치면 과열 게이지가 오릅니다. 어느 쪽이든 100이면 게임오버.
- 배터리(용량 60)가 이탈량을 먼저 받아냅니다 — 흡수 초당 최대 18, 방전 초당 최대 22.
여기서 안정 밴드의 폭이 이 글의 주인공입니다. 상단 130%에서 하단 85%를 빼면 0.45 × 수요. 즉 밴드 폭은 절대값이 아니라 수요에 비례합니다. 이 한 줄을 내가 나중까지 제대로 못 읽었습니다.
결함 ① 오프닝이 플레이어를 벌하고 있었다
가장 먼저 잡힌 건 시작 구간이었습니다. scripts/opening-probe.mjs로 시드를 고정하고 정전 게이지 추이를 그렸습니다.
| 경과 | 무대응 | 20초에 1줄 | 20초에 2줄 |
|---|---|---|---|
| 10s | 7 | 7 | 7 |
| 20s | 22 | 22 | 22 |
| 40s | 63 | 49 | 19 |
| 60s | 100 | 91 | 60 |
20초 열의 세 값이 전부 22로 같다는 게 진단입니다. 20초 전에는 플레이어가 무엇을 하든 결과가 똑같습니다 — 손쓸 수 없는 구간에서 실패 예산의 22%가 이미 소진되어 있습니다.
원인은 산수로 나옵니다. 라운드 시작 출력은 demand(0) ≈ 51이고 밴드 하한은 43.4입니다. 반감기가 5.5초이므로 5.5 × log₂(51 / 43.4) = 1.3초 만에 밴드 아래로 떨어집니다. 그런데 배터리는 라운드 시작 시 0이라 완충이 없고, 10열 보드에서 첫 줄을 완성하려면 조각 4~5개 — 15~25초가 필요합니다.
이건 난이도가 아니라 램프업으로 벌하는 것입니다. 그래서 예열(WARM-UP)을 넣었습니다. 라운드 시작부터 첫 방출 또는 25초 중 먼저 오는 시점까지 정전 게이지가 누적되지 않습니다(과열은 그대로 작동합니다). 예열 중엔 배터리도 방전하지 않습니다 — 벌점이 없는데 태우면 낭비니까요. 스톨 악용은 성립하지 않습니다: 예열 중엔 안정 점수도 안 쌓이고 라운드는 180초 고정입니다. 그래도 25초 상한으로 못을 박았습니다.
결함 ② 낭비 페널티가 사실상 「방출세」였다
원래 낭비 페널티는 «제거하고 3초 안에 과열 밴드로 넘어가면 −80»이었습니다. 의도는 «배터리 잔량을 보고 4줄을 쏜다»는 판단에 값을 매기는 것이었습니다.
scripts/balance-bot.mjs로 방출 후 3초 창에서 실제로 버려진 과열량의 분포를 재 봤습니다.
| 버려진 과열량 | 비율 |
|---|---|
| 0 | 26% |
| 0 ~ 1 | 13% |
| 1 ~ 3 | 15% |
| 3 ~ 8 | 34% |
| 8 이상 | 12% |
중앙값 2.4입니다. 문제는 정액 −80은 0.01만 버려도 12만큼 버려도 똑같이 부과된다는 것입니다. 그리고 방출 직후엔 출력이 대개 밴드 위로 튄 뒤 내려오므로, 이 조건은 거의 모든 방출에서 발동했습니다. 판당 22~27회 × −80 = 판당 −1790점. 정시 발전 보너스를 통째로 상쇄하고 있었습니다. 이름은 «낭비 페널티»인데 실제로는 방출세였습니다.
처방은 조건을 «버린 양»으로 좁힌 것입니다. 버려진 과열량에 비례해 ×12, 데드존 1, 방출 1회당 상한 −80. 결과: 낭비 감점이 판당 1790 → 585점(조각당 900ms) / 61점(1400ms), 중앙 점수 1640 → 2731. 이 정의라야 «배터리가 받아준다»는 판단이 실제로 점수와 연결됩니다.
결함 ③ 등급 컷이 뒤집혀 있었다
등급 컷은 계측 전 추정치였습니다(S 5200 / A 3800 / B 2200). scripts/grade-calibrate.mjs로 캐주얼 대역(조각당 1100~2400ms) 100판의 점수 분위수를 재니 25% = 3519 · 50% = 4489 · 75% = 5325 · 90% = 5807이었습니다.
구 컷은 숙련 페이스에서 전원 C, 느린 페이스에서 전원 S/A가 나왔습니다. 등급이 실력이 아니라 페이스를 재고 있었던 겁니다. 새 컷 S 5600 / A 4500 / B 3000에서 봇 분포는 S 17% / A 33% / B 35% / C 15%가 됩니다(사람은 더 아래로 퍼집니다).
결함 ④ — 국면 난이도 곡선이 통째로 뒤집혀 있었다
여기가 이 프로젝트에서 가장 뼈아팠던 발견입니다.
스펙 4절은 한 판을 3국면으로 나눕니다. 아침(0~60초) 수요 완만, 낮(60~120초) 상승 + 스파이크 2회, 피크(120~180초) 급변 + 스파이크 4회. 여기에는 말하지 않은 전제가 하나 깔려 있습니다 — «수요가 낮으면 쉽다.» 그래서 아침 수요를 44±7로 낮게 잡았습니다.
scripts/morning-sweep.mjs로 국면별 게이지 누적을 쟀습니다(조각당 1400ms, 20판).
| 아침 수요 | 아침 | 낮 | 피크 | 아침 안정% |
|---|---|---|---|---|
| 44±7 (구) | 37.4 | 12.4 | 24.2 | 27% |
| 48±7 | 33.4 | 20.9 | 25.8 | 30% |
| 54±8 (신) | 18.8 | 15.5 | 25.8 | 39% |
첫 줄을 보면 아침(37.4)이 피크(24.2)보다 험합니다. «익히는 구간»으로 만든 곳이 판 전체에서 제일 어려웠고, 나는 그걸 두 달치 감으로는 절대 못 잡았을 겁니다.
원인은 앞에서 흘려 둔 그 한 줄입니다. 밴드 폭은 0.45 × 수요라, 수요가 낮으면 밴드가 절대값으로 좁아집니다. 아침 수요 44에서 밴드는 37~57, 폭 20입니다. 그런데 2줄 방출은 +62입니다 — 무조건 초과입니다. 그래서 봇도 사람도 아침엔 1줄씩만 쏠 수 있고, 1줄(+34)은 금방 식으니 방출 간격이 벌어지고 그 사이 정전이 쌓입니다.
처방이 직관에 정면으로 반합니다. 쉬워야 할 구간의 수요를 올렸습니다. 아침 수요를 54±8(실측 44~66)로 올리면 밴드가 넓어져 1줄과 2줄 방출이 둘 다 밴드에 들어갑니다. 아침 압박이 37.4 → 18.8로 떨어지고 아침 안정 시간이 27% → 39%로 올랐습니다.
대신 «완만함»은 다른 축으로 표현합니다. 수요 수준이 아니라 변동폭으로 — 진폭이 아침 8, 낮 13, 피크 28이고 스파이크는 아침에 없습니다. 이게 이 결함에서 얻은 진짜 문장입니다: 이 시스템에서 「쉽다」는 수요의 높이가 아니라 밴드의 폭이 정한다.
최종 국면 곡선은 아침 18.4 → 낮 15.8 → 피크 24.8로 의도한 모양이 됐습니다.
기준선 — 3분을 버틸 수 있는가
balance-bot.mjs로 페이스별 16판씩 돌렸습니다. 봇은 실제 규칙 함수(fits·collapse·stepPower·demandAt)로 돌고 DOM만 건너뜁니다. 한계는 명확합니다 — 최종 안착 위치만 보고 경로 실현가능성은 검사하지 않으므로 배치 실력의 상한입니다.
| 조각당 | 생존 | 중앙 점수 | 등급 | 방출/판 | 방출당 줄 | 배터리(흡수/방전) | 낭비 |
|---|---|---|---|---|---|---|---|
| 900ms | 15/16 | 1536 | 전원 C | 30.6 | 1.7 | 163 / 105 | 740 |
| 1400ms | 16/16 | 5684 | S9·A5·B2 | 32.2 | 1.4 | 292 / 248 | 61 |
| 2000ms | 16/16 | 4786 | A10·B6 | 28.3 | 1.2 | 189 / 170 | 11 |
여기서 확인한 것 하나 — 배터리는 죽은 기능이 아닙니다. 1400ms에서 판당 292 흡수 / 248 방전, 평균 잔량 25/60으로 실제로 순환합니다. 구 밸런스에서는 평균 47~51/60으로 «항상 만충»이라 완충 역할을 전혀 못 했습니다. 기능이 «있는지»가 아니라 «순환하는지»를 재야 안다는 걸 여기서 배웠습니다.
정직하게, 아직 못 고친 것
고치지 못한 채 문서에 남겨 둔 게 하나 있습니다. 빠르게 놓을수록 불리합니다. 그리고 이건 봇 정책 문제가 아니라 구조입니다.
방출 1회가 계통에 주입하는 에너지는 상승량 × 5.5/ln2, 줄당 약 254 MW·s입니다. 반면 계통이 180초 동안 소비할 수 있는 총량은 평균수요 58 × 180 ≈ 10,400 MW·s입니다. 나누면 지속 가능한 제거량은 41줄, 조각당 1750ms가 나옵니다.
| 조각당 | 조각수 | 강제 제거 줄수 | 주입 에너지 | 소비 가능 | 공급과잉 |
|---|---|---|---|---|---|
| 650ms | 277 | 111줄 | 28.1k MW·s | 10.4k | +169% |
| 900ms | 200 | 80줄 | 20.3k | 10.4k | +95% |
| 1400ms | 129 | 51줄 | 13.1k | 10.4k | +25% |
| 1750ms | 103 | 41줄 | 10.4k | 10.4k | 0% |
10열 보드는 조각당 4칸을 무한히 저장할 수 없으므로, 그보다 빠르게 놓으면 어떤 전략으로도 공급과잉을 피할 수 없습니다 — 장전을 미루면 탑아웃, 쏘면 과열입니다. 실측으로 650ms에서 생존 81%, 중앙 점수 837, 전원 C입니다. 그리고 장전을 조절하는 봇(throttle)은 650ms에서 생존 17%로 오히려 더 나빴습니다. 「더 잘 참으면 된다」가 답이 아니라는 증거입니다.
해결하려면 LINE_GAIN(2절의 34/62/88/120) 또는 수요 상한 100을 건드려야 합니다. 예를 들어 LINE_GAIN을 0.6배(20/37/53/72)로 낮추면 지속 가능 페이스가 ~1050ms로 내려옵니다. 둘 다 게임의 손맛을 바꾸는 변경이라 단독으로 반영하지 않고 판단을 남겨 뒀습니다. 타깃인 캐주얼 대역(1100~2400ms)에서는 생존 98~100%로 건전해서 출시에는 지장이 없습니다 — 하지만 «지장이 없다»와 «없는 문제다»는 다른 말이라, 스펙에 표까지 붙여 남겼습니다.
남는 생각
결함 넷 중 셋(오프닝·페널티·등급 컷)은 «수치를 잘못 잡았다»입니다. 하지만 넷째는 다릅니다 — 스펙 문장이 참이라고 믿었던 전제가 거짓이었습니다. 「아침은 수요가 낮아서 쉽다」는 문장은 그럴듯해서 아무도 의심하지 않고, 손으로 플레이해서도 «아침이 좀 빡세네» 정도로만 느껴집니다. 국면별로 따로 재기 전까지는 절대 안 보였습니다.
그래서 이제 밸런스를 잴 때 총점이나 생존율 같은 합계 지표를 먼저 믿지 않습니다. 판을 구간으로 잘라서 재면, 설계자가 «여기는 쉬운 곳»이라고 이름 붙여 둔 자리가 사실 가장 험한 곳이라는 게 표 한 장에 나옵니다.
직접 해 보기
다른 제작 노트
이 글의 모든 수치는 레포의 SPEC.md §9와 실제 계측 스크립트 출력에서 그대로 가져왔습니다. 사실과 다른 부분을 발견하면 문의로 알려 주세요.