Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
| <title>Neon Tetris | Ultimate Edition</title> | |
| <style> | |
| /* ========================================= | |
| 1. CSS VARIABLES & RESET | |
| ========================================= */ | |
| :root { | |
| --bg-color: #0f172a; | |
| --surface-color: #1e293b; | |
| --primary-color: #38bdf8; | |
| --accent-color: #f472b6; | |
| --text-color: #f1f5f9; | |
| --grid-line: rgba(255, 255, 255, 0.05); | |
| --glass-bg: rgba(30, 41, 59, 0.7); | |
| --glass-border: rgba(255, 255, 255, 0.1); | |
| /* Tetromino Colors */ | |
| --color-i: #00f0f0; | |
| --color-j: #0000f0; | |
| --color-l: #f0a000; | |
| --color-o: #f0f000; | |
| --color-s: #00f000; | |
| --color-t: #a000f0; | |
| --color-z: #f00000; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| user-select: none; | |
| -webkit-tap-highlight-color: transparent; | |
| } | |
| body { | |
| font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; | |
| background-color: var(--bg-color); | |
| background-image: | |
| radial-gradient(circle at 10% 20%, rgba(56, 189, 248, 0.1) 0%, transparent 20%), | |
| radial-gradient(circle at 90% 80%, rgba(244, 114, 182, 0.1) 0%, transparent 20%); | |
| color: var(--text-color); | |
| height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| overflow: hidden; | |
| } | |
| /* ========================================= | |
| 2. LAYOUT & CONTAINERS | |
| ========================================= */ | |
| .game-wrapper { | |
| display: flex; | |
| gap: 2rem; | |
| padding: 2rem; | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(12px); | |
| -webkit-backdrop-filter: blur(12px); | |
| border: 1px solid var(--glass-border); | |
| border-radius: 24px; | |
| box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5); | |
| position: relative; | |
| max-width: 95vw; | |
| } | |
| /* Main Game Canvas */ | |
| .canvas-container { | |
| position: relative; | |
| border: 2px solid var(--surface-color); | |
| border-radius: 8px; | |
| box-shadow: inset 0 0 20px rgba(0,0,0,0.5); | |
| background-color: #000; | |
| } | |
| canvas { | |
| display: block; | |
| border-radius: 6px; | |
| } | |
| /* Sidebar UI */ | |
| .sidebar { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1.5rem; | |
| min-width: 200px; | |
| } | |
| .panel { | |
| background: rgba(15, 23, 42, 0.6); | |
| padding: 1rem; | |
| border-radius: 12px; | |
| border: 1px solid var(--glass-border); | |
| } | |
| .panel-title { | |
| font-size: 0.85rem; | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| color: #94a3b8; | |
| margin-bottom: 0.5rem; | |
| } | |
| .stat-value { | |
| font-size: 1.8rem; | |
| font-weight: 700; | |
| color: var(--primary-color); | |
| text-shadow: 0 0 10px rgba(56, 189, 248, 0.3); | |
| } | |
| /* Next Piece Preview */ | |
| #next-canvas { | |
| background: transparent; | |
| margin: 0 auto; | |
| border-radius: 4px; | |
| } | |
| /* Controls Guide */ | |
| .controls { | |
| font-size: 0.8rem; | |
| color: #64748b; | |
| line-height: 1.6; | |
| } | |
| .key { | |
| display: inline-block; | |
| background: var(--surface-color); | |
| padding: 2px 6px; | |
| border-radius: 4px; | |
| color: var(--text-color); | |
| font-family: monospace; | |
| border: 1px solid var(--glass-border); | |
| } | |
| /* Buttons */ | |
| .btn { | |
| width: 100%; | |
| padding: 12px; | |
| border: none; | |
| border-radius: 8px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| text-transform: uppercase; | |
| font-size: 0.9rem; | |
| } | |
| .btn-primary { | |
| background: linear-gradient(135deg, var(--primary-color), #2563eb); | |
| color: white; | |
| box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3); | |
| } | |
| .btn-primary:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 16px rgba(37, 99, 235, 0.4); | |
| } | |
| .btn-primary:active { | |
| transform: translateY(0); | |
| } | |
| /* Overlay (Start/Game Over) */ | |
| .overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(15, 23, 42, 0.85); | |
| backdrop-filter: blur(8px); | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| border-radius: 24px; | |
| z-index: 10; | |
| transition: opacity 0.3s ease; | |
| } | |
| .overlay.hidden { | |
| opacity: 0; | |
| pointer-events: none; | |
| } | |
| .overlay h1 { | |
| font-size: 3rem; | |
| margin-bottom: 0.5rem; | |
| background: linear-gradient(to right, var(--primary-color), var(--accent-color)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| text-shadow: 0 0 30px rgba(56, 189, 248, 0.2); | |
| } | |
| .overlay p { | |
| font-size: 1.2rem; | |
| color: #cbd5e1; | |
| margin-bottom: 2rem; | |
| } | |
| /* ========================================= | |
| 3. RESPONSIVE DESIGN | |
| ========================================= */ | |
| @media (max-width: 768px) { | |
| .game-wrapper { | |
| flex-direction: column; | |
| padding: 1rem; | |
| gap: 1rem; | |
| } | |
| .sidebar { | |
| flex-direction: row; | |
| flex-wrap: wrap; | |
| justify-content: center; | |
| min-width: auto; | |
| } | |
| .panel { | |
| padding: 0.5rem 1rem; | |
| flex: 1; | |
| } | |
| .controls { | |
| display: none; /* Hide complex controls on mobile to save space */ | |
| } | |
| canvas { | |
| max-height: 60vh; | |
| width: auto; | |
| } | |
| } | |
| /* Footer Credit */ | |
| footer { | |
| margin-top: 1rem; | |
| font-size: 0.75rem; | |
| color: #475569; | |
| } | |
| footer a { | |
| color: var(--primary-color); | |
| text-decoration: none; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="game-wrapper"> | |
| <!-- Game Canvas --> | |
| <div class="canvas-container"> | |
| <canvas id="tetris" width="300" height="600"></canvas> | |
| <!-- Start / Game Over Overlay --> | |
| <div id="overlay" class="overlay"> | |
| <h1 id="overlay-title">NEON TETRIS</h1> | |
| <p id="overlay-msg">Press Start to Play</p> | |
| <button id="start-btn" class="btn btn-primary">Start Game</button> | |
| </div> | |
| </div> | |
| <!-- Sidebar --> | |
| <aside class="sidebar"> | |
| <!-- Score Panel --> | |
| <div class="panel"> | |
| <div class="panel-title">Score</div> | |
| <div id="score" class="stat-value">0</div> | |
| </div> | |
| <!-- Level Panel --> | |
| <div class="panel"> | |
| <div class="panel-title">Level</div> | |
| <div id="level" class="stat-value">1</div> | |
| </div> | |
| <!-- Lines Panel --> | |
| <div class="panel"> | |
| <div class="panel-title">Lines</div> | |
| <div id="lines" class="stat-value">0</div> | |
| </div> | |
| <!-- Next Piece --> | |
| <div class="panel" style="text-align: center;"> | |
| <div class="panel-title">Next</div> | |
| <canvas id="next-canvas" width="100" height="100"></canvas> | |
| </div> | |
| <!-- Controls --> | |
| <div class="panel controls"> | |
| <div class="panel-title">Controls</div> | |
| <p><span class="key">←</span> <span class="key">→</span> Move</p> | |
| <p><span class="key">↑</span> Rotate</p> | |
| <p><span class="key">↓</span> Soft Drop</p> | |
| <p><span class="key">Space</span> Hard Drop</p> | |
| <p><span class="key">P</span> Pause</p> | |
| </div> | |
| <button id="reset-btn" class="btn btn-primary" style="background: var(--accent-color);">Reset</button> | |
| </aside> | |
| </div> | |
| <footer> | |
| Built with <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank">anycoder</a> | |
| </footer> | |
| <script> | |
| /** | |
| * TETRIS GAME ENGINE | |
| * Uses HTML5 Canvas for rendering. | |
| * Implements standard Tetris mechanics with modern JS classes. | |
| */ | |
| // --- Configuration --- | |
| const COLS = 10; | |
| const ROWS = 20; | |
| const BLOCK_SIZE = 30; // Base size, scaled by canvas | |
| const COLORS = [ | |
| null, | |
| '#00f0f0', // I - Cyan | |
| '#0000f0', // J - Blue | |
| '#f0a000', // L - Orange | |
| '#f0f000', // O - Yellow | |
| '#00f000', // S - Green | |
| '#a000f0', // T - Purple | |
| '#f00000' // Z - Red | |
| ]; | |
| // --- Shapes Definitions --- | |
| const PIECES = [ | |
| [], | |
| [[0, 0, 0, 0], [1, 1, 1, 1], [0, 0, 0, 0], [0, 0, 0, 0]], // I | |
| [[2, 0, 0], [2, 2, 2], [0, 0, 0]], // J | |
| [[0, 0, 3], [3, 3, 3], [0, 0, 0]], // L | |
| [[4, 4], [4, 4]], // O | |
| [[0, 5, 5], [5, 5, 0], [0, 0, 0]], // S | |
| [[0, 6, 0], [6, 6, 6], [0, 0, 0]], // T | |
| [[7, 7, 0], [0, 7, 7], [0, 0, 0]] // Z | |
| ]; | |
| // --- State Management --- | |
| const canvas = document.getElementById('tetris'); | |
| const context = canvas.getContext('2d'); | |
| const nextCanvas = document.getElementById('next-canvas'); | |
| const nextContext = nextCanvas.getContext('2d'); | |
| // Scale canvas for high DPI | |
| function scaleCanvas(c, ctx, w, h) { | |
| const dpr = window.devicePixelRatio || 1; | |
| c.width = w * dpr; | |
| c.height = h * dpr; | |
| c.style.width = `${w}px`; | |
| c.style.height = `${h}px`; | |
| ctx.scale(dpr, dpr); | |
| } | |
| scaleCanvas(canvas, context, COLS * BLOCK_SIZE, ROWS * BLOCK_SIZE); | |
| scaleCanvas(nextCanvas, nextContext, 4 * BLOCK_SIZE, 4 * BLOCK_SIZE); | |
| let arena = createMatrix(COLS, ROWS); | |
| let player = { | |
| pos: {x: 0, y: 0}, | |
| matrix: null, | |
| score: 0, | |
| lines: 0, | |
| level: 1, | |
| next: null | |
| }; | |
| let dropCounter = 0; | |
| let dropInterval = 1000; | |
| let lastTime = 0; | |
| let isPaused = false; | |
| let isGameOver = false; | |
| let requestId = null; | |
| // --- Core Functions --- | |
| function createMatrix(w, h) { | |
| const matrix = []; | |
| while (h--) { | |
| matrix.push(new Array(w).fill(0)); | |
| } | |
| return matrix; | |
| } | |
| function createPiece(type) { | |
| // Returns a deep copy of the piece definition | |
| return PIECES[type].map(row => [...row]); | |
| } | |
| function drawMatrix(matrix, offset, ctx = context) { | |
| matrix.forEach((row, y) => { | |
| row.forEach((value, x) => { | |
| if (value !== 0) { | |
| // Main Block | |
| ctx.fillStyle = COLORS[value]; | |
| ctx.fillRect( | |
| (x + offset.x) * BLOCK_SIZE, | |
| (y + offset.y) * BLOCK_SIZE, | |
| BLOCK_SIZE, | |
| BLOCK_SIZE | |
| ); | |
| // Inner Bevel/Highlight (Pseudo-3D) | |
| ctx.lineWidth = 2; | |
| ctx.strokeStyle = 'rgba(255,255,255,0.5)'; | |
| ctx.strokeRect( | |
| (x + offset.x) * BLOCK_SIZE, | |
| (y + offset.y) * BLOCK_SIZE, | |
| BLOCK_SIZE, | |
| BLOCK_SIZE | |
| ); | |
| ctx.fillStyle = 'rgba(0,0,0,0.2)'; | |
| ctx.fillRect( | |
| (x + offset.x) * BLOCK_SIZE + 5, | |
| (y + offset.y) * BLOCK_SIZE + 5, | |
| BLOCK_SIZE - 10, | |
| BLOCK_SIZE - 10 | |
| ); | |
| } | |
| }); | |
| }); | |
| } | |
| function draw() { | |
| // Fill background with semi-transparent black for trail effect? No, clean clear is better for Tetris. | |
| context.fillStyle = '#000'; | |
| context.fillRect(0, 0, canvas.width, canvas.height); | |
| drawMatrix(arena, {x: 0, y: 0}); | |
| drawMatrix(player.matrix, player.pos); | |
| } | |
| function drawNext() { | |
| nextContext.fillStyle = '#1e293b'; // Match surface color | |
| nextContext.fillRect(0, 0, nextCanvas.width, nextCanvas.height); | |
| // Center the piece | |
| const offsetX = (4 - player.next[0].length) / 2; | |
| const offsetY = (4 - player.next.length) / 2; | |
| drawMatrix(player.next, {x: offsetX, y: offsetY}, nextContext); | |
| } | |
| function merge(arena, player) { | |
| player.matrix.forEach((row, y) => { | |
| row.forEach((value, x) => { | |
| if (value !== 0) { | |
| arena[y + player.pos.y][x + player.pos.x] = value; | |
| } | |
| }); | |
| }); | |
| } | |
| function rotate(matrix, dir) { | |
| for (let y = 0; y < matrix.length; ++y) { | |
| for (let x = 0; x < y; ++x) { | |
| [matrix[x][y], matrix[y][x]] = [matrix[y][x], matrix[x][y]]; | |
| } | |
| } | |
| if (dir > 0) { | |
| matrix.forEach(row => row.reverse()); | |
| } else { | |
| matrix.reverse(); | |
| } | |
| } | |
| function playerRotate(dir) { | |
| const pos = player.pos.x; | |
| let offset = 1; | |
| rotate(player.matrix, dir); | |
| // Wall kick logic (basic) | |
| while (collide(arena, player)) { | |
| player.pos.x += offset; | |
| offset = -(offset + (offset > 0 ? 1 : -1)); | |
| if (offset > player.matrix[0].length) { | |
| rotate(player.matrix, -dir); // Rotate back if fails | |
| player.pos.x = pos; | |
| return; | |
| } | |
| } | |
| } | |
| function collide(arena, player) { | |
| const [m, o] = [player.matrix, player.pos]; | |
| for (let y = 0; y < m.length; ++y) { | |
| for (let x = 0; x < m[y].length; ++x) { | |
| if (m[y][x] !== 0 && | |
| (arena[y + o.y] && arena[y + o.y][x + o.x]) !== 0) { | |
| return true; | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| function playerReset() { | |
| if (player.next === null) { | |
| player.next = createPiece((Math.random() * 7 | 0) + 1); | |
| } | |
| player.matrix = player.next; | |
| player.next = createPiece((Math.random() * 7 | 0) + 1); | |
| drawNext(); | |
| player.pos.y = 0; | |
| player.pos.x = (arena[0].length / 2 | 0) - (player.matrix[0].length / 2 | 0); | |
| if (collide(arena, player)) { | |
| gameOver(); | |
| } | |
| } | |
| function arenaSweep() { | |
| let rowCount = 0; | |
| outer: for (let y = arena.length - 1; y > 0; --y) { | |
| for (let x = 0; x < arena[y].length; ++x) { | |
| if (arena[y][x] === 0) { | |
| continue outer; | |
| } | |
| } | |
| const row = arena.splice(y, 1)[0].fill(0); | |
| arena.unshift(row); | |
| ++y; | |
| rowCount++; | |
| } | |
| if (rowCount > 0) { | |
| // Scoring: 100, 300, 500, 800 | |
| const lineScores = [0, 100, 300, 500, 800]; | |
| player.score += lineScores[rowCount] * player.level; | |
| player.lines += rowCount; | |
| player.level = Math.floor(player.lines / 10) + 1; | |
| // Speed up | |
| dropInterval = Math.max(100, 1000 - (player.level - 1) * 100); | |
| updateScore(); | |
| } | |
| } | |
| function playerDrop() { | |
| player.pos.y++; | |
| if (collide(arena, player)) { | |
| player.pos.y--; | |
| merge(arena, player); | |
| playerReset(); | |
| arenaSweep(); | |
| updateScore(); | |
| } | |
| dropCounter = 0; | |
| } | |
| function playerMove(dir) { | |
| player.pos.x += dir; | |
| if (collide(arena, player)) { | |
| player.pos.x -= dir; | |
| } | |
| } | |
| function playerHardDrop() { | |
| while (!collide(arena, player)) { | |
| player.pos.y++; | |
| } | |
| player.pos.y--; // Back up one step | |
| merge(arena, player); | |
| playerReset(); | |
| arenaSweep(); | |
| updateScore(); | |
| dropCounter = 0; | |
| } | |
| function updateScore() { | |
| document.getElementById('score').innerText = player.score; | |
| document.getElementById('level').innerText = player.level; | |
| document.getElementById('lines').innerText = player.lines; | |
| } | |
| function gameOver() { | |
| isGameOver = true; | |
| cancelAnimationFrame(requestId); | |
| const overlay = document.getElementById('overlay'); | |
| const title = document.getElementById('overlay-title'); | |
| const msg = document.getElementById('overlay-msg'); | |
| const btn = document.getElementById('start-btn'); | |
| title.innerText = "GAME OVER"; | |
| title.style.background = "linear-gradient(to right, #f472b6, #ef4444)"; | |
| title.style.webkitBackgroundClip = "text"; | |
| title.style.webkitTextFillColor = "transparent"; | |
| msg.innerText = `Final Score: ${player.score}`; | |
| btn.innerText = "Try Again"; | |
| overlay.classList.remove('hidden'); | |
| } | |
| function resetGame() { | |
| arena.forEach(row => row.fill(0)); | |
| player.score = 0; | |
| player.lines = 0; | |
| player.level = 1; | |
| player.next = null; | |
| dropInterval = 1000; | |
| updateScore(); | |
| playerReset(); | |
| isGameOver = false; | |
| isPaused = false; | |
| } | |
| function update(time = 0) { | |
| if (isPaused || isGameOver) return; | |
| const deltaTime = time - lastTime; | |
| lastTime = time; | |
| dropCounter += deltaTime; | |
| if (dropCounter > dropInterval) { | |
| playerDrop(); | |
| } | |
| draw(); | |
| requestId = requestAnimationFrame(update); | |
| } | |
| // --- Input Handling --- | |
| document.addEventListener('keydown', event => { | |
| if (isGameOver) return; | |
| // Prevent default scrolling for game keys | |
| if(["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"," "].indexOf(event.code) > -1) { | |
| event.preventDefault(); | |
| } | |
| if (event.code === 'KeyP') { | |
| isPaused = !isPaused; | |
| if (!isPaused) { | |
| lastTime = performance.now(); // Reset time to prevent jump | |
| update(); | |
| } | |
| return; | |
| } | |
| if (isPaused) return; | |
| if (event.code === 'ArrowLeft') { | |
| playerMove(-1); | |
| } else if (event.code === 'ArrowRight') { | |
| playerMove(1); | |
| } else if (event.code === 'ArrowDown') { | |
| playerDrop(); | |
| } else if (event.code === 'ArrowUp') { | |
| playerRotate(1); | |
| } else if (event.code === 'Space') { | |
| playerHardDrop(); | |
| } | |
| }); | |
| document.getElementById('start-btn').addEventListener('click', () => { | |
| document.getElementById('overlay').classList.add('hidden'); | |
| resetGame(); | |
| update(); | |
| }); | |
| document.getElementById('reset-btn').addEventListener('click', () => { | |
| // If game is running, pause it | |
| if (!isGameOver && !isPaused) { | |
| isPaused = true; | |
| } | |
| resetGame(); | |
| document.getElementById('overlay').classList.add('hidden'); | |
| update(); | |
| }); | |
| // Initial Draw (Empty Board) | |
| context.fillStyle = '#000'; | |
| context.fillRect(0, 0, canvas.width, canvas.height); | |
| </script> | |
| </body> | |
| </html> |