Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Classic Tetris</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| font-family: 'Arial', sans-serif; | |
| } | |
| body { | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| min-height: 100vh; | |
| background: linear-gradient(135deg, #1e1e2f, #2d2d44); | |
| color: #fff; | |
| overflow: hidden; | |
| } | |
| .game-container { | |
| display: flex; | |
| gap: 30px; | |
| align-items: flex-start; | |
| } | |
| #game-board { | |
| border: 4px solid #4a4a6b; | |
| border-radius: 5px; | |
| display: grid; | |
| grid-template-rows: repeat(20, 1fr); | |
| grid-template-columns: repeat(10, 1fr); | |
| gap: 1px; | |
| background-color: #2a2a3a; | |
| box-shadow: 0 0 20px rgba(0, 0, 0, 0.3); | |
| width: 300px; | |
| height: 600px; | |
| } | |
| .cell { | |
| border: 1px solid rgba(255, 255, 255, 0.05); | |
| background-color: #2a2a3a; | |
| } | |
| .controls { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 30px; | |
| } | |
| .info-panel { | |
| background-color: #2a2a3a; | |
| border: 4px solid #4a4a6b; | |
| border-radius: 5px; | |
| padding: 20px; | |
| width: 180px; | |
| box-shadow: 0 0 15px rgba(0, 0, 0, 0.2); | |
| } | |
| .next-piece-container { | |
| width: 120px; | |
| height: 120px; | |
| display: grid; | |
| grid-template-rows: repeat(4, 1fr); | |
| grid-template-columns: repeat(4, 1fr); | |
| gap: 2px; | |
| margin-top: 10px; | |
| margin-bottom: 20px; | |
| } | |
| .next-cell { | |
| background-color: #3a3a4a; | |
| border-radius: 2px; | |
| } | |
| h2 { | |
| font-size: 18px; | |
| margin-bottom: 10px; | |
| color: #ddd; | |
| } | |
| .score-display { | |
| font-size: 24px; | |
| margin-bottom: 15px; | |
| color: #fff; | |
| } | |
| .level-display { | |
| font-size: 18px; | |
| margin-bottom: 15px; | |
| color: #fff; | |
| } | |
| .controls-info { | |
| background-color: #2a2a3a; | |
| border: 4px solid #4a4a6b; | |
| border-radius: 5px; | |
| padding: 20px; | |
| width: 180px; | |
| box-shadow: 0 0 15px rgba(0, 0, 0, 0.2); | |
| } | |
| .control-item { | |
| display: flex; | |
| justify-content: space-between; | |
| margin-bottom: 10px; | |
| font-size: 14px; | |
| color: #ccc; | |
| } | |
| .key { | |
| background-color: #4a4a6b; | |
| color: #fff; | |
| padding: 3px 8px; | |
| border-radius: 4px; | |
| font-family: monospace; | |
| } | |
| .game-over { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-color: rgba(0, 0, 0, 0.8); | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 10; | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: opacity 0.3s; | |
| } | |
| .game-over.show { | |
| opacity: 1; | |
| pointer-events: all; | |
| } | |
| .game-over h1 { | |
| font-size: 42px; | |
| margin-bottom: 20px; | |
| color: #ff5555; | |
| } | |
| .final-score { | |
| font-size: 24px; | |
| margin-bottom: 30px; | |
| } | |
| .restart-btn { | |
| background-color: #4CAF50; | |
| color: white; | |
| border: none; | |
| padding: 12px 24px; | |
| font-size: 16px; | |
| cursor: pointer; | |
| border-radius: 4px; | |
| transition: background-color 0.3s; | |
| } | |
| .restart-btn:hover { | |
| background-color: #45a049; | |
| } | |
| .tetromino-i { background-color: #00f0f0; } | |
| .tetromino-j { background-color: #0000f0; } | |
| .tetromino-l { background-color: #f0a000; } | |
| .tetromino-o { background-color: #f0f000; } | |
| .tetromino-s { background-color: #00f000; } | |
| .tetromino-t { background-color: #a000f0; } | |
| .tetromino-z { background-color: #f00000; } | |
| .tetromino-i.ghost { background-color: rgba(0, 240, 240, 0.2); } | |
| .tetromino-j.ghost { background-color: rgba(0, 0, 240, 0.2); } | |
| .tetromino-l.ghost { background-color: rgba(240, 160, 0, 0.2); } | |
| .tetromino-o.ghost { background-color: rgba(240, 240, 0, 0.2); } | |
| .tetromino-s.ghost { background-color: rgba(0, 240, 0, 0.2); } | |
| .tetromino-t.ghost { background-color: rgba(160, 0, 240, 0.2); } | |
| .tetromino-z.ghost { background-color: rgba(240, 0, 0, 0.2); } | |
| @media (max-width: 768px) { | |
| .game-container { | |
| flex-direction: column; | |
| align-items: center; | |
| } | |
| .controls { | |
| flex-direction: row; | |
| margin-top: 20px; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="game-container"> | |
| <div id="game-board"></div> | |
| <div class="controls"> | |
| <div class="info-panel"> | |
| <h2>Next Piece</h2> | |
| <div class="next-piece-container" id="next-piece"></div> | |
| <h2>Score</h2> | |
| <div class="score-display" id="score">0</div> | |
| <h2>Level</h2> | |
| <div class="level-display" id="level">1</div> | |
| </div> | |
| <div class="controls-info"> | |
| <h2>Controls</h2> | |
| <div class="control-item"> | |
| <span>Move Left</span> | |
| <span class="key">←</span> | |
| </div> | |
| <div class="control-item"> | |
| <span>Move Right</span> | |
| <span class="key">→</span> | |
| </div> | |
| <div class="control-item"> | |
| <span>Rotate</span> | |
| <span class="key">↑</span> | |
| </div> | |
| <div class="control-item"> | |
| <span>Soft Drop</span> | |
| <span class="key">↓</span> | |
| </div> | |
| <div class="control-item"> | |
| <span>Hard Drop</span> | |
| <span class="key">Space</span> | |
| </div> | |
| <div class="control-item"> | |
| <span>Pause</span> | |
| <span class="key">P</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="game-over" id="game-over"> | |
| <h1>GAME OVER</h1> | |
| <div class="final-score">Score: <span id="final-score">0</span></div> | |
| <button class="restart-btn" id="restart-btn">Play Again</button> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // Game constants | |
| const COLS = 10; | |
| const ROWS = 20; | |
| const BLOCK_SIZE = 30; | |
| const NEXT_PIECE_COLS = 4; | |
| const NEXT_PIECE_ROWS = 4; | |
| // DOM elements | |
| const gameBoard = document.getElementById('game-board'); | |
| const nextPieceContainer = document.getElementById('next-piece'); | |
| const scoreDisplay = document.getElementById('score'); | |
| const levelDisplay = document.getElementById('level'); | |
| const gameOverScreen = document.getElementById('game-over'); | |
| const finalScoreDisplay = document.getElementById('final-score'); | |
| const restartBtn = document.getElementById('restart-btn'); | |
| // Game state | |
| let board = Array(ROWS).fill().map(() => Array(COLS).fill(0)); | |
| let currentPiece = null; | |
| let nextPiece = null; | |
| let currentPosition = { x: 0, y: 0 }; | |
| let score = 0; | |
| let level = 1; | |
| let linesCleared = 0; | |
| let gameOver = false; | |
| let isPaused = false; | |
| let dropStart; | |
| let gameInterval; | |
| // Tetromino shapes | |
| const SHAPES = { | |
| I: [ | |
| [0, 0, 0, 0], | |
| [1, 1, 1, 1], | |
| [0, 0, 0, 0], | |
| [0, 0, 0, 0] | |
| ], | |
| J: [ | |
| [1, 0, 0], | |
| [1, 1, 1], | |
| [0, 0, 0] | |
| ], | |
| L: [ | |
| [0, 0, 1], | |
| [1, 1, 1], | |
| [0, 0, 0] | |
| ], | |
| O: [ | |
| [1, 1], | |
| [1, 1] | |
| ], | |
| S: [ | |
| [0, 1, 1], | |
| [1, 1, 0], | |
| [0, 0, 0] | |
| ], | |
| T: [ | |
| [0, 1, 0], | |
| [1, 1, 1], | |
| [0, 0, 0] | |
| ], | |
| Z: [ | |
| [1, 1, 0], | |
| [0, 1, 1], | |
| [0, 0, 0] | |
| ] | |
| }; | |
| const COLORS = { | |
| I: 'tetromino-i', | |
| J: 'tetromino-j', | |
| L: 'tetromino-l', | |
| O: 'tetromino-o', | |
| S: 'tetromino-s', | |
| T: 'tetromino-t', | |
| Z: 'tetromino-z' | |
| }; | |
| // Initialize game board | |
| function initBoard() { | |
| gameBoard.innerHTML = ''; | |
| for (let row = 0; row < ROWS; row++) { | |
| for (let col = 0; col < COLS; col++) { | |
| const cell = document.createElement('div'); | |
| cell.className = 'cell'; | |
| cell.id = `${row}-${col}`; | |
| gameBoard.appendChild(cell); | |
| } | |
| } | |
| } | |
| // Initialize next piece display | |
| function initNextPieceDisplay() { | |
| nextPieceContainer.innerHTML = ''; | |
| for (let row = 0; row < NEXT_PIECE_ROWS; row++) { | |
| for (let col = 0; col < NEXT_PIECE_COLS; col++) { | |
| const cell = document.createElement('div'); | |
| cell.className = 'next-cell'; | |
| cell.id = `next-${row}-${col}`; | |
| nextPieceContainer.appendChild(cell); | |
| } | |
| } | |
| } | |
| // Get random tetromino | |
| function getRandomPiece() { | |
| const keys = Object.keys(SHAPES); | |
| const randomKey = keys[Math.floor(Math.random() * keys.length)]; | |
| return { | |
| shape: SHAPES[randomKey], | |
| color: COLORS[randomKey], | |
| type: randomKey | |
| }; | |
| } | |
| // Draw the current piece on the board | |
| function drawPiece(x, y, piece, isGhost = false) { | |
| piece.shape.forEach((row, rowIndex) => { | |
| row.forEach((value, colIndex) => { | |
| if (value) { | |
| const boardRow = y + rowIndex; | |
| const boardCol = x + colIndex; | |
| if (boardRow >= 0 && boardRow < ROWS && boardCol >= 0 && boardCol < COLS) { | |
| const cell = document.getElementById(`${boardRow}-${boardCol}`); | |
| if (cell) { | |
| cell.classList.add(piece.color); | |
| if (isGhost) { | |
| cell.classList.add('ghost'); | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| }); | |
| } | |
| // Clear the current piece from the board | |
| function clearPiece(x, y, piece) { | |
| piece.shape.forEach((row, rowIndex) => { | |
| row.forEach((value, colIndex) => { | |
| if (value) { | |
| const boardRow = y + rowIndex; | |
| const boardCol = x + colIndex; | |
| if (boardRow >= 0 && boardRow < ROWS && boardCol >= 0 && boardCol < COLS) { | |
| const cell = document.getElementById(`${boardRow}-${boardCol}`); | |
| if (cell) { | |
| cell.className = 'cell'; | |
| } | |
| } | |
| } | |
| }); | |
| }); | |
| } | |
| // Draw the ghost piece (projection of where the piece will land) | |
| function drawGhostPiece() { | |
| let ghostY = currentPosition.y; | |
| while (!collision(currentPosition.x, ghostY + 1, currentPiece)) { | |
| ghostY++; | |
| } | |
| if (ghostY !== currentPosition.y) { | |
| // First clear any existing ghost pieces | |
| clearGhost(); | |
| // Draw the new ghost piece | |
| drawPiece(currentPosition.x, ghostY, currentPiece, true); | |
| } | |
| } | |
| // Clear all ghost pieces from the board | |
| function clearGhost() { | |
| const ghostCells = document.querySelectorAll('.ghost'); | |
| ghostCells.forEach(cell => { | |
| const className = cell.className; | |
| const colorClass = className.split(' ').find(cls => cls.startsWith('tetromino-')); | |
| cell.className = 'cell'; | |
| if (colorClass) { | |
| cell.classList.remove(colorClass, 'ghost'); | |
| } | |
| }); | |
| } | |
| // Check for collision | |
| function collision(x, y, piece) { | |
| for (let row = 0; row < piece.shape.length; row++) { | |
| for (let col = 0; col < piece.shape[row].length; col++) { | |
| if (!piece.shape[row][col]) continue; | |
| const boardX = x + col; | |
| const boardY = y + row; | |
| if ( | |
| boardX < 0 || | |
| boardX >= COLS || | |
| boardY >= ROWS || | |
| (boardY >= 0 && board[boardY][boardX]) | |
| ) { | |
| return true; | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| // Rotate piece | |
| function rotate(piece) { | |
| const N = piece.shape.length; | |
| const rotated = Array(N).fill().map(() => Array(N).fill(0)); | |
| // Transpose the matrix | |
| for (let i = 0; i < N; i++) { | |
| for (let j = 0; j < N; j++) { | |
| rotated[i][j] = piece.shape[N - j - 1][i]; | |
| } | |
| } | |
| // Special case for I piece to make it rotate properly | |
| if (piece.type === 'I') { | |
| if (currentPosition.x < 0) { | |
| currentPosition.x = 0; | |
| } else if (currentPosition.x > COLS - 4) { | |
| currentPosition.x = COLS - 4; | |
| } | |
| } | |
| return { | |
| ...piece, | |
| shape: rotated | |
| }; | |
| } | |
| // Lock piece in place | |
| function lockPiece() { | |
| currentPiece.shape.forEach((row, rowIndex) => { | |
| row.forEach((value, colIndex) => { | |
| if (value) { | |
| const boardRow = currentPosition.y + rowIndex; | |
| const boardCol = currentPosition.x + colIndex; | |
| if (boardRow >= 0) { | |
| board[boardRow][boardCol] = currentPiece.color; | |
| } | |
| } | |
| }); | |
| }); | |
| // Check for completed lines | |
| checkLines(); | |
| // Check for game over | |
| if (currentPosition.y <= 0) { | |
| gameOver = true; | |
| showGameOver(); | |
| return; | |
| } | |
| // Get next piece | |
| currentPiece = nextPiece; | |
| nextPiece = getRandomPiece(); | |
| currentPosition = { x: Math.floor(COLS / 2) - Math.floor(currentPiece.shape[0].length / 2), y: 0 }; | |
| // Update next piece display | |
| updateNextPieceDisplay(); | |
| // Draw the new piece and ghost | |
| drawPiece(currentPosition.x, currentPosition.y, currentPiece); | |
| drawGhostPiece(); | |
| // Reset drop interval | |
| dropStart = Date.now(); | |
| } | |
| // Check for completed lines | |
| function checkLines() { | |
| let linesToClear = 0; | |
| for (let row = ROWS - 1; row >= 0; row--) { | |
| if (board[row].every(cell => cell)) { | |
| linesToClear++; | |
| // Shift all rows above down | |
| for (let y = row; y > 0; y--) { | |
| board[y] = [...board[y - 1]]; | |
| } | |
| board[0] = Array(COLS).fill(0); | |
| // Since we modified the current row, need to check it again | |
| row++; | |
| } | |
| } | |
| if (linesToClear > 0) { | |
| // Update score | |
| updateScore(linesToClear); | |
| // Redraw the board | |
| drawBoard(); | |
| } | |
| } | |
| // Update score | |
| function updateScore(lines) { | |
| const points = [0, 40, 100, 300, 1200]; // Points for 0, 1, 2, 3, 4 lines | |
| score += points[lines] * level; | |
| linesCleared += lines; | |
| // Every 10 lines increases the level | |
| level = Math.floor(linesCleared / 10) + 1; | |
| // Update displays | |
| scoreDisplay.textContent = score; | |
| levelDisplay.textContent = level; | |
| } | |
| // Draw the entire board | |
| function drawBoard() { | |
| for (let row = 0; row < ROWS; row++) { | |
| for (let col = 0; col < COLS; col++) { | |
| const cell = document.getElementById(`${row}-${col}`); | |
| cell.className = 'cell'; | |
| if (board[row][col]) { | |
| cell.classList.add(board[row][col]); | |
| } | |
| } | |
| } | |
| } | |
| // Update next piece display | |
| function updateNextPieceDisplay() { | |
| // Clear the next piece display | |
| for (let row = 0; row < NEXT_PIECE_ROWS; row++) { | |
| for (let col = 0; col < NEXT_PIECE_COLS; col++) { | |
| const cell = document.getElementById(`next-${row}-${col}`); | |
| cell.className = 'next-cell'; | |
| } | |
| } | |
| // Draw the next piece in the center | |
| const startRow = Math.floor((NEXT_PIECE_ROWS - nextPiece.shape.length) / 2); | |
| const startCol = Math.floor((NEXT_PIECE_COLS - nextPiece.shape[0].length) / 2); | |
| for (let row = 0; row < nextPiece.shape.length; row++) { | |
| for (let col = 0; col < nextPiece.shape[row].length; col++) { | |
| if (nextPiece.shape[row][col]) { | |
| const cell = document.getElementById(`next-${startRow + row}-${startCol + col}`); | |
| if (cell) { | |
| cell.classList.add(nextPiece.color); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // Move piece down | |
| function moveDown() { | |
| if (gameOver || isPaused) return; | |
| clearPiece(currentPosition.x, currentPosition.y, currentPiece); | |
| clearGhost(); | |
| if (!collision(currentPosition.x, currentPosition.y + 1, currentPiece)) { | |
| currentPosition.y++; | |
| drawPiece(currentPosition.x, currentPosition.y, currentPiece); | |
| drawGhostPiece(); | |
| return true; | |
| } else { | |
| drawPiece(currentPosition.x, currentPosition.y, currentPiece); | |
| drawGhostPiece(); | |
| lockPiece(); | |
| return false; | |
| } | |
| } | |
| // Hard drop | |
| function hardDrop() { | |
| if (gameOver || isPaused) return; | |
| clearPiece(currentPosition.x, currentPosition.y, currentPiece); | |
| clearGhost(); | |
| while (!collision(currentPosition.x, currentPosition.y + 1, currentPiece)) { | |
| currentPosition.y++; | |
| } | |
| drawPiece(currentPosition.x, currentPosition.y, currentPiece); | |
| lockPiece(); | |
| dropStart = Date.now(); | |
| } | |
| // Move piece left or right | |
| function movePiece(direction) { | |
| if (gameOver || isPaused) return; | |
| clearPiece(currentPosition.x, currentPosition.y, currentPiece); | |
| clearGhost(); | |
| const newX = currentPosition.x + direction; | |
| if (!collision(newX, currentPosition.y, currentPiece)) { | |
| currentPosition.x = newX; | |
| } | |
| drawPiece(currentPosition.x, currentPosition.y, currentPiece); | |
| drawGhostPiece(); | |
| } | |
| // Rotate current piece | |
| function rotatePiece() { | |
| if (gameOver || isPaused) return; | |
| clearPiece(currentPosition.x, currentPosition.y, currentPiece); | |
| clearGhost(); | |
| const rotated = rotate(currentPiece); | |
| // Wall kick - try moving left/right if rotation would cause a collision | |
| if (!collision(currentPosition.x, currentPosition.y, rotated)) { | |
| currentPiece = rotated; | |
| } else if (!collision(currentPosition.x - 1, currentPosition.y, rotated)) { | |
| currentPiece = rotated; | |
| currentPosition.x--; | |
| } else if (!collision(currentPosition.x + 1, currentPosition.y, rotated)) { | |
| currentPiece = rotated; | |
| currentPosition.x++; | |
| } | |
| drawPiece(currentPosition.x, currentPosition.y, currentPiece); | |
| drawGhostPiece(); | |
| } | |
| // Game loop | |
| function gameLoop() { | |
| const now = Date.now(); | |
| const delta = now - dropStart; | |
| const dropInterval = Math.max(1000 - (level - 1) * 100, 100); // Speeds up as level increases | |
| if (delta > dropInterval) { | |
| moveDown(); | |
| dropStart = now; | |
| } | |
| if (!gameOver) { | |
| requestAnimationFrame(gameLoop); | |
| } | |
| } | |
| // Show game over screen | |
| function showGameOver() { | |
| gameOverScreen.classList.add('show'); | |
| finalScoreDisplay.textContent = score; | |
| clearInterval(gameInterval); | |
| } | |
| // Reset game | |
| function resetGame() { | |
| board = Array(ROWS).fill().map(() => Array(COLS).fill(0)); | |
| score = 0; | |
| level = 1; | |
| linesCleared = 0; | |
| gameOver = false; | |
| isPaused = false; | |
| scoreDisplay.textContent = score; | |
| levelDisplay.textContent = level; | |
| // Initialize pieces | |
| currentPiece = getRandomPiece(); | |
| nextPiece = getRandomPiece(); | |
| currentPosition = { x: Math.floor(COLS / 2) - Math.floor(currentPiece.shape[0].length / 2), y: 0 }; | |
| // Clear the board | |
| drawBoard(); | |
| updateNextPieceDisplay(); | |
| // Draw current piece and ghost | |
| drawPiece(currentPosition.x, currentPosition.y, currentPiece); | |
| drawGhostPiece(); | |
| // Hide game over screen | |
| gameOverScreen.classList.remove('show'); | |
| // Reset drop time and start game loop | |
| dropStart = Date.now(); | |
| gameLoop(); | |
| } | |
| // Pause/unpause the game | |
| function togglePause() { | |
| if (gameOver) return; | |
| isPaused = !isPaused; | |
| if (!isPaused) { | |
| dropStart = Date.now(); // Reset drop timer | |
| gameLoop(); | |
| } | |
| } | |
| // Initialize the game | |
| function init() { | |
| initBoard(); | |
| initNextPieceDisplay(); | |
| // Initialize first pieces | |
| currentPiece = getRandomPiece(); | |
| nextPiece = getRandomPiece(); | |
| currentPosition = { x: Math.floor(COLS / 2) - Math.floor(currentPiece.shape[0].length / 2), y: 0 }; | |
| updateNextPieceDisplay(); | |
| drawPiece(currentPosition.x, currentPosition.y, currentPiece); | |
| drawGhostPiece(); | |
| // Keyboard controls | |
| document.addEventListener('keydown', event => { | |
| if (event.key === 'ArrowLeft') { | |
| movePiece(-1); | |
| } else if (event.key === 'ArrowRight') { | |
| movePiece(1); | |
| } else if (event.key === 'ArrowDown') { | |
| moveDown(); | |
| } else if (event.key === 'ArrowUp') { | |
| rotatePiece(); | |
| } else if (event.key === ' ') { | |
| hardDrop(); | |
| } else if (event.key === 'p' || event.key === 'P') { | |
| togglePause(); | |
| } | |
| // Prevent default for arrow keys and space to avoid scrolling | |
| if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', ' '].includes(event.key)) { | |
| event.preventDefault(); | |
| } | |
| }); | |
| restartBtn.addEventListener('click', resetGame); | |
| // Start game loop | |
| dropStart = Date.now(); | |
| gameLoop(); | |
| } | |
| init(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |