Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Classic Tetris</title> | |
<style> | |
* { | |
margin: 0; | |
padding: 0; | |
box-sizing: border-box; | |
font-family: 'Arial', sans-serif; | |
} | |
body { | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
min-height: 100vh; | |
background: linear-gradient(135deg, #1e1e2f, #2d2d44); | |
color: #fff; | |
overflow: hidden; | |
} | |
.game-container { | |
display: flex; | |
gap: 30px; | |
align-items: flex-start; | |
} | |
#game-board { | |
border: 4px solid #4a4a6b; | |
border-radius: 5px; | |
display: grid; | |
grid-template-rows: repeat(20, 1fr); | |
grid-template-columns: repeat(10, 1fr); | |
gap: 1px; | |
background-color: #2a2a3a; | |
box-shadow: 0 0 20px rgba(0, 0, 0, 0.3); | |
width: 300px; | |
height: 600px; | |
} | |
.cell { | |
border: 1px solid rgba(255, 255, 255, 0.05); | |
background-color: #2a2a3a; | |
} | |
.controls { | |
display: flex; | |
flex-direction: column; | |
gap: 30px; | |
} | |
.info-panel { | |
background-color: #2a2a3a; | |
border: 4px solid #4a4a6b; | |
border-radius: 5px; | |
padding: 20px; | |
width: 180px; | |
box-shadow: 0 0 15px rgba(0, 0, 0, 0.2); | |
} | |
.next-piece-container { | |
width: 120px; | |
height: 120px; | |
display: grid; | |
grid-template-rows: repeat(4, 1fr); | |
grid-template-columns: repeat(4, 1fr); | |
gap: 2px; | |
margin-top: 10px; | |
margin-bottom: 20px; | |
} | |
.next-cell { | |
background-color: #3a3a4a; | |
border-radius: 2px; | |
} | |
h2 { | |
font-size: 18px; | |
margin-bottom: 10px; | |
color: #ddd; | |
} | |
.score-display { | |
font-size: 24px; | |
margin-bottom: 15px; | |
color: #fff; | |
} | |
.level-display { | |
font-size: 18px; | |
margin-bottom: 15px; | |
color: #fff; | |
} | |
.controls-info { | |
background-color: #2a2a3a; | |
border: 4px solid #4a4a6b; | |
border-radius: 5px; | |
padding: 20px; | |
width: 180px; | |
box-shadow: 0 0 15px rgba(0, 0, 0, 0.2); | |
} | |
.control-item { | |
display: flex; | |
justify-content: space-between; | |
margin-bottom: 10px; | |
font-size: 14px; | |
color: #ccc; | |
} | |
.key { | |
background-color: #4a4a6b; | |
color: #fff; | |
padding: 3px 8px; | |
border-radius: 4px; | |
font-family: monospace; | |
} | |
.game-over { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background-color: rgba(0, 0, 0, 0.8); | |
display: flex; | |
flex-direction: column; | |
justify-content: center; | |
align-items: center; | |
z-index: 10; | |
opacity: 0; | |
pointer-events: none; | |
transition: opacity 0.3s; | |
} | |
.game-over.show { | |
opacity: 1; | |
pointer-events: all; | |
} | |
.game-over h1 { | |
font-size: 42px; | |
margin-bottom: 20px; | |
color: #ff5555; | |
} | |
.final-score { | |
font-size: 24px; | |
margin-bottom: 30px; | |
} | |
.restart-btn { | |
background-color: #4CAF50; | |
color: white; | |
border: none; | |
padding: 12px 24px; | |
font-size: 16px; | |
cursor: pointer; | |
border-radius: 4px; | |
transition: background-color 0.3s; | |
} | |
.restart-btn:hover { | |
background-color: #45a049; | |
} | |
.tetromino-i { background-color: #00f0f0; } | |
.tetromino-j { background-color: #0000f0; } | |
.tetromino-l { background-color: #f0a000; } | |
.tetromino-o { background-color: #f0f000; } | |
.tetromino-s { background-color: #00f000; } | |
.tetromino-t { background-color: #a000f0; } | |
.tetromino-z { background-color: #f00000; } | |
.tetromino-i.ghost { background-color: rgba(0, 240, 240, 0.2); } | |
.tetromino-j.ghost { background-color: rgba(0, 0, 240, 0.2); } | |
.tetromino-l.ghost { background-color: rgba(240, 160, 0, 0.2); } | |
.tetromino-o.ghost { background-color: rgba(240, 240, 0, 0.2); } | |
.tetromino-s.ghost { background-color: rgba(0, 240, 0, 0.2); } | |
.tetromino-t.ghost { background-color: rgba(160, 0, 240, 0.2); } | |
.tetromino-z.ghost { background-color: rgba(240, 0, 0, 0.2); } | |
@media (max-width: 768px) { | |
.game-container { | |
flex-direction: column; | |
align-items: center; | |
} | |
.controls { | |
flex-direction: row; | |
margin-top: 20px; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="game-container"> | |
<div id="game-board"></div> | |
<div class="controls"> | |
<div class="info-panel"> | |
<h2>Next Piece</h2> | |
<div class="next-piece-container" id="next-piece"></div> | |
<h2>Score</h2> | |
<div class="score-display" id="score">0</div> | |
<h2>Level</h2> | |
<div class="level-display" id="level">1</div> | |
</div> | |
<div class="controls-info"> | |
<h2>Controls</h2> | |
<div class="control-item"> | |
<span>Move Left</span> | |
<span class="key">←</span> | |
</div> | |
<div class="control-item"> | |
<span>Move Right</span> | |
<span class="key">→</span> | |
</div> | |
<div class="control-item"> | |
<span>Rotate</span> | |
<span class="key">↑</span> | |
</div> | |
<div class="control-item"> | |
<span>Soft Drop</span> | |
<span class="key">↓</span> | |
</div> | |
<div class="control-item"> | |
<span>Hard Drop</span> | |
<span class="key">Space</span> | |
</div> | |
<div class="control-item"> | |
<span>Pause</span> | |
<span class="key">P</span> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="game-over" id="game-over"> | |
<h1>GAME OVER</h1> | |
<div class="final-score">Score: <span id="final-score">0</span></div> | |
<button class="restart-btn" id="restart-btn">Play Again</button> | |
</div> | |
<script> | |
document.addEventListener('DOMContentLoaded', () => { | |
// Game constants | |
const COLS = 10; | |
const ROWS = 20; | |
const BLOCK_SIZE = 30; | |
const NEXT_PIECE_COLS = 4; | |
const NEXT_PIECE_ROWS = 4; | |
// DOM elements | |
const gameBoard = document.getElementById('game-board'); | |
const nextPieceContainer = document.getElementById('next-piece'); | |
const scoreDisplay = document.getElementById('score'); | |
const levelDisplay = document.getElementById('level'); | |
const gameOverScreen = document.getElementById('game-over'); | |
const finalScoreDisplay = document.getElementById('final-score'); | |
const restartBtn = document.getElementById('restart-btn'); | |
// Game state | |
let board = Array(ROWS).fill().map(() => Array(COLS).fill(0)); | |
let currentPiece = null; | |
let nextPiece = null; | |
let currentPosition = { x: 0, y: 0 }; | |
let score = 0; | |
let level = 1; | |
let linesCleared = 0; | |
let gameOver = false; | |
let isPaused = false; | |
let dropStart; | |
let gameInterval; | |
// 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: 'tetromino-i', | |
J: 'tetromino-j', | |
L: 'tetromino-l', | |
O: 'tetromino-o', | |
S: 'tetromino-s', | |
T: 'tetromino-t', | |
Z: 'tetromino-z' | |
}; | |
// Initialize game board | |
function initBoard() { | |
gameBoard.innerHTML = ''; | |
for (let row = 0; row < ROWS; row++) { | |
for (let col = 0; col < COLS; col++) { | |
const cell = document.createElement('div'); | |
cell.className = 'cell'; | |
cell.id = `${row}-${col}`; | |
gameBoard.appendChild(cell); | |
} | |
} | |
} | |
// Initialize next piece display | |
function initNextPieceDisplay() { | |
nextPieceContainer.innerHTML = ''; | |
for (let row = 0; row < NEXT_PIECE_ROWS; row++) { | |
for (let col = 0; col < NEXT_PIECE_COLS; col++) { | |
const cell = document.createElement('div'); | |
cell.className = 'next-cell'; | |
cell.id = `next-${row}-${col}`; | |
nextPieceContainer.appendChild(cell); | |
} | |
} | |
} | |
// Get random tetromino | |
function getRandomPiece() { | |
const keys = Object.keys(SHAPES); | |
const randomKey = keys[Math.floor(Math.random() * keys.length)]; | |
return { | |
shape: SHAPES[randomKey], | |
color: COLORS[randomKey], | |
type: randomKey | |
}; | |
} | |
// Draw the current piece on the board | |
function drawPiece(x, y, piece, isGhost = false) { | |
piece.shape.forEach((row, rowIndex) => { | |
row.forEach((value, colIndex) => { | |
if (value) { | |
const boardRow = y + rowIndex; | |
const boardCol = x + colIndex; | |
if (boardRow >= 0 && boardRow < ROWS && boardCol >= 0 && boardCol < COLS) { | |
const cell = document.getElementById(`${boardRow}-${boardCol}`); | |
if (cell) { | |
cell.classList.add(piece.color); | |
if (isGhost) { | |
cell.classList.add('ghost'); | |
} | |
} | |
} | |
} | |
}); | |
}); | |
} | |
// Clear the current piece from the board | |
function clearPiece(x, y, piece) { | |
piece.shape.forEach((row, rowIndex) => { | |
row.forEach((value, colIndex) => { | |
if (value) { | |
const boardRow = y + rowIndex; | |
const boardCol = x + colIndex; | |
if (boardRow >= 0 && boardRow < ROWS && boardCol >= 0 && boardCol < COLS) { | |
const cell = document.getElementById(`${boardRow}-${boardCol}`); | |
if (cell) { | |
cell.className = 'cell'; | |
} | |
} | |
} | |
}); | |
}); | |
} | |
// Draw the ghost piece (projection of where the piece will land) | |
function drawGhostPiece() { | |
let ghostY = currentPosition.y; | |
while (!collision(currentPosition.x, ghostY + 1, currentPiece)) { | |
ghostY++; | |
} | |
if (ghostY !== currentPosition.y) { | |
// First clear any existing ghost pieces | |
clearGhost(); | |
// Draw the new ghost piece | |
drawPiece(currentPosition.x, ghostY, currentPiece, true); | |
} | |
} | |
// Clear all ghost pieces from the board | |
function clearGhost() { | |
const ghostCells = document.querySelectorAll('.ghost'); | |
ghostCells.forEach(cell => { | |
const className = cell.className; | |
const colorClass = className.split(' ').find(cls => cls.startsWith('tetromino-')); | |
cell.className = 'cell'; | |
if (colorClass) { | |
cell.classList.remove(colorClass, 'ghost'); | |
} | |
}); | |
} | |
// Check for collision | |
function collision(x, y, piece) { | |
for (let row = 0; row < piece.shape.length; row++) { | |
for (let col = 0; col < piece.shape[row].length; col++) { | |
if (!piece.shape[row][col]) continue; | |
const boardX = x + col; | |
const boardY = y + row; | |
if ( | |
boardX < 0 || | |
boardX >= COLS || | |
boardY >= ROWS || | |
(boardY >= 0 && board[boardY][boardX]) | |
) { | |
return true; | |
} | |
} | |
} | |
return false; | |
} | |
// Rotate piece | |
function rotate(piece) { | |
const N = piece.shape.length; | |
const rotated = Array(N).fill().map(() => Array(N).fill(0)); | |
// Transpose the matrix | |
for (let i = 0; i < N; i++) { | |
for (let j = 0; j < N; j++) { | |
rotated[i][j] = piece.shape[N - j - 1][i]; | |
} | |
} | |
// Special case for I piece to make it rotate properly | |
if (piece.type === 'I') { | |
if (currentPosition.x < 0) { | |
currentPosition.x = 0; | |
} else if (currentPosition.x > COLS - 4) { | |
currentPosition.x = COLS - 4; | |
} | |
} | |
return { | |
...piece, | |
shape: rotated | |
}; | |
} | |
// Lock piece in place | |
function lockPiece() { | |
currentPiece.shape.forEach((row, rowIndex) => { | |
row.forEach((value, colIndex) => { | |
if (value) { | |
const boardRow = currentPosition.y + rowIndex; | |
const boardCol = currentPosition.x + colIndex; | |
if (boardRow >= 0) { | |
board[boardRow][boardCol] = currentPiece.color; | |
} | |
} | |
}); | |
}); | |
// Check for completed lines | |
checkLines(); | |
// Check for game over | |
if (currentPosition.y <= 0) { | |
gameOver = true; | |
showGameOver(); | |
return; | |
} | |
// Get next piece | |
currentPiece = nextPiece; | |
nextPiece = getRandomPiece(); | |
currentPosition = { x: Math.floor(COLS / 2) - Math.floor(currentPiece.shape[0].length / 2), y: 0 }; | |
// Update next piece display | |
updateNextPieceDisplay(); | |
// Draw the new piece and ghost | |
drawPiece(currentPosition.x, currentPosition.y, currentPiece); | |
drawGhostPiece(); | |
// Reset drop interval | |
dropStart = Date.now(); | |
} | |
// Check for completed lines | |
function checkLines() { | |
let linesToClear = 0; | |
for (let row = ROWS - 1; row >= 0; row--) { | |
if (board[row].every(cell => cell)) { | |
linesToClear++; | |
// Shift all rows above down | |
for (let y = row; y > 0; y--) { | |
board[y] = [...board[y - 1]]; | |
} | |
board[0] = Array(COLS).fill(0); | |
// Since we modified the current row, need to check it again | |
row++; | |
} | |
} | |
if (linesToClear > 0) { | |
// Update score | |
updateScore(linesToClear); | |
// Redraw the board | |
drawBoard(); | |
} | |
} | |
// Update score | |
function updateScore(lines) { | |
const points = [0, 40, 100, 300, 1200]; // Points for 0, 1, 2, 3, 4 lines | |
score += points[lines] * level; | |
linesCleared += lines; | |
// Every 10 lines increases the level | |
level = Math.floor(linesCleared / 10) + 1; | |
// Update displays | |
scoreDisplay.textContent = score; | |
levelDisplay.textContent = level; | |
} | |
// Draw the entire board | |
function drawBoard() { | |
for (let row = 0; row < ROWS; row++) { | |
for (let col = 0; col < COLS; col++) { | |
const cell = document.getElementById(`${row}-${col}`); | |
cell.className = 'cell'; | |
if (board[row][col]) { | |
cell.classList.add(board[row][col]); | |
} | |
} | |
} | |
} | |
// Update next piece display | |
function updateNextPieceDisplay() { | |
// Clear the next piece display | |
for (let row = 0; row < NEXT_PIECE_ROWS; row++) { | |
for (let col = 0; col < NEXT_PIECE_COLS; col++) { | |
const cell = document.getElementById(`next-${row}-${col}`); | |
cell.className = 'next-cell'; | |
} | |
} | |
// Draw the next piece in the center | |
const startRow = Math.floor((NEXT_PIECE_ROWS - nextPiece.shape.length) / 2); | |
const startCol = Math.floor((NEXT_PIECE_COLS - nextPiece.shape[0].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 cell = document.getElementById(`next-${startRow + row}-${startCol + col}`); | |
if (cell) { | |
cell.classList.add(nextPiece.color); | |
} | |
} | |
} | |
} | |
} | |
// Move piece down | |
function moveDown() { | |
if (gameOver || isPaused) return; | |
clearPiece(currentPosition.x, currentPosition.y, currentPiece); | |
clearGhost(); | |
if (!collision(currentPosition.x, currentPosition.y + 1, currentPiece)) { | |
currentPosition.y++; | |
drawPiece(currentPosition.x, currentPosition.y, currentPiece); | |
drawGhostPiece(); | |
return true; | |
} else { | |
drawPiece(currentPosition.x, currentPosition.y, currentPiece); | |
drawGhostPiece(); | |
lockPiece(); | |
return false; | |
} | |
} | |
// Hard drop | |
function hardDrop() { | |
if (gameOver || isPaused) return; | |
clearPiece(currentPosition.x, currentPosition.y, currentPiece); | |
clearGhost(); | |
while (!collision(currentPosition.x, currentPosition.y + 1, currentPiece)) { | |
currentPosition.y++; | |
} | |
drawPiece(currentPosition.x, currentPosition.y, currentPiece); | |
lockPiece(); | |
dropStart = Date.now(); | |
} | |
// Move piece left or right | |
function movePiece(direction) { | |
if (gameOver || isPaused) return; | |
clearPiece(currentPosition.x, currentPosition.y, currentPiece); | |
clearGhost(); | |
const newX = currentPosition.x + direction; | |
if (!collision(newX, currentPosition.y, currentPiece)) { | |
currentPosition.x = newX; | |
} | |
drawPiece(currentPosition.x, currentPosition.y, currentPiece); | |
drawGhostPiece(); | |
} | |
// Rotate current piece | |
function rotatePiece() { | |
if (gameOver || isPaused) return; | |
clearPiece(currentPosition.x, currentPosition.y, currentPiece); | |
clearGhost(); | |
const rotated = rotate(currentPiece); | |
// Wall kick - try moving left/right if rotation would cause a collision | |
if (!collision(currentPosition.x, currentPosition.y, rotated)) { | |
currentPiece = rotated; | |
} else if (!collision(currentPosition.x - 1, currentPosition.y, rotated)) { | |
currentPiece = rotated; | |
currentPosition.x--; | |
} else if (!collision(currentPosition.x + 1, currentPosition.y, rotated)) { | |
currentPiece = rotated; | |
currentPosition.x++; | |
} | |
drawPiece(currentPosition.x, currentPosition.y, currentPiece); | |
drawGhostPiece(); | |
} | |
// Game loop | |
function gameLoop() { | |
const now = Date.now(); | |
const delta = now - dropStart; | |
const dropInterval = Math.max(1000 - (level - 1) * 100, 100); // Speeds up as level increases | |
if (delta > dropInterval) { | |
moveDown(); | |
dropStart = now; | |
} | |
if (!gameOver) { | |
requestAnimationFrame(gameLoop); | |
} | |
} | |
// Show game over screen | |
function showGameOver() { | |
gameOverScreen.classList.add('show'); | |
finalScoreDisplay.textContent = score; | |
clearInterval(gameInterval); | |
} | |
// Reset game | |
function resetGame() { | |
board = Array(ROWS).fill().map(() => Array(COLS).fill(0)); | |
score = 0; | |
level = 1; | |
linesCleared = 0; | |
gameOver = false; | |
isPaused = false; | |
scoreDisplay.textContent = score; | |
levelDisplay.textContent = level; | |
// Initialize pieces | |
currentPiece = getRandomPiece(); | |
nextPiece = getRandomPiece(); | |
currentPosition = { x: Math.floor(COLS / 2) - Math.floor(currentPiece.shape[0].length / 2), y: 0 }; | |
// Clear the board | |
drawBoard(); | |
updateNextPieceDisplay(); | |
// Draw current piece and ghost | |
drawPiece(currentPosition.x, currentPosition.y, currentPiece); | |
drawGhostPiece(); | |
// Hide game over screen | |
gameOverScreen.classList.remove('show'); | |
// Reset drop time and start game loop | |
dropStart = Date.now(); | |
gameLoop(); | |
} | |
// Pause/unpause the game | |
function togglePause() { | |
if (gameOver) return; | |
isPaused = !isPaused; | |
if (!isPaused) { | |
dropStart = Date.now(); // Reset drop timer | |
gameLoop(); | |
} | |
} | |
// Initialize the game | |
function init() { | |
initBoard(); | |
initNextPieceDisplay(); | |
// Initialize first pieces | |
currentPiece = getRandomPiece(); | |
nextPiece = getRandomPiece(); | |
currentPosition = { x: Math.floor(COLS / 2) - Math.floor(currentPiece.shape[0].length / 2), y: 0 }; | |
updateNextPieceDisplay(); | |
drawPiece(currentPosition.x, currentPosition.y, currentPiece); | |
drawGhostPiece(); | |
// Keyboard controls | |
document.addEventListener('keydown', event => { | |
if (event.key === 'ArrowLeft') { | |
movePiece(-1); | |
} else if (event.key === 'ArrowRight') { | |
movePiece(1); | |
} else if (event.key === 'ArrowDown') { | |
moveDown(); | |
} else if (event.key === 'ArrowUp') { | |
rotatePiece(); | |
} else if (event.key === ' ') { | |
hardDrop(); | |
} else if (event.key === 'p' || event.key === 'P') { | |
togglePause(); | |
} | |
// Prevent default for arrow keys and space to avoid scrolling | |
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', ' '].includes(event.key)) { | |
event.preventDefault(); | |
} | |
}); | |
restartBtn.addEventListener('click', resetGame); | |
// Start game loop | |
dropStart = Date.now(); | |
gameLoop(); | |
} | |
init(); | |
}); | |
</script> | |
</body> | |
</html> |