Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Modern Tetris</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --primary-color: #6c5ce7; | |
| --secondary-color: #a29bfe; | |
| --accent-color: #fd79a8; | |
| --dark-color: #2d3436; | |
| --light-color: #f9f9f9; | |
| --shadow: 0 10px 30px rgba(0, 0, 0, 0.1); | |
| --border-radius: 10px; | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Poppins', sans-serif; | |
| background: linear-gradient(135deg, #dfe6e9, #b2bec3); | |
| min-height: 100vh; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| padding: 20px; | |
| color: var(--dark-color); | |
| } | |
| .container { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| max-width: 1000px; | |
| width: 100%; | |
| background-color: rgba(255, 255, 255, 0.85); | |
| backdrop-filter: blur(10px); | |
| border-radius: var(--border-radius); | |
| box-shadow: var(--shadow); | |
| padding: 20px; | |
| } | |
| @media (min-width: 768px) { | |
| .container { | |
| flex-direction: row; | |
| justify-content: center; | |
| gap: 40px; | |
| padding: 40px; | |
| } | |
| } | |
| h1 { | |
| font-size: 2.5rem; | |
| margin-bottom: 20px; | |
| color: var(--primary-color); | |
| text-align: center; | |
| width: 100%; | |
| } | |
| .game-container { | |
| position: relative; | |
| } | |
| .tetris-board { | |
| border: 2px solid var(--primary-color); | |
| border-radius: var(--border-radius); | |
| background-color: #fff; | |
| box-shadow: var(--shadow); | |
| display: grid; | |
| grid-template-rows: repeat(20, 1fr); | |
| grid-template-columns: repeat(10, 1fr); | |
| gap: 1px; | |
| width: 300px; | |
| height: 600px; | |
| overflow: hidden; | |
| } | |
| .cell { | |
| background-color: #f5f5f5; | |
| border-radius: 2px; | |
| } | |
| .side-panel { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 20px; | |
| } | |
| .next-piece-container, .score-container, .level-container, .controls-container { | |
| background-color: white; | |
| border-radius: var(--border-radius); | |
| padding: 15px; | |
| box-shadow: var(--shadow); | |
| width: 200px; | |
| } | |
| .next-piece-container h2, .score-container h2, .level-container h2, .controls-container h2 { | |
| color: var(--primary-color); | |
| font-size: 1.2rem; | |
| margin-bottom: 10px; | |
| text-align: center; | |
| } | |
| .next-piece-preview { | |
| height: 100px; | |
| display: grid; | |
| grid-template-rows: repeat(4, 1fr); | |
| grid-template-columns: repeat(4, 1fr); | |
| gap: 1px; | |
| margin: 0 auto; | |
| } | |
| .control-btn { | |
| background-color: var(--primary-color); | |
| color: white; | |
| border: none; | |
| border-radius: var(--border-radius); | |
| padding: 10px 15px; | |
| font-family: 'Poppins', sans-serif; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| display: block; | |
| width: 100%; | |
| margin-bottom: 10px; | |
| box-shadow: 0 3px 5px rgba(0, 0, 0, 0.1); | |
| } | |
| .control-btn:hover { | |
| background-color: var(--secondary-color); | |
| transform: translateY(-2px); | |
| box-shadow: 0 5px 10px rgba(0, 0, 0, 0.15); | |
| } | |
| .control-btn:active { | |
| transform: translateY(0); | |
| box-shadow: 0 2px 3px rgba(0, 0, 0, 0.1); | |
| } | |
| .score, .level { | |
| font-size: 1.5rem; | |
| font-weight: 600; | |
| color: var(--accent-color); | |
| text-align: center; | |
| } | |
| .game-over-modal { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-color: rgba(0, 0, 0, 0.85); | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| border-radius: var(--border-radius); | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: opacity 0.3s ease; | |
| } | |
| .game-over-modal.active { | |
| opacity: 1; | |
| pointer-events: all; | |
| } | |
| .game-over-modal h2 { | |
| color: white; | |
| font-size: 2rem; | |
| margin-bottom: 20px; | |
| } | |
| .game-over-modal p { | |
| color: white; | |
| font-size: 1.2rem; | |
| margin-bottom: 30px; | |
| } | |
| .keyboard-controls { | |
| margin-top: 15px; | |
| font-size: 0.9rem; | |
| color: #666; | |
| } | |
| .keyboard-controls p { | |
| margin: 5px 0; | |
| } | |
| .tetromino { | |
| border: 1px solid rgba(0, 0, 0, 0.2); | |
| border-radius: 2px; | |
| } | |
| .I { | |
| background-color: #00cec9; | |
| } | |
| .J { | |
| background-color: #0984e3; | |
| } | |
| .L { | |
| background-color: #e17055; | |
| } | |
| .O { | |
| background-color: #fdcb6e; | |
| } | |
| .S { | |
| background-color: #00b894; | |
| } | |
| .T { | |
| background-color: #6c5ce7; | |
| } | |
| .Z { | |
| background-color: #d63031; | |
| } | |
| @keyframes flash { | |
| 0%, 100% { | |
| filter: brightness(1); | |
| } | |
| 50% { | |
| filter: brightness(1.5); | |
| } | |
| } | |
| .flash-row { | |
| animation: flash 0.2s 3; | |
| } | |
| @media (max-width: 767px) { | |
| .tetris-board { | |
| width: 280px; | |
| height: 560px; | |
| } | |
| .side-panel { | |
| margin-top: 20px; | |
| flex-direction: row; | |
| flex-wrap: wrap; | |
| justify-content: center; | |
| gap: 10px; | |
| } | |
| .next-piece-container, .score-container, .level-container, .controls-container { | |
| width: calc(50% - 10px); | |
| min-width: 130px; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="game-container"> | |
| <h1>Modern Tetris</h1> | |
| <div class="tetris-board" id="tetris-board"></div> | |
| <div class="game-over-modal" id="game-over-modal"> | |
| <h2>Game Over</h2> | |
| <p>Your score: <span id="final-score">0</span></p> | |
| <button class="control-btn" id="restart-btn">Play Again</button> | |
| </div> | |
| </div> | |
| <div class="side-panel"> | |
| <div class="next-piece-container"> | |
| <h2>Next Piece</h2> | |
| <div class="next-piece-preview" id="next-piece-preview"></div> | |
| </div> | |
| <div class="score-container"> | |
| <h2>Score</h2> | |
| <div class="score" id="score">0</div> | |
| </div> | |
| <div class="level-container"> | |
| <h2>Level</h2> | |
| <div class="level" id="level">1</div> | |
| </div> | |
| <div class="controls-container"> | |
| <h2>Controls</h2> | |
| <button class="control-btn" id="start-btn">Start / Pause</button> | |
| <div class="keyboard-controls"> | |
| <p>← → : Move left/right</p> | |
| <p>↓ : Soft drop</p> | |
| <p>↑ : Rotate</p> | |
| <p>Space : Hard drop</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // Game constants | |
| const BOARD_WIDTH = 10; | |
| const BOARD_HEIGHT = 20; | |
| const PREVIEW_SIZE = 4; | |
| // Game elements | |
| const tetrisBoard = document.getElementById('tetris-board'); | |
| const nextPiecePreview = document.getElementById('next-piece-preview'); | |
| const scoreElement = document.getElementById('score'); | |
| const levelElement = document.getElementById('level'); | |
| const startBtn = document.getElementById('start-btn'); | |
| const gameOverModal = document.getElementById('game-over-modal'); | |
| const finalScoreElement = document.getElementById('final-score'); | |
| const restartBtn = document.getElementById('restart-btn'); | |
| // Game state | |
| let gameBoard = Array(BOARD_HEIGHT).fill().map(() => Array(BOARD_WIDTH).fill(0)); | |
| let score = 0; | |
| let level = 1; | |
| let linesCleared = 0; | |
| let currentPiece = null; | |
| let nextPiece = null; | |
| let gameInterval = null; | |
| let isPaused = false; | |
| let isGameOver = false; | |
| // Tetrominoes | |
| const tetrominoes = { | |
| I: { | |
| shape: [ | |
| [0, 0, 0, 0], | |
| [1, 1, 1, 1], | |
| [0, 0, 0, 0], | |
| [0, 0, 0, 0] | |
| ], | |
| color: 'I' | |
| }, | |
| J: { | |
| shape: [ | |
| [1, 0, 0], | |
| [1, 1, 1], | |
| [0, 0, 0] | |
| ], | |
| color: 'J' | |
| }, | |
| L: { | |
| shape: [ | |
| [0, 0, 1], | |
| [1, 1, 1], | |
| [0, 0, 0] | |
| ], | |
| color: 'L' | |
| }, | |
| O: { | |
| shape: [ | |
| [1, 1], | |
| [1, 1] | |
| ], | |
| color: 'O' | |
| }, | |
| S: { | |
| shape: [ | |
| [0, 1, 1], | |
| [1, 1, 0], | |
| [0, 0, 0] | |
| ], | |
| color: 'S' | |
| }, | |
| T: { | |
| shape: [ | |
| [0, 1, 0], | |
| [1, 1, 1], | |
| [0, 0, 0] | |
| ], | |
| color: 'T' | |
| }, | |
| Z: { | |
| shape: [ | |
| [1, 1, 0], | |
| [0, 1, 1], | |
| [0, 0, 0] | |
| ], | |
| color: 'Z' | |
| } | |
| }; | |
| // Initialize game board | |
| function initBoard() { | |
| tetrisBoard.innerHTML = ''; | |
| nextPiecePreview.innerHTML = ''; | |
| // Create game board cells | |
| for (let row = 0; row < BOARD_HEIGHT; row++) { | |
| for (let col = 0; col < BOARD_WIDTH; col++) { | |
| const cell = document.createElement('div'); | |
| cell.classList.add('cell'); | |
| cell.dataset.row = row; | |
| cell.dataset.col = col; | |
| tetrisBoard.appendChild(cell); | |
| } | |
| } | |
| // Create next piece preview cells | |
| for (let row = 0; row < PREVIEW_SIZE; row++) { | |
| for (let col = 0; col < PREVIEW_SIZE; col++) { | |
| const cell = document.createElement('div'); | |
| cell.classList.add('cell'); | |
| cell.dataset.row = row; | |
| cell.dataset.col = col; | |
| nextPiecePreview.appendChild(cell); | |
| } | |
| } | |
| } | |
| // Generate random tetromino | |
| function getRandomTetromino() { | |
| const tetrominoKeys = Object.keys(tetrominoes); | |
| const randomKey = tetrominoKeys[Math.floor(Math.random() * tetrominoKeys.length)]; | |
| const tetromino = { ...tetrominoes[randomKey] }; | |
| return { | |
| ...tetromino, | |
| row: 0, | |
| col: Math.floor((BOARD_WIDTH - tetromino.shape[0].length) / 2) | |
| }; | |
| } | |
| // Draw tetromino on the board | |
| function drawTetromino() { | |
| clearBoard(); | |
| // Draw settled pieces | |
| for (let row = 0; row < BOARD_HEIGHT; row++) { | |
| for (let col = 0; col < BOARD_WIDTH; col++) { | |
| if (gameBoard[row][col]) { | |
| const cell = document.querySelector(`.cell[data-row="${row}"][data-col="${col}"]`); | |
| if (cell) { | |
| cell.classList.add('tetromino', gameBoard[row][col]); | |
| } | |
| } | |
| } | |
| } | |
| // Draw current piece | |
| if (currentPiece) { | |
| for (let row = 0; row < currentPiece.shape.length; row++) { | |
| for (let col = 0; col < currentPiece.shape[row].length; col++) { | |
| if (currentPiece.shape[row][col]) { | |
| const boardRow = currentPiece.row + row; | |
| const boardCol = currentPiece.col + col; | |
| if (boardRow >= 0 && boardRow < BOARD_HEIGHT && boardCol >= 0 && boardCol < BOARD_WIDTH) { | |
| const cell = document.querySelector(`.cell[data-row="${boardRow}"][data-col="${boardCol}"]`); | |
| if (cell) { | |
| cell.classList.add('tetromino', currentPiece.color); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // Draw next piece in preview | |
| function drawNextPiece() { | |
| // Clear the preview | |
| const previewCells = nextPiecePreview.querySelectorAll('.cell'); | |
| previewCells.forEach(cell => { | |
| cell.className = 'cell'; | |
| }); | |
| if (!nextPiece) return; | |
| // Center the piece in the preview | |
| const offsetRow = Math.floor((PREVIEW_SIZE - nextPiece.shape.length) / 2); | |
| const offsetCol = Math.floor((PREVIEW_SIZE - 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 previewRow = offsetRow + row; | |
| const previewCol = offsetCol + col; | |
| if (previewRow >= 0 && previewRow < PREVIEW_SIZE && previewCol >= 0 && previewCol < PREVIEW_SIZE) { | |
| const cell = nextPiecePreview.querySelector(`.cell[data-row="${previewRow}"][data-col="${previewCol}"]`); | |
| if (cell) { | |
| cell.classList.add('tetromino', nextPiece.color); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // Clear the visual board (not the data) | |
| function clearBoard() { | |
| const cells = tetrisBoard.querySelectorAll('.cell'); | |
| cells.forEach(cell => { | |
| cell.className = 'cell'; | |
| }); | |
| } | |
| // Check if a move is valid | |
| function isValidMove(piece, rowOffset = 0, colOffset = 0) { | |
| for (let row = 0; row < piece.shape.length; row++) { | |
| for (let col = 0; col < piece.shape[row].length; col++) { | |
| if (piece.shape[row][col]) { | |
| const newRow = piece.row + row + rowOffset; | |
| const newCol = piece.col + col + colOffset; | |
| // Check if out of bounds or colliding with settled pieces | |
| if ( | |
| newRow < 0 || | |
| newRow >= BOARD_HEIGHT || | |
| newCol < 0 || | |
| newCol >= BOARD_WIDTH || | |
| (newRow >= 0 && gameBoard[newRow][newCol]) | |
| ) { | |
| return false; | |
| } | |
| } | |
| } | |
| } | |
| return true; | |
| } | |
| // Check and clear completed lines | |
| function checkLines() { | |
| let linesComplete = 0; | |
| let flashRows = []; | |
| for (let row = BOARD_HEIGHT - 1; row >= 0; row--) { | |
| if (gameBoard[row].every(cell => cell !== 0)) { | |
| linesComplete++; | |
| flashRows.push(row); | |
| } | |
| } | |
| if (linesComplete > 0) { | |
| // Flash the lines that will be cleared | |
| flashRows.forEach(row => { | |
| for (let col = 0; col < BOARD_WIDTH; col++) { | |
| const cell = document.querySelector(`.cell[data-row="${row}"][data-col="${col}"]`); | |
| if (cell) { | |
| cell.classList.add('flash-row'); | |
| } | |
| } | |
| }); | |
| // Wait for animation to complete then clear lines | |
| setTimeout(() => { | |
| for (let row of flashRows) { | |
| // Remove the row | |
| gameBoard.splice(row, 1); | |
| // Add a new empty row at the top | |
| gameBoard.unshift(Array(BOARD_WIDTH).fill(0)); | |
| } | |
| // Update score and level | |
| updateScore(linesComplete); | |
| linesCleared += linesComplete; | |
| if (linesCleared >= level * 10) { | |
| level++; | |
| levelElement.textContent = level; | |
| updateGameSpeed(); | |
| } | |
| drawTetromino(); | |
| }, 600); | |
| } | |
| } | |
| // Calculate score based on lines cleared | |
| function updateScore(lines) { | |
| const linePoints = [0, 40, 100, 300, 1200]; // Points for 0, 1, 2, 3, 4 lines | |
| const points = linePoints[lines] * level; | |
| score += points; | |
| scoreElement.textContent = score; | |
| } | |
| // Update game speed based on level | |
| function updateGameSpeed() { | |
| if (gameInterval) { | |
| clearInterval(gameInterval); | |
| } | |
| const speed = Math.max(100, 1000 - (level - 1) * 100); // Decrease interval as level increases | |
| gameInterval = setInterval(moveDown, speed); | |
| } | |
| // Move the current piece down | |
| function moveDown() { | |
| if (isPaused || isGameOver || !currentPiece) return; | |
| if (isValidMove(currentPiece, 1, 0)) { | |
| currentPiece.row++; | |
| drawTetromino(); | |
| } else { | |
| // Lock piece in place | |
| lockPiece(); | |
| // Check for completed lines | |
| checkLines(); | |
| // Generate new piece | |
| spawnPiece(); | |
| } | |
| } | |
| // Move the current piece left | |
| function moveLeft() { | |
| if (isPaused || isGameOver || !currentPiece) return; | |
| if (isValidMove(currentPiece, 0, -1)) { | |
| currentPiece.col--; | |
| drawTetromino(); | |
| } | |
| } | |
| // Move the current piece right | |
| function moveRight() { | |
| if (isPaused || isGameOver || !currentPiece) return; | |
| if (isValidMove(currentPiece, 0, 1)) { | |
| currentPiece.col++; | |
| drawTetromino(); | |
| } | |
| } | |
| // Rotate the current piece | |
| function rotatePiece() { | |
| if (isPaused || isGameOver || !currentPiece) return; | |
| // Create a copy of the current piece | |
| const rotatedPiece = { ...currentPiece }; | |
| // Create a new rotated shape matrix | |
| const N = rotatedPiece.shape.length; | |
| const rotatedShape = Array(N).fill().map(() => Array(N).fill(0)); | |
| // Perform the rotation (90 degrees clockwise) | |
| for (let row = 0; row < N; row++) { | |
| for (let col = 0; col < N; col++) { | |
| rotatedShape[col][N - 1 - row] = rotatedPiece.shape[row][col]; | |
| } | |
| } | |
| rotatedPiece.shape = rotatedShape; | |
| // Check if the rotation is valid | |
| if (isValidMove(rotatedPiece)) { | |
| currentPiece.shape = rotatedShape; | |
| drawTetromino(); | |
| } | |
| } | |
| // Hard drop the current piece | |
| function hardDrop() { | |
| if (isPaused || isGameOver || !currentPiece) return; | |
| while (isValidMove(currentPiece, 1, 0)) { | |
| currentPiece.row++; | |
| } | |
| drawTetromino(); | |
| lockPiece(); | |
| checkLines(); | |
| spawnPiece(); | |
| } | |
| // Lock the current piece in place | |
| function lockPiece() { | |
| for (let row = 0; row < currentPiece.shape.length; row++) { | |
| for (let col = 0; col < currentPiece.shape[row].length; col++) { | |
| if (currentPiece.shape[row][col]) { | |
| const boardRow = currentPiece.row + row; | |
| const boardCol = currentPiece.col + col; | |
| if (boardRow >= 0 && boardRow < BOARD_HEIGHT && boardCol >= 0 && boardCol < BOARD_WIDTH) { | |
| gameBoard[boardRow][boardCol] = currentPiece.color; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // Spawn a new piece | |
| function spawnPiece() { | |
| currentPiece = nextPiece || getRandomTetromino(); | |
| nextPiece = getRandomTetromino(); | |
| drawNextPiece(); | |
| // Check if game is over (collision on spawn) | |
| if (!isValidMove(currentPiece)) { | |
| gameOver(); | |
| } | |
| drawTetromino(); | |
| } | |
| // Game over | |
| function gameOver() { | |
| isGameOver = true; | |
| clearInterval(gameInterval); | |
| finalScoreElement.textContent = score; | |
| gameOverModal.classList.add('active'); | |
| } | |
| // Start or pause the game | |
| function toggleGame() { | |
| if (isGameOver) { | |
| resetGame(); | |
| return; | |
| } | |
| if (isPaused) { | |
| // Resume game | |
| isPaused = false; | |
| startBtn.textContent = 'Pause'; | |
| updateGameSpeed(); | |
| } else { | |
| // Pause game | |
| isPaused = true; | |
| startBtn.textContent = 'Resume'; | |
| clearInterval(gameInterval); | |
| } | |
| } | |
| // Reset the game | |
| function resetGame() { | |
| // Reset game state | |
| gameBoard = Array(BOARD_HEIGHT).fill().map(() => Array(BOARD_WIDTH).fill(0)); | |
| score = 0; | |
| level = 1; | |
| linesCleared = 0; | |
| isPaused = false; | |
| isGameOver = false; | |
| // Reset UI | |
| scoreElement.textContent = score; | |
| levelElement.textContent = level; | |
| startBtn.textContent = 'Pause'; | |
| gameOverModal.classList.remove('active'); | |
| // Start new game | |
| spawnPiece(); | |
| updateGameSpeed(); | |
| } | |
| // Handle keyboard controls | |
| function handleKeyPress(e) { | |
| if (isGameOver) return; | |
| switch(e.key) { | |
| case 'ArrowLeft': | |
| moveLeft(); | |
| break; | |
| case 'ArrowRight': | |
| moveRight(); | |
| break; | |
| case 'ArrowDown': | |
| moveDown(); | |
| break; | |
| case 'ArrowUp': | |
| rotatePiece(); | |
| break; | |
| case ' ': | |
| hardDrop(); | |
| break; | |
| case 'p': | |
| case 'P': | |
| toggleGame(); | |
| break; | |
| } | |
| } | |
| // Initialize the game | |
| function init() { | |
| initBoard(); | |
| spawnPiece(); | |
| // Event listeners | |
| document.addEventListener('keydown', handleKeyPress); | |
| startBtn.addEventListener('click', toggleGame); | |
| restartBtn.addEventListener('click', resetGame); | |
| // Start the game paused | |
| isPaused = true; | |
| startBtn.textContent = 'Start'; | |
| } | |
| // Start the game | |
| init(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |