용코바코 게임즈 노트

개발 노트

화면이 보내는 점수는 「주장」이다 — 웹게임 랭킹을 믿을 수 있게 만든 네 단계

브라우저에서 돌아가는 게임은 코드가 전부 그 사람 손에 내려가 있어서, 화면이 보내는 점수는 사실이 아니라 주장입니다. 게임 넷에 각각 다른 방어를 붙였습니다 — 재화의 정본을 서버가 쥐기, 서버가 판을 열고 시계를 재기, 서버가 문제를 내기, 서버가 판을 처음부터 다시 돌려 보기. 단계마다 무엇을 막는지와, 어느 단계로도 안 되는 것이 무엇인지까지 적었습니다.

랭킹이 있는 게임을 브라우저에 올리면 첫날 밟는 돌이 하나 있습니다. 점수를 누가 정하나입니다.

브라우저 게임은 코드가 전부 그 사람 컴퓨터에 내려가 있습니다. 개발자 도구를 열면 변수가 보이고, 서버로 가는 요청을 그대로 다시 부를 수 있습니다. 그러니 화면이 「12,400점을 냈습니다」라고 보내는 값은 사실이 아니라 주장입니다. 순위표에 그 주장을 그대로 올리면, 올라오는 것은 제일 잘한 사람이 아니라 제일 먼저 요청을 흉내 낸 사람입니다.

흔히 먼저 떠올리는 두 가지는 둘 다 답이 아닙니다.

  • 점수에 서명을 붙인다 — 서명하는 코드도 같이 내려가 있습니다. 그 코드를 불러서 아무 점수에나 서명을 받을 수 있으니, 자물쇠와 열쇠를 같이 보낸 셈입니다.
  • 코드를 알아보기 어렵게 만든다 — 시간을 벌 뿐입니다. 랭킹 1등이 걸린 일이면 그 시간은 길지 않습니다.

성립하는 전제는 하나뿐이었습니다. 점수를 「받지」 않는 것. 게임 넷을 만들면서 그 전제를 네 가지 깊이로 붙였습니다. 아래로 갈수록 비싸고, 그만큼 확실합니다.

① 재화의 정본을 서버가 쥔다 — 스트라이커 2125

제일 싼 단계입니다. 판 내용은 서버가 보지 않고, 판이 끝난 뒤 계좌에 무엇이 오르는지만 서버가 정합니다.

결과 화면에는 골드 내역과 적 종류별 격추 수가 꽤 세세하게 뜹니다. 그건 엔진이 판 내내 적어 둔 기록이고, 서버는 그 칸을 하나도 믿지 않습니다. 보여 주기만 합니다. 골드와 경험치의 정본은 동기화 응답에 실려 오는 값이고, 결과 화면은 그 값으로 다시 그려집니다.

그래서 이상한 상태 하나가 「맞는」 상태가 됩니다 — 출격을 알리지 못한 판은 골드가 0입니다. 전부 아니면 0이고, 중간이 없습니다. 예전에는 엔진이 센 「+N 골드」가 결과 화면에 남아 있다가 격납고에 가서야 사라졌는데, 그게 더 나쁜 꼴이었습니다. 화면이 받지도 않은 보상을 약속한 셈이니까요.

광고를 보고 받는 골드도 같은 자리에 있습니다. 앱에는 구글이 「정말 봤다」를 서버로 알려 주는 길 (서버 측 확인)이 있지만 웹에는 그에 해당하는 콜백이 없습니다. 그래서 「봤다」는 아예 믿지 않고, 방어를 셋으로 두었습니다 — 하루 열 번 · 한 번에 25초 간격 · 금액은 서버가 정합니다(그 사람이 도달한 스테이지 기준). 증명을 받는 「시늉」을 하던 옛 장치는 걷어냈습니다. 검증할 길이 없는 증명은 코드만 늘립니다.

이 단계가 막는 것은 재화 부풀리기입니다. 보낸 숫자가 그대로 지갑이 되지 않습니다.

② 서버가 판을 열고 시계를 잰다 — 플래닛 대시

한 단계 올라갑니다. 화면이 「해냈습니다」를 먼저 말하지 못하게 합니다.

순서가 뒤집혀 있습니다. 화면은 먼저 「출발하겠습니다」를 말하고, 서버가 그때 판을 하나 엽니다 — 어느 행성인지, 어느 난이도인지, 언제 시작했는지를 서버가 제 쪽에 적습니다. 끝나면 화면이 「이렇게 됐습니다」를 신고하는데, 기록을 정하는 값은 전부 서버가 적어 둔 사본에서 읽습니다.

이 구조에서 공짜로 따라오는 것이 둘입니다.

  • 시간 미션은 부풀릴 수 없습니다 — 걸린 시간을 재는 시계가 서버 것입니다. 화면이 「3초에 끝냈다」고 해도 서버가 판을 연 시각과 신고가 들어온 시각이 그걸 반박합니다.
  • 건너뛰기가 막힙니다 — 아직 안 열린 행성으로 출발하겠다고 하면 거기서 끊깁니다.

③ 서버가 문제를 낸다 — 연산용사 X

여기는 점수의 재료가 하나 더 있습니다. 답을 맞힌 수입니다.

화면이 숫자를 만들고 화면이 채점하면, 맞힌 수를 부풀리는 데 아무 장치도 필요하지 않습니다. 그래서 숫자를 서버가 내고 서버가 채점하도록 옮겼습니다. 그 길은 그날로 닫혔습니다.

그다음은 전투 자체였습니다. 두 안을 놓고 견줬습니다.

  • 매 턴 서버가 계산한다 — 화면은 서버가 낸 결과를 그리기만 합니다. 확실하지만 매 턴이 실패 지점이 됩니다. 앱으로 나가면 지하철과 엘리베이터를 겪는 사람이 생기고, 터널 하나에 판이 죽습니다.
  • 서버가 시드를 주고, 판이 끝난 뒤 다시 돌려 본다 — ④와 같은 방식입니다. 전투 중에는 네트워크를 타지 않습니다.

뒤의 것을 골랐습니다. 한 턴에 사람이 쓰는 시간이 10~30초라 지연은 이 게임의 병목이 아니고, 앱이 키우는 것은 지연이 아니라 끊김이라고 봤습니다. 시드를 받아 보고 마음에 안 들면 다시 받는 길(리롤)을 먼저 막았고, 지금은 이 방식을 단계적으로 붙이는 중입니다.

⚠ 그리고 어느 안이든 봇은 못 막습니다. 봇은 API를 「정상적으로」 부르면 됩니다. 이 단계가 막는 것은 결과 위조이고 자동화가 아닙니다. 둘을 같은 문제로 묶으면 끝이 없습니다.

④ 서버가 판을 처음부터 다시 돌린다 — 디펜스 킹덤

제일 비싸고 제일 확실한 단계입니다. 서버가 그 판을 다시 돌려 봅니다.

받는 것은 점수가 아닙니다. [틱, 명령] 기록과 판이 민 틱 수, 그것뿐입니다. 「몇 층까지 깼다」 「골드가 얼마 남았다」 「도감에 무엇이 올랐다」는 화면이 말하지 않습니다 — 서버가 같은 판을 처음부터 다시 돌려서 제가 낸 결과만 진행과 랭킹과 도감에 올립니다. 화면이 보낸 결과 칸은 아예 받는 자리가 없습니다.

이게 성립하려면 셋이 필요했습니다.

  • 시드를 서버가 준다 — 판을 열 때 서버가 정합니다. 화면이 고른 시드로 돌리면 같은 판을 다시 돌려 본 것이 아니라 그 사람이 고른 판을 돌려 본 것이 됩니다.
  • 엔진이 결정론적이다 — 같은 시드에 같은 명령을 넣으면 반드시 같은 결과가 나와야 합니다. 이 조건이 엔진 설계를 크게 묶습니다. 시계도 난수도 「지금」에 기대면 안 됩니다.
  • 판정이 순수 함수다 — 저장소도 HTTP도 안 씁니다. 그래서 같은 판정을 평소 점검에서 그대로 돌려 볼 수 있고, 「서버에서만 되는 계산」이 생기지 않습니다.

재현만으로는 아직 모자라서 곁다리 셋을 더 달았습니다.

  • 틱 상한 — 서버 CPU를 지킵니다. 틱 하나가 6~9마이크로초라 긴 판도 돌릴 만하지만, 끝나지 않는 판 (약한 말과 적이 서로 못 죽이는 상태)이 서버를 붙잡지 않게 상한에서 중단합니다.
  • 벽시계 하한 — 4배속으로도 그 틱 수를 이보다 빨리 돌릴 수 없다는 하한을 두고, 그보다 빨리 들어온 신고는 거부합니다. 판을 열어 두고 오프라인에서 계산해 바로 신고하는 길이 이 한 줄로 막힙니다.
  • 분당 판 열기 상한 — 좋은 시드가 나올 때까지 마구 여는 것을 늦춥니다. 새 스테이지에 도전하는 판은 도전권도 한 장 들어서, 값을 두 가지로 치르게 됩니다.

기록 한 판이 얼마나 되냐면, 무한모드 20분이 수백 KB 안입니다. 그래서 받는 본문 상한을 2MB로 두고도 넉넉합니다 — 점수 하나를 받는 것보다 비싸지만, 「믿을 수 있는 랭킹」의 값으로는 쌉니다.

네 단계를 한 줄로

단계서버가 쥐는 것막는 것
① 재화 정본판이 끝난 뒤 계좌에 오를 값재화 부풀리기
② 판 열기·시계언제 시작했나 · 무엇이 열렸나시간 부풀리기 · 건너뛰기
③ 서버가 출제무엇을 물었나 · 무엇이 정답인가맞힌 수 부풀리기
④ 판 재현판에서 일어난 일 전부규칙에서 나올 수 없는 결과

그래서 어디까지 믿을 수 있나

정직하게 적어 두면 이렇습니다. 순위표의 숫자가 「사람이 손으로 낸 것」이라는 보장은 어느 단계도 주지 못합니다. ④가 주는 보장은 그보다 좁고 분명합니다 — 그 숫자가 이 게임의 규칙에서 실제로 나올 수 있는 값이라는 것입니다. 봇을 막는 것은 전혀 다른 문제(조작 패턴을 보거나 기기를 검사하는 쪽)이고, 지금 우리 게임의 크기에서는 그걸 할 이유가 없습니다.

하나 더 배운 것은 ④는 나중에 붙이기 어렵다는 것입니다. 같은 시드로 같은 판이 나오려면 엔진이 처음부터 시계와 난수를 바깥에서 받는 순수한 계산이어야 합니다. 이미 돌아가는 게임에 붙이려면 엔진을 그 꼴로 떼어 내는 일부터 해야 해서, 랭킹을 붙일 게임이라면 첫날 정하는 편이 쌉니다.

어디에 먼저 비싼 방어를 붙일지는 「얼마나 안전한가」보다 「틀리면 무엇이 남나」로 보는 편이 맞았습니다. 일일·주간 랭킹은 때가 되면 새로 시작하니 틀린 기록도 같이 지워집니다. 반대로 레벨과 전체 랭킹은 쌓이기만 합니다 — 한 번 잘못 오른 숫자가 계속 맨 위에 서 있게 됩니다. 비싼 방어는 쌓이는 쪽부터입니다.

다른 노트

← 노트 목록