Spaces:
Running
Running
| document.addEventListener('DOMContentLoaded', () => { | |
| const canvas = document.getElementById('tetris'); | |
| const ctx = canvas.getContext('2d'); | |
| // Scale canvas | |
| const scale = window.devicePixelRatio; | |
| canvas.width = Math.floor(canvas.clientWidth * scale); | |
| canvas.height = Math.floor(canvas.clientHeight * scale); | |
| ctx.scale(scale, scale); | |
| // Game state | |
| const COLS = 10; | |
| const ROWS = 20; | |
| const BLOCK_SIZE = canvas.width / 10 / scale; | |
| const COLORS = [ | |
| null, | |
| '#3B82F6', // I | |
| '#8B5CF6', // J | |
| '#F59E0B', // L | |
| '#10B981', // O | |
| '#EF4444', // S | |
| '#EC4899', // T | |
| '#84CC16' // Z | |
| ]; | |
| let board = createBoard(); | |
| let piece = null; | |
| let score = 0; | |
| let gameOver = false; | |
| let dropCounter = 0; | |
| let lastTime = 0; | |
| let dropInterval = 1000; | |
| // Pieces | |
| const PIECES = [ | |
| null, | |
| { shape: [[0,0,0,0], [1,1,1,1], [0,0,0,0], [0,0,0,0]], color: 'I' }, | |
| { shape: [[2,0,0], [2,2,2], [0,0,0]], color: 'J' }, | |
| { shape: [[0,0,3], [3,3,3], [0,0,0]], color: 'L' }, | |
| { shape: [[0,4,4], [0,4,4], [0,0,0]], color: 'O' }, | |
| { shape: [[0,5,5], [5,5,0], [0,0,0]], color: 'S' }, | |
| { shape: [[0,6,0], [6,6,6], [0,0,0]], color: 'T' }, | |
| { shape: [[7,7,0], [0,7,7], [0,0,0]], color: 'Z' } | |
| ]; | |
| // Game functions | |
| function createBoard() { | |
| return Array.from({length: ROWS}, () => Array(COLS).fill(0)); | |
| } | |
| function createPiece() { | |
| const randomIndex = Math.floor(Math.random() * 7) + 1; | |
| return { | |
| position: {x: Math.floor(COLS / 2) - 1, y: 0}, | |
| shape: PIECES[randomIndex].shape, | |
| color: PIECES[randomIndex].color | |
| }; | |
| } | |
| function draw() { | |
| // Clear canvas | |
| ctx.fillStyle = '#111827'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| // Draw board | |
| board.forEach((row, y) => { | |
| row.forEach((value, x) => { | |
| if (value !== 0) { | |
| ctx.fillStyle = COLORS[value]; | |
| ctx.fillRect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE); | |
| ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)'; | |
| ctx.strokeRect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE); | |
| } | |
| }); | |
| }); | |
| // Draw current piece | |
| if (piece) { | |
| piece.shape.forEach((row, y) => { | |
| row.forEach((value, x) => { | |
| if (value !== 0) { | |
| ctx.fillStyle = COLORS[value]; | |
| ctx.fillRect( | |
| (piece.position.x + x) * BLOCK_SIZE, | |
| (piece.position.y + y) * BLOCK_SIZE, | |
| BLOCK_SIZE, | |
| BLOCK_SIZE | |
| ); | |
| ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)'; | |
| ctx.strokeRect( | |
| (piece.position.x + x) * BLOCK_SIZE, | |
| (piece.position.y + y) * BLOCK_SIZE, | |
| BLOCK_SIZE, | |
| BLOCK_SIZE | |
| ); | |
| } | |
| }); | |
| }); | |
| } | |
| } | |
| function collide() { | |
| for (let y = 0; y < piece.shape.length; y++) { | |
| for (let x = 0; x < piece.shape[y].length; x++) { | |
| if (piece.shape[y][x] !== 0 && | |
| (board[y + piece.position.y] === undefined || | |
| board[y + piece.position.y][x + piece.position.x] === undefined || | |
| board[y + piece.position.y][x + piece.position.x] !== 0)) { | |
| return true; | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| function rotate() { | |
| const rotated = []; | |
| for (let y = 0; y < piece.shape[0].length; y++) { | |
| const row = []; | |
| for (let x = piece.shape.length - 1; x >= 0; x--) { | |
| row.push(piece.shape[x][y]); | |
| } | |
| rotated.push(row); | |
| } | |
| const previousShape = piece.shape; | |
| piece.shape = rotated; | |
| if (collide()) { | |
| piece.shape = previousShape; | |
| } | |
| } | |
| function movePiece(direction) { | |
| piece.position.x += direction; | |
| if (collide()) { | |
| piece.position.x -= direction; | |
| } | |
| } | |
| function hardDrop() { | |
| while (!collide()) { | |
| piece.position.y++; | |
| } | |
| piece.position.y--; | |
| merge(); | |
| removeRows(); | |
| piece = createPiece(); | |
| if (collide()) { | |
| gameOver = true; | |
| document.querySelector('custom-game-over').setAttribute('visible', 'true'); | |
| } | |
| } | |
| function merge() { | |
| piece.shape.forEach((row, y) => { | |
| row.forEach((value, x) => { | |
| if (value !== 0) { | |
| board[y + piece.position.y][x + piece.position.x] = value; | |
| } | |
| }); | |
| }); | |
| } | |
| function removeRows() { | |
| let linesCleared = 0; | |
| outer: for (let y = board.length - 1; y >= 0; y--) { | |
| for (let x = 0; x < board[y].length; x++) { | |
| if (board[y][x] === 0) { | |
| continue outer; | |
| } | |
| } | |
| const row = board.splice(y, 1)[0].fill(0); | |
| board.unshift(row); | |
| y++; | |
| linesCleared++; | |
| } | |
| if (linesCleared > 0) { | |
| score += calculateScore(linesCleared); | |
| updateScoreDisplay(); | |
| } | |
| } | |
| function calculateScore(lines) { | |
| switch(lines) { | |
| case 1: return 100; | |
| case 2: return 300; | |
| case 3: return 500; | |
| case 4: return 800; | |
| default: return 0; | |
| } | |
| } | |
| function updateScoreDisplay() { | |
| document.querySelector('custom-score-display').setAttribute('score', score.toString()); | |
| } | |
| function resetGame() { | |
| board = createBoard(); | |
| piece = createPiece(); | |
| score = 0; | |
| gameOver = false; | |
| dropInterval = 1000; | |
| updateScoreDisplay(); | |
| document.querySelector('custom-game-over').setAttribute('visible', 'false'); | |
| } | |
| // Game loop | |
| function update(time = 0) { | |
| if (gameOver) return; | |
| const deltaTime = time - lastTime; | |
| lastTime = time; | |
| dropCounter += deltaTime; | |
| if (dropCounter > dropInterval) { | |
| piece.position.y++; | |
| if (collide()) { | |
| piece.position.y--; | |
| merge(); | |
| removeRows(); | |
| piece = createPiece(); | |
| if (collide()) { | |
| gameOver = true; | |
| document.querySelector('custom-game-over').setAttribute('visible', 'true'); | |
| } | |
| } | |
| dropCounter = 0; | |
| } | |
| draw(); | |
| requestAnimationFrame(update); | |
| } | |
| // Controls | |
| document.addEventListener('keydown', event => { | |
| if (gameOver) return; | |
| switch(event.keyCode) { | |
| case 37: // Left | |
| movePiece(-1); | |
| break; | |
| case 39: // Right | |
| movePiece(1); | |
| break; | |
| case 40: // Down | |
| piece.position.y++; | |
| if (collide()) { | |
| piece.position.y--; | |
| merge(); | |
| removeRows(); | |
| piece = createPiece(); | |
| if (collide()) { | |
| gameOver = true; | |
| document.querySelector('custom-game-over').setAttribute('visible', 'true'); | |
| } | |
| } | |
| dropCounter = 0; | |
| break; | |
| case 38: // Up | |
| rotate(); | |
| break; | |
| case 32: // Space | |
| hardDrop(); | |
| break; | |
| case 80: // P | |
| // Pause logic would go here | |
| break; | |
| } | |
| }); | |
| // Initialize game | |
| piece = createPiece(); | |
| update(); | |
| // Custom element event listeners | |
| document.addEventListener('start-game', resetGame); | |
| document.addEventListener('reset-game', resetGame); | |
| }); |