<div id="wrapper">
<table id="tetris"></table>
<table id="next-table"></table>
<button id="stop">Stop</button>
<button id="start">Start</button>
<div class="score-cont">Score: <span id="score">0</span></div>
</div>
#tetris,
#next-table {
border-collapse: collapse;
box-shadow: 0px 0px 1px #d6d6d6;
}
#tetris td {
border: 1px solid #d6d6d6;
width: 30px;
height: 30px;
}
#next-table td {
width: 15px;
height: 15px;
}
td.white {
background-color: white;
}
td.red {
background-color: firebrick;
}
td.blue {
background-color: cornflowerblue;
}
td.orange {
background-color: tomato;
}
td.green {
background-color: darkseagreen;
}
td.navy {
background-color: steelblue;
}
td.violet {
background-color: palevioletred;
}
td.yellow {
background-color: yellowgreen;
}
td.black {
background-color: #252525;
}
//tetris
let tetris = document.querySelector('#tetris');
let tetrisData = [];
let currentBlock;
let nextBlock;
let currentTopLeft = [0, 3];
let blocks = [{
name: 's', // 네모
center: false,
numCode: 1,
color: 'red',
currentShapeIndex: 0,
shape: [
[
[0, 0, 0],
[0, 1, 1],
[0, 1, 1],
]
],
},
{
name: 't', // T자
center: true,
numCode: 2,
color: 'orange',
currentShapeIndex: 0,
shape: [
[
[0, 0, 0],
[1, 1, 1],
[0, 1, 0],
],
[
[0, 1, 0],
[1, 1, 0],
[0, 1, 0],
],
[
[0, 1, 0],
[1, 1, 1],
[0, 0, 0],
],
[
[0, 1, 0],
[0, 1, 1],
[0, 1, 0],
],
]
},
{
name: 'z', // 지그재그
center: true,
numCode: 3,
color: 'yellow',
currentShapeIndex: 0,
shape: [
[
[0, 0, 0],
[1, 1, 0],
[0, 1, 1],
],
[
[0, 1, 0],
[1, 1, 0],
[1, 0, 0],
],
[
[1, 1, 0],
[0, 1, 1],
[0, 0, 0],
],
[
[0, 0, 1],
[0, 1, 1],
[0, 1, 0],
],
]
},
{
name: 'zr', // 반대 지그재그
center: true,
numCode: 4,
color: 'green',
startRow: 1,
currentShapeIndex: 0,
shape: [
[
[0, 0, 0],
[0, 1, 1],
[1, 1, 0],
],
[
[1, 0, 0],
[1, 1, 0],
[0, 1, 0],
],
[
[0, 1, 1],
[1, 1, 0],
[0, 0, 0],
],
[
[0, 1, 0],
[0, 1, 1],
[0, 0, 1],
],
]
},
{
name: 'l', // L자
center: true,
numCode: 5,
color: 'blue',
currentShapeIndex: 0,
shape: [
[
[0, 0, 0],
[1, 1, 1],
[1, 0, 0],
],
[
[1, 1, 0],
[0, 1, 0],
[0, 1, 0],
],
[
[0, 0, 1],
[1, 1, 1],
[0, 0, 0],
],
[
[0, 1, 0],
[0, 1, 0],
[0, 1, 1],
],
]
},
{
name: 'lr', // 반대 L자
center: true,
numCode: 6,
color: 'navy',
currentShapeIndex: 0,
shape: [
[
[0, 0, 0],
[1, 1, 1],
[0, 0, 1],
],
[
[0, 1, 0],
[0, 1, 0],
[1, 1, 0],
],
[
[1, 0, 0],
[1, 1, 1],
[0, 0, 0],
],
[
[0, 1, 1],
[0, 1, 0],
[0, 1, 0],
],
]
},
{
name: 'b', // 1자
center: true,
numCode: 7,
color: 'violet',
currentShapeIndex: 0,
shape: [
[
[0, 0, 0, 0],
[0, 0, 0, 0],
[1, 1, 1, 1],
[0, 0, 0, 0],
],
[
[0, 1, 0, 0],
[0, 1, 0, 0],
[0, 1, 0, 0],
[0, 1, 0, 0],
],
[
[0, 0, 0, 0],
[1, 1, 1, 1],
[0, 0, 0, 0],
[0, 0, 0, 0],
],
[
[0, 0, 1, 0],
[0, 0, 1, 0],
[0, 0, 1, 0],
[0, 0, 1, 0],
],
]
},
];
const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'navy', 'violet'];
const isActiveBlock = value => (value > 0 && value < 10);
const isInvalidBlock = value => (value === undefined || value >= 10);
function init() {
const fragment = document.createDocumentFragment(); //메모리를 아끼기 위해 데이터 생성후 한번에 화면에 개시.
[...Array(20).keys()].forEach((col, i) => {
// 스프레드 문법: ...[1,2,3] -> 1,2,3
//function(...[1,2,3]) -> function(1,2,3) 꺽쇠밖으로 꺼낼 수 있음
//[...Array(숫자).keys()] -> [0,1,2,3,4,...숫자 -1]
//[...Array(숫자).keys().map((v)=>v+1)] -> [1,2,3,4,... 숫자]
const tr = document.createElement('tr');
fragment.appendChild(tr);
[...Array(10).keys()].forEach((row, j) => {
const td = document.createElement('td');
tr.appendChild(td);
});
const column = Array(10).fill(0);
tetrisData.push(column);
});
tetris.appendChild(fragment);
}
function draw() {
tetrisData.forEach((col, i) => {
col.forEach((row, j) => {
if (row > 0) {
tetris.children[i].children[j].className = tetrisData[i][j] >= 10 ? colors[tetrisData[i][j] / 10 - 1] : colors[tetrisData[i][j] - 1];
} else {
tetris.children[i].children[j].className = '';
}
});
});
}
function drawNext() { // 다음 블록 그리는 함수
const nextTable = document.getElementById('next-table');
nextTable.querySelectorAll('tr').forEach((col, i) => {
Array.from(col.children).forEach((row, j) => {
if (nextBlock.shape[0][i] && nextBlock.shape[0][i][j] > 0) {
nextTable.querySelectorAll('tr')[i].children[j].className = colors[nextBlock.numCode - 1];
} else {
nextTable.querySelectorAll('tr')[i].children[j].className = 'white';
}
});
})
}
function generate() { // 테트리스 블록 생성
if (!currentBlock) {
currentBlock = blocks[Math.floor(Math.random() * blocks.length)];
} else {
currentBlock = nextBlock;
}
currentBlock.currentShapeIndex = 0;
nextBlock = blocks[Math.floor(Math.random() * blocks.length)]; // 다음 블록 미리 생성
drawNext();
currentTopLeft = [-1, 3];
let isGameOver = false;
currentBlock.shape[0].slice(1).forEach((col, i) => { // 게임 오버 판단
col.forEach((row, j) => {
if (row && tetrisData[i][j + 3]) {
isGameOver = true;
}
});
});
currentBlock.shape[0].slice(1).forEach((col, i) => { // 블록 데이터 생성
col.forEach((row, j) => {
if (row) {
tetrisData[i][j + 3] = currentBlock.numCode;
}
});
});
if (isGameOver) {
clearInterval(int);
draw();
alert('game over');
} else {
draw();
}
}
function checkRows() { // 한 줄 다 찼는지 검사
const fullRows = [];
tetrisData.forEach((col, i) => {
let count = 0;
col.forEach((row, j) => {
if (row > 0) {
count++;
}
});
if (count === 10) {
fullRows.push(i);
}
});
const fullRowsCount = fullRows.length;
tetrisData = tetrisData.filter((row, i) => !fullRows.includes(i));
for (let i = 0; i < fullRowsCount; i++) {
tetrisData.unshift([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
}
let score = parseInt(document.getElementById('score').textContent, 10);
score += fullRowsCount ** 2;
document.getElementById('score').textContent = String(score);
}
function tick() { // 한 칸 아래로
const nextTopLeft = [currentTopLeft[0] + 1, currentTopLeft[1]];
const activeBlocks = [];
let canGoDown = true;
let currentBlockShape = currentBlock.shape[currentBlock.currentShapeIndex];
for (let i = currentTopLeft[0]; i < currentTopLeft[0] + currentBlockShape.length; i++) { // 아래 블럭이 있으면
if (i < 0 || i >= 20) continue;
for (let j = currentTopLeft[1]; j < currentTopLeft[1] + currentBlockShape.length; j++) {
if (isActiveBlock(tetrisData[i][j])) { // 현재 움직이는 블럭이면
activeBlocks.push([i, j]);
if (isInvalidBlock(tetrisData[i + 1] && tetrisData[i + 1][j])) {
canGoDown = false;
}
}
}
}
if (!canGoDown) {
activeBlocks.forEach((blocks) => {
tetrisData[blocks[0]][blocks[1]] *= 10;
});
checkRows(); // 지워질 줄 있나 확인
generate(); // 새 블록 생성
return false;
} else if (canGoDown) {
for (let i = tetrisData.length - 1; i >= 0; i--) {
const col = tetrisData[i];
col.forEach((row, j) => {
if (row < 10 && tetrisData[i + 1] && tetrisData[i + 1][j] < 10) {
tetrisData[i + 1][j] = row;
tetrisData[i][j] = 0;
}
});
}
currentTopLeft = nextTopLeft;
draw();
return true;
}
}
let int = setInterval(tick, 1500);
init();
generate();
document.getElementById('stop').addEventListener('click', function() {
clearInterval(int);
});
document.getElementById('start').addEventListener('click', function() {
if (int) {
clearInterval(int);
}
int = setInterval(tick, 1500);
});
window.addEventListener('keydown', (e) => {
switch (e.code) {
case 'ArrowLeft': { // 키보드 왼쪽 클릭 = 좌측 한 칸 이동
const nextTopLeft = [currentTopLeft[0], currentTopLeft[1] - 1];
let isMovable = true;
let currentBlockShape = currentBlock.shape[currentBlock.currentShapeIndex];
for (let i = currentTopLeft[0]; i < currentTopLeft[0] + currentBlockShape.length; i++) { // 왼쪽 공간 체크
if (!isMovable) break;
for (let j = currentTopLeft[1]; j < currentTopLeft[1] + currentBlockShape.length; j++) {
if (!tetrisData[i] || !tetrisData[i][j]) continue;
if (isActiveBlock(tetrisData[i][j]) && isInvalidBlock(tetrisData[i] && tetrisData[i][j - 1])) {
isMovable = false;
}
}
}
if (isMovable) {
currentTopLeft = nextTopLeft;
tetrisData.forEach((col, i) => {
for (var j = 0; j < col.length; j++) {
const row = col[j];
if (tetrisData[i][j - 1] === 0 && row < 10) {
tetrisData[i][j - 1] = row;
tetrisData[i][j] = 0;
}
}
});
draw();
}
break;
}
case 'ArrowRight': { // 키보드 오른쪽 클릭 = 우측 한 칸 이동
const nextTopLeft = [currentTopLeft[0], currentTopLeft[1] + 1];
let isMovable = true;
let currentBlockShape = currentBlock.shape[currentBlock.currentShapeIndex];
for (let i = currentTopLeft[0]; i < currentTopLeft[0] + currentBlockShape.length; i++) { // 오른쪽 공간 체크
if (!isMovable) break;
for (let j = currentTopLeft[1]; j < currentTopLeft[1] + currentBlockShape.length; j++) {
if (!tetrisData[i] || !tetrisData[i][j]) continue;
if (isActiveBlock(tetrisData[i][j]) && isInvalidBlock(tetrisData[i] && tetrisData[i][j + 1])) {
isMovable = false;
}
}
}
if (isMovable) {
currentTopLeft = nextTopLeft;
tetrisData.forEach((col, i) => {
for (var j = col.length - 1; j >= 0; j--) {
const row = col[j];
if (tetrisData[i][j + 1] === 0 && row < 10) {
tetrisData[i][j + 1] = row;
tetrisData[i][j] = 0;
}
}
});
draw();
}
break;
}
case 'ArrowDown': { // 키보드 아래쪽 클릭 = 하방측 한 칸 이동
tick();
}
}
});
window.addEventListener('keyup', (e) => {
switch (e.code) {
case 'ArrowUp': { // 방향 전환
let currentBlockShape = currentBlock.shape[currentBlock.currentShapeIndex];
let isChangeable = true;
const nextShapeIndex = currentBlock.currentShapeIndex + 1 === currentBlock.shape.length ?
0 :
currentBlock.currentShapeIndex + 1;
const nextBlockShape = currentBlock.shape[nextShapeIndex];
for (let i = currentTopLeft[0]; i < currentTopLeft[0] + currentBlockShape.length; i++) { // 돌린 이후 공간 체크
if (!isChangeable) break;
for (let j = currentTopLeft[1]; j < currentTopLeft[1] + currentBlockShape.length; j++) {
if (!tetrisData[i]) continue;
if (nextBlockShape[i - currentTopLeft[0]][j - currentTopLeft[1]] > 0 && isInvalidBlock(tetrisData[i] && tetrisData[i][j])) {
isChangeable = false;
}
}
}
if (isChangeable) {
while (currentTopLeft[0] < 0) {
tick();
}
for (let i = currentTopLeft[0]; i < currentTopLeft[0] + currentBlockShape.length; i++) { // 돌린 이후 공간 체크
for (let j = currentTopLeft[1]; j < currentTopLeft[1] + currentBlockShape.length; j++) {
if (!tetrisData[i]) continue;
let nextBlockShapeCell = nextBlockShape[i - currentTopLeft[0]][j - currentTopLeft[1]];
if (nextBlockShapeCell > 0 && tetrisData[i][j] === 0) {
// 다음 모양은 있는데 현재 칸이 없으면
tetrisData[i][j] = currentBlock.numCode;
} else if (nextBlockShapeCell === 0 && tetrisData[i][j] && tetrisData[i][j] < 10) {
// 다음 모양은 없는데 현재 칸이 있으면
tetrisData[i][j] = 0;
}
}
}
currentBlock.currentShapeIndex = nextShapeIndex;
}
draw();
break;
}
case 'Space': // 한방에 쭉 떨구기
while (tick()) {}
break;
}
});