십이지천에는 원래 맵 만들기가 있었습니다. 언덕·강·대숲·지형 굴곡·자원 양· 확장 기지 수를 손잡이로 고르면 시드와 합쳐 수십 글자짜리 맵 코드가 되고, 그 링크를 받은 사람은 한 칸도 다르지 않은 맵에서 둡니다. 맵은 절차 생성이라 시드와 조절값만 같으면 어느 기기에서든 똑같이 다시 나오니, 서버에 맵을 올릴 필요가 없었습니다.
만들어 놓고 보니 그건 고르는 것이지 그리는 것이 아니었습니다. "여기에 강을 하나 더", "이 확장은 언덕 위로" 같은 것은 손잡이로는 안 됩니다. 그래서 붓으로 칠하는 편집기와, 그린 맵을 서로 나누는 공개 목록을 붙였습니다.
그린 맵은 칸을 전부 싣는다
붓으로 그린 맵은 시드로 다시 만들 수 없습니다. 사람이 칠한 칸 하나하나가 곧 맵입니다. 288×288 맵이면 82,944칸이고, 시작 자리와 자원까지 담으면 83,410바이트입니다. 압축하면 1만 1천~1만 2천 자 안팎이 됩니다.
주소창에 넣기엔 긴 길이라 길을 셋으로 나눴습니다.
| 어디로 | 무엇을 싣는가 |
|---|---|
| 편집기의 "해 보기" | 주소의 # 뒤에 압축한 맵 전체. 서버로 가지 않고, 새로 고쳐도 남는다 |
| 공개 목록 | ?mapid= 뒤에 16자 번호 하나 |
| 온라인 방 | 방장이 시작 신호에 맵 전체를 싣고, 중계 서버가 양쪽에 그대로 나눠 준다 |
자원이 누구 것인지(본진 몫인지 확장인지)와 확장 자리는 싣지 않고 자리에서 계산합니다. 실으면 "본진 바로 곁인데 확장이라고 적힌" 앞뒤 안 맞는 맵을 손으로 만들 수 있기 때문입니다. 판의 난수 씨앗도 맵 내용의 지문으로 정하므로, 같은 맵이면 두 기기가 같은 씨앗을 씁니다.
망가뜨릴 수 없는 곳은 붓이 비켜 간다
편집기는 늘 밑그림에서 시작합니다. 손잡이로 만든 맵이나 남이 올린 맵을 받아 고칩니다. 빈 들판에서 8만 칸을 칠하라고 하면 아무도 안 만듭니다.
| 도구 | 하는 일 |
|---|---|
| 들·언덕·절벽·강·대숲 | 끌어서 칠한다. 붓 반경 0~14 |
| 논 | 한 칸씩 놓는다. 끌면 여러 개 |
| 지맥천 | 둘레 3×3 을 고르고 놓는다 |
| 확장 묶음 | 논 6 + 지맥천 1 을 기본 맵과 같은 모양으로 놓고 둘레를 평지로 고른다 |
| 시작 자리 | 끌어서 옮긴다. 본진 곁 자원이 따라온다 |
| 대칭 | 점대칭(기본)·좌우·상하·끄기 |
가장 먼저 정한 것은 붓이 어디를 비켜 가는가입니다. 자원이 놓인 칸과 지맥천 둘레는 칠하지 않고, 시작 자리 반경 6칸에는 절벽·강·대숲을 칠하지 않습니다. 막아 놓고 나서 "막혔습니다" 라고 알려 주는 것보다, 처음부터 못 막게 하는 편이 낫습니다.
대칭은 기본으로 켜 둡니다. 한쪽만 칠하면 한 진영이 유리한 맵이 됩니다. 점대칭이면 한쪽에 강을 그을 때 맞은편에도 같은 강이 생기고, 화면에는 맞은편 붓 자리가 점선으로 같이 보입니다.
시험하면서 고친 것이 셋 있습니다.
- 시작 자리를 끌어도 본진 자원이 따라오지 않았습니다. 손을 떼는 순간 "무엇을 끌고 있었는지"를 먼저 지운 뒤에 그걸 읽고 있었습니다. 순서를 바로잡으니 본진 곁 자원 20곳이 함께 옮겨 갑니다
- 외따로 찍은 논 한 칸이 확장 기지로 잡혔습니다. 논 두 칸을 따로 찍었더니 확장 수가 그만큼 늘었고, 컴퓨터는 그 한 칸을 보고 본진을 지으러 갑니다. 이제는 자원이 3곳 이상 모여야 확장으로 칩니다
- 폰에서 두 손가락으로 확대하면 첫 손가락이 붓 자국을 남겼습니다. 둘째 손가락이 닿는 순간 첫 손가락이 칠한 것을 되돌리고 확대로 넘어갑니다. 확인할 때는 확대가 1.21배에서 4.29배로 바뀌는 동안 바뀐 칸이 0이었습니다
검사는 한 벌이다
막힌 맵을 판정하는 함수는 하나뿐이고, 편집기·주소창으로 여는 쪽·공개 목록 서버가 모두 그걸 씁니다. 따로 두면 "편집기에선 된다더니 올리면 거절"이 됩니다.
손잡이 맵 때부터 있던 검사(두 본진이 이어지는가, 확장 기지와 자원에 닿는가, 다닐 수 있는 땅이 30% 이상인가)에, 붓으로만 생기는 잘못을 더했습니다.
- 본진 터와 그 테두리가 지을 수 있는 땅인가, 둘레 반경 6칸이 걸을 수 있는 땅인가
- 논이 논 칸 위에 있는가. 들 위에 있으면 그 위에 건물을 지어 자원을 묻어 버립니다
- 한 칸에 자원이 겹치지 않았는가, 본진 밑에 자원이 깔리지 않았는가
- 본진마다 곁에 논이 하나는 있는가. 없으면 첫 일꾼 여섯이 할 일이 없습니다
- 두 본진이 32칸 이상 떨어졌는가
기본 맵 474장을 옮겨 넣으면 전부 통과하고(생성기 자체가 막힌 맵을 내는 6장은 뺐습니다), 각 검사는 맵을 하나씩 일부러 망가뜨려 실제로 잡는지 확인했습니다. 통과만 보고 넘어가면 아무것도 안 보는 검사와 구별되지 않습니다.

막힌 맵은 걸린 자리를 붉게 두르고 해 보기·방 열기·올리기를 끕니다. 위 화면은 확장 하나를 강으로 두른 것인데, 점대칭이라 맞은편 확장도 같이 막혀서 "2곳"으로 나옵니다.
마지막 검사(두 본진 거리)에서 하나를 배웠습니다. 처음엔 거리를 Math.hypot으로 쟀는데, 시뮬레이션 코드에 이런 함수가 들어오면 자가 검사가 막습니다. 자바스크립트 규격은 이 함수의 마지막 자리를 엔진마다 다르게 두도록 허용하므로, 정확히 32칸 언저리에서 편집기는 통과시키고 서버는 거절하는 일이 생길 수 있습니다. 제곱한 정수끼리 비교하도록 바꿨습니다.
공개 목록 — 아무것도 믿지 않는다
공개 목록은 아무나 쓰는 곳이라 되는 것보다 막는 것이 중요했습니다. 저장소는 따로 데이터베이스를 세우지 않고, 이미 돌고 있던 온라인 대전 중계 서버 안에 하나 더 두었습니다.
| 막는 것 | 어떻게 |
|---|---|
| 손댄 맵, 막힌 맵 | 서버가 직접 풀어서 편집기와 같은 검사를 다시 한다 |
| 번호와 다른 맵 | 번호는 서버가 내용으로 계산한다(해시의 앞 12바이트). 받는 쪽도 다시 계산해 대조하므로, 서버가 뚫려 다른 맵을 내줘도 받지 않는다 |
| 목록에 아무 그림이나 걸기 | 미리보기로 그림을 받지 않는다. 서버가 맵에서 줄인 64×64 칸 종류를 주고 화면이 제 색으로 칠한다 |
| 욕설·광고 이름 | 띄어 쓰거나 숫자를 끼운 욕설까지 붙여서 보고, 주소·연락처를 막는다. 화면과 서버가 같은 파일을 쓴다 |
| 올리기 폭주 | 한 사람이 한 시간 8번, 하루 25번까지 |
| 순위 부풀리기 | 판 수는 한 사람이 한 맵을 하루 한 번만 센다 |
| 장난 신고 | 신고는 한 사람 한 번, 세 사람이 신고하면 목록에서 숨긴다. 이미 받은 링크로는 계속 열린다 |
| 다른 사이트에서 오는 쓰기 | ntsapp.dev 에서 온 요청만 받는다 |
횟수를 세려면 누가 했는지 알아야 하는데, 주소를 그대로 남기면 그게 곧 개인정보입니다. 그래서 비밀 소금을 친 해시로만 남기고, 횟수를 세는 기록은 하루가 지나면 지웁니다. 관리자가 맵을 숨기거나 지울 때 쓰는 토큰도 비밀값으로 두고, 대조할 때는 글자를 끝까지 다 비교합니다. 처음 다른 글자에서 멈추면 걸린 시간으로 앞자리부터 한 글자씩 맞혀 나갈 수 있기 때문입니다.
거름망은 첫 거름망일 뿐입니다. 말은 끝없이 바꿔 쓸 수 있습니다. 그리고 이 게임은 십이지 짐승이 주제라 "새끼 범" 같은 이름이 자연스러우니, 뜻이 분명한 말만 막고 놓친 것은 신고가 받게 했습니다.
작게 보내서 크게 터지는 것
압축한 데이터를 받는 곳은 압축 폭탄을 조심해야 합니다. 몇만 자짜리 압축문이 풀면 수십 MB가 되어 서버와 브라우저의 메모리를 채웁니다. 그래서 상한을 건 버퍼에만 풀도록 했는데, 여기에 함정이 있었습니다.
쓰고 있는 압축 라이브러리(fflate)는 버퍼가 넘치면 예외 없이 조용히 잘라서 돌려줍니다. 실제로 50MB짜리 폭탄이 110,000바이트로 잘려 정상처럼 돌아왔습니다. 버퍼를 상한과 같은 크기로 잡으면, 잘려 온 폭탄과 딱 상한 크기인 진짜 맵을 구분할 수 없습니다.
그래서 버퍼를 상한보다 1바이트 크게 잡고, 그 마지막 한 칸이 채워졌으면 넘친 것으로 봅니다. 0으로만 된 40MB를 압축한 5만 2천 자를 넣으면 0.12초 만에 거절합니다.
16KB 벽
온라인에서 그린 맵으로 둘 수 있는지 시험하다가 가장 큰 문제를 찾았습니다. 중계 서버는 16KB가 넘는 메시지를 받으면 그 연결을 끊습니다. 명령 하나가 그렇게 클 이유가 없으니 맞는 규칙이었는데, 그린 맵은 시작 신호에 맵 전체를 싣습니다. 공들여 칠한 맵일수록 압축이 덜 되니, 그대로 내보냈다면 맵을 정성껏 그린 방장일수록 시작 단추를 누르는 순간 방에서 쫓겨났을 것입니다.
시작 신호만 96KB까지 받게 했습니다. 방마다 한 번뿐이고 방장만 보낼 수 있는 신호라 상한을 따로 둘 수 있습니다. 2만 5천 자짜리 맵으로 확인하니 양쪽 모두 시작되고, 예전 상한으로 되돌리면 방장이 끊기는 것까지 봤습니다. 시작 신호가 아닌 큰 메시지는 여전히 끊습니다.
결과
- 자가 검사 311항목(이번에 28항목 더함), 온라인 동기 검사 35항목, 공개 목록 검사 22항목
- 공개 목록 검사는 손댄 데이터·압축 폭탄·욕설 이름·막힌 맵·다른 사이트의 쓰기·신고 중복· 판 수 부풀리기·올리기 폭주를 모두 막는지 봅니다. 서버가 번호와 다른 맵을 내주는 경우도 흉내 내서, 받는 쪽이 거절하는지 확인합니다
- 운영 서버에서는 시험용 맵을 올려 → 다시 받아 번호를 대조하고 → 관리 도구로 지우는 것까지 11가지를 확인했습니다
- 탭 두 개로 방을 열어 그린 맵으로 시작하면 두 쪽의 지형 지문이 같습니다
아쉬운 점
- 그린 맵에는 흙길이 없습니다. 길은 생성기가 지형을 깎을 때만 생기는 무늬라, 붓으로 그린 맵에서는 빠집니다. 게임에는 영향이 없지만 맵이 조금 밋밋해 보입니다
- 올린 맵은 올린 사람이 직접 지울 수 없습니다. 계정이 없어서 누가 올렸는지를 증명할 길이 없기 때문입니다. 올리기 전에 그렇게 알립니다
- 시작 자리는 둘뿐입니다. 3~4인 맵은 대전 인원을 늘릴 때 같이 다뤄야 합니다
- 공개 목록은 아직 비어 있습니다. 첫 맵을 기다립니다
/zodiac/의 시작 화면에서 "맵 만들기"를 누르고 "붓으로 고치기"로 들어가면 됩니다. 남이 그린 맵은 시작 화면의 "공개 맵"에 모입니다.