Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Responsive Tetris Game</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<style> | |
/* Custom styles that can't be done with Tailwind */ | |
.cell { | |
box-sizing: border-box; | |
border: 1px solid rgba(255, 255, 255, 0.1); | |
} | |
.cell-filled { | |
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.5); | |
} | |
@keyframes flash { | |
0% { opacity: 1; } | |
50% { opacity: 0.5; } | |
100% { opacity: 1; } | |
} | |
.flash-animation { | |
animation: flash 0.3s 2; | |
} | |
/* Tetromino colors */ | |
.cell-I { background-color: #00FFFF; } | |
.cell-O { background-color: #FFFF00; } | |
.cell-T { background-color: #AA00FF; } | |
.cell-S { background-color: #00FF00; } | |
.cell-Z { background-color: #FF0000; } | |
.cell-J { background-color: #0000FF; } | |
.cell-L { background-color: #FFAA00; } | |
</style> | |
</head> | |
<body class="bg-gray-900 text-white min-h-screen flex flex-col items-center justify-center p-4"> | |
<div class="text-center mb-4"> | |
<h1 class="text-4xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-pink-600 mb-2">Tetris</h1> | |
<div class="flex justify-center gap-8"> | |
<div class="text-xl"> | |
<span class="text-gray-300">Score:</span> | |
<span id="score" class="font-bold ml-2">0</span> | |
</div> | |
<div class="text-xl"> | |
<span class="text-gray-300">Level:</span> | |
<span id="level" class="font-bold ml-2">1</span> | |
</div> | |
<div class="text-xl"> | |
<span class="text-gray-300">Lines:</span> | |
<span id="lines" class="font-bold ml-2">0</span> | |
</div> | |
</div> | |
</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 bg-gray-800 border-2 border-gray-700 rounded-md overflow-hidden"></div> | |
<div id="game-over" class="absolute inset-0 bg-black bg-opacity-80 flex flex-col items-center justify-center hidden"> | |
<h2 class="text-3xl font-bold text-red-500 mb-4">Game Over!</h2> | |
<button id="restart-btn" class="px-6 py-2 bg-blue-600 hover:bg-blue-700 rounded-md font-bold transition">Play Again</button> | |
</div> | |
<div id="pause-screen" class="absolute inset-0 bg-black bg-opacity-70 flex items-center justify-center hidden"> | |
<h2 class="text-3xl font-bold text-yellow-400">Paused</h2> | |
</div> | |
</div> | |
<div class="flex flex-col gap-6"> | |
<div class="bg-gray-800 p-4 rounded-md border border-gray-700"> | |
<h3 class="text-xl font-semibold mb-2 text-center">Next Piece</h3> | |
<div id="next-piece" class="grid grid-cols-4 grid-rows-4 gap-0 w-24 h-24 mx-auto"></div> | |
</div> | |
<div class="bg-gray-800 p-4 rounded-md border border-gray-700"> | |
<h3 class="text-xl font-semibold mb-2 text-center">Controls</h3> | |
<div class="grid grid-cols-3 gap-2 text-center"> | |
<div class="bg-gray-700 p-2 rounded">← Left</div> | |
<div class="bg-gray-700 p-2 rounded">→ Right</div> | |
<div class="bg-gray-700 p-2 rounded">↑ Rotate</div> | |
<div class="bg-gray-700 p-2 rounded">↓ Soft Drop</div> | |
<div class="bg-gray-700 p-2 rounded">Space Hard Drop</div> | |
<div class="bg-gray-700 p-2 rounded">P Pause</div> | |
</div> | |
</div> | |
<button id="pause-btn" class="px-4 py-2 bg-yellow-600 hover:bg-yellow-700 rounded-md font-bold transition">Pause</button> | |
</div> | |
</div> | |
<div class="mt-8 text-gray-400 text-sm"> | |
<p>Use keyboard or touch controls to play. Clear lines to score points!</p> | |
</div> | |
<script> | |
document.addEventListener('DOMContentLoaded', () => { | |
// 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 score = 0; | |
let level = 1; | |
let lines = 0; | |
let gameOver = false; | |
let isPaused = false; | |
let dropInterval = null; | |
let dropSpeed = 1000; // Initial speed (ms) | |
// DOM elements | |
const gameBoard = document.getElementById('game-board'); | |
const nextPieceDisplay = document.getElementById('next-piece'); | |
const scoreDisplay = document.getElementById('score'); | |
const levelDisplay = document.getElementById('level'); | |
const linesDisplay = document.getElementById('lines'); | |
const gameOverScreen = document.getElementById('game-over'); | |
const pauseScreen = document.getElementById('pause-screen'); | |
const restartBtn = document.getElementById('restart-btn'); | |
const pauseBtn = document.getElementById('pause-btn'); | |
// Tetromino shapes | |
const SHAPES = { | |
I: [ | |
[0, 0, 0, 0], | |
[1, 1, 1, 1], | |
[0, 0, 0, 0], | |
[0, 0, 0, 0] | |
], | |
O: [ | |
[1, 1], | |
[1, 1] | |
], | |
T: [ | |
[0, 1, 0], | |
[1, 1, 1], | |
[0, 0, 0] | |
], | |
S: [ | |
[0, 1, 1], | |
[1, 1, 0], | |
[0, 0, 0] | |
], | |
Z: [ | |
[1, 1, 0], | |
[0, 1, 1], | |
[0, 0, 0] | |
], | |
J: [ | |
[1, 0, 0], | |
[1, 1, 1], | |
[0, 0, 0] | |
], | |
L: [ | |
[0, 0, 1], | |
[1, 1, 1], | |
[0, 0, 0] | |
] | |
}; | |
const COLORS = { | |
I: 'cell-I', | |
O: 'cell-O', | |
T: 'cell-T', | |
S: 'cell-S', | |
Z: 'cell-Z', | |
J: 'cell-J', | |
L: 'cell-L' | |
}; | |
// Initialize the game board | |
function initBoard() { | |
gameBoard.innerHTML = ''; | |
gameBoard.style.width = `${COLS * BLOCK_SIZE}px`; | |
gameBoard.style.height = `${ROWS * BLOCK_SIZE}px`; | |
for (let row = 0; row < ROWS; row++) { | |
for (let col = 0; col < COLS; col++) { | |
const cell = document.createElement('div'); | |
cell.className = 'cell w-full h-full'; | |
cell.dataset.row = row; | |
cell.dataset.col = col; | |
gameBoard.appendChild(cell); | |
} | |
} | |
} | |
// Initialize the next piece display | |
function initNextPieceDisplay() { | |
nextPieceDisplay.innerHTML = ''; | |
nextPieceDisplay.style.width = `${NEXT_PIECE_SIZE * BLOCK_SIZE}px`; | |
nextPieceDisplay.style.height = `${NEXT_PIECE_SIZE * BLOCK_SIZE}px`; | |
for (let row = 0; row < NEXT_PIECE_SIZE; row++) { | |
for (let col = 0; col < NEXT_PIECE_SIZE; col++) { | |
const cell = document.createElement('div'); | |
cell.className = 'cell w-full h-full'; | |
cell.dataset.row = row; | |
cell.dataset.col = col; | |
nextPieceDisplay.appendChild(cell); | |
} | |
} | |
} | |
// Create a new random piece | |
function createPiece() { | |
const types = Object.keys(SHAPES); | |
const type = types[Math.floor(Math.random() * types.length)]; | |
const shape = SHAPES[type]; | |
// For O piece (2x2), adjust starting position | |
const startCol = type === 'O' ? Math.floor(COLS / 2) - 1 : Math.floor(COLS / 2) - 2; | |
return { | |
shape, | |
type, | |
color: COLORS[type], | |
x: startCol, | |
y: 0 | |
}; | |
} | |
// Draw the current piece on the board | |
function drawPiece() { | |
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) { | |
const cellIndex = boardRow * COLS + boardCol; | |
const cell = gameBoard.children[cellIndex]; | |
cell.className = `cell cell-filled ${currentPiece.color}`; | |
} | |
} | |
} | |
} | |
} | |
// Draw the next piece in the preview | |
function drawNextPiece() { | |
if (!nextPiece) return; | |
// Clear the next piece display | |
for (let i = 0; i < nextPieceDisplay.children.length; i++) { | |
nextPieceDisplay.children[i].className = 'cell w-full h-full'; | |
} | |
// Center the piece in the 4x4 grid | |
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 row = 0; row < nextPiece.shape.length; row++) { | |
for (let col = 0; col < nextPiece.shape[row].length; col++) { | |
if (nextPiece.shape[row][col]) { | |
const displayRow = row + offsetY; | |
const displayCol = col + offsetX; | |
const cellIndex = displayRow * NEXT_PIECE_SIZE + displayCol; | |
const cell = nextPieceDisplay.children[cellIndex]; | |
cell.className = `cell cell-filled ${nextPiece.color}`; | |
} | |
} | |
} | |
} | |
// Clear the current piece from the board (before moving/rotating) | |
function clearPiece() { | |
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) { | |
const cellIndex = boardRow * COLS + boardCol; | |
const cell = gameBoard.children[cellIndex]; | |
cell.className = 'cell w-full h-full'; | |
} | |
} | |
} | |
} | |
} | |
// Check if the current piece can move to the specified position | |
function isValidMove(offsetX, offsetY, rotatedShape = null) { | |
const shape = rotatedShape || currentPiece.shape; | |
for (let row = 0; row < shape.length; row++) { | |
for (let col = 0; col < shape[row].length; col++) { | |
if (shape[row][col]) { | |
const newX = currentPiece.x + col + offsetX; | |
const newY = currentPiece.y + row + offsetY; | |
// Check boundaries | |
if (newX < 0 || newX >= COLS || newY >= ROWS) { | |
return false; | |
} | |
// Check collision with existing pieces (only check if moving down) | |
if (newY >= 0 && board[newY][newX] && offsetY >= 0) { | |
return false; | |
} | |
} | |
} | |
} | |
return true; | |
} | |
// Rotate the current piece | |
function rotatePiece() { | |
if (!currentPiece) return; | |
// Create a rotated version of the shape | |
const rotated = []; | |
for (let col = 0; col < currentPiece.shape[0].length; col++) { | |
const newRow = []; | |
for (let row = currentPiece.shape.length - 1; row >= 0; row--) { | |
newRow.push(currentPiece.shape[row][col]); | |
} | |
rotated.push(newRow); | |
} | |
// Check if the rotation is valid | |
if (isValidMove(0, 0, rotated)) { | |
clearPiece(); | |
currentPiece.shape = rotated; | |
drawPiece(); | |
} else { | |
// Try wall kicks (adjust position if rotation would cause collision) | |
const kicks = [-1, 1, -2, 2]; | |
for (const kick of kicks) { | |
if (isValidMove(kick, 0, rotated)) { | |
clearPiece(); | |
currentPiece.shape = rotated; | |
currentPiece.x += kick; | |
drawPiece(); | |
break; | |
} | |
} | |
} | |
} | |
// Move the current piece left | |
function moveLeft() { | |
if (!currentPiece || gameOver || isPaused) return; | |
if (isValidMove(-1, 0)) { | |
clearPiece(); | |
currentPiece.x--; | |
drawPiece(); | |
} | |
} | |
// Move the current piece right | |
function moveRight() { | |
if (!currentPiece || gameOver || isPaused) return; | |
if (isValidMove(1, 0)) { | |
clearPiece(); | |
currentPiece.x++; | |
drawPiece(); | |
} | |
} | |
// Move the current piece down (soft drop) | |
function moveDown() { | |
if (!currentPiece || gameOver || isPaused) return; | |
if (isValidMove(0, 1)) { | |
clearPiece(); | |
currentPiece.y++; | |
drawPiece(); | |
return true; | |
} else { | |
lockPiece(); | |
return false; | |
} | |
} | |
// Hard drop - move piece all the way down immediately | |
function hardDrop() { | |
if (!currentPiece || gameOver || isPaused) return; | |
while (moveDown()) { | |
// Keep moving down until it can't anymore | |
} | |
} | |
// Lock the current piece in place and check for completed lines | |
function lockPiece() { | |
if (!currentPiece) return; | |
// Add piece to the board | |
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; | |
} | |
} | |
} | |
} | |
// Check for completed lines | |
checkLines(); | |
// Get next piece | |
currentPiece = nextPiece; | |
nextPiece = createPiece(); | |
drawNextPiece(); | |
// Check if game over (new piece can't be placed) | |
if (!isValidMove(0, 0)) { | |
gameOver = true; | |
clearInterval(dropInterval); | |
gameOverScreen.classList.remove('hidden'); | |
} | |
} | |
// Check for completed lines and clear them | |
function checkLines() { | |
let linesCleared = 0; | |
for (let row = ROWS - 1; row >= 0; row--) { | |
if (board[row].every(cell => cell !== 0)) { | |
// Line is complete | |
linesCleared++; | |
// Flash animation for cleared lines | |
for (let col = 0; col < COLS; col++) { | |
const cellIndex = row * COLS + col; | |
const cell = gameBoard.children[cellIndex]; | |
cell.classList.add('flash-animation'); | |
} | |
// Remove the line after animation | |
setTimeout(() => { | |
// Shift all rows above down | |
for (let r = row; r > 0; r--) { | |
board[r] = [...board[r - 1]]; | |
} | |
board[0] = Array(COLS).fill(0); | |
// Redraw the entire board | |
redrawBoard(); | |
}, 300); | |
} | |
} | |
if (linesCleared > 0) { | |
// Update score based on lines cleared | |
const points = [0, 40, 100, 300, 1200]; // Points for 0, 1, 2, 3, 4 lines | |
score += points[linesCleared] * level; | |
lines += linesCleared; | |
// Every 10 lines increases the level | |
level = Math.floor(lines / 10) + 1; | |
// Increase speed with level (capped at 100ms) | |
dropSpeed = Math.max(100, 1000 - (level - 1) * 100); | |
// Update displays | |
updateDisplays(); | |
// Restart the drop interval with new speed | |
clearInterval(dropInterval); | |
dropInterval = setInterval(moveDown, dropSpeed); | |
} | |
} | |
// Redraw the entire board (after line clears) | |
function redrawBoard() { | |
for (let row = 0; row < ROWS; row++) { | |
for (let col = 0; col < COLS; col++) { | |
const cellIndex = row * COLS + col; | |
const cell = gameBoard.children[cellIndex]; | |
// Remove animation class if present | |
cell.classList.remove('flash-animation'); | |
if (board[row][col]) { | |
cell.className = `cell cell-filled ${board[row][col]}`; | |
} else { | |
cell.className = 'cell w-full h-full'; | |
} | |
} | |
} | |
} | |
// Update score, level, and lines displays | |
function updateDisplays() { | |
scoreDisplay.textContent = score; | |
levelDisplay.textContent = level; | |
linesDisplay.textContent = lines; | |
} | |
// Start the game | |
function startGame() { | |
// Reset game state | |
board = Array(ROWS).fill().map(() => Array(COLS).fill(0)); | |
score = 0; | |
level = 1; | |
lines = 0; | |
gameOver = false; | |
dropSpeed = 1000; | |
// Create pieces | |
currentPiece = createPiece(); | |
nextPiece = createPiece(); | |
// Update displays | |
updateDisplays(); | |
// Draw initial state | |
redrawBoard(); | |
drawPiece(); | |
drawNextPiece(); | |
// Hide game over screen | |
gameOverScreen.classList.add('hidden'); | |
// Start the drop interval | |
clearInterval(dropInterval); | |
dropInterval = setInterval(moveDown, dropSpeed); | |
} | |
// Toggle pause | |
function togglePause() { | |
if (gameOver) return; | |
isPaused = !isPaused; | |
if (isPaused) { | |
clearInterval(dropInterval); | |
pauseScreen.classList.remove('hidden'); | |
} else { | |
dropInterval = setInterval(moveDown, dropSpeed); | |
pauseScreen.classList.add('hidden'); | |
} | |
} | |
// Event listeners | |
document.addEventListener('keydown', (e) => { | |
if (gameOver && e.key === 'Enter') { | |
startGame(); | |
return; | |
} | |
if (isPaused && e.key !== 'p') return; | |
switch (e.key) { | |
case 'ArrowLeft': | |
moveLeft(); | |
break; | |
case 'ArrowRight': | |
moveRight(); | |
break; | |
case 'ArrowDown': | |
moveDown(); | |
break; | |
case 'ArrowUp': | |
rotatePiece(); | |
break; | |
case ' ': | |
hardDrop(); | |
break; | |
case 'p': | |
case 'P': | |
togglePause(); | |
break; | |
} | |
}); | |
// Touch controls for mobile | |
let touchStartX = 0; | |
let touchStartY = 0; | |
gameBoard.addEventListener('touchstart', (e) => { | |
if (gameOver || isPaused) return; | |
touchStartX = e.touches[0].clientX; | |
touchStartY = e.touches[0].clientY; | |
e.preventDefault(); | |
}, { passive: false }); | |
gameBoard.addEventListener('touchmove', (e) => { | |
if (gameOver || isPaused) 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) { | |
moveRight(); | |
touchStartX = touchX; | |
} else if (diffX < -30) { | |
moveLeft(); | |
touchStartX = touchX; | |
} | |
} | |
// Vertical swipe down (soft drop) | |
if (diffY > 30) { | |
moveDown(); | |
touchStartY = touchY; | |
} | |
e.preventDefault(); | |
}, { passive: false }); | |
gameBoard.addEventListener('touchend', (e) => { | |
if (gameOver || isPaused) return; | |
const touchEndX = e.changedTouches[0].clientX; | |
const touchEndY = e.changedTouches[0].clientY; | |
const diffX = touchEndX - touchStartX; | |
const diffY = touchEndY - touchStartY; | |
// Tap (rotate) | |
if (Math.abs(diffX) < 10 && Math.abs(diffY) < 10) { | |
rotatePiece(); | |
} | |
// Quick swipe up (hard drop) | |
if (diffY < -50) { | |
hardDrop(); | |
} | |
}); | |
// Button event listeners | |
restartBtn.addEventListener('click', startGame); | |
pauseBtn.addEventListener('click', togglePause); | |
// Initialize the game | |
initBoard(); | |
initNextPieceDisplay(); | |
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=bozhong/tetris" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |