- 웹 기술(HTML, CSS, JavaScript)로 재탄생한 고전 게임, 테트리스 만들어 보기2025년 07월 05일 22시 38분 17초에 업로드 된 글입니다.작성자: DandyNow728x90반응형
웹 기술로 재탄생한 고전 게임, 테트리스 만들어 보기
유년 시절의 향수를 자극하며 수많은 이들의 손에서 즐겨지던 테트리스는 단순한 규칙 속에서 깊이 있는 몰입감을 선사하는 퍼즐 게임의 대명사라 할 수 있다. 본고에서는 이러한 테트리스 게임을 웹 기술만을 활용하여 구현한 과정을 상세히 기술하며, 웹 기술이 가진 무한한 잠재력에 대하여 논한다.
1. 게임의 근간: HTML5 (
index.html)모든 웹 페이지의 출발점이 그러하듯, 본 테트리스 게임 또한 HTML5를 기반으로 하여 그 구조와 콘텐츠를 정의하였다.
index.html파일은 게임의 전체적인 틀을 제시하는 역할을 담당한다.<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>테트리스</title> <link rel="stylesheet" href="style.css"> </head> <body> <h1>테트리스</h1> <div class="game-container"> <canvas id="tetris"></canvas> <div class="info"> <h2>점수: <span id="score">0</span></h2> <h2>레벨: <span id="level">1</span></h2> <div class="next-piece"> <h2>다음 블록</h2> <canvas id="next"></canvas> </div> <button id="start-button">게임 시작</button> </div> </div> <script src="script.js"></script> </body> </html><canvas>태그: 게임의 핵심적인 시각적 요소를 구현하기 위해 사용된 HTML5의 중요 기능이다.id="tetris"는 주 게임 보드를,id="next"는 다음 출현할 블록을 미리 보여주는 영역을 각각 담당하며, JavaScript를 통해 이 캔버스 위에 블록들이 동적으로 그려진다.- 게임 정보 및 제어 요소:
<h2>및<span>태그는 현재 점수(score)와 레벨(level)을 사용자에게 명확히 전달하는 역할을 수행한다.<button id="start-button">은 게임 시작 기능을 담당하는 버튼으로서, 사용자 상호작용의 첫 지점을 제공한다. - 외부 파일 연동:
<link rel="stylesheet" href="style.css">는 게임의 시각적 스타일을 정의하는 CSS 파일을,<script src="script.js"></script>는 게임의 전반적인 논리를 포함하는 JavaScript 파일을 각각 연동한다.
2. 시각적 표현의 구현: CSS3 (
style.css)style.css파일은 본 게임의 레이아웃 및 시각적 심미성을 책임진다. 이 파일은 게임 화면의 배치, 글꼴, 색상 등 전반적인 디자인 요소들을 제어한다. (여기서는style.css파일의 구체적인 내용은 생략하나, 그 역할에 대한 설명은 포함한다.)- 캔버스 스타일링:
tetris및next캔버스의 배경색, 테두리, 크기 등을 설정함으로써 게임 영역을 시각적으로 명확히 구분한다. 이는 게임의 '무대'를 확립하는 데 기여한다. - 레이아웃 구성:
.game-container와.info클래스 등을 활용하여 게임 보드와 점수, 레벨, 다음 블록 표시 영역 등 정보 패널이 효과적으로 배치될 수 있도록 CSS Flexbox 혹은 Grid와 같은 레이아웃 기법을 적용하였다. 이를 통해 사용자 인터페이스는 정돈된 형태로 나타난다.
body { font-family: Arial, sans-serif; background-color: #f0f0f0; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; margin: 0; } h1 { color: #333; } .game-container { display: flex; flex-direction: row; align-items: flex-start; border: 3px solid #333; padding: 10px; background-color: #ddd; gap: 20px; } canvas#tetris { border: 2px solid #333; background-color: #000; } .info { text-align: left; } .info h2 { margin: 0 0 10px 0; } .next-piece { text-align: center; } canvas#next { border: 2px solid #333; background-color: #000; } #start-button { margin-top: 20px; padding: 10px 20px; font-size: 16px; cursor: pointer; background-color: #4CAF50; color: white; border: none; border-radius: 5px; } #start-button:hover { background-color: #45a049; }
3. 게임의 지성: JavaScript (
script.js)script.js파일은 테트리스 게임의 모든 논리를 구현한다. 블록의 생성, 이동, 회전, 충돌 감지, 줄 제거, 점수 계산, 게임 오버 판정 등 게임 플레이에 필요한 핵심 기능들이 이곳에 정의되어 있다. 특히 Canvas API를 활용하여 게임의 모든 그래픽 요소를 동적으로 렌더링한다.3.1. 초기 환경 설정 및 변수 선언
const COLS = 10; const ROWS = 20; const BLOCK_SIZE = 20; // 초기 설정 const canvas = document.getElementById('tetris'); const context = canvas.getContext('2d'); canvas.width = COLS * BLOCK_SIZE; canvas.height = ROWS * BLOCK_SIZE; const nextCanvas = document.getElementById('next'); const nextContext = nextCanvas.getContext('2d'); nextCanvas.width = 4 * BLOCK_SIZE; nextCanvas.height = 4 * BLOCK_SIZE; const scoreElement = document.getElementById('score'); const levelElement = document.getElementById('level'); const startButton = document.getElementById('start-button'); // 게임 변수 let board = []; let score = 0; let level = 1; let lines = 0; let gameOver = false; let currentPiece; let nextPiece; let dropCounter = 0; let dropInterval = 1000; // 1초마다 한 칸씩 떨어집니다. let lastTime = 0;COLS,ROWS,BLOCK_SIZE: 게임 보드의 가로, 세로 칸 수 및 개별 블록의 크기를 정의하는 상수로서, 게임의 기본적인 스케일을 결정한다.canvas,context,nextCanvas,nextContext: HTML에 정의된<canvas>요소와 2D 그래픽 렌더링을 위한 컨텍스트를 획득한다. 이 객체들을 통하여 화면에 블록을 그릴 수 있다.scoreElement,levelElement,startButton: HTML에서 점수와 레벨을 표시할 요소 및 게임 시작 버튼 요소를 가져온다.- 게임 변수:
board는 게임 보드의 현재 상태를 나타내는 2차원 배열이다.score,level,lines는 각각 게임 점수, 레벨, 제거된 줄의 수를 추적한다.gameOver는 게임 종료 상태를 나타내는 불리언 변수이다.currentPiece와nextPiece는 현재 조작 중인 블록과 다음에 출현할 블록을 저장하는 객체이다.dropCounter,dropInterval,lastTime은 블록이 자동으로 낙하하는 시간 간격을 제어하는 데 활용된다.
3.2. 테트리스 블록의 정의 (
PIECES)const PIECES = [ { shape: [[1, 1, 1, 1]], // I color: 'cyan' }, { shape: [[1, 1, 0], [0, 1, 1]], // Z color: 'red' }, { shape: [[0, 1, 1], [1, 1, 0]], // S color: 'green' }, { shape: [[1, 1, 1], [0, 0, 1]], // L color: 'orange' }, { shape: [[1, 1, 1], [1, 0, 0]], // J color: 'blue' }, { shape: [[1, 1, 1], [0, 1, 0]], // T color: 'purple' }, { shape: [[1, 1], [1, 1]], // O color: 'yellow' } ];PIECES: 테트리스 게임에 등장하는 7가지 유형의 블록 각각의 형태(shape)와 색상(color)을 정의한 배열이다.shape는 2차원 배열로 구성되어 블록의 형태를 이진 값(1은 블록의 구성 요소, 0은 빈 공간)으로 표현한다.
3.3. 게임 보드 생성 (
createBoard)function createBoard() { return Array.from({ length: ROWS }, () => Array(COLS).fill(0)); }createBoard(): 게임 보드를 초기 상태로 설정하는 함수이다.ROWSxCOLS크기의 2차원 배열을 생성하고 모든 칸을 0으로 채워 빈 공간임을 나타낸다.
3.4. 게임 화면 렌더링 (
draw)function draw() { // Draw board context.clearRect(0, 0, canvas.width, canvas.height); for (let row = 0; row < ROWS; row++) { for (let col = 0; col < COLS; col++) { if (board[row][col]) { context.fillStyle = board[row][col]; context.fillRect(col * BLOCK_SIZE, row * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE); context.strokeRect(col * BLOCK_SIZE, row * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE); } } } // Draw current piece if (currentPiece) { context.fillStyle = currentPiece.color; currentPiece.shape.forEach((row, y) => { row.forEach((value, x) => { if (value) { context.fillRect((currentPiece.x + x) * BLOCK_SIZE, (currentPiece.y + y) * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE); context.strokeRect((currentPiece.x + x) * BLOCK_SIZE, (currentPiece.y + y) * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE); } }); }); } // Draw next piece nextContext.clearRect(0, 0, nextCanvas.width, nextCanvas.height); if (nextPiece) { nextContext.fillStyle = nextPiece.color; const shape = nextPiece.shape; const shapeWidth = shape[0].length * BLOCK_SIZE; const shapeHeight = shape.length * BLOCK_SIZE; const startX = (nextCanvas.width - shapeWidth) / 2; const startY = (nextCanvas.height - shapeHeight) / 2; shape.forEach((row, y) => { row.forEach((value, x) => { if (value) { nextContext.fillRect(startX + x * BLOCK_SIZE, startY + y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE); nextContext.strokeRect(startX + x * BLOCK_SIZE, startY + y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE); } }); }); } }draw(): 게임의 모든 시각적 요소들을 캔버스에 렌더링하는 함수이다.context.clearRect(): 매 프레임마다 이전 그림을 제거하여 잔상을 방지한다.board배열을 순회하며 이미 고정된 블록들을 게임 보드에 그린다. 각 블록의 색상(context.fillStyle)과 위치/크기(context.fillRect,context.strokeRect)를 설정하여 사각형 형태로 표현한다.currentPiece(현재 조작 중인 블록)와nextPiece(다음에 출현할 블록)를 각각의 지정된 캔버스에 그린다.nextCanvas의 경우, 블록을 중앙에 배치하는 로직이 포함되어 있어 분리된 UI 요소를 효율적으로 관리한다.
3.5. 게임 루프 (
gameLoop)function gameLoop(time = 0) { if (gameOver) { return; } const deltaTime = time - lastTime; lastTime = time; dropCounter += deltaTime; if (dropCounter > dropInterval) { if (currentPiece) { if (!movePiece(0, 1)) { lockPiece(); removeLines(); if (isGameOver()) { gameOver = true; alert('Game Over'); return; } currentPiece = nextPiece; nextPiece = newPiece(); } } dropCounter = 0; } draw(); requestAnimationFrame(gameLoop); }gameLoop(time = 0): 게임의 핵심적인 흐름을 제어하는 함수로서,requestAnimationFrame을 통해 브라우저의 화면 재생 빈도에 맞추어 호출되어 부드러운 애니메이션 효과를 구현한다.deltaTime을 계산하여 마지막 프레임 이후의 경과 시간을 파악하고, 이를dropCounter에 누적한다.dropCounter가dropInterval을 초과할 경우, 블록을 한 칸 아래로 이동(movePiece(0, 1))시킨다.movePiece호출이 실패하면 (즉, 블록이 더 이상 하강할 수 없으면)lockPiece()를 호출하여 블록을 보드에 고정하고,removeLines()를 호출하여 완성된 줄을 제거한다.isGameOver()를 통해 게임 종료 여부를 확인하고, 게임이 종료되면 경고 메시지를 표시하고 루프를 중단한다.- 새로운
currentPiece와nextPiece를 준비한다. draw()를 호출하여 화면을 업데이트한다.
3.6. 새 블록 생성 (
newPiece)function newPiece() { const piece = PIECES[Math.floor(Math.random() * PIECES.length)]; return { ...piece, x: Math.floor(COLS / 2) - Math.floor(piece.shape[0].length / 2), y: 0 }; }newPiece():PIECES배열에서 무작위로 하나의 블록을 선택한 후, 게임 보드의 상단 중앙에 위치하도록 초기x,y좌표를 설정하여 해당 블록 객체를 반환한다.
3.7. 블록 이동 제어 (
movePiece)function movePiece(dx, dy) { if (isValidMove(currentPiece.shape, currentPiece.x + dx, currentPiece.y + dy)) { currentPiece.x += dx; currentPiece.y += dy; return true; } return false; }movePiece(dx, dy): 현재 블록을dx만큼 수평으로,dy만큼 수직으로 이동시킨다. 이동하기에 앞서isValidMove함수를 호출하여 이동할 위치가 유효한지 검사한다. 이동이 유효하면 블록의 위치를 갱신하고true를 반환하며, 그렇지 않을 경우false를 반환한다.
3.8. 블록 회전 제어 (
rotatePiece)function rotatePiece() { const shape = currentPiece.shape; const newShape = shape[0].map((_, colIndex) => shape.map(row => row[colIndex]).reverse()); if (isValidMove(newShape, currentPiece.x, currentPiece.y)) { currentPiece.shape = newShape; } }rotatePiece(): 현재 블록을 시계 방향으로 90도 회전시킨다. 회전된 새로운 형태(newShape)가 유효한 위치에 놓이는지isValidMove로 확인한 후, 유효할 경우currentPiece.shape를 갱신한다.
3.9. 유효한 이동 판단 (
isValidMove)function isValidMove(shape, x, y) { for (let row = 0; row < shape.length; row++) { for (let col = 0; col < shape[row].length; col++) { if (shape[row][col]) { const newX = x + col; const newY = y + row; if (newX < 0 || newX >= COLS || newY >= ROWS || (newY >= 0 && board[newY] && board[newY][newX])) { return false; } } } } return true; }isValidMove(shape, x, y): 주어진shape(블록의 형태)가 특정x,y좌표에 배치되었을 때 게임 보드의 경계를 벗어나거나 이미 채워진 다른 블록과 겹치는지 여부를 판단하는 함수이다. 충돌이 발생하면false를 반환하며, 그렇지 않으면true를 반환한다.
3.10. 블록 고정 (
lockPiece)function lockPiece() { currentPiece.shape.forEach((row, y) => { row.forEach((value, x) => { if (value) { board[currentPiece.y + y][currentPiece.x + x] = currentPiece.color; } }); }); }lockPiece(): 현재 블록이 더 이상 하강할 수 없을 때, 해당 블록을 게임 보드(board)에 영구적으로 고정시키는 함수이다. 블록의 각 구성 칸에 해당하는 보드 셀에 블록의 색상을 기록한다.
3.11. 줄 제거 및 점수 산정 (
removeLines)function removeLines() { let linesRemoved = 0; outer: for (let row = ROWS - 1; row >= 0; row--) { for (let col = 0; col < COLS; col++) { if (board[row][col] === 0) { continue outer; } } linesRemoved++; const newRow = Array(COLS).fill(0); board.splice(row, 1); board.unshift(newRow); row++; } if (linesRemoved > 0) { score += linesRemoved * 10; lines += linesRemoved; if (lines >= 10) { level++; lines -= 10; dropInterval *= 0.9; } scoreElement.innerText = score; levelElement.innerText = level; } }removeLines(): 게임 보드를 하단에서 상단으로 스캔하며 완성된 줄의 존재 여부를 확인하고 이를 제거하는 함수이다.- 완성된 줄을 발견할 경우, 해당 줄을
splice메서드를 통해 제거하고,unshift를 사용하여 보드 최상단에 새로운 빈 줄을 추가한다. - 제거된 줄의 수(
linesRemoved)에 비례하여score가 증가하며,lines(총 제거된 줄의 수)가 10 이상이 되면level이 상승하고dropInterval을 감소시켜 블록 낙하 속도를 가속화함으로써 게임의 난이도를 높인다. - 갱신된 점수와 레벨은 HTML 요소에 반영된다.
- 완성된 줄을 발견할 경우, 해당 줄을
3.12. 게임 종료 판정 (
isGameOver)function isGameOver() { return board[0].some(cell => cell); }isGameOver(): 게임 오버 조건을 판정하는 함수이다. 게임 보드의 가장 최상단 줄(board[0])에 블록이 하나라도 존재한다면 게임 오버로 간주하고true를 반환한다.
3.13. 하드 드롭 기능 (
hardDrop)function hardDrop() { while (movePiece(0, 1)); lockPiece(); removeLines(); if (isGameOver()) { gameOver = true; alert('Game Over'); return; } currentPiece = nextPiece; nextPiece = newPiece(); }hardDrop(): 스페이스바가 눌렸을 때 현재 블록을 즉시 바닥으로 하강시키는 기능이다.while (movePiece(0, 1));:movePiece(0, 1)(블록을 한 칸 아래로 이동시키는 명령)이false를 반환할 때까지 (즉, 블록이 더 이상 하강할 수 없을 때까지) 반복적으로movePiece함수를 호출하여 블록을 빠르게 내린다.- 이후
lockPiece(),removeLines(),isGameOver()를 순차적으로 호출하여 블록을 고정하고 줄을 제거하며 게임 종료 여부를 확인한다. - 다음 블록을 준비한다.
3.14. 이벤트 리스너 (키보드 및 버튼 제어)
document.addEventListener('keydown', event => { if (gameOver) return; switch (event.code) { case 'ArrowLeft': movePiece(-1, 0); break; case 'ArrowRight': movePiece(1, 0); break; case 'ArrowDown': movePiece(0, 1); dropCounter = 0; break; case 'ArrowUp': rotatePiece(); break; case 'Space': event.preventDefault(); // 스페이스바의 기본 동작 방지 hardDrop(); break; } draw(); }); function startGame() { board = createBoard(); score = 0; level = 1; lines = 0; gameOver = false; scoreElement.innerText = score; levelElement.innerText = level; currentPiece = newPiece(); nextPiece = newPiece(); lastTime = 0; dropCounter = 0; dropInterval = 1000; gameLoop(); } startButton.addEventListener('click', startGame);document.addEventListener('keydown', event => { ... }): 사용자가 키보드를 누를 때 발생하는 이벤트를 감지하는 리스너이다.event.code를 활용하여 어떤 키가 눌렸는지 확인하며, 방향키(ArrowLeft,ArrowRight,ArrowDown,ArrowUp)에 따라movePiece또는rotatePiece함수를 호출한다.Space키가 눌렸을 경우,event.preventDefault()를 호출하여 스페이스바의 기본 동작(예: 포커스된 버튼의 활성화)을 제어하고,hardDrop()함수를 실행하여 블록을 즉시 하강시킨다.- 모든 키 입력 처리 후
draw()를 호출하여 화면을 즉시 갱신한다.
startGame(): 게임을 초기화하고 시작하는 함수이다. 보드, 점수, 레벨, 게임 오버 상태 등을 초기화하며, 새로운 블록을 생성한 후gameLoop()를 호출하여 게임을 개시한다.startButton.addEventListener('click', startGame): "게임 시작" 버튼이 클릭될 때startGame함수를 호출하도록 이벤트 리스너를 설정한다.
결론
이처럼 웹의 근간을 이루는 표준 기술인 HTML5, CSS3, 그리고 JavaScript만을 활용하여도 충분히 동적이고 흥미로운 게임을 구현할 수 있음을 본 사례는 명확히 보여준다. 특히 JavaScript와 Canvas API의 유기적인 조합을 통한 그래픽 처리와
requestAnimationFrame을 이용한 부드러운 애니메이션 구현, 그리고 섬세한 이벤트 핸들링은 웹 기반 테트리스 게임 개발의 핵심적인 성공 요인으로 작용한다.
전체 코드:
script.jsconst COLS = 10; const ROWS = 20; const BLOCK_SIZE = 20; // 초기 설정 const canvas = document.getElementById('tetris'); const context = canvas.getContext('2d'); canvas.width = COLS * BLOCK_SIZE; canvas.height = ROWS * BLOCK_SIZE; const nextCanvas = document.getElementById('next'); const nextContext = nextCanvas.getContext('2d'); nextCanvas.width = 4 * BLOCK_SIZE; nextCanvas.height = 4 * BLOCK_SIZE; const scoreElement = document.getElementById('score'); const levelElement = document.getElementById('level'); const startButton = document.getElementById('start-button'); // 게임 변수 let board = []; let score = 0; let level = 1; let lines = 0; let gameOver = false; let currentPiece; let nextPiece; let dropCounter = 0; let dropInterval = 1000; // 1초마다 한 칸씩 떨어집니다. let lastTime = 0; const PIECES = [ { shape: [[1, 1, 1, 1]], // I color: 'cyan' }, { shape: [[1, 1, 0], [0, 1, 1]], // Z color: 'red' }, { shape: [[0, 1, 1], [1, 1, 0]], // S color: 'green' }, { shape: [[1, 1, 1], [0, 0, 1]], // L color: 'orange' }, { shape: [[1, 1, 1], [1, 0, 0]], // J color: 'blue' }, { shape: [[1, 1, 1], [0, 1, 0]], // T color: 'purple' }, { shape: [[1, 1], [1, 1]], // O color: 'yellow' } ]; function createBoard() { return Array.from({ length: ROWS }, () => Array(COLS).fill(0)); } function draw() { // Draw board context.clearRect(0, 0, canvas.width, canvas.height); for (let row = 0; row < ROWS; row++) { for (let col = 0; col < COLS; col++) { if (board[row][col]) { context.fillStyle = board[row][col]; context.fillRect(col * BLOCK_SIZE, row * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE); context.strokeRect(col * BLOCK_SIZE, row * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE); } } } // Draw current piece if (currentPiece) { context.fillStyle = currentPiece.color; currentPiece.shape.forEach((row, y) => { row.forEach((value, x) => { if (value) { context.fillRect((currentPiece.x + x) * BLOCK_SIZE, (currentPiece.y + y) * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE); context.strokeRect((currentPiece.x + x) * BLOCK_SIZE, (currentPiece.y + y) * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE); } }); }); } // Draw next piece nextContext.clearRect(0, 0, nextCanvas.width, nextCanvas.height); if (nextPiece) { nextContext.fillStyle = nextPiece.color; const shape = nextPiece.shape; const shapeWidth = shape[0].length * BLOCK_SIZE; const shapeHeight = shape.length * BLOCK_SIZE; const startX = (nextCanvas.width - shapeWidth) / 2; const startY = (nextCanvas.height - shapeHeight) / 2; shape.forEach((row, y) => { row.forEach((value, x) => { if (value) { nextContext.fillRect(startX + x * BLOCK_SIZE, startY + y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE); nextContext.strokeRect(startX + x * BLOCK_SIZE, startY + y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE); } }); }); } } function gameLoop(time = 0) { if (gameOver) { return; } const deltaTime = time - lastTime; lastTime = time; dropCounter += deltaTime; if (dropCounter > dropInterval) { if (currentPiece) { if (!movePiece(0, 1)) { lockPiece(); removeLines(); if (isGameOver()) { gameOver = true; alert('Game Over'); return; } currentPiece = nextPiece; nextPiece = newPiece(); } } dropCounter = 0; } draw(); requestAnimationFrame(gameLoop); } function newPiece() { const piece = PIECES[Math.floor(Math.random() * PIECES.length)]; return { ...piece, x: Math.floor(COLS / 2) - Math.floor(piece.shape[0].length / 2), y: 0 }; } function movePiece(dx, dy) { if (isValidMove(currentPiece.shape, currentPiece.x + dx, currentPiece.y + dy)) { currentPiece.x += dx; currentPiece.y += dy; return true; } return false; } function rotatePiece() { const shape = currentPiece.shape; const newShape = shape[0].map((_, colIndex) => shape.map(row => row[colIndex]).reverse()); if (isValidMove(newShape, currentPiece.x, currentPiece.y)) { currentPiece.shape = newShape; } } function isValidMove(shape, x, y) { for (let row = 0; row < shape.length; row++) { for (let col = 0; col < shape[row].length; col++) { if (shape[row][col]) { const newX = x + col; const newY = y + row; if (newX < 0 || newX >= COLS || newY >= ROWS || (newY >= 0 && board[newY] && board[newY][newX])) { return false; } } } } return true; } function lockPiece() { currentPiece.shape.forEach((row, y) => { row.forEach((value, x) => { if (value) { board[currentPiece.y + y][currentPiece.x + x] = currentPiece.color; } }); }); } function removeLines() { let linesRemoved = 0; outer: for (let row = ROWS - 1; row >= 0; row--) { for (let col = 0; col < COLS; col++) { if (board[row][col] === 0) { continue outer; } } linesRemoved++; const newRow = Array(COLS).fill(0); board.splice(row, 1); board.unshift(newRow); row++; } if (linesRemoved > 0) { score += linesRemoved * 10; lines += linesRemoved; if (lines >= 10) { level++; lines -= 10; dropInterval *= 0.9; } scoreElement.innerText = score; levelElement.innerText = level; } } function isGameOver() { return board[0].some(cell => cell); } function hardDrop() { while (movePiece(0, 1)); lockPiece(); removeLines(); if (isGameOver()) { gameOver = true; alert('Game Over'); return; } currentPiece = nextPiece; nextPiece = newPiece(); } document.addEventListener('keydown', event => { if (gameOver) return; switch (event.code) { case 'ArrowLeft': movePiece(-1, 0); break; case 'ArrowRight': movePiece(1, 0); break; case 'ArrowDown': movePiece(0, 1); dropCounter = 0; break; case 'ArrowUp': rotatePiece(); break; case 'Space': event.preventDefault(); // 스페이스바의 기본 동작 방지 hardDrop(); break; } draw(); }); function startGame() { board = createBoard(); score = 0; level = 1; lines = 0; gameOver = false; scoreElement.innerText = score; levelElement.innerText = level; currentPiece = newPiece(); nextPiece = newPiece(); lastTime = 0; dropCounter = 0; dropInterval = 1000; gameLoop(); } startButton.addEventListener('click', startGame);728x90반응형'언어·프레임워크 > JavaScript' 카테고리의 다른 글
[JavaScript] 프로토타입 이해하기 (1) 2025.06.10 [JavaScript] 커링 함수 이해하기 (0) 2025.06.10 [JavaScript] 객체 유효성 검사로 Proxy 이해하기 (0) 2025.04.23 [JavaScript] 리플렉트(Reflect) 완벽 정복: JavaScript 메타 프로그래밍의 숨겨진 능력 (1) 2025.04.21 [JavaScript] JavaScript 배열 메서드, 무엇을 바꾸고 무엇을 반환할까? (0) 2025.04.18 다음글이 없습니다.이전글이 없습니다.댓글