ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • JavaScript 입문 : 지뢰찾기 게임 - 에러 수정 (화면 중복 생김 방지, 지뢰 폭발후 시간 흐름 방지, Placeholder 화면 초기화) 및 게임 시작시 줄, 칸, 지뢰갯수 입력하기
    컴퓨터 알아가기/JavaScript 2022. 11. 4. 19:30
    728x90
    반응형

    이 글은 제로초  TV 자바스크립트 강좌를 기본으로 공부하고 있습니다. 

     

    지금까지 지뢰찾기 게임에 대해 코드를 완성 하였습니다. 게임을 하면서 나욌던 에러에 대해 수정 보완 하도록 하겠습니다. 먼저 게임에 대해 조금 그림으로 이해하기 쉽게 textContent 입력칸에 지뢰를 'X', 깃발을 '!'로 표시한 것을 이모지로 대신해서 다음과 같이 그림화 하였습니다.  

     

     

    그리고 이제는 게임의 난이도를 즐기기 위해서 먼저 가로줄, 세로줄 그리고 지뢰갯수를 직접 입력하면서 하는 방식을 만들어 보도록 하겠습니다. input 태그를 이용하여 진행하도록 하겠습니다. 

     

    1. 가로 세로 지뢰 갯수 input태그 이용 입력하기

     

    HTML에 <form>태그를 만들고 자바스크립트 form 이벤트인  'submit' 이벤트를 활용합니다. 

     

    ① HTML form 태그 만들기

     

     

    상기 form 태그에 대해 설명하면 placeholder는 input창에 간단한 설명이 나오고 각 id가 자바스크립트에서 데이터랑 연결하는 역할을 합니다. 사이즈는 최대 입력 크기를 말하고요.

     

    ② 자바스크립트와 연결

     

    자바스크립트가 작동이 되기 위해서는 document로 form태그를 연결합니다. 

     

     

    ③ submit 이벤트 작성 

     

    form태그를 선택 하였으니 submit이벤트 함수를 만들어 줍니다. 

     

     

    onSubmit(  ) 함수에서 가로 세로 지뢰갯수를 입력하는 식을 만드는데 target값에 접근하는 방식을 참조하기 바랍니다. 

    여기서 보면 모든 기본 순서가 이 곳에 표현이 되야합니다. 

     

    게임의 흐름을 생각해 보면 

    사용자가 가로 세로 지뢰의 숫자를 입력하고 엔터나 클릭을 하면 화면에 게임판이 그려지겠죠. 그러면 게임이 시작이 되어야 하고 (startGame) 이 순간 타이머도 작동이 되야합니다. 당연히 openCount(  )도 여기에 추가를 해줘야 합니다. 다음과 같이 추가 코딩할 수 있습니다. 

     

     

    2. 에러 수정

     

    ① 중복 생성 제거

     

    이제 브라우저를 보면 가로 세로 지뢰 갯수를 입력하는 이벤트가 발생이 되고 숫자를 입력하면 초기 화면이 그려집니다. 에러가 있는지를 확인할 차례입니다. 편의상 5개줄로 생성하고 보겠습니다. 엔터를 무심코 2번 쳤더니 다음과 같이 2번 게임판이 생성되네요. 

     

     

    이런 경우는 가로 세로 지뢰를 입력하고 화면을 초기회 시키면 해결이 됩니다.

     

     

    ② placeholder 초기화 

     

    게임을 진행해 보겠습니다. 끝까지 해 보죠. 

     

     

    상기 게임 결과를 보면 결과화면이 나오고 숫자 입력창을 보면 기존에 입력하였던 5가 그대로 남아 있습니다. 사실 에러는 아니지만 이 부분을 게임이 시작되면 다시 원래  placeholder 값으로 바꾸고 싶네요. 정말 일반 개발자들은 쉽겠지만 한참 생각했습니다. 

     

    다음과 같이 수정해 주니 되네요. 

     

     

    브라우저를 보면 다음과 같이 초기화 되어 다음 게임을 원하는 숫자로 할 수 있습니다. 

     

     

    ③ 지뢰폭발후 시간 흐름 방지

     

    이번에는 지뢰가 터지는 경우도 검증해 봐야겠네요.  다음 그림과 같이 지뢰가 터졌는데 시간은 계속 흐르고 있습니다. 

     

     

    이를 수정하기 위해서 지뢰가 터진 코드에서 clearInterval(  )을 사용해야 할 듯 합니다. 게임종료 코드로 이동하여 다음과 같이 추가합니다. 

     

     

    ※ 글로 표현하다 보니 순서상 놓친 부분이 있는데 이렇게 수정을 할 경우 외부에 const 변수는 향후에도 사용할 수 있게 전부 let 변수로 바꾸어 줍니다. 

     

     

    3. 실전 게임해 보기 

     

    이제 다음과 같이 개발자 모드를 주석처리하고 15줄을 만들어 보겠습니다. 

     

     

     

    이렇게 게임을 시작하면 되네요. 완성된 코드는 다음과 같습니다. 

     

    <!DOCTYPE html>
    <html lang="ko">
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Minesweeper Game</title>
    </head>
    <style>
      table {border-collapse: collapse;} /* 칸 경계선 이중없앰 */
    
      td {
        border: 1px solid #bbb;
        text-align: center;
        line-height: 20px;
        width: 20px;
        height: 20px;
        background-color: #999;
      }
    
      td.opened { background-color: white; } /* 열린칸 */
      td.flag { background-color: green; }
      td.question { background-color: orange; }
    </style>
    <body>
      <!-- 가로 세로 지뢰의 수를 직접 입력하여 진행, form태그 input태그 -->
      <!-- form을 사용하면 submit 사용 -->
      <form id="form">
        <input placeholder="가로 줄" id="row" size="5"></input>
        <input placeholder="세로 줄" id="cell" size="5"></input>
        <input placeholder="지뢰 개수" id="mine" size="5"></input>
        <br><br>
        <button>생성</button>
      </form>
    
      <div id="timer">0</div>
    
      <table id="table">
        <tbody></tbody>
      </table>
    
      <div id="result"></div>
    
      <script>
        // form 1. form태그 선택
        const $form = document.querySelector('#form');
    
        // 1. HTML 활성화 (자바스크립트로 연결)    
        const $timer = document.querySelector('#timer');
        const $tbody = document.querySelector('#table tbody'); // tr, td가 기입예정
        const $result = document.querySelector('#result');
    
        /* // 2. 가로 세로 지뢰의 수 
        const row = 10; // 줄
        const cell = 10; // 칸
        const mine = 10; // 지뢰 */
    
        // form 2-2 변수 변경 
        let row;
        let cell;
        let mine;
    
        // 3. 종류에 대한 코드명과 코드숫자 넣기 
        const CODE = {
          NORMAL: -1, // 닫힌칸 (지뢰없음)
          QUESTION: -2, // 물음표칸 (지뢰없음)
          FLAG: -3, // 깃발칸 (지뢰없음)
          QUESTION_MINE: -4, // 물음표칸 (지뢰있음)
          FLAG_MINE: -5, // 깃발칸 (지뢰있음)
          MINE: -6, // 닫힌칸 (지뢰있음)
          OPENED: 0, // 열린칸, 0이상이면 (8까지) 모두 열린칸 
        }
    
        // 4. 변수 묶기     
        let data;
        let openCount;
        let startTime;
        let interval;
    
        // form 2-1. onsubmit()
        function onSubmit(event) {
          // submit 이밴트는 자동 새로고침되는 기능을 막아야 함
          event.preventDefault();
          // event.target을 하면 form이 됨, id로 다가섬
          row = parseInt(event.target.row.value); // row 값에 접근
          cell = parseInt(event.target.cell.value); // cell 값에 접근
          mine = parseInt(event.target.mine.value); // mine 값에 접근, 상단 변수 let으로
    
          // 에러수정 2. placeholder 입력 후 초기화
          event.target.row.value = '';
          event.target.cell.value = '';
          event.target.mine.value = '';
          
          // 에러수정 1. 밑에 화면이 더 생김
          $tbody.innerHTML = '';
          
          openCount = 0;
          
          // from 3. 입력 받으면 startGame()
          startGame();
          
          // form 4. 게임시작되면 interval 
          startTime = new Date();
          interval = setInterval(() => { 
          // new Date(): 현재 시간, startTime: 시작한 시간
            const time = Math.floor((new Date() - startTime) / 1000); 
            $timer.textContent = `${time}초`;
          }, 1000);
        }
    
        // form 2. sumbit 이벤트
        $form.addEventListener('submit', onSubmit);
    
        // 5. 게임의 흐름 
    
        // 5.3 
        function plantMine() {
    
          // 10 X 10 테이블 각각 칸을 위한 인덱스 만들기 
          const candidate = Array(row * cell).fill().map((e, i) => {
            return i;
          });
          console.log(candidate);
    
          // 지뢰에 연결할 랜덤 10개 뽑기
          const shuffle = [];
          for (let i = 0; i < mine; i++) {
            const random = Math.floor(Math.random() * candidate.length);
            const choice = candidate.splice(random, 1)[0];
            shuffle.push(choice);
          }
          console.log(shuffle);
    
          // 100개의 칸 NORMAL 코드 심기 
          const data = []; // data에 최종 지뢰심기
          
          for (let i = 0; i < row; i++ ) {
            const normal = [];
            data.push(normal);
            for (j = 0; j < cell; j++) {
              normal.push(CODE.NORMAL); 
            }
          }
          console.log(data);
    
          // 10개의 칸 MINE 코드 심기
          for (k = 0; k < shuffle.length; k++) {
            const rowTen = Math.floor(shuffle[k] / row); // 2
            const cellOne = shuffle[k] % cell; // 2 
            data[rowTen][cellOne] = CODE.MINE;
          }
          return data;
    
          console.log(data);
        }
    
        // 6.1 onRightclick()
        function onRightClick(e) {
          e.preventDefault(); // 우클릭 기본기능 삭제
          const target = e.target;
          // 몇번째 칸 클릭했는지 알기위해 row와 cell 변수
          const rowIndex = target.parentNode.rowIndex; // tr
          const cellIndex = target.cellIndex; 
          const cellData = data[rowIndex][cellIndex]; // data 변수 선언필요
    
          if (cellData === CODE.MINE) { // 지뢰면 물음표지뢰로
            data[rowIndex][cellIndex] = CODE.QUESTION_MINE; // data에 직접 입력       
            target.className = 'question';
            target.textContent = '?';
            
          } else if (cellData === CODE.QUESTION_MINE) { // 물음표지뢰면 깃발지뢰로
            data[rowIndex][cellIndex] = CODE.FLAG_MINE;
            target.className = 'flag';
            target.textContent = '🚩';        
            
          } else if (cellData === CODE.FLAG_MINE) { // 깃발지뢰면 지뢰로
            data[rowIndex][cellIndex] = CODE.MINE;
            target.className = '';
            target.textContent = '💣'; // 개발자모드 2        
          
          } else if (cellData === CODE.NORMAL) { // 일반사항이면 물음표로
            data[rowIndex][cellIndex] = CODE.QUESTION;
            target.className = 'question';
            target.textContent = '?';            
            
          } else if (cellData === CODE.QUESTION) { // 물음표면 깃발로
            data[rowIndex][cellIndex] = CODE.FLAG;
            target.className = 'flag';
            target.textContent = '🚩';    
            
          } else if (cellData === CODE.FLAG) { // 깃발이면 일반사항으로 
            data[rowIndex][cellIndex] = CODE.NORMAL;
            target.className = '';
            target.textContent = '';        
    
          } 
        }
    
        // 8.1 countMine()
        function countMine(rowIndex, cellIndex) {
          const mines = [CODE.MINE, CODE.QUESTION_MINE, CODE.FLAG_MINE];
          let i = 0;
    
          // 공통식 추론
          mines.includes(data[rowIndex - 1]?.[cellIndex - 1]) && i++; 
          mines.includes(data[rowIndex - 1]?.[cellIndex]) && i++; 
          mines.includes(data[rowIndex - 1]?.[cellIndex + 1]) && i++; 
          mines.includes(data[rowIndex][cellIndex - 1]) && i++; 
          mines.includes(data[rowIndex][cellIndex + 1]) && i++; 
          mines.includes(data[rowIndex + 1]?.[cellIndex - 1]) && i++; 
          mines.includes(data[rowIndex + 1]?.[cellIndex]) && i++; 
          mines.includes(data[rowIndex + 1]?.[cellIndex + 1]) && i++; 
          return i;
    
        }
    
        // 10. open()
        function open(rowIndex, cellIndex) {
          // 먼저 target 활성화 (클릭이벤트가 필요한 태그)
          const target = $tbody.children[rowIndex]?.children[cellIndex];
    
          // 12. 재귀최적화 (한번 연곳 오픈 금지)
          if(data[rowIndex]?.[cellIndex] >= 0) return;
    
          if (!target) return;
          const count = countMine(rowIndex, cellIndex);    
          target.textContent = count || '';
          target.className = 'opened';
          data[rowIndex][cellIndex] = count; // 데이터에 지뢰갯수 저장 
          openCount++;
          console.log(openCount);
          
          // 13. 승리 표시 
          if (openCount === row * cell - mine) {
            // 14.1 시간
            const time = Math.floor((new Date() - startTime) / 1000);
            clearInterval(interval);
    
            $tbody.removeEventListener('contextmenu', onRightClick);
            $tbody.removeEventListener('click', onLeftClick);
    
            setTimeout(() => {
              alert( // 에러수정 4. alert 2줄 만들기 
              `승리했습니다.${time}초가 걸렸습니다.\n더 높은 레벨에 도전하세요.`
              );
            }, 500);        
            
          } 
          return count;      
    
        }
    
        // 9.1 openAround()
        function openAround(rowIndex, cellIndex) {
    
          // 11.1 콜스택 초과 방지
          setTimeout(() => {
            // 10. 클릭한 주변 8개 여는 함수
            const count = open(rowIndex, cellIndex);
            // 클릭한 곳이 지뢰가 없다면 주위 8개 열기
            if (count === 0) {
              // 11. 재귀함수 사용 주변 다 열기
              openAround(rowIndex - 1, cellIndex -1);
              openAround(rowIndex - 1, cellIndex);
              openAround(rowIndex - 1, cellIndex + 1);
              openAround(rowIndex, cellIndex - 1);
              openAround(rowIndex, cellIndex + 1);
              openAround(rowIndex + 1, cellIndex - 1);
              openAround(rowIndex +1, cellIndex);
              openAround(rowIndex +1, cellIndex + 1);
            } 
          }, 0);
    
        }
    
        
        // 7.1 onLeftClick()
        function onLeftClick(e) {
          // 우클릭시 사용한 target 이용
          const target = e.target;
          // 몇번째 칸 클릭했는지 알기위해 row와 cell 변수
          const rowIndex = target.parentNode.rowIndex; // tr
          const cellIndex = target.cellIndex; 
          const cellData = data[rowIndex]?.[cellIndex]; // data 변수 선언필요
    
          // 일반칸이면 주변칸 열고 근처 지뢰갯수 표시
          if (cellData === CODE.NORMAL) {
            // 9. 지뢰 주변칸 열기 
            openAround(rowIndex, cellIndex);
          }
          // 지뢰칸이면 게임종료
          else if (cellData === CODE.MINE) {
            // 에러수정 3. 지뢰 터지고도 시간흐름
            clearInterval(interval);
            target.textContent = '💥';
            target.className = 'opened';
            $tbody.removeEventListener('contextmenu', onRightClick);
            $tbody.removeEventListener('click', onLeftClick);
          }
        }
    
        // 5.2 
        function startGame() {
          data = plantMine();
    
          // data와 연결하는 tr, td 화면 만들고 지뢰 연결
          data.forEach((row) => { // row부터
            const $tr = document.createElement('tr');
            row.forEach((cell) => {
              const $td = document.createElement('td');
              if (cell === CODE.MINE) {
               //  $td.textContent = '💣'; // 개발자 모드 1
               //  $td.style.backgroundColor = 'red';
              }
              $tr.append($td);
            });
            $tbody.append($tr);
    
            // 6. 우클릭 이벤트
            $tbody.addEventListener('contextmenu', onRightClick);
            // 7. 좌클릭 이벤트
            $tbody.addEventListener('click', onLeftClick);
          });
        }
    
        // startGame(); // 5.1
    
      </script>
      
    </body>
    </html>
    반응형

    댓글

이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
Designed by Tistory.