nara-simba's picture
Add 2 files
d36a31e 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>
<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');
// Calculate size of each block
const blockSize = canvas.width / COLS;
// Game variables
let score = 0;
let level = 1;
let gameSpeed = 800; // Initial speed in ms
let gameOver = false;
let isPaused = false;
let dropInterval;
// Current piece
let currentPiece = null;
let nextPiece = null;
// Board - 2D array (COLS x ROWS)
let board = createBoard();
// Initialize the game
function init() {
board = createBoard();
score = 0;
level = 1;
gameSpeed = 800;
gameOver = false;
scoreElement.textContent = score;
levelElement.textContent = level;
gameOverElement.style.display = 'none';
// Generate first pieces
nextPiece = generatePiece();
newPiece();
// Start game loop
if (dropInterval) clearInterval(dropInterval);
dropInterval = setInterval(dropPiece, gameSpeed);
// Draw initial state
draw();
drawNext();
}
// Create empty board
function createBoard() {
return Array.from(Array(ROWS), () => Array(COLS).fill(0));
}
// Draw the board and current piece
function draw() {
// Clear the canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw the board
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]);
}
}
}
// Draw the current piece
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
);
}
}
}
}
}
// Draw the next piece
function drawNext() {
nextCtx.clearRect(0, 0, nextCanvas.width, nextCanvas.height);
if (nextPiece) {
// Center the piece in the next canvas
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
);
}
}
}
}
}
// Draw a single block
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);
// Add 3D effect
context.strokeStyle = 'rgba(255, 255, 255, 0.3)';
context.lineWidth = 2;
context.strokeRect(x * blockSize + offset, y * blockSize + offset, size, size);
// Add highlight effect
context.fillStyle = 'rgba(255, 255, 255, 0.1)';
context.fillRect(x * blockSize + offset + 2, y * blockSize + offset + 2, size * 0.4, size * 0.4);
}
// Generate a new random piece
function generatePiece() {
const randomIndex = Math.floor(Math.random() * PIECES.length);
const piece = JSON.parse(JSON.stringify(PIECES[randomIndex]));
// Adjust position to spawn at top center
piece.x = Math.floor(COLS / 2) - Math.floor(piece.tetromino[0].length / 2);
piece.y = 0;
return piece;
}
// Create new piece for play
function newPiece() {
currentPiece = nextPiece;
nextPiece = generatePiece();
drawNext();
// Check for collision immediately (game over condition)
if (isColliding()) {
endGame();
}
}
// Move piece down
function dropPiece() {
if (!gameOver && !isPaused) {
if (isColliding(0, 1)) {
lockPiece();
clearLines();
newPiece();
} else {
currentPiece.y++;
draw();
}
}
}
// Move piece left
function moveLeft() {
if (!isColliding(-1, 0)) {
currentPiece.x--;
draw();
}
}
// Move piece right
function moveRight() {
if (!isColliding(1, 0)) {
currentPiece.x++;
draw();
}
}
// Rotate piece
function rotate() {
const originalTetromino = currentPiece.tetromino;
const originalRotation = currentPiece.rotation;
// Try current rotation
currentPiece.tetromino = rotateMatrix(currentPiece.tetromino);
currentPiece.rotation = (currentPiece.rotation + 1) % 4;
// If rotation causes collision, try wall kicks
if (isColliding()) {
// Try moving left
currentPiece.x--;
if (isColliding()) {
currentPiece.x += 2; // Try moving right
if (isColliding()) {
currentPiece.x--; // Restore x position
currentPiece.tetromino = originalTetromino;
currentPiece.rotation = originalRotation;
}
}
}
draw();
}
// Rotate matrix 90 degrees
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;
}
// Check for collisions
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++) {
// Skip empty blocks
if (!currentPiece.tetromino[r][c]) continue;
const newX = currentPiece.x + c + offsetX;
const newY = currentPiece.y + r + offsetY;
// Check walls and floor
if (newX < 0 || newX >= COLS || newY >= ROWS) {
return true;
}
// Check for collision with existing blocks
if (newY >= 0 && board[newY][newX]) {
return true;
}
}
}
return false;
}
// Lock piece in place
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) { // Don't draw above the board
board[y][x] = currentPiece.color;
}
}
}
}
}
// Check for completed lines and clear them
function clearLines() {
let linesCleared = 0;
for (let r = ROWS - 1; r >= 0; r--) {
// Check if line is complete
if (board[r].every(cell => cell !== 0)) {
// Remove the line
board.splice(r, 1);
// Add new empty line at top
board.unshift(Array(COLS).fill(0));
linesCleared++;
r++; // Check the same row again (it's now the next row after splice)
}
}
if (linesCleared > 0) {
// Update score
const points = [0, 40, 100, 300, 1200]; // Points for 0, 1, 2, 3, 4 lines
score += points[linesCleared] * level;
scoreElement.textContent = score;
// Check for level up (every 10 lines)
const newLevel = Math.floor(score / 1000) + 1;
if (newLevel > level) {
level = newLevel;
levelElement.textContent = level;
// Increase speed
gameSpeed = Math.max(100, 800 - (level - 1) * 70);
clearInterval(dropInterval);
dropInterval = setInterval(dropPiece, gameSpeed);
}
}
}
// Hard drop - move piece all the way down immediately
function hardDrop() {
while (!isColliding(0, 1)) {
currentPiece.y++;
}
lockPiece();
clearLines();
newPiece();
draw();
}
// Toggle pause
function togglePause() {
isPaused = !isPaused;
if (isPaused) {
// Show pause message
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 {
// Redraw game board
draw();
}
}
// End game
function endGame() {
gameOver = true;
clearInterval(dropInterval);
finalScoreElement.textContent = score;
gameOverElement.style.display = 'flex';
}
// Event listeners
document.addEventListener('keydown', (e) => {
if (gameOver) return;
switch (e.keyCode) {
case 37: // Left arrow
moveLeft();
break;
case 39: // Right arrow
moveRight();
break;
case 40: // Down arrow
dropPiece();
break;
case 38: // Up arrow
rotate();
break;
case 32: // Space (hard drop)
hardDrop();
break;
case 80: // P (pause)
togglePause();
break;
}
});
// Mobile touch controls
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) { // Threshold to register a swipe
if (dx > 0) {
moveRight();
} else {
moveLeft();
}
touchStartX = touchX;
}
}
// Mobile tap for rotation
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) { // Tapped without much movement
rotate();
} else if (dy > 30) { // Swiped down
hardDrop();
}
}
touchStartX = null;
});
// Restart button
restartBtn.addEventListener('click', init);
// Game constants
const COLS = 10;
const ROWS = 20;
// Tetromino pieces
const PIECES = [
{ // Z
tetromino: [
[1, 1, 0],
[0, 1, 1],
[0, 0, 0]
],
color: '#FF0000',
rotation: 0
},
{ // S
tetromino: [
[0, 1, 1],
[1, 1, 0],
[0, 0, 0]
],
color: '#00FF00',
rotation: 0
},
{ // T
tetromino: [
[0, 1, 0],
[1, 1, 1],
[0, 0, 0]
],
color: '#AA00FF',
rotation: 0
},
{ // O
tetromino: [
[1, 1],
[1, 1]
],
color: '#FFFF00',
rotation: 0
},
{ // L
tetromino: [
[0, 0, 1],
[1, 1, 1],
[0, 0, 0]
],
color: '#FFA500',
rotation: 0
},
{ // J
tetromino: [
[1, 0, 0],
[1, 1, 1],
[0, 0, 0]
],
color: '#0000FF',
rotation: 0
},
{ // I
tetromino: [
[0, 0, 0, 0],
[1, 1, 1, 1],
[0, 0, 0, 0],
[0, 0, 0, 0]
],
color: '#00FFFF',
rotation: 0
}
];
// 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>