Spaces:
Running
Running
it doesnt work now, please fix the game and make sure it works - Initial Deployment
bdf8912
verified
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Retro Tetris</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| .particle { | |
| position: absolute; | |
| width: 4px; | |
| height: 4px; | |
| border-radius: 50%; | |
| pointer-events: none; | |
| z-index: 10; | |
| } | |
| @keyframes flash { | |
| 0% { opacity: 1; } | |
| 50% { opacity: 0.5; } | |
| 100% { opacity: 1; } | |
| } | |
| .flash { | |
| animation: flash 0.5s infinite; | |
| } | |
| @keyframes lineClear { | |
| 0% { transform: scaleY(1); opacity: 1; } | |
| 50% { transform: scaleY(0.1); opacity: 0.5; } | |
| 100% { transform: scaleY(1); opacity: 1; } | |
| } | |
| .line-clear { | |
| animation: lineClear 0.3s ease-out; | |
| } | |
| .cell { | |
| width: 30px; | |
| height: 30px; | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| box-sizing: border-box; | |
| } | |
| @media (max-width: 640px) { | |
| .cell { | |
| width: 20px; | |
| height: 20px; | |
| } | |
| } | |
| .game-container { | |
| background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); | |
| box-shadow: 0 0 30px rgba(0, 0, 0, 0.5); | |
| } | |
| .grid-line { | |
| position: absolute; | |
| background-color: rgba(255, 255, 255, 0.05); | |
| } | |
| .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; } | |
| .ghost { | |
| opacity: 0.3; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-900 text-white min-h-screen flex flex-col items-center justify-center p-4 font-mono"> | |
| <div class="text-center mb-4"> | |
| <h1 class="text-4xl md:text-5xl font-bold mb-2 text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-cyan-400"> | |
| RETRO TETRIS | |
| </h1> | |
| <p class="text-gray-400">Use arrow keys to move, space to drop, and 'P' to pause</p> | |
| </div> | |
| <div class="flex flex-col md:flex-row items-center justify-center gap-8"> | |
| <div class="game-container relative rounded-lg overflow-hidden"> | |
| <div id="game-board" class="grid grid-cols-10 grid-rows-20 gap-0 relative"></div> | |
| <!-- Grid lines for visual enhancement --> | |
| <div class="grid-lines absolute inset-0 pointer-events-none"> | |
| <!-- Vertical lines --> | |
| <div class="grid-line w-px h-full" style="left: 0%;"></div> | |
| <div class="grid-line w-px h-full" style="left: 10%;"></div> | |
| <div class="grid-line w-px h-full" style="left: 20%;"></div> | |
| <div class="grid-line w-px h-full" style="left: 30%;"></div> | |
| <div class="grid-line w-px h-full" style="left: 40%;"></div> | |
| <div class="grid-line w-px h-full" style="left: 50%;"></div> | |
| <div class="grid-line w-px h-full" style="left: 60%;"></div> | |
| <div class="grid-line w-px h-full" style="left: 70%;"></div> | |
| <div class="grid-line w-px h-full" style="left: 80%;"></div> | |
| <div class="grid-line w-px h-full" style="left: 90%;"></div> | |
| <div class="grid-line w-px h-full" style="left: 100%;"></div> | |
| <!-- Horizontal lines --> | |
| <div class="grid-line w-full h-px" style="top: 0%;"></div> | |
| <div class="grid-line w-full h-px" style="top: 5%;"></div> | |
| <div class="grid-line w-full h-px" style="top: 10%;"></div> | |
| <div class="grid-line w-full h-px" style="top: 15%;"></div> | |
| <div class="grid-line w-full h-px" style="top: 20%;"></div> | |
| <div class="grid-line w-full h-px" style="top: 25%;"></div> | |
| <div class="grid-line w-full h-px" style="top: 30%;"></div> | |
| <div class="grid-line w-full h-px" style="top: 35%;"></div> | |
| <div class="grid-line w-full h-px" style="top: 40%;"></div> | |
| <div class="grid-line w-full h-px" style="top: 45%;"></div> | |
| <div class="grid-line w-full h-px" style="top: 50%;"></div> | |
| <div class="grid-line w-full h-px" style="top: 55%;"></div> | |
| <div class="grid-line w-full h-px" style="top: 60%;"></div> | |
| <div class="grid-line w-full h-px" style="top: 65%;"></div> | |
| <div class="grid-line w-full h-px" style="top: 70%;"></div> | |
| <div class="grid-line w-full h-px" style="top: 75%;"></div> | |
| <div class="grid-line w-full h-px" style="top: 80%;"></div> | |
| <div class="grid-line w-full h-px" style="top: 85%;"></div> | |
| <div class="grid-line w-full h-px" style="top: 90%;"></div> | |
| <div class="grid-line w-full h-px" style="top: 95%;"></div> | |
| <div class="grid-line w-full h-px" style="top: 100%;"></div> | |
| </div> | |
| </div> | |
| <div class="game-info bg-gray-800 p-6 rounded-lg w-full max-w-xs"> | |
| <div class="mb-6"> | |
| <h2 class="text-xl font-bold mb-2 text-cyan-400">STATS</h2> | |
| <div class="grid grid-cols-2 gap-4"> | |
| <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> | |
| <p class="text-gray-400">Lines</p> | |
| <p id="lines" class="text-2xl font-bold">0</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="mb-6"> | |
| <h2 class="text-xl font-bold mb-2 text-cyan-400">HOLD</h2> | |
| <div id="hold-piece" class="grid grid-cols-4 grid-rows-4 gap-1 bg-gray-700 p-2 rounded h-32 mb-4"></div> | |
| <h2 class="text-xl font-bold mb-2 text-cyan-400">NEXT</h2> | |
| <div id="next-piece" class="grid grid-cols-4 grid-rows-4 gap-1 bg-gray-700 p-2 rounded h-32"></div> | |
| <div id="next-piece-2" class="grid grid-cols-4 grid-rows-4 gap-1 bg-gray-700 p-2 rounded h-32 mt-2"></div> | |
| <div id="next-piece-3" class="grid grid-cols-4 grid-rows-4 gap-1 bg-gray-700 p-2 rounded h-32 mt-2"></div> | |
| </div> | |
| <div class="mb-6"> | |
| <h2 class="text-xl font-bold mb-2 text-cyan-400">CONTROLS</h2> | |
| <div class="grid grid-cols-3 gap-2 text-center"> | |
| <div class="bg-gray-700 p-2 rounded"> | |
| <i class="fas fa-arrow-up"></i> | |
| <p class="text-xs mt-1">Rotate</p> | |
| </div> | |
| <div class="bg-gray-700 p-2 rounded"> | |
| <i class="fas fa-arrow-left"></i> | |
| <p class="text-xs mt-1">Left</p> | |
| </div> | |
| <div class="bg-gray-700 p-2 rounded"> | |
| <i class="fas fa-arrow-right"></i> | |
| <p class="text-xs mt-1">Right</p> | |
| </div> | |
| <div class="bg-gray-700 p-2 rounded"> | |
| <i class="fas fa-arrow-down"></i> | |
| <p class="text-xs mt-1">Soft Drop</p> | |
| </div> | |
| <div class="bg-gray-700 p-2 rounded"> | |
| <i class="fas fa-space-shuttle"></i> | |
| <p class="text-xs mt-1">Hard Drop</p> | |
| </div> | |
| <div class="bg-gray-700 p-2 rounded"> | |
| <i class="fas fa-pause"></i> | |
| <p class="text-xs mt-1">Pause</p> | |
| </div> | |
| </div> | |
| </div> | |
| <button id="start-btn" class="w-full bg-gradient-to-r from-purple-500 to-cyan-500 py-2 rounded-lg font-bold hover:opacity-90 transition"> | |
| START GAME | |
| </button> | |
| <button id="pause-btn" class="w-full bg-yellow-500 py-2 rounded-lg font-bold hover:opacity-90 transition hidden mt-2"> | |
| PAUSE | |
| </button> | |
| </div> | |
| </div> | |
| <div id="game-over" class="fixed inset-0 bg-black bg-opacity-80 flex items-center justify-center hidden z-50"> | |
| <div class="bg-gray-800 p-8 rounded-lg max-w-md w-full text-center"> | |
| <h2 class="text-3xl font-bold text-red-500 mb-4">GAME OVER</h2> | |
| <p class="text-xl mb-2">Final Score: <span id="final-score" class="font-bold">0</span></p> | |
| <p class="text-lg mb-6">Lines Cleared: <span id="final-lines" class="font-bold">0</span></p> | |
| <button id="restart-btn" class="bg-gradient-to-r from-purple-500 to-cyan-500 py-2 px-6 rounded-lg font-bold hover:opacity-90 transition"> | |
| PLAY AGAIN | |
| </button> | |
| </div> | |
| </div> | |
| <div id="pause-screen" class="fixed inset-0 bg-black bg-opacity-80 flex items-center justify-center hidden z-50"> | |
| <div class="bg-gray-800 p-8 rounded-lg max-w-md w-full text-center"> | |
| <h2 class="text-3xl font-bold text-yellow-500 mb-6">GAME PAUSED</h2> | |
| <button id="resume-btn" class="bg-gradient-to-r from-purple-500 to-cyan-500 py-2 px-6 rounded-lg font-bold hover:opacity-90 transition"> | |
| RESUME | |
| </button> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // Sound effects | |
| const sounds = { | |
| rotate: new Audio('https://assets.mixkit.co/sfx/preview/mixkit-game-click-1114.mp3'), | |
| move: new Audio('https://assets.mixkit.co/sfx/preview/mixkit-arcade-game-jump-coin-216.mp3'), | |
| drop: new Audio('https://assets.mixkit.co/sfx/preview/mixkit-quick-jump-arcade-game-239.mp3'), | |
| clear: new Audio('https://assets.mixkit.co/sfx/preview/mixkit-unlock-game-notification-253.mp3'), | |
| gameover: new Audio('https://assets.mixkit.co/sfx/preview/mixkit-retro-arcade-lose-2027.mp3') | |
| }; | |
| // Game constants | |
| const COLS = 10; | |
| const ROWS = 20; | |
| const BLOCK_SIZE = 30; | |
| const NEXT_PIECE_SIZE = 4; | |
| // Game variables | |
| let board = Array(ROWS).fill().map(() => Array(COLS).fill(0)); | |
| let currentPiece = null; | |
| let nextPiece = null; | |
| let holdPiece = null; | |
| let canHold = true; | |
| let currentPosition = { x: 0, y: 0 }; | |
| let score = 0; | |
| let level = 1; | |
| let lines = 0; | |
| let gameInterval = null; | |
| let gameSpeed = 1000; | |
| let isGameRunning = false; | |
| let isPaused = false; | |
| let lastDropTime = 0; | |
| // DOM elements | |
| const gameBoard = document.getElementById('game-board'); | |
| const nextPieceDisplay = document.getElementById('next-piece'); | |
| const holdPieceDisplay = document.getElementById('hold-piece'); | |
| const scoreDisplay = document.getElementById('score'); | |
| const levelDisplay = document.getElementById('level'); | |
| const linesDisplay = document.getElementById('lines'); | |
| const startBtn = document.getElementById('start-btn'); | |
| const pauseBtn = document.getElementById('pause-btn'); | |
| const gameOverScreen = document.getElementById('game-over'); | |
| const finalScoreDisplay = document.getElementById('final-score'); | |
| const finalLinesDisplay = document.getElementById('final-lines'); | |
| const restartBtn = document.getElementById('restart-btn'); | |
| const pauseScreen = document.getElementById('pause-screen'); | |
| const resumeBtn = document.getElementById('resume-btn'); | |
| // 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: 'piece-I', | |
| J: 'piece-J', | |
| L: 'piece-L', | |
| O: 'piece-O', | |
| S: 'piece-S', | |
| T: 'piece-T', | |
| Z: 'piece-Z' | |
| }; | |
| // Initialize game board | |
| function initBoard() { | |
| gameBoard.innerHTML = ''; | |
| gameBoard.style.width = `${COLS * BLOCK_SIZE}px`; | |
| gameBoard.style.height = `${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'; | |
| cell.id = `cell-${y}-${x}`; | |
| gameBoard.appendChild(cell); | |
| } | |
| } | |
| } | |
| // Initialize piece displays | |
| function initPieceDisplays() { | |
| // Next piece display | |
| nextPieceDisplay.innerHTML = ''; | |
| nextPieceDisplay.style.width = `${NEXT_PIECE_SIZE * BLOCK_SIZE}px`; | |
| nextPieceDisplay.style.height = `${NEXT_PIECE_SIZE * BLOCK_SIZE}px`; | |
| for (let y = 0; y < NEXT_PIECE_SIZE; y++) { | |
| for (let x = 0; x < NEXT_PIECE_SIZE; x++) { | |
| const cell = document.createElement('div'); | |
| cell.className = 'cell bg-gray-700'; | |
| cell.id = `next-cell-${y}-${x}`; | |
| nextPieceDisplay.appendChild(cell); | |
| } | |
| } | |
| // Hold piece display | |
| holdPieceDisplay.innerHTML = ''; | |
| holdPieceDisplay.style.width = `${NEXT_PIECE_SIZE * BLOCK_SIZE}px`; | |
| holdPieceDisplay.style.height = `${NEXT_PIECE_SIZE * BLOCK_SIZE}px`; | |
| for (let y = 0; y < NEXT_PIECE_SIZE; y++) { | |
| for (let x = 0; x < NEXT_PIECE_SIZE; x++) { | |
| const cell = document.createElement('div'); | |
| cell.className = 'cell bg-gray-700'; | |
| cell.id = `hold-cell-${y}-${x}`; | |
| holdPieceDisplay.appendChild(cell); | |
| } | |
| } | |
| } | |
| // Get random piece | |
| function getRandomPiece() { | |
| const pieces = Object.keys(SHAPES); | |
| const randomPiece = pieces[Math.floor(Math.random() * pieces.length)]; | |
| return { | |
| shape: SHAPES[randomPiece], | |
| color: COLORS[randomPiece], | |
| type: randomPiece | |
| }; | |
| } | |
| // Draw piece on board | |
| function drawPiece(piece, position, isGhost = false) { | |
| 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 (boardY >= 0 && boardY < ROWS && boardX >= 0 && boardX < COLS) { | |
| const cell = document.getElementById(`cell-${boardY}-${boardX}`); | |
| if (cell) { | |
| cell.className = `cell ${piece.color} ${isGhost ? 'ghost' : ''}`; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // Draw next piece | |
| function drawNextPiece() { | |
| // Clear next piece display | |
| const cells = nextPieceDisplay.querySelectorAll('.cell'); | |
| cells.forEach(cell => { | |
| cell.className = 'cell bg-gray-700'; | |
| }); | |
| // Center the piece in the next piece display | |
| const offsetX = Math.floor((NEXT_PIECE_SIZE - nextPiece.shape[0].length) / 2); | |
| const offsetY = Math.floor((NEXT_PIECE_SIZE - 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 displayY = y + offsetY; | |
| const displayX = x + offsetX; | |
| const cell = document.getElementById(`next-cell-${displayY}-${displayX}`); | |
| if (cell) { | |
| cell.className = `cell ${nextPiece.color}`; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // Clear board (except locked pieces) | |
| function clearBoard() { | |
| for (let y = 0; y < ROWS; y++) { | |
| for (let x = 0; x < COLS; x++) { | |
| if (board[y][x] === 0) { | |
| const cell = document.getElementById(`cell-${y}-${x}`); | |
| if (cell) { | |
| cell.className = 'cell'; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // Draw locked pieces | |
| function drawLockedPieces() { | |
| for (let y = 0; y < ROWS; y++) { | |
| for (let x = 0; x < COLS; x++) { | |
| if (board[y][x] !== 0) { | |
| const cell = document.getElementById(`cell-${y}-${x}`); | |
| if (cell) { | |
| cell.className = `cell ${board[y][x]}`; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // Check collision | |
| function checkCollision(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; | |
| // Check boundaries | |
| if (boardX < 0 || boardX >= COLS || boardY >= ROWS) { | |
| return true; | |
| } | |
| // Check if already occupied (and not above the board) | |
| if (boardY >= 0 && board[boardY][boardX] !== 0) { | |
| return true; | |
| } | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| // Rotate piece | |
| function rotatePiece() { | |
| if (!currentPiece) return; | |
| const rotated = []; | |
| for (let x = 0; x < currentPiece.shape[0].length; x++) { | |
| const newRow = []; | |
| for (let y = currentPiece.shape.length - 1; y >= 0; y--) { | |
| newRow.push(currentPiece.shape[y][x]); | |
| } | |
| rotated.push(newRow); | |
| } | |
| const originalShape = currentPiece.shape; | |
| currentPiece.shape = rotated; | |
| // Check if rotation causes collision | |
| if (checkCollision(currentPiece, currentPosition)) { | |
| // Try wall kicks | |
| const originalX = currentPosition.x; | |
| // Try moving left | |
| currentPosition.x--; | |
| if (checkCollision(currentPiece, currentPosition)) { | |
| // Try moving right | |
| currentPosition.x = originalX + 1; | |
| if (checkCollision(currentPiece, currentPosition)) { | |
| // Try moving left twice | |
| currentPosition.x = originalX - 2; | |
| if (checkCollision(currentPiece, currentPosition)) { | |
| // Try moving right twice | |
| currentPosition.x = originalX + 2; | |
| if (checkCollision(currentPiece, currentPosition)) { | |
| // Revert if all wall kicks fail | |
| currentPosition.x = originalX; | |
| currentPiece.shape = originalShape; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| updateDisplay(); | |
| } | |
| // Move piece | |
| function movePiece(direction) { | |
| if (!currentPiece || isPaused) return; | |
| const newPosition = { ...currentPosition }; | |
| switch (direction) { | |
| case 'left': | |
| newPosition.x--; | |
| break; | |
| case 'right': | |
| newPosition.x++; | |
| break; | |
| case 'down': | |
| newPosition.y++; | |
| break; | |
| } | |
| if (!checkCollision(currentPiece, newPosition)) { | |
| currentPosition = newPosition; | |
| updateDisplay(); | |
| return true; | |
| } | |
| // If moving down and collision, lock the piece | |
| if (direction === 'down') { | |
| lockPiece(); | |
| return false; | |
| } | |
| return false; | |
| } | |
| // Hard drop | |
| function hardDrop() { | |
| if (!currentPiece || isPaused) return; | |
| let dropDistance = 0; | |
| while (!checkCollision(currentPiece, { ...currentPosition, y: currentPosition.y + dropDistance + 1 })) { | |
| dropDistance++; | |
| } | |
| if (dropDistance > 0) { | |
| currentPosition.y += dropDistance; | |
| updateDisplay(); | |
| } | |
| lockPiece(); | |
| } | |
| // Lock piece | |
| 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 piece is locked above the board, game over | |
| if (boardY < 0) { | |
| gameOver(); | |
| return; | |
| } | |
| board[boardY][boardX] = currentPiece.color; | |
| } | |
| } | |
| } | |
| // Check for completed lines | |
| checkLines(); | |
| // Get next piece | |
| spawnPiece(); | |
| } | |
| // Create particle effects | |
| function createParticles(x, y, color, count = 10) { | |
| for (let i = 0; i < count; i++) { | |
| const particle = document.createElement('div'); | |
| particle.className = 'particle'; | |
| particle.style.backgroundColor = color; | |
| particle.style.left = `${x}px`; | |
| particle.style.top = `${y}px`; | |
| const angle = Math.random() * Math.PI * 2; | |
| const velocity = 2 + Math.random() * 3; | |
| const lifetime = 500 + Math.random() * 500; | |
| document.body.appendChild(particle); | |
| const startTime = Date.now(); | |
| function update() { | |
| const elapsed = Date.now() - startTime; | |
| const progress = elapsed / lifetime; | |
| if (progress >= 1) { | |
| particle.remove(); | |
| return; | |
| } | |
| particle.style.opacity = 1 - progress; | |
| particle.style.transform = `translate(${Math.cos(angle) * velocity * elapsed / 20}px, ${Math.sin(angle) * velocity * elapsed / 20}px)`; | |
| requestAnimationFrame(update); | |
| } | |
| update(); | |
| } | |
| } | |
| // Check for completed lines | |
| function checkLines() { | |
| let linesCleared = 0; | |
| let completedLines = []; | |
| for (let y = ROWS - 1; y >= 0; y--) { | |
| if (board[y].every(cell => cell !== 0)) { | |
| completedLines.push(y); | |
| } | |
| } | |
| if (completedLines.length > 0) { | |
| // Visual feedback for line clears | |
| completedLines.forEach(y => { | |
| for (let x = 0; x < COLS; x++) { | |
| const cell = document.getElementById(`cell-${y}-${x}`); | |
| if (cell) cell.classList.add('line-clear'); | |
| } | |
| }); | |
| // Wait for animation to complete | |
| setTimeout(() => { | |
| completedLines.sort((a, b) => a - b); | |
| completedLines.forEach(y => { | |
| // Remove the line | |
| board.splice(y, 1); | |
| // Add new empty line at top | |
| board.unshift(Array(COLS).fill(0)); | |
| linesCleared++; | |
| }); | |
| // Update score with combo bonus and back-to-back | |
| const basePoints = [0, 100, 300, 500, 800]; | |
| const comboBonus = (completedLines.length - 1) * 100; | |
| const backToBackBonus = completedLines.length >= 4 ? 1200 : 0; | |
| score += (basePoints[completedLines.length] + comboBonus + backToBackBonus) * level; | |
| // Play clear sound | |
| sounds.clear.currentTime = 0; | |
| sounds.clear.play(); | |
| // Create particles | |
| completedLines.forEach(y => { | |
| for (let x = 0; x < COLS; x++) { | |
| const cell = document.getElementById(`cell-${y}-${x}`); | |
| if (cell) { | |
| const rect = cell.getBoundingClientRect(); | |
| createParticles( | |
| rect.left + rect.width/2, | |
| rect.top + rect.height/2, | |
| getComputedStyle(cell).backgroundColor, | |
| 5 | |
| ); | |
| } | |
| } | |
| }); | |
| lines += completedLines.length; | |
| // Update level (every 10 lines) | |
| const newLevel = Math.floor(lines / 10) + 1; | |
| if (newLevel > level) { | |
| level = newLevel; | |
| // Increase game speed (capped at 100ms) | |
| gameSpeed = Math.max(100, 1000 - (level - 1) * 75); | |
| clearInterval(gameInterval); | |
| gameInterval = setInterval(gameTick, gameSpeed); | |
| } | |
| updateStats(); | |
| updateDisplay(); | |
| }, 300); | |
| } | |
| } | |
| // Hold current piece | |
| function holdCurrentPiece() { | |
| if (!canHold) return; | |
| if (holdPiece) { | |
| // Swap current piece with hold piece | |
| const temp = currentPiece; | |
| currentPiece = holdPiece; | |
| holdPiece = temp; | |
| } else { | |
| // First hold - just store current piece | |
| holdPiece = currentPiece; | |
| currentPiece = nextPiece; | |
| nextPiece = getRandomPiece(); | |
| } | |
| // Reset position | |
| currentPosition = { | |
| x: Math.floor((COLS - currentPiece.shape[0].length) / 2), | |
| y: -2 | |
| }; | |
| // Update displays | |
| drawHoldPiece(); | |
| drawNextPiece(); | |
| updateDisplay(); | |
| canHold = false; | |
| } | |
| // Draw hold piece | |
| function drawHoldPiece() { | |
| // Clear hold piece display | |
| const cells = holdPieceDisplay.querySelectorAll('.cell'); | |
| cells.forEach(cell => { | |
| cell.className = 'cell bg-gray-700'; | |
| }); | |
| if (!holdPiece) return; | |
| // Center the piece in the hold display | |
| const offsetX = Math.floor((NEXT_PIECE_SIZE - holdPiece.shape[0].length) / 2); | |
| const offsetY = Math.floor((NEXT_PIECE_SIZE - holdPiece.shape.length) / 2); | |
| for (let y = 0; y < holdPiece.shape.length; y++) { | |
| for (let x = 0; x < holdPiece.shape[y].length; x++) { | |
| if (holdPiece.shape[y][x]) { | |
| const displayY = y + offsetY; | |
| const displayX = x + offsetX; | |
| const cell = document.getElementById(`hold-cell-${displayY}-${displayX}`); | |
| if (cell) { | |
| cell.className = `cell ${holdPiece.color}`; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // Spawn new piece | |
| function spawnPiece() { | |
| currentPiece = nextPiece || getRandomPiece(); | |
| nextPiece = getRandomPiece(); | |
| // Set starting position (centered at top) | |
| currentPosition = { | |
| x: Math.floor((COLS - currentPiece.shape[0].length) / 2), | |
| y: -2 // Start slightly above the board | |
| }; | |
| // If collision immediately, game over | |
| if (checkCollision(currentPiece, currentPosition)) { | |
| gameOver(); | |
| return; | |
| } | |
| canHold = true; | |
| drawHoldPiece(); | |
| drawNextPiece(); | |
| updateDisplay(); | |
| } | |
| // Update display | |
| function updateDisplay() { | |
| clearBoard(); | |
| drawLockedPieces(); | |
| // Draw ghost piece (shows where piece will land) | |
| if (currentPiece) { | |
| const ghostPosition = { ...currentPosition }; | |
| while (!checkCollision(currentPiece, { ...ghostPosition, y: ghostPosition.y + 1 })) { | |
| ghostPosition.y++; | |
| } | |
| drawPiece(currentPiece, ghostPosition, true); | |
| } | |
| // Draw current piece | |
| if (currentPiece) { | |
| drawPiece(currentPiece, currentPosition); | |
| } | |
| } | |
| // Update stats display | |
| function updateStats() { | |
| scoreDisplay.textContent = score; | |
| levelDisplay.textContent = level; | |
| linesDisplay.textContent = lines; | |
| } | |
| // Game tick | |
| function gameTick() { | |
| if (!isPaused) { | |
| movePiece('down'); | |
| } | |
| } | |
| // Start game | |
| function startGame() { | |
| // Reset game state | |
| board = Array(ROWS).fill().map(() => Array(COLS).fill(0)); | |
| score = 0; | |
| level = 1; | |
| lines = 0; | |
| gameSpeed = 1000; | |
| isGameRunning = true; | |
| isPaused = false; | |
| updateStats(); | |
| initBoard(); | |
| initPieceDisplays(); | |
| // Get first pieces | |
| nextPiece = getRandomPiece(); | |
| spawnPiece(); | |
| // Start game loop | |
| clearInterval(gameInterval); | |
| gameInterval = setInterval(gameTick, gameSpeed); | |
| // Update UI | |
| startBtn.classList.add('hidden'); | |
| pauseBtn.classList.remove('hidden'); | |
| gameOverScreen.classList.add('hidden'); | |
| pauseScreen.classList.add('hidden'); | |
| } | |
| // Pause game | |
| function pauseGame() { | |
| isPaused = !isPaused; | |
| if (isPaused) { | |
| pauseBtn.textContent = 'RESUME'; | |
| pauseScreen.classList.remove('hidden'); | |
| } else { | |
| pauseBtn.textContent = 'PAUSE'; | |
| pauseScreen.classList.add('hidden'); | |
| } | |
| } | |
| // Game over | |
| function gameOver() { | |
| isGameRunning = false; | |
| clearInterval(gameInterval); | |
| // Flash the board | |
| const cells = gameBoard.querySelectorAll('.cell'); | |
| cells.forEach(cell => { | |
| cell.classList.add('flash'); | |
| }); | |
| // Show game over screen | |
| setTimeout(() => { | |
| cells.forEach(cell => { | |
| cell.classList.remove('flash'); | |
| }); | |
| finalScoreDisplay.textContent = score; | |
| finalLinesDisplay.textContent = lines; | |
| gameOverScreen.classList.remove('hidden'); | |
| pauseBtn.classList.add('hidden'); | |
| startBtn.classList.remove('hidden'); | |
| }, 1000); | |
| } | |
| // Event listeners | |
| startBtn.addEventListener('click', startGame); | |
| restartBtn.addEventListener('click', startGame); | |
| pauseBtn.addEventListener('click', pauseGame); | |
| resumeBtn.addEventListener('click', pauseGame); | |
| // Keyboard controls | |
| document.addEventListener('keydown', (e) => { | |
| if (!isGameRunning) return; | |
| switch (e.key) { | |
| case 'ArrowLeft': | |
| movePiece('left'); | |
| break; | |
| case 'ArrowRight': | |
| movePiece('right'); | |
| break; | |
| case 'ArrowDown': | |
| movePiece('down'); | |
| break; | |
| case 'ArrowUp': | |
| rotatePiece(); | |
| break; | |
| case ' ': | |
| hardDrop(); | |
| break; | |
| case 'p': | |
| case 'P': | |
| pauseGame(); | |
| break; | |
| case 'c': | |
| case 'C': | |
| case 'Shift': | |
| holdCurrentPiece(); | |
| break; | |
| } | |
| }); | |
| // Touch controls for mobile | |
| let touchStartX = 0; | |
| let touchStartY = 0; | |
| gameBoard.addEventListener('touchstart', (e) => { | |
| if (!isGameRunning) return; | |
| touchStartX = e.touches[0].clientX; | |
| touchStartY = e.touches[0].clientY; | |
| e.preventDefault(); | |
| }, { passive: false }); | |
| gameBoard.addEventListener('touchmove', (e) => { | |
| if (!isGameRunning) return; | |
| const touchX = e.touches[0].clientX; | |
| const touchY = e.touches[0].clientY; | |
| const diffX = touchX - touchStartX; | |
| const diffY = touchY - touchStartY; | |
| // Horizontal swipe | |
| if (Math.abs(diffX) > Math.abs(diffY)) { | |
| if (diffX > 30) { | |
| movePiece('right'); | |
| touchStartX = touchX; | |
| } else if (diffX < -30) { | |
| movePiece('left'); | |
| touchStartX = touchX; | |
| } | |
| } | |
| // Vertical swipe down | |
| else if (diffY > 30) { | |
| movePiece('down'); | |
| touchStartY = touchY; | |
| } | |
| e.preventDefault(); | |
| }, { passive: false }); | |
| gameBoard.addEventListener('touchend', (e) => { | |
| if (!isGameRunning) return; | |
| const touchEndX = e.changedTouches[0].clientX; | |
| const touchEndY = e.changedTouches[0].clientY; | |
| const diffX = touchEndX - touchStartX; | |
| const diffY = touchEndY - touchStartY; | |
| // Tap (small movement) | |
| if (Math.abs(diffX) < 10 && Math.abs(diffY) < 10) { | |
| rotatePiece(); | |
| } | |
| // Quick swipe up | |
| else if (diffY < -30) { | |
| hardDrop(); | |
| } | |
| e.preventDefault(); | |
| }, { passive: false }); | |
| // Initialize displays | |
| initBoard(); | |
| initNextPieceDisplay(); | |
| }); | |
| </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=MitchiMitch/retrotetris" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |