tetris / index.html
bozhong's picture
请实现一个俄罗斯方块小游戏 - Initial Deployment
c9ee90d verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Responsive Tetris Game</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
/* Custom styles that can't be done with Tailwind */
.cell {
box-sizing: border-box;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.cell-filled {
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.5);
}
@keyframes flash {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
.flash-animation {
animation: flash 0.3s 2;
}
/* Tetromino colors */
.cell-I { background-color: #00FFFF; }
.cell-O { background-color: #FFFF00; }
.cell-T { background-color: #AA00FF; }
.cell-S { background-color: #00FF00; }
.cell-Z { background-color: #FF0000; }
.cell-J { background-color: #0000FF; }
.cell-L { background-color: #FFAA00; }
</style>
</head>
<body class="bg-gray-900 text-white min-h-screen flex flex-col items-center justify-center p-4">
<div class="text-center mb-4">
<h1 class="text-4xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-pink-600 mb-2">Tetris</h1>
<div class="flex justify-center gap-8">
<div class="text-xl">
<span class="text-gray-300">Score:</span>
<span id="score" class="font-bold ml-2">0</span>
</div>
<div class="text-xl">
<span class="text-gray-300">Level:</span>
<span id="level" class="font-bold ml-2">1</span>
</div>
<div class="text-xl">
<span class="text-gray-300">Lines:</span>
<span id="lines" class="font-bold ml-2">0</span>
</div>
</div>
</div>
<div class="flex flex-col md:flex-row items-center gap-8">
<div class="relative">
<div id="game-board" class="grid grid-cols-10 grid-rows-20 gap-0 bg-gray-800 border-2 border-gray-700 rounded-md overflow-hidden"></div>
<div id="game-over" class="absolute inset-0 bg-black bg-opacity-80 flex flex-col items-center justify-center hidden">
<h2 class="text-3xl font-bold text-red-500 mb-4">Game Over!</h2>
<button id="restart-btn" class="px-6 py-2 bg-blue-600 hover:bg-blue-700 rounded-md font-bold transition">Play Again</button>
</div>
<div id="pause-screen" class="absolute inset-0 bg-black bg-opacity-70 flex items-center justify-center hidden">
<h2 class="text-3xl font-bold text-yellow-400">Paused</h2>
</div>
</div>
<div class="flex flex-col gap-6">
<div class="bg-gray-800 p-4 rounded-md border border-gray-700">
<h3 class="text-xl font-semibold mb-2 text-center">Next Piece</h3>
<div id="next-piece" class="grid grid-cols-4 grid-rows-4 gap-0 w-24 h-24 mx-auto"></div>
</div>
<div class="bg-gray-800 p-4 rounded-md border border-gray-700">
<h3 class="text-xl font-semibold mb-2 text-center">Controls</h3>
<div class="grid grid-cols-3 gap-2 text-center">
<div class="bg-gray-700 p-2 rounded">← Left</div>
<div class="bg-gray-700 p-2 rounded">→ Right</div>
<div class="bg-gray-700 p-2 rounded">↑ Rotate</div>
<div class="bg-gray-700 p-2 rounded">↓ Soft Drop</div>
<div class="bg-gray-700 p-2 rounded">Space Hard Drop</div>
<div class="bg-gray-700 p-2 rounded">P Pause</div>
</div>
</div>
<button id="pause-btn" class="px-4 py-2 bg-yellow-600 hover:bg-yellow-700 rounded-md font-bold transition">Pause</button>
</div>
</div>
<div class="mt-8 text-gray-400 text-sm">
<p>Use keyboard or touch controls to play. Clear lines to score points!</p>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// Game constants
const COLS = 10;
const ROWS = 20;
const BLOCK_SIZE = 30;
const NEXT_PIECE_SIZE = 4;
// Game variables
let board = Array(ROWS).fill().map(() => Array(COLS).fill(0));
let currentPiece = null;
let nextPiece = null;
let score = 0;
let level = 1;
let lines = 0;
let gameOver = false;
let isPaused = false;
let dropInterval = null;
let dropSpeed = 1000; // Initial speed (ms)
// DOM elements
const gameBoard = document.getElementById('game-board');
const nextPieceDisplay = document.getElementById('next-piece');
const scoreDisplay = document.getElementById('score');
const levelDisplay = document.getElementById('level');
const linesDisplay = document.getElementById('lines');
const gameOverScreen = document.getElementById('game-over');
const pauseScreen = document.getElementById('pause-screen');
const restartBtn = document.getElementById('restart-btn');
const pauseBtn = document.getElementById('pause-btn');
// Tetromino shapes
const SHAPES = {
I: [
[0, 0, 0, 0],
[1, 1, 1, 1],
[0, 0, 0, 0],
[0, 0, 0, 0]
],
O: [
[1, 1],
[1, 1]
],
T: [
[0, 1, 0],
[1, 1, 1],
[0, 0, 0]
],
S: [
[0, 1, 1],
[1, 1, 0],
[0, 0, 0]
],
Z: [
[1, 1, 0],
[0, 1, 1],
[0, 0, 0]
],
J: [
[1, 0, 0],
[1, 1, 1],
[0, 0, 0]
],
L: [
[0, 0, 1],
[1, 1, 1],
[0, 0, 0]
]
};
const COLORS = {
I: 'cell-I',
O: 'cell-O',
T: 'cell-T',
S: 'cell-S',
Z: 'cell-Z',
J: 'cell-J',
L: 'cell-L'
};
// Initialize the game board
function initBoard() {
gameBoard.innerHTML = '';
gameBoard.style.width = `${COLS * BLOCK_SIZE}px`;
gameBoard.style.height = `${ROWS * BLOCK_SIZE}px`;
for (let row = 0; row < ROWS; row++) {
for (let col = 0; col < COLS; col++) {
const cell = document.createElement('div');
cell.className = 'cell w-full h-full';
cell.dataset.row = row;
cell.dataset.col = col;
gameBoard.appendChild(cell);
}
}
}
// Initialize the next piece display
function initNextPieceDisplay() {
nextPieceDisplay.innerHTML = '';
nextPieceDisplay.style.width = `${NEXT_PIECE_SIZE * BLOCK_SIZE}px`;
nextPieceDisplay.style.height = `${NEXT_PIECE_SIZE * BLOCK_SIZE}px`;
for (let row = 0; row < NEXT_PIECE_SIZE; row++) {
for (let col = 0; col < NEXT_PIECE_SIZE; col++) {
const cell = document.createElement('div');
cell.className = 'cell w-full h-full';
cell.dataset.row = row;
cell.dataset.col = col;
nextPieceDisplay.appendChild(cell);
}
}
}
// Create a new random piece
function createPiece() {
const types = Object.keys(SHAPES);
const type = types[Math.floor(Math.random() * types.length)];
const shape = SHAPES[type];
// For O piece (2x2), adjust starting position
const startCol = type === 'O' ? Math.floor(COLS / 2) - 1 : Math.floor(COLS / 2) - 2;
return {
shape,
type,
color: COLORS[type],
x: startCol,
y: 0
};
}
// Draw the current piece on the board
function drawPiece() {
if (!currentPiece) return;
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.y + row;
const boardCol = currentPiece.x + col;
if (boardRow >= 0 && boardRow < ROWS && boardCol >= 0 && boardCol < COLS) {
const cellIndex = boardRow * COLS + boardCol;
const cell = gameBoard.children[cellIndex];
cell.className = `cell cell-filled ${currentPiece.color}`;
}
}
}
}
}
// Draw the next piece in the preview
function drawNextPiece() {
if (!nextPiece) return;
// Clear the next piece display
for (let i = 0; i < nextPieceDisplay.children.length; i++) {
nextPieceDisplay.children[i].className = 'cell w-full h-full';
}
// Center the piece in the 4x4 grid
const offsetX = Math.floor((NEXT_PIECE_SIZE - nextPiece.shape[0].length) / 2);
const offsetY = Math.floor((NEXT_PIECE_SIZE - nextPiece.shape.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 displayRow = row + offsetY;
const displayCol = col + offsetX;
const cellIndex = displayRow * NEXT_PIECE_SIZE + displayCol;
const cell = nextPieceDisplay.children[cellIndex];
cell.className = `cell cell-filled ${nextPiece.color}`;
}
}
}
}
// Clear the current piece from the board (before moving/rotating)
function clearPiece() {
if (!currentPiece) return;
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.y + row;
const boardCol = currentPiece.x + col;
if (boardRow >= 0 && boardRow < ROWS && boardCol >= 0 && boardCol < COLS) {
const cellIndex = boardRow * COLS + boardCol;
const cell = gameBoard.children[cellIndex];
cell.className = 'cell w-full h-full';
}
}
}
}
}
// Check if the current piece can move to the specified position
function isValidMove(offsetX, offsetY, rotatedShape = null) {
const shape = rotatedShape || currentPiece.shape;
for (let row = 0; row < shape.length; row++) {
for (let col = 0; col < shape[row].length; col++) {
if (shape[row][col]) {
const newX = currentPiece.x + col + offsetX;
const newY = currentPiece.y + row + offsetY;
// Check boundaries
if (newX < 0 || newX >= COLS || newY >= ROWS) {
return false;
}
// Check collision with existing pieces (only check if moving down)
if (newY >= 0 && board[newY][newX] && offsetY >= 0) {
return false;
}
}
}
}
return true;
}
// Rotate the current piece
function rotatePiece() {
if (!currentPiece) return;
// Create a rotated version of the shape
const rotated = [];
for (let col = 0; col < currentPiece.shape[0].length; col++) {
const newRow = [];
for (let row = currentPiece.shape.length - 1; row >= 0; row--) {
newRow.push(currentPiece.shape[row][col]);
}
rotated.push(newRow);
}
// Check if the rotation is valid
if (isValidMove(0, 0, rotated)) {
clearPiece();
currentPiece.shape = rotated;
drawPiece();
} else {
// Try wall kicks (adjust position if rotation would cause collision)
const kicks = [-1, 1, -2, 2];
for (const kick of kicks) {
if (isValidMove(kick, 0, rotated)) {
clearPiece();
currentPiece.shape = rotated;
currentPiece.x += kick;
drawPiece();
break;
}
}
}
}
// Move the current piece left
function moveLeft() {
if (!currentPiece || gameOver || isPaused) return;
if (isValidMove(-1, 0)) {
clearPiece();
currentPiece.x--;
drawPiece();
}
}
// Move the current piece right
function moveRight() {
if (!currentPiece || gameOver || isPaused) return;
if (isValidMove(1, 0)) {
clearPiece();
currentPiece.x++;
drawPiece();
}
}
// Move the current piece down (soft drop)
function moveDown() {
if (!currentPiece || gameOver || isPaused) return;
if (isValidMove(0, 1)) {
clearPiece();
currentPiece.y++;
drawPiece();
return true;
} else {
lockPiece();
return false;
}
}
// Hard drop - move piece all the way down immediately
function hardDrop() {
if (!currentPiece || gameOver || isPaused) return;
while (moveDown()) {
// Keep moving down until it can't anymore
}
}
// Lock the current piece in place and check for completed lines
function lockPiece() {
if (!currentPiece) return;
// Add piece to the board
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.y + row;
const boardCol = currentPiece.x + col;
if (boardRow >= 0 && boardRow < ROWS && boardCol >= 0 && boardCol < COLS) {
board[boardRow][boardCol] = currentPiece.color;
}
}
}
}
// Check for completed lines
checkLines();
// Get next piece
currentPiece = nextPiece;
nextPiece = createPiece();
drawNextPiece();
// Check if game over (new piece can't be placed)
if (!isValidMove(0, 0)) {
gameOver = true;
clearInterval(dropInterval);
gameOverScreen.classList.remove('hidden');
}
}
// Check for completed lines and clear them
function checkLines() {
let linesCleared = 0;
for (let row = ROWS - 1; row >= 0; row--) {
if (board[row].every(cell => cell !== 0)) {
// Line is complete
linesCleared++;
// Flash animation for cleared lines
for (let col = 0; col < COLS; col++) {
const cellIndex = row * COLS + col;
const cell = gameBoard.children[cellIndex];
cell.classList.add('flash-animation');
}
// Remove the line after animation
setTimeout(() => {
// Shift all rows above down
for (let r = row; r > 0; r--) {
board[r] = [...board[r - 1]];
}
board[0] = Array(COLS).fill(0);
// Redraw the entire board
redrawBoard();
}, 300);
}
}
if (linesCleared > 0) {
// Update score based on lines cleared
const points = [0, 40, 100, 300, 1200]; // Points for 0, 1, 2, 3, 4 lines
score += points[linesCleared] * level;
lines += linesCleared;
// Every 10 lines increases the level
level = Math.floor(lines / 10) + 1;
// Increase speed with level (capped at 100ms)
dropSpeed = Math.max(100, 1000 - (level - 1) * 100);
// Update displays
updateDisplays();
// Restart the drop interval with new speed
clearInterval(dropInterval);
dropInterval = setInterval(moveDown, dropSpeed);
}
}
// Redraw the entire board (after line clears)
function redrawBoard() {
for (let row = 0; row < ROWS; row++) {
for (let col = 0; col < COLS; col++) {
const cellIndex = row * COLS + col;
const cell = gameBoard.children[cellIndex];
// Remove animation class if present
cell.classList.remove('flash-animation');
if (board[row][col]) {
cell.className = `cell cell-filled ${board[row][col]}`;
} else {
cell.className = 'cell w-full h-full';
}
}
}
}
// Update score, level, and lines displays
function updateDisplays() {
scoreDisplay.textContent = score;
levelDisplay.textContent = level;
linesDisplay.textContent = lines;
}
// Start the game
function startGame() {
// Reset game state
board = Array(ROWS).fill().map(() => Array(COLS).fill(0));
score = 0;
level = 1;
lines = 0;
gameOver = false;
dropSpeed = 1000;
// Create pieces
currentPiece = createPiece();
nextPiece = createPiece();
// Update displays
updateDisplays();
// Draw initial state
redrawBoard();
drawPiece();
drawNextPiece();
// Hide game over screen
gameOverScreen.classList.add('hidden');
// Start the drop interval
clearInterval(dropInterval);
dropInterval = setInterval(moveDown, dropSpeed);
}
// Toggle pause
function togglePause() {
if (gameOver) return;
isPaused = !isPaused;
if (isPaused) {
clearInterval(dropInterval);
pauseScreen.classList.remove('hidden');
} else {
dropInterval = setInterval(moveDown, dropSpeed);
pauseScreen.classList.add('hidden');
}
}
// Event listeners
document.addEventListener('keydown', (e) => {
if (gameOver && e.key === 'Enter') {
startGame();
return;
}
if (isPaused && e.key !== 'p') 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':
togglePause();
break;
}
});
// Touch controls for mobile
let touchStartX = 0;
let touchStartY = 0;
gameBoard.addEventListener('touchstart', (e) => {
if (gameOver || isPaused) return;
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
e.preventDefault();
}, { passive: false });
gameBoard.addEventListener('touchmove', (e) => {
if (gameOver || isPaused) return;
const touchX = e.touches[0].clientX;
const touchY = e.touches[0].clientY;
const diffX = touchX - touchStartX;
const diffY = touchY - touchStartY;
// Horizontal swipe
if (Math.abs(diffX) > Math.abs(diffY)) {
if (diffX > 30) {
moveRight();
touchStartX = touchX;
} else if (diffX < -30) {
moveLeft();
touchStartX = touchX;
}
}
// Vertical swipe down (soft drop)
if (diffY > 30) {
moveDown();
touchStartY = touchY;
}
e.preventDefault();
}, { passive: false });
gameBoard.addEventListener('touchend', (e) => {
if (gameOver || isPaused) return;
const touchEndX = e.changedTouches[0].clientX;
const touchEndY = e.changedTouches[0].clientY;
const diffX = touchEndX - touchStartX;
const diffY = touchEndY - touchStartY;
// Tap (rotate)
if (Math.abs(diffX) < 10 && Math.abs(diffY) < 10) {
rotatePiece();
}
// Quick swipe up (hard drop)
if (diffY < -50) {
hardDrop();
}
});
// Button event listeners
restartBtn.addEventListener('click', startGame);
pauseBtn.addEventListener('click', togglePause);
// Initialize the game
initBoard();
initNextPieceDisplay();
startGame();
});
</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 <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=bozhong/tetris" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>