나는 자바스크립트를 책으로만 공부했다. 자바스크립트의 문법과 개념 이해에만 주력하고 정작 중요한 프로젝트는 하나도 진행하지 않았었다.『모던 자바스크립트 Deep Dive』라는 책을 N회독하면서 실행 컨텍스트, this바인딩, ES6+의 문법을 나름 잘 이해했다고 생각했고, 그렇게 자신감에 찬 상태로 첫 프로젝트를 시작하게 되었다.
💣 지뢰피하기
https://avoidmine-gvwrc.run.goorm.io/
지뢰피하기는 키보드로 조작하는 지뢰찾기(MineSweeper)라는 컨셉으로 구상한 게임이다. 기존의 지뢰찾기 게임은 마우스로 지뢰가 있는 칸에 깃발을 꽂고 지뢰가 아닌 모든 칸을 다 열면 승리하는 방식이지만, 이 게임은 시작점과 도착점이 있고 WASD키로 자신의 캐릭터를 움직여 지뢰를 피해 도착점으로 들어가야 승리하는 방식이다. 지뢰피하기는 지뢰찾기에서 "지뢰를 건드리면 패배하고, 각 칸의 지뢰 여부를 주변에 지뢰가 몇 개 존재한다는 방식으로 알려준다"는 개념을 차용하여 새롭게 변형시킨 게임이다.
키보드로 조작하는 방식 때문에 지뢰찾기와는 큰 차이점이 있다.
1. 지뢰찾기는 마우스로 클릭하여 숫자 힌트를 볼 수 있지만, 지뢰피하기는 캐릭터를 움직여서 캐릭터 근처의 숫자 힌트만 볼 수 있다. 지뢰의 위치를 찾아내려면 숫자 힌트가 필요한데, 이 숫자 힌트를 보는 것 자체에 큰 제약이 있다.
2. 지뢰찾기의 숫자 힌트는 지뢰가 없는 칸에만 있지만, 지뢰피하기는 모든 칸에 숫자 힌트가 존재한다. 따라서 지뢰찾기에서 주변에 지뢰가 하나도 없으면 빈 칸이지만 지뢰피하기에서는 숫자 0이 표시된다.
3. 지뢰찾기는 (아마도?) 첫 클릭 직후에 지뢰의 위치가 정해진다. (첫 클릭에 지뢰가 터진다면 어이가 없지 않은가...) 지뢰피하기는 지뢰의 위치가 이미 결정되어 있고, 최소한의 움직임을 보장하기 위해 시작점 포함 9칸이 공개된다.
4. 지뢰피하기는 반드시 시작점에서 도착점으로 도달하는 길이 존재한다. 따라서, 숫자 힌트 외에도 반드시 길이 존재한다는 점이 지뢰 위치를 찾아내는 또 다른 힌트가 된다.
5. 지뢰찾기는 모든 지뢰를 다 찾아내야 하지만, 지뢰피하기는 그냥 도착점에 도달하기만 하면 된다. 모든 지뢰의 위치를 다 찾아낼 필요도 없으며, 실제로 그렇게 하는 것이 (어려운 스테이지에서) 매우 어렵다. 그냥 도착점으로 가는 안전한 길을 찾기만 하면 성공인 것이다. (이미 여러 제약 때문에 난이도가 높기 때문에 이러한 점이 난이도를 많이 낮춰줄 수 있다고 생각한다.)
🔍 아이템과 특수한 힌트들
이런 퍼즐류 게임은 난이도가 너무 높으면 보통 사람들이 플레이하기가 꺼려질 수 있다. 하지만 퍼즐류 게임을 좋아하는 사람들에게는 이 게임은 그렇게 어려운 편은 아니다. 그래서 아이템을 도입해서 아이템으로 칸과 숫자 힌트 공개를 도와서 난이도를 낮출 수 있게 하고, 아이템을 사용하지 않으면 보너스 점수를 주는 방식을 채택했다.
이 외에도 점수를 얻는 방식에 변화를 주어 클리어가 목적인 사람들과 고득점이 목적인 사람들이 각자의 방식으로 게임을 플레이할 수 있도록 하였다. 물론 이러한 노력이 의미가 있으려면 LeaderBoard를 도입해서 경쟁할 수 있도록 해야 할 것이다.
게임이 진행되면서 점점 난이도를 높여나가야 하는데, 처음에 생각한 방법은 지뢰 비율의 증가와 칸 개수의 증가뿐이었다. 그러나 칸 개수는 가독성 때문에 어느 정도 한계가 있었고, 반드시 길이 존재해야 한다는 조건 때문에 지뢰 비율을 증가시키면 지뢰의 배치가 굉장히 단조롭고 뭉쳐진 지뢰 때문에 칸의 개수가 많아져도 의미 없는 칸이 되어버린다.
그래서 새로운 힌트를 게임이 진행되면서 하나씩 등장시키면서 복잡도를 더하였다.
예를 들면 빨간색 숫자는 가로 5칸의 지뢰 개수를 의미하고, 노란색 숫자는 십자가 모양 5칸 내의 지뢰 개수를 의미한다.
그리고 일부 칸은 숫자가 아니라 "홀짝"으로 표시되도록 하고, 이러한 칸들의 비율로 난이도를 조절하였다.
마지막 스테이지에 도달하는 과정에서 이러한 요소들이 누적되어 점점 생각해야될 것이 많아진다.
사실 지뢰피하기는 이미 1년 전에 Python의 Pygame으로 만들어 본 경험이 있다. 더 거슬러 올라가 3년 전에 군대에서 엑셀(정확히는 한셀) 매크로로 만들었었다. 군대에서 엑셀로 만들었던 것 중에 가장 애착이 갔었기에, Python을 배우고 나서 바로 지뢰피하기부터 만들었었다. 그러나 그 때는 정말 조건문과 반복문 정도만 아는 상태로 무작정 만들었기 때문에 결과물이 만족스럽지 못했다. 이후에 자바스크립트를 배우면서 어느 정도 익숙해지면 바로 지뢰피하기부터 다시 만들어봐야 겠다고 생각했다.
💣 지뢰 배치 알고리즘 💣
이번에 지뢰피하기를 만들면서 가장 신경 썼던 부분은 지뢰를 배치하는 방식이다.
예전에 Python으로 만들 때에도 지뢰 비율이 늘어날 때 나타나는 단조로운 길 모양을 해결하지 못했었다.
특히, 시작점에서 도착점까지의 안전 경로를 먼저 생성하고 지뢰를 배치하는 방식을 택했기 때문에, 안전 경로 주변에 지뢰들이 뭉쳐있는 형태가 반복되었다.
그래서 일단 지뢰를 배치하고 길을 뚫어주는 방식으로 바꿔보기로 했다.
더 개선이 필요한 불완전한 방식이지만 내가 생각해낸 방법은 아래와 같다.
1. 시작점과 도착점을 제외한 칸에 지정된 개수만큼 지뢰를 랜덤하게 흩뿌린다.
2. 지뢰 9개가 뭉쳐 있는 칸이 있으면 그 9칸의 지뢰 중 무작위로 하나를 제거하고, 제거된 지뢰 배열에 추가한다.
3. 그래프 탐색으로 이동 가능한 칸들의 덩어리(연결 요소)들을 분류한다.
4. 연결 요소가 단 1개가 될 때까지(모든 안전한 칸들이 연결될 때까지) 아래의 내용을 반복한다.
4-1. 두 개 이상의 덩어리와 접하는 지뢰를 찾는다.
4-2. 접하는 덩어리들이 동일한 지뢰들 중 랜덤하게 하나씩 제거하고, 제거된 지뢰 배열에 추가한다.
예를 들어 1번 덩어리와 2번 덩어리와 동시에 접하는 지뢰가 두 개 이상 있다면 그 지뢰들 중 하나를 골라 제거한다.
만약 1번, 2번, 3번 덩어리와 접하는 지뢰와, 1번과 2번 덩어리와만 접하는 지뢰가 있다면, 1번과 2번 덩어리와 접하는 지뢰는 무시한다.
4-3. 제거된 지뢰들과 단 한 번도 접하지 않았던 덩어리가 있다면, 그 덩어리는 고립된 덩어리이다.(두 겹 이상의 지뢰들로 감싸져 있는 상태) 이러한 덩어리들은 내부에서 접하는 지뢰들 중 무작위로 하나를 제거하고, 제거된 지뢰 배열에 추가한다.
5. 지금까지 지뢰가 제거되었던 모든 칸을 제외한 나머지 칸에 지금까지 제거된 지뢰의 개수만큼을 무작위로 흩뿌린다. 이 때, 반드시 시작점에서 도착점으로 가는 경로가 존재하는지를 그래프 탐색으로 매번 확인하면서 지뢰의 위치를 결정한다. (경로가 존재하는 것이 제거된 만큼의 지뢰를 다시 뿌리는 것보다 우선하므로 지뢰 할당량을 다 채우지 못할 수도 있다.)
이 방법으로 지뢰의 배치가 어느 정도 균등해지고, 내가 원했던 모양(도착점으로 가는 가능성 있어 보이는 여러 경로가 존재)이 잘 나오게 되었다.
🔧 개선할 점들
처음으로 배포라는 것을 하면서 heroku도 사용해보고 express로 간단하게 서버도 만들어 보았다. 처음에 구현만을 목적으로 만들었기 때문에 배포했을 때 생겨날 문제점들을 하나도 고려하지 않았다.
먼저 클라이언트 쪽에 모든 소스가 다 공개되어 있다. 따라서, 콘솔에서 게임 내 모든 값들이 전부 조작이 가능하다. 뷰를 분리시키고 게임에 필요한 모든 값들을 서버에 요청하여 db에서 불러오도록 바꾸어야 한다.
(21.09.05. 시도해보았으나 매 클릭마다, 키를 누를 때 마다 fetch를 하는데 시간이 걸려서 게임 플레이가 답답해지고, 사용자가 늘어나면 감당할 수 없을 것 같다... 구글링을 하면서 서버나 DB에 대해서 내가 정말 아는 것이 없다는 것을 깨달았다. 기초적인 것부터 차근차근 다시 공부해봐야겠다.)
두 번째로 아직 랭킹 시스템을 만들지 못했다. 처음에 서버를 만들줄도 모르는 상태에서 시작했기 때문에 그렇다. 앞으로 mongoDB를 활용하여 랭킹 시스템을 도입해보려고 한다.
세 번째로 UI가 너무 구리다. 원래 미적 감각이 썩 좋지 않아서 어떻게 개선해야 할지 잘 모르겠다. 이 부분은 일단 위의 두 내용을 성공적으로 해낸 후에 고민해 봐야겠다.