|
<!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> |
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
|
<style> |
|
@keyframes pulse { |
|
0% { opacity: 0.7; } |
|
50% { opacity: 1; } |
|
100% { opacity: 0.7; } |
|
} |
|
|
|
@keyframes colorBlast { |
|
0% { transform: scale(1); opacity: 1; } |
|
100% { transform: scale(1.5); opacity: 0; } |
|
} |
|
|
|
.cell { |
|
transition: background-color 0.1s, transform 0.1s; |
|
} |
|
|
|
.cell.filled { |
|
box-shadow: inset 0 0 10px rgba(255, 255, 255, 0.5); |
|
} |
|
|
|
.color-blast { |
|
position: absolute; |
|
animation: colorBlast 0.5s forwards; |
|
z-index: 10; |
|
border-radius: 2px; |
|
} |
|
|
|
.game-container { |
|
perspective: 1000px; |
|
} |
|
|
|
.tetris-board { |
|
transform-style: preserve-3d; |
|
transition: transform 0.3s; |
|
} |
|
|
|
.tetris-board.tilt-left { |
|
transform: rotateY(-5deg); |
|
} |
|
|
|
.tetris-board.tilt-right { |
|
transform: rotateY(5deg); |
|
} |
|
|
|
.next-piece-container { |
|
background-color: rgba(255, 255, 255, 0.1); |
|
border-radius: 8px; |
|
padding: 10px; |
|
} |
|
|
|
.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; } |
|
|
|
.dark .piece-I { background-color: #00f0f0; } |
|
.dark .piece-J { background-color: #0000f0; } |
|
.dark .piece-L { background-color: #f0a000; } |
|
.dark .piece-O { background-color: #f0f000; } |
|
.dark .piece-S { background-color: #00f000; } |
|
.dark .piece-T { background-color: #a000f0; } |
|
.dark .piece-Z { background-color: #f00000; } |
|
|
|
.light .piece-I { background-color: #00b8b8; } |
|
.light .piece-J { background-color: #0000b8; } |
|
.light .piece-L { background-color: #b87800; } |
|
.light .piece-O { background-color: #b8b800; } |
|
.light .piece-S { background-color: #00b800; } |
|
.light .piece-T { background-color: #7800b8; } |
|
.light .piece-Z { background-color: #b80000; } |
|
|
|
.ghost-piece { |
|
opacity: 0.3; |
|
} |
|
|
|
.game-over-overlay { |
|
background-color: rgba(0, 0, 0, 0.7); |
|
display: flex; |
|
justify-content: center; |
|
align-items: center; |
|
z-index: 20; |
|
} |
|
|
|
.pulse-text { |
|
animation: pulse 1.5s infinite; |
|
} |
|
|
|
.rotate-btn:hover { |
|
transform: rotate(90deg); |
|
transition: transform 0.3s; |
|
} |
|
</style> |
|
</head> |
|
<body class="dark:bg-gray-900 dark:text-white bg-gray-100 text-gray-900 transition-colors duration-300 min-h-screen flex flex-col"> |
|
<div class="container mx-auto px-4 py-8 flex-grow"> |
|
<header class="flex justify-between items-center mb-8"> |
|
<h1 class="text-4xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-purple-500 to-blue-500"> |
|
Neon Tetris |
|
</h1> |
|
<button id="theme-toggle" class="p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"> |
|
<i class="fas fa-moon dark:hidden"></i> |
|
<i class="fas fa-sun hidden dark:inline"></i> |
|
</button> |
|
</header> |
|
|
|
<div class="flex flex-col lg:flex-row gap-8 items-center lg:items-start justify-center"> |
|
<div class="game-container relative"> |
|
<div id="tetris-board" class="tetris-board grid grid-cols-10 gap-px bg-gray-300 dark:bg-gray-700 p-px border-2 border-gray-400 dark:border-gray-600 rounded"> |
|
|
|
</div> |
|
|
|
<div id="game-over-overlay" class="game-over-overlay hidden absolute inset-0 rounded"> |
|
<div class="text-center p-6 bg-white dark:bg-gray-800 rounded-lg shadow-xl"> |
|
<h2 class="text-3xl font-bold mb-4 text-red-500 pulse-text">Game Over!</h2> |
|
<p class="text-xl mb-6">Your score: <span id="final-score" class="font-bold">0</span></p> |
|
<button id="restart-btn" class="px-6 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg font-semibold transition-colors"> |
|
Play Again |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="flex flex-col gap-6 w-full lg:w-auto"> |
|
<div class="bg-white dark:bg-gray-800 p-6 rounded-xl shadow-lg"> |
|
<h2 class="text-xl font-semibold mb-4">Game Info</h2> |
|
|
|
<div class="grid grid-cols-2 gap-4 mb-6"> |
|
<div> |
|
<p class="text-sm text-gray-500 dark:text-gray-400">Score</p> |
|
<p id="score" class="text-2xl font-bold">0</p> |
|
</div> |
|
<div> |
|
<p class="text-sm text-gray-500 dark:text-gray-400">Level</p> |
|
<p id="level" class="text-2xl font-bold">1</p> |
|
</div> |
|
<div> |
|
<p class="text-sm text-gray-500 dark:text-gray-400">Lines</p> |
|
<p id="lines" class="text-2xl font-bold">0</p> |
|
</div> |
|
<div> |
|
<p class="text-sm text-gray-500 dark:text-gray-400">Next</p> |
|
</div> |
|
</div> |
|
|
|
<div class="next-piece-container mb-6"> |
|
<div id="next-piece" class="grid grid-cols-4 gap-px bg-gray-200 dark:bg-gray-700 p-px w-24 h-24 mx-auto"> |
|
|
|
</div> |
|
</div> |
|
|
|
<div class="flex flex-col gap-3"> |
|
<div class="flex items-center gap-2"> |
|
<kbd class="px-2 py-1 bg-gray-200 dark:bg-gray-700 rounded text-sm">← →</kbd> |
|
<span>Move</span> |
|
</div> |
|
<div class="flex items-center gap-2"> |
|
<kbd class="px-2 py-1 bg-gray-200 dark:bg-gray-700 rounded text-sm">↑</kbd> |
|
<span>Rotate</span> |
|
</div> |
|
<div class="flex items-center gap-2"> |
|
<kbd class="px-2 py-1 bg-gray-200 dark:bg-gray-700 rounded text-sm">↓</kbd> |
|
<span>Soft Drop</span> |
|
</div> |
|
<div class="flex items-center gap-2"> |
|
<kbd class="px-2 py-1 bg-gray-200 dark:bg-gray-700 rounded text-sm">Space</kbd> |
|
<span>Hard Drop</span> |
|
</div> |
|
<div class="flex items-center gap-2"> |
|
<kbd class="px-2 py-1 bg-gray-200 dark:bg-gray-700 rounded text-sm">P</kbd> |
|
<span>Pause</span> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="flex gap-3 justify-center"> |
|
<button id="start-btn" class="px-6 py-3 bg-green-500 hover:bg-green-600 text-white rounded-lg font-semibold transition-colors flex items-center gap-2"> |
|
<i class="fas fa-play"></i> Start Game |
|
</button> |
|
<button id="pause-btn" class="px-6 py-3 bg-yellow-500 hover:bg-yellow-600 text-white rounded-lg font-semibold transition-colors flex items-center gap-2" disabled> |
|
<i class="fas fa-pause"></i> Pause |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<footer class="text-center py-4 text-gray-600 dark:text-gray-400 text-sm"> |
|
<p>Use arrow keys to play. Clear lines to score points!</p> |
|
</footer> |
|
|
|
<script> |
|
|
|
const COLS = 10; |
|
const ROWS = 20; |
|
const BLOCK_SIZE = 30; |
|
const EMPTY = 'empty'; |
|
|
|
|
|
const SHAPES = { |
|
I: { shape: [[1, 1, 1, 1]], className: 'piece-I' }, |
|
J: { shape: [[1, 0, 0], [1, 1, 1]], className: 'piece-J' }, |
|
L: { shape: [[0, 0, 1], [1, 1, 1]], className: 'piece-L' }, |
|
O: { shape: [[1, 1], [1, 1]], className: 'piece-O' }, |
|
S: { shape: [[0, 1, 1], [1, 1, 0]], className: 'piece-S' }, |
|
T: { shape: [[0, 1, 0], [1, 1, 1]], className: 'piece-T' }, |
|
Z: { shape: [[1, 1, 0], [0, 1, 1]], className: 'piece-Z' } |
|
}; |
|
|
|
const PIECES = Object.keys(SHAPES); |
|
|
|
|
|
let board = []; |
|
let currentPiece = null; |
|
let nextPiece = null; |
|
let currentPosition = { x: 0, y: 0 }; |
|
let score = 0; |
|
let level = 1; |
|
let lines = 0; |
|
let gameInterval = null; |
|
let isPaused = false; |
|
let isGameOver = false; |
|
let dropSpeed = 1000; |
|
let lastDropTime = 0; |
|
|
|
|
|
const tetrisBoard = document.getElementById('tetris-board'); |
|
const nextPieceDisplay = document.getElementById('next-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 restartBtn = document.getElementById('restart-btn'); |
|
const gameOverOverlay = document.getElementById('game-over-overlay'); |
|
const finalScoreDisplay = document.getElementById('final-score'); |
|
const themeToggle = document.getElementById('theme-toggle'); |
|
|
|
|
|
function init() { |
|
createBoard(); |
|
generateNextPiece(); |
|
updateDisplays(); |
|
setupEventListeners(); |
|
} |
|
|
|
|
|
function createBoard() { |
|
tetrisBoard.innerHTML = ''; |
|
board = Array(ROWS).fill().map(() => Array(COLS).fill(EMPTY)); |
|
|
|
|
|
for (let y = 0; y < ROWS; y++) { |
|
for (let x = 0; x < COLS; x++) { |
|
const cell = document.createElement('div'); |
|
cell.className = 'cell w-8 h-8 bg-white dark:bg-gray-800'; |
|
cell.dataset.x = x; |
|
cell.dataset.y = y; |
|
tetrisBoard.appendChild(cell); |
|
} |
|
} |
|
|
|
|
|
tetrisBoard.style.gridTemplateColumns = `repeat(${COLS}, minmax(0, 1fr))`; |
|
} |
|
|
|
|
|
function createNextPieceDisplay() { |
|
nextPieceDisplay.innerHTML = ''; |
|
|
|
|
|
for (let y = 0; y < 4; y++) { |
|
for (let x = 0; x < 4; x++) { |
|
const cell = document.createElement('div'); |
|
cell.className = 'cell w-6 h-6 bg-gray-200 dark:bg-gray-700'; |
|
cell.dataset.x = x; |
|
cell.dataset.y = y; |
|
nextPieceDisplay.appendChild(cell); |
|
} |
|
} |
|
} |
|
|
|
|
|
function generateRandomPiece() { |
|
const randomPiece = PIECES[Math.floor(Math.random() * PIECES.length)]; |
|
return { |
|
type: randomPiece, |
|
shape: SHAPES[randomPiece].shape, |
|
className: SHAPES[randomPiece].className |
|
}; |
|
} |
|
|
|
|
|
function generateNextPiece() { |
|
if (!nextPiece) { |
|
nextPiece = generateRandomPiece(); |
|
} |
|
|
|
currentPiece = nextPiece; |
|
nextPiece = generateRandomPiece(); |
|
|
|
|
|
currentPosition = { |
|
x: Math.floor(COLS / 2) - Math.floor(currentPiece.shape[0].length / 2), |
|
y: 0 |
|
}; |
|
|
|
|
|
displayNextPiece(); |
|
|
|
|
|
if (checkCollision()) { |
|
gameOver(); |
|
} |
|
} |
|
|
|
|
|
function displayNextPiece() { |
|
createNextPieceDisplay(); |
|
|
|
const cells = Array.from(nextPieceDisplay.children); |
|
const piece = nextPiece; |
|
const offsetX = Math.floor((4 - piece.shape[0].length) / 2); |
|
const offsetY = Math.floor((4 - piece.shape.length) / 2); |
|
|
|
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 cellIndex = (y + offsetY) * 4 + (x + offsetX); |
|
if (cellIndex >= 0 && cellIndex < cells.length) { |
|
cells[cellIndex].classList.add(piece.className, 'filled'); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
function drawPiece() { |
|
clearGhostPiece(); |
|
drawGhostPiece(); |
|
|
|
const cells = Array.from(tetrisBoard.children); |
|
const piece = currentPiece; |
|
|
|
|
|
cells.forEach(cell => { |
|
if (cell.classList.contains('current')) { |
|
cell.classList.remove('current', piece.className, 'filled'); |
|
} |
|
}); |
|
|
|
|
|
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 boardX = currentPosition.x + x; |
|
const boardY = |
|
</html> |