시드 생성기와 풀이기로 깰 수 있는 레벨만 내보내기: 게임 다섯 개의 방식 비교
무작위 레벨 생성에서 가장 나쁜 결과는 못 깨는 판이다. 플레이어는 자기 실력 탓인지 게임 탓인지 알 수 없고, 날마다 같은 코스를 주는 DAILY 모드라면 그날 전원이 막힌다. 우리 게임 중 다섯 개는 같은 원칙을 쓴다. 생성기는 시드로 판을 만들고, 풀이기(솔버)가 깰 수 있다고 확인한 판만 내보낸다. 원칙은 같지만 보장하는 방법은 장르마다 크게 다르다. 가벼운 간격 규칙부터 두 사람이 줄로 묶인 물리 시뮬레이션 재생까지 실제 코드와 수치로 비교한다.
전제: 어느 기기에서나 같은 판이 나와야 한다
솔버가 확인한 판과 플레이어가 받는 판이 다르면 보장은 의미가 없다. 그래서 다섯 게임 모두 난수를 시드 RNG에서만 뽑는다. DAILY 시드는 slingstar-daily-YYYY-MM-DD처럼 날짜 문자열을 해시해서 만들고, JJ 설계 문서는 시뮬레이션 코드에서 Math.random 사용을 아예 금지한다.
덜 알려진 함정은 삼각함수다. Math.sin, Math.cos의 마지막 자리는 자바스크립트 엔진마다 다를 수 있고, 궤도 시뮬레이션처럼 오차가 누적되는 계산에서는 그 차이가 "들어간다/안 들어간다"로 갈릴 수 있다. SLINGSTAR는 발사 방향을 각도로 만들지 않고 정사각형 둘레의 점을 정규화해서 만든다.
/* unit directions from the square perimeter (no trig → identical on every device) */
const t=i/DIRS*4; let x,y;
if(t<1){x=1;y=-1+2*t;} else if(t<2){x=1-2*(t-1);y=1;}
else if(t<3){x=-1;y=1-2*(t-2);} else {x=-1+2*(t-3);y=-1;}
const l=Math.sqrt(x*x+y*y); out.push({x:x/l,y:y/l});
방향 간격이 완전히 균일하지는 않지만, 모든 기기에서 비트 단위로 같은 결과가 나오는 쪽이 더 중요했다. STARLINK도 같은 이유로 생성기에서 삼각함수를 쓰지 않는다.
FLIP: 풀이기 없이 간격 규칙 하나로
가장 가벼운 쪽은 중력 반전 러너 FLIP이다. 탭 한 번으로 기체가 반대쪽 벽으로 떨어지는 단순한 물리라서, 탐색 없이 규칙 하나로 보장할 수 있었다. 장애물마다 "기체가 있어야 하는 쪽"(왼쪽, 오른쪽, 아무 벽)을 요구하고, 직전 장애물과 다른 쪽을 요구하려면 그 사이 간격이 건너는 거리의 1.25배 이상이어야 한다. 건너는 시간은 터널 폭과 가로 중력으로 계산하면 약 0.56초다.
const crossDist=speed*CROSS_T*1.25;
if(w.lastReq!==0&&req!==w.lastReq&&gap<crossDist){ side=-side; req=-req; }
간격이 모자라면 장애물을 버리지 않고 같은 쪽을 요구하도록 뒤집는다. 생성 실패나 재시도가 없으니 끝없이 이어지는 ENDLESS 모드에도 비용이 들지 않는다. 1.25라는 여유 계수가 사람의 반응 시간 몫이다.
SLINGSTAR: 쏠 수 있는 샷을 전부 쏴 본다
우주 골프 SLINGSTAR는 행성 중력 때문에 궤적이 직관적이지 않아서 규칙으로 보장하기 어렵다. 대신 홀마다 정해진 방향과 세기로 실제 물리 시뮬레이션을 전부 돌려 본다. 1.0.0에서는 48방향 × 3세기, 총 144회였고 들어가는 샷이 1회 이상 24회 이하인 홀만 채택했다.
이 기준은 같은 날 나온 1.1.0에서 바로 바뀌었다. 깰 수 있는지만 보면 행성을 피해 곧장 쏘는 샷으로 풀리는 홀도 통과하기 때문에, 해결 가능성과 별개로 "중력을 써야 하는 홀"이라는 조건이 필요했다. 1.1.0에서 검증을 96방향 × 4세기, 384회로 촘촘하게 바꾸고, 2번 홀부터는 시작점과 블랙홀을 잇는 직선을 행성이 막고 있어야 하며 직선에서 ±15도 안쪽 샷으로는 하나도 들어가지 않는 홀만 받도록 조건을 더했다. 여기서 솔버는 존재 확인을 넘어 난이도 측정기가 된다. 1.1.0의 문턱은 들어가는 샷이 4회 이하면 기준 4샷, 10회 이하면 3샷, 그보다 많으면 2샷이었다. 그런데 384회 중 들어가는 샷은 블랙홀 크기와 방향 간격 때문에 10회를 거의 넘지 않아서, 실측해 보니 기준 2샷 홀이 한 번도 나오지 않았다. 1.1.2에서 문턱을 4회 이상이면 2샷, 2~3회면 3샷, 1회면 4샷으로 고쳤고(2번 홀부터 채택 하한 1회, 1번 홀 3회), DAILY 30일치 270홀의 분포가 2샷 0·3샷 86·4샷 184에서 81·119·70으로 바뀌었다. 80번 안에 조건을 만족하는 홀을 못 찾으면 성공 샷이 하나라도 있었던 첫 후보를 쓴다.

STARLINK: 답이 하나뿐인지까지 센다
별자리 퍼즐 STARLINK는 반대 방향에서 접근한다. 정답을 먼저 만든다. 교차하지 않는 신장 트리로 별을 잇고 선을 몇 개 더한 다음, 각 별의 숫자를 정답에서의 연결 수로 적는다. 정답에서 출발했으니 해결 가능성은 이미 보장된다. 풀이기가 확인하는 것은 답이 하나뿐인지다.
풀이기는 후보 선마다 넣을지 뺄지를 정하는 탐색이다. 별에 남은 필요 수가 0이면 나머지 후보를 전부 빼고, 남은 후보 수와 같으면 전부 넣으며, 넣기로 한 선과 교차하는 후보는 뺀다. 이 전파로 대부분이 정해지고, 남은 부분은 제약이 가장 센 별부터 분기한다. 해를 두 개까지만 세고 탐색 노드는 15만 개로 제한한다. 지금 코드는 최대 30번 생성해서 유일해가 나오면 바로 쓰고, 끝내 못 찾으면 처음 만든 퍼즐을 쓴다. 승리 판정은 규칙을 만족하는 모든 해를 인정하므로 유일성은 퍼즐 품질 기준일 뿐 정답 판정에는 영향이 없다. 그래서 탐색 한도를 넘겨도 게임이 망가지지 않는다.
FOURSTEP과 JJ: 사람이 아닌 완벽한 플레이어를 경계하기
리듬 러너 FOURSTEP은 지형마다 층별 BFS 솔버가 1/8박마다 "누름/안 누름"을 탐색해 실제 물리로 통과 경로를 찾는다. 게임과 솔버가 같은 stepHero 함수와 1/24박 고정 스텝을 쓰므로, 솔버가 통과한 경로는 게임에서도 통과한다. 문제는 솔버가 프레임 단위로 완벽하게 누른다는 점이다. 솔버가 겨우 통과한 판은 사람에게 불가능에 가깝다.
그래서 솔버를 풀 때 장애물을 형태별 안전 여유만큼 키운다. 러너 0.4칸, 두더지 0.55칸, 해파리와 글라이더 0.5칸이다. 입력이 그만큼 어긋나도 통과하는 코스만 채택하는 셈이다. 실패하면 그 지형을 난이도 ×0.72로 다시 만들고, 최대 8번까지 시도한다. 다음 지형은 앞 지형 해답이 끝난 상태에서 이어 풀기 때문에 지형 경계에서 막히는 일도 없다.
2인 협동 줄 퍼즐 JJ는 상태 공간이 너무 커서 BFS가 불가능하다. 두 코어의 위치와 속도에 줄의 모양까지 상태에 들어가기 때문이다. 설계 문서는 처음부터 2단 구조를 택했다. 1단에서 계획을 세우고, 2단에서 그 계획을 실제 물리로 재생해 검증한다. 현재 구현의 1단은 상태 탐색 대신 검증된 챔버 템플릿마다 붙은 저자 플랜을 이어 붙이는 방식이다. 2단에서는 가시를 0.2칸 넓히고 게이트와 문 열림을 0.3초 줄인 안전 여유를 둔 채 재생하고, 실패하면 실패한 챔버만 최대 3번 다시 뽑은 뒤 그래도 안 되면 전체를 다시 뽑는다. 통과한 플랜 시간의 1.6배가 파 타임이 된다.
이 글을 쓰면서 node jj/tests/gen.mjs 200을 돌려 본 결과는 시드 200개 전부 통과, 그중 180개는 첫 시도에 통과, 평균 생성 17ms, 최대 73ms였고 같은 시드를 두 번 생성해 맵과 파 타임이 같은지 보는 결정성 검사도 통과했다. 설계 문서의 가설 P-5(통과율 95% 이상, 평균 300ms 미만)를 만족하는 수치다. CI도 푸시마다 같은 테스트를 시드 60개로 돌린다.
정리: 보장의 강도는 장르가 정한다
| 게임 | 보장 방식 | 실패하면 |
|---|---|---|
| FLIP | 요구 방향 간격 ≥ 건너기 거리 × 1.25 | 장애물 방향을 뒤집음 |
| SLINGSTAR | 384회 실제 발사, 성공 샷 수로 기준 타수 결정 | 홀 재배치(최대 80회) |
| STARLINK | 정답에서 역으로 숫자 생성, 유일해 검사 | 재생성(최대 30회) |
| FOURSTEP | 층별 BFS + 형태별 안전 여유 | 난이도 ×0.72로 재생성 |
| JJ | 템플릿 플랜을 실제 물리로 재생 + 안전 여유 | 챔버 3회, 이후 전체 재생성 |
다섯 게임을 거치며 얻은 결론은 두 가지다. 하나는 솔버를 완벽한 플레이어로 보고 반드시 안전 여유를 두어야 한다는 것이다. FLIP의 1.25배, FOURSTEP의 0.4~0.55칸, JJ의 0.2칸과 0.3초가 모두 사람의 몫이다. 다른 하나는 솔버의 산출물이 보장에서 끝나지 않는다는 것이다. 기준 타수와 파 타임, 타이틀 화면의 자동 플레이, JJ의 SOLO AI 파트너, CI 회귀 테스트가 전부 솔버 경로에서 나온다. 다음 게임 PRISM도 8방향 이산 광선 위에서 같은 생성기·솔버 구조를 쓰도록 설계했다.