자바스크립트로 테트리스를 만드는 게시글이다. 코린이를 위해 친절하게 설명하였다. 소스코드 역시 제공된다. 기존에 있었던 게시글에서는 클론코딩을 하고, 그 버그들을 수정했다. 이제는 새로운 기능들을 추가할 것이다.
이전 게시글들 링크!
2023.02.05 - [개발] - JavaScript로 테트리스 만들기, 코린이가 코린이를 위한 A-Z 설명
2023.04.22 - [개발] - JavaScript로 테트리스 만들기, 코린이가 코린이를 위한 A-Z 설명 2
기존에 있던 버그들을 지난 게시글에서 고쳤으니 이제는 새로운 기능들을 추가해 보자.
이번 게시글에서 추가한 것들은 크게 4가지이다.
1. 시간이 지날수록 빠르게 내려오게 하기
2. 점수제도 개선하기
3. 다음에 내려올 블록 보이기 + 블럭 1개 저장해 두었다가 원할 때 꺼내 쓰는 기능 추가하기
1. 시간이 지날수록 빠르게 내려오게 하기
테트리스 게임을 실제로 하다 보면 게임이 진행될수록 난이도가 올라가 점점 내려오는 속도가 빨라진다.
그러니 고정된 속도가 아닌 갈수록 빨라지도록 코드를 수정해 보자!
function generateNewBlock() {
clearInterval(downInterval);
downInterval = setInterval(() => { moveBlock('top', 1) }, duration)
if(duration>50){
duration-=10;
}
const blockArray = Object.entries(BLOCKS);
movingItem.type = blockArray[nextblock][0]
const randomIndex = Math.floor(Math.random() * blockArray.length);
nextblock = randomIndex;
movingItem.top = 0;
movingItem.left = 3;
movingItem.direction = 0;
tempMovingItem = { ...movingItem };
renderBlocks()
}
generateNewBlock()을 수정하였다.
수정사항을 이해하기 위해 중요하게 봐야 되는 부분만 작성하면 다음과 같다.
function generateNewBlock() {
downInterval = setInterval(() => { moveBlock('top', 1) }, duration)
if(duration>50){
duration-=10;
}
}
블록이 내려오는 시간 주기는 setInterval()에서 duration이 정한다.
그러므로 우리는 duration을 블럭이 새로 내려올 때마다 감소시켜, 갈수록 빠르게 내려오게 한다.
그 대신 난이도가 아무리 어려워져도 반응하고 정상적으로 플레이할 수 있는 상한선은 주어야 하기에 duration이 50보다 클 때까지만 속도를 줄이기로 정했다.
다시 시작할 경우, 난이도도 초기화되어야 하기에 init() 함수에 duration 초기화를 추가하였다.
function init() {
duration =1000
// 새로 추가한 부분만 작성하였다.
}
2. 점수제 개선
한 줄을 완성시킬 때마다 단순히 점수가 1점씩 오르는 것은 재미가 없다.
한 번에 더 많은 줄을 완성시킬 때, 더 많은 점수를 준다면 훨씬 재밌어질 것이다.
그래서 점수를 '(한 번에 없앤 줄의 수)의 제곱' 만큼 추가하기로 정했다.
한 번에 없앨 수 있는 줄의 개수가 일자 블럭을 통한 4개이니 한번에 얻을 수 있는 최고 점수는 4의 제곱인 16점이 된다.
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()
}
수정한 checkMatch 함수다.
기존 코드와는 달리 한 줄 없앨 때마다 bonus를 1씩 늘리고, bonus가 1 이상일 때만 bonus의 제곱만큼 score을 올려 반영시키는 방식으로 바꾸었다.
holdable 변수는 추가한 hold 기능에 관한 것으로 후술 하겠다.
3. 다음에 내려올 블록 보이기 + 블럭 1개 저장해 두었다가 원할 때 꺼내 쓰는 기능 추가하기
다음 내려올 블록을 보이게 하는 기능을 추가하기 위해서는 일단, 보이게 하는 부분을 화면 구성에 넣어야 한다.
따라서 index.html과 style.css에 해당 부분을 추가하였다.
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>
<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: #67c23a;
}
.bar{
background: salmon;
}
.square{
background: #2c82c9;
}
.zee{
background: #e6a23c;
}
.elLeft{
background: #8e44ad;
}
.elRight{
background: #16a085;
}
다음 블럭을 보여주는 부분은 nextbox로, 저장한 블럭을 보여주는 부분은 holdbox로 각각 추가되었다.
함수 밖에서 선언된 상수, 변수들이 추가된 부분들이다.
const holdbox = document.querySelector(".holdbox > ul");
const nextbox = document.querySelector(".nextbox > ul");
//html의 holdbox, nextbox에 접근할 수 있게 하는 변수 선언
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]]],
["elLeft",[[1, 0],[1, 1],[2, 1],[3, 1]]],
["elRight",[[1, 1],[2, 1],[3, 1],[3, 0]]]
]
//각각 holdbox, nextbox에 보일 블럭 모양들 집합을 따로 2차원 배열로 만들었다.
const blocktonumber =
{
"tree":0,
"square":1,
"bar":2,
"zee":3,
"elLeft":4,
"elRight":5
}
//블럭의 정보를 인덱스와 문자로 주고 받을 때 문자 정보인지 숫자 정보인지 서로 스위칭 할 수 있게 새로 선언하였다.
let nextblock;
//다음으로 나올 블럭이 무엇인지 저장하는 변수
let replay = false;
// 다시시작 버튼을 눌러 게임을 다시 실행할 경우 기존의 next상자와 hold상자 데이터를 지워야 한다. 그러기 위해 선언한 flag 변수다.
let holdable = true;
// 블럭을 하나 저장하는 hold는 연속으로 할 수는 없게 해야한다. 그것을 판별하기 위한 변수다.
const holditem={
type: "",
direction: 0,
top: 0,
left: 3,
}
//hold된 아티메 구조체로 이전에 선언한 movingitem과 같은 형식이다.
짧았던 기존의 init() 함수에 뭔가 많이 추가되었다. prependNewLine()이 들어있는 for문과 generateNewBlock 사이에 새로운 코드들이 추가되었다. 각각 어떤 기능을 하는지 주석을 통해 알아보자.
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");
//만약 재시작 버튼을 눌러 다시시작 하는 것이라면 새로운 nextbox와 holdbox에는 이미 li들이 생성되어 있기에 더이상 생성할 필요가 없다.
//그리고 기존의 nextbox,holdbox에 저장된 기록들을 지워주는 기능을 한다.
})
}
else{
for(let i=0; i<2; i++){
prependbox(nextbox);
prependbox(holdbox);
}
// replay가 false로 아예 처음 시작할때는 nextbox와 holdbox를 반복문 함수 호출로 만들어준다.
}
const blockArrays = Object.entries(BLOCKS);
nextblock=Math.floor(Math.random() * blockArrays.length);
//시작할때부터 next블럭을 알려주어야 하기에 init함수에도 처음 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");
}
})
//게임에서 현재 떨어지는 아이템을 랜더링 하는 것과 같은 원리로,처음 nextbox의 블럭을 렌더링 해준다.
// 여기까지 추가된 부분
generateNewBlock();
}
prependbox는 기존의 prependNewLine과 같은 메커니즘의 함수로 파라미터로 받은 부분에 화면에 보일 상자들을 추가해 주는 기능을 한다.
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);
}
c버튼을 누르면 hold 기능이 사용되도록 키 입력 이벤트에 case 67: hold()를 추가하였다.
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;
}
}
})
c버튼을 눌렀을 때 호출되는 hold() 함수다. 내려오고 있는 블록은 저장하고, 기존의 블록을 꺼내서 재 생성시키는 역할을 한다.
function hold(){
if(holdable){
//hold를 연속해서 사용하지 않은 상태로 hold가 가능한 상태일때만 함수를 작동시킨다.
const holdedBlocks = document.querySelectorAll(".hold");
holdedBlocks.forEach(express => {
express.classList.remove(holditem.type,"hold");
})
//holdbox ui를 모두 지워 초기화 시킨다.
let beforehold = holditem.type
//기존의 hold되었던 아이템 타입을 저장해 기억한다.
let holdnumber=blocktonumber[movingItem.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");
}
})
//holdbox를 새로 hold한 블럭으로 업데이트한다.
holdable=false;
//함수의 문지기 역할을 하는 처음 나온 변수를 false로 바꾸어 연속으로 hold함수 실행을 막는다.
const fallingblocks = document.querySelectorAll(".moving");
fallingblocks.forEach(falling => {
falling.classList.remove(movingItem.type, "moving");
})
//기존에 이미 떨어지고 있는 중이었던 블럭은 삭제한다.
clearInterval(generateNewBlock);
//이미 실행되고 있던 함수는 멈춘다.
if(beforehold == ""){
generateNewBlock();
//만약 이전에 hold하고 있는 블럭이 없는 상태, 즉 게임 중 처음 hold를 누른 상태면 인수 없이 generateNewBlock()을 호출한다.
}
else{
generateNewBlock(beforehold);
// hold하고 있는 블럭이 있다면, 그 블럭을 새로 생성할 것이므로 인자로 전달한다.
}
}
}
이제 hold를 통해서도 generateNewBlock을 호출할 수 있게 되었다. 수정된 generateNewBlock을 살펴보자
function generateNewBlock(orderbyhold) {
clearInterval(downInterval);
downInterval = setInterval(() => { moveBlock('top', 1) }, duration)
if(duration>50){
duration-=10;
}
if(orderbyhold){
movingItem.type= orderbyhold;
movingItem.top = 0;
movingItem.left = 3;
movingItem.direction = 0;
tempMovingItem = {...movingItem };
//인수가 있는 경우 즉, hold 기능으로 인해 새로운 블럭이 생성되는 경우를 처리한다.
//받은 인수로 생성되는 블럭을 정해준다.
}else{
//hold기능을 사용하지 않고 그냥 블럭을 생성할 때 이다.
const blockArray = Object.entries(BLOCKS);
movingItem.type = blockArray[nextblock][0]
//next 기능이 추가되었으므로 nextblock에 있는 블럭으로 생성한다.
nextblock = Math.floor(Math.random() * blockArray.length);
//nextblock은 랜덤으로 정한다.
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");
})
//새롭게 next가 바뀌었으므로 기존의 nextbox의 랜더링된 ui는 지운다.
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");
}
})
//그리고 새로운 next에 맞추어 nextbox를 렌더링한다.
}
renderBlocks()
}
블럭을 hold를 통해 바꾸고, 그 블록이 완전히 seized 되면 그 이후부터는 다시 hold 기능을 사용할 수 있어야 한다. 따라서 generateNewBlock()을 호출하기 전 checkedMatch()에서 holdable을 다시 true로 바꾸어 주었다.
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()
}
전체 코드
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>
<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: #67c23a;
}
.bar{
background: salmon;
}
.square{
background: #2c82c9;
}
.zee{
background: #e6a23c;
}
.elLeft{
background: #8e44ad;
}
.elRight{
background: #16a085;
}
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 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]]],
["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,
"elLeft":4,
"elRight":5
}
//variables
let score = 0;
let duration = 1000;
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);
}
}
const blockArrays = Object.entries(BLOCKS);
nextblock=Math.floor(Math.random() * blockArrays.length);
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>50){
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 = Math.floor(Math.random() * blockArray.length);
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 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;
}
setTimeout(() => {
renderBlocks('retry')
if (moveType === "top") {
seizeBlock();
}
}, 0)
return true;
}
}
function checkEmpty(target) {
if (!target || target.classList.contains("seized")) {
return false;
}
return true;
}
function seizeBlock() {
const movingBlocks = document.querySelectorAll(".moving");
movingBlocks.forEach(moving => {
moving.classList.remove("moving");
moving.classList.add("seized");
})
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(){
if(holdable){
const holdedBlocks = document.querySelectorAll(".hold");
holdedBlocks.forEach(express => {
express.classList.remove(holditem.type,"hold");
})
let beforehold = holditem.type
let holdnumber=blocktonumber[movingItem.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;
direction === 3 ? tempMovingItem.direction = 0 : tempMovingItem.direction += 1
renderBlocks()
}
function dropBlock() {
clearInterval(downInterval);
downInterval = setInterval(() => {
moveBlock("top", 1)
}, 10)
}
function showGameoverText() {
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: [
[[0, 0],[0, 1],[1, 0],[1, 1]],
[[0, 0],[0, 1],[1, 0],[1, 1]],
[[0, 0],[0, 1],[1, 0],[1, 1]],
[[0, 0],[0, 1],[1, 0],[1, 1]]
],
bar: [
[[1, 0],[2, 0],[3, 0],[4, 0]],
[[2, -1],[2, 0],[2, 1],[2, 2]],
[[1, 0],[2, 0],[3, 0],[4, 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, 1],[1, 1],[1, 2],[2, 2]],
[[2, 0],[2, 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;
게임 중 화면모습
그럼 다음 포스팅에서 여기에 더 디벨롭시켜 볼 것이다!
여담
게임에는 유동적인 난이도가 필요하다. 항상 같은 난이도라면 재미가 반감되고 유저들이 달성하는 점수 또한 운에 따라 널뛸 것이다. 그래서 기존에 없던 난이도를 넣고 점수도 제곱점수 시스템으로 바꾸어 더 게임다운 게임을 만들어냈다고 생각한다.
hold와 next 기능을 구현하는데 생각보다 오래 걸렸다. 기존에 없던 기능을 아예 처음부터 추가해야 하기도 했고, 예외처리 또한 정확히 해야 하며 화면 구성도 신경 써야 했기 때문이다. next나 hold 블록에 테두리를 표시할 때 가운데 정렬되어 보이지 않아 불편했다. 그래서 테두리를 없앴더니 훨씬 편해졌다.
다음 게시글 링크!
2023.05.08 - [개발/클론코딩] - JavaScript로 테트리스 만들기, 코린이가 코린이를 위한 A-Z 설명 4 (完)
'개발 > 클론코딩' 카테고리의 다른 글
[Nomad Coders] Flutter로 UI 만들기 (2) | 2024.01.10 |
---|---|
[Nomad Coders] Flutter 를 위한 DART 문법 요약 (1) | 2024.01.09 |
JavaScript로 테트리스 만들기, 코린이가 코린이를 위한 A-Z 설명 4 (完) (0) | 2023.05.08 |
JavaScript로 테트리스 만들기, 코린이가 코린이를 위한 A-Z 설명 2 (0) | 2023.04.22 |
JavaScript로 테트리스 만들기, 코린이가 코린이를 위한 A-Z 설명 (0) | 2023.02.05 |