← PRISM 소개로

240판을 손으로 만들지 않았습니다

퍼즐 게임을 만들 때 가장 비싼 일은 판을 짜는 게 아니라 그 판이 정말 풀리는지 확인하는 일입니다. 240판이면 사람이 할 수 있는 규모가 아닙니다. 그래서 순서를 뒤집었습니다.

문제: 손으로 만들면 검증할 수 없다

PRISM의 규칙은 하나입니다. 조각을 탭하면 90도 돌고, 빛이 그 즉시 새 길을 찾습니다. 모든 결정에 정확히 요구된 색을 동시에 비추면 그 판이 풀립니다.

규칙이 단순하다고 판을 짜기 쉬운 건 아닙니다. 조각을 늘어놓고 "이게 풀리긴 하나"를 사람이 확인하려면 결국 직접 풀어봐야 하고, 못 풀었을 때 그게 어려운 판인지 불가능한 판인지 구별할 방법이 없습니다. 240판을 그렇게 만들 수는 없습니다.

뒤집기: 정답에서 출발한다

회전 퍼즐 생성기의 표준 접근을 썼습니다. 판을 만들고 정답을 찾는 대신, 정답을 먼저 만들고 흐트러뜨립니다.

  1. 광원과 조각을 배치하고 빛을 흘려봅니다.
  2. 빛이 실제로 도달한 자리에 그 색의 결정을 놓습니다. 이 순간 정답 상태가 구성적으로 완성됩니다.
  3. 목표 탭 수만큼 거꾸로 회전시켜 흐트러뜨립니다.

이렇게 하면 풀 수 없는 레벨이 존재할 수 없습니다. 정답에서 출발했기 때문입니다. 흐트러뜨린 회전을 되돌리는 것이 곧 정답입니다.

"풀린다"로는 부족합니다

여기서 멈추면 나쁜 퍼즐이 잔뜩 나옵니다. 풀리기는 하는데 정답이 여러 개인 판이 섞이기 때문입니다. 정답이 둘이면 논리로 좁혀지지 않고, 아무렇게나 돌리다 맞는 일이 생깁니다.

그래서 후보마다 전수 조사를 합니다. 회전 가능한 조각들의 모든 조합을 만들어 보고, 클리어 상태가 몇 개인지 셉니다. 정확히 1이 아니면 그 후보는 버립니다.

let total = 1;
for (let i = 0; i < n; i++) {
  total *= radix[i];          // 조각별 회전 상태 수를 곱한다
  if (total > maxStates) return null;
}
for (let state = 0; state < total; state++) {
  // state를 각 조각의 회전값으로 풀어 넣고
  if (tracer.isSolved(scratch)) count++;
}

상태 공간이 20만을 넘으면 세는 것을 포기하고, 그 후보도 버립니다. "아마 유일할 것"으로 통과시키지 않습니다. 증명하지 못한 판은 출하하지 않습니다.

이 검사에는 예상하지 못한 부수 효과가 하나 있었습니다. 놀고 있는 조각이 자동으로 걸러집니다. 빛이 닿지 않는 조각은 어떻게 돌려도 결과가 같습니다. 그러면 클리어 상태가 그 조각의 회전 수만큼 배로 잡히고, 해가 2개 이상이 되어 탈락합니다. 화면에 있는 모든 회전 조각은 반드시 쓰입니다.

최소 탭 수는 추정이 아닙니다

각 레벨에는 최소 탭 수가 표시됩니다. 그 수 안에 풀면 「완벽」입니다. 이 값은 대략 잡은 목표치가 아니라 실제 최솟값입니다.

정답이 유일하다는 것이 여기서 다시 일합니다. 정답이 하나뿐이면 "각 조각을 정답까지 몇 번 더 돌려야 하는가"의 합이 곧 최소 탭 수입니다. 그래서 생성기는 최소 탭 수를 추정하지 않고 지정할 수 있습니다. 목표치를 정하고 그만큼만 거꾸로 돌리면 됩니다.

그리고 그 지정이 맞았는지를 독립적으로 다시 확인합니다. 흐트러진 판을 너비 우선 탐색으로 처음부터 풀어보고, 나온 최단 길이가 지정한 값과 다르면 그 후보를 버립니다. 생성기에 버그가 있으면 여기서 잡힙니다.

대부분은 버려집니다

증명을 통과해도 재미없으면 소용이 없습니다. 그래서 품질 게이트가 더 붙습니다. 너무 쉬운 판, 이미 풀려 있는 판, 결정이 모자란 판, 빛이 한 번도 꺾이지 않는 판, 그 챕터가 가르치려는 현상(분산, 혼합색)이 실제로 안 일어나는 판을 전부 떨어뜨립니다.

통과율은 낮습니다. 조각이 많아지는 후반 챕터에서는 빛이 지나갈 빈 칸이 귀해지고 결정 배치가 시도 대부분을 잡아먹습니다. 6챕터 45판을 채우는 데 백만 회 규모의 시도가 듭니다. 릴리스 준비 때 한 번 돌리는 스크립트라 시간보다 결과가 중요합니다.

어느 게이트가 후보를 얼마나 떨어뜨렸는지는 집계해서 출력합니다. 챕터 설정을 조율할 근거가 그것밖에 없기 때문입니다.

출하물에서 다시 증명합니다

생성기가 옳게 돌았다는 것과 실제로 나간 파일이 옳다는 것은 다른 사실입니다. 그래서 같은 검사를 최종 레벨 데이터에 대고 다시 합니다. 매 커밋마다 240판 전부에 대해 세 가지를 확인합니다. 풀리는가, 솔버가 낸 경로대로 탭하면 진짜로 클리어되는가, 그리고 해가 정확히 하나인가.

이 테스트가 없어서 한 번 사고가 났습니다. 빛줄기를 흡수 조각 앞에서 조금 일찍 끊는 렌더링 개선이 빛 세그먼트의 끝점을 정수가 아닌 값으로 만들었는데, 생성기의 게이트가 끝점의 정수 여부로 "빛이 여기 닿았는가"를 판정하고 있었습니다. 그 순간부터 벽이 있는 레벨은 전부 탈락하게 됐습니다. 그런데도 테스트는 전부 초록이었습니다. 이미 커밋된 결과물만 검증하고 생성 규칙은 검증하지 않았기 때문입니다.

지금은 도달 판정을 끝점 좌표가 아니라 위치와 진행 방향으로 합니다. 렌더링이 빛줄기를 얼마나 짧게 끊든 결과가 달라지지 않습니다.

결과

6개 챕터에 240판입니다. 평균 최소 탭 수가 챕터마다 단조 증가합니다.

챕터판 수최소 탭 수평균
I 반사141–42.6
II 분열321–63.7
III 분산441–85.1
IV 혼합521–85.7
V 정제531–117.2
VI 수렴452–118.3

플레이어에게 이 이야기가 의미하는 것은 하나입니다. 막히면 반드시 이유가 있습니다. 운으로 풀리는 판이 없고, 표시된 탭 수는 실제로 도달 가능한 값입니다. 못 푸는 판이 있다면 그건 버그가 아니라 아직 안 보인 논리입니다.