← 개발일지

사과게임을 대전으로 바꾼 이유, 결! 성공률을 정한 방법

원래 있던 게임

합이 10인 사과를 지워 점수를 내는 규칙 자체는 한국에서 이미 유명한 싱글 퍼즐(사과게임/Fruit Box)이다. 혼자 하는 게임이라 최고점을 갱신하는 재미가 전부고, 상대와 겨루는 구조는 없었다.

그 규칙에 실시간 1:1을 얹었다. 같은 시드로 같은 보드를 두 사람에게 주고 누가 더 잘 지우는지 겨루는 형태다. 랭크(MMR)는 이 대전 구조 위에 나중에 붙었다. 혼자 하는 퍼즐엔 승패가 없다.

GPT 웹챗으로 짜다가 멈췄다

초기 개발은 GPT 웹챗에 코드를 붙여넣고 고치는 방식이었다. 싱글로 시작해서 규칙 변형을 붙이고, 1:1 멀티를 붙이고, 소형 ELO를 구상하고, 코드 기반으로 최대 4인까지 늘렸다.

연결 구조를 star topology로 바꾸는 작업 중에 1:1 멀티가 Supabase RLS 정책 변경으로 깨졌다. 원인을 일부 고치다가 멈췄고, 그 상태로 1년 가까이 방치됐다.

2026-07-07에 Claude Code 하네스를 붙이고 다시 시작했다. 코드와 남은 기억, 레거시 PRD를 대조해 스펙부터 다시 시딩했다.

이름을 두 번 바꿨다

처음 확정한 공식 이름은 사과배틀: 결!이었다. 사과게임이 이미 굳은 관용명이라 차별점이 안 산다는 이유였다.

얼마 뒤 사과게임: remix로 다시 바꿨다. 관용명의 인지도를 SEO·검색 유입에 쓰고, 차별점은 remix 한 단어에 맡기는 쪽으로 판단이 바뀌었다. 브랜드 타이틀과 모드 이름(클래식/마라톤/럼블)·내부 식별자(classic/nahon/gyul)를 애초에 분리해뒀던 덕에 이름 교체 작업은 URL·DB 스키마·엣지 함수를 안 건드렸다.

결!은 벳이다

럼블은 공유 보드 레이스다. 두 사람이 같은 판에서 합이 10인 조합을 서로 먼저 지운다. 남은 조합이 없다고 판단되면 '결!'을 선언할 수 있다 — checkGyulFast(grid).found === false, 즉 더 지울 게 없는 상태에 걸었다는 뜻이다.

맞으면 팟을 받고 즉시 끝난다. 틀리면(아직 조합이 남아있으면) -2점에 팟이 1 커지고 10초 쿨다운이 붙는다. 팟은 15에서 시작해 누가 틀릴 때마다 커진다.

보너스를 받았는데 점수는 그대로였다

GameResultModal을 다시 짜다가 결함 두 개가 드러났다. 하나: 성공 시 자기 쪽 finalScores는 '보너스만' 담고, 상대에게 전달되는 값은 '보너스를 포함한 총점'이었다 — 같은 필드가 관점에 따라 다른 걸 의미했다.

둘: 승패 판정과 화면 표시가 쓰는 값(preGyulScores)에 실패 페널티(-2)는 반영되는데 성공 보너스는 안 들어갔다. 그래서 '결 성공 +15' 배지가 뜨는데 점수도 승패도 그대로인 상태가 나왔다.

고친 방식은 보너스를 preGyulScores에 접는 것 하나였다. 실패 페널티가 이미 거기 들어가고 있었으니 대칭만 맞추면 됐다. endGame과 모달 둘 다 이 값 하나만 본다.

성공률이 1% 밑이었다

180초 제한에 10×17 보드에서 실측 성공률이 1% 미만이었다. 목표는 150초로 줄이면서 성공률을 8~12% 대역으로 올리는 것. 코드를 바로 건드리지 않고 먼저 봇 시뮬레이션을 짰다.

완전탐색 봇(매 수마다 checkGyulFast가 찾는 첫 사각형을 무조건 제거)을 mulberry32 보드 생성 로직 그대로 이식해서, 이동당 몇 초 걸린다고 가정해야 실측 <1%가 재현되는지부터 캘리브레이션했다. msPerMove 3000ms면 성공률 99.7%, 5700ms면 0.65%로 실측과 가장 가까웠다.

봇이 너무 똑똑했다

이 완전탐색 봇으로 150초에 8×14(112칸) 보드를 돌리니 성공률 28.6% — 목표의 3배가 나왔다. 그런데 이 봇은 매 순간 보드 전체를 훑어 사각형을 무조건 찾는다는 가정이 있다. 실제 사람은 큰 사각형을 눈으로 잘 못 찾고, 찾아도 놓치고, 가끔만 전체를 다시 훑는다.

그래서 탐색 반경 제한·놓침 확률·가끔 전체 재훑기 세 가지를 넣은 두 번째 모델을 만들었다. 같은 <1% 앵커에 다시 맞춘 뒤 150초 스윕을 돌리니 8×14가 6.4~6.7%로 이번엔 밴드 밑으로 떨어졌다. 모델을 바꾸는 것만으로 같은 보드 크기의 예측이 28.6%에서 6.4%까지 갈렸다.

8×13으로 정한 이유

두 모델 다 만족하는 지점을 찾아 8×13(104칸)으로 정했다 — 두 번째 모델 기준 8.9%로 밴드 안, 원래 보드 비율(10×17)에서도 가장 덜 벗어난 크기였다. nahon·gyul 두 모드만 이 크기로 줄이고 classic(10×17)은 그대로 뒀다.

보드 생성 함수가 크기를 하드코딩하고 있어서 시드·행·열을 받는 형태로 바꿔야 했고, 버전을 올려 구버전 고스트 기록이 새 보드 크기에 잘못 재생되지 않게 필터를 추가했다. 시뮬레이션 리포트에는 이 숫자가 캘리브레이션 파라미터에 상당히 민감하다는 경고를 남겨뒀다 — 실측 플레이테스트 없이 확정값으로 쓰지 말라고.