|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Modern 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 flash { |
|
0%, 100% { opacity: 1; } |
|
50% { opacity: 0.5; } |
|
} |
|
|
|
.flash-animation { |
|
animation: flash 0.3s ease-in-out 3; |
|
} |
|
|
|
.game-container { |
|
perspective: 1000px; |
|
} |
|
|
|
.tetris-block { |
|
box-shadow: inset 0 0 10px rgba(255, 255, 255, 0.3); |
|
transition: all 0.1s ease; |
|
} |
|
|
|
.tetris-block.active { |
|
transform: scale(0.95); |
|
} |
|
|
|
.next-piece-container { |
|
background: rgba(255, 255, 255, 0.1); |
|
border-radius: 10px; |
|
} |
|
</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"> |
|
<h1 class="text-4xl font-bold text-center mb-6 text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-pink-600"> |
|
<i class="fas fa-gamepad mr-2"></i> Modern Tetris |
|
</h1> |
|
|
|
<div class="flex flex-col md:flex-row gap-8 items-center justify-center"> |
|
|
|
<div class="game-container relative"> |
|
<div class="bg-gray-800 rounded-lg overflow-hidden shadow-2xl"> |
|
<canvas id="tetris" width="300" height="600" class="block"></canvas> |
|
</div> |
|
|
|
|
|
<div id="gameOver" class="absolute inset-0 bg-black bg-opacity-70 flex flex-col items-center justify-center hidden"> |
|
<h2 class="text-4xl font-bold mb-4 text-red-500">Game Over!</h2> |
|
<p class="text-xl mb-6">Your score: <span id="finalScore">0</span></p> |
|
<button id="restartBtn" class="px-6 py-3 bg-gradient-to-r from-purple-500 to-pink-600 rounded-full font-bold hover:opacity-90 transition"> |
|
<i class="fas fa-redo mr-2"></i> Play Again |
|
</button> |
|
</div> |
|
|
|
|
|
<div id="pauseScreen" class="absolute inset-0 bg-black bg-opacity-70 flex flex-col items-center justify-center hidden"> |
|
<h2 class="text-4xl font-bold mb-4 text-yellow-400">Paused</h2> |
|
<button id="resumeBtn" class="px-6 py-3 bg-gradient-to-r from-blue-500 to-teal-400 rounded-full font-bold hover:opacity-90 transition"> |
|
<i class="fas fa-play mr-2"></i> Resume |
|
</button> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="flex flex-col gap-6 w-full md:w-auto"> |
|
|
|
<div class="bg-gray-800 p-6 rounded-xl shadow-lg"> |
|
<div class="flex justify-between items-center mb-4"> |
|
<div> |
|
<p class="text-gray-400">Score</p> |
|
<p id="score" class="text-3xl font-bold">0</p> |
|
</div> |
|
<div> |
|
<p class="text-gray-400">Level</p> |
|
<p id="level" class="text-3xl font-bold">1</p> |
|
</div> |
|
</div> |
|
<div class="flex justify-between items-center"> |
|
<div> |
|
<p class="text-gray-400">Lines</p> |
|
<p id="lines" class="text-3xl font-bold">0</p> |
|
</div> |
|
<div> |
|
<p class="text-gray-400">High Score</p> |
|
<p id="highScore" class="text-3xl font-bold">0</p> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="bg-gray-800 p-6 rounded-xl shadow-lg"> |
|
<h3 class="text-xl font-bold mb-4 text-center">Next Piece</h3> |
|
<div class="next-piece-container p-4 flex justify-center"> |
|
<canvas id="nextPiece" width="120" height="120" class="block"></canvas> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="bg-gray-800 p-6 rounded-xl shadow-lg"> |
|
<h3 class="text-xl font-bold mb-4 text-center">Controls</h3> |
|
<div class="grid grid-cols-3 gap-3 text-center"> |
|
<div class="bg-gray-700 p-3 rounded-lg"> |
|
<div class="text-2xl mb-1"><i class="fas fa-arrow-up"></i></div> |
|
<p class="text-sm">Rotate</p> |
|
</div> |
|
<div class="bg-gray-700 p-3 rounded-lg"> |
|
<div class="text-2xl mb-1"><i class="fas fa-arrow-left"></i></div> |
|
<p class="text-sm">Left</p> |
|
</div> |
|
<div class="bg-gray-700 p-3 rounded-lg"> |
|
<div class="text-2xl mb-1"><i class="fas fa-arrow-right"></i></div> |
|
<p class="text-sm">Right</p> |
|
</div> |
|
<div class="bg-gray-700 p-3 rounded-lg"> |
|
<div class="text-2xl mb-1"><i class="fas fa-arrow-down"></i></div> |
|
<p class="text-sm">Down</p> |
|
</div> |
|
<div class="bg-gray-700 p-3 rounded-lg col-span-2"> |
|
<div class="text-2xl mb-1"><i class="fas fa-space-shuttle"></i></div> |
|
<p class="text-sm">Hard Drop</p> |
|
</div> |
|
<div class="bg-gray-700 p-3 rounded-lg"> |
|
<div class="text-2xl mb-1"><i class="fas fa-pause"></i></div> |
|
<p class="text-sm">Pause</p> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="flex gap-4"> |
|
<button id="startBtn" class="flex-1 px-6 py-3 bg-gradient-to-r from-green-500 to-emerald-600 rounded-full font-bold hover:opacity-90 transition"> |
|
<i class="fas fa-play mr-2"></i> Start |
|
</button> |
|
<button id="pauseBtn" class="flex-1 px-6 py-3 bg-gradient-to-r from-blue-500 to-teal-400 rounded-full font-bold hover:opacity-90 transition"> |
|
<i class="fas fa-pause mr-2"></i> Pause |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
|
const COLS = 10; |
|
const ROWS = 20; |
|
const BLOCK_SIZE = 30; |
|
const COLORS = [ |
|
null, |
|
'#FF0D72', |
|
'#0DC2FF', |
|
'#0DFF72', |
|
'#F538FF', |
|
'#FF8E0D', |
|
'#FFE138', |
|
'#3877FF' |
|
]; |
|
|
|
|
|
let canvas = document.getElementById('tetris'); |
|
let ctx = canvas.getContext('2d'); |
|
let nextPieceCanvas = document.getElementById('nextPiece'); |
|
let nextPieceCtx = nextPieceCanvas.getContext('2d'); |
|
let scoreElement = document.getElementById('score'); |
|
let linesElement = document.getElementById('lines'); |
|
let levelElement = document.getElementById('level'); |
|
let highScoreElement = document.getElementById('highScore'); |
|
let finalScoreElement = document.getElementById('finalScore'); |
|
let gameOverElement = document.getElementById('gameOver'); |
|
let pauseScreenElement = document.getElementById('pauseScreen'); |
|
let startBtn = document.getElementById('startBtn'); |
|
let pauseBtn = document.getElementById('pauseBtn'); |
|
let resumeBtn = document.getElementById('resumeBtn'); |
|
let restartBtn = document.getElementById('restartBtn'); |
|
|
|
|
|
scaleCanvas(); |
|
|
|
let score = 0; |
|
let lines = 0; |
|
let level = 1; |
|
let highScore = localStorage.getItem('tetrisHighScore') || 0; |
|
let gameOver = true; |
|
let paused = false; |
|
let dropCounter = 0; |
|
let dropInterval = 1000; |
|
let lastTime = 0; |
|
let animationId = null; |
|
|
|
highScoreElement.textContent = highScore; |
|
|
|
|
|
const board = createMatrix(COLS, ROWS); |
|
|
|
|
|
let player = { |
|
pos: {x: 0, y: 0}, |
|
matrix: null, |
|
next: null |
|
}; |
|
|
|
|
|
const SHAPES = [ |
|
null, |
|
[[0, 0, 0, 0], [1, 1, 1, 1], [0, 0, 0, 0], [0, 0, 0, 0]], |
|
[[2, 0, 0], [2, 2, 2], [0, 0, 0]], |
|
[[0, 0, 3], [3, 3, 3], [0, 0, 0]], |
|
[[0, 4, 4], [0, 4, 4], [0, 0, 0]], |
|
[[0, 5, 5], [5, 5, 0], [0, 0, 0]], |
|
[[0, 6, 0], [6, 6, 6], [0, 0, 0]], |
|
[[7, 7, 0], [0, 7, 7], [0, 0, 0]] |
|
]; |
|
|
|
|
|
document.addEventListener('keydown', handleKeyPress); |
|
startBtn.addEventListener('click', startGame); |
|
pauseBtn.addEventListener('click', togglePause); |
|
resumeBtn.addEventListener('click', togglePause); |
|
restartBtn.addEventListener('click', startGame); |
|
|
|
|
|
resetGame(); |
|
drawNextPiece(); |
|
|
|
|
|
function createMatrix(w, h) { |
|
const matrix = []; |
|
while (h--) { |
|
matrix.push(new Array(w).fill(0)); |
|
} |
|
return matrix; |
|
} |
|
|
|
function createPiece(type) { |
|
return SHAPES[type]; |
|
} |
|
|
|
function drawMatrix(matrix, offset) { |
|
matrix.forEach((row, y) => { |
|
row.forEach((value, x) => { |
|
if (value !== 0) { |
|
ctx.fillStyle = COLORS[value]; |
|
ctx.fillRect( |
|
(x + offset.x) * BLOCK_SIZE, |
|
(y + offset.y) * BLOCK_SIZE, |
|
BLOCK_SIZE, |
|
BLOCK_SIZE |
|
); |
|
|
|
|
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)'; |
|
ctx.lineWidth = 2; |
|
ctx.strokeRect( |
|
(x + offset.x) * BLOCK_SIZE, |
|
(y + offset.y) * BLOCK_SIZE, |
|
BLOCK_SIZE, |
|
BLOCK_SIZE |
|
); |
|
|
|
|
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.1)'; |
|
ctx.fillRect( |
|
(x + offset.x) * BLOCK_SIZE + 2, |
|
(y + offset.y) * BLOCK_SIZE + 2, |
|
BLOCK_SIZE - 4, |
|
BLOCK_SIZE - 4 |
|
); |
|
} |
|
}); |
|
}); |
|
} |
|
|
|
function drawNextPiece() { |
|
nextPieceCtx.clearRect(0, 0, nextPieceCanvas.width, nextPieceCanvas.height); |
|
|
|
if (player.next) { |
|
const matrix = player.next; |
|
const offsetX = (nextPieceCanvas.width / BLOCK_SIZE - matrix[0].length) / 2; |
|
const offsetY = (nextPieceCanvas.height / BLOCK_SIZE - matrix.length) / 2; |
|
|
|
matrix.forEach((row, y) => { |
|
row.forEach((value, x) => { |
|
if (value !== 0) { |
|
nextPieceCtx.fillStyle = COLORS[value]; |
|
nextPieceCtx.fillRect( |
|
(x + offsetX) * BLOCK_SIZE, |
|
(y + offsetY) * BLOCK_SIZE, |
|
BLOCK_SIZE, |
|
BLOCK_SIZE |
|
); |
|
|
|
nextPieceCtx.strokeStyle = 'rgba(255, 255, 255, 0.2)'; |
|
nextPieceCtx.lineWidth = 2; |
|
nextPieceCtx.strokeRect( |
|
(x + offsetX) * BLOCK_SIZE, |
|
(y + offsetY) * BLOCK_SIZE, |
|
BLOCK_SIZE, |
|
BLOCK_SIZE |
|
); |
|
|
|
nextPieceCtx.fillStyle = 'rgba(255, 255, 255, 0.1)'; |
|
nextPieceCtx.fillRect( |
|
(x + offsetX) * BLOCK_SIZE + 2, |
|
(y + offsetY) * BLOCK_SIZE + 2, |
|
BLOCK_SIZE - 4, |
|
BLOCK_SIZE - 4 |
|
); |
|
} |
|
}); |
|
}); |
|
} |
|
} |
|
|
|
function draw() { |
|
|
|
ctx.fillStyle = '#111827'; |
|
ctx.fillRect(0, 0, canvas.width, canvas.height); |
|
|
|
|
|
drawMatrix(board, {x: 0, y: 0}); |
|
|
|
|
|
if (player.matrix) { |
|
drawMatrix(player.matrix, player.pos); |
|
} |
|
|
|
|
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)'; |
|
ctx.lineWidth = 1; |
|
|
|
|
|
for (let i = 0; i <= COLS; i++) { |
|
ctx.beginPath(); |
|
ctx.moveTo(i * BLOCK_SIZE, 0); |
|
ctx.lineTo(i * BLOCK_SIZE, ROWS * BLOCK_SIZE); |
|
ctx.stroke(); |
|
} |
|
|
|
|
|
for (let i = 0; i <= ROWS; i++) { |
|
ctx.beginPath(); |
|
ctx.moveTo(0, i * BLOCK_SIZE); |
|
ctx.lineTo(COLS * BLOCK_SIZE, i * BLOCK_SIZE); |
|
ctx.stroke(); |
|
} |
|
} |
|
|
|
function merge() { |
|
player.matrix.forEach((row, y) => { |
|
row.forEach((value, x) => { |
|
if (value !== 0) { |
|
board[y + player.pos.y][x + player.pos.x] = value; |
|
} |
|
}); |
|
}); |
|
} |
|
|
|
function rotate(matrix) { |
|
const N = matrix.length; |
|
const result = createMatrix(N, N); |
|
|
|
for (let y = 0; y < N; ++y) { |
|
for (let x = 0; x < N; ++x) { |
|
result[x][N - 1 - y] = matrix[y][x]; |
|
} |
|
} |
|
|
|
return result; |
|
} |
|
|
|
function playerRotate() { |
|
const pos = player.pos.x; |
|
let offset = 1; |
|
const matrix = rotate(player.matrix); |
|
|
|
if (collide(board, {matrix, pos: player.pos})) { |
|
const left = player.pos.x - offset; |
|
const right = player.pos.x + offset; |
|
|
|
if (!collide(board, {matrix, pos: {...player.pos, x: left}})) { |
|
player.pos.x = left; |
|
} else if (!collide(board, {matrix, pos: {...player.pos, x: right}})) { |
|
player.pos.x = right; |
|
} else { |
|
return; |
|
} |
|
} |
|
|
|
player.matrix = matrix; |
|
} |
|
|
|
function playerMove(dir) { |
|
player.pos.x += dir; |
|
if (collide(board, player)) { |
|
player.pos.x -= dir; |
|
} |
|
} |
|
|
|
function playerDrop() { |
|
player.pos.y++; |
|
if (collide(board, player)) { |
|
player.pos.y--; |
|
merge(); |
|
playerReset(); |
|
arenaSweep(); |
|
updateScore(); |
|
} |
|
dropCounter = 0; |
|
} |
|
|
|
function playerHardDrop() { |
|
while (!collide(board, player)) { |
|
player.pos.y++; |
|
} |
|
player.pos.y--; |
|
merge(); |
|
playerReset(); |
|
arenaSweep(); |
|
updateScore(); |
|
dropCounter = 0; |
|
} |
|
|
|
function collide(board, player) { |
|
const [m, o] = [player.matrix, player.pos]; |
|
for (let y = 0; y < m.length; ++y) { |
|
for (let x = 0; x < m[y].length; ++x) { |
|
if (m[y][x] !== 0 && |
|
(board[y + o.y] === undefined || |
|
board[y + o.y][x + o.x] === undefined || |
|
board[y + o.y][x + o.x] !== 0)) { |
|
return true; |
|
} |
|
} |
|
} |
|
return false; |
|
} |
|
|
|
function playerReset() { |
|
const pieces = 'IJLOSTZ'; |
|
player.matrix = player.next || createPiece(pieces.charCodeAt(Math.floor(Math.random() * pieces.length)) - 64); |
|
player.next = createPiece(pieces.charCodeAt(Math.floor(Math.random() * pieces.length)) - 64); |
|
player.pos.y = 0; |
|
player.pos.x = Math.floor((COLS / 2) - (player.matrix[0].length / 2)); |
|
|
|
drawNextPiece(); |
|
|
|
if (collide(board, player)) { |
|
gameOver = true; |
|
gameOverElement.classList.remove('hidden'); |
|
finalScoreElement.textContent = score; |
|
|
|
if (score > highScore) { |
|
highScore = score; |
|
localStorage.setItem('tetrisHighScore', highScore); |
|
highScoreElement.textContent = highScore; |
|
} |
|
} |
|
} |
|
|
|
function arenaSweep() { |
|
let linesCleared = 0; |
|
|
|
outer: for (let y = board.length - 1; y >= 0; --y) { |
|
for (let x = 0; x < board[y].length; ++x) { |
|
if (board[y][x] === 0) { |
|
continue outer; |
|
} |
|
} |
|
|
|
|
|
const row = board.splice(y, 1)[0].fill(0); |
|
board.unshift(row); |
|
++y; |
|
|
|
linesCleared++; |
|
} |
|
|
|
if (linesCleared > 0) { |
|
|
|
const linesElement = document.createElement('div'); |
|
linesElement.className = 'absolute inset-0 bg-white opacity-30 flash-animation'; |
|
document.querySelector('.game-container').appendChild(linesElement); |
|
|
|
setTimeout(() => { |
|
linesElement.remove(); |
|
}, 1000); |
|
|
|
lines += linesCleared; |
|
updateLevel(); |
|
} |
|
} |
|
|
|
function updateScore() { |
|
const points = [0, 40, 100, 300, 1200]; |
|
const linesCleared = Math.min(4, lines - Math.floor(lines / 10) * 10); |
|
|
|
if (linesCleared > 0) { |
|
score += points[linesCleared] * level; |
|
scoreElement.textContent = score; |
|
linesElement.textContent = lines; |
|
} |
|
} |
|
|
|
function updateLevel() { |
|
level = Math.floor(lines / 10) + 1; |
|
levelElement.textContent = level; |
|
dropInterval = 1000 - (level - 1) * 100; |
|
if (dropInterval < 100) dropInterval = 100; |
|
} |
|
|
|
function handleKeyPress(e) { |
|
if (gameOver || paused) return; |
|
|
|
switch (e.keyCode) { |
|
case 37: |
|
playerMove(-1); |
|
break; |
|
case 39: |
|
playerMove(1); |
|
break; |
|
case 40: |
|
playerDrop(); |
|
break; |
|
case 38: |
|
playerRotate(); |
|
break; |
|
case 32: |
|
playerHardDrop(); |
|
break; |
|
case 80: |
|
togglePause(); |
|
break; |
|
} |
|
} |
|
|
|
function togglePause() { |
|
if (gameOver) return; |
|
|
|
paused = !paused; |
|
|
|
if (paused) { |
|
pauseScreenElement.classList.remove('hidden'); |
|
cancelAnimationFrame(animationId); |
|
} else { |
|
pauseScreenElement.classList.add('hidden'); |
|
lastTime = 0; |
|
animationId = requestAnimationFrame(update); |
|
} |
|
} |
|
|
|
function resetGame() { |
|
|
|
for (let y = 0; y < ROWS; y++) { |
|
for (let x = 0; x < COLS; x++) { |
|
board[y][x] = 0; |
|
} |
|
} |
|
|
|
|
|
score = 0; |
|
lines = 0; |
|
level = 1; |
|
dropInterval = 1000; |
|
|
|
|
|
scoreElement.textContent = score; |
|
linesElement.textContent = lines; |
|
levelElement.textContent = level; |
|
|
|
|
|
gameOverElement.classList.add('hidden'); |
|
} |
|
|
|
function startGame() { |
|
if (!gameOver && !paused) return; |
|
|
|
resetGame(); |
|
gameOver = false; |
|
paused = false; |
|
pauseScreenElement.classList.add('hidden'); |
|
playerReset(); |
|
|
|
if (animationId) { |
|
cancelAnimationFrame(animationId); |
|
} |
|
|
|
lastTime = 0; |
|
animationId = requestAnimationFrame(update); |
|
} |
|
|
|
function update(time = 0) { |
|
const deltaTime = time - lastTime; |
|
lastTime = time; |
|
|
|
dropCounter += deltaTime; |
|
if (dropCounter > dropInterval) { |
|
playerDrop(); |
|
} |
|
|
|
draw(); |
|
animationId = requestAnimationFrame(update); |
|
} |
|
|
|
function scaleCanvas() { |
|
|
|
const scale = window.devicePixelRatio; |
|
canvas.style.width = canvas.width + 'px'; |
|
canvas.style.height = canvas.height + 'px'; |
|
canvas.width = canvas.width * scale; |
|
canvas.height = canvas.height * scale; |
|
ctx.scale(scale, scale); |
|
|
|
|
|
nextPieceCanvas.style.width = nextPieceCanvas.width + 'px'; |
|
nextPieceCanvas.style.height = nextPieceCanvas.height + 'px'; |
|
nextPieceCanvas.width = nextPieceCanvas.width * scale; |
|
nextPieceCanvas.height = nextPieceCanvas.height * scale; |
|
nextPieceCtx.scale(scale, scale); |
|
} |
|
|
|
|
|
draw(); |
|
}); |
|
</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=ysharma/modern-tetris-vibe-coded" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p></body> |
|
</html> |