[14-4] 복습
두더지 게임 코드리뷰(복습)
두더지 게임 페이지의 최종코드를 보면서 복습을 해보도록 하겠습니다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>두더지 잡기</title>
<style>
.cell {
display: inline-block;
position: relative;
width: 200px;
height: 200px;
background: 'yellow';
overflow: hidden;
}
.gopher,
.bomb {
width: 200px;
height: 200px;
bottom: 0;
position: absolute;
transition: bottom 1s;
}
.gopher {
background: url('./gopher.png') center center no-repeat;
background-size: 200px 200px;
}
.dead {
background: url('./dead_gopher.png') center center no-repeat;
background-size: 200px 200px;
}
.bomb {
background: url('./bomb.png') center center no-repeat;
background-size: 200px 200px;
}
.boom {
background: url('./explode.png') center center no-repeat;
background-size: 200px 200px;
}
.hidden {
bottom: -200px;
}
.hole {
width: 200px;
height: 150px;
position: absolute;
bottom: 0;
background: url('./mole-hole.png') center center no-repeat;
background-size: 200px 150px;
}
.hole-front {
width: 200px;
height: 30px;
position: absolute;
bottom: 0;
background: url('./mole-hole-front.png') center center no-repeat;
background-size: 200px 30px;
}
</style>
</head>
<body>
<div>
<span id="timer">0</span>초
<span id="score">0</span>점
<span>남은 목숨: <span id="life"> 3</span></span>
<button id="start">시작</button>
</div>
<div id="game">
<div class="row">
<div class="cell">
<div class="hole"></div>
<div class="gopher hidden"></div>
<div class="bomb hidden"></div>
<div class="hole-front"></div>
</div>
<div class="cell">
<div class="hole"></div>
<div class="gopher hidden"></div>
<div class="bomb hidden"></div>
<div class="hole-front"></div>
</div>
<div class="cell">
<div class="hole"></div>
<div class="gopher hidden"></div>
<div class="bomb hidden"></div>
<div class="hole-front"></div>
</div>
</div>
<div class="row">
<div class="cell">
<div class="hole"></div>
<div class="gopher hidden"></div>
<div class="bomb hidden"></div>
<div class="hole-front"></div>
</div>
<div class="cell">
<div class="hole"></div>
<div class="gopher hidden"></div>
<div class="bomb hidden"></div>
<div class="hole-front"></div>
</div>
<div class="cell">
<div class="hole"></div>
<div class="gopher hidden"></div>
<div class="bomb hidden"></div>
<div class="hole-front"></div>
</div>
</div>
<div class="row">
<div class="cell">
<div class="hole"></div>
<div class="gopher hidden"></div>
<div class="bomb hidden"></div>
<div class="hole-front"></div>
</div>
<div class="cell">
<div class="hole"></div>
<div class="gopher hidden"></div>
<div class="bomb hidden"></div>
<div class="hole-front"></div>
</div>
<div class="cell">
<div class="hole"></div>
<div class="gopher hidden"></div>
<div class="bomb hidden"></div>
<div class="hole-front"></div>
</div>
</div>
</div>
<script>
const $timer = document.querySelector('#timer');
const $score = document.querySelector('#score');
const $game = document.querySelector('#game');
const $start = document.querySelector('#start');
const $$cells = document.querySelectorAll('.cell');
const $life = document.querySelector('#life');
const holes = [0, 0, 0, 0, 0, 0, 0, 0, 0];
let started = false;
let score = 0;
let time = 30;
let life = 3;
let timerId;
let tickId;
$start.addEventListener('click', () => {
if (started) return; // 이미 시작했으면 무시
started = true;
console.log('시작!');
timerId = setInterval(() => {
time = (time * 10 - 1) / 10; // 소수점 계산 시 문제 있기 때문에 소수점을 사용하지 않고, 풀어씀
$timer.textContent = time;
if (time === 0) {
clearInterval(timerId);
clearInterval(tickId);
setTimeout(() => {
alert(`Game Over! score: ${score}`);
}, 50);
}
}, 100);
tickId = setInterval(tick, 1000);
});
let gopherPercent = 0.3;
let bombPercent = 0.5;
function tick() {
holes.forEach((hole, index) => {
if (hole) return; // 무언가 일어나고 있으면 return
const randomValue = Math.random();
if (randomValue < gopherPercent) {
const $gopher = $$cells[index].querySelector('.gopher');
holes[index] = setTimeout(() => { // 1초 뒤에 사라짐
$gopher.classList.add('hidden');
holes[index] = 0;
}, 1000);
$gopher.classList.remove('hidden');
} else if (randomValue < bombPercent) {
const $bomb = $$cells[index].querySelector('.bomb');
holes[index] = setTimeout(() => { // 1초 뒤에 사라짐
$bomb.classList.add('hidden');
holes[index] = 0;
}, 1000);
$bomb.classList.remove('hidden');
}
});
}
$$cells.forEach(($cell, index) => {
$cell.querySelector('.gopher').addEventListener('click', (event) => {
if (!event.target.classList.contains('dead')) {
score += 1;
$score.textContent = score;
}
event.target.classList.add('dead');
event.target.classList.add('hidden');
clearTimeout(holes[index]); // 기존 내려가는 타이머 제거
setTimeout(() => {
holes[index] = 0;
event.target.classList.remove('dead');
}, 1000);
});
$cell.querySelector('.bomb').addEventListener('click', (event) => {
event.target.classList.add('boom');
event.target.classList.add('hidden');
clearTimeout(holes[index]); // 기존 내려가는 타이머 제거
setTimeout(() => {
holes[index] = 0;
event.target.classList.remove('boom');
}, 1000);
life--;
$life.textContent = life;
if (life === 0) {
clearInterval(timerId);
clearInterval(tickId);
setTimeout(() => {
alert(`Game Over! score: ${score}`);
}, 50);
}
});
});
</script>
</body>
</html>
$start.addEventListener
start 이벤트는 게임 시작을 위해 '시작' 버튼에 이벤트를 넣은 것입니다.
1. 이미 시작했으면 return 으로 버튼클릭을 무시합니다. false 였던 started 를 true 로 바꿔줘 버튼이 두 번 클릭되는 것을 방지합니다.
if (started) return; // 이미 시작했으면 무시
started = true;
2. 0.1 초 단위로 줄어드는 타이머를 만들어주기 위해 setInterval 을 이용해 0.1 초씩 0.1 을 빼는 기능을 합니다. 타이머의 숫자는 time 으로 변수 선언하고, 이를 $timer 의 textContent 로 다시 변수 선언합니다. 만약 시간이 종료됐다면, 타이머를 종료하고, tickId 를 종료하므로써 두더지와 폭탄이 반복해서 올라오는 것을 막습니다. 그리고, '끝이 났다' 는 안내문구를 띄우기 위해 alert 함수를 사용하게 되는데, 그냥 alert 함수를 사용하지 않고 setTimeout 으로 감싸는 이유는 그냥 alert 함수를 사용하게 되면 alert 함수는 화면이 움직이는 것을 즉시 멈추고 알림창을 띄우므로, 화면 변경 상항이나 애니메이션이 있다면 그것도 멈춰 버립니다. 그래서 보통은 alert 함수를 사용할 때 setTimeout 을 함께 사용하면 좋습니다.
timerId = setInterval(() => {
time = (time * 10 - 1) / 10; // 소수점 계산 시 문제 있기 때문에 소수점을 사용하지 않고, 풀어씀
$timer.textContent = time;
if (time === 0) {
clearInterval(timerId);
clearInterval(tickId);
setTimeout(() => {
alert(`Game Over! score: ${score}`);
}, 50);
}
}, 100);
3. 두더지와 폭탄은 1초마다 올라갔다 내려가기를 반복하므로 tickId 를 setInterval 을 이용해 1초마다 반복합니다.
tickId = setInterval(tick, 1000);
$start.addEventListener('click', () => {
if (started) return; // 이미 시작했으면 무시
started = true;
console.log('시작!');
timerId = setInterval(() => {
time = (time * 10 - 1) / 10; // 소수점 계산 시 문제 있기 때문에 소수점을 사용하지 않고, 풀어씀
$timer.textContent = time;
if (time === 0) {
clearInterval(timerId);
clearInterval(tickId);
setTimeout(() => {
alert(`Game Over! score: ${score}`);
}, 50);
}
}, 100);
tickId = setInterval(tick, 1000);
});
function tick
tick 함수는 두더지 게임에서 가장 핵심 기능으로 두더지와 폭탄을 1초마다 올라갔다 내려가는 것을 반복하게 하는 역할을 합니다.
console 창
console.log(randomvalue);

1. holes 배열의 요소값을 하나씩 반복하며 해당 요소값에 값이 있다면,(무언가 일어나고 있으면, = 두더지나 폭탄이 움직이고 있다면,) return 으로 함수의 기능을 작동하지 않게 합니다.
if (hole) return; // 무언가 일어나고 있으면 return
2. 숫자 1 보다 작은 소수점 아래로 랜덤한 숫자를 randomvalue 로 변수 선언합니다. 만약 0.3(gopherPercent) 보다 작은 0.0.., 0.1.., 0.2.. 와 같은 숫자이므로, 10 가지 중 3 가지 경우이므로 30% 확률이 되고, 해당 순서에 있는 cell 클래스를 갖는 div 태그의 gopher 클래스를 갖고 있는 div 태그를 $gopher 로 변수 선언합니다. 해당 태그, $gopher 에서 hidden 클래스를 제거해 두더지가 모습을 보이도록 합니다. 그리고, 1 초 뒤에 해당 태그에 다시 hidden 클래스를 추가해 두더지가 다시 들어가게 하고, holes 배열의 해당 순서에 있는 요소값을 0 으로 바꿔 다시 tick 함수를 실행가능하게 해줍니다.
const randomValue = Math.random();
// console.log(randomValue);
if (randomValue < gopherPercent) {
const $gopher = $$cells[index].querySelector('.gopher');
holes[index] = setTimeout(() => { // 1초 뒤에 사라짐
$gopher.classList.add('hidden');
holes[index] = 0;
}, 1000);
$gopher.classList.remove('hidden');
3. 그렇지 않고, 만약 randomvalue 가 bombPercent 보다 작아서 randomvalue 가 0.3.., 0.4.. 와 같은 숫자일 경우, 10 가지 중 2 가지 경우이므로 20% 확률이 되고, 해당 순서에 있는 cell 클래스를 갖는 div 태그의 bomb 클래스를 갖는 div 태그를 $bomb 라고 변수 선언합니다. 해당 태그에서 hidden 클래스를 제거해 폭탄의 모습이 보이도록합니다. 그리고, 1 초 뒤에 해당 태그에 다시 hidden 클래스를 추가해 폭탄이 다시 들어가게 하고, holes 배열의 해당 순서에 있는 요소값을 0 으로 바꿔 다시 tick 함수를 실행가능하게 해줍니다.
} else if (randomValue < bombPercent) {
const $bomb = $$cells[index].querySelector('.bomb');
holes[index] = setTimeout(() => { // 1초 뒤에 사라짐
$bomb.classList.add('hidden');
holes[index] = 0;
}, 1000);
$bomb.classList.remove('hidden');
}
let gopherPercent = 0.3;
let bombPercent = 0.5;
function tick() {
// console.log(holes);
holes.forEach((hole, index) => {
if (hole) return; // 무언가 일어나고 있으면 return
const randomValue = Math.random();
// console.log(randomValue);
if (randomValue < gopherPercent) {
const $gopher = $$cells[index].querySelector('.gopher');
holes[index] = setTimeout(() => { // 1초 뒤에 사라짐
$gopher.classList.add('hidden');
holes[index] = 0;
}, 1000);
$gopher.classList.remove('hidden');
} else if (randomValue < bombPercent) {
const $bomb = $$cells[index].querySelector('.bomb');
holes[index] = setTimeout(() => { // 1초 뒤에 사라짐
$bomb.classList.add('hidden');
holes[index] = 0;
}, 1000);
$bomb.classList.remove('hidden');
}
});
}
$$cells.forEach
cells 반복문은 사용자가 두더지나 폭탄을 눌렀을 경우 이미지를 바꾸고, 두더지와 폭탄에 걸려 있는 setTimeout 을 clear 해 클릭한 두더지와 폭탄이 바로 내려가게 해주는 역할과 두더지를 잡은 횟수(점수) 를 계산하고, 폭탄을 실수로 3 번을 클릭한 경우 목숨을 다 사용했기 때문에 게임을 종료시키는 역할을 합니다.
1. 우선, 두더지에 클릭이벤트를 추가합니다. 만약, gopher 클래스를 갖는 div 태그 중 dead 클래스를 갖지 않는 태그라면, 점수를 1 점 추가합니다.
$cell.querySelector('.gopher').addEventListener('click', (event) => {
if (!event.target.classList.contains('dead')) {
score += 1;
$score.textContent = score;
}
2. 그리고, 해당 div 태그에 dead 클래스와 hidden 클래스를 추가해 cell 클래스를 갖는 div 태그 중 gopher 클래스를 갖는 div 태그들에 click 이벤트를 추가합니다. 추가로 기존의 내려가는 타이머를 제거하기 위해 tick 함수에서 변수 선언한 holes[index] 의 setTimeout 을 제거합니다.
event.target.classList.add('dead');
event.target.classList.add('hidden');
console.log(holes[index])
clearTimeout(holes[index]); // 기존 내려가는 타이머 제거
3. 다시 tick 함수가 실행될 수 있게 하기 위해 클릭한 해당 순서에 holes 배열의 요소값을 0 으로 바꿔주고, 해당 div 태그에 추가한 dead 클래스를 다시 지워줍니다. 이와 같은 행위는 두더지가 모두 내려갔을 경우 진행되어야 하기 때문에 setTimeout 으로 감싸줍니다. (1 초라고 한 경우는 hidden 클래스에서 두더지가 내려가는 속도가 1 초라고 돼 있기 때문.)
setTimeout(() => {
holes[index] = 0;
event.target.classList.remove('dead');
}, 1000);
4. 다음으로 폭탄에 클릭이벤트를 걸어줍니다. 눌렀을 때 해당 div 태그에 boom 클래스와 hidden 클래스를 추가해 폭탄의 이미지를 바꾸고, 즉시 폭탄이 내려가게 해줍니다. 물론 기존에 내려가는 타이머도 제거해줍니다.
$cell.querySelector('.bomb').addEventListener('click', (event) => {
event.target.classList.add('boom');
event.target.classList.add('hidden');
clearTimeout(holes[index]); // 기존 내려가는 타이머 제거
5. 두더지 클릭과 마찬가지로 해당 순서에 있는 holes 배열의 요소값을 0 으로 바꿔 tick 함수가 실행가능하도록 만들고, 추가했던 boom 클래스를 다시 지웁니다.
setTimeout(() => {
holes[index] = 0;
event.target.classList.remove('boom');
}, 1000);
6. 폭탄을 3 번 눌렀을 경우, 목숨을 다 사용했기 때문에 게임을 종료해야 합니다. 그래서 폭탄을 클릭할 때마다 life 를 하나씩 빼줍니다. 그리고, 3 번 클릭한 경우 timerId 를 clear 해 줄어들고 있는 타이머를 종료하고, timerId 를 clear 해 1초 마다 반복되는 tick 함수를 종료합니다. 마지막으로 알림창(대화상자) 를 띄우기 위해 setTimeout 으로 감싸 alert 함수를 사용하면 게임이 종료됩니다.
life--;
$life.textContent = life;
if (life === 0) {
clearInterval(timerId);
clearInterval(tickId);
setTimeout(() => {
alert(`Game Over! score: ${score}`);
}, 50);
}