| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>The Aspiring Ed Tetris Game</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <style> |
| @keyframes flash { |
| 0% { opacity: 1; } |
| 50% { opacity: 0.5; } |
| 100% { opacity: 1; } |
| } |
| .flash-animation { |
| animation: flash 0.3s infinite; |
| } |
| .cell { |
| transition: all 0.1s ease; |
| } |
| .game-container { |
| perspective: 1000px; |
| } |
| .game-board { |
| transform-style: preserve-3d; |
| box-shadow: 0 0 30px rgba(59, 130, 246, 0.5); |
| } |
| .piece-i { background-color: #00f0f0; } |
| .piece-j { background-color: #0000f0; } |
| .piece-l { background-color: #f0a000; } |
| .piece-o { background-color: #f0f000; } |
| .piece-s { background-color: #00f000; } |
| .piece-t { background-color: #a000f0; } |
| .piece-z { background-color: #f00000; } |
| .piece-ghost { opacity: 0.3; } |
| .logo { |
| max-height: 120px; |
| object-fit: contain; |
| } |
| </style> |
| </head> |
| <body class="bg-gray-900 text-white min-h-screen flex flex-col items-center justify-center p-4"> |
| <div class="max-w-4xl w-full"> |
| <div class="flex flex-col items-center mb-6"> |
| <img src="https://i.ytimg.com/vi/fPDyAbG9s0A/maxresdefault.jpg" alt="Tetris Logo" class="logo mb-2"> |
| <h1 class="text-4xl font-bold text-center text-blue-400">Modern Tetris</h1> |
| </div> |
| |
| <div class="flex flex-col md:flex-row gap-8 items-center justify-center"> |
| |
| <div class="game-container relative"> |
| <div class="game-board bg-gray-800 rounded-lg overflow-hidden border-2 border-blue-500"> |
| <div id="board" class="grid grid-cols-10 grid-rows-20 gap-0.5 p-1 bg-gray-700"></div> |
| </div> |
| <div id="game-over" class="absolute inset-0 bg-black bg-opacity-70 flex flex-col items-center justify-center hidden"> |
| <h2 class="text-3xl font-bold mb-4 text-red-500">Game Over!</h2> |
| <button id="restart-btn" class="px-6 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg font-semibold transition">Play Again</button> |
| </div> |
| </div> |
| |
| |
| <div class="flex flex-col gap-6 w-full md:w-64"> |
| |
| <div class="bg-gray-800 p-4 rounded-lg border border-blue-500"> |
| <h2 class="text-xl font-semibold mb-2 text-blue-400">Score</h2> |
| <div class="flex justify-between items-center"> |
| <div> |
| <p class="text-gray-400">Score</p> |
| <p id="score" class="text-2xl font-bold">0</p> |
| </div> |
| <div> |
| <p class="text-gray-400">Level</p> |
| <p id="level" class="text-2xl font-bold">1</p> |
| </div> |
| </div> |
| <div class="mt-2"> |
| <p class="text-gray-400">Lines</p> |
| <p id="lines" class="text-xl font-bold">0</p> |
| </div> |
| </div> |
| |
| |
| <div class="bg-gray-800 p-4 rounded-lg border border-blue-500"> |
| <h2 class="text-xl font-semibold mb-2 text-blue-400">Next Piece</h2> |
| <div id="next-piece" class="grid grid-cols-4 grid-rows-4 gap-1 h-32 w-32 bg-gray-700 p-1"></div> |
| </div> |
| |
| |
| <div class="bg-gray-800 p-4 rounded-lg border border-blue-500"> |
| <h2 class="text-xl font-semibold mb-2 text-blue-400">Controls</h2> |
| <ul class="space-y-2 text-sm"> |
| <li class="flex items-center"><span class="bg-gray-700 px-2 py-1 rounded mr-2">← →</span> Move</li> |
| <li class="flex items-center"><span class="bg-gray-700 px-2 py-1 rounded mr-2">↑</span> Rotate</li> |
| <li class="flex items-center"><span class="bg-gray-700 px-2 py-1 rounded mr-2">↓</span> Soft Drop</li> |
| <li class="flex items-center"><span class="bg-gray-700 px-2 py-1 rounded mr-2">Space</span> Hard Drop</li> |
| <li class="flex items-center"><span class="bg-gray-700 px-2 py-1 rounded mr-2">P</span> Pause</li> |
| </ul> |
| </div> |
| |
| |
| <div class="flex gap-2"> |
| <button id="start-btn" class="flex-1 px-4 py-2 bg-green-600 hover:bg-green-700 rounded-lg font-semibold transition">Start</button> |
| <button id="pause-btn" class="flex-1 px-4 py-2 bg-yellow-600 hover:bg-yellow-700 rounded-lg font-semibold transition hidden">Pause</button> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| document.addEventListener('DOMContentLoaded', () => { |
| |
| const COLS = 10; |
| const ROWS = 20; |
| const BLOCK_SIZE = 30; |
| const EMPTY = 'empty'; |
| |
| |
| let board = Array(ROWS).fill().map(() => Array(COLS).fill(EMPTY)); |
| let currentPiece = null; |
| let nextPiece = null; |
| let currentPosition = { x: 0, y: 0 }; |
| let score = 0; |
| let lines = 0; |
| let level = 1; |
| let gameInterval = null; |
| let isPaused = false; |
| let isGameOver = false; |
| let dropSpeed = 1000; |
| |
| |
| const PIECES = [ |
| { shape: [[1, 1, 1, 1]], color: 'piece-i' }, |
| { shape: [[1, 0, 0], [1, 1, 1]], color: 'piece-j' }, |
| { shape: [[0, 0, 1], [1, 1, 1]], color: 'piece-l' }, |
| { shape: [[1, 1], [1, 1]], color: 'piece-o' }, |
| { shape: [[0, 1, 1], [1, 1, 0]], color: 'piece-s' }, |
| { shape: [[0, 1, 0], [1, 1, 1]], color: 'piece-t' }, |
| { shape: [[1, 1, 0], [0, 1, 1]], color: 'piece-z' } |
| ]; |
| |
| |
| const boardElement = document.getElementById('board'); |
| const nextPieceElement = document.getElementById('next-piece'); |
| const scoreElement = document.getElementById('score'); |
| const linesElement = document.getElementById('lines'); |
| const levelElement = document.getElementById('level'); |
| const startBtn = document.getElementById('start-btn'); |
| const pauseBtn = document.getElementById('pause-btn'); |
| const restartBtn = document.getElementById('restart-btn'); |
| const gameOverElement = document.getElementById('game-over'); |
| |
| |
| function initBoard() { |
| boardElement.innerHTML = ''; |
| boardElement.style.gridTemplateColumns = `repeat(${COLS}, ${BLOCK_SIZE}px)`; |
| boardElement.style.gridTemplateRows = `repeat(${ROWS}, ${BLOCK_SIZE}px)`; |
| |
| for (let y = 0; y < ROWS; y++) { |
| for (let x = 0; x < COLS; x++) { |
| const cell = document.createElement('div'); |
| cell.className = 'cell w-full h-full border border-gray-800'; |
| cell.id = `cell-${y}-${x}`; |
| boardElement.appendChild(cell); |
| } |
| } |
| } |
| |
| |
| function initNextPiece() { |
| nextPieceElement.innerHTML = ''; |
| nextPieceElement.style.gridTemplateColumns = `repeat(4, 1fr)`; |
| nextPieceElement.style.gridTemplateRows = `repeat(4, 1fr)`; |
| |
| for (let y = 0; y < 4; y++) { |
| for (let x = 0; x < 4; x++) { |
| const cell = document.createElement('div'); |
| cell.className = 'cell w-full h-full border border-gray-800'; |
| cell.id = `next-cell-${y}-${x}`; |
| nextPieceElement.appendChild(cell); |
| } |
| } |
| } |
| |
| |
| function randomPiece() { |
| const randomIndex = Math.floor(Math.random() * PIECES.length); |
| return { |
| shape: PIECES[randomIndex].shape, |
| color: PIECES[randomIndex].color |
| }; |
| } |
| |
| |
| function drawBoard() { |
| for (let y = 0; y < ROWS; y++) { |
| for (let x = 0; x < COLS; x++) { |
| const cell = document.getElementById(`cell-${y}-${x}`); |
| cell.className = 'cell w-full h-full border border-gray-800'; |
| |
| if (board[y][x] !== EMPTY) { |
| cell.classList.add(board[y][x]); |
| } |
| } |
| } |
| } |
| |
| |
| function drawPiece() { |
| |
| const ghostY = getDropPosition(); |
| drawGhostPiece(ghostY); |
| |
| |
| for (let y = 0; y < currentPiece.shape.length; y++) { |
| for (let x = 0; x < currentPiece.shape[y].length; x++) { |
| if (currentPiece.shape[y][x]) { |
| const boardY = currentPosition.y + y; |
| const boardX = currentPosition.x + x; |
| |
| if (boardY >= 0 && boardX >= 0 && boardX < COLS) { |
| const cell = document.getElementById(`cell-${boardY}-${boardX}`); |
| if (cell) { |
| cell.className = `cell w-full h-full border border-gray-800 ${currentPiece.color}`; |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| |
| function drawGhostPiece(ghostY) { |
| for (let y = 0; y < currentPiece.shape.length; y++) { |
| for (let x = 0; x < currentPiece.shape[y].length; x++) { |
| if (currentPiece.shape[y][x]) { |
| const boardY = ghostY + y; |
| const boardX = currentPosition.x + x; |
| |
| if (boardY >= 0 && boardX >= 0 && boardX < COLS) { |
| const cell = document.getElementById(`cell-${boardY}-${boardX}`); |
| if (cell && !cell.classList.contains(currentPiece.color)) { |
| cell.classList.add(currentPiece.color, 'piece-ghost'); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| |
| function drawNextPiece() { |
| |
| for (let y = 0; y < 4; y++) { |
| for (let x = 0; x < 4; x++) { |
| const cell = document.getElementById(`next-cell-${y}-${x}`); |
| cell.className = 'cell w-full h-full border border-gray-800'; |
| } |
| } |
| |
| |
| const offsetX = Math.floor((4 - nextPiece.shape[0].length) / 2); |
| const offsetY = Math.floor((4 - nextPiece.shape.length) / 2); |
| |
| |
| for (let y = 0; y < nextPiece.shape.length; y++) { |
| for (let x = 0; x < nextPiece.shape[y].length; x++) { |
| if (nextPiece.shape[y][x]) { |
| const cell = document.getElementById(`next-cell-${y + offsetY}-${x + offsetX}`); |
| cell.classList.add(nextPiece.color); |
| } |
| } |
| } |
| } |
| |
| |
| function isValidPosition(piece, position) { |
| for (let y = 0; y < piece.shape.length; y++) { |
| for (let x = 0; x < piece.shape[y].length; x++) { |
| if (piece.shape[y][x]) { |
| const boardY = position.y + y; |
| const boardX = position.x + x; |
| |
| |
| if (boardX < 0 || boardX >= COLS || boardY >= ROWS) { |
| return false; |
| } |
| |
| |
| if (boardY >= 0 && board[boardY][boardX] !== EMPTY) { |
| return false; |
| } |
| } |
| } |
| } |
| return true; |
| } |
| |
| |
| function rotatePiece() { |
| if (!currentPiece) return; |
| |
| |
| const newPiece = { |
| shape: [], |
| color: currentPiece.color |
| }; |
| |
| |
| for (let x = 0; x < currentPiece.shape[0].length; x++) { |
| newPiece.shape.push([]); |
| for (let y = currentPiece.shape.length - 1; y >= 0; y--) { |
| newPiece.shape[x].push(currentPiece.shape[y][x]); |
| } |
| } |
| |
| |
| if (isValidPosition(newPiece, currentPosition)) { |
| currentPiece = newPiece; |
| draw(); |
| } else { |
| |
| const kicks = [ |
| { x: -1, y: 0 }, { x: 1, y: 0 }, |
| { x: 0, y: -1 }, { x: 0, y: 1 }, |
| { x: -1, y: -1 }, { x: 1, y: -1 }, |
| { x: -1, y: 1 }, { x: 1, y: 1 } |
| ]; |
| |
| for (const kick of kicks) { |
| const newPosition = { |
| x: currentPosition.x + kick.x, |
| y: currentPosition.y + kick.y |
| }; |
| |
| if (isValidPosition(newPiece, newPosition)) { |
| currentPiece = newPiece; |
| currentPosition = newPosition; |
| draw(); |
| break; |
| } |
| } |
| } |
| } |
| |
| |
| function moveLeft() { |
| if (!currentPiece) return; |
| |
| const newPosition = { |
| x: currentPosition.x - 1, |
| y: currentPosition.y |
| }; |
| |
| if (isValidPosition(currentPiece, newPosition)) { |
| currentPosition = newPosition; |
| draw(); |
| } |
| } |
| |
| |
| function moveRight() { |
| if (!currentPiece) return; |
| |
| const newPosition = { |
| x: currentPosition.x + 1, |
| y: currentPosition.y |
| }; |
| |
| if (isValidPosition(currentPiece, newPosition)) { |
| currentPosition = newPosition; |
| draw(); |
| } |
| } |
| |
| |
| function moveDown() { |
| if (!currentPiece || isPaused || isGameOver) return; |
| |
| const newPosition = { |
| x: currentPosition.x, |
| y: currentPosition.y + 1 |
| }; |
| |
| if (isValidPosition(currentPiece, newPosition)) { |
| currentPosition = newPosition; |
| draw(); |
| return true; |
| } else { |
| |
| lockPiece(); |
| return false; |
| } |
| } |
| |
| |
| function hardDrop() { |
| if (!currentPiece || isPaused || isGameOver) return; |
| |
| let dropPosition = currentPosition.y; |
| while (isValidPosition(currentPiece, { x: currentPosition.x, y: dropPosition + 1 })) { |
| dropPosition++; |
| } |
| |
| currentPosition.y = dropPosition; |
| lockPiece(); |
| } |
| |
| |
| function getDropPosition() { |
| let dropPosition = currentPosition.y; |
| while (isValidPosition(currentPiece, { x: currentPosition.x, y: dropPosition + 1 })) { |
| dropPosition++; |
| } |
| return dropPosition; |
| } |
| |
| |
| function lockPiece() { |
| for (let y = 0; y < currentPiece.shape.length; y++) { |
| for (let x = 0; x < currentPiece.shape[y].length; x++) { |
| if (currentPiece.shape[y][x]) { |
| const boardY = currentPosition.y + y; |
| const boardX = currentPosition.x + x; |
| |
| if (boardY >= 0) { |
| board[boardY][boardX] = currentPiece.color; |
| } |
| } |
| } |
| } |
| |
| |
| checkLines(); |
| |
| |
| spawnPiece(); |
| |
| |
| if (!isValidPosition(currentPiece, currentPosition)) { |
| gameOver(); |
| } |
| } |
| |
| |
| function checkLines() { |
| let linesCleared = 0; |
| |
| for (let y = ROWS - 1; y >= 0; y--) { |
| let isLineComplete = true; |
| |
| for (let x = 0; x < COLS; x++) { |
| if (board[y][x] === EMPTY) { |
| isLineComplete = false; |
| break; |
| } |
| } |
| |
| if (isLineComplete) { |
| |
| for (let x = 0; x < COLS; x++) { |
| board[y][x] = EMPTY; |
| } |
| |
| |
| for (let yy = y; yy > 0; yy--) { |
| for (let x = 0; x < COLS; x++) { |
| board[yy][x] = board[yy - 1][x]; |
| } |
| } |
| |
| |
| for (let x = 0; x; x++) { |
| board[0][x] = EMPTY; |
| } |
| |
| linesCleared++; |
| y++; |
| } |
| } |
| |
| if (linesCleared > 0) { |
| |
| const points = [0, 40, 100, 300, 1200]; |
| score += points[linesCleared] * level; |
| lines += linesCleared; |
| |
| |
| level = Math.floor(lines / 10) + 1; |
| |
| |
| dropSpeed = Math.max(100, 1000 - (level - 1) * 50); |
| |
| updateScore(); |
| |
| |
| flashBoard(); |
| } |
| } |
| |
| |
| function flashBoard() { |
| const cells = document.querySelectorAll('.cell'); |
| cells.forEach(cell => { |
| cell.classList.add('flash-animation'); |
| }); |
| |
| setTimeout(() => { |
| cells.forEach(cell => { |
| cell.classList.remove('flash-animation'); |
| }); |
| }, 300); |
| } |
| |
| |
| function spawnPiece() { |
| currentPiece = nextPiece; |
| nextPiece = randomPiece(); |
| |
| |
| currentPosition = { |
| x: Math.floor(COLS / 2) - Math.floor(currentPiece.shape[0].length / 2), |
| y: -1 |
| }; |
| |
| |
| while (!isValidPosition(currentPiece, currentPosition) && currentPosition.y < 0) { |
| currentPosition.y++; |
| } |
| |
| drawNextPiece(); |
| } |
| |
| |
| function updateScore() { |
| scoreElement.textContent = score; |
| linesElement.textContent = lines; |
| levelElement.textContent = level; |
| } |
| |
| |
| function gameOver() { |
| clearInterval(gameInterval); |
| isGameOver = true; |
| gameOverElement.classList.remove('hidden'); |
| } |
| |
| |
| function startGame() { |
| |
| board = Array(ROWS).fill().map(() => Array(COLS).fill(EMPTY)); |
| score = 0; |
| lines = 0; |
| level = 1; |
| dropSpeed = 1000; |
| isPaused = false; |
| isGameOver = false; |
| |
| updateScore(); |
| gameOverElement.classList.add('hidden'); |
| startBtn.classList.add('hidden'); |
| pauseBtn.classList.remove('hidden'); |
| |
| |
| nextPiece = randomPiece(); |
| spawnPiece(); |
| |
| |
| gameInterval = setInterval(() => { |
| if (!isPaused && !isGameOver) { |
| moveDown(); |
| } |
| }, dropSpeed); |
| |
| draw(); |
| } |
| |
| |
| function togglePause() { |
| isPaused = !isPaused; |
| |
| if (isPaused) { |
| pauseBtn.textContent = 'Resume'; |
| clearInterval(gameInterval); |
| } else { |
| pauseBtn.textContent = 'Pause'; |
| gameInterval = setInterval(() => { |
| if (!isPaused && !isGameOver) { |
| moveDown(); |
| } |
| }, dropSpeed); |
| } |
| } |
| |
| |
| function draw() { |
| drawBoard(); |
| drawPiece(); |
| } |
| |
| |
| function init() { |
| initBoard(); |
| initNextPiece(); |
| |
| |
| document.addEventListener('keydown', (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': |
| if (!isGameOver) togglePause(); |
| break; |
| } |
| }); |
| |
| startBtn.addEventListener('click', startGame); |
| pauseBtn.addEventListener('click', togglePause); |
| restartBtn.addEventListener('click', startGame); |
| } |
| |
| init(); |
| }); |
| </script> |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=C50BARZ/tetris-2" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| </html> |