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 Game</title> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
<style> | |
:root { | |
--primary: #1a1a2e; | |
--secondary: #16213e; | |
--accent: #0f3460; | |
--highlight: #e94560; | |
--text: #f1f1f1; | |
--grid-line: rgba(255, 255, 255, 0.1); | |
--shadow: rgba(0, 0, 0, 0.3); | |
} | |
* { | |
margin: 0; | |
padding: 0; | |
box-sizing: border-box; | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
} | |
body { | |
background-color: var(--primary); | |
color: var(--text); | |
height: 100vh; | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
justify-content: center; | |
overflow: hidden; | |
position: relative; | |
} | |
body::before { | |
content: ''; | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background: linear-gradient(135deg, rgba(15, 52, 96, 0.2) 0%, rgba(21, 33, 62, 0.3) 100%); | |
z-index: -1; | |
} | |
.game-container { | |
display: flex; | |
gap: 20px; | |
align-items: flex-start; | |
} | |
#game-board { | |
border: 2px solid var(--accent); | |
background-color: var(--secondary); | |
box-shadow: 0 10px 30px var(--shadow); | |
position: relative; | |
overflow: hidden; | |
} | |
#game-board::before { | |
content: ''; | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background: linear-gradient(135deg, transparent 0%, rgba(255, 255, 255, 0.03) 100%); | |
pointer-events: none; | |
} | |
.info-panel { | |
background-color: var(--secondary); | |
padding: 20px; | |
border-radius: 10px; | |
box-shadow: 0 5px 15px var(--shadow); | |
width: 180px; | |
display: flex; | |
flex-direction: column; | |
gap: 20px; | |
border: 1px solid var(--accent); | |
} | |
.panel-section { | |
display: flex; | |
flex-direction: column; | |
gap: 10px; | |
} | |
.panel-title { | |
color: var(--highlight); | |
font-size: 1.2rem; | |
margin-bottom: 5px; | |
text-align: center; | |
border-bottom: 1px dotted var(--accent); | |
padding-bottom: 5px; | |
} | |
.score-display { | |
font-size: 1.5rem; | |
font-weight: bold; | |
text-align: center; | |
background-color: var(--primary); | |
padding: 10px; | |
border-radius: 5px; | |
border: 1px solid var(--accent); | |
} | |
.next-piece-container { | |
width: 120px; | |
height: 120px; | |
margin: 0 auto; | |
position: relative; | |
background-color: var(--primary); | |
border-radius: 5px; | |
border: 1px solid var(--accent); | |
} | |
#next-piece { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
} | |
.controls { | |
display: grid; | |
grid-template-columns: repeat(3, 1fr); | |
gap: 5px; | |
margin-top: 10px; | |
} | |
.btn { | |
background-color: var(--primary); | |
border: 1px solid var(--accent); | |
color: var(--text); | |
padding: 8px; | |
border-radius: 5px; | |
cursor: pointer; | |
transition: all 0.2s; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
} | |
.btn:hover { | |
background-color: var(--accent); | |
transform: translateY(-2px); | |
} | |
.btn i { | |
font-size: 1.2rem; | |
} | |
#pause-btn { | |
grid-column: span 3; | |
margin-top: 5px; | |
} | |
.game-overlay { | |
position: fixed; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background-color: rgba(0, 0, 0, 0.8); | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
justify-content: center; | |
z-index: 10; | |
opacity: 0; | |
pointer-events: none; | |
transition: opacity 0.3s; | |
} | |
.game-overlay.show { | |
opacity: 1; | |
pointer-events: all; | |
} | |
.overlay-content { | |
background-color: var(--primary); | |
padding: 30px; | |
border-radius: 10px; | |
text-align: center; | |
max-width: 400px; | |
box-shadow: 0 10px 30px var(--shadow); | |
border: 1px solid var(--highlight); | |
transform: scale(0.9); | |
transition: transform 0.3s; | |
} | |
.game-overlay.show .overlay-content { | |
transform: scale(1); | |
} | |
.overlay-title { | |
font-size: 2rem; | |
color: var(--highlight); | |
margin-bottom: 20px; | |
} | |
.overlay-text { | |
margin-bottom: 20px; | |
line-height: 1.6; | |
} | |
.overlay-btn { | |
background-color: var(--highlight); | |
color: white; | |
border: none; | |
padding: 12px 25px; | |
font-size: 1.1rem; | |
border-radius: 5px; | |
cursor: pointer; | |
transition: all 0.2s; | |
margin: 5px; | |
} | |
.overlay-btn:hover { | |
background-color: #ff5773; | |
transform: translateY(-2px); | |
} | |
.level-indicator { | |
width: 100%; | |
height: 10px; | |
background-color: var(--primary); | |
border-radius: 5px; | |
margin-top: 10px; | |
overflow: hidden; | |
border: 1px solid var(--accent); | |
} | |
.level-progress { | |
height: 100%; | |
background-color: var(--highlight); | |
width: 0%; | |
transition: width 0.3s; | |
} | |
.combo-display { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%) scale(0); | |
font-size: 3rem; | |
font-weight: bold; | |
color: var(--highlight); | |
text-shadow: 0 0 10px rgba(233, 69, 96, 0.6); | |
pointer-events: none; | |
opacity: 0; | |
transition: all 0.3s; | |
} | |
.combo-display.show { | |
opacity: 1; | |
transform: translate(-50%, -150%) scale(1); | |
} | |
.mobile-controls { | |
display: none; | |
margin-top: 20px; | |
} | |
.mobile-row { | |
display: flex; | |
justify-content: center; | |
margin-bottom: 10px; | |
} | |
.mobile-btn { | |
background-color: var(--secondary); | |
border: 1px solid var(--accent); | |
color: var(--text); | |
width: 60px; | |
height: 60px; | |
border-radius: 50%; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
margin: 0 5px; | |
font-size: 1.5rem; | |
} | |
@media (max-width: 768px) { | |
.game-container { | |
flex-direction: column; | |
align-items: center; | |
} | |
.info-panel { | |
width: 100%; | |
max-width: 300px; | |
} | |
.mobile-controls { | |
display: block; | |
} | |
} | |
.flash-effect { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background-color: rgba(255, 255, 255, 0.4); | |
opacity: 0; | |
pointer-events: none; | |
} | |
.flash-effect.active { | |
opacity: 1; | |
transition: opacity 0.3s; | |
} | |
.grid-cell { | |
position: absolute; | |
box-shadow: inset 0 0 0 1px var(--grid-line); | |
border-radius: 2px; | |
transition: all 0.1s; | |
} | |
/* Tetromino colors */ | |
.cell-i { background-color: #00f0f0; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.5); } | |
.cell-j { background-color: #0000f0; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.5); } | |
.cell-l { background-color: #f0a000; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.5); } | |
.cell-o { background-color: #f0f000; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.5); } | |
.cell-s { background-color: #00f000; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.5); } | |
.cell-t { background-color: #a000f0; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.5); } | |
.cell-z { background-color: #f00000; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.5); } | |
.cell-ghost { background-color: rgba(255, 255, 255, 0.1); } | |
</style> | |
</head> | |
<body> | |
<h1>Modern Tetris</h1> | |
<div class="game-container"> | |
<canvas id="game-board" width="300" height="600"></canvas> | |
<div class="info-panel"> | |
<div class="panel-section"> | |
<div class="panel-title">Score</div> | |
<div id="score" class="score-display">0</div> | |
</div> | |
<div class="panel-section"> | |
<div class="panel-title">Next</div> | |
<div class="next-piece-container"> | |
<canvas id="next-piece" width="100" height="100"></canvas> | |
</div> | |
</div> | |
<div class="panel-section"> | |
<div class="panel-title">Level</div> | |
<div id="level" class="score-display">1</div> | |
<div class="level-indicator"> | |
<div id="level-progress" class="level-progress"></div> | |
</div> | |
</div> | |
<div class="panel-section"> | |
<div class="panel-title">Controls</div> | |
<div class="controls"> | |
<button id="left-btn" class="btn" title="Move Left"><i class="fas fa-arrow-left"></i></button> | |
<button id="rotate-btn" class="btn" title="Rotate"><i class="fas fa-redo"></i></button> | |
<button id="right-btn" class="btn" title="Move Right"><i class="fas fa-arrow-right"></i></button> | |
<button id="soft-drop-btn" class="btn" title="Soft Drop"><i class="fas fa-arrow-down"></i></button> | |
<button id="hard-drop-btn" class="btn" title="Hard Drop"><i class="fas fa-angle-double-down"></i></button> | |
<button id="hold-btn" class="btn" title="Hold"><i class="fas fa-exchange-alt"></i></button> | |
<button id="pause-btn" class="btn" title="Pause"><i class="fas fa-pause"></i></button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="mobile-controls"> | |
<div class="mobile-row"> | |
<div id="mobile-rotate" class="mobile-btn"><i class="fas fa-redo"></i></div> | |
</div> | |
<div class="mobile-row"> | |
<div id="mobile-left" class="mobile-btn"><i class="fas fa-arrow-left"></i></div> | |
<div id="mobile-down" class="mobile-btn"><i class="fas fa-arrow-down"></i></div> | |
<div id="mobile-right" class="mobile-btn"><i class="fas fa-arrow-right"></i></div> | |
</div> | |
</div> | |
<div class="game-overlay" id="start-screen"> | |
<div class="overlay-content"> | |
<h2 class="overlay-title">Modern Tetris</h2> | |
<p class="overlay-text">Use arrow keys to move, ↑ to rotate, space for hard drop.</p> | |
<p class="overlay-text">Hold piece with 'C' key or the hold button.</p> | |
<button id="start-btn" class="overlay-btn">Start Game</button> | |
</div> | |
</div> | |
<div class="game-overlay" id="game-over-screen"> | |
<div class="overlay-content"> | |
<h2 class="overlay-title">Game Over</h2> | |
<p id="final-score" class="overlay-text">Your score: 0</p> | |
<button id="restart-btn" class="overlay-btn">Play Again</button> | |
</div> | |
</div> | |
<div class="game-overlay" id="pause-screen"> | |
<div class="overlay-content"> | |
<h2 class="overlay-title">Game Paused</h2> | |
<p class="overlay-text">Press ESC or click the button below to continue</p> | |
<button id="resume-btn" class="overlay-btn">Resume Game</button> | |
</div> | |
</div> | |
<div id="flash-effect" class="flash-effect"></div> | |
<div id="combo-display" class="combo-display"></div> | |
<audio id="clear-sound" src="data:audio/mp3;base64,SUQzBAAAAAABEVRYWFgAAAAtAAADY29tbWVudABCaWdTb3VuZEJhbmsuY29tIC8gTGFTb25vdGhlcXVlLm9yZwBURU5DAAAAHQAAA1N3aXRjaCBQbHVzIMKpIE5DSCBTb2Z0d2FyZQBUSVQyAAAABgAAAzIyMzUAVFNTRQAAAA8AAANMYXZmNTcuODYuMTAwAAAAAAAAAAAAAAD/80DEAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/zQsRbAAADSAAAAABVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/zQMSkAAADSAAAAABVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV" preload="auto"></audio> | |
<audio id="drop-sound" src="data:audio/mp3;base64,SUQzBAAAAAABEVRYWFgAAAAtAAADY29tbWVudABCaWdTb3VuZEJhbmsuY29tIC8gTGFTb25vdGhlcXVlLm9yZwBURU5DAAAAHQAAA1N3aXRjaCBQbHVzIMKpIE5DSCBTb2Z0d2FyZQBUSVQyAAAABgAAAzIyMzUAVFNTRQAAAA8AAANMYXZmNTcuODYuMTAwAAAAAAAAAAAAAAD/80DEAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/zQsRbAAADSAAAAABVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/zQMSkAAADSAAAAABVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV" preload="auto"></audio> | |
<audio id="game-over-sound" src="data:audio/mp3;base64,SUQzBAAAAAABEVRYWFgAAAAtAAADY29tbWVudABCaWdTb3VuZEJhbmsuY29tIC8gTGFTb25vdGhlcXVlLm9yZwBURU5DAAAAHQAAA1N3aXRjaCBQbHVzIMKpIE5DSCBTb2Z0d2FyZQBUSVQyAAAABgAAAzIyMzUAVFNTRQAAAA8AAANMYXZmNTcuODYuMTAwAAAAAAAAAAAAAAD/80DEAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/zQsRbAAADSAAAAABVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/zQMSkAAADSAAAAABVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV" preload="auto"></audio> | |
<script> | |
document.addEventListener('DOMContentLoaded', () => { | |
// Game constants | |
const COLS = 10; | |
const ROWS = 20; | |
const BLOCK_SIZE = 30; | |
const NEXT_BLOCK_SIZE = 20; | |
// Game state | |
let canvas, ctx, nextCtx; | |
let board = Array(ROWS).fill().map(() => Array(COLS).fill(0)); | |
let currentPiece = null; | |
let nextPiece = null; | |
let heldPiece = null; | |
let canHold = true; | |
let score = 0; | |
let level = 1; | |
let linesCleared = 0; | |
let gameOver = false; | |
let isPaused = false; | |
let dropCounter = 0; | |
let lastTime = 0; | |
let dropInterval = 1000; // ms | |
let comboCount = 0; | |
let lastComboTime = 0; | |
// Tetromino shapes (I, J, L, O, S, T, Z) | |
const PIECES = [ | |
{ // I | |
shape: [ | |
[0, 0, 0, 0], | |
[1, 1, 1, 1], | |
[0, 0, 0, 0], | |
[0, 0, 0, 0] | |
], | |
className: 'cell-i' | |
}, | |
{ // J | |
shape: [ | |
[1, 0, 0], | |
[1, 1, 1], | |
[0, 0, 0] | |
], | |
className: 'cell-j' | |
}, | |
{ // L | |
shape: [ | |
[0, 0, 1], | |
[1, 1, 1], | |
[0, 0, 0] | |
], | |
className: 'cell-l' | |
}, | |
{ // O | |
shape: [ | |
[1, 1], | |
[1, 1] | |
], | |
className: 'cell-o' | |
}, | |
{ // S | |
shape: [ | |
[0, 1, 1], | |
[1, 1, 0], | |
[0, 0, 0] | |
], | |
className: 'cell-s' | |
}, | |
{ // T | |
shape: [ | |
[0, 1, 0], | |
[1, 1, 1], | |
[0, 0, 0] | |
], | |
className: 'cell-t' | |
}, | |
{ // Z | |
shape: [ | |
[1, 1, 0], | |
[0, 1, 1], | |
[0, 0, 0] | |
], | |
className: 'cell-z' | |
} | |
]; | |
// DOM elements | |
const scoreDisplay = document.getElementById('score'); | |
const levelDisplay = document.getElementById('level'); | |
const levelProgress = document.getElementById('level-progress'); | |
const startScreen = document.getElementById('start-screen'); | |
const gameOverScreen = document.getElementById('game-over-screen'); | |
const pauseScreen = document.getElementById('pause-screen'); | |
const startBtn = document.getElementById('start-btn'); | |
const restartBtn = document.getElementById('restart-btn'); | |
const resumeBtn = document.getElementById('resume-btn'); | |
const finalScoreDisplay = document.getElementById('final-score'); | |
const flashEffect = document.getElementById('flash-effect'); | |
const comboDisplay = document.getElementById('combo-display'); | |
const clearSound = document.getElementById('clear-sound'); | |
const dropSound = document.getElementById('drop-sound'); | |
const gameOverSound = document.getElementById('game-over-sound'); | |
// Control buttons | |
const leftBtn = document.getElementById('left-btn'); | |
const rightBtn = document.getElementById('right-btn'); | |
const rotateBtn = document.getElementById('rotate-btn'); | |
const softDropBtn = document.getElementById('soft-drop-btn'); | |
const hardDropBtn = document.getElementById('hard-drop-btn'); | |
const holdBtn = document.getElementById('hold-btn'); | |
const pauseBtn = document.getElementById('pause-btn'); | |
// Mobile controls | |
const mobileLeft = document.getElementById('mobile-left'); | |
const mobileRight = document.getElementById('mobile-right'); | |
const mobileRotate = document.getElementById('mobile-rotate'); | |
const mobileDown = document.getElementById('mobile-down'); | |
// Initialize game | |
function init() { | |
canvas = document.getElementById('game-board'); | |
ctx = canvas.getContext('2d'); | |
nextCtx = document.getElementById('next-piece').getContext('2d'); | |
// Scale canvas | |
canvas.style.width = `${COLS * BLOCK_SIZE}px`; | |
canvas.style.height = `${ROWS * BLOCK_SIZE}px`; | |
// Event listeners | |
document.addEventListener('keydown', handleKeyPress); | |
startBtn.addEventListener('click', startGame); | |
restartBtn.addEventListener('click', startGame); | |
resumeBtn.addEventListener('click', togglePause); | |
pauseBtn.addEventListener('click', togglePause); | |
// Control buttons | |
leftBtn.addEventListener('click', () => move(-1)); | |
rightBtn.addEventListener('click', () => move(1)); | |
rotateBtn.addEventListener('click', rotate); | |
softDropBtn.addEventListener('click', () => softDrop()); | |
hardDropBtn.addEventListener('click', hardDrop); | |
holdBtn.addEventListener('click', holdPiece); | |
// Mobile controls | |
mobileLeft.addEventListener('click', () => move(-1)); | |
mobileRight.addEventListener('click', () => move(1)); | |
mobileRotate.addEventListener('click', rotate); | |
mobileDown.addEventListener('click', () => softDrop()); | |
// Show start screen | |
startScreen.classList.add('show'); | |
} | |
// Start a new game | |
function startGame() { | |
// Reset game state | |
board = Array(ROWS).fill().map(() => Array(COLS).fill(0)); | |
score = 0; | |
level = 1; | |
linesCleared = 0; | |
gameOver = false; | |
isPaused = false; | |
dropInterval = 1000; | |
canHold = true; | |
heldPiece = null; | |
comboCount = 0; | |
// Update UI | |
scoreDisplay.textContent = '0'; | |
levelDisplay.textContent = '1'; | |
levelProgress.style.width = '0%'; | |
// Hide overlays | |
startScreen.classList.remove('show'); | |
gameOverScreen.classList.remove('show'); | |
pauseScreen.classList.remove('show'); | |
// Create first pieces | |
nextPiece = createRandomPiece(); | |
spawnNewPiece(); | |
// Start game loop | |
requestAnimationFrame(gameLoop); | |
} | |
// Main game loop | |
function gameLoop(time = 0) { | |
if (gameOver || isPaused) return; | |
const deltaTime = time - lastTime; | |
lastTime = time; | |
dropCounter += deltaTime; | |
if (dropCounter > dropInterval) { | |
dropPiece(); | |
dropCounter = 0; | |
} | |
draw(); | |
drawNextPiece(); | |
requestAnimationFrame(gameLoop); | |
} | |
// Create a random tetromino piece | |
function createRandomPiece() { | |
const randomIndex = Math.floor(Math.random() * PIECES.length); | |
return { | |
shape: PIECES[randomIndex].shape, | |
className: PIECES[randomIndex].className, | |
pos: {x: 0, y: 0}, | |
rotation: 0 | |
}; | |
} | |
// Spawn a new piece at the top of the board | |
function spawnNewPiece() { | |
currentPiece = nextPiece; | |
nextPiece = createRandomPiece(); | |
// Center the piece | |
currentPiece.pos.x = Math.floor((COLS - currentPiece.shape[0].length) / 2); | |
currentPiece.pos.y = -1; // Start above the board | |
// Reset hold ability | |
canHold = true; | |
// Check if game over (no room for new piece) | |
if (collision()) { | |
gameOver = true; | |
gameOverSound.play(); | |
gameOverScreen.classList.add('show'); | |
finalScoreDisplay.textContent = `Your score: ${score}`; | |
} | |
} | |
// Draw the game board and current piece | |
function draw() { | |
// Clear the board | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
// Draw the board | |
for (let y = 0; y < ROWS; y++) { | |
for (let x = 0; x < COLS; x++) { | |
if (board[y][x]) { | |
drawBlock(x, y, board[y][x].className, ctx); | |
} | |
} | |
} | |
// Draw ghost piece | |
drawGhostPiece(); | |
// Draw current piece | |
drawPiece(currentPiece, ctx); | |
} | |
// Draw the next piece preview | |
function drawNextPiece() { | |
nextCtx.clearRect(0, 0, 100, 100); | |
// Center the next piece in the preview area | |
const offsetX = (100 - (nextPiece.shape[0].length * NEXT_BLOCK_SIZE)) / 2; | |
const offsetY = (100 - (nextPiece.shape.length * NEXT_BLOCK_SIZE)) / 2; | |
for (let y = 0; y < nextPiece.shape.length; y++) { | |
for (let x = 0; x < nextPiece.shape[y].length; x++) { | |
if (nextPiece.shape[y][x]) { | |
drawBlock( | |
x, | |
y, | |
nextPiece.className, | |
nextCtx, | |
offsetX, | |
offsetY, | |
NEXT_BLOCK_SIZE | |
); | |
} | |
} | |
} | |
} | |
// Draw a single block | |
function drawBlock(x, y, className, context, offsetX = 0, offsetY = 0, size = BLOCK_SIZE) { | |
const realX = x * size + offsetX; | |
const realY = y * size + offsetY; | |
context.fillStyle = className ? getComputedStyle(document.documentElement).getPropertyValue(`--${className.replace('cell-', '')}`) : 'transparent'; | |
context.fillRect(realX, realY, size, size); | |
context.strokeStyle = 'rgba(255, 255, 255, 0.2)'; | |
context.strokeRect(realX, realY, size, size); | |
} | |
// Draw the current piece | |
function drawPiece(piece, context) { | |
for (let y = 0; y < piece.shape.length; y++) { | |
for (let x = 0; x < piece.shape[y].length; x++) { | |
if (piece.shape[y][x]) { | |
drawBlock( | |
piece.pos.x + x, | |
piece.pos.y + y, | |
piece.className, | |
context | |
); | |
} | |
} | |
} | |
} | |
// Draw the ghost piece (shows where the piece will land) | |
function drawGhostPiece() { | |
const ghostPiece = JSON.parse(JSON.stringify(currentPiece)); | |
// Drop ghost piece to the bottom | |
while (!collision(ghostPiece.pos.x, ghostPiece.pos.y + 1, ghostPiece.shape)) { | |
ghostPiece.pos.y++; | |
} | |
// Draw ghost piece | |
for (let y = 0; y < ghostPiece.shape.length; y++) { | |
for (let x = 0; x < ghostPiece.shape[y].length; x++) { | |
if (ghostPiece.shape[y][x]) { | |
const className = 'cell-ghost'; | |
drawBlock( | |
ghostPiece.pos.x + x, | |
ghostPiece.pos.y + y, | |
className, | |
ctx | |
); | |
} | |
} | |
} | |
} | |
// Check for collisions | |
function collision(offsetX = currentPiece.pos.x, offsetY = currentPiece.pos.y, shape = currentPiece.shape) { | |
for (let y = 0; y < shape.length; y++) { | |
for (let x = 0; x < shape[y].length; x++) { | |
if (!shape[y][x]) continue; | |
const newX = offsetX + x; | |
const newY = offsetY + y; | |
// Check boundaries and filled blocks | |
if ( | |
newX < 0 || | |
newX >= COLS || | |
newY >= ROWS || | |
(newY >= 0 && board[newY][newX]) | |
) { | |
return true; | |
} | |
} | |
} | |
return false; | |
} | |
// Rotate the current piece | |
function rotate() { | |
if (isPaused || gameOver) return; | |
const originalShape = currentPiece.shape; | |
const originalRotation = currentPiece.rotation; | |
// Rotate the shape | |
const rotated = []; | |
for (let i = 0; i < originalShape[0].length; i++) { | |
const row = []; | |
for (let j = originalShape.length - 1; j >= 0; j--) { | |
row.push(originalShape[j][i]); | |
} | |
rotated.push(row); | |
} | |
currentPiece.shape = rotated; | |
currentPiece.rotation = (currentPiece.rotation + 90) % 360; | |
// Wall kicks if rotation causes collision | |
if (collision()) { | |
// Try moving left | |
currentPiece.pos.x -= 1; | |
if (collision()) { | |
// Try moving right (original position + 1) | |
currentPiece.pos.x += 2; | |
if (collision()) { | |
// Try moving left 2 (from original position) | |
currentPiece.pos.x -= 3; | |
if (collision()) { | |
// Revert rotation if all wall kicks fail | |
currentPiece.shape = originalShape; | |
currentPiece.rotation = originalRotation; | |
currentPiece.pos.x += 2; // Back to original position | |
return; | |
} | |
} | |
} | |
} | |
} | |
// Move the current piece horizontally | |
function move(direction) { | |
if (isPaused || gameOver) return; | |
currentPiece.pos.x += direction; | |
if (collision()) { | |
currentPiece.pos.x -= direction; | |
} | |
} | |
// Soft drop (move down manually) | |
function softDrop() { | |
if (isPaused || gameOver) return; | |
currentPiece.pos.y++; | |
if (collision()) { | |
currentPiece.pos.y--; | |
mergePiece(); | |
clearLines(); | |
spawnNewPiece(); | |
// Count this as a soft drop for scoring | |
addScore(1); | |
} | |
dropCounter = 0; // Reset drop counter to prevent double movement | |
} | |
// Hard drop (instantly drop to bottom) | |
function hardDrop() { | |
if (isPaused || gameOver) return; | |
let dropDistance = 0; | |
while (!collision(currentPiece.pos.x, currentPiece.pos.y + 1, currentPiece.shape)) { | |
currentPiece.pos.y++; | |
dropDistance++; | |
} | |
if (dropDistance > 0) { | |
dropSound.play(); | |
mergePiece(); | |
clearLines(); | |
spawnNewPiece(); | |
// Score 2 points per cell dropped | |
addScore(dropDistance * 2); | |
} | |
} | |
// Automatically drop the piece | |
function dropPiece() { | |
currentPiece.pos.y++; | |
if (collision()) { | |
currentPiece.pos.y--; | |
mergePiece(); | |
clearLines(); | |
spawnNewPiece(); | |
} | |
} | |
// Merge the current piece into the board | |
function mergePiece() { | |
for (let y = 0; y < currentPiece.shape.length; y++) { | |
for (let x = 0; x < currentPiece.shape[y].length; x++) { | |
if (currentPiece.shape[y][x]) { | |
const boardY = currentPiece.pos.y + y; | |
if (boardY >= 0) { // Only merge if it's on the board | |
board[boardY][currentPiece.pos.x + x] = { | |
className: currentPiece.className | |
}; | |
} | |
} | |
} | |
} | |
} | |
// Clear completed lines and update score | |
function clearLines() { | |
let linesToClear = []; | |
// Check for complete lines | |
for (let y = ROWS - 1; y >= 0; y--) { | |
if (board[y].every(cell => cell)) { | |
linesToClear.push(y); | |
} | |
} | |
if (linesToClear.length > 0) { | |
// Update combo | |
const now = Date.now(); | |
if (now - lastComboTime < 1000) { | |
comboCount++; | |
} else { | |
comboCount = 1; | |
} | |
lastComboTime = now; | |
// Show combo text if 3 or more | |
if (comboCount >= 3) { | |
comboDisplay.textContent = `COMBO x${comboCount}!`; | |
comboDisplay.classList.add('show'); | |
setTimeout(() => { | |
comboDisplay.classList.remove('show'); | |
}, 1000); | |
} | |
// Play clear sound | |
clearSound.currentTime = 0; | |
clearSound.play(); | |
// Flash effect | |
flashEffect.classList.add('active'); | |
setTimeout(() => { | |
flashEffect.classList.remove('active'); | |
}, 100); | |
// Score based on lines cleared at once | |
const linePoints = [0, 40, 100, 300, 1200]; | |
const levelBonus = level; | |
const comboBonus = Math.max(0, comboCount - 2) * 50; | |
const points = linePoints[linesToClear.length] * levelBonus + comboBonus; | |
addScore(points); | |
// Remove cleared lines | |
for (const line of linesToClear) { | |
board.splice(line, 1); | |
board.unshift(Array(COLS).fill(0)); | |
} | |
// Update level | |
linesCleared += linesToClear.length; | |
updateLevel(); | |
} else { | |
// Reset combo if no lines cleared | |
comboCount = 0; | |
} | |
} | |
// Update score display | |
function addScore(points) { | |
score += points; | |
scoreDisplay.textContent = score; | |
} | |
// Update level based on lines cleared | |
function updateLevel() { | |
const newLevel = Math.floor(linesCleared / 10) + 1; | |
if (newLevel !== level) { | |
level = newLevel; | |
levelDisplay.textContent = level; | |
// Increase speed | |
dropInterval = Math.max(100, 1000 - (level - 1) * 50); | |
} | |
// Update level progress | |
const progress = (linesCleared % 10) * 10; | |
levelProgress.style.width = `${progress}%`; | |
} | |
// Hold the current piece | |
function holdPiece() { | |
if (isPaused || gameOver || !canHold) return; | |
if (heldPiece) { | |
// Swap current piece with held piece | |
const temp = currentPiece; | |
currentPiece = { | |
shape: heldPiece.shape, | |
className: heldPiece.className, | |
pos: {x: Math.floor((COLS - heldPiece.shape[0].length) / 2), y: -1}, | |
rotation: 0 | |
}; | |
heldPiece = { | |
shape: temp.shape, | |
className: temp.className | |
}; | |
} else { | |
// First hold - just store the current piece | |
heldPiece = { | |
shape: currentPiece.shape, | |
className: currentPiece.className | |
}; | |
spawnNewPiece(); | |
} | |
canHold = false; | |
} | |
// Toggle pause state | |
function togglePause() { | |
if (gameOver) return; | |
isPaused = !isPaused; | |
pauseScreen.classList.toggle('show'); | |
if (!isPaused) { | |
lastTime = performance.now(); | |
requestAnimationFrame(gameLoop); | |
} | |
} | |
// Handle keyboard input | |
function handleKeyPress(e) { | |
if (e.key === 'Escape') { | |
togglePause(); | |
} | |
if (isPaused || gameOver) return; | |
switch (e.key) { | |
case 'ArrowLeft': | |
move(-1); | |
break; | |
case 'ArrowRight': | |
move(1); | |
break; | |
case 'ArrowDown': | |
softDrop(); | |
break; | |
case 'ArrowUp': | |
rotate(); | |
break; | |
case ' ': | |
hardDrop(); | |
break; | |
case 'c': | |
case 'C': | |
holdPiece(); | |
break; | |
} | |
} | |
// Start the game | |
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> |