KKflow

BUG · 버그 도감 #8

안내선대로 놓았더니 100% 빗나갔다: 정답을 아는 테스트의 함정

스택 타워의 모양 맞추기 스테이지에는 '여기에 맞추세요' 안내선이 있었습니다. 그런데 안내선대로 놓으면 좁히는 단계는 한 번도 성공하지 못했습니다. 안내선이 만들 수 없는 모양을 가리키던 기하학적 이유와, 자동 테스트가 그걸 놓친 이유를 정리했습니다.

· KKflow · 읽는 시간 약 5분

버그 도감은 KKflow 프로젝트에서 실제로 만난 버그를 한 편에 하나씩 기록하는 시리즈입니다. 모든 내용은 해당 저장소의 커밋 기록과 코드를 근거로 합니다.

프로젝트 스택 타워 · 시기 2026년 8월 29일 · 관련 기술 게임 규칙 설계, three.js, 자동 플레이 테스트

증상

스택 타워는 좌우로 움직이는 블록을 탭해서 아래 블록 위에 떨어뜨리는 게임입니다. 아래 블록과 겹친 부분만 남고 삐져나온 부분은 잘려 나가니, 정확히 놓을수록 탑이 넓게 유지됩니다. 8월 말에 들어간 스테이지 모드에는 여러 목표가 있었고, 그중 하나가 모양 맞추기입니다. 층마다 정해진 폭으로 블록을 맞춰 가며 탑의 모양을 만드는 목표로, 화면에 노란 안내선이 "이 폭으로 맞추세요"를 보여 줬습니다.

8월 29일 커밋의 첫 줄은 짧습니다.

"안내선대로 놓으면 좁히는 단계는 100% 빗나갔다."

탑을 넓히는 단계가 아니라 좁히는 단계에서, 안내선을 믿고 놓은 플레이어는 한 번도 목표 폭을 만들 수 없었습니다. 커밋에는 측정값도 남아 있습니다. 목표 폭이 1.09인 층에서 안내선 한가운데에 맞춰 놓으면 결과는 1.35였고, 좁히는 단계 세 번 모두 빗나갔습니다.

원인: 안내선은 만들 수 없는 것을 가리켰다

고치기 전의 안내선은 목표 폭을, 탑 한가운데에 그렸습니다. 직관적으로는 맞아 보입니다. "이 크기로 맞추세요"니까요. 그런데 이 게임의 규칙에서는 그 모양을 만들 수 없습니다.

블록을 놓는 장면을 한 줄로 생각해 보겠습니다. 아래 블록의 폭이 W이고, 같은 폭 W의 블록이 떨어집니다. 두 블록의 중심이 o만큼 어긋나 있으면 겹치는 부분, 즉 남는 조각은 이렇게 정해집니다.

  • 남는 폭 = W − |o| (어긋난 만큼 줄어든다)
  • 남는 조각의 중심 = 아래 블록 중심 + o ÷ 2 (어긋난 쪽으로 절반만큼 밀린다)

이제 안내선을 보겠습니다. 안내선은 "폭 T짜리 조각이 탑 한가운데에 남는다"를 그렸습니다. 하지만 위 식에서 조각이 한가운데(밀림 0)에 남으려면 o = 0이어야 하고, 그러면 남는 폭은 W 그대로입니다. 폭을 T로 줄이려면 정확히 W − T만큼 어긋나게 놓아야 하고, 그때 남는 조각은 반드시 그 절반만큼 옆으로 밀립니다. 커밋의 표현으로 "안내선은 크기도 위치도 만들 수 없는 것을 가리키고 있었다."

고치기 전 안내선 = 가운데에 폭 1.09 그대로 놓으면 가운데 맞춤 = 퍼펙트 → 폭 1.35 그대로 고친 뒤 블록 크기 테두리를 0.26 비킨 자리에 만들 수 없는 모양 1.35 남는 조각 1.09 W − T = 0.26만큼 비켜 놓기 아래 블록
옆에서 본 그림입니다(커밋의 측정값 기준: 아래 블록 폭 1.35, 목표 1.09). 회색이 아래 블록, 점선이 안내선입니다. 남는 조각은 언제나 어긋난 쪽으로 밀리기 때문에, '가운데에 폭 1.09'는 규칙상 만들 수 없는 모양이었습니다.

플레이어가 안내선 가운데를 노리고 블록을 떨어뜨리면, 블록은 아래 블록과 거의 정확히 겹칩니다. 게임은 그걸 "퍼펙트"로 처리해 폭을 그대로 남깁니다. 커밋에 남은 측정값이 바로 이 상황입니다. 목표 1.09를 노렸는데 1.35가 남았다는 것은, 탑의 폭이 하나도 줄지 않았다는 뜻입니다.

해결: '남을 모양'이 아니라 '놓을 자리'를 보여 준다

고친 안내선은 보여 주는 대상을 바꿨습니다. 결과물(남을 조각)이 아니라 행동(블록을 놓을 자리)을 가리킵니다.

  • 테두리의 크기는 목표 폭이 아니라 움직이는 블록과 같은 크기입니다.
  • 테두리의 위치는 탑 가운데가 아니라 W − T만큼 옆으로 비킨 자리입니다.
  • 테두리의 높이는 탑 위가 아니라 블록이 지나다니는 높이입니다. 그래야 "여기에 넣어라"로 읽힙니다.

그 테두리 안에 블록을 넣으면, 위의 식대로 목표 폭 T가 남습니다. 실제 코드(index.html의 shapeSlot)는 이 계산을 그대로 옮겼습니다.

const target = clamp(cur + plan.steps[i], 0.3, BLOCK.max);
const off = cur - target;                 // 이만큼 어긋나게 놓아야 한다
const side = (i % 2) ? 1 : -1;            // 한쪽으로만 몰리지 않게 번갈아
return {target, off, side, ax, cur,
        pos: topC + side * off,           // 블록 중심이 와야 할 자리
        land: topC + side * off / 2};     // 실제로 남을 조각의 중심

한 가지가 더 있습니다. 좁힐 때마다 같은 쪽으로 비켜 놓으면 탑이 한쪽으로 점점 기웁니다. 그래서 층마다 왼쪽과 오른쪽을 번갈아 가리키게 했습니다(side).

색도 바꿨습니다. 노란 테두리는 골든 블록이나 황금 스킨 위에서는 거의 보이지 않았습니다. 어떤 테마에서도 보이는 흰빛으로 바꿨다고 커밋에 적혀 있습니다.

테스트는 왜 통과했나

이 버그에서 가장 오래 남는 교훈은 여기에 있습니다. 스택 타워에는 모양 맞추기를 자동으로 플레이해 보는 테스트(shapetest)가 이미 있었습니다. 그런데도 이 버그는 통과했습니다. 커밋이 이유를 적었습니다.

"목표 폭에서 오프셋을 직접 계산해 놓았다. 즉 화면을 보지 않고 정답을 알고 있는 셈이라, 안내선이 어디를 가리키든 통과했다."

테스트는 "목표 폭을 만들려면 얼마나 어긋나게 놓아야 하는가"를 스스로 계산해서 블록을 놓았습니다. 정답을 알고 있으니 당연히 성공합니다. 하지만 플레이어는 정답을 모릅니다. 플레이어가 아는 건 화면의 안내선뿐입니다. 테스트가 플레이어와 다른 정보를 보고 있었기 때문에, 플레이어가 보는 안내선이 틀려도 테스트는 알 방법이 없었습니다.

그래서 테스트도 두 가지로 바꿨습니다.

  1. 플레이어처럼 놓기 — 정답을 계산하지 않고, 화면의 안내선 자리에 블록을 넣는 방식으로 플레이합니다.
  2. 안내선 자체를 검사 — 안내선이 가리키는 자리가 실제 정답(목표 폭을 남기는 자리)과 같은지 따로 확인합니다.

교훈

  • 안내는 결과가 아니라 행동을 보여 준다. "이렇게 될 것"보다 "여기에 놓아라"가 플레이어에게 쓸모 있습니다. 특히 결과와 행동이 다른 자리에 있는 규칙이라면 더 그렇습니다.
  • 보여 주는 값이 규칙으로 만들 수 있는 값인지 확인한다. 이번 안내선은 게임 규칙상 존재할 수 없는 모양을 그렸습니다. 식 두 줄로 확인할 수 있는 일이었습니다.
  • 테스트는 사용자가 보는 것만 보고 움직여야 한다. 정답을 아는 테스트는 정답을 알려 주는 화면이 틀려도 통과합니다. 버그 도감 #4의 검증 배지, 버그 도감 #2의 "안 쓰는 것을 검증하던 검증"과 같은 계열의 교훈입니다.

스택 타워의 다른 기록은 스택 타워 제작기와 버그 도감 #1: 멈춘 카운트다운에 있습니다. 블록 높이 기준이 된 랜드마크는 세계 랜드마크 높이표에 정리했습니다. 이 작업은 Claude Code와 함께 했습니다.

함께 읽으면 좋은 글