|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Modern Tetris Game</title> |
|
<style> |
|
* { |
|
margin: 0; |
|
padding: 0; |
|
box-sizing: border-box; |
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
|
} |
|
|
|
body { |
|
background: linear-gradient(135deg, #1a1a2e, #16213e); |
|
color: #fff; |
|
min-height: 100vh; |
|
display: flex; |
|
flex-direction: column; |
|
align-items: center; |
|
padding: 20px; |
|
} |
|
|
|
h1 { |
|
margin: 20px 0; |
|
text-align: center; |
|
color: #fff; |
|
text-shadow: 0 0 10px rgba(0, 255, 255, 0.5); |
|
font-size: 2.5rem; |
|
} |
|
|
|
.game-container { |
|
display: flex; |
|
flex-wrap: wrap; |
|
justify-content: center; |
|
gap: 20px; |
|
max-width: 1000px; |
|
width: 100%; |
|
} |
|
|
|
.game-board { |
|
border: 4px solid #00ffff; |
|
border-radius: 5px; |
|
box-shadow: 0 0 20px rgba(0, 255, 255, 0.3); |
|
background-color: rgba(0, 0, 0, 0.7); |
|
} |
|
|
|
.info-panel { |
|
display: flex; |
|
flex-direction: column; |
|
gap: 20px; |
|
background-color: rgba(0, 0, 0, 0.7); |
|
padding: 20px; |
|
border-radius: 10px; |
|
border: 2px solid #00ffff; |
|
box-shadow: 0 0 15px rgba(0, 255, 255, 0.2); |
|
min-width: 200px; |
|
} |
|
|
|
.next-piece { |
|
text-align: center; |
|
padding: 10px; |
|
background-color: rgba(0, 0, 0, 0.5); |
|
border-radius: 5px; |
|
border: 1px solid #00ffff; |
|
} |
|
|
|
.score-display { |
|
text-align: center; |
|
padding: 15px; |
|
background-color: rgba(0, 0, 0, 0.5); |
|
border-radius: 5px; |
|
border: 1px solid #00ffff; |
|
} |
|
|
|
.score-display h3 { |
|
margin-bottom: 10px; |
|
color: #00ffff; |
|
} |
|
|
|
.score-value { |
|
font-size: 2rem; |
|
font-weight: bold; |
|
color: #fff; |
|
} |
|
|
|
.level-display { |
|
text-align: center; |
|
padding: 15px; |
|
background-color: rgba(0, 0, 0, 0.5); |
|
border-radius: 5px; |
|
border: 1px solid #00ffff; |
|
} |
|
|
|
.level-display h3 { |
|
margin-bottom: 10px; |
|
color: #00ffff; |
|
} |
|
|
|
.level-value { |
|
font-size: 2rem; |
|
font-weight: bold; |
|
color: #fff; |
|
} |
|
|
|
.controls { |
|
margin-top: 10px; |
|
text-align: center; |
|
} |
|
|
|
.controls h3 { |
|
margin-bottom: 10px; |
|
color: #00ffff; |
|
} |
|
|
|
.control-key { |
|
display: inline-block; |
|
margin: 5px; |
|
padding: 8px 12px; |
|
background-color: rgba(0, 255, 255, 0.2); |
|
border: 1px solid #00ffff; |
|
border-radius: 5px; |
|
font-family: monospace; |
|
font-weight: bold; |
|
} |
|
|
|
.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; |
|
display: none; |
|
} |
|
|
|
.game-over h2 { |
|
font-size: 3rem; |
|
color: #ff0000; |
|
margin-bottom: 20px; |
|
text-shadow: 0 0 10px rgba(255, 0, 0, 0.5); |
|
} |
|
|
|
.restart-btn { |
|
padding: 15px 30px; |
|
background-color: #00ffff; |
|
color: #000; |
|
border: none; |
|
border-radius: 5px; |
|
font-size: 1.2rem; |
|
font-weight: bold; |
|
cursor: pointer; |
|
transition: all 0.3s; |
|
} |
|
|
|
.restart-btn:hover { |
|
background-color: #fff; |
|
box-shadow: 0 0 15px rgba(0, 255, 255, 0.8); |
|
} |
|
|
|
@media (max-width: 768px) { |
|
.game-container { |
|
flex-direction: column; |
|
align-items: center; |
|
} |
|
|
|
.game-board { |
|
width: 300px; |
|
height: 600px; |
|
} |
|
|
|
.info-panel { |
|
width: 300px; |
|
flex-direction: row; |
|
justify-content: space-between; |
|
flex-wrap: wrap; |
|
} |
|
|
|
.next-piece, .score-display, .level-display { |
|
flex: 1; |
|
min-width: 120px; |
|
} |
|
} |
|
|
|
@media (max-width: 400px) { |
|
.game-board { |
|
width: 250px; |
|
height: 500px; |
|
} |
|
|
|
.info-panel { |
|
width: 250px; |
|
} |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<h1>MODERN TETRIS</h1> |
|
|
|
<div class="game-container"> |
|
<canvas id="board" class="game-board" width="300" height="600"></canvas> |
|
|
|
<div class="info-panel"> |
|
<div class="next-piece"> |
|
<h3>Next Piece</h3> |
|
<canvas id="next" width="150" height="150"></canvas> |
|
</div> |
|
|
|
<div class="score-display"> |
|
<h3>Score</h3> |
|
<div class="score-value" id="score">0</div> |
|
</div> |
|
|
|
<div class="level-display"> |
|
<h3>Level</h3> |
|
<div class="level-value" id="level">1</div> |
|
</div> |
|
|
|
<div class="controls"> |
|
<h3>Controls</h3> |
|
<div> |
|
<span class="control-key">←</span> |
|
<span class="control-key">→</span> Move<br> |
|
<span class="control-key">↑</span> Rotate<br> |
|
<span class="control-key">↓</span> Soft Drop<br> |
|
<span class="control-key">Space</span> Hard Drop<br> |
|
<span class="control-key">P</span> Pause |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="game-over" id="gameOver"> |
|
<h2>GAME OVER</h2> |
|
<div class="final-score">Score: <span id="finalScore">0</span></div> |
|
<button class="restart-btn" id="restartBtn">Play Again</button> |
|
</div> |
|
|
|
<script> |
|
document.addEventListener('DOMContentLoaded', () => { |
|
const canvas = document.getElementById('board'); |
|
const ctx = canvas.getContext('2d'); |
|
|
|
const nextCanvas = document.getElementById('next'); |
|
const nextCtx = nextCanvas.getContext('2d'); |
|
|
|
const scoreElement = document.getElementById('score'); |
|
const levelElement = document.getElementById('level'); |
|
const finalScoreElement = document.getElementById('finalScore'); |
|
const gameOverElement = document.getElementById('gameOver'); |
|
const restartBtn = document.getElementById('restartBtn'); |
|
|
|
|
|
const blockSize = canvas.width / COLS; |
|
|
|
|
|
let score = 0; |
|
let level = 1; |
|
let gameSpeed = 800; |
|
let gameOver = false; |
|
let isPaused = false; |
|
let dropInterval; |
|
|
|
|
|
let currentPiece = null; |
|
let nextPiece = null; |
|
|
|
|
|
let board = createBoard(); |
|
|
|
|
|
function init() { |
|
board = createBoard(); |
|
score = 0; |
|
level = 1; |
|
gameSpeed = 800; |
|
gameOver = false; |
|
scoreElement.textContent = score; |
|
levelElement.textContent = level; |
|
gameOverElement.style.display = 'none'; |
|
|
|
|
|
nextPiece = generatePiece(); |
|
newPiece(); |
|
|
|
|
|
if (dropInterval) clearInterval(dropInterval); |
|
dropInterval = setInterval(dropPiece, gameSpeed); |
|
|
|
|
|
draw(); |
|
drawNext(); |
|
} |
|
|
|
|
|
function createBoard() { |
|
return Array.from(Array(ROWS), () => Array(COLS).fill(0)); |
|
} |
|
|
|
|
|
function draw() { |
|
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height); |
|
|
|
|
|
for (let r = 0; r < ROWS; r++) { |
|
for (let c = 0; c < COLS; c++) { |
|
if (board[r][c]) { |
|
drawBlock(ctx, c, r, board[r][c]); |
|
} |
|
} |
|
} |
|
|
|
|
|
if (currentPiece) { |
|
for (let r = 0; r < currentPiece.tetromino.length; r++) { |
|
for (let c = 0; c < currentPiece.tetromino[r].length; c++) { |
|
if (currentPiece.tetromino[r][c]) { |
|
drawBlock( |
|
ctx, |
|
currentPiece.x + c, |
|
currentPiece.y + r, |
|
currentPiece.color |
|
); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
function drawNext() { |
|
nextCtx.clearRect(0, 0, nextCanvas.width, nextCanvas.height); |
|
|
|
if (nextPiece) { |
|
|
|
const offsetX = (nextCanvas.width / blockSize - nextPiece.tetromino[0].length) / 2; |
|
const offsetY = (nextCanvas.height / blockSize - nextPiece.tetromino.length) / 2; |
|
|
|
for (let r = 0; r < nextPiece.tetromino.length; r++) { |
|
for (let c = 0; c < nextPiece.tetromino[r].length; c++) { |
|
if (nextPiece.tetromino[r][c]) { |
|
drawBlock( |
|
nextCtx, |
|
offsetX + c, |
|
offsetY + r, |
|
nextPiece.color, |
|
true |
|
); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
function drawBlock(context, x, y, color, isNext = false) { |
|
const size = isNext ? blockSize * 0.8 : blockSize; |
|
const offset = isNext ? blockSize * 0.1 : 0; |
|
|
|
context.fillStyle = color; |
|
context.fillRect(x * blockSize + offset, y * blockSize + offset, size, size); |
|
|
|
|
|
context.strokeStyle = 'rgba(255, 255, 255, 0.3)'; |
|
context.lineWidth = 2; |
|
context.strokeRect(x * blockSize + offset, y * blockSize + offset, size, size); |
|
|
|
|
|
context.fillStyle = 'rgba(255, 255, 255, 0.1)'; |
|
context.fillRect(x * blockSize + offset + 2, y * blockSize + offset + 2, size * 0.4, size * 0.4); |
|
} |
|
|
|
|
|
function generatePiece() { |
|
const randomIndex = Math.floor(Math.random() * PIECES.length); |
|
const piece = JSON.parse(JSON.stringify(PIECES[randomIndex])); |
|
|
|
|
|
piece.x = Math.floor(COLS / 2) - Math.floor(piece.tetromino[0].length / 2); |
|
piece.y = 0; |
|
|
|
return piece; |
|
} |
|
|
|
|
|
function newPiece() { |
|
currentPiece = nextPiece; |
|
nextPiece = generatePiece(); |
|
drawNext(); |
|
|
|
|
|
if (isColliding()) { |
|
endGame(); |
|
} |
|
} |
|
|
|
|
|
function dropPiece() { |
|
if (!gameOver && !isPaused) { |
|
if (isColliding(0, 1)) { |
|
lockPiece(); |
|
clearLines(); |
|
newPiece(); |
|
} else { |
|
currentPiece.y++; |
|
draw(); |
|
} |
|
} |
|
} |
|
|
|
|
|
function moveLeft() { |
|
if (!isColliding(-1, 0)) { |
|
currentPiece.x--; |
|
draw(); |
|
} |
|
} |
|
|
|
|
|
function moveRight() { |
|
if (!isColliding(1, 0)) { |
|
currentPiece.x++; |
|
draw(); |
|
} |
|
} |
|
|
|
|
|
function rotate() { |
|
const originalTetromino = currentPiece.tetromino; |
|
const originalRotation = currentPiece.rotation; |
|
|
|
|
|
currentPiece.tetromino = rotateMatrix(currentPiece.tetromino); |
|
currentPiece.rotation = (currentPiece.rotation + 1) % 4; |
|
|
|
|
|
if (isColliding()) { |
|
|
|
currentPiece.x--; |
|
if (isColliding()) { |
|
currentPiece.x += 2; |
|
if (isColliding()) { |
|
currentPiece.x--; |
|
currentPiece.tetromino = originalTetromino; |
|
currentPiece.rotation = originalRotation; |
|
} |
|
} |
|
} |
|
|
|
draw(); |
|
} |
|
|
|
|
|
function rotateMatrix(matrix) { |
|
const N = matrix.length; |
|
const rotated = Array.from(Array(matrix[0].length), () => Array(N).fill(0)); |
|
|
|
for (let r = 0; r < N; r++) { |
|
for (let c = 0; c < matrix[r].length; c++) { |
|
rotated[c][N - 1 - r] = matrix[r][c]; |
|
} |
|
} |
|
|
|
return rotated; |
|
} |
|
|
|
|
|
function isColliding(offsetX = 0, offsetY = 0) { |
|
for (let r = 0; r < currentPiece.tetromino.length; r++) { |
|
for (let c = 0; c < currentPiece.tetromino[r].length; c++) { |
|
|
|
if (!currentPiece.tetromino[r][c]) continue; |
|
|
|
const newX = currentPiece.x + c + offsetX; |
|
const newY = currentPiece.y + r + offsetY; |
|
|
|
|
|
if (newX < 0 || newX >= COLS || newY >= ROWS) { |
|
return true; |
|
} |
|
|
|
|
|
if (newY >= 0 && board[newY][newX]) { |
|
return true; |
|
} |
|
} |
|
} |
|
|
|
return false; |
|
} |
|
|
|
|
|
function lockPiece() { |
|
for (let r = 0; r < currentPiece.tetromino.length; r++) { |
|
for (let c = 0; c < currentPiece.tetromino[r].length; c++) { |
|
if (currentPiece.tetromino[r][c]) { |
|
const y = currentPiece.y + r; |
|
const x = currentPiece.x + c; |
|
|
|
if (y >= 0) { |
|
board[y][x] = currentPiece.color; |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
function clearLines() { |
|
let linesCleared = 0; |
|
|
|
for (let r = ROWS - 1; r >= 0; r--) { |
|
|
|
if (board[r].every(cell => cell !== 0)) { |
|
|
|
board.splice(r, 1); |
|
|
|
board.unshift(Array(COLS).fill(0)); |
|
linesCleared++; |
|
r++; |
|
} |
|
} |
|
|
|
if (linesCleared > 0) { |
|
|
|
const points = [0, 40, 100, 300, 1200]; |
|
score += points[linesCleared] * level; |
|
scoreElement.textContent = score; |
|
|
|
|
|
const newLevel = Math.floor(score / 1000) + 1; |
|
if (newLevel > level) { |
|
level = newLevel; |
|
levelElement.textContent = level; |
|
|
|
|
|
gameSpeed = Math.max(100, 800 - (level - 1) * 70); |
|
clearInterval(dropInterval); |
|
dropInterval = setInterval(dropPiece, gameSpeed); |
|
} |
|
} |
|
} |
|
|
|
|
|
function hardDrop() { |
|
while (!isColliding(0, 1)) { |
|
currentPiece.y++; |
|
} |
|
lockPiece(); |
|
clearLines(); |
|
newPiece(); |
|
draw(); |
|
} |
|
|
|
|
|
function togglePause() { |
|
isPaused = !isPaused; |
|
if (isPaused) { |
|
|
|
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; |
|
ctx.fillRect(0, canvas.height / 2 - 30, canvas.width, 60); |
|
ctx.fillStyle = '#00ffff'; |
|
ctx.font = '30px Arial'; |
|
ctx.textAlign = 'center'; |
|
ctx.fillText('PAUSED', canvas.width / 2, canvas.height / 2 + 10); |
|
} else { |
|
|
|
draw(); |
|
} |
|
} |
|
|
|
|
|
function endGame() { |
|
gameOver = true; |
|
clearInterval(dropInterval); |
|
finalScoreElement.textContent = score; |
|
gameOverElement.style.display = 'flex'; |
|
} |
|
|
|
|
|
document.addEventListener('keydown', (e) => { |
|
if (gameOver) return; |
|
|
|
switch (e.keyCode) { |
|
case 37: |
|
moveLeft(); |
|
break; |
|
case 39: |
|
moveRight(); |
|
break; |
|
case 40: |
|
dropPiece(); |
|
break; |
|
case 38: |
|
rotate(); |
|
break; |
|
case 32: |
|
hardDrop(); |
|
break; |
|
case 80: |
|
togglePause(); |
|
break; |
|
} |
|
}); |
|
|
|
|
|
canvas.addEventListener('touchstart', handleTouchStart, false); |
|
canvas.addEventListener('touchmove', handleTouchMove, false); |
|
let touchStartX = null; |
|
|
|
function handleTouchStart(e) { |
|
if (gameOver || isPaused) return; |
|
touchStartX = e.touches[0].clientX; |
|
} |
|
|
|
function handleTouchMove(e) { |
|
if (gameOver || isPaused || touchStartX === null) return; |
|
e.preventDefault(); |
|
|
|
const touchX = e.touches[0].clientX; |
|
const dx = touchX - touchStartX; |
|
|
|
if (Math.abs(dx) > 50) { |
|
if (dx > 0) { |
|
moveRight(); |
|
} else { |
|
moveLeft(); |
|
} |
|
touchStartX = touchX; |
|
} |
|
} |
|
|
|
|
|
canvas.addEventListener('touchend', (e) => { |
|
if (gameOver || isPaused) return; |
|
|
|
if (touchStartX !== null) { |
|
const touchY = e.changedTouches[0].clientY; |
|
const startY = e.changedTouches[0].clientY; |
|
const dy = touchY - startY; |
|
|
|
if (Math.abs(dy) < 10) { |
|
rotate(); |
|
} else if (dy > 30) { |
|
hardDrop(); |
|
} |
|
} |
|
|
|
touchStartX = null; |
|
}); |
|
|
|
|
|
restartBtn.addEventListener('click', init); |
|
|
|
|
|
const COLS = 10; |
|
const ROWS = 20; |
|
|
|
|
|
const PIECES = [ |
|
{ |
|
tetromino: [ |
|
[1, 1, 0], |
|
[0, 1, 1], |
|
[0, 0, 0] |
|
], |
|
color: '#FF0000', |
|
rotation: 0 |
|
}, |
|
{ |
|
tetromino: [ |
|
[0, 1, 1], |
|
[1, 1, 0], |
|
[0, 0, 0] |
|
], |
|
color: '#00FF00', |
|
rotation: 0 |
|
}, |
|
{ |
|
tetromino: [ |
|
[0, 1, 0], |
|
[1, 1, 1], |
|
[0, 0, 0] |
|
], |
|
color: '#AA00FF', |
|
rotation: 0 |
|
}, |
|
{ |
|
tetromino: [ |
|
[1, 1], |
|
[1, 1] |
|
], |
|
color: '#FFFF00', |
|
rotation: 0 |
|
}, |
|
{ |
|
tetromino: [ |
|
[0, 0, 1], |
|
[1, 1, 1], |
|
[0, 0, 0] |
|
], |
|
color: '#FFA500', |
|
rotation: 0 |
|
}, |
|
{ |
|
tetromino: [ |
|
[1, 0, 0], |
|
[1, 1, 1], |
|
[0, 0, 0] |
|
], |
|
color: '#0000FF', |
|
rotation: 0 |
|
}, |
|
{ |
|
tetromino: [ |
|
[0, 0, 0, 0], |
|
[1, 1, 1, 1], |
|
[0, 0, 0, 0], |
|
[0, 0, 0, 0] |
|
], |
|
color: '#00FFFF', |
|
rotation: 0 |
|
} |
|
]; |
|
|
|
|
|
init(); |
|
}); |
|
</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 <a href="https://enzostvs-deepsite.hf.space" style="color: #fff;" target="_blank" >DeepSite</a> <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;"></p></body> |
|
</html> |