t-boy / index.html
wangjinrambo's picture
Add 2 files
f99ebd9 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Neon Tetris</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
@keyframes explode {
0% { transform: scale(1); opacity: 1; }
100% { transform: scale(3); opacity: 0; }
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
}
@keyframes rainbow {
0% { background-color: #ff0000; }
14% { background-color: #ff7f00; }
28% { background-color: #ffff00; }
42% { background-color: #00ff00; }
57% { background-color: #0000ff; }
71% { background-color: #4b0082; }
85% { background-color: #9400d3; }
100% { background-color: #ff0000; }
}
.explosion {
animation: explode 0.5s forwards;
position: absolute;
border-radius: 50%;
pointer-events: none;
}
.cleared-row {
animation: pulse 0.2s 3, rainbow 1.5s;
}
.tetris-grid {
box-shadow: 0 0 20px rgba(0, 255, 255, 0.7);
}
.tetris-cell {
transition: background-color 0.1s;
box-shadow: inset 0 0 5px rgba(255, 255, 255, 0.3);
}
.ghost-piece {
opacity: 0.3;
}
.next-piece-container {
box-shadow: 0 0 15px rgba(255, 0, 255, 0.5);
}
</style>
</head>
<body class="bg-gray-900 text-white min-h-screen flex flex-col items-center justify-center p-4 font-mono">
<div class="flex flex-col md:flex-row items-center gap-8">
<!-- Game board -->
<div class="relative">
<div class="tetris-grid bg-gray-800 border-2 border-cyan-400 rounded">
<div id="game-board" class="grid grid-cols-10 grid-rows-20 gap-0"></div>
</div>
<div id="game-over" class="absolute inset-0 bg-black bg-opacity-70 flex flex-col items-center justify-center hidden">
<h2 class="text-4xl font-bold mb-4 text-red-500">GAME OVER</h2>
<button id="restart-btn" class="px-6 py-2 bg-cyan-500 hover:bg-cyan-600 rounded-lg font-bold transition">Play Again</button>
</div>
</div>
<!-- Side panel -->
<div class="flex flex-col gap-6">
<div class="bg-gray-800 p-4 rounded-lg border-2 border-purple-400">
<h2 class="text-xl font-bold mb-2 text-center text-purple-300">NEXT</h2>
<div class="next-piece-container bg-gray-700 p-2 rounded">
<div id="next-piece" class="grid grid-cols-4 grid-rows-4 gap-1 w-24 h-24"></div>
</div>
</div>
<div class="bg-gray-800 p-4 rounded-lg border-2 border-green-400">
<h2 class="text-xl font-bold mb-2 text-center text-green-300">SCORE</h2>
<div class="text-3xl font-bold text-center" id="score">0</div>
</div>
<div class="bg-gray-800 p-4 rounded-lg border-2 border-yellow-400">
<h2 class="text-xl font-bold mb-2 text-center text-yellow-300">LEVEL</h2>
<div class="text-3xl font-bold text-center" id="level">1</div>
</div>
<div class="bg-gray-800 p-4 rounded-lg border-2 border-red-400">
<h2 class="text-xl font-bold mb-2 text-center text-red-300">LINES</h2>
<div class="text-3xl font-bold text-center" id="lines">0</div>
</div>
<div class="flex flex-col gap-2 mt-2">
<button id="pause-btn" class="px-4 py-2 bg-yellow-500 hover:bg-yellow-600 rounded font-bold transition">Pause</button>
<button id="sound-btn" class="px-4 py-2 bg-blue-500 hover:bg-blue-600 rounded font-bold transition">Sound: ON</button>
</div>
</div>
</div>
<!-- Controls info -->
<div class="mt-8 text-center text-gray-400">
<p class="mb-2">Controls: Arrow keys to move, Up to rotate, Space to drop, P to pause</p>
<p>Made with HTML, CSS & JavaScript</p>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// Game constants
const COLS = 10;
const ROWS = 20;
const BLOCK_SIZE = 30;
const EMPTY = 'empty';
// Scoring
const SCORE = {
SINGLE: 100,
DOUBLE: 300,
TRIPLE: 500,
TETRIS: 800,
SOFT_DROP: 1,
HARD_DROP: 2
};
const LEVEL_SPEED = {
1: 800,
2: 720,
3: 630,
4: 550,
5: 470,
6: 380,
7: 300,
8: 220,
9: 130,
10: 100,
11: 80,
12: 80,
13: 80,
14: 70,
15: 70,
16: 70,
17: 50,
18: 50,
19: 50,
20: 30
};
// Tetromino shapes and colors
const SHAPES = [
{ shape: [[0, 0, 0, 0], [1, 1, 1, 1], [0, 0, 0, 0], [0, 0, 0, 0]], color: 'bg-cyan-500' }, // I
{ shape: [[1, 0, 0], [1, 1, 1], [0, 0, 0]], color: 'bg-blue-500' }, // J
{ shape: [[0, 0, 1], [1, 1, 1], [0, 0, 0]], color: 'bg-orange-500' }, // L
{ shape: [[1, 1], [1, 1]], color: 'bg-yellow-500' }, // O
{ shape: [[0, 1, 1], [1, 1, 0], [0, 0, 0]], color: 'bg-green-500' }, // S
{ shape: [[0, 1, 0], [1, 1, 1], [0, 0, 0]], color: 'bg-purple-500' }, // T
{ shape: [[1, 1, 0], [0, 1, 1], [0, 0, 0]], color: 'bg-red-500' } // Z
];
// Game state
let board = Array(ROWS).fill().map(() => Array(COLS).fill(EMPTY));
let currentPiece = null;
let nextPiece = null;
let currentPosition = { x: 0, y: 0 };
let score = 0;
let lines = 0;
let level = 1;
let gameOver = false;
let isPaused = false;
let soundEnabled = true;
let dropInterval = null;
let ghostPiece = null;
// DOM elements
const gameBoard = document.getElementById('game-board');
const nextPieceDisplay = document.getElementById('next-piece');
const scoreDisplay = document.getElementById('score');
const linesDisplay = document.getElementById('lines');
const levelDisplay = document.getElementById('level');
const gameOverDisplay = document.getElementById('game-over');
const restartBtn = document.getElementById('restart-btn');
const pauseBtn = document.getElementById('pause-btn');
const soundBtn = document.getElementById('sound-btn');
// Initialize game board
function initBoard() {
gameBoard.innerHTML = '';
gameBoard.style.width = `${COLS * BLOCK_SIZE}px`;
gameBoard.style.height = `${ROWS * BLOCK_SIZE}px`;
for (let y = 0; y < ROWS; y++) {
for (let x = 0; x < COLS; x++) {
const cell = document.createElement('div');
cell.className = 'tetris-cell w-7 h-7 border border-gray-700';
cell.dataset.row = y;
cell.dataset.col = x;
gameBoard.appendChild(cell);
}
}
}
// Create a random tetromino
function createPiece() {
const randomIndex = Math.floor(Math.random() * SHAPES.length);
return {
shape: SHAPES[randomIndex].shape,
color: SHAPES[randomIndex].color,
width: SHAPES[randomIndex].shape[0].length,
height: SHAPES[randomIndex].shape.length
};
}
// Draw the game board
function drawBoard() {
const cells = document.querySelectorAll('.tetris-cell');
// Clear all cells
cells.forEach(cell => {
cell.className = 'tetris-cell w-7 h-7 border border-gray-700';
cell.innerHTML = '';
});
// Draw locked pieces
for (let y = 0; y < ROWS; y++) {
for (let x = 0; x < COLS; x++) {
if (board[y][x] !== EMPTY) {
const cell = document.querySelector(`[data-row="${y}"][data-col="${x}"]`);
cell.className = `tetris-cell w-7 h-7 border border-gray-700 ${board[y][x]}`;
}
}
}
// Draw current piece
if (currentPiece) {
for (let y = 0; y < currentPiece.height; y++) {
for (let x = 0; x < currentPiece.width; x++) {
if (currentPiece.shape[y][x]) {
const boardX = currentPosition.x + x;
const boardY = currentPosition.y + y;
if (boardY >= 0) {
const cell = document.querySelector(`[data-row="${boardY}"][data-col="${boardX}"]`);
if (cell) {
cell.className = `tetris-cell w-7 h-7 border border-gray-700 ${currentPiece.color}`;
}
}
}
}
}
}
// Draw ghost piece
if (ghostPiece) {
for (let y = 0; y < ghostPiece.height; y++) {
for (let x = 0; x < ghostPiece.width; x++) {
if (ghostPiece.shape[y][x]) {
const boardX = ghostPiece.position.x + x;
const boardY = ghostPiece.position.y + y;
if (boardY >= 0) {
const cell = document.querySelector(`[data-row="${boardY}"][data-col="${boardX}"]`);
if (cell) {
cell.classList.add('ghost-piece', currentPiece.color);
}
}
}
}
}
}
}
// Draw next piece preview
function drawNextPiece() {
nextPieceDisplay.innerHTML = '';
for (let y = 0; y < 4; y++) {
for (let x = 0; x < 4; x++) {
const cell = document.createElement('div');
cell.className = 'w-5 h-5 border border-gray-600';
if (nextPiece && y < nextPiece.height && x < nextPiece.width && nextPiece.shape[y][x]) {
cell.className += ` ${nextPiece.color}`;
}
nextPieceDisplay.appendChild(cell);
}
}
}
// Check for collisions
function collision(x, y, piece) {
for (let py = 0; py < piece.height; py++) {
for (let px = 0; px < piece.width; px++) {
if (piece.shape[py][px]) {
const boardX = x + px;
const boardY = y + py;
// Check boundaries
if (boardX < 0 || boardX >= COLS || boardY >= ROWS) {
return true;
}
// Check if already occupied (and not above the board)
if (boardY >= 0 && board[boardY][boardX] !== EMPTY) {
return true;
}
}
}
}
return false;
}
// Rotate the current piece
function rotate() {
if (!currentPiece || gameOver || isPaused) return;
const rotated = {
shape: Array(currentPiece.width).fill().map(() => Array(currentPiece.height).fill(0)),
color: currentPiece.color,
width: currentPiece.height,
height: currentPiece.width
};
// Transpose and reverse rows to rotate 90 degrees
for (let y = 0; y < currentPiece.height; y++) {
for (let x = 0; x < currentPiece.width; x++) {
rotated.shape[x][currentPiece.height - 1 - y] = currentPiece.shape[y][x];
}
}
// Check if rotation is possible
if (!collision(currentPosition.x, currentPosition.y, rotated)) {
currentPiece = rotated;
updateGhostPiece();
drawBoard();
playSound('rotate');
} else {
// Try wall kicks (move left/right if rotation hits wall)
const kicks = [-1, 1, -2, 2];
for (const kick of kicks) {
if (!collision(currentPosition.x + kick, currentPosition.y, rotated)) {
currentPosition.x += kick;
currentPiece = rotated;
updateGhostPiece();
drawBoard();
playSound('rotate');
break;
}
}
}
}
// Move the piece left/right
function move(direction) {
if (!currentPiece || gameOver || isPaused) return;
const newX = currentPosition.x + direction;
if (!collision(newX, currentPosition.y, currentPiece)) {
currentPosition.x = newX;
updateGhostPiece();
drawBoard();
if (direction !== 0) playSound('move');
}
}
// Move the piece down (soft drop)
function softDrop() {
if (!currentPiece || gameOver || isPaused) return;
const newY = currentPosition.y + 1;
if (!collision(currentPosition.x, newY, currentPiece)) {
currentPosition.y = newY;
updateGhostPiece();
drawBoard();
addScore(SCORE.SOFT_DROP);
return false; // Didn't hit bottom
} else {
lockPiece();
return true; // Hit bottom
}
}
// Hard drop (instant drop)
function hardDrop() {
if (!currentPiece || gameOver || isPaused) return;
let dropDistance = 0;
while (!collision(currentPosition.x, currentPosition.y + 1, currentPiece)) {
currentPosition.y++;
dropDistance++;
}
lockPiece();
addScore(SCORE.HARD_DROP * dropDistance);
playSound('hardDrop');
}
// Lock the piece in place
function lockPiece() {
for (let y = 0; y < currentPiece.height; y++) {
for (let x = 0; x < currentPiece.width; x++) {
if (currentPiece.shape[y][x]) {
const boardY = currentPosition.y + y;
const boardX = currentPosition.x + x;
// If piece is locked above the board, game over
if (boardY < 0) {
endGame();
return;
}
board[boardY][boardX] = currentPiece.color;
}
}
}
// Check for completed lines
checkLines();
// Get next piece
spawnPiece();
playSound('lock');
}
// Check for completed lines
function checkLines() {
const linesToClear = [];
for (let y = ROWS - 1; y >= 0; y--) {
if (board[y].every(cell => cell !== EMPTY)) {
linesToClear.push(y);
}
}
if (linesToClear.length > 0) {
clearLines(linesToClear);
}
}
// Clear completed lines with special effects
function clearLines(linesToClear) {
// Highlight lines about to be cleared
linesToClear.forEach(y => {
for (let x = 0; x < COLS; x++) {
const cell = document.querySelector(`[data-row="${y}"][data-col="${x}"]`);
if (cell) cell.classList.add('cleared-row');
}
});
playSound('clear');
// Wait for animation then clear
setTimeout(() => {
// Create explosion effects
linesToClear.forEach(y => {
for (let x = 0; x < COLS; x++) {
createExplosion(x, y);
}
});
// Remove lines and add new empty ones at top
linesToClear.forEach(y => {
board.splice(y, 1);
board.unshift(Array(COLS).fill(EMPTY));
});
// Update score
addScore(
linesToClear.length === 1 ? SCORE.SINGLE * level :
linesToClear.length === 2 ? SCORE.DOUBLE * level :
linesToClear.length === 3 ? SCORE.TRIPLE * level :
SCORE.TETRIS * level
);
// Update lines and level
lines += linesToClear.length;
linesDisplay.textContent = lines;
const newLevel = Math.floor(lines / 10) + 1;
if (newLevel > level && newLevel <= 20) {
level = newLevel;
levelDisplay.textContent = level;
updateDropSpeed();
playSound('levelUp');
}
drawBoard();
}, 300);
}
// Create explosion effect at a cell
function createExplosion(x, y) {
const cell = document.querySelector(`[data-row="${y}"][data-col="${x}"]`);
if (!cell) return;
const rect = cell.getBoundingClientRect();
const colors = [
'bg-red-500', 'bg-orange-500', 'bg-yellow-500',
'bg-green-500', 'bg-blue-500', 'bg-purple-500'
];
// Create multiple explosion particles
for (let i = 0; i < 5; i++) {
const particle = document.createElement('div');
const color = colors[Math.floor(Math.random() * colors.length)];
particle.className = `explosion ${color} w-3 h-3`;
particle.style.left = `${rect.left + rect.width / 2}px`;
particle.style.top = `${rect.top + rect.height / 2}px`;
document.body.appendChild(particle);
// Remove after animation
setTimeout(() => {
particle.remove();
}, 500);
}
}
// Spawn a new piece
function spawnPiece() {
currentPiece = nextPiece || createPiece();
nextPiece = createPiece();
currentPosition = { x: Math.floor(COLS / 2) - Math.floor(currentPiece.width / 2), y: -2 };
// Check if game over (new piece collides immediately)
if (collision(currentPosition.x, currentPosition.y, currentPiece)) {
endGame();
}
updateGhostPiece();
drawBoard();
drawNextPiece();
}
// Update ghost piece position
function updateGhostPiece() {
if (!currentPiece) return;
ghostPiece = {
shape: currentPiece.shape,
color: currentPiece.color,
width: currentPiece.width,
height: currentPiece.height,
position: { ...currentPosition }
};
// Drop ghost to lowest possible position
while (!collision(ghostPiece.position.x, ghostPiece.position.y + 1, ghostPiece)) {
ghostPiece.position.y++;
}
}
// Update drop speed based on level
function updateDropSpeed() {
if (dropInterval) {
clearInterval(dropInterval);
}
dropInterval = setInterval(() => {
softDrop();
}, LEVEL_SPEED[level]);
}
// Add to score
function addScore(points) {
score += points;
scoreDisplay.textContent = score;
}
// End the game
function endGame() {
gameOver = true;
clearInterval(dropInterval);
gameOverDisplay.classList.remove('hidden');
playSound('gameOver');
}
// Reset the game
function resetGame() {
board = Array(ROWS).fill().map(() => Array(COLS).fill(EMPTY));
score = 0;
lines = 0;
level = 1;
gameOver = false;
scoreDisplay.textContent = score;
linesDisplay.textContent = lines;
levelDisplay.textContent = level;
gameOverDisplay.classList.add('hidden');
nextPiece = createPiece();
spawnPiece();
updateDropSpeed();
drawBoard();
drawNextPiece();
}
// Play sound effects
function playSound(type) {
if (!soundEnabled) return;
const sounds = {
rotate: () => {
const audio = new Audio('data:audio/wav;base64,UklGRl9vT19XQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YU...');
audio.volume = 0.3;
audio.play();
},
move: () => {
const audio = new Audio('data:audio/wav;base64,UklGRl9vT19XQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YU...');
audio.volume = 0.2;
audio.play();
},
hardDrop: () => {
const audio = new Audio('data:audio/wav;base64,UklGRl9vT19XQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YU...');
audio.volume = 0.4;
audio.play();
},
lock: () => {
const audio = new Audio('data:audio/wav;base64,UklGRl9vT19XQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YU...');
audio.volume = 0.3;
audio.play();
},
clear: () => {
const audio = new Audio('data:audio/wav;base64,UklGRl9vT19XQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YU...');
audio.volume = 0.5;
audio.play();
},
levelUp: () => {
const audio = new Audio('data:audio/wav;base64,UklGRl9vT19XQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YU...');
audio.volume = 0.5;
audio.play();
},
gameOver: () => {
const audio = new Audio('data:audio/wav;base64,UklGRl9vT19XQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YU...');
audio.volume = 0.6;
audio.play();
}
};
if (sounds[type]) {
sounds[type]();
}
}
// Toggle pause
function togglePause() {
isPaused = !isPaused;
pauseBtn.textContent = isPaused ? 'Resume' : 'Pause';
if (isPaused) {
clearInterval(dropInterval);
playSound('pause');
} else {
updateDropSpeed();
playSound('resume');
}
}
// Toggle sound
function toggleSound() {
soundEnabled = !soundEnabled;
soundBtn.textContent = `Sound: ${soundEnabled ? 'ON' : 'OFF'}`;
}
// Keyboard controls
document.addEventListener('keydown', (e) => {
if (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 'p':
case 'P':
togglePause();
break;
}
});
// Button controls
restartBtn.addEventListener('click', resetGame);
pauseBtn.addEventListener('click', togglePause);
soundBtn.addEventListener('click', toggleSound);
// Initialize game
initBoard();
nextPiece = createPiece();
spawnPiece();
updateDropSpeed();
});
</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=wangjinrambo/t-boy" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>