|
|
|
|
|
const canvas = document.getElementById('tetris-board'); |
|
|
const context = canvas.getContext('2d'); |
|
|
const scoreElement = document.getElementById('score'); |
|
|
const startButton = document.getElementById('start-button'); |
|
|
|
|
|
|
|
|
const ROWS = 20; |
|
|
const COLS = 10; |
|
|
const BLOCK_SIZE = 30; |
|
|
|
|
|
context.canvas.width = COLS * BLOCK_SIZE; |
|
|
context.canvas.height = ROWS * BLOCK_SIZE; |
|
|
|
|
|
|
|
|
const COLORS = [null, '#FF0D72', '#0DC2FF', '#0DFF72', '#F538FF', '#FF8E0D', '#FFE138', '#3877FF']; |
|
|
const SHAPES = [ |
|
|
[], |
|
|
[[1, 1, 1, 1]], |
|
|
[[1, 1, 1], [0, 1, 0]], |
|
|
[[1, 1, 1], [1, 0, 0]], |
|
|
[[1, 1, 1], [0, 0, 1]], |
|
|
[[1, 1, 0], [0, 1, 1]], |
|
|
[[0, 1, 1], [1, 1, 0]], |
|
|
[[1, 1], [1, 1]] |
|
|
]; |
|
|
|
|
|
let board = Array.from({ length: ROWS }, () => Array(COLS).fill(0)); |
|
|
let score = 0; |
|
|
let gameOver = false; |
|
|
let currentPiece; |
|
|
let gameInterval; |
|
|
|
|
|
|
|
|
class Piece { |
|
|
constructor(shape, color) { |
|
|
this.shape = shape; |
|
|
this.color = color; |
|
|
this.x = Math.floor(COLS / 2) - Math.floor(shape[0].length / 2); |
|
|
this.y = 0; |
|
|
} |
|
|
|
|
|
draw() { |
|
|
context.fillStyle = this.color; |
|
|
this.shape.forEach((row, y) => { |
|
|
row.forEach((value, x) => { |
|
|
if (value > 0) { |
|
|
context.fillRect((this.x + x) * BLOCK_SIZE, (this.y + y) * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE); |
|
|
} |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
move(dx, dy) { |
|
|
this.x += dx; |
|
|
this.y += dy; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function generateNewPiece() { |
|
|
const rand = Math.floor(Math.random() * (SHAPES.length - 1)) + 1; |
|
|
const shape = SHAPES[rand]; |
|
|
const color = COLORS[rand]; |
|
|
return new Piece(shape, color); |
|
|
} |
|
|
|
|
|
|
|
|
function drawBoard() { |
|
|
board.forEach((row, y) => { |
|
|
row.forEach((value, x) => { |
|
|
if (value > 0) { |
|
|
context.fillStyle = COLORS[value]; |
|
|
context.fillRect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE); |
|
|
} |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function draw() { |
|
|
context.clearRect(0, 0, canvas.width, canvas.height); |
|
|
drawBoard(); |
|
|
if (currentPiece) { |
|
|
currentPiece.draw(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function isValidMove(piece, dx = 0, dy = 0) { |
|
|
return piece.shape.every((row, y) => { |
|
|
return row.every((value, x) => { |
|
|
if (value === 0) { |
|
|
return true; |
|
|
} |
|
|
const newX = piece.x + x + dx; |
|
|
const newY = piece.y + y + dy; |
|
|
return newX >= 0 && newX < COLS && newY < ROWS && (board[newY] && board[newY][newX] === 0); |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function rotatePiece() { |
|
|
const shape = currentPiece.shape; |
|
|
const newShape = shape[0].map((_, colIndex) => shape.map(row => row[colIndex]).reverse()); |
|
|
|
|
|
const originalX = currentPiece.x; |
|
|
let offsetX = 1; |
|
|
|
|
|
const tempPiece = { ...currentPiece, shape: newShape }; |
|
|
|
|
|
if (isValidMove(tempPiece)) { |
|
|
currentPiece.shape = newShape; |
|
|
return; |
|
|
} |
|
|
|
|
|
while (true) { |
|
|
if (isValidMove(tempPiece, offsetX)) { |
|
|
currentPiece.x += offsetX; |
|
|
currentPiece.shape = newShape; |
|
|
return; |
|
|
} |
|
|
if (offsetX > 0) { |
|
|
if (offsetX > tempPiece.shape[0].length) break; |
|
|
offsetX = -(offsetX + 1); |
|
|
} else { |
|
|
if (Math.abs(offsetX) > tempPiece.shape[0].length) break; |
|
|
offsetX = -(offsetX - 1); |
|
|
} |
|
|
} |
|
|
currentPiece.x = originalX; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function lockPiece() { |
|
|
currentPiece.shape.forEach((row, y) => { |
|
|
row.forEach((value, x) => { |
|
|
if (value > 0) { |
|
|
const boardY = currentPiece.y + y; |
|
|
const boardX = currentPiece.x + x; |
|
|
if (boardY < ROWS) { |
|
|
const colorIndex = COLORS.indexOf(currentPiece.color); |
|
|
board[boardY][boardX] = colorIndex > 0 ? colorIndex : 1; |
|
|
} |
|
|
} |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function clearLines() { |
|
|
let linesCleared = 0; |
|
|
for (let y = ROWS - 1; y >= 0; y--) { |
|
|
if (board[y].every(value => value > 0)) { |
|
|
linesCleared++; |
|
|
board.splice(y, 1); |
|
|
board.unshift(Array(COLS).fill(0)); |
|
|
y++; |
|
|
} |
|
|
} |
|
|
if (linesCleared > 0) { |
|
|
score += linesCleared * 100; |
|
|
scoreElement.textContent = score; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function checkGameOver() { |
|
|
if (!isValidMove(currentPiece)) { |
|
|
gameOver = true; |
|
|
clearInterval(gameInterval); |
|
|
alert(`Game Over! Your score: ${score}`); |
|
|
startButton.textContent = "Restart Game"; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function hardDrop() { |
|
|
|
|
|
while (isValidMove(currentPiece, 0, 1)) { |
|
|
currentPiece.move(0, 1); |
|
|
score += 2; |
|
|
} |
|
|
scoreElement.textContent = score; |
|
|
|
|
|
gameLoop(); |
|
|
} |
|
|
|
|
|
|
|
|
function gameLoop() { |
|
|
if (gameOver) return; |
|
|
|
|
|
if (isValidMove(currentPiece, 0, 1)) { |
|
|
currentPiece.move(0, 1); |
|
|
} else { |
|
|
lockPiece(); |
|
|
clearLines(); |
|
|
currentPiece = generateNewPiece(); |
|
|
checkGameOver(); |
|
|
} |
|
|
draw(); |
|
|
} |
|
|
|
|
|
|
|
|
document.addEventListener('keydown', (event) => { |
|
|
if (gameOver || !currentPiece) return; |
|
|
|
|
|
switch (event.key) { |
|
|
case 'ArrowLeft': |
|
|
if (isValidMove(currentPiece, -1, 0)) currentPiece.move(-1, 0); |
|
|
break; |
|
|
case 'ArrowRight': |
|
|
if (isValidMove(currentPiece, 1, 0)) currentPiece.move(1, 0); |
|
|
break; |
|
|
case 'ArrowDown': |
|
|
if (isValidMove(currentPiece, 0, 1)) { |
|
|
currentPiece.move(0, 1); |
|
|
score += 1; |
|
|
scoreElement.textContent = score; |
|
|
} else { |
|
|
gameLoop(); |
|
|
} |
|
|
break; |
|
|
case 'ArrowUp': |
|
|
rotatePiece(); |
|
|
break; |
|
|
case ' ': |
|
|
event.preventDefault(); |
|
|
hardDrop(); |
|
|
break; |
|
|
} |
|
|
draw(); |
|
|
}); |
|
|
|
|
|
|
|
|
function startGame() { |
|
|
board = Array.from({ length: ROWS }, () => Array(COLS).fill(0)); |
|
|
score = 0; |
|
|
scoreElement.textContent = score; |
|
|
gameOver = false; |
|
|
currentPiece = generateNewPiece(); |
|
|
|
|
|
if (gameInterval) clearInterval(gameInterval); |
|
|
gameInterval = setInterval(gameLoop, 800); |
|
|
|
|
|
draw(); |
|
|
startButton.textContent = "Start Game"; |
|
|
} |
|
|
|
|
|
startButton.addEventListener('click', startGame); |
|
|
|
|
|
|
|
|
context.fillStyle = 'white'; |
|
|
context.font = '20px Arial'; |
|
|
context.textAlign = 'center'; |
|
|
context.fillText('Click "Start Game" to play!', canvas.width / 2, canvas.height / 2); |