자바스크립트로 테트리스를 만드는 게시글이다. 코린이를 위해 친절하게 설명했다. 클론코딩을 바탕으로 해서 버그들을 수정하고 새로운 기능들을 넣었다. 이 게시글에서는 이제 새로운 것들을 추가할 것이다.
이전 게시글 링크들!
2023.02.05 - [개발/클론코딩] - JavaScript로 테트리스 만들기, 코린이가 코린이를 위한 A-Z 설명
2023.04.22 - [개발/클론코딩] - JavaScript로 테트리스 만들기, 코린이가 코린이를 위한 A-Z 설명 2
2023.04.29 - [개발/클론코딩] - JavaScript로 테트리스 만들기, 코린이가 코린이를 위한 A-Z 설명 3
이번 게시물에서 추가, 수정한 기능들!
1. 신규 블록 추가 및 블록 색, 오류 개선
2. 7-bag 적용
3. 게임 종료 후에 최종 스코어 보이기
4. 블럭 착지 이펙트 적용
5. 같은 종류의 블록 hold 금지
6. 벽을 만나도 회전 가능
최종 완성본은 하단의 링크에 접속해 할수 있다!
https://replit.com/@juneforpay/JS-TETRIS?v=1
1. 신규 블럭블록 추가 및 블록 색, 오류 개선
충격적인 사실이 있다. 지금까지 우리는 테트리스 블록을 6개라고 여기고 코딩했지만.. 사실 1개가 더 있었다. 즉 7개다!!!
빠진 블럭은 z모양 블록의 좌우 대칭 모양으로 지금껏 빼먹고 있었다...
그래서 해당하는 블럭의 클래스의 색깔, 좌표 등을 새로 넣어주었다.
달라진 부분만 표시하면
tetris.js
const BLOCKBOX = [
["tree",[[3, 1],[1, 1],[2, 0],[2, 1]]],
["square",[[1, 0],[1, 1],[2, 0],[2, 1]]],
["bar",[[0, 1],[1, 1],[2, 1],[3, 1]]],
["zee",[[1, 0],[2, 0],[2, 1],[3, 1]]],
["see",[[2, 0],[1, 0],[1, 1],[0, 1]]],
["elLeft",[[1, 0],[1, 1],[2, 1],[3, 1]]],
["elRight",[[1, 1],[2, 1],[3, 1],[3, 0]]]
]
const blocktonumber =
{
"tree":0,
"square":1,
"bar":2,
"zee":3,
"see":4,
"elLeft":5,
"elRight":6
}
BLOCKBOX와 blocktonumber에 "see"라는 이름으로 추가했다.
blocks.js
const BLOCKS = {
tree: [
[[2, 1],[0, 1],[1, 0],[1, 1]],
[[1, 2],[0, 1],[1, 0],[1, 1]],
[[1, 2],[0, 1],[2, 1],[1, 1]],
[[2, 1],[1, 2],[1, 0],[1, 1]]
],
square: [
[[1, 0],[1, 1],[2, 0],[2, 1]],
[[1, 0],[1, 1],[2, 0],[2, 1]],
[[1, 0],[1, 1],[2, 0],[2, 1]],
[[1, 0],[1, 1],[2, 0],[2, 1]]
],
bar: [
[[0, 0],[1, 0],[2, 0],[3, 0]],
[[2, -1],[2, 0],[2, 1],[2, 2]],
[[0, 0],[1, 0],[2, 0],[3, 0]],
[[2, -1],[2, 0],[2, 1],[2, 2]]
],
zee: [
[[0, 0],[1, 0],[1, 1],[2, 1]],
[[0, 1],[1, 0],[1, 1],[0, 2]],
[[0, 0],[1, 0],[1, 1],[2, 1]],
[[0, 1],[1, 0],[1, 1],[0, 2]]
],
see: [
[[0, 1],[1, 0],[1, 1],[2, 0]],
[[0, 0],[0, 1],[1, 1],[1, 2]],
[[0, 1],[1, 0],[1, 1],[2, 0]],
[[0, 0],[0, 1],[1, 1],[1, 2]]
],
elLeft: [
[[0, 0],[0, 1],[1, 1],[2, 1]],
[[1, 0],[1, 1],[1, 2],[0, 2]],
[[0, 1],[1, 1],[2, 1],[2, 2]],
[[1, 0],[2, 0],[1, 1],[1, 2]]
],
elRight: [
[[0, 1],[1, 1],[2, 1],[2, 0]],
[[0, 0],[1, 0],[1, 1],[1, 2]],
[[0, 1],[1, 1],[2, 1],[0, 2]],
[[1, 0],[1, 1],[1, 2],[2, 2]]
]
}
export default BLOCKS;
blocks.js에도 돌렸을 때 각각의 좌표들을 추가했다.
그리고 기존에 있던 zee 블록의 좌표에 오류가 있어서 그 부분 수치도 수정했다.
zee 블럭의 경우 2번 돌렸을 때 제자리로 와야 하나 좌우로 계속 한 칸씩 움직이는 오류였다.
또한 내려올때 왼쪽으로 치우쳐 내려오는 블록들도 위치를 수정하였다.
style.css
.tree{
background: #7e2a7e;
}
.bar{
background: #3abbee;
}
.square{
background: #e2e228;
}
.zee{
background: #e13737;
}
.see{
background: #46c346;
}
.elLeft{
background: #4747ff;
}
.elRight{
background: orange;
}
see 클래스에 해당하는 블럭의 색깔을 새로 추가하였다.
그리고 전면적으로 기존 테트리스 색깔에 맞게 블록들 색깔을 개편하였다.
2. 7-bag 적용
테트리스 블럭이 나오는 방식은 사실 완전 무작위가 아니라고 한다.
7-bag 방식으로, 7개의 블록이 각각 묶음으로 묶여 한 묶음 안에 있는 블록을 다 써야 다음 묶음으로 넘어간다.
7개의 블럭으로 구성된 한 사이클 안에서 블록은 무조건 1번 나오는 방식이다.
즉, 블럭이 연속으로 2번 나올 수는 있어도 3번 나올 수는 없다.
특정 블럭이 아무리 안 나와도 13번 안에는 무조건 나온다.
이 방식을 도입하면 게임에서 블록의 운 적 요소를 줄일 수 있다.
nextblock = randomdecide();
tetris.js에서 init()와 generateNewblock()의 nextblock은 randomdecide() 함수에서 결정하게 하였고.
다음과 같이 randomdecide() 함수를 새로 만들었다.
let randombox= [0,1,2,3,4,5,6]
function randomdecide() {
if(randombox.length<1){
for(let i=0; i<7; i++){
randombox.push(i);
}
//랜덤박스 안에서 한 싸이클이 끝이나 블럭이 다 떨어지면 원래대로 7개를 다시 추가해준다.
}
let randoms = Math.floor(Math.random() * randombox.length);
let ans= randombox[randoms];
randombox.splice(randoms,1);
return ans;
//현재 랜덤박스 안의 블럭 중 랜덤으로 하나를 선택하고, 없애며 없앤 숫자를 리턴한다.
}
3. 게임 종료 후에 최종 스코어 보이기
index.html
<div class="game-text">
<span>게임종료</span>
<span>최종 스코어</span>
<span class="finalscore">
0
</span>
<button>다시시작</button>
</div>
다음과 같이 html의 game-text에 최종 스코어와 finalscore 클래스를 추가한다.
tetris.js
const finalscoreDisplay = document.querySelector(".finalscore");
다음과 같이 DOM을 추가한 후
function showGameoverText() {
finalscoreDisplay.innerText = score;
gameText.style.display = "flex"
}
게임이 종료 될때 score로 최종 스코어를 변경해 준다.
이제 게임이 종료되어도 최종 스코어가 크게 표시된다.
4. 블록 착지 이펙트 적용
블록이 착지 했을 때 착지해서 seized 되었다는 것을 보여주는 이팩트가 필요하다. 따라서 style.css 에서 다음 코드를 추가했다.
style.css
@keyframes wave {
50% {
opacity: 0;
width: 10%;
}
}
.wave {
animation-name: wave;
animation-duration: 0.5s;
animation-delay: 0s;
animation-iteration-count: 1;
animation-timing-function: linear;
animation-direction: alternate;
}
0.5초간 번쩍거리며 잠깐 해당 블럭이 커지는 효과를 준다
tetris.js
function seizeBlock() {
const movingBlocks = document.querySelectorAll(".moving");
movingBlocks.forEach(moving => {
moving.classList.remove("moving");
moving.classList.add("seized");
moving.classList.add("wave");
})
checkMatch()
}
그리고 seizeBlock 함수에서 wave 클래스를 추가하면 블록이 고정될때 번쩍하는 피드백을 줄 수 있다.
5. 같은 종류의 블럭 hold 금지
원래 hold는 블록을 세이브하고 나중에 교환하기 위해 만든 시스템이다.
그러므로, 현재블록과 hold 한 블록이 같다면 교환되지 않게 hold를 막아놓아야 한다.
그렇지 않으면 단순 시간 끌기로 hold기능이 악용되기 때문이다.
tetris.js
function hold() {
let holdnumber = blocktonumber[movingItem.type];
if (holdable && holditem.type !== BLOCKBOX[holdnumber][0]){
const holdedBlocks = document.querySelectorAll(".hold");
holdedBlocks.forEach(express => {
express.classList.remove(holditem.type, "hold");
})
let beforehold = holditem.type
holditem.type = BLOCKBOX[holdnumber][0];
BLOCKBOX[holdnumber][1].some(block => {
const x = block[0];
const y = block[1];
for (var i = 0; i < 4; i++) {
holdbox.childNodes[y].childNodes[0].childNodes[x].classList.add(BLOCKBOX[holdnumber][0], "hold");
}
})
holdable = false;
const fallingblocks = document.querySelectorAll(".moving");
fallingblocks.forEach(falling => {
falling.classList.remove(movingItem.type, "moving");
})
clearInterval(generateNewBlock);
if (beforehold == "") {
generateNewBlock();
}
else {
generateNewBlock(beforehold);
}
}
}
수정한 hold 함수다. 수정한 부분만 보면
function hold() {
let holdnumber = blocktonumber[movingItem.type];
if (holdable && holditem.type !== BLOCKBOX[holdnumber][0]){
}
}
hold 함수의 처음 부분에서 holdnumber를 먼저 불러오고, 현재 holdable 할 뿐만 아니라(방금 막 hold를 쓰지 않았어야 함) 현재 떨어지는 블록 종류와 이미 hold 한 블록의 종류가 달라야만 hold가 작동한다.
6. 벽을 만나도 회전 가능
기존에는 좌표상의 문제로 왼쪽 벽이나 오른쪽 끝에서 방향전환을 시도할 경우, 화면 밖으로 넘어가 전환이 되지 않았다.
그런 문제를 해결하기 위해서 Edge Case를 처리해 벽에서 튕겨내는 구문을 changeDirection에 추가했다.
tetris.js
function changeDirection() {
const direction = tempMovingItem.direction;
const left = tempMovingItem.left;
const type = tempMovingItem.type;
direction === 3 ? tempMovingItem.direction = 0 : tempMovingItem.direction += 1
switch (type) {
case "tree":
if (left == -1 && direction == 3) {
tempMovingItem.left++;
break;
}
if(left == 8 && direction == 1){
tempMovingItem.left--;
break;
}
break;
case "bar":
if (direction % 2 == 1) {
if (left == -2) {
tempMovingItem.left += 2;
break;
}
if (left == -1) {
tempMovingItem.left++;
break;
}
if(left == 7){
tempMovingItem.left--;
break;
}
}
break;
case "zee":
if(left == 8 && direction %2 == 1){
tempMovingItem.left--;
break;
}
break;
case "see":
if(left == 8 && direction %2 == 1){
tempMovingItem.left--;
break;
}
break;
case "elLeft":
if (left == -1 && direction == 3) {
tempMovingItem.left++;
break;
}
if(left == 8 && direction == 1){
tempMovingItem.left--;
break;
}
break;
case "elRight":
if (left == -1 && direction == 3) {
tempMovingItem.left++;
break;
}
if(left == 8 && direction == 1){
tempMovingItem.left--;
break;
}
break;
default:
break;
}
renderBlocks()
}
특수 케이스를 블록에 따라, 블록의 방향에 따라, 그리고 그 위치에 따라 구별하고
블록을 왼쪽이나 오른쪽으로 한두 칸 이동시켜서 랜더링 할 수 있도록 조정했다.
최종 코드들
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,inital-scale=1.0">
<title>테트리스</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div class="wrapper">
<div class="game-text">
<span>게임종료</span>
<span>최종 스코어</span>
<span class="finalscore">
0
</span>
<button>다시시작</button>
</div>
<span>
</span>
<span class="score">
0
</span>
<span>
</span>
<div class="holdbox">
HOLD
<ul></ul>
</div>
<span class="playground">
<ul></ul>
</span>
<span class="nextbox">
NEXT
<ul></ul>
</span>
</div>
<script src="javascript/tetris.js" type="module"></script>
</body>
</html>
style.css
* {
margin: 0;
padding: 0;
}
ul{
list-style:none;
}
body{
height: 100%;
overflow: hidden;
}
.wrapper{
display: grid;
grid-template-columns: 150px 300px 150px ;
margin: 25px;
justify-content:center;
}
.game-text{
display: none;
justify-content:center;
align-items: center;
flex-direction: column;
position: fixed;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.7);
left: 0;
top: 0;
color: #fff;
font-size: 50px;
}
.game-text > button{
padding: 0.5rem 1rem;
color: #fff;
background: salmon;
border: none;
cursor: pointer;
}
.score{
text-align: center;
font-size: 36px;
margin-bottom: 2rem;
}
.playground > ul {
border:1px solid #333;
width: 250px;
margin: auto;
/*모든 블럭을 합친 전체 칸의 크기다.*/
}
.playground > ul > li {
width: 100%;
height: 25px;
/*세로 한 줄의 크기다*/
}
.playground > ul > li > ul {
display: flex;
}
.playground > ul > li > ul > li {
width: 25px;
height: 25px;
outline: 1px solid #c8b1b1;
/*테트리스 블럭에서 최종적으로 한 칸이 되는 블럭 단위의 크기를 설정하였다.*/
}
.nextbox{
width: 125px;
height: 75px;
/*border: 1px solid #000;
background-color: gray;*/
align-items: center;
text-align : center
}
.nextbox > ul > li {
width: 100%;
height: 25px;
margin-left: 10px;
}
.nextbox > ul > li > ul {
display: flex;
text-align : center;
}
.nextbox > ul > li > ul > li {
width: 25px;
height: 25px;
/*outline: 1px solid #ccc;*/
}
.holdbox{
width: 125px;
height: 75px;
/*border: 1px solid #000;
background-color: gray;*/
align-items: center;
text-align : center
}
.holdbox > ul > li {
width: 100%;
height: 25px;
margin-left: 10px;
}
.holdbox > ul > li > ul {
display: flex;
text-align : center;
}
.holdbox > ul > li > ul > li {
width: 25px;
height: 25px;
/*outline: 1px solid #ccc;*/
}
.tree{
background: #7e2a7e;
}
.bar{
background: #3abbee;
}
.square{
background: #e2e228;
}
.zee{
background: #e13737;
}
.see{
background: #46c346;
}
.elLeft{
background: #4747ff;
}
.elRight{
background: orange;
}
@keyframes wave {
50% {
opacity: 0;
width: 10%;
}
}
@keyframes bomb {
50% {
opacity: 0;
width: 50%;
}
}
.wave {
animation-name: wave;
animation-duration: 0.5s;
animation-delay: 0s;
animation-iteration-count: 1;
animation-timing-function: linear;
animation-direction: alternate;
transition: 0.5s;
}
tetris.js
import BLOCKS from "./blocks.js"
// DOM
const playground = document.querySelector(".playground > ul");
const holdbox = document.querySelector(".holdbox > ul");
const nextbox = document.querySelector(".nextbox > ul");
const gameText = document.querySelector(".game-text");
const scoreDisplay = document.querySelector(".score");
const finalscoreDisplay = document.querySelector(".finalscore");
const restartButton = document.querySelector(".game-text > button");
// Setting
const GAME_ROWS = 20;
const GAME_COLS = 10;
const BLOCKBOX = [
["tree", [[3, 1], [1, 1], [2, 0], [2, 1]]],
["square", [[1, 0], [1, 1], [2, 0], [2, 1]]],
["bar", [[0, 1], [1, 1], [2, 1], [3, 1]]],
["zee", [[1, 0], [2, 0], [2, 1], [3, 1]]],
["see", [[2, 0], [1, 0], [1, 1], [0, 1]]],
["elLeft", [[1, 0], [1, 1], [2, 1], [3, 1]]],
["elRight", [[1, 1], [2, 1], [3, 1], [3, 0]]]
]
const blocktonumber =
{
"tree": 0,
"square": 1,
"bar": 2,
"zee": 3,
"see": 4,
"elLeft": 5,
"elRight": 6
}
let randombox = [0, 1, 2, 3, 4, 5, 6]
//variables
let score = 0;
let duration;
let downInterval;
let tempMovingItem;
let gameover = false;
let nextblock;
let replay = false;
let holdable = true;
const movingItem = {
type: "",
direction: 0,
top: 0,
left: 3,
};
const holditem = {
type: "",
direction: 0,
top: 0,
left: 3,
}
init()
// fUNCTIONS
function init() {
gameover = false;
duration = 1000;
score = 0;
scoreDisplay.innerText = score;
tempMovingItem = { ...movingItem };
for (let i = 0; i < GAME_ROWS; i++) {
prependNewLine();
}
if (replay) {
const movingBlocks = document.querySelectorAll(".next");
movingBlocks.forEach(express => {
express.classList.remove(BLOCKBOX[nextblock][0], "next");
})
const holdedBlocks = document.querySelectorAll(".hold");
holdedBlocks.forEach(express => {
express.classList.remove(holditem.type, "hold");
})
}
else {
for (let i = 0; i < 2; i++) {
prependbox(nextbox);
prependbox(holdbox);
}
}
nextblock = randomdecide();
BLOCKBOX[nextblock][1].some(block => {
const x = block[0];
const y = block[1];
for (var i = 0; i < 4; i++) {
nextbox.childNodes[y].childNodes[0].childNodes[x].classList.add(BLOCKBOX[nextblock][0], "next");
}
})
generateNewBlock();
}
function prependbox(obj) {
const li = document.createElement("li");
const ul = document.createElement("ul");
for (let j = 0; j < 5; j++) {
const matrix = document.createElement("li");
ul.prepend(matrix);
}
li.prepend(ul);
obj.prepend(li);
}
function prependNewLine() {
const li = document.createElement("li");
const ul = document.createElement("ul");
for (let j = 0; j < GAME_COLS; j++) {
const matrix = document.createElement("li");
ul.prepend(matrix);
}
li.prepend(ul);
playground.prepend(li);
}
function generateNewBlock(orderbyhold) {
clearInterval(downInterval);
downInterval = setInterval(() => { moveBlock('top', 1) }, duration)
if (duration > 100) {
duration -= 10;
}
if (orderbyhold) {
movingItem.type = orderbyhold;
movingItem.top = 0;
movingItem.left = 3;
movingItem.direction = 0;
tempMovingItem = { ...movingItem };
} else {
const blockArray = Object.entries(BLOCKS);
movingItem.type = blockArray[nextblock][0]
nextblock = randomdecide();
movingItem.top = 0;
movingItem.left = 3;
movingItem.direction = 0;
tempMovingItem = { ...movingItem };
const movingBlocks = document.querySelectorAll(".next");
movingBlocks.forEach(express => {
express.classList.remove(movingItem.type, "next");
})
BLOCKBOX[nextblock][1].some(block => {
const x = block[0];
const y = block[1];
for (var i = 0; i < 4; i++) {
nextbox.childNodes[y].childNodes[0].childNodes[x].classList.add(BLOCKBOX[nextblock][0], "next");
}
})
}
renderBlocks()
}
function randomdecide() {
if (randombox.length < 1) {
for (let i = 0; i < 7; i++) {
randombox.push(i);
}
}
let randoms = Math.floor(Math.random() * randombox.length);
let ans = randombox[randoms];
randombox.splice(randoms, 1);
return ans;
}
function checkEmpty(target) {
if (!target || target.classList.contains("seized")) {
return false;
}
return true;
}
function renderBlocks(moveType = "") {
let possible = true;
const { type, direction, top, left } = tempMovingItem;
const arr = [];
BLOCKS[type][direction].some(block => {
const x = block[0] + left;
const y = block[1] + top;
const target = playground.childNodes[y] ? playground.childNodes[y].childNodes[0].childNodes[x] : null;
const isAvailable = checkEmpty(target);
arr.push([x, y]);
if (!isAvailable) {
possible = false;
}
})
if (possible) {
const movingBlocks = document.querySelectorAll(".moving");
movingBlocks.forEach(moving => {
moving.classList.remove(type, "moving");
})
for (var i = 0; i < 4; i++) {
playground.childNodes[arr[i][1]].childNodes[0].childNodes[arr[i][0]].classList.add(type, "moving");
}
movingItem.left = left;
movingItem.top = top;
movingItem.direction = direction;
}
else {
tempMovingItem = { ...movingItem }
if (moveType === 'retry') {
clearInterval(downInterval)
gameover = true;
showGameoverText()
return true;
}
//sto = setTimeout(() => {
if (moveType === "top") {
seizeBlock();
}
renderBlocks('retry')
//}, 0)
return true;
}
}
function seizeBlock() {
const movingBlocks = document.querySelectorAll(".moving");
movingBlocks.forEach(moving => {
moving.classList.remove("moving");
moving.classList.add("seized");
moving.classList.add("wave");
})
checkMatch()
}
function checkMatch() {
let bonus = 0;
const childNodes = playground.childNodes;
childNodes.forEach(child => {
let matched = true;
child.children[0].childNodes.forEach(li => {
if (!li.classList.contains("seized")) {
matched = false;
}
})
if (matched) {
child.remove();
prependNewLine()
bonus++;
}
})
if (bonus > 0) {
score += bonus ** 2;
scoreDisplay.innerText = score;
}
holdable = true;
generateNewBlock();
}
function hold() {
let holdnumber = blocktonumber[movingItem.type];
if (holdable && holditem.type !== BLOCKBOX[holdnumber][0]) {
const holdedBlocks = document.querySelectorAll(".hold");
holdedBlocks.forEach(express => {
express.classList.remove(holditem.type, "hold");
})
let beforehold = holditem.type
holditem.type = BLOCKBOX[holdnumber][0];
BLOCKBOX[holdnumber][1].some(block => {
const x = block[0];
const y = block[1];
for (var i = 0; i < 4; i++) {
holdbox.childNodes[y].childNodes[0].childNodes[x].classList.add(BLOCKBOX[holdnumber][0], "hold");
}
})
holdable = false;
const fallingblocks = document.querySelectorAll(".moving");
fallingblocks.forEach(falling => {
falling.classList.remove(movingItem.type, "moving");
})
clearInterval(generateNewBlock);
if (beforehold == "") {
generateNewBlock();
}
else {
generateNewBlock(beforehold);
}
}
}
function moveBlock(moveType, amount) {
tempMovingItem[moveType] += amount;
renderBlocks(moveType);
}
function changeDirection() {
const direction = tempMovingItem.direction;
const left = tempMovingItem.left;
const type = tempMovingItem.type;
direction === 3 ? tempMovingItem.direction = 0 : tempMovingItem.direction += 1
switch (type) {
case "tree":
if (left == -1 && direction == 3) {
tempMovingItem.left++;
break;
}
if(left == 8 && direction == 1){
tempMovingItem.left--;
break;
}
break;
case "bar":
if (direction % 2 == 1) {
if (left == -2) {
tempMovingItem.left += 2;
break;
}
if (left == -1) {
tempMovingItem.left++;
break;
}
if(left == 7){
tempMovingItem.left--;
break;
}
}
break;
case "zee":
if(left == 8 && direction %2 == 1){
tempMovingItem.left--;
break;
}
break;
case "see":
if(left == 8 && direction %2 == 1){
tempMovingItem.left--;
break;
}
break;
case "elLeft":
if (left == -1 && direction == 3) {
tempMovingItem.left++;
break;
}
if(left == 8 && direction == 1){
tempMovingItem.left--;
break;
}
break;
case "elRight":
if (left == -1 && direction == 3) {
tempMovingItem.left++;
break;
}
if(left == 8 && direction == 1){
tempMovingItem.left--;
break;
}
break;
default:
break;
}
renderBlocks()
}
function dropBlock() {
clearInterval(downInterval);
downInterval = setInterval(() => {
moveBlock("top", 1)
}, 10)
}
function showGameoverText() {
finalscoreDisplay.innerText = score;
gameText.style.display = "flex"
}
document.addEventListener("keyup", e => {
if (!gameover) {
switch (e.keyCode) {
case 38:
changeDirection();
break;
default:
break;
}
}
})
document.addEventListener("keydown", e => {
if (!gameover) {
switch (e.keyCode) {
case 39:
moveBlock("left", 1);
break;
case 37:
moveBlock("left", -1);
break;
case 40:
moveBlock("top", 1);
break;
case 32:
dropBlock();
break;
case 67:
hold();
default:
break;
}
}
})
restartButton.addEventListener("click", () => {
playground.innerHTML = "";
gameText.style.display = "none"
replay = true;
init()
})
blocks.js
const BLOCKS = {
tree: [
[[2, 1], [0, 1], [1, 0], [1, 1]],//ㅗ
[[1, 2], [0, 1], [1, 0], [1, 1]],//ㅓ
[[1, 2], [0, 1], [2, 1], [1, 1]],//ㅜ
[[2, 1], [1, 2], [1, 0], [1, 1]]//ㅏ
],
square: [
[[1, 0], [1, 1], [2, 0], [2, 1]],
[[1, 0], [1, 1], [2, 0], [2, 1]],
[[1, 0], [1, 1], [2, 0], [2, 1]],
[[1, 0], [1, 1], [2, 0], [2, 1]]
],
bar: [
[[0, 0], [1, 0], [2, 0], [3, 0]],//ㅡ
[[2, -1], [2, 0], [2, 1], [2, 2]],//ㅣ
[[0, 0], [1, 0], [2, 0], [3, 0]],//ㅡ
[[2, -1], [2, 0], [2, 1], [2, 2]]//ㅣ
],
zee: [
[[0, 0], [1, 0], [1, 1], [2, 1]],
// ㅁ ㅁ
// ㅁ ㅁ
[[0, 1], [1, 0], [1, 1], [0, 2]],
// ㅁ
//ㅁ ㅁ
//ㅁ
[[0, 0], [1, 0], [1, 1], [2, 1]],
//ㅁ ㅁ
// ㅁ ㅁ
[[0, 1], [1, 0], [1, 1], [0, 2]]
// ㅁ
//ㅁ ㅁ
//ㅁ
],
see: [
[[0, 1], [1, 0], [1, 1], [2, 0]],
// ㅁ ㅁ
//ㅁ ㅁ
[[0, 0], [0, 1], [1, 1], [1, 2]],
// ㅁ
// ㅁ ㅁ
// ㅁ
[[0, 1], [1, 0], [1, 1], [2, 0]],
// ㅁ ㅁ
//ㅁ ㅁ
[[0, 0], [0, 1], [1, 1], [1, 2]]
// ㅁ
// ㅁ ㅁ
// ㅁ
],
elLeft: [
[[0, 0], [0, 1], [1, 1], [2, 1]],
//ㅁ
//ㅁ ㅁ ㅁ
[[1, 0], [1, 1], [1, 2], [0, 2]],
// ㅁ
// ㅁ
//ㅁ ㅁ
[[0, 1], [1, 1], [2, 1], [2, 2]],
//ㅁ ㅁ ㅁ
// ㅁ
[[1, 0], [2, 0], [1, 1], [1, 2]]
// ㅁ ㅁ
// ㅁ
// ㅁ
],
elRight: [
[[0, 1], [1, 1], [2, 1], [2, 0]],
// ㅁ
// ㅁ ㅁ ㅁ
[[0, 0], [1, 0], [1, 1], [1, 2]],
// ㅁ ㅁ
// ㅁ
// ㅁ
[[0, 1], [1, 1], [2, 1], [0, 2]],
// ㅁ ㅁ ㅁ
// ㅁ
[[1, 0], [1, 1], [1, 2], [2, 2]]
// ㅁ
// ㅁ
// ㅁ ㅁ
]
}
export default BLOCKS;
이상 자바스크립트로 테트리스 만들기 강의를 마칩니다!
여담
테트리스는 버전에 따라 다르긴 하지만 국룰 색깔이 있다고 한다. 그래서 색깔들을 강의에서 나온 것과 달리 다른 표준 국룰 색깔로 변경했다. 여기에 7-BAG까지 도입해 랜덤 비율을 조정해 색깔이 골고루 나오니 뭔가 화면을 바라보는데 색감이 더 미적으로 변했다. 눈이 편해진 것 같다.
retry 함수, 게임이 오버되었는지 판별하고 종료시키는 함수를 이해하고 수정하는데 많은 애로사항이 있었다. 이 함수가 재귀함수로 호출되며 동기처리에 큰 고려사항이었기 때문이다. 이 함수를 이해하고 개선해 보려고 콜백함수에 관한 내용, promise, async, awiat에 대한 내용을 추가로 학습했다. retry 관련 부분은 혼자만 비동기처리되어 있어 건드릴 때마다 꼬여 수정하기 않기로 했지만 그래도 좋은 학습이 되었다.
이펙트를 어떻게 넣을지 고민을 많이 했었다. 화면 위에 새로운 이미지를 띄우는 방식으로 이펙트를 만들면 화면 구성에 있어 더 많은 걸 신경 써야 할 테니 말이다. 그러던 중 그냥 css로 li들에 효과크기를 조금 변화시켰다가 돌아오게 하는 게 효과적인 방식이라는 걸 우연히 발견했다. 생각지 못한 부분에서 좋은 효과를 찾았다.
이 게시글들은 구글에 노출이 안 되는 문제가 있어서 구글 콘솔에서 html 태그를 다시 받아오는 에피소드도 있었다. 이 글을 현재 구글 검색해서 들어오셨다면, 그 문제는 해결된 것이다.
테트리스 프로젝트를 클론코딩하게 된 이유는 가장 시각적으로 직관적으로 보이며, 모두가 알고 있는 예시이기 때문이다. 그리고 초등학생 때부터 그러니까 꽤 오래전부터 나 스스로가 게임을 만든다는 것에 로망이 있었다. 그래서 다른 것 말고 게임으로 정한 것이기도 하다. 프로젝트를 진행하며 자신감이 생겼다. 뭐든지 직접 해봐야 자신감이 붙는데 나한테는 이게 첫 프런트엔드 프로젝트 이자 클론코딩이었다. 앞으로 다른 프론트엔드 작업을 하거나 클론코딩을 할 때 더 자신감 있게 할 수 있을 것이다.
https://github.com/SKKUKang/TETRIS-JS/tree/master
'개발 > 클론코딩' 카테고리의 다른 글
[Nomad Coders] Flutter로 UI 만들기 (2) | 2024.01.10 |
---|---|
[Nomad Coders] Flutter 를 위한 DART 문법 요약 (1) | 2024.01.09 |
JavaScript로 테트리스 만들기, 코린이가 코린이를 위한 A-Z 설명 3 (0) | 2023.04.29 |
JavaScript로 테트리스 만들기, 코린이가 코린이를 위한 A-Z 설명 2 (0) | 2023.04.22 |
JavaScript로 테트리스 만들기, 코린이가 코린이를 위한 A-Z 설명 (0) | 2023.02.05 |