진돗개 러너(ntsapp.dev/jindo)에 스테이지를 넷 더 붙였습니다. 해안 하나뿐이던 것이 대숲·화산·빙하·야경 도시까지 다섯이 됐고, 코스 길이는 2.54 km에서 9.76 km가 됐습니다.

붙이고 나서 헤드리스 회귀 검사를 돌렸습니다.

VERIFIED / 49 checks / 0 failures
PASS / autoplay completes ACT 01
PASS / autoplay completes ACT 02
PASS / autoplay completes ACT 03
PASS / autoplay completes ACT 04
PASS / autoplay completes ACT 05

전부 통과했습니다. 자동 주행이 다섯 액트를 끝까지 달렸고, 공중 타깃 연쇄도 붙었고, 체크포인트도 다 지났습니다.

그리고 브라우저로 열었더니 이랬습니다.

액트 3·4·5는 코스 전체가 바다 아래에 있었습니다

타이틀 화면부터 이상했습니다. 액트 1은 멀쩡한데 3·4·5는 캐릭터가 화면 위로 잘리고 카메라가 지형 안에 박혀 있었습니다.

숫자로 재 봤습니다.

액트코스 고도해수면 아래절벽 두께
1 해안-14 … 790%+20
2 대숲-50 … 15.924%-16
3 화산-138 … -42.7100%-104
4 빙하-134.7 … -62.7100%-100.7
5 도시-214.7 … -125.7100%-180.7

바다 평면은 y = -35에 고정돼 있었습니다. 액트를 이어 붙일 때마다 코스가 아래로 내려갔는데 물은 그대로 있었으니, 3번부터는 코스 전체가 수면 아래였습니다. 절벽 두께도 높이 = 코스 y + 34로 계산하고 있어서 y가 -34 밑으로 내려가면 음수가 됐습니다. 높이가 음수인 상자는 안팎이 뒤집힌 채로 그려집니다.

액트 1을 만들 때는 둘 다 맞는 식이었습니다. 액트 1이 y = -14 … 79 사이에서만 움직였으니까요. 절대 좌표로 적힌 상수는 그때 한 번 맞으면 그 뒤로 조용히 틀립니다.

시작 지점의 상방 벡터가 90도 누워 있었습니다

카메라가 지형에 박힌 건 따로 이유가 있었습니다. 액트를 수직으로 띄우려고 이렇게 적혀 있었습니다.

straight(2, 0, height_shift, names[0], 2)   # 2 m 전진하며 y를 ±18~30 m 이동

2 m 가는 동안 높이를 18~30 m 바꾸면 경사가 85도입니다. 그리고 액트의 시작점이 하필 그 램프 위에 찍혀 있었습니다. 시작 프레임을 뽑아 봤더니 이랬습니다.

ACT 2  forward=(0, +0.994, -0.110)   up=(0.000, 0.110, 0.994)
ACT 3  forward=(0, -0.997, -0.083)   up=(0.001, 0.083, -0.997)

up이 위가 아니라 z 방향을 가리키고 있습니다. 이 게임은 코스를 지형이 아니라 곡선 하나로 만듭니다. 진행 거리를 넣으면 그 지점의 전방·상방·우방 벡터가 나오고, 도로도 난간도 캐릭터 자세도 카메라도 전부 그 벡터 위에 얹혀 있습니다. 루프가 따로 구현돼 있지 않고 상방 벡터를 한 바퀴 돌린 것이며, 벽 달리기는 90도 눕힌 것입니다.

그 구조가 장점인 동안에는 좋았는데, 시작 프레임이 누우니 거기 얹힌 전부가 같이 누웠습니다.

높이차를 첫 105 m 구간에 나눠 넣는 것으로 고쳤습니다. 액트 사이 고도차는 그대로 유지되고 경사만 10~19도가 됩니다.

그런데 왜 검사는 통과했을까

이게 이 글의 요점입니다. 49개 검사는 거짓말을 하지 않았습니다. 검사가 확인한 것은 전부 사실이었습니다 — 가속이 붙고, 부스트가 통하고, 루프에서 프레임이 뒤집히고, 자동 주행이 완주했습니다.

주행은 코스 곡선 위에서 일어납니다. 물이 어디 있든, 절벽이 뒤집혔든, 카메라가 어디를 보든 곡선 자체는 멀쩡하니까 완주는 됩니다. 검사는 "끝까지 갈 수 있는가"를 물었고 답은 정말로 "그렇다"였습니다.

검사가 묻지 않은 것은 "그래서 무엇이 보이는가" 였습니다. 카메라 위치를 보는 검사가 하나도 없었고, 코스와 수면의 관계를 보는 검사도 없었습니다. 헤드리스라 화면이 아예 없으니 물어볼 수도 없었습니다.

그래서 이렇게 정리했습니다.

  • 주행 로직은 헤드리스 검사가 맞다. 결정론적이고 빠르고, 사람이 놓치는 경계를 잡는다.
  • 보이는 것은 헤드리스로 검사할 수 없다. 브라우저에 실제로 띄워서 다섯 액트를 전부 눈으로 봐야 한다.
  • 절대 좌표 상수는 언젠가 틀린다. y = -35, y + 34 같은 값은 처음 만든 스테이지에서만 맞다. 기준을 스테이지에 매달아야 한다.

지금은 수면·배경 섬·구름·절벽 두께가 전부 액트별 기준 고도를 따라갑니다. 코스가 아무리 내려가도 물 위에서 달립니다. 그리고 그걸 확인한 방법은 검사를 하나 더 늘린 것이 아니라, 다섯 액트를 브라우저에서 하나씩 열어 본 것이었습니다.

같이 고친 것들

스테이지를 붙이기 전에 잡아 둔 것도 몇 개 있었습니다.

  • 레일 스냅이 없는 레일로 붙었습니다. 바다 위 레일은 셋(-4.1, 0, +4.1)인데 스냅 식이 round(lane / 4.1) * 4.1이고 조향 한계가 ±8.8이라, 오른쪽으로 계속 밀면 레일이 없는 ±8.2 자리에 붙어 허공을 달렸습니다. 게다가 그 상태는 코스 이탈 감속도 안 걸립니다.
  • 재시작이 드리프트 충전을 안 비웠습니다. 드리프트 도중에 R을 누르면 충전량이 새 판으로 넘어가 출발하자마자 공짜 가속이 붙었습니다.
  • 타이틀 화면에서 R·Q가 게임을 시작시켰습니다. 일시정지에만 started 가드가 있었습니다.
  • 최고 기록을 저장만 하고 아무 데도 안 보여 줬습니다. 파일에는 쌓이는데 읽는 곳이 자기 자신의 비교문뿐이었습니다.

네 건 모두 회귀 검사를 같이 붙였습니다. 고친 것만으로는 다시 안 돌아온다는 보장이 없습니다.