DEV NOTE · STARLINK

숫자, 비교차, 시야: STARLINK 규칙을 정하기까지

STARLINK의 규칙은 화면에 다섯 줄로 들어간다. 그런데 그 다섯 줄 뒤에는 "어디까지를 이을 수 있는 선으로 볼 것인가"를 정하는 수치가 여럿 숨어 있다. 격자 방향 제한을 없앤 대가로 생긴 문제와, 그것을 숫자 몇 개로 다잡은 과정을 기록해 둔다.

출발점: 숫자는 선의 개수

가장 먼저 정한 규칙은 "별의 숫자 = 그 별에 닿는 선의 수"다. 그래프 용어로는 각 정점의 차수를 알려 주는 퍼즐이다. 이 규칙 하나만으로는 퍼즐이 되지 않는다. 별이 열 개만 있어도 차수를 맞추는 연결 방법은 수없이 많고, 플레이어가 논리로 좁혀 갈 근거가 없다. 그래서 선을 놓을 수 있는 자리를 줄이는 규칙이 필요했다.

비슷한 계열의 퍼즐은 보통 격자의 가로·세로 방향으로만 선을 허용해서 후보를 줄인다. STARLINK는 별자리를 그리는 게임이라 대각선과 임의 각도가 꼭 필요했다. 실제 별자리 그림에 수평·수직선만 있으면 별자리처럼 보이지 않는다. 방향 제한을 풀면 후보가 폭발하므로, 그 대신 세 가지 장치를 넣었다. 길이 제한, 교차 금지, 시야(가림) 규칙이다.

길이 제한 3.2칸

별은 6×6에서 9×9까지의 격자 칸에 하나씩 놓이고, 칸 중심에서 ±0.15칸 정도 흔들려 자연스러운 배치를 만든다. 두 별 사이가 격자 3.2칸을 넘으면 직접 이을 수 없다. 이 값이 없으면 판 반대편 별끼리도 이을 수 있게 되어 후보가 별 수의 제곱에 가깝게 늘고, 긴 선 하나가 판 전체를 가로질러 다른 선들을 모두 막는 단조로운 퍼즐이 된다.

3.2라는 값은 대각선 두 칸 남짓(약 2.83칸)과 직선 세 칸을 모두 품으면서 네 칸 거리는 배제하는 크기다. 저장소의 생성기로 난이도마다 시드 60개를 돌려 보면 직접 이을 수 있는 후보 선이 EASY 평균 10.0개, EXPERT 평균 47.9개였다. EXPERT의 47.9개 중 12.9개는 중력 별을 거치는 중계 후보이고 직선 후보는 약 35개다. 일반 별 18개로 나누면 별 하나당 직접 이을 수 있는 이웃이 네 개 안팎이다. 손으로 따져 보기에 부담스럽지 않은 수준이다.

시야 규칙과 가림 반경 0.42칸

"선은 다른 별 위를 지나갈 수 없다"는 규칙은 원래 교차 규칙의 연장으로 넣었다. 선이 별을 관통하면 그 별에 닿은 것인지 지나간 것인지 화면만 보고는 구분하기 어렵다. 문제는 "위를 지나간다"의 정의였다. 선이 별의 중심을 정확히 통과할 때만 막으면, 화면에서는 별 그림을 반쯤 가리며 지나가는 선이 허용된다. 플레이어 눈에는 분명 별을 덮고 있는데 규칙상 통과하는 셈이다.

그래서 선분과 다른 별 중심 사이의 최단 거리가 0.42칸보다 가까우면 가려진 것으로 판정한다. 별을 탭으로 집는 반경이 0.45칸이므로 "손가락이 닿을 만한 영역"과 거의 같은 크기를 가림 영역으로 쓴 셈이다. 별끼리는 최소 1칸 간격을 두고 배치되기 때문에 0.42칸이면 이웃한 별 사이를 지나는 선은 대부분 살아남고, 정말로 별에 걸치는 선만 막힌다.

// js/puzzle.js 발췌
const MAXLEN=3.2;  // max link length in cells
const CLEAR=0.42;  // a segment passing closer than this to another star is blocked
function los(stars,i,j){
  const a=stars[i],b=stars[j];
  for(let k=0;k<stars.length;k++){ if(k===i||k===j) continue;
    if(GEOM.segPointDist(a,b,stars[k])<CLEAR) return false; }
  return true;
}

이 규칙은 퍼즐 쪽에서도 쓸모가 컸다. 가려진 별 쌍이 생기면서 "이 둘은 가까워 보여도 이을 수 없다"는 추론이 가능해졌고, 나중에 중력 별(◎)이라는 독자 메카닉이 들어설 자리도 여기서 나왔다. 가려진 두 별을 이어 주는 유일한 수단이 중력 별의 휘는 선이다.

교차 금지는 끝점에서만 예외

교차 판정은 외적(orientation) 네 번으로 두 선분이 서로를 가로지르는지 본다. 여기서 두 가지를 정해야 했다. 하나는 같은 별을 끝점으로 공유하는 두 선이다. 별 하나에 선이 서너 개 닿는 것이 이 퍼즐의 본질이므로 끝점에서 만나는 것은 교차로 치지 않는다. 다른 하나는 한 직선 위에 포개지는 두 선분이다. 판정 함수는 일직선 위에서 겹치거나 맞닿는 경우까지 교차로 처리한다. 포개진 선은 화면에서 한 줄로 보여서 플레이어가 선이 두 개라는 것을 알아챌 수 없기 때문이다. 실제로는 가림 규칙이 이런 배치를 먼저 걸러 내는 경우가 대부분이지만, 판정 함수는 다른 규칙에 기대지 않고 혼자서도 맞게 동작하도록 두었다.

교차 여부는 게임 중에 매번 계산하지 않는다. 퍼즐이 만들어질 때 모든 후보 선 쌍의 충돌 여부를 한 번 계산해 충돌 표로 저장하고, 선을 놓을 때는 표만 확인한다. 같은 표를 풀이기도 그대로 쓴다.

하나로 이어져야 별자리다

숫자와 교차 규칙만 있으면 별들이 두세 개의 작은 덩어리로 나뉘어도 숫자는 맞을 수 있다. 이런 해는 퍼즐을 쉽게 만들고 답의 수도 늘린다. 무엇보다 하나의 별자리를 완성한다는 게임의 그림과 맞지 않는다. 그래서 "모든 별이 선을 따라 하나로 연결될 것"을 다섯 번째 규칙으로 넣었다. 생성기는 정답을 신장 트리에서 출발해 만들기 때문에 정답은 항상 이 조건을 만족한다. 중력 별은 선이 지나가는 중계점일 뿐이라 연결 검사에서는 빠진다.

완성 판정은 생성기가 만든 정답과 비교하지 않는다. 각 별의 차수가 숫자와 같은지, 일반 별이 모두 한 덩어리인지만 본다. 생성기가 답이 하나뿐인 퍼즐을 찾지 못해 다중해 퍼즐이 나오더라도, 규칙대로 푼 사람이 "정답과 달라서" 실패하는 일은 없다.

STARLINK 게임 화면. 숫자가 적힌 별들이 사이안·마젠타 선으로 이어져 있다
숫자가 다 찬 별은 사이안으로 채워진다. 선은 어느 각도로든 그을 수 있지만 길이·교차·가림 규칙이 후보를 좁힌다.

규칙을 화면이 가르치게

규칙이 늘면 "왜 안 그어지지?"라는 순간도 는다. 그래서 선이 놓이지 않을 때 이유를 구분해 알려 주는 진단 함수를 따로 두었다. 너무 멀면 "너무 멀어요", 가려졌으면 "다른 별에 가려져 있어요. 중력 별 ◎을 거쳐 보세요", 기존 선과 부딪히면 "선이 겹쳐요", 숫자가 다 찼으면 "이미 가득 찬 별이에요"가 뜨고 해당 별이 흔들린다. 같은 실패라도 막힌 이유에 따라 다음 행동이 달라지므로 문구를 하나로 뭉치지 않았다. 가림 안내에 중력 별을 언급한 것은 의도적이다. 막힌 지점에서 바로 다음 규칙을 떠올리게 하려는 것이다.

정리

방향 제한이 없는 퍼즐은 자유로워 보이지만, 실제로는 길이 3.2칸, 가림 0.42칸, 끝점 예외, 연결 조건 같은 세부 수치가 퍼즐의 성격을 결정한다. 이 값들은 생성기와 풀이기, 화면의 히트 테스트가 모두 같은 함수를 공유하도록 묶어 두어 규칙과 판정이 어긋나지 않게 했다. 다음 과제는 색약 모드처럼 숫자 채움 상태를 색에만 의존하지 않고 전달하는 방법이다. 설계 문서의 1.1.0 로드맵에 올라 있다.