Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Modern Tetris</title> | |
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet"> | |
<style> | |
:root { | |
--primary-color: #6c5ce7; | |
--secondary-color: #a29bfe; | |
--accent-color: #fd79a8; | |
--dark-color: #2d3436; | |
--light-color: #f9f9f9; | |
--shadow: 0 10px 30px rgba(0, 0, 0, 0.1); | |
--border-radius: 10px; | |
} | |
* { | |
margin: 0; | |
padding: 0; | |
box-sizing: border-box; | |
} | |
body { | |
font-family: 'Poppins', sans-serif; | |
background: linear-gradient(135deg, #dfe6e9, #b2bec3); | |
min-height: 100vh; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
padding: 20px; | |
color: var(--dark-color); | |
} | |
.container { | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
max-width: 1000px; | |
width: 100%; | |
background-color: rgba(255, 255, 255, 0.85); | |
backdrop-filter: blur(10px); | |
border-radius: var(--border-radius); | |
box-shadow: var(--shadow); | |
padding: 20px; | |
} | |
@media (min-width: 768px) { | |
.container { | |
flex-direction: row; | |
justify-content: center; | |
gap: 40px; | |
padding: 40px; | |
} | |
} | |
h1 { | |
font-size: 2.5rem; | |
margin-bottom: 20px; | |
color: var(--primary-color); | |
text-align: center; | |
width: 100%; | |
} | |
.game-container { | |
position: relative; | |
} | |
.tetris-board { | |
border: 2px solid var(--primary-color); | |
border-radius: var(--border-radius); | |
background-color: #fff; | |
box-shadow: var(--shadow); | |
display: grid; | |
grid-template-rows: repeat(20, 1fr); | |
grid-template-columns: repeat(10, 1fr); | |
gap: 1px; | |
width: 300px; | |
height: 600px; | |
overflow: hidden; | |
} | |
.cell { | |
background-color: #f5f5f5; | |
border-radius: 2px; | |
} | |
.side-panel { | |
display: flex; | |
flex-direction: column; | |
gap: 20px; | |
} | |
.next-piece-container, .score-container, .level-container, .controls-container { | |
background-color: white; | |
border-radius: var(--border-radius); | |
padding: 15px; | |
box-shadow: var(--shadow); | |
width: 200px; | |
} | |
.next-piece-container h2, .score-container h2, .level-container h2, .controls-container h2 { | |
color: var(--primary-color); | |
font-size: 1.2rem; | |
margin-bottom: 10px; | |
text-align: center; | |
} | |
.next-piece-preview { | |
height: 100px; | |
display: grid; | |
grid-template-rows: repeat(4, 1fr); | |
grid-template-columns: repeat(4, 1fr); | |
gap: 1px; | |
margin: 0 auto; | |
} | |
.control-btn { | |
background-color: var(--primary-color); | |
color: white; | |
border: none; | |
border-radius: var(--border-radius); | |
padding: 10px 15px; | |
font-family: 'Poppins', sans-serif; | |
font-weight: 600; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
display: block; | |
width: 100%; | |
margin-bottom: 10px; | |
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.1); | |
} | |
.control-btn:hover { | |
background-color: var(--secondary-color); | |
transform: translateY(-2px); | |
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.15); | |
} | |
.control-btn:active { | |
transform: translateY(0); | |
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.1); | |
} | |
.score, .level { | |
font-size: 1.5rem; | |
font-weight: 600; | |
color: var(--accent-color); | |
text-align: center; | |
} | |
.game-over-modal { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background-color: rgba(0, 0, 0, 0.85); | |
display: flex; | |
flex-direction: column; | |
justify-content: center; | |
align-items: center; | |
border-radius: var(--border-radius); | |
opacity: 0; | |
pointer-events: none; | |
transition: opacity 0.3s ease; | |
} | |
.game-over-modal.active { | |
opacity: 1; | |
pointer-events: all; | |
} | |
.game-over-modal h2 { | |
color: white; | |
font-size: 2rem; | |
margin-bottom: 20px; | |
} | |
.game-over-modal p { | |
color: white; | |
font-size: 1.2rem; | |
margin-bottom: 30px; | |
} | |
.keyboard-controls { | |
margin-top: 15px; | |
font-size: 0.9rem; | |
color: #666; | |
} | |
.keyboard-controls p { | |
margin: 5px 0; | |
} | |
.tetromino { | |
border: 1px solid rgba(0, 0, 0, 0.2); | |
border-radius: 2px; | |
} | |
.I { | |
background-color: #00cec9; | |
} | |
.J { | |
background-color: #0984e3; | |
} | |
.L { | |
background-color: #e17055; | |
} | |
.O { | |
background-color: #fdcb6e; | |
} | |
.S { | |
background-color: #00b894; | |
} | |
.T { | |
background-color: #6c5ce7; | |
} | |
.Z { | |
background-color: #d63031; | |
} | |
@keyframes flash { | |
0%, 100% { | |
filter: brightness(1); | |
} | |
50% { | |
filter: brightness(1.5); | |
} | |
} | |
.flash-row { | |
animation: flash 0.2s 3; | |
} | |
@media (max-width: 767px) { | |
.tetris-board { | |
width: 280px; | |
height: 560px; | |
} | |
.side-panel { | |
margin-top: 20px; | |
flex-direction: row; | |
flex-wrap: wrap; | |
justify-content: center; | |
gap: 10px; | |
} | |
.next-piece-container, .score-container, .level-container, .controls-container { | |
width: calc(50% - 10px); | |
min-width: 130px; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<div class="game-container"> | |
<h1>Modern Tetris</h1> | |
<div class="tetris-board" id="tetris-board"></div> | |
<div class="game-over-modal" id="game-over-modal"> | |
<h2>Game Over</h2> | |
<p>Your score: <span id="final-score">0</span></p> | |
<button class="control-btn" id="restart-btn">Play Again</button> | |
</div> | |
</div> | |
<div class="side-panel"> | |
<div class="next-piece-container"> | |
<h2>Next Piece</h2> | |
<div class="next-piece-preview" id="next-piece-preview"></div> | |
</div> | |
<div class="score-container"> | |
<h2>Score</h2> | |
<div class="score" id="score">0</div> | |
</div> | |
<div class="level-container"> | |
<h2>Level</h2> | |
<div class="level" id="level">1</div> | |
</div> | |
<div class="controls-container"> | |
<h2>Controls</h2> | |
<button class="control-btn" id="start-btn">Start / Pause</button> | |
<div class="keyboard-controls"> | |
<p>← → : Move left/right</p> | |
<p>↓ : Soft drop</p> | |
<p>↑ : Rotate</p> | |
<p>Space : Hard drop</p> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script> | |
document.addEventListener('DOMContentLoaded', () => { | |
// Game constants | |
const BOARD_WIDTH = 10; | |
const BOARD_HEIGHT = 20; | |
const PREVIEW_SIZE = 4; | |
// Game elements | |
const tetrisBoard = document.getElementById('tetris-board'); | |
const nextPiecePreview = document.getElementById('next-piece-preview'); | |
const scoreElement = document.getElementById('score'); | |
const levelElement = document.getElementById('level'); | |
const startBtn = document.getElementById('start-btn'); | |
const gameOverModal = document.getElementById('game-over-modal'); | |
const finalScoreElement = document.getElementById('final-score'); | |
const restartBtn = document.getElementById('restart-btn'); | |
// Game state | |
let gameBoard = Array(BOARD_HEIGHT).fill().map(() => Array(BOARD_WIDTH).fill(0)); | |
let score = 0; | |
let level = 1; | |
let linesCleared = 0; | |
let currentPiece = null; | |
let nextPiece = null; | |
let gameInterval = null; | |
let isPaused = false; | |
let isGameOver = false; | |
// Tetrominoes | |
const tetrominoes = { | |
I: { | |
shape: [ | |
[0, 0, 0, 0], | |
[1, 1, 1, 1], | |
[0, 0, 0, 0], | |
[0, 0, 0, 0] | |
], | |
color: 'I' | |
}, | |
J: { | |
shape: [ | |
[1, 0, 0], | |
[1, 1, 1], | |
[0, 0, 0] | |
], | |
color: 'J' | |
}, | |
L: { | |
shape: [ | |
[0, 0, 1], | |
[1, 1, 1], | |
[0, 0, 0] | |
], | |
color: 'L' | |
}, | |
O: { | |
shape: [ | |
[1, 1], | |
[1, 1] | |
], | |
color: 'O' | |
}, | |
S: { | |
shape: [ | |
[0, 1, 1], | |
[1, 1, 0], | |
[0, 0, 0] | |
], | |
color: 'S' | |
}, | |
T: { | |
shape: [ | |
[0, 1, 0], | |
[1, 1, 1], | |
[0, 0, 0] | |
], | |
color: 'T' | |
}, | |
Z: { | |
shape: [ | |
[1, 1, 0], | |
[0, 1, 1], | |
[0, 0, 0] | |
], | |
color: 'Z' | |
} | |
}; | |
// Initialize game board | |
function initBoard() { | |
tetrisBoard.innerHTML = ''; | |
nextPiecePreview.innerHTML = ''; | |
// Create game board cells | |
for (let row = 0; row < BOARD_HEIGHT; row++) { | |
for (let col = 0; col < BOARD_WIDTH; col++) { | |
const cell = document.createElement('div'); | |
cell.classList.add('cell'); | |
cell.dataset.row = row; | |
cell.dataset.col = col; | |
tetrisBoard.appendChild(cell); | |
} | |
} | |
// Create next piece preview cells | |
for (let row = 0; row < PREVIEW_SIZE; row++) { | |
for (let col = 0; col < PREVIEW_SIZE; col++) { | |
const cell = document.createElement('div'); | |
cell.classList.add('cell'); | |
cell.dataset.row = row; | |
cell.dataset.col = col; | |
nextPiecePreview.appendChild(cell); | |
} | |
} | |
} | |
// Generate random tetromino | |
function getRandomTetromino() { | |
const tetrominoKeys = Object.keys(tetrominoes); | |
const randomKey = tetrominoKeys[Math.floor(Math.random() * tetrominoKeys.length)]; | |
const tetromino = { ...tetrominoes[randomKey] }; | |
return { | |
...tetromino, | |
row: 0, | |
col: Math.floor((BOARD_WIDTH - tetromino.shape[0].length) / 2) | |
}; | |
} | |
// Draw tetromino on the board | |
function drawTetromino() { | |
clearBoard(); | |
// Draw settled pieces | |
for (let row = 0; row < BOARD_HEIGHT; row++) { | |
for (let col = 0; col < BOARD_WIDTH; col++) { | |
if (gameBoard[row][col]) { | |
const cell = document.querySelector(`.cell[data-row="${row}"][data-col="${col}"]`); | |
if (cell) { | |
cell.classList.add('tetromino', gameBoard[row][col]); | |
} | |
} | |
} | |
} | |
// Draw current piece | |
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.row + row; | |
const boardCol = currentPiece.col + col; | |
if (boardRow >= 0 && boardRow < BOARD_HEIGHT && boardCol >= 0 && boardCol < BOARD_WIDTH) { | |
const cell = document.querySelector(`.cell[data-row="${boardRow}"][data-col="${boardCol}"]`); | |
if (cell) { | |
cell.classList.add('tetromino', currentPiece.color); | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
// Draw next piece in preview | |
function drawNextPiece() { | |
// Clear the preview | |
const previewCells = nextPiecePreview.querySelectorAll('.cell'); | |
previewCells.forEach(cell => { | |
cell.className = 'cell'; | |
}); | |
if (!nextPiece) return; | |
// Center the piece in the preview | |
const offsetRow = Math.floor((PREVIEW_SIZE - nextPiece.shape.length) / 2); | |
const offsetCol = Math.floor((PREVIEW_SIZE - 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 previewRow = offsetRow + row; | |
const previewCol = offsetCol + col; | |
if (previewRow >= 0 && previewRow < PREVIEW_SIZE && previewCol >= 0 && previewCol < PREVIEW_SIZE) { | |
const cell = nextPiecePreview.querySelector(`.cell[data-row="${previewRow}"][data-col="${previewCol}"]`); | |
if (cell) { | |
cell.classList.add('tetromino', nextPiece.color); | |
} | |
} | |
} | |
} | |
} | |
} | |
// Clear the visual board (not the data) | |
function clearBoard() { | |
const cells = tetrisBoard.querySelectorAll('.cell'); | |
cells.forEach(cell => { | |
cell.className = 'cell'; | |
}); | |
} | |
// Check if a move is valid | |
function isValidMove(piece, rowOffset = 0, colOffset = 0) { | |
for (let row = 0; row < piece.shape.length; row++) { | |
for (let col = 0; col < piece.shape[row].length; col++) { | |
if (piece.shape[row][col]) { | |
const newRow = piece.row + row + rowOffset; | |
const newCol = piece.col + col + colOffset; | |
// Check if out of bounds or colliding with settled pieces | |
if ( | |
newRow < 0 || | |
newRow >= BOARD_HEIGHT || | |
newCol < 0 || | |
newCol >= BOARD_WIDTH || | |
(newRow >= 0 && gameBoard[newRow][newCol]) | |
) { | |
return false; | |
} | |
} | |
} | |
} | |
return true; | |
} | |
// Check and clear completed lines | |
function checkLines() { | |
let linesComplete = 0; | |
let flashRows = []; | |
for (let row = BOARD_HEIGHT - 1; row >= 0; row--) { | |
if (gameBoard[row].every(cell => cell !== 0)) { | |
linesComplete++; | |
flashRows.push(row); | |
} | |
} | |
if (linesComplete > 0) { | |
// Flash the lines that will be cleared | |
flashRows.forEach(row => { | |
for (let col = 0; col < BOARD_WIDTH; col++) { | |
const cell = document.querySelector(`.cell[data-row="${row}"][data-col="${col}"]`); | |
if (cell) { | |
cell.classList.add('flash-row'); | |
} | |
} | |
}); | |
// Wait for animation to complete then clear lines | |
setTimeout(() => { | |
for (let row of flashRows) { | |
// Remove the row | |
gameBoard.splice(row, 1); | |
// Add a new empty row at the top | |
gameBoard.unshift(Array(BOARD_WIDTH).fill(0)); | |
} | |
// Update score and level | |
updateScore(linesComplete); | |
linesCleared += linesComplete; | |
if (linesCleared >= level * 10) { | |
level++; | |
levelElement.textContent = level; | |
updateGameSpeed(); | |
} | |
drawTetromino(); | |
}, 600); | |
} | |
} | |
// Calculate score based on lines cleared | |
function updateScore(lines) { | |
const linePoints = [0, 40, 100, 300, 1200]; // Points for 0, 1, 2, 3, 4 lines | |
const points = linePoints[lines] * level; | |
score += points; | |
scoreElement.textContent = score; | |
} | |
// Update game speed based on level | |
function updateGameSpeed() { | |
if (gameInterval) { | |
clearInterval(gameInterval); | |
} | |
const speed = Math.max(100, 1000 - (level - 1) * 100); // Decrease interval as level increases | |
gameInterval = setInterval(moveDown, speed); | |
} | |
// Move the current piece down | |
function moveDown() { | |
if (isPaused || isGameOver || !currentPiece) return; | |
if (isValidMove(currentPiece, 1, 0)) { | |
currentPiece.row++; | |
drawTetromino(); | |
} else { | |
// Lock piece in place | |
lockPiece(); | |
// Check for completed lines | |
checkLines(); | |
// Generate new piece | |
spawnPiece(); | |
} | |
} | |
// Move the current piece left | |
function moveLeft() { | |
if (isPaused || isGameOver || !currentPiece) return; | |
if (isValidMove(currentPiece, 0, -1)) { | |
currentPiece.col--; | |
drawTetromino(); | |
} | |
} | |
// Move the current piece right | |
function moveRight() { | |
if (isPaused || isGameOver || !currentPiece) return; | |
if (isValidMove(currentPiece, 0, 1)) { | |
currentPiece.col++; | |
drawTetromino(); | |
} | |
} | |
// Rotate the current piece | |
function rotatePiece() { | |
if (isPaused || isGameOver || !currentPiece) return; | |
// Create a copy of the current piece | |
const rotatedPiece = { ...currentPiece }; | |
// Create a new rotated shape matrix | |
const N = rotatedPiece.shape.length; | |
const rotatedShape = Array(N).fill().map(() => Array(N).fill(0)); | |
// Perform the rotation (90 degrees clockwise) | |
for (let row = 0; row < N; row++) { | |
for (let col = 0; col < N; col++) { | |
rotatedShape[col][N - 1 - row] = rotatedPiece.shape[row][col]; | |
} | |
} | |
rotatedPiece.shape = rotatedShape; | |
// Check if the rotation is valid | |
if (isValidMove(rotatedPiece)) { | |
currentPiece.shape = rotatedShape; | |
drawTetromino(); | |
} | |
} | |
// Hard drop the current piece | |
function hardDrop() { | |
if (isPaused || isGameOver || !currentPiece) return; | |
while (isValidMove(currentPiece, 1, 0)) { | |
currentPiece.row++; | |
} | |
drawTetromino(); | |
lockPiece(); | |
checkLines(); | |
spawnPiece(); | |
} | |
// Lock the current piece in place | |
function lockPiece() { | |
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.row + row; | |
const boardCol = currentPiece.col + col; | |
if (boardRow >= 0 && boardRow < BOARD_HEIGHT && boardCol >= 0 && boardCol < BOARD_WIDTH) { | |
gameBoard[boardRow][boardCol] = currentPiece.color; | |
} | |
} | |
} | |
} | |
} | |
// Spawn a new piece | |
function spawnPiece() { | |
currentPiece = nextPiece || getRandomTetromino(); | |
nextPiece = getRandomTetromino(); | |
drawNextPiece(); | |
// Check if game is over (collision on spawn) | |
if (!isValidMove(currentPiece)) { | |
gameOver(); | |
} | |
drawTetromino(); | |
} | |
// Game over | |
function gameOver() { | |
isGameOver = true; | |
clearInterval(gameInterval); | |
finalScoreElement.textContent = score; | |
gameOverModal.classList.add('active'); | |
} | |
// Start or pause the game | |
function toggleGame() { | |
if (isGameOver) { | |
resetGame(); | |
return; | |
} | |
if (isPaused) { | |
// Resume game | |
isPaused = false; | |
startBtn.textContent = 'Pause'; | |
updateGameSpeed(); | |
} else { | |
// Pause game | |
isPaused = true; | |
startBtn.textContent = 'Resume'; | |
clearInterval(gameInterval); | |
} | |
} | |
// Reset the game | |
function resetGame() { | |
// Reset game state | |
gameBoard = Array(BOARD_HEIGHT).fill().map(() => Array(BOARD_WIDTH).fill(0)); | |
score = 0; | |
level = 1; | |
linesCleared = 0; | |
isPaused = false; | |
isGameOver = false; | |
// Reset UI | |
scoreElement.textContent = score; | |
levelElement.textContent = level; | |
startBtn.textContent = 'Pause'; | |
gameOverModal.classList.remove('active'); | |
// Start new game | |
spawnPiece(); | |
updateGameSpeed(); | |
} | |
// Handle keyboard controls | |
function handleKeyPress(e) { | |
if (isGameOver) 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': | |
toggleGame(); | |
break; | |
} | |
} | |
// Initialize the game | |
function init() { | |
initBoard(); | |
spawnPiece(); | |
// Event listeners | |
document.addEventListener('keydown', handleKeyPress); | |
startBtn.addEventListener('click', toggleGame); | |
restartBtn.addEventListener('click', resetGame); | |
// Start the game paused | |
isPaused = true; | |
startBtn.textContent = 'Start'; | |
} | |
// Start the game | |
init(); | |
}); | |
</script> | |
</body> | |
</html> |