playbuns
2026-09-05 · 제작 노트

판이 조용히 죽는 퍼즐을 만들었다

Chroma Drop은 낙하 블록 퍼즐에 색 혼합을 얹은 게임입니다. 줄을 채우는 것만으로는 안 지워지고, 그 줄 전체가 목표색이어야 지워집니다. MVP를 끝내고 게임오버 시점의 판을 자동으로 재 봤더니, 채워진 줄의 27%가 이미 「영원히 지울 수 없는 상태」였습니다. 그리고 플레이어에게는 아무 신호도 없었습니다.

Read in English →

규칙 한 줄이 만든 되돌릴 수 없는 상태

혼합 규칙은 예측 가능해야 해서 아주 좁게 잡았습니다. 조각이 잠기는 순간, 아래쪽으로만, 딱 한 번 섞입니다. mix(R,B)=P 보라, mix(B,Y)=G 초록, mix(R,Y)=O 주황. 그리고 연쇄 폭주를 막으려고 한 줄을 더 넣었습니다 — 혼합색은 다시 섞이지 않는다.

이 한 줄이 조용한 결과를 낳습니다. 각 칸이 «앞으로 가질 수 있는 색»이 이렇게 굳습니다.

칸앞으로 가능한 색
빈칸전부
R 빨강{R, P, O}
B 파랑{B, P, G}
Y 노랑{Y, G, O}
혼합색 X{X} — 영원히 자기 자신

그러면 판정은 자명합니다. 한 행의 채워진 칸들에 대해 이 집합의 교집합이 공집합이면, 그 행은 섞기만으로는 영원히 단색이 될 수 없습니다. 서로 다른 혼합색이 한 줄에 들어가는 순간(P와 O 같은)이 대표적이고, 원색 셋(R·B·Y)이 한 줄에 모여도 죽습니다.

나는 이 성질을 «연쇄를 막았다»는 이득으로만 생각했습니다. 그게 동시에 수복 불가 상태를 만드는 규칙이라는 건 계측이 알려 줬습니다.

계측 — 판이 얼마나 죽어 있었나

scripts/deadrow-probe.mjs는 휴리스틱 봇으로 자유 모드를 반복해서 돌리고, 게임오버 시점의 판을 그대로 떠서 각 행의 도달 가능 색 교집합을 계산합니다. 결과는 이랬습니다.

더 중요한 건 비율 자체가 아니라 플레이어가 그걸 알 방법도, 되돌릴 방법도 없었다는 점입니다. 화면에는 «꽉 찼는데 안 지워지는 줄»만 보입니다. 왜 안 지워지는지 모르니 계속 그 줄을 살리려 하고, 그러는 동안 판이 차오르다 탑아웃합니다.

보드 폭을 8에서 6으로 줄여도 이 문제는 그대로 남습니다 — 줄당 칸이 줄어 조금 늦게 올 뿐입니다. 그래서 이건 밸런스가 아니라 규칙의 구조적 결함이라고 판단했습니다.

처방을 둘로 나눠서 재기

처방은 두 갈래였습니다. 신호(죽은 줄을 눈에 보이게) 와 수복 경로(살릴 수단). 그리고 «둘 다 넣었더니 좋아졌다»로 끝내면 아무것도 배우지 못하므로, probe에 모드를 세 개 두고 따로 쟀습니다.

모드 (각 30판, 잉크 미사용 최저 기준선)수복 불가 비율
base — 기준선36%
+ 표백만 (사후 수복)28%
+ 표백 · 죽은 줄 회피 (경고를 보는 플레이어)9%

목표(10% 미만)는 넘겼습니다. 그런데 분해해 보면 실제로 일한 쪽은 신호입니다. 표백만으로는 36%에서 28%까지밖에 안 내려갑니다.

이유는 표백의 사정거리에 있습니다. 표백 조각(잉크 2 소모)은 섞는 대신 아래 칸을 조각 색으로 덮어씁니다. 하지만 덮어쓰기는 스택 표면 — 각 열의 맨 위 칸에만 닿습니다. 이미 다른 줄 밑에 묻힌 죽은 줄은 못 살립니다. 즉 표백은 「예방」이 아니라 「마지막 수단」이고, 예방을 하는 건 경고 표시입니다. 두 처방은 같이 있어야 의미가 있었습니다.

경고 표시 자체도 대충 만들 수 없었습니다. 죽은 행은 채도를 낮추고, 반투명 빗금 밴드를 덮고, 행 왼쪽 끝에 ⚠ 배지를 답니다. 빗금은 반드시 반투명이어야 합니다 — 어느 칸을 표백할지가 그 칸의 색으로 결정되기 때문에, 경고가 색을 가리면 경고가 수복을 방해합니다. (구현 함정도 하나 있었습니다. 이 밴드를 CSS 그리드 아이템으로 두면 명시 배치가 auto 배치보다 먼저 깔려 행 전체를 차지하는 밴드가 칸 112개를 아래로 밀어내 보드 높이가 2배가 됩니다. 실제로 겪고 position:absolute로 띄웠습니다.)

그런데 제거 줄 수는 안 올랐다

여기서 두 번째 교훈이 나옵니다. deadrow probe의 봇은 잉크를 아예 안 쓰는 최저 기준선이라 어느 모드에서든 제거 줄이 0입니다. 그래서 잉크·리롤·표백을 실제로 쓰는 더 강한 그리디(scripts/clearrate-probe.mjs, 40판)로 다시 쟀습니다.

모드판당 제거 줄수복 불가
대조군1.3821%
처방(회피 + 표백)1.136%

제거 줄이 오히려 줄었습니다. 회피는 제약이니 그리디 봇은 대가를 치릅니다. 회피 가중치를 스윕해 보면 w=150에서 24판 기준 1.46줄 / 9%로 대조군과 동률이고, w=900이면 0.88줄 / 1%로 과교정입니다.

즉 이 처방이 고친 것은 난이도가 아니라 「이유를 알 수 없는 패배」였습니다. 이해 가능성의 버그였지 밸런스 버그가 아니었고, 제거 줄 병목은 손도 안 댄 채 그대로 남아 있었습니다. 지표를 하나만 봤으면 «9% 달성» 하고 닫았을 문제입니다.

병목 진단 — A인가 B인가

«판당 1.3줄»이면 유일한 보상 신호가 3~6분에 한두 번뿐입니다. 플레이어는 뭘 잘못하는지 모른 채 튕겨 나갑니다. 가설은 둘이었습니다.

둘을 가르려고 probe를 확장해 예약 줄(꽉 찬 단색 줄)의 생성 → 소멸을 색깔별 큐로 추적했습니다. 행 인덱스는 제거가 일어날 때마다 밀리기 때문에 y가 아니라 색깔별 개수 변화로 셌습니다.

진단 (폭 8, 44판)값
판당 예약 줄 생성1.30개
예약 줄 대기 중앙값4조각
게임오버까지 안 지워지고 남은 예약 줄0.50개
리롤 사용0.93회 (4회 중)

대기는 짧고 리롤도 남습니다. B가 아니라 A였습니다 — 줄이 애초에 안 만들어지고 있었습니다.

A를 고치자 B가 드러났다

1차 처방은 보드 폭 8 → 6. 한 줄을 단색으로 채우는 데 필요한 칸이 줄어드니 A에 직접 듣습니다. 예약 줄 생성이 1.30 → 2.07개(+59%)로 올랐고 수복 불가도 9% → 3%로 떨어졌습니다.

그런데 제거 줄은 1.41줄에 그쳤고, 게임오버까지 남은 예약 줄이 0.50 → 1.09개로 늘었습니다. 이제 줄을 목표색 회전보다 빨리 만들어 내고 있었습니다. A를 고치니 B가 드러난 겁니다.

2차 처방은 B 레버 3종. 목표색 자동 전진을 10조각 → 4조각, 리롤 3 → 4회(회복도 3줄 → 2줄마다), 그리고 판에 이미 예약된 색을 2배 가중해 뽑기. 마지막 것은 «내가 만든 줄을 게임이 알아본다»는 신호이기도 합니다. 전진 간격은 88판 스윕으로 골랐습니다 — t=6 2.09줄 / t=5 2.33 / t=4 2.51. (44판에서는 비단조하게 나와서 표본을 키워야 갈렸습니다.)

지표처방 전 (폭 8)폭 6만최종 (폭 6 + B)
판당 제거 줄1.301.412.47~2.51
0줄로 끝난 판23% (10/44)23%6.8% (6/88)
수복 불가9%3%8%
예약 줄 생성/판1.302.072.10
예약 줄 중 지워진 비율59%46%82%

맨 아랫줄이 이 표에서 가장 마음에 드는 숫자입니다. 만든 줄의 82%가 실제로 보상으로 이어집니다. 폭만 줄였을 때는 46%였습니다 — 열심히 만들고 절반 이상을 묻고 있었다는 뜻입니다.

같은 계측으로 확인한 부수적인 것도 있습니다. 6열 보드에서 폭 4짜리 I4 조각이 과한지 재 봤는데, 오히려 지렛대였습니다. 단색 4칸이 한 행의 3분의 2를 한 번에 덮습니다. I4를 빼면 판당 제거가 2.47 → 2.03줄로 떨어지고 0줄 종료가 6/88 → 15/88로 나빠집니다. 유지.

정직하게 남긴 것

혼합색 줄은 여전히 판당 0.07개입니다. 2.5배 배점이 사실상 죽은 콘텐츠인데, 이건 바닥이 아니라 천장 문제로 봅니다. 혼합색 6칸 줄을 만들려면 «단색 한 줄을 깔고 다른 원색 한 줄로 덮어 두 줄을 한 번에 혼합색으로 바꾸는» 2수 이상의 계획이 필요하고, 계측에 쓴 1-ply 그리디 봇은 원리적으로 못 찾습니다. 사람의 실력 상한으로 남겨 뒀습니다.

그리고 과적합 경고를 SPEC에 같이 적어 뒀습니다. 위 수치는 전부 무계획 1-ply 그리디 기준입니다. 특히 전진 간격 4는 이 봇에 맞춰 고른 값이라, 사람에게는 목표색이 너무 빨리 흘러 «계획할 틈이 없다»고 느껴질 수 있습니다. 그렇게 나오면 되돌릴 후보는 t=5(2.33줄 / 0줄 종료 10.2%)이고 근거 숫자는 스윕에 남아 있습니다. 봇에 맞춰 튜닝했다는 사실 자체를 문서에 남기는 것이, 나중에 이 값을 되돌릴 사람에게 줄 수 있는 가장 정직한 것이었습니다.

덤 — daily가 이미 거짓말이었다

계측 과정에서 전혀 다른 결함도 하나 잡혔습니다. 「오늘의 팔레트」 모드는 날짜 시드로 «같은 날 같은 판»을 약속하는데, 조각·색 뽑기와 목표색 뽑기가 같은 난수 스트림을 쓰고 있었습니다.

그러면 리롤을 한 번만 더 눌러도 이후 조각 순서까지 갈립니다. 즉 두 사람이 같은 날 같은 daily를 열어도 서로 다른 판을 하고 있었습니다 — 약속이 이미 깨져 있었던 겁니다. 스트림을 조각·색용과 목표색용으로 분리해서 해결했습니다. 이제 조각·색 시퀀스는 플레이와 무관하게 날짜로 고정되고, 목표색만 예약 가중을 통해 판에 반응합니다. FAQ 문구도 이에 맞게 고쳤습니다.

재튜닝할 사람에게

이 게임을 다시 만질 때 지킬 것 하나만 남기자면 이겁니다 — 제거 줄 수와 수복 불가 비율을 항상 같이 본다. 한쪽만 보고 튜닝하면 반드시 다른 쪽이 무너집니다. 이 글의 중간이 정확히 그 사례입니다: 수복 불가를 9%로 만든 처방이 제거 줄을 1.38 → 1.13으로 깎아 놨고, 그걸 다른 지표를 보지 않았으면 몰랐을 겁니다.

직접 해 보기

다른 제작 노트

이 글의 모든 수치는 레포의 SPEC.md §9와 실제 계측 스크립트 출력에서 그대로 가져왔습니다. 사실과 다른 부분을 발견하면 문의로 알려 주세요.