Html + CSS + Vanilla JS 만을 이용해 지뢰 찾기 게임 만들기 (2)

앞선 글에서 소개했던 조건들과, Index.html을 기준으로 계속해서 진행하겠다.

들어가며

시작에 앞서 이 글은 기본적으로 JS와 dom을 사용할 줄 안다고 가정하고 진행한다. 이를 감안하고 글을 읽으면 된다. 또, dom element에 접근할 때 추가적인 설명을 진행하지 않는다. 이전 글을 읽었다면, 따라 작성한 index.html을 옆에 켜두고, 아니라면 이전 글로 돌아가 index.html 코드 사진을 가져와서 같이 보자.

dom element에 class를 부여하고 제거하는 동작이 많이 나올텐데 이 부분은 해당 class가 부여되면 그와 관련된 css적인 요소가 추가되는 것이라고 생각하면 된다.

예시로는 blockDom에 flagged class가 추가되면 화면에 그 block에 깃발이 꽂혀있는것으로 보이고, 이 class를 제거하면 깃발이 사라지는 것이다.


이 글에선 game.js를 작성할 것이다. 즉 지뢰찾기 게임에 대한 logic이 여기에 들어갈 것이다. 주요 구현에 포함되어야 할 것들을 순서대로 알아가자!

  • 게임의 조건 판별
    기본 셋팅된 조건에 맞는 가로와 세로, 지뢰갯수가 정해졌는지 확인해야한다.

  • 지뢰찾기 판 만들기
    당연하다! 판이 있어야 게임을 할 것이다.

  • click에 대한 동작구현
    마우스 좌클릭과 우클릭을 구분하여, 칸을 열거나, 깃발을 꽂는 동작에 대한 구현이 필요하다.

  • 지뢰 배치하기

  • 게임 시간 기록하기
    지뢰찾기는 기록을 재는 게임이다.

  • 지뢰를 밟았을 때에 대한 동작

  • 게임을 클리어 했을 때의 동작

  • 게임을 재시작 할 때의 동작


0. 시작하기

다른 코드 구현들에 앞서 기본적으로 설정해두는 변수와 Dom element를 선언하고 시작하자

  • rows : 지뢰찾기 판을 만들고 그 안의 각각의 block을 관리하기 위해 필요
  • sec : 게임 플레이 초
  • onGame : game의 상태
  • flag : 깃발의 남은 갯수
  • resultText : 게임 종료후 문구

1. 게임의 조건 판별

이전 글에서 언급했지만 게임의 조건은 가로와 세로가 5~30 사이의 숫자로 정해지고, 지뢰의 숫자는 전체 지뢰판의 블록 수를 넘지만 않으면 된다.

조건의 판별은 게임 시작 버튼을 누르는 순간 시행되어야 한다. 즉 버튼의 click event에 해당 로직을 바인딩 시켜주면 된다.

이 부분의 코드는 어려울 게 없다. 차근차근 살펴보면
1. startButton에 'click' 이벤트가 들어왔을 때
2. 가로 길이(width.value)와 세로 길이가 5~30 사이 이고
3. mine의 수가 0과 가로 세로 곱 사이에 있다면!
4. introduction dom에 class 'hidden'을 추가하고
5. initGame(바로 이어서 작성할 것이다.)을 실행한다.
6. 조건을 충족하기 못하면 경고문을 띄운다.

introduction dom에 hidden을 추가하는 이유는 게임이 시작되면 input을 받는 부분은 보일 필요가 없기 때문이다!!

2~5 게임 전반 코드 작성(initGame 작성)

이 부분에서는 단순하게 판만 만들지는 않는다. 사실 내가 코드를 작성한 스타일에는 initGame이라는 function 안에서 판도 만들고, click event 지정도 하고, 시간도 잰다. 따라서 2번, 3번, 4번, 5번이 모두 한 function 안에 들어있다고(?) 할 수 있다. 차근차근 따라오자.


initgame function을 정의하며 시작하자

width, height, numMine을 파라미터로 받는다. 실행시에 앞서 설정한 변수들을 초기화 시켜주고, Html Tag안에 값들도 초기화 시켜준다.
rows = [] 에 2중 array 형식으로 각 row와 block이 담길 것이다.


지뢰찾기 판

전체적인 구조는 rows = [] 가 존재하고, height의 수 만큼 row = [] 를 생성해서 rows 안에 집어넣고, row 안에는 width 만큼의 block을 만들어서 집어 넣는다.

이 결과 rows는 (width만큼의 block 객체를 갖는 row)(height 만큼 가지고 있게 된다.)

  • Dom적으로 보면 rowDom이 존재해서 html의 game이 들어가야 하는 부분에 rowDom을 차근차근 추가해 나가는 것이고,
    gameBoard.appendChild(rowDom)
    그 각각의 rowDom안에 blockDom을 만들어 child로 넣어주는 것이다.
    rowDom.appendChild(blockDom)

block object의 구성요소

  • Dom element
  • 좌표
  • isMine : 지뢰여부
  • clicked : click 되었었는지 여부
  • flagged : 깃발이 꽂혀있는지 여부
  • willClicked : 로직을 줄이기 위하여 추가되었음.

결과

결과적으로 render된 html을 보며 다음과 같은 형태를 띈다.


click event handling

다음으로는 클릭에 대한 처리이다. 지뢰찾기에서는 마우스 좌클릭과 우클릭이 존재한다. 좌클릭의 경우 해당 칸을 여는 동작이고, 우클릭의 경우 깃발을 꽂거나, 꽂혀있는 깃발을 제거하는 동작이다.

  • 우클릭 이벤트 핸들링이다. 우클릭의 경우 개별 function을 따로 만드는 것이 아닌 callback 형식으로 바로 function 코드를 작성했다. 흐름을 따라가며 하나씩 살펴보자

    우선 마우스 우클릭은 'contextmenu'라는 event로 정의되어 있다. 이를 참고하자.

  1. 먼저 기본동작, 즉 마우스 오른쪽 클릭 시 메뉴창이 나오지 않도록 event객체(e)를 받아 preventDefault를 실행해준다. (Line. 82)
  2. 이미 block이 click 됐을 경우 아무것도 하지 않는다. (Line. 83)
  3. 깃발이 꽂혀있다면 깃발을 제거하는 로직을 실행한다. (Line. 84~87)
  • block의 flagged 변수를 false로 바꿔주고
  • 부여했던 flagged class를 제거하고
  • 남은 깃발 수에 더하기 1을 해줍니다.
  1. else -> 깃발이 꽂혀있지 않다면, 깃발을 꽂는 로직을 실행해줍니다. 3번과 정 반대입니다. (Line. 88~91)
  2. 마지막에 flagRemain dom element에 변경된 남은 깃발 수를 update 해줍니다. (Line. 93)

대망의 좌클릭 이벤트이다.

우선 getUnclickedNeighbors를 먼저 봐보자
이 코드는 block을 받았을 때 주변의 8개의 block들 중, click된 block과 willClicked 된 block을 제외한 block의 array를 return 해준다.

위 함수를 이용해서 handler function을 따로 만들어줬다.
1. click된 block이 이미 click 되었거나, 깃발이 꽂혀있으면 아무동작 하지 않는다. (Line. 121)
2. click한 block이 지뢰이면 게임을 종료시킨다. (Line. 122~125)
3. 위 경우에 포함되지 않을 경우 이제 block.clicked = true로 바꿔주고 clicked class도 추가해준다. (Line. 128~129)
4. 주변 지뢰의 수를 파악해서 지뢰가 없다면 주변 block들을 모두 click 시키고, 지뢰가 존재한다면 그 block에 text로 주변 지뢰 갯수를 넣어줍니다. (Line. 131~143)

이 부분이 핵심적인 내용이다. 집중해서 한줄 씩 알아보겠다.

주변에 지뢰가 없는 block을 눌렀을 때!!

const neighbors = getUnclickedNeighbors(block)
const neighborsMineNum = neighbors.filter(neighbor => neighbor.isMine === true).length // 주변 8개 중의 지뢰 갯수

우선 getUnclickedNeighbors(block)을 이용해 block 주변 8개의 block들 중 click 되지 않은 block 들의 array를 구한다.
그리고 filter를 통해서 주변 block들 중 지뢰의 갯수를 구한다.

if (neighborsMineNum === 0 ) {
  neighbors.forEach(neighbor => neighbor.willClicked=true)
  neighbors.forEach((neighbor) => {
    click_handler(neighbor)
  })

만약 지뢰 갯수가 0이면 주변 block들의 willClicked = true로 바꿔주고, click_handler()를 실행시켜 준다.
여기서 willClicked를 본격적으로 사용한다. 이 변수를 사용하는 이유는 neighbors에 click_handler를 실행시키면 결국 click_handler가 중첩되서 실행이 되기 때문이다. 여기서 willClicked가 없다면 발생 할 수 있는 문제의 예시를 보여주겠다.

주변에 지뢰가 없는 block을 클릭 후 주변 block array로 [a, b, c]를 받았다고 하자. 그러면 forEach 특성상 a에서 먼저 click_handler가 실행될 것이고, 그 안에서 다시 getUnclickedNeighbors를 통해 주변 array를 받아올 것이다. 아직 b에 대한 click_handler가 실행되지 않았기 때문에 b.clicked는 아직 false일 것이고 따라서 'a'의 주변 array에는 b가 포함되어 있을 것이여서 같은 block에 대해 반복적인 click이 계속되 결국 게임이 터져버리게 된다!!!

따라서 willClicked를 통해서 클릭이 예정되어 있다는 표시를 해줘서 이 부분을 관리하는 것이다. 앞선 forEach에서 willclicked를 true로 바꿔줘서 반복적인 getUnclickedNeighbors를 통해 return 되는 array에서 중복을 제거한다!!!

또 이부분에서 기억해야 할 점이있다. 원래는 forEach문 안에서 dispatch를 통해 click event를 발생시키려 했지만 이는 옳지 않은 접근 방식이었다. 이 경우 click event가 recursive하게 너무 많이 일어나 maximum call stack size excceded 에러가 난다. 꼭 기억하자!

  } else {
    block.blockDom.textContent = neighborsMineNum
    block.blockDom.classList.add('num' + neighborsMineNum)
  } 
  checkGameCleared()

나머지 부분은 간단하다. 주변에 지뢰가 하나라도 있다면 지뢰 숫자를 나타내주고 숫자의 색을 위해 class도 부여한다. 마지막으로 checkGameCleared()를 실행시켜 게임이 종료되었는지 확인한다.


지뢰 배치하기

어려울 게 전혀 없는 코드다
Math.random과 Math.floor를 통해 주어진 width와 height 사이에 존재하는 랜덤한 정수를 하나씩 뽑아내고, 그 자리에 지뢰를 배치시킨다!! random 하게 뽑은 좌표가 동일할 수 있으므로 n 변수를 이용해 이를 control 해준다!


게임 플레이 시간 기록

JS의 setInterval 함수를 이용한다. setInterval의 경우 첫번째 인자로 주어진 callback function을 두 번째 인자로 주어진 시간마다 실행시킨다. 이 코드의 경우 1000, 즉 1초를 의미한다. 따라서 1초마다 sec 를 1씩 키우고 timerText에 재 렌더링 시킨다.

마지막 한줄은 timer code는 아니지만, initGame의 마지막 코드로 hidden class가 주어졌던 inGame의 class를 ''로 초기화 시켜 보이도록 해준다

6. 지뢰를 밟았을 때

이 이후로는 이전의 click handler와 같이 어렵지 않다. 코드의 라인을 따라 천천히 살펴보자

1. 우선 게임이 끝났으니 timer를 없애 시간이 더이상 흐르지 않도록 한다.
2. 게임이 종료됐다는 경고창을 띄운다.
3. 게임 실패에 대한 결과문을 만들고, dom element의 주입시켜 게임 실패 문구를 띄운다.
4. 지뢰찾기판에 finished class를 추가해 click이 불가능하게 바꾼다.
5. 현재 게임의 상태를 false로 바꾼다.
6. 마지막으로 게임이 끝나면 모든 지뢰의 위치를 알려주므로 모든 지뢰인 block에 'mine' class를 넣어 지뢰의 위치를 표시해준다.

아주 직관적인 코드라 결과 사진을 띄우고 끝낸다.

7. 게임을 클리어 했을 때

클리어 했는지를 확인하기 위해선 지뢰가 아닌 모든 칸이 클릭되어 열렸는지를 확인하면 된다.
1. 그래서 우선 모든 block을 돌며 isMine===false이며 clicked===true인 block의 수를 센다.
2. 그리고 선 지뢰가 아닌 숫자의 수와 앞서 구한 n이 동일하고, onGame이 true일 경우
3. 위 지뢰를 밟았을 때와 유사하게 게임을 종료시키고 클릭 불가능하도록 class 추가해주고, 문구를 띄워주면 된다.

8. 게임을 재시작할 때을 재시작할 때

따로 한 부분을 떼어내서 설명하는게 민망할 정도로 단순하다.

마무리

게임의 구현은 다 끝났다. 다만 이상태로 작성한 코드를 단순히 열어보면 아아아아주 이상할 것이다. CSS 적용을 하지 않았기 때문이다. 따라서 다음 시간에 마지막으로 CSS 적용을 하고 글을 마치겠다.

좋은 웹페이지 즐겨찾기