-
JavaScript 입문 : 지뢰찾기 게임 - 지뢰없는 칸 한꺼번에 열기 (재귀함수 최적화), 게임 승리 및 걸린시간 표시컴퓨터 알아가기/JavaScript 2022. 11. 3. 19:30728x90반응형
Recursion 재귀함수에 대해 공부를 하고 있습니다. 나 자신의 함수를 나에게 다시 적용함으로써 같은 조건하에서는 지속적으로 작동되게 만드는 함수입니다.
그런데 지난시간에도 보았듯이 작동이 되긴하는데 시간차가 생기고 결국에는 브라우저가 다운되는 현상이 벌어졌습니다. 이는 이벤트루트의 문제도 아니며 자기 자신이 루프를 돌면서 지난간 곳을 반복적으로 지나가기 때문입니다.
관련된 재귀함수를 최적화하는 내용을 공부해보겠습니다. 관련 내용은 제로초 TV 자바스크립트 강좌의 도움을 받아 공부하고 있습니다.
1. 재귀함수 최적화
지난시간 에러를 다시보면 빈칸이 열리긴 하는데 시간이 더디고 마지막에는 브라우저가 다운되는 현상이 있었습니다. 원인을 고민해 보면 내가 열었던 칸을 기준으로 다시 열리고 다시 열린칸을 기준으로 또 열리는 현상인데 그림을 그려 보겠습니다.
위 그림을 보면 다음과 같은 원리를 찾아낼 수 있습니다.
만일 지뢰가 없는 count가 0인 조건이라고 가정하고 빨간(5번)을 클릭한다면 주변 1, 2, 3, 4, 6, 7, 8, 9번을 열게 되겠지요. 이게 최초의 open( ) 함수이고 주변 없는 곳까지 쭉 연결시키는 openAround( ) 함수를 재귀시켰으니 6번에 가서는 다시 6번 기준으로 2, 3, 10, 5, 11, 8, 9, 12번이 한꺼번에 열립니다.
그런데 이럴경우 6번을 기준으로 5번으로 코드가 넘어가고 5번은 처음 클릭한 지점으로 다시 6번으로 넘어가는 무한 반복이 재귀에서 나타나게 됩니다.
이를 방지하면 에러가 해결이 됩니다.
코드로는 작성을 못하더라도 말로라도 표현하면 "한번 열린곳은 열지말아라"를 코드로 나타내면 해결이 될 듯 합니다.
강좌의 도움을 받아 작성하면 다음과 같이 작성할 수 있습니다.
강좌에서는 const target위로 재귀최적화를 시켰지만 클릭다음에 이루어져야 할 것 같아서 위치를 바꿨습니다. 브라우저에서 [rowIndex]가 undefined 에러가 나네요. 이럴 경우 역시 opetional chaining을 사용합니다. 이거 무척 편하군요.
다시 코드를 작성하고 브라우저를 보면 다음과 같습니다.
이제 작동이 잘 되는 듯 합니다.
2. 몇 칸 열렸는지 확인
사실 작동은 잘되는 듯 한데 에러인지를 확인하기 위해서 지뢰갯수가 10개이니 총 90칸이 열리면 이기는 게임이니 얼마나 칸이 열리고 있는지 콘솔에서 확인해 볼 수 있습니다.
변수를 만들고 open( )함수에서 콘솔로 확인이 가능합니다.
브라우저를 보겠습니다. 총 82개가 열렸고 빈칸까지 더하면 90칸이 맞네요. 제대로 만들었네요.
2. 게임승리 표시
게임 승리에 대한 부분을 표시하기 위해서는 open( ) 함수내에서 이루어져야 합니다.
결국 게임을 이겼다는 것은 지뢰가 터지지 않고(터지는 코드는 기반영) 90개의 칸을 다 열었다는 뜻입니다. openCount를 이용할 수 있습니다.
즉, if (openCount === 90)으로 나타낼 수 있는데 숫자를 공통된 코드로 바꾸면 if (openCount === row * cell - mine)도 가능할 것 같습니다.
이때 카드 뒤집기에서 공부했듯이 마지막 칸이 열리기 전에 승리표시가 나오기 때문에 time여유를 0.5초정도 줍니다.
여기서 중요한 점은 마지막에 return count를 꼭 해주어야 합니다. 그렇지 않으면 하나씩 클릭하는 대로만 열립니다.
브라우저를 보면 잘 작동이 되네요.
3. 게임시간 측정하기 setInterval( )
이제는 조금 더 게임이 시작이 되면 타이머가 작동이 되고 최종 게임 완료시 승리표시와 함께 몇초 걸렸는지 표시하도록 합니다. setInterval( ) 함수를 사용합니다.
타이머를 사용하는 함수를 만들어 주고 다시 승리표시 하는 open( ) 함수로 가서 다음과 같이 수정해 줍니다.
브라우저에서 게임을 하면 다음과 같은 결과가 나오는 군요.
여기까지 기본적인 지뢰찾기 게임에 대한 코드를 보았습니다. 물론 아직 업데이트해야 될 내용은 많지만 가장 기본적인 코드는 완성 하였습니다.
현재까지 전체 코드를 공유합니다.
<!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: red; } td.question { background-color: orange; } </style> <body> <div id="timer">0</div> <table id="table"> <tbody></tbody> </table> <div id="result"></div> <script> // 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; // 지뢰 // 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 = 0; // 14. 타이머 사용하기 let startTime = new Date(); const interval = setInterval(() => { // new Date(): 현재 시간, startTime: 시작한 시간 const time = Math.floor((new Date() - startTime) / 1000); $timer.textContent = `${time}초`; }, 1000); // 1초맞다 올려주기 // 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 = 'X'; } 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(`승리했습니다.${time}초가 걸렸습니다.`); }, 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) { 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 = 'X'; } $tr.append($td); }); $tbody.append($tr); // 6. 우클릭 이벤트 $tbody.addEventListener('contextmenu', onRightClick); // 7. 좌클릭 이벤트 $tbody.addEventListener('click', onLeftClick); }); } startGame(); // 5.1 </script> </body> </html>
반응형'컴퓨터 알아가기 > JavaScript' 카테고리의 다른 글