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> |