|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>Neon Tetris</title> |
|
|
<script src="https://cdn.tailwindcss.com"></script> |
|
|
<style> |
|
|
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); |
|
|
|
|
|
body { |
|
|
font-family: 'Press Start 2P', cursive; |
|
|
background-color: #0f172a; |
|
|
overflow: hidden; |
|
|
} |
|
|
|
|
|
.tetris-block { |
|
|
box-sizing: border-box; |
|
|
border: 2px solid rgba(255, 255, 255, 0.1); |
|
|
transition: all 0.1s ease; |
|
|
} |
|
|
|
|
|
.tetris-block.I { |
|
|
background-color: #00f0f0; |
|
|
box-shadow: inset 0 0 10px rgba(0, 240, 240, 0.5), 0 0 10px rgba(0, 240, 240, 0.5); |
|
|
} |
|
|
|
|
|
.tetris-block.J { |
|
|
background-color: #0000f0; |
|
|
box-shadow: inset 0 0 10px rgba(0, 0, 240, 0.5), 0 0 10px rgba(0, 0, 240, 0.5); |
|
|
} |
|
|
|
|
|
.tetris-block.L { |
|
|
background-color: #f0a000; |
|
|
box-shadow: inset 0 0 10px rgba(240, 160, 0, 0.5), 0 0 10px rgba(240, 160, 0, 0.5); |
|
|
} |
|
|
|
|
|
.tetris-block.O { |
|
|
background-color: #f0f000; |
|
|
box-shadow: inset 0 0 10px rgba(240, 240, 0, 0.5), 0 0 10px rgba(240, 240, 0, 0.5); |
|
|
} |
|
|
|
|
|
.tetris-block.S { |
|
|
background-color: #00f000; |
|
|
box-shadow: inset 0 0 10px rgba(0, 240, 0, 0.5), 0 0 10px rgba(0, 240, 0, 0.5); |
|
|
} |
|
|
|
|
|
.tetris-block.T { |
|
|
background-color: #a000f0; |
|
|
box-shadow: inset 0 0 10px rgba(160, 0, 240, 0.5), 0 0 10px rgba(160, 0, 240, 0.5); |
|
|
} |
|
|
|
|
|
.tetris-block.Z { |
|
|
background-color: #f00000; |
|
|
box-shadow: inset 0 0 10px rgba(240, 0, 0, 0.5), 0 0 10px rgba(240, 0, 0, 0.5); |
|
|
} |
|
|
|
|
|
.grid-cell { |
|
|
background-color: rgba(30, 41, 59, 0.5); |
|
|
border: 1px solid rgba(255, 255, 255, 0.05); |
|
|
} |
|
|
|
|
|
.next-piece-container { |
|
|
background-color: rgba(30, 41, 59, 0.7); |
|
|
border: 2px solid rgba(255, 255, 255, 0.1); |
|
|
} |
|
|
|
|
|
.game-overlay { |
|
|
background-color: rgba(15, 23, 42, 0.9); |
|
|
} |
|
|
|
|
|
@keyframes pulse { |
|
|
0% { transform: scale(1); } |
|
|
50% { transform: scale(1.05); } |
|
|
100% { transform: scale(1); } |
|
|
} |
|
|
|
|
|
.pulse { |
|
|
animation: pulse 1.5s infinite; |
|
|
} |
|
|
|
|
|
|
|
|
.next-piece-grid { |
|
|
display: grid; |
|
|
width: 100px; |
|
|
height: 100px; |
|
|
position: relative; |
|
|
} |
|
|
|
|
|
.next-piece-inner { |
|
|
position: absolute; |
|
|
display: grid; |
|
|
gap: 0; |
|
|
} |
|
|
|
|
|
.next-piece-cell { |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body class="min-h-screen flex flex-col items-center justify-center text-white p-4"> |
|
|
<div class="text-center mb-6"> |
|
|
<h1 class="text-4xl md:text-5xl font-bold mb-2 text-transparent bg-clip-text bg-gradient-to-r from-purple-400 via-pink-500 to-red-500"> |
|
|
NEON TETRIS |
|
|
</h1> |
|
|
<p class="text-sm text-gray-400">Use arrow keys to play • ↑ to rotate • ↓ to drop faster</p> |
|
|
</div> |
|
|
|
|
|
<div class="flex flex-col md:flex-row items-center gap-8"> |
|
|
|
|
|
<div class="relative"> |
|
|
<div id="game-board" class="grid grid-cols-10 grid-rows-20 gap-0 w-[250px] h-[500px] border-4 border-indigo-500/50 rounded-lg overflow-hidden"></div> |
|
|
<div id="game-overlay" class="game-overlay absolute inset-0 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-3 bg-indigo-600 hover:bg-indigo-700 rounded-lg font-bold transition-all pulse"> |
|
|
PLAY AGAIN |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="flex flex-col gap-6 w-full max-w-[250px]"> |
|
|
<div class="next-piece-container p-4 rounded-lg"> |
|
|
<h3 class="text-center text-lg mb-3">NEXT PIECE</h3> |
|
|
<div id="next-piece" class="next-piece-grid"> |
|
|
<div id="next-piece-inner" class="next-piece-inner"></div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="bg-slate-800/50 p-4 rounded-lg"> |
|
|
<div class="flex justify-between mb-2"> |
|
|
<span>SCORE:</span> |
|
|
<span id="score" class="font-bold">0</span> |
|
|
</div> |
|
|
<div class="flex justify-between mb-2"> |
|
|
<span>LEVEL:</span> |
|
|
<span id="level" class="font-bold">1</span> |
|
|
</div> |
|
|
<div class="flex justify-between"> |
|
|
<span>LINES:</span> |
|
|
<span id="lines" class="font-bold">0</span> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="bg-slate-800/50 p-4 rounded-lg hidden md:block"> |
|
|
<h3 class="text-center text-lg mb-2">CONTROLS</h3> |
|
|
<div class="grid grid-cols-3 gap-2 text-xs"> |
|
|
<div class="col-span-3 flex justify-center"> |
|
|
<div class="bg-slate-700/50 p-2 rounded text-center">↑ Rotate</div> |
|
|
</div> |
|
|
<div class="flex justify-center"> |
|
|
<div class="bg-slate-700/50 p-2 rounded text-center">← Left</div> |
|
|
</div> |
|
|
<div class="flex justify-center"> |
|
|
<div class="bg-slate-700/50 p-2 rounded text-center">↓ Drop</div> |
|
|
</div> |
|
|
<div class="flex justify-center"> |
|
|
<div class="bg-slate-700/50 p-2 rounded text-center">→ Right</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="mt-8 text-xs text-gray-500 text-center"> |
|
|
<p>Press any key to start • P to pause</p> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
|
|
|
const COLS = 10; |
|
|
const ROWS = 20; |
|
|
const BLOCK_SIZE = 25; |
|
|
const EMPTY = 'empty'; |
|
|
|
|
|
|
|
|
let board = createEmptyBoard(); |
|
|
let currentPiece = null; |
|
|
let nextPiece = null; |
|
|
let score = 0; |
|
|
let level = 1; |
|
|
let lines = 0; |
|
|
let gameOver = false; |
|
|
let isPaused = false; |
|
|
let dropInterval = null; |
|
|
let dropSpeed = 1000; |
|
|
|
|
|
|
|
|
const gameBoard = document.getElementById('game-board'); |
|
|
const nextPieceDisplay = document.getElementById('next-piece-inner'); |
|
|
const scoreDisplay = document.getElementById('score'); |
|
|
const levelDisplay = document.getElementById('level'); |
|
|
const linesDisplay = document.getElementById('lines'); |
|
|
const gameOverlay = document.getElementById('game-overlay'); |
|
|
const restartBtn = document.getElementById('restart-btn'); |
|
|
|
|
|
|
|
|
function initializeBoard() { |
|
|
gameBoard.innerHTML = ''; |
|
|
for (let row = 0; row < ROWS; row++) { |
|
|
for (let col = 0; col < COLS; col++) { |
|
|
const cell = document.createElement('div'); |
|
|
cell.className = 'grid-cell w-full h-full'; |
|
|
cell.dataset.row = row; |
|
|
cell.dataset.col = col; |
|
|
gameBoard.appendChild(cell); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function createEmptyBoard() { |
|
|
return Array.from({ length: ROWS }, () => Array(COLS).fill(EMPTY)); |
|
|
} |
|
|
|
|
|
|
|
|
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: 'I', |
|
|
J: 'J', |
|
|
L: 'L', |
|
|
O: 'O', |
|
|
S: 'S', |
|
|
T: 'T', |
|
|
Z: 'Z' |
|
|
}; |
|
|
|
|
|
|
|
|
function getRandomPiece() { |
|
|
const pieces = Object.keys(SHAPES); |
|
|
const randomPiece = pieces[Math.floor(Math.random() * pieces.length)]; |
|
|
return { |
|
|
shape: SHAPES[randomPiece], |
|
|
color: COLORS[randomPiece], |
|
|
x: Math.floor(COLS / 2) - Math.floor(SHAPES[randomPiece][0].length / 2), |
|
|
y: 0 |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
function drawBoard() { |
|
|
|
|
|
document.querySelectorAll('#game-board div').forEach(cell => { |
|
|
cell.className = 'grid-cell w-full h-full'; |
|
|
}); |
|
|
|
|
|
|
|
|
for (let row = 0; row < ROWS; row++) { |
|
|
for (let col = 0; col < COLS; col++) { |
|
|
if (board[row][col] !== EMPTY) { |
|
|
const cell = document.querySelector(`#game-board div[data-row="${row}"][data-col="${col}"]`); |
|
|
if (cell) { |
|
|
cell.className = `tetris-block ${board[row][col]} w-full h-full`; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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.y + row; |
|
|
const boardCol = currentPiece.x + col; |
|
|
if (boardRow >= 0 && boardRow < ROWS && boardCol >= 0 && boardCol < COLS) { |
|
|
const cell = document.querySelector(`#game-board div[data-row="${boardRow}"][data-col="${boardCol}"]`); |
|
|
if (cell) { |
|
|
cell.className = `tetris-block ${currentPiece.color} w-full h-full`; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function drawNextPiece() { |
|
|
if (!nextPiece) return; |
|
|
|
|
|
|
|
|
nextPieceDisplay.innerHTML = ''; |
|
|
|
|
|
|
|
|
const rows = nextPiece.shape.length; |
|
|
const cols = nextPiece.shape[0].length; |
|
|
|
|
|
|
|
|
const cellSize = Math.min(100 / cols, 100 / rows); |
|
|
const containerWidth = cellSize * cols; |
|
|
const containerHeight = cellSize * rows; |
|
|
|
|
|
|
|
|
nextPieceDisplay.style.gridTemplateColumns = `repeat(${cols}, ${cellSize}px)`; |
|
|
nextPieceDisplay.style.gridTemplateRows = `repeat(${rows}, ${cellSize}px)`; |
|
|
nextPieceDisplay.style.width = `${containerWidth}px`; |
|
|
nextPieceDisplay.style.height = `${containerHeight}px`; |
|
|
nextPieceDisplay.style.left = `${(100 - containerWidth) / 2}px`; |
|
|
nextPieceDisplay.style.top = `${(100 - containerHeight) / 2}px`; |
|
|
|
|
|
|
|
|
for (let row = 0; row < rows; row++) { |
|
|
for (let col = 0; col < cols; col++) { |
|
|
const cell = document.createElement('div'); |
|
|
if (nextPiece.shape[row][col]) { |
|
|
cell.className = `tetris-block ${nextPiece.color} next-piece-cell`; |
|
|
} else { |
|
|
cell.className = 'next-piece-cell opacity-0'; |
|
|
} |
|
|
nextPieceDisplay.appendChild(cell); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function collision(x, y, shape) { |
|
|
for (let row = 0; row < shape.length; row++) { |
|
|
for (let col = 0; col < shape[row].length; col++) { |
|
|
if (shape[row][col]) { |
|
|
const newX = x + col; |
|
|
const newY = y + row; |
|
|
|
|
|
|
|
|
if (newX < 0 || newX >= COLS || newY >= ROWS) { |
|
|
return true; |
|
|
} |
|
|
|
|
|
|
|
|
if (newY >= 0 && board[newY][newX] !== EMPTY) { |
|
|
return true; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
return false; |
|
|
} |
|
|
|
|
|
|
|
|
function rotatePiece() { |
|
|
if (!currentPiece || gameOver || isPaused) return; |
|
|
|
|
|
const newShape = []; |
|
|
for (let col = currentPiece.shape[0].length - 1; col >= 0; col--) { |
|
|
const newRow = []; |
|
|
for (let row = 0; row < currentPiece.shape.length; row++) { |
|
|
newRow.push(currentPiece.shape[row][col]); |
|
|
} |
|
|
newShape.push(newRow); |
|
|
} |
|
|
|
|
|
|
|
|
if (!collision(currentPiece.x, currentPiece.y, newShape)) { |
|
|
currentPiece.shape = newShape; |
|
|
drawBoard(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function moveLeft() { |
|
|
if (!currentPiece || gameOver || isPaused) return; |
|
|
|
|
|
if (!collision(currentPiece.x - 1, currentPiece.y, currentPiece.shape)) { |
|
|
currentPiece.x--; |
|
|
drawBoard(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function moveRight() { |
|
|
if (!currentPiece || gameOver || isPaused) return; |
|
|
|
|
|
if (!collision(currentPiece.x + 1, currentPiece.y, currentPiece.shape)) { |
|
|
currentPiece.x++; |
|
|
drawBoard(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function moveDown() { |
|
|
if (!currentPiece || gameOver || isPaused) return; |
|
|
|
|
|
if (!collision(currentPiece.x, currentPiece.y + 1, currentPiece.shape)) { |
|
|
currentPiece.y++; |
|
|
drawBoard(); |
|
|
} else { |
|
|
lockPiece(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function dropPiece() { |
|
|
if (!currentPiece || gameOver || isPaused) return; |
|
|
|
|
|
while (!collision(currentPiece.x, currentPiece.y + 1, currentPiece.shape)) { |
|
|
currentPiece.y++; |
|
|
} |
|
|
lockPiece(); |
|
|
drawBoard(); |
|
|
} |
|
|
|
|
|
|
|
|
function lockPiece() { |
|
|
if (!currentPiece) return; |
|
|
|
|
|
|
|
|
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.y + row; |
|
|
const boardCol = currentPiece.x + col; |
|
|
|
|
|
if (boardRow >= 0 && boardRow < ROWS && boardCol >= 0 && boardCol < COLS) { |
|
|
board[boardRow][boardCol] = currentPiece.color; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
checkLines(); |
|
|
|
|
|
|
|
|
if (currentPiece.y <= 0) { |
|
|
gameOver = true; |
|
|
clearInterval(dropInterval); |
|
|
gameOverlay.classList.remove('hidden'); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
currentPiece = nextPiece; |
|
|
nextPiece = getRandomPiece(); |
|
|
drawNextPiece(); |
|
|
} |
|
|
|
|
|
|
|
|
function checkLines() { |
|
|
let linesCleared = 0; |
|
|
|
|
|
for (let row = ROWS - 1; row >= 0; row--) { |
|
|
if (board[row].every(cell => cell !== EMPTY)) { |
|
|
|
|
|
board.splice(row, 1); |
|
|
|
|
|
board.unshift(Array(COLS).fill(EMPTY)); |
|
|
linesCleared++; |
|
|
row++; |
|
|
} |
|
|
} |
|
|
|
|
|
if (linesCleared > 0) { |
|
|
|
|
|
const points = [0, 40, 100, 300, 1200][linesCleared] * level; |
|
|
score += points; |
|
|
lines += linesCleared; |
|
|
|
|
|
|
|
|
const newLevel = Math.floor(lines / 10) + 1; |
|
|
if (newLevel > level) { |
|
|
level = newLevel; |
|
|
|
|
|
dropSpeed = Math.max(100, 1000 - (level - 1) * 100); |
|
|
clearInterval(dropInterval); |
|
|
dropInterval = setInterval(moveDown, dropSpeed); |
|
|
} |
|
|
|
|
|
updateStats(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function updateStats() { |
|
|
scoreDisplay.textContent = score; |
|
|
levelDisplay.textContent = level; |
|
|
linesDisplay.textContent = lines; |
|
|
} |
|
|
|
|
|
|
|
|
function startGame() { |
|
|
board = createEmptyBoard(); |
|
|
score = 0; |
|
|
level = 1; |
|
|
lines = 0; |
|
|
gameOver = false; |
|
|
isPaused = false; |
|
|
dropSpeed = 1000; |
|
|
|
|
|
updateStats(); |
|
|
|
|
|
currentPiece = getRandomPiece(); |
|
|
nextPiece = getRandomPiece(); |
|
|
|
|
|
drawBoard(); |
|
|
drawNextPiece(); |
|
|
|
|
|
gameOverlay.classList.add('hidden'); |
|
|
|
|
|
clearInterval(dropInterval); |
|
|
dropInterval = setInterval(moveDown, dropSpeed); |
|
|
} |
|
|
|
|
|
|
|
|
function togglePause() { |
|
|
if (gameOver) return; |
|
|
|
|
|
isPaused = !isPaused; |
|
|
|
|
|
if (isPaused) { |
|
|
clearInterval(dropInterval); |
|
|
document.querySelector('h1').textContent = 'PAUSED'; |
|
|
} else { |
|
|
dropInterval = setInterval(moveDown, dropSpeed); |
|
|
document.querySelector('h1').textContent = 'NEON TETRIS'; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
document.addEventListener('keydown', (e) => { |
|
|
if (gameOver && e.key === ' ') { |
|
|
startGame(); |
|
|
return; |
|
|
} |
|
|
|
|
|
switch (e.key) { |
|
|
case 'ArrowLeft': |
|
|
moveLeft(); |
|
|
break; |
|
|
case 'ArrowRight': |
|
|
moveRight(); |
|
|
break; |
|
|
case 'ArrowDown': |
|
|
moveDown(); |
|
|
break; |
|
|
case 'ArrowUp': |
|
|
rotatePiece(); |
|
|
break; |
|
|
case ' ': |
|
|
dropPiece(); |
|
|
break; |
|
|
case 'p': |
|
|
case 'P': |
|
|
togglePause(); |
|
|
break; |
|
|
} |
|
|
}); |
|
|
|
|
|
restartBtn.addEventListener('click', startGame); |
|
|
|
|
|
|
|
|
let touchStartX = 0; |
|
|
let touchStartY = 0; |
|
|
|
|
|
gameBoard.addEventListener('touchstart', (e) => { |
|
|
touchStartX = e.touches[0].clientX; |
|
|
touchStartY = e.touches[0].clientY; |
|
|
}, false); |
|
|
|
|
|
gameBoard.addEventListener('touchend', (e) => { |
|
|
if (!touchStartX || !touchStartY || gameOver || isPaused) return; |
|
|
|
|
|
const touchEndX = e.changedTouches[0].clientX; |
|
|
const touchEndY = e.changedTouches[0].clientY; |
|
|
|
|
|
const diffX = touchStartX - touchEndX; |
|
|
const diffY = touchStartY - touchEndY; |
|
|
|
|
|
|
|
|
if (diffX > 50 && Math.abs(diffY) < 100) { |
|
|
moveLeft(); |
|
|
} |
|
|
|
|
|
else if (diffX < -50 && Math.abs(diffY) < 100) { |
|
|
moveRight(); |
|
|
} |
|
|
|
|
|
else if (diffY < -50 && Math.abs(diffX) < 100) { |
|
|
dropPiece(); |
|
|
} |
|
|
|
|
|
else if (Math.abs(diffX) < 50 && Math.abs(diffY) < 50) { |
|
|
rotatePiece(); |
|
|
} |
|
|
}, false); |
|
|
|
|
|
|
|
|
initializeBoard(); |
|
|
startGame(); |
|
|
}); |
|
|
</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=FriedsU/tetris" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
|
|
</html> |