tetris-by-deepsite / index.html
christiansca's picture
Add 2 files
9ceb509 verified
<!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>
<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>