지뢰 찾기 코드리뷰(복습)
지뢰찾기 페이지의 최종코드를 보면서 복습을 해보도록 하겠습니다.
mine-sweeper.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>지뢰 찾기</title>
<style>
table {
border-collapse: collapse;
}
td {
border: 1px solid #bbb;
text-align: center;
line-height: 20px;
width: 20px;
height: 20px;
background-color: #888;
}
td.opened {
background-color: white;
}
td.flag {
background-color: red;
}
td.question {
background-color: orange;
}
</style>
</head>
<body>
<form id="form">
<input placeholder="가로 줄" id="row" size="5" />
<input placeholder="세로 줄" id="cell" size="5" />
<input placeholder="지뢰" id="mine" size="5" />
<button>생성</button>
</form>
<div id="timer">0초</div>
<table id="table">
<tbody></tbody>
</table>
<div id="result"></div>
<script>
const $form = document.querySelector('#form');
const $timer = document.querySelector('#timer');
const $tbody = document.querySelector('#table tbody');
const $result = document.querySelector('#result');
let row; // 줄
let cell; // 칸
let mine; // 지뢰
const CODE = {
NORMAL: -1,
QUESTION: -2,
FLAG: -3,
QUESTION_MINE: -4,
FLAG_MINE: -5,
MINE: -6,
OPENED: 0, // 0 이상이면 모두 열린 칸
};
let data;
let openCount;
let startTime;
let interval;
function onSubmit(event) {
event.preventDefault();
row = parseInt(event.target.row.value);
cell = parseInt(event.target.cell.value);
mine = parseInt(event.target.mine.value);
openCount = 0;
normalCellFound = false;
searched = null;
firstClick = true;
clearInterval(interval);
$tbody.innerHTML = '';
drawTable();
startTime = new Date();
interval = setInterval(() => {
const time = Math.floor((new Date() - startTime) / 1000);
$timer.textContent = `${time}초`;
}, 1000);
}
$form.addEventListener('submit', onSubmit);
function plantMine() {
const candidate = Array(row * cell).fill().map((arr, i) => {
return i;
});
const shuffle = [];
while (candidate.length > row * cell - mine) {
const chosen = candidate.splice(Math.floor(Math.random() * candidate.length), 1)[0];
shuffle.push(chosen);
}
const data = [];
for (let i = 0; i < row; i++) {
const rowData = [];
data.push(rowData);
for (let j = 0; j < cell; j++) {
rowData.push(CODE.NORMAL);
}
}
for (let k = 0; k < shuffle.length; k++) {
const ver = Math.floor(shuffle[k] / cell);
const hor = shuffle[k] % cell;
data[ver][hor] = CODE.MINE;
}
return data;
}
function onRightClick(event) {
event.preventDefault();
const target = event.target;
const rowIndex = target.parentNode.rowIndex;
const cellIndex = target.cellIndex;
const cellData = data[rowIndex][cellIndex];
if (cellData === CODE.MINE) { // 지뢰라면,
data[rowIndex][cellIndex] = CODE.QUESTION_MINE; // 물음표 지뢰로
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 = '';
} 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 = '';
}
}
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;
}
function open(rowIndex, cellIndex) {
if (data[rowIndex]?.[cellIndex] >= CODE.OPENED) return;
const target = $tbody.children[rowIndex]?.children[cellIndex];
if (!target) {
return;
}
const count = countMine(rowIndex, cellIndex);
target.textContent = count || '';
target.className = 'opened';
data[rowIndex][cellIndex] = count;
openCount++;
console.log(openCount);
if (openCount === row * cell - mine) {
const time = (new Date() - startTime) / 1000;
clearInterval(interval);
$tbody.removeEventListener('contextmenu', onRightClick);
$tbody.removeEventListener('click', onLeftClick);
setTimeout(() => {
alert(`성공~! ${time}초가 걸렸습니다.`);
}, 0);
}
return count;
}
function openAround(rI, cI) {
setTimeout(() => {
const count = open(rI, cI);
if (count === 0) {
openAround(rI - 1, cI - 1);
openAround(rI - 1, cI);
openAround(rI - 1, cI + 1);
openAround(rI, cI - 1);
openAround(rI, cI + 1);
openAround(rI + 1, cI - 1);
openAround(rI + 1, cI);
openAround(rI + 1, cI + 1);
}
}, 0);
}
let normalCellFound = false;
let searched;
let firstClick = true;
function transferMine(rI, cI) {
if (normalCellFound) return; // 이미 빈 칸을 찾았으면 종료
if (rI < 0 || rI >= row || cI < 0 || cI >= cell) return;
if (searched[rI][cI]) return;
if (data[rI][cI] === CODE.NORMAL) { // 빈 칸인 경우
normalCellFound = true;
data[rI][cI] = CODE.MINE;
} else { // 지뢰 칸인 경우 8방향 탐색
searched[rI][cI] = true;
transferMine(rI - 1, cI - 1);
transferMine(rI - 1, cI);
transferMine(rI - 1, cI + 1);
transferMine(rI, cI - 1);
transferMine(rI, cI + 1);
transferMine(rI + 1, cI - 1);
transferMine(rI + 1, cI);
transferMine(rI + 1, cI + 1);
}
}
function showMines() {
const mines = [CODE.MINE, CODE.QUSETION_MINE, CODE.FLAG_MINE];
data.forEach((row, rowIndex) => {
row.forEach((cell, cellIndex) => {
if (mines.includes(cell)) {
$tbody.children[rowIndex].children[cellIndex].textContent = 'X';
}
});
});
}
function onLeftClick(event) {
const target = event.target; // td 태그겠죠?
const rowIndex = target.parentNode.rowIndex;
const cellIndex = target.cellIndex;
let cellData = data[rowIndex][cellIndex];
if (firstClick) {
firstClick = false;
searched = Array(row).fill().map(() => []);
if (cellData === CODE.MINE) { // 첫 클릭이 지뢰면
transferMine(rowIndex, cellIndex); // 지뢰 옮기기
data[rowIndex][cellIndex] = CODE.NORMAL; // 지금 칸을 빈 칸으로
cellData = CODE.NORMAL;
}
}
if (cellData === CODE.NORMAL) { // 닫힌 칸이면
openAround(rowIndex, cellIndex);
} else if (cellData === CODE.MINE) { // 지뢰 칸이면
showMines();
target.textConten = '펑';
target.className = 'opened';
clearInterval(interval);
$tbody.removeEventListener('contextmenu', onRightClick);
$tbody.removeEventListener('click', onLeftClick);
} // 나머지는 무시
}
function drawTable() {
data = plantMine();
data.forEach((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);
$tbody.addEventListener('contextmenu', onRightClick);
$tbody.addEventListener('click', onLeftClick);
})
}
</script>
</body>
</html>
function onSubmit
onSubmit 함수는 사용자가 직접 지뢰판을 커스텀할 수 있게 해주기 위해 만들어 둔 form 태그의 input 에 입력한 값을 javascript 내에서 함수들끼리 상호작용하는 역할을 수행합니다.
console 창에 해당 변수들을 보며 설명해보려고 합니다.
console.log(event)
console.log(event.target)
console.log(event.target.row)
console.log(event.target.cell)
console.log(event.target.mine)
console.log(startTime)
setInterval, clearInterval(시간 재기)
setInterval(콜백함수, 시간)는 "시간(ms)" 을 간격으로 "콜백함수" 를 반복 호출하는 함수입니다. 잠깐 반복을 중단했다가, 재시작하는 코드를 구현해야할 때가 이를 사용합니다.
방법은 간단합니다.
- setInterval 함수의 반환값을 변수에 할당해 반복을 시작하고,
- clearInterval(변수) 를 호출해 반복을 중단하고,
- 다시 setInterval 로 재시작해주면 됩니다.
let 변수 = setInterval(콜백함수, 시간);
clearInterval(변수);
변수 = setInterval(콜백함수, 시간);
function plantMine
plantMine 함수는 지뢰판에 랜덤으로 지뢰를 심게 하는 역할을 수행합니다.
console 창에 해당 변수들을 보며 설명해보려고 합니다.
console.log(candidate);
console.log(shuffle);
console.log(data);
1. 우선, 참고로 candidate 의 배열과 data 의 배열은 별개의 배열입니다. candidate 는 0부터 row * cell 만큼의 숫자를 요소값으로 넣은 배열입니다. shuffle 은 candidate 의 길이(row * cell) 만큼의 숫자들 중 랜덤하게 onSubmit 에서 입력받은 mine 수만큼 뽑아 요소값으로 넣은 배열입니다. chosen 은 shuffle 배열에 넣기 위해 임의로 만들어 둔 변수입니다.
const candidate = Array(row * cell).fill().map((arr, i) => {
return i;
});
const shuffle = [];
while (candidate.length > row * cell - mine) {
const chosen = candidate.splice(Math.floor(Math.random() * candidate.length), 1)[0];
shuffle.push(chosen);
}
2. data 의 배열은 본격적인 지뢰판(table 태그) 을 만들기 위한 배열입니다. 이중 반복문을 통해 onSubmit 에서 입력한 row 만큼의 배열, rowData 를 만들고, 다시 rowData 에 CODE.NORMAL(지뢰가 아닌 칸(= -1))을 onSubmit 에서 입력한 cell 만큼 요소값으로 넣습니다.
const data = [];
for (let i = 0; i < row; i++) {
const rowData = [];
data.push(rowData);
for (let j = 0; j < cell; j++) {
rowData.push(CODE.NORMAL);
}
}
3. 그러고서 지뢰를 넣기 위해 row * cell 만큼의 숫자 중 mine 만큼의 숫자를 랜덤하게 뽑은 배열 shuffle 의 요소값들을 하나씩 반복문으로 해당숫자를
- cell 로 나눈 몫은 data 의 n 번째 row 배열이 될 것이고,
- cell 로 나눈 나머지는 rowData 의 요소값의 위치가 될 것입니다.
이를 각각 ver 와 hor 로 변수선언해 위치에 맞게 data 이중배열에 넣어줘 지뢰를 넣은 table, data 의 값을 만든 후, plantMine 을 종료합니다.
for (let k = 0; k < shuffle.length; k++) {
const ver = Math.floor(shuffle[k] / cell);
const hor = shuffle[k] % cell;
data[ver][hor] = CODE.MINE;
}
return data;
function onRightClick
onRightClick 함수는 말 그대로 마우스 우클릭의 역할을 수행합니다. 지뢰찾기에서 마우스 우클릭은 총 6가지 경우로 해당 칸의 textContent 와 className 을 바꿔 사용자에게 메모 기능을 수행합니다.
6가지 경우, 클릭한 칸이
- 지뢰이면서 닫힌 칸이라면, 물음표 칸으로
- 지뢰이면서 물음표 칸이라면, 느낌표 칸으로
- 지뢰이면서 느낌표 칸이라면, 닫힌 칸으로
- 빈칸이면서 닫힌 칸이라면, 물음표 칸으로
- 빈칸이면서 물음표 칸이라면, 느낌표 칸으로
- 빈칸이면서 느낌표 칸이라면, 닫힌 칸으로
console.log(event);
console.log(event.target);
console.log(target.parentNode.rowIndex);
console.log(target.cellIndex);
console.log(data[rowIndex][cellIndex]);
1. 우선, 기존 event 의 우클릭 기능인 contentmenu 대신 원하고자 하는 기능을 구현하기 위해 event.perventDefault 를 적어둡니다.
event.preventDefault();
2. 다음으로 선택한 빈 칸, event.target(= td 태그, 밑에서 나올 drawTable에서 data 에 있는 rowData 배열의 -1 요소값에 td 태그를 선언해줌. 이를 통해 비동기함수로 drawTable 이 먼저 실행되었음을 알 수 있습니다.) 을 target 으로 변수 선언하고, 이에 해당하는 target(td 태그) 의 rowIndex 와 cellIndex 를 table 내 자체적으로 index 값을 찾아내는 rowIndex 와 cellIndex 를 이용해 rowIndex 와 cellIndex 로 변수 선언해 해당 target 의 위치를 구해 data 이중배열에 있는 해당 target 의 요소값(= -6 이면 지뢰일 것이고, 그 외면 지뢰가 아니면서 닫힌 칸, 물음표, 느낌표, 지뢰이면서 닫힌 칸, 물음표, 느낌표로 총 6가지 경우 중 하나일 것입니다.)을 cellData 로 변수선언해줍니다.
const target = event.target;
const rowIndex = target.parentNode.rowIndex;
const cellIndex = target.cellIndex;
const cellData = data[rowIndex][cellIndex];
3. 위에서 말한 target 의 요소값, cellData 의 값이 6가지 중 무엇인지 반복문을 통해 질문한 뒤,
- 지뢰면, 물음표로
- 지뢰지만 물음표로 표시되어 있으면, 느낌표로
- 지뢰지만 느낌표로 표시되어 있으면, 닫힌 칸으로
- 일반 닫힌 칸이면, 물음표로
- 물음표면 느낌표로
- 느낌표면 일반 닫힌 칸으로
화면에 표시합니다.
if (cellData === CODE.MINE) { // 지뢰라면,
data[rowIndex][cellIndex] = CODE.QUESTION_MINE; // 물음표 지뢰로
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 = '';
}
function countMine
countMine 함수는 사용자가 빈 칸을 클릭했을 때, 해당 칸 주위로 8칸에서 지뢰를 확인하는 역할을 수행합니다.
1. 우선 해당 빈 칸 주위 8칸들의 요소값이 지뢰인 3가지 경우(CODE.MINE, CODE.QUSETION_MINE, CODE.FALG_MINE) 를 mines 배열에 요소값으로 넣어 mines 배열을 변수 선언하고, 지뢰의 개수를 세기 위해 i 를 0 으로 변수선언합니다.
const mines = [CODE.MINE, CODE.QUESTION_MINE, CODE.FLAG_MINE];
let i = 0;
2. 그리고 주위 8칸들의 요소값이 mines 배열에 있는 요소값 중 포함하고 있는지 각각 조사하고, 있다면 i 에 1을 더합니다. 그리고 i 를 return 해 함수를 종료합니다. ?. 는 옵셔널 체이닝(optional chanining)이라는 연산자입니다. 현재 클릭한 칸의 줄 수(rowIndex) 는 반드시 0 부터 9 사이에 있어야 합니다. 따라서 data[rowIndex] 가 undefined 될 일은 없습니다. 그래서 ?. 를 붙일 필요가 없습니다. 그런데 data[rowIndex - 1] 은 rowIndex가 0 이면 data[- 1] 이 됩니다. 이 때는 undefined 가 되므로 data[rowIndex - 1][cellIndex] 를 하면 오류가 납니다. 따라서 ?. 를 넣어 data[rowIndex - 1] 이 undefined 가 되는 상황에서 오류가 발생하는 것을 막습니다. undefined[cellIndex] 를 하면 오류가 발생하지만, undefined?.[cellIndex] 를 하면 undefined 를 반환합니다. 즉, 옵셔널 체이닝 문법은 보호 장치입니다.
자세한 내용은 https://yngjnhyk.tistory.com/51 에 있습니다.
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;
function open
open 함수는 사용자가 빈칸을 클릭했을 때, 열린 칸으로 만들어주는 역할을 합니다.
console 창에 해당 변수들을 보며 설명해보려고 합니다.
console.log(rowIndex);
console.log(cellIndex);
console.log($tbody.children);
console.log($tbody.children[rowIndex]);
1. 해당 빈칸이 열려있는 칸이거나 위에서 말했던 6가지 경우가 아닌 예외인 경우는 모두 열린 칸이므로 return 을 통해 open 함수를 종료합니다.(실행하지 않습니다.)
if (data[rowIndex]?.[cellIndex] >= CODE.OPENED) return;
2. tbody 에 있는 tr 태그의 rowIndex 에 있는 요소값의 index 를 target 으로 변수선언합니다. 쉽게 말해 클릭한 빈칸의 rowIndex 와 cellIndex 로 위치를 측정해 해당 요소값을 target 으로 변수선언합니다.
const target = $tbody.children[rowIndex]?.children[cellIndex];
3. target 이 존재하는지(undefined 가 아닌지) 확인해서 존재하는 경우에만 open 함수를 진행합니다.(해당 빈칸을 엽니다.) countMine 함수를 count 로 변수선언하고, 해당 닫힌 빈칸의 textContent 가 || 논리 연산자를 이용해 count 의 값이 true 면, count 를 반환하고, false 면, ''(빈칸) 을 반환합니다.(count 를 통해 주위 8칸에서 지뢰가 없다면, 0 이 아닌 빈칸으로 반환합니다.) 또한, 해당 닫힌 빈칸을 엽니다. 그리고, data 이중 배열에 있는 해당 칸의 요소값을 -1 에서 count 로 반환합니다.
if (!target) {
return;
}
const count = countMine(rowIndex, cellIndex);
target.textContent = count || '';
target.className = 'opened';
data[rowIndex][cellIndex] = count;
4. open 함수를 실행할 때마다 실행한 횟수를 openCount 에 저장하기 위해 1씩 더합니다. 만약 openCount 가 지뢰판의 총 칸 에서 지뢰칸을 뺀 나머지 칸이 될 경우, 즉 지뢰를 빼고 빈칸을 모두 연 경우 성공입니다. 성공했다면 현재시각에서 onSubmit 에서 변수선언한 startTime 을 뺀 시간 을 1000 으로 나눠(편의상 초 단위로 표시하기 위해) time 에 변수선언합니다. 지뢰판 위에서 움직이고 있는 타이머도 멈춥니다. 게임이 종료되었기 때문에 지뢰판을 클릭해도 더 이상 반응하지 않게 합니다. alert 함수를 setTimeout 으로 감싸지 않으면 마지막 칸이 열리기 전에 알림창이 뜨므로 이벤트 루프를 이용해 setTImeout 을 백그라운드로 보내 코드의 진행순서를 후순위로 해 이를 방지합니다. 마직막으로 count 를 반환하며 open 함수를 종료합니다.
openCount++;
if (openCount === row * cell - mine) {
const time = (new Date() - startTime) / 1000;
clearInterval(interval);
$tbody.removeEventListener('contextmenu', onRightClick);
$tbody.removeEventListener('click', onLeftClick);
setTimeout(() => {
alert(`성공~! ${time}초가 걸렸습니다.`);
}, 0);
}
return count;
function openAround
openAround 함수는 사용자가 닫혀 있는 빈칸을 누르고 해당 빈칸을 포함해 빈칸 주위로 8개, 총 9개의 빈칸에 지뢰가 없을 때, 다시 한 번 재귀함수로 해당 빈칸 주위 8칸에서 openAround 함수를 진행하도록 해 지뢰가 주위 8칸의 주위 8칸.. 에서 지뢰가 나올 때까지 빈칸을 여는 역할을 합니다
1. rowIndex 와 cellIndex 를 rI, cI 로 매개변수로 선언해 진행합니다. 비동기 함수인 setTimeout 을 통해 재귀함수의 문제를 해결하고, 클릭한 빈칸 주위의 지뢰 개수를 open 함수를 통해 반환한 count 를 다시 한 번 count 로 변수 선언해줍니다. 주위 8칸에서 지뢰가 없다면 주위 8칸에서 다시 openAround 함수를 수행합니다.
setTimeout(() => {
const count = open(rI, cI);
if (count === 0) {
openAround(rI - 1, cI - 1);
openAround(rI - 1, cI);
openAround(rI - 1, cI + 1);
openAround(rI, cI - 1);
openAround(rI, cI + 1);
openAround(rI + 1, cI - 1);
openAround(rI + 1, cI);
openAround(rI + 1, cI + 1);
}
}, 0);
function transferMine
지뢰게임의 2가지 오류 중 한 가지 오류니다. 본래 지뢰게임은 첫 번째 클릭에 지뢰가 걸리지 않게 하기 위한 기능이 숨겨져 있습니다. 이를 구현하기 위한 역할을 transferMine 이 수행합니다. 이 함수는 지뢰칸일 경우에만 진행하고, 해당 칸 주위로 8칸이 빈칸인지 찾고, 없다면 재귀함수로 해당 칸 주위 8칸들의 주위 8칸까지 검사해 빈칸을 지뢰함수로 바꾸는 함수입니다.
1. 이미 빈 칸이라면 함수를 종료합니다.
if (normalCellFound) return;
2. 해당 칸이 지뢰판의 영역에서 벗어나므로 함수를 종료합니다.
if (rI < 0 || rI >= row || cI < 0 || cI >= cell) return;
3. 해당 칸이 이미 else 에서 검사한 칸이므로 함수를 종료합니다.
if (searched[rI][cI]) return;
4. else 에서 transferMine 을 통해 빈칸을 찾은 경우, 해당 칸을 지뢰칸으로 바꿉니다. 그렇지 않으면, 해당 칸을 true 로 바꿔 검사한 칸으로 바꿔 더 이상 진행하지 않게 하고, 해당 칸 주위 8칸을 차례로 다시 transferMine 함수를 진행해 빈칸을 찾을 때까지 진행합니다.
if (data[rI][cI] === CODE.NORMAL) { // 빈 칸인 경우
normalCellFound = true;
data[rI][cI] = CODE.MINE;
} else { // 지뢰 칸인 경우 8방향 탐색
searched[rI][cI] = true;
transferMine(rI - 1, cI - 1);
transferMine(rI - 1, cI);
transferMine(rI - 1, cI + 1);
transferMine(rI, cI - 1);
transferMine(rI, cI + 1);
transferMine(rI + 1, cI - 1);
transferMine(rI + 1, cI);
transferMine(rI + 1, cI + 1);
}
function showMine
showMine 함수는 게임이 종료된 후, 지뢰의 위치를 알려주는 역할을 합니다.
1. 우선, 지뢰, 물음표 지뢰, 느낌표 지뢰들을 mines 배열에 넣습니다.
const mines = [CODE.MINE, CODE.QUSETION_MINE, CODE.FLAG_MINE];
2. data 이중 배열에서 다시 이중 반복해 row 에 있는 cell 들의 요소값을 mines 배열이 포함하고 있는지 검사하고, 있다면 해당 태그의 textContent 를 X 로 바꿉니다.
data.forEach((row, rowIndex) => {
row.forEach((cell, cellIndex) => {
if (mines.includes(cell)) {
$tbody.children[rowIndex].children[cellIndex].textContent = 'X';
}
});
});
onLeftClick
onLeftClick 함수는 말 그대로 마우스 좌클릭 기능을 담당합니다. 좌클릭은 닫힌 칸을 오픈할 수도 있고, 오픈하면서 게임을 종료시킬 수도, 성공시킬 수도 있습니다. 플러스로 첫 번째 클릭에서 지뢰를 피하게 해주는 transferMine 함수도 포함되어 있겠죠?
1. 만약 첫 번째 클릭이라면 첫 번째 클릭을 표현하는 firstClick 은 false 로 전환해주고, 새로운 이중배열, searched 를 만듭니다. 첫 클릭이 지뢰면 지뢰를 옮기고, 지금 칸을 빈 칸으로 만들어줍니다.
if (firstClick) {
firstClick = false;
searched = Array(row).fill().map(() => []);
if (cellData === CODE.MINE) { // 첫 클릭이 지뢰면
transferMine(rowIndex, cellIndex); // 지뢰 옮기기
data[rowIndex][cellIndex] = CODE.NORMAL; // 지금 칸을 빈 칸으로
cellData = CODE.NORMAL;
}
}
2. 첫 번째 클릭 후, 클릭한 칸이 닫혀 있는 빈칸이면 주위 8칸도 함께 오픈합니다. 그렇지 않고, 지뢰 칸이면 지뢰판에 있는 모든 지뢰의 위치를 표시하고, 클릭한 해당 칸에 '펑' 을 표시와 함께 오픈합니다. 타이머를 종료하고, 게임이 종료되어 더 이상 좌클릭 기능이 작동되지 않도록 이벤트를 지웁니다.
if (cellData === CODE.NORMAL) { // 닫힌 칸이면
openAround(rowIndex, cellIndex);
} else if (cellData === CODE.MINE) { // 지뢰 칸이면
showMines();
target.textConten = '펑';
target.className = 'opened';
clearInterval(interval);
$tbody.removeEventListener('contextmenu', onRightClick);
$tbody.removeEventListener('click', onLeftClick);
}
function drawTable
drawTable 은 처음 onSubmit 을 통해 받은 row 와 cell, mine 의 값을 받아 지뢰판을 HTML 로 지뢰판을 만드는 역할을 합니다.
1. plantMine 을 통해 지뢰(CODE.MINE, -6)가 있는 이중 배열을 data 로 변수선언합니다.
data = plantMine();
2. data 는 이중배열이기에 이중반복문을 통해 row 만큼의 tr 태그를 data 에 넣고, cell 만큼의 td 태그를 각각의 tr 태그에 넣습니다. 개발의 편의를 위해 지뢰의 위치를 HTML 상에서 직관적으로 확인해보고 싶다면, td 태그의 textContent 를 X 로 바꿉니다. 이제 이벤트 버블링으로 tbody 에 미리 만들어둔 좌클릭과 우클릭 기능을 추가합니다.
data.forEach((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);
$tbody.addEventListener('contextmenu', onRightClick);
$tbody.addEventListener('click', onLeftClick);
})
하고 난 후,
이전까지는 천천히 훓으면 충분히 읽혀서 따로 글을 작성하지 않았지만, 앞으로는 지뢰찾기 에서보다 더 생소하고 어려운 메소드와 문법들이 나올 거기 때문에 눈으로만 보고도 바로바로 읽히는 수준이 되지 않는 이상 이렇게 복습하려고 합니다. 충분히 익힐때까지요.
하고 나니 대충 이러이러하게 작동하고 기능하겠지 라고 생각했었는데 지금은 이 곳에서 이벤트 버블링, 저 곳에서 이벤트 루프 등 좀 더 디테일하게 기능이 구현되는 모습을 머릿속으로 상상할 수 있는 느낌입니다. 이제 제 자식이라고 해도 면이 설 것 같네요.
'zerocho > ES2021 자바스크립트 강좌' 카테고리의 다른 글
[14-4] 복습 (0) | 2023.03.07 |
---|---|
[13-5] 복습 (0) | 2023.03.03 |
[12-5] 셀프체크 (0) | 2023.02.22 |
[12-4] 마무리 (0) | 2023.02.22 |
[12-3] 재귀, Maximum call (0) | 2023.02.22 |