playbuns
2026-09-05 · 제작 노트

아침이 피크보다 험했다 — 밴드 폭의 역설

Peak Load는 낙하 블록 퍼즐 위에 발전소를 얹은 게임입니다. 완성된 줄은 저절로 지워지지 않고 금색으로 «장전»된 채 보드에 남고, 언제 쏠지는 플레이어가 정합니다. MVP 이후 계측으로 결함 4건을 잡았는데, 그중 하나는 스펙 4절의 전제 자체가 거꾸로 되어 있었습니다.

Read in English →

규칙 — 많이 지우는 게 아니라 제때 지운다

보드는 정통 그대로입니다. 10×20, 7-bag, SRS 회전과 월킥, 홀드 1칸, NEXT 5개, 고스트. 배울 게 없어야 해서 일부러 하나도 안 바꿨습니다. 새 규칙은 오직 전력 레이어입니다.

여기서 안정 밴드의 폭이 이 글의 주인공입니다. 상단 130%에서 하단 85%를 빼면 0.45 × 수요. 즉 밴드 폭은 절대값이 아니라 수요에 비례합니다. 이 한 줄을 내가 나중까지 제대로 못 읽었습니다.

결함 ① 오프닝이 플레이어를 벌하고 있었다

가장 먼저 잡힌 건 시작 구간이었습니다. scripts/opening-probe.mjs로 시드를 고정하고 정전 게이지 추이를 그렸습니다.

경과무대응20초에 1줄20초에 2줄
10s777
20s222222
40s634919
60s1009160

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초 창에서 실제로 버려진 과열량의 분포를 재 봤습니다.

버려진 과열량비율
026%
0 ~ 113%
1 ~ 315%
3 ~ 834%
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.412.424.227%
48±733.420.925.830%
54±8 (신)18.815.525.839%

첫 줄을 보면 아침(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만 건너뜁니다. 한계는 명확합니다 — 최종 안착 위치만 보고 경로 실현가능성은 검사하지 않으므로 배치 실력의 상한입니다.

조각당생존중앙 점수등급방출/판방출당 줄배터리(흡수/방전)낭비
900ms15/161536전원 C30.61.7163 / 105740
1400ms16/165684S9·A5·B232.21.4292 / 24861
2000ms16/164786A10·B628.31.2189 / 17011

여기서 확인한 것 하나 — 배터리는 죽은 기능이 아닙니다. 1400ms에서 판당 292 흡수 / 248 방전, 평균 잔량 25/60으로 실제로 순환합니다. 구 밸런스에서는 평균 47~51/60으로 «항상 만충»이라 완충 역할을 전혀 못 했습니다. 기능이 «있는지»가 아니라 «순환하는지»를 재야 안다는 걸 여기서 배웠습니다.

정직하게, 아직 못 고친 것

고치지 못한 채 문서에 남겨 둔 게 하나 있습니다. 빠르게 놓을수록 불리합니다. 그리고 이건 봇 정책 문제가 아니라 구조입니다.

방출 1회가 계통에 주입하는 에너지는 상승량 × 5.5/ln2, 줄당 약 254 MW·s입니다. 반면 계통이 180초 동안 소비할 수 있는 총량은 평균수요 58 × 180 ≈ 10,400 MW·s입니다. 나누면 지속 가능한 제거량은 41줄, 조각당 1750ms가 나옵니다.

조각당조각수강제 제거 줄수주입 에너지소비 가능공급과잉
650ms277111줄28.1k MW·s10.4k+169%
900ms20080줄20.3k10.4k+95%
1400ms12951줄13.1k10.4k+25%
1750ms10341줄10.4k10.4k0%

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와 실제 계측 스크립트 출력에서 그대로 가져왔습니다. 사실과 다른 부분을 발견하면 문의로 알려 주세요.