구구단 게임 만드는 법 — 설명 없는 게임 설계
구구단 게임 만드는 법을 기록한다. 처음 시킨 건 “국민게임 다 섞어봐”였는데, 완성된 건 곱셈식이 떨어지는 게임이다. 그 사이에 설계를 세 번 갈아엎었다. 트로트 자판기에 이은 두 번째 실험이고, 결과물은 구구단 드롭에서 바로 해볼 수 있다.
1차: 다섯 개를 한 판에 밀어 넣기
스도쿠, 지뢰찾기, 구구단, 테트리스, 카드 짝맞추기를 하나의 규칙으로 묶어봤다. 스도쿠 판 위에 테트리스 조각을 놓는데, 판 밑에 지뢰가 숨어 있고, 지뢰를 밟으면 구구단 문제로 해체하고, 지뢰 위치는 시작할 때 잠깐 보여주고 숨겨서 기억력을 쓰게 하는 구조였다.
설계로만 보면 그럴듯했다. 다섯 개가 억지로 붙은 게 아니라 서로 물려서 돌아갔으니까. 문제는 사람이 이해하는 데 드는 비용이었다. 규칙 설명 화면을 열면 스크롤이 생겼다. 게임을 시작하기 전에 공부를 해야 하는 게임이 된 것이다.
돌아온 반응은 한 줄이었다. “이거 너무 복잡해.”
2차: 세 개로 줄였는데 운빨이 남았다
카드 짝맞추기에 구구단과 지뢰찾기를 얹었다. 카드를 뒤집어서 4×6과 24를 맞추고, 가끔 나오는 지뢰를 밟으면 하트가 깎이는 게임. 이건 설명이 필요 없었다. 뒤집힌 카드가 깔려 있으면 누구나 짝을 맞출 거라고 생각하고, 폭탄이 나오면 누구나 나쁜 거라고 안다.
그런데 재미가 없었다. 지뢰가 4개고 하트가 3개인데, 지뢰를 밟으면 카드가 도로 뒤집혀서 같은 지뢰를 세 번 밟고 죽을 수 있었다. 정보가 아무것도 없는 초반에 순전히 운으로 죽는 것이다.
1차 수정은 하트를 늘리고 밟은 지뢰를 열어두는 거였다. 같은 지뢰로 반복해서 죽지는 않게 됐다. 그래도 반응은 똑같았다. “너무 운빨이 심하다.”
진짜 원인: 벌칙만 가져오고 재미를 두고 왔다
여기서 원인을 잘못 짚고 있었다는 걸 알았다. 숫자를 조정할 문제가 아니었다.
지뢰찾기의 본질은 지뢰가 아니라 숫자 단서다. “주변에 지뢰 2개”라는 정보로 안전한 칸을 추론해 나가는 게 그 게임의 전부고, 첫 클릭 이후로는 사실상 논리 퍼즐이다. 운이 아니라 추론으로 이기는 게임이란 얘기다.
그런데 내가 가져온 건 폭탄과 목숨 감소, 즉 벌칙뿐이었다. 추론에 쓸 단서는 통째로 버렸다. 재미는 두고 오고 처벌만 챙겨온 셈이니 운빨만 남는 게 당연했다. 하트를 늘린 건 그 위에 반창고를 붙인 거였고.
3차: 운이 낄 자리가 없는 구조
그래서 숨긴 정보를 아예 없앴다.
떨어지는 블록에 6×7이 적혀 있고, 화면 아래에는 답 다섯 개가 깔려 있다. 그중 하나가 42다. 열을 눌러 블록을 그리로 옮기고, 맞는 칸에 안착하면 사라진다. 틀리면 그 자리에 쌓이고, 천장에 닿으면 끝.
운이 낄 자리를 구조적으로 막았다.
- 화면에 모든 정보가 처음부터 보인다. 숨긴 게 하나도 없다
- 떨어지는 식의 답은 항상 다섯 개 라벨 중 하나다. 답이 없는 문제는 나오지 않는다
- 시작 열은 절대 정답 열이 아니게 강제했다. 가만히 둬서 얻는 공짜 점수가 없다
실수를 만회할 장치도 넣었다. 잘못 쌓인 기둥은 그 열의 정답을 맞출 때마다 한 칸씩 줄어든다. 한 번 틀렸다고 사형선고가 아니라 계속 만회할 기회가 열려 있는 것이다. 2차에서 지뢰가 준 좌절의 정확히 반대편이다.
여기까지 만들고 나서 또 지적을 받았다. 답 숫자가 고정이면 위치를 외워버려서 나중엔 암산이 아니라 패턴 반사가 된다는 것. 그래서 한 숫자를 두 번 맞추면 그 자리가 새 숫자로 갈리게 했다. 두 번은 4×9와 9×4, 즉 교환법칙 양쪽으로 한 번씩 나온다. 한 번 맞춘 숫자 밑에는 작은 점이 찍혀서, 한 번 더 맞추면 바뀐다는 걸 글자 없이 알려준다.
삽질 1: 변수 이름 하나로 스크립트가 통째로 죽었다
숫자 교체 기능을 넣고 새로고침했더니 게임이 아무 반응이 없었다. 브라우저 콘솔에는 에러가 한 줄도 안 찍혔다.
콘솔에서 변수를 찍어보니 함수가 전부 undefined였고, labels는 배열이 아니라 DOM 엘리먼트로 잡혔다. 이게 결정적 단서였다. id가 있는 요소는 전역에 잡히니까, 스크립트가 아예 실행되지 않았다는 뜻이다.
원인은 새로 만든 변수 이름 f가 기존의 const f = $('#faller')와 겹친 거였다. 문법 에러라서 스크립트 로드 자체가 실패했고, 그래서 런타임 에러가 안 찍힌 것이다. 콘솔이 조용하다고 멀쩡한 게 아니었다.
스크립트만 떼어내 node --check로 돌리니 한 방에 잡혔다.
SyntaxError: Identifier 'f' has already been declared
삽질 2: 디버깅 흔적을 안 지워서 멀쩡한 걸 버그로 오해했다
좌우 이동을 검증하는데 블록이 자꾸 떨어져서 방해가 됐다. 그래서 콘솔에서 낙하 속도를 0으로 세워놓고 테스트했다. 그리고 그걸 되돌리지 않았다.
잠시 뒤 “이제 왜 자동으로 안 내려오냐”는 말이 나왔다. 파일은 멀쩡했다. 살아 있는 페이지에 내 디버깅 값이 남아 있었을 뿐이다. 게다가 프리뷰 창이 문서를 붙잡고 있어서 새로고침해도 그 상태가 유지됐다.
검증하려고 상태를 건드렸으면 반드시 되돌려야 한다. 안 그러면 멀쩡한 결과물을 버그로 오해하게 만든다.
사이트에 붙이면서
게임은 단독 HTML로 먼저 만들고 나중에 사이트로 옮겼는데, 옮기는 과정에서 두 군데가 걸렸다.
하나는 스타일. Astro는 컴포넌트의 <style>을 자동으로 스코프 처리하는데, 이 게임은 답 라벨과 쌓인 블록을 JS가 만든다. 나중에 생긴 요소에는 스코프 표시가 안 붙어서 CSS가 먹지 않는다. 그래서 게임 내부 스타일만 전역으로 두고 셀렉터마다 .tt 접두사를 직접 달아 스코프를 잡았다.
다른 하나는 버튼. 원래 onclick="move(-1)" 방식이었는데 이건 함수가 전역에 있어야 동작한다. 게임 전체를 즉시실행 함수로 감싸서 전역을 안 쓰도록 바꾸고, 버튼은 addEventListener로 다시 연결했다. 덕분에 사이트 전역에 게임 변수가 한 개도 새지 않는다.
결과
구구단 드롭에서 해볼 수 있다. 규칙 설명은 페이지 어디에도 없다. 보라색 곱셈식이 떨어지고 아래에 청록색 숫자 다섯 개가 있으면, 뭘 해야 하는지는 보면 안다.
10개 맞출 때마다 속도가 붙는다. 키보드는 좌우 방향키와 아래 방향키를 쓰고, 폰에서는 그냥 답이 적힌 열을 누르면 된다.
소감
이번에 배운 건 기능을 더하는 것보다 뭘 빼야 하는지 아는 게 어렵다는 거다.
다섯 개를 섞은 1차는 설계로만 보면 제일 정교했다. 다섯 게임이 서로 물려 돌아갔으니까. 그런데 아무도 안 하고 싶은 게임이었다. 반대로 지금 버전은 규칙이 한 줄이고, 그래서 그냥 하게 된다.
그리고 “재미없다”는 말을 들었을 때 숫자부터 만지면 안 된다는 것. 지뢰 4개에 하트 3개가 문제가 아니라 추론할 단서가 없는 게 문제였다. 원인을 잘못 짚으면 아무리 조정해도 제자리다.