<style>
table {
border-collapse: collapse;
}
td {
border: 1px solid #bbb;
text-align: center;
line-height: 20px;
width: 20px;
height: 20px;
background: #888;
}
td.opened {
background: white;
}
td.flag {
background: red;
}
td.question {
background: orange;
}
</style>
<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;
clearInterval(interval);
$tbody.innerHTML = '';
drawTable();
firstClick = true;
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) { //10개만 뽑겠다
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); //일단 지뢰없는 닫힌칸으로 채워주기
}
}
//shuffle = [85, 19, 93]
for (let k = 0; k < shuffle.length; k++) {
const ver = Math.floor(shuffle[k] / cell); //(85 /10) 몇번째 줄인지 알아내기 위해
const hor = shuffle[k] % cell; // 85%10 = 5번째 칸
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 = '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 = '';
}
}
//1 2 3
//4 5 6
//7 8 9
// ?. = 옵셔널 체이닝 if와 같이 보호해주는 역할
// 앞에있는것이 참인 값이면 뒤코드를 실행하고, 거짓인 값이면 코드를 통째로 undefined를 만들어버림.
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++; //나 자신이 5번칸일때 1번칸 / 앞에것이 존재하면 i++ 실행
mines.includes(data[rowIndex - 1]?.[cellIndex]) && i++; //2번칸
mines.includes(data[rowIndex - 1]?.[cellIndex + 1]) && i++; //3번칸
mines.includes(data[rowIndex][cellIndex - 1]) && i++; //4번칸
mines.includes(data[rowIndex][cellIndex + 1]) && i++; //6번칸
mines.includes(data[rowIndex + 1]?.[cellIndex - 1]) && i++; //7번칸
mines.includes(data[rowIndex + 1]?.[cellIndex]) && i++; //8번칸
mines.includes(data[rowIndex + 1]?.[cellIndex + 1]) && i++; //9번칸
return i;
}
function open(rowIndex, cellIndex) {
if (data[rowIndex]?.[cellIndex] >= CODE.OPENED) return;
//code.opened 가 0 이니까, 0~8까지가 나오면 이미 연 칸. 이미 연칸은 다시 열지않게 하는 코드
//data[rowIndex]가 undefined일수도 있으니까 ?. 넣어주기
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}초가 걸렸습니다.`);
}, 500);
}
return count;
}
function openAround(rI, cI) { //재귀함수
// maximum call stack size exceeded 문제
// 문제 해결: 호출스택부분이 넘쳤으니 백그라운드, 태스크큐로 옮겨주기 (setTimeout 실행)
// => 이렇게하면 한번 연 칸을 또 열어주게되서 무한루프에 갇혀 에러가난다. => 이미 연 칸은 무시
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; //실수로 -1되는거 막아줌 undefined안나오게
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.QUESTION_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); //내칸을 먼저 열고 주변칸이 비어있으면 같이 여는 함수
// const count = countMine(rowIndex, cellIndex);
// target.textContent = count || ''; //count ?? '' = nullish coalescing
// target.className = 'opened';
// data[rowIndex][cellIndex] = count;
} else if (cellData === CODE.MINE) { //지뢰칸이면
showMines();
target.textContent = '펑';
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>
maximum call stack size exceeded
재귀함수가 반복될때 생기는 문제 호출 스택 부분에 재귀함수가 쌓여 터지게 된다.
문제해결 방법 : 호출 스택 부분이 넘쳤으니, 백그라운드, 태스크 큐로 옮겨서 부담을 덜어준다.
-> setTimeout실행해주기
=> 이렇게 하면 지뢰찾기에서 한번 연 칸을 또 열어주게되어 무한루프에 갇혀 에러가 발생한다.
=> 이미 열어준 칸은 무시하기
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)
}
if (data[rowIndex]?.[cellIndex] >= CODE.OPENED) return;
code.opened가 0 이니까, 0~8까지 나오면 이미 연칸이 된다.
이미 연칸은 다시 열지않게 하는 코드를 작성해준다.
옵셔널 체이닝(optional chaining) ?.을 사용하면 프로퍼티가 없는 중첩 객체를 에러 없이 안전하게 접근할 수 있다.
if와 같이 에러가 나지 않게 보호해주는 역할을 한다.
앞에 있는 조건이 참인 값이면 뒤 코드를 실행하고, 거짓인 값이면 코드를 통째로 undefined를 만들어버린다.
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++; //나 자신이 5번칸일때 1번칸 / 앞에것이 존재하면 i++ 실행
mines.includes(data[rowIndex - 1]?.[cellIndex]) && i++; //2번칸
mines.includes(data[rowIndex - 1]?.[cellIndex + 1]) && i++; //3번칸
mines.includes(data[rowIndex][cellIndex - 1]) && i++; //4번칸
mines.includes(data[rowIndex][cellIndex + 1]) && i++; //6번칸
mines.includes(data[rowIndex + 1]?.[cellIndex - 1]) && i++; //7번칸
mines.includes(data[rowIndex + 1]?.[cellIndex]) && i++; //8번칸
mines.includes(data[rowIndex + 1]?.[cellIndex + 1]) && i++; //9번칸
return i;
}
위 코드에서 옵셔널 체이닝을 활용하였는데, 지뢰찾기에서 자신을 기준으로 사방을 검사할때 앞,옆,위,아래가 없는 칸이 있으면 undefined처리가 되므로 안전하게 옵셔널 체이닝을 사용하였다.