Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>NEON-TRIS | Cyberpunk Tetris</title> | |
<style> | |
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); | |
:root { | |
--neon-pink: #ff2a6d; | |
--neon-blue: #05d9e8; | |
--neon-purple: #d300c5; | |
--neon-green: #00ff9d; | |
--dark-bg: #0d0221; | |
--grid-bg: rgba(13, 2, 33, 0.7); | |
} | |
* { | |
box-sizing: border-box; | |
margin: 0; | |
padding: 0; | |
} | |
body { | |
font-family: 'Press Start 2P', cursive; | |
background-color: var(--dark-bg); | |
color: var(--neon-blue); | |
overflow: hidden; | |
height: 100vh; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
position: relative; | |
} | |
.cyber-grid { | |
position: fixed; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background: | |
linear-gradient(rgba(5, 217, 232, 0.1) 1px, transparent 1px), | |
linear-gradient(90deg, rgba(5, 217, 232, 0.1) 1px, transparent 1px); | |
background-size: 30px 30px; | |
z-index: -1; | |
opacity: 0.3; | |
} | |
.cyber-lines { | |
position: fixed; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background: | |
linear-gradient(135deg, var(--neon-purple) 0%, transparent 50%), | |
linear-gradient(-135deg, var(--neon-green) 0%, transparent 50%); | |
z-index: -2; | |
opacity: 0.2; | |
} | |
.game-container { | |
display: flex; | |
gap: 30px; | |
align-items: flex-start; | |
position: relative; | |
} | |
#game-board { | |
border: 5px solid var(--neon-purple); | |
border-radius: 3px; | |
box-shadow: 0 0 20px var(--neon-purple), | |
0 0 40px rgba(211, 0, 197, 0.3); | |
background-color: var(--grid-bg); | |
} | |
.side-panel { | |
display: flex; | |
flex-direction: column; | |
gap: 30px; | |
min-width: 200px; | |
} | |
.panel { | |
border: 3px solid var(--neon-blue); | |
border-radius: 3px; | |
padding: 15px; | |
box-shadow: 0 0 15px var(--neon-blue), | |
0 0 30px rgba(5, 217, 232, 0.3); | |
background-color: var(--grid-bg); | |
text-align: center; | |
} | |
h1 { | |
font-size: 24px; | |
margin-bottom: 15px; | |
color: var(--neon-pink); | |
text-shadow: 0 0 10px var(--neon-pink); | |
letter-spacing: 2px; | |
} | |
.score-display { | |
font-size: 22px; | |
margin: 15px 0; | |
color: var(--neon-green); | |
text-shadow: 0 0 8px var(--neon-green); | |
} | |
.level-display { | |
font-size: 18px; | |
margin: 10px 0; | |
color: var(--neon-blue); | |
text-shadow: 0 0 8px var(--neon-blue); | |
} | |
#next-piece { | |
width: 120px; | |
height: 120px; | |
margin: 0 auto; | |
position: relative; | |
} | |
button { | |
background-color: transparent; | |
border: 2px solid var(--neon-green); | |
color: var(--neon-green); | |
font-family: 'Press Start 2P', cursive; | |
padding: 10px; | |
margin: 5px 0; | |
cursor: pointer; | |
transition: all 0.3s; | |
text-shadow: 0 0 5px var(--neon-green); | |
} | |
button:hover { | |
background-color: var(--neon-green); | |
color: var(--dark-bg); | |
box-shadow: 0 0 15px var(--neon-green); | |
} | |
.controls { | |
margin-top: 20px; | |
} | |
.controls p { | |
font-size: 12px; | |
margin: 10px 0; | |
color: var(--neon-blue); | |
line-height: 1.6; | |
} | |
.neon-text { | |
animation: flicker 1.5s infinite alternate; | |
} | |
.game-over { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background-color: rgba(13, 2, 33, 0.9); | |
display: none; | |
flex-direction: column; | |
justify-content: center; | |
align-items: center; | |
z-index: 10; | |
} | |
.game-over h1 { | |
font-size: 36px; | |
margin-bottom: 30px; | |
color: var(--neon-pink); | |
text-shadow: 0 0 15px var(--neon-pink); | |
} | |
.scanline { | |
position: fixed; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background: linear-gradient( | |
to bottom, | |
transparent 0%, | |
rgba(0, 255, 157, 0.1) 50%, | |
transparent 100% | |
); | |
background-size: 100% 8px; | |
pointer-events: none; | |
z-index: 0; | |
animation: scanline 8s linear infinite; | |
} | |
@keyframes flicker { | |
0%, 19%, 21%, 23%, 25%, 54%, 56%, 100% { | |
text-shadow: 0 0 10px var(--neon-pink), | |
0 0 20px var(--neon-pink), | |
0 0 30px var(--neon-purple), | |
0 0 40px rgba(255, 42, 109, 0.5); | |
} | |
20%, 24%, 55% { | |
text-shadow: none; | |
} | |
} | |
@keyframes scanline { | |
from { | |
transform: translateY(-100%); | |
} | |
to { | |
transform: translateY(100%); | |
} | |
} | |
.glitch { | |
position: fixed; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2 2"><rect width="1" height="1" fill="%2305d9e8" opacity="0.05"/></svg>') repeat; | |
z-index: -1; | |
opacity: 0.2; | |
animation: glitch 10s infinite; | |
pointer-events: none; | |
mix-blend-mode: overlay; | |
} | |
@keyframes glitch { | |
0% { transform: translate(0); } | |
20% { transform: translate(-1px, 1px); } | |
40% { transform: translate(-1px, -1px); } | |
60% { transform: translate(1px, 1px); } | |
80% { transform: translate(1px, -1px); } | |
100% { transform: translate(0); } | |
} | |
/* Responsive adjustments */ | |
@media (max-width: 768px) { | |
.game-container { | |
flex-direction: column; | |
align-items: center; | |
} | |
.side-panel { | |
min-width: 300px; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="cyber-grid"></div> | |
<div class="cyber-lines"></div> | |
<div class="scanline"></div> | |
<div class="glitch"></div> | |
<div class="game-container"> | |
<canvas id="game-board" width="300" height="600"></canvas> | |
<div class="side-panel"> | |
<div class="panel"> | |
<h1 class="neon-text">NEON-TRIS</h1> | |
<div class="score-display" id="score">00000</div> | |
<div class="level-display">LEVEL: <span id="level">1</span></div> | |
<div class="level-display">LINES: <span id="lines">0</span></div> | |
</div> | |
<div class="panel"> | |
<h1>NEXT</h1> | |
<canvas id="next-piece" width="120" height="120"></canvas> | |
</div> | |
<div class="panel"> | |
<button id="start-btn">NEW GAME</button> | |
<button id="pause-btn">PAUSE</button> | |
<div class="controls"> | |
<p>← → : MOVE</p> | |
<p>↑ : ROTATE</p> | |
<p>↓ : SOFT DROP</p> | |
<p>SPACE : HARD DROP</p> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="game-over" id="game-over"> | |
<h1 class="neon-text">GAME OVER</h1> | |
<button id="restart-btn">RETRY</button> | |
</div> | |
<audio id="move-sound" src="data:audio/wav;base64,UklGRl9vT19XQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YU..."></audio> | |
<audio id="rotate-sound" src="data:audio/wav;base64,UklGRl9vT19XQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0Y..."></audio> | |
<audio id="drop-sound" src="data:audio/wav;base64,UklGRl9vT19XQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0Y..."></audio> | |
<audio id="clear-sound" src="data:audio/wav;base64,UklGRl9vT19XQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0Y..."></audio> | |
<audio id="gameover-sound" src="data:audio/wav;base64,UklGRl9vT19XQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0Y..."></audio> | |
<script> | |
// 8-bit sound effects as base64 encoded strings | |
function createSound(src) { | |
const audio = new Audio(); | |
audio.src = src; | |
return audio; | |
} | |
document.addEventListener('DOMContentLoaded', () => { | |
// Game variables | |
const canvas = document.getElementById('game-board'); | |
const ctx = canvas.getContext('2d'); | |
const nextCanvas = document.getElementById('next-piece'); | |
const nextCtx = nextCanvas.getContext('2d'); | |
// Sound effects (simplified base64 strings - in a real app these would be full sound files) | |
const sounds = { | |
move: document.getElementById('move-sound'), | |
rotate: document.getElementById('rotate-sound'), | |
drop: document.getElementById('drop-sound'), | |
clear: document.getElementById('clear-sound'), | |
gameover: document.getElementById('gameover-sound'), | |
}; | |
// For demonstration, we'll just play short beeps | |
function playSound(type) { | |
try { | |
sounds[type].currentTime = 0; | |
sounds[type].play(); | |
} catch(e) { | |
console.log("Sound error:", e); | |
// Fallback to simple beep | |
const oscillator = new (window.AudioContext || window.webkitAudioContext)().createOscillator(); | |
const gainNode = new (window.AudioContext || window.webkitAudioContext)().createGain(); | |
oscillator.connect(gainNode); | |
gainNode.connect((window.AudioContext || window.webkitAudioContext)().destination); | |
if (type === 'move') { | |
oscillator.frequency.value = 200; | |
gainNode.gain.value = 0.1; | |
} else if (type === 'rotate') { | |
oscillator.frequency.value = 300; | |
gainNode.gain.value = 0.1; | |
} else if (type === 'drop') { | |
oscillator.frequency.value = 100; | |
gainNode.gain.value = 0.2; | |
} else if (type === 'clear') { | |
oscillator.frequency.value = 500; | |
gainNode.gain.value = 0.3; | |
} else if (type === 'gameover') { | |
oscillator.frequency.value = 150; | |
gainNode.gain.value = 0.3; | |
} | |
oscillator.start(); | |
oscillator.stop((window.AudioContext || window.webkitAudioContext)().currentTime + 0.1); | |
} | |
} | |
// Game constants | |
const COLS = 10; | |
const ROWS = 20; | |
const BLOCK_SIZE = 30; | |
const COLORS = [ | |
'rgba(255, 42, 109, 0)', | |
'rgba(255, 42, 109, 1)', // I | |
'rgba(5, 217, 232, 1)', // J | |
'rgba(211, 0, 197, 1)', // L | |
'rgba(0, 255, 157, 1)', // O | |
'rgba(255, 204, 0, 1)', // S | |
'rgba(255, 128, 0, 1)', // T | |
'rgba(180, 0, 255, 1)' // Z | |
]; | |
const SHAPES = [ | |
[], | |
[[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 | |
[[0, 4, 4], [0, 4, 4], [0, 0, 0]], // 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 | |
]; | |
// Game state | |
let board = Array(ROWS).fill().map(() => Array(COLS).fill(0)); | |
let piece = null; | |
let nextPiece = null; | |
let score = 0; | |
let lines = 0; | |
let level = 1; | |
let gameOver = false; | |
let isPaused = false; | |
let dropCounter = 0; | |
let dropInterval = 1000; | |
let lastTime = 0; | |
// UI elements | |
const scoreElement = document.getElementById('score'); | |
const levelElement = document.getElementById('level'); | |
const linesElement = document.getElementById('lines'); | |
const startButton = document.getElementById('start-btn'); | |
const pauseButton = document.getElementById('pause-btn'); | |
const restartButton = document.getElementById('restart-btn'); | |
const gameOverScreen = document.getElementById('game-over'); | |
// Initialize the game | |
function init() { | |
board = Array(ROWS).fill().map(() => Array(COLS).fill(0)); | |
score = 0; | |
lines = 0; | |
level = 1; | |
gameOver = false; | |
isPaused = false; | |
dropInterval = 1000; | |
updateScore(); | |
createNextPiece(); | |
spawnPiece(); | |
draw(); | |
} | |
// Create a new random piece | |
function createNextPiece() { | |
const shapeId = Math.floor(Math.random() * 7) + 1; | |
const shape = SHAPES[shapeId]; | |
nextPiece = { | |
shapeId, | |
shape, | |
pos: {x: 0, y: 0}, | |
width: shape[0].length, | |
height: shape.length | |
}; | |
drawNextPiece(); | |
} | |
// Draw the next piece preview | |
function drawNextPiece() { | |
nextCtx.clearRect(0, 0, nextCanvas.width, nextCanvas.height); | |
if (!nextPiece) return; | |
nextCtx.fillStyle = COLORS[nextPiece.shapeId]; | |
nextCtx.strokeStyle = '#fff'; | |
nextCtx.lineWidth = 1; | |
// Center the piece in the preview | |
const offsetX = (nextCanvas.width - nextPiece.width * (BLOCK_SIZE / 2)) / 2; | |
const offsetY = (nextCanvas.height - nextPiece.height * (BLOCK_SIZE / 2)) / 2; | |
for (let y = 0; y < nextPiece.height; y++) { | |
for (let x = 0; x < nextPiece.width; x++) { | |
if (nextPiece.shape[y][x]) { | |
drawBlock( | |
nextCtx, | |
x * (BLOCK_SIZE / 2) + offsetX, | |
y * (BLOCK_SIZE / 2) + offsetY, | |
BLOCK_SIZE / 2, | |
COLORS[nextPiece.shapeId], | |
true | |
); | |
} | |
} | |
} | |
} | |
// Spawn the next piece | |
function spawnPiece() { | |
piece = { | |
shapeId: nextPiece.shapeId, | |
shape: nextPiece.shape, | |
pos: { | |
x: Math.floor(COLS / 2) - Math.floor(nextPiece.width / 2), | |
y: 0 | |
}, | |
width: nextPiece.width, | |
height: nextPiece.height | |
}; | |
createNextPiece(); | |
// Check if game over | |
if (collision()) { | |
gameOver = true; | |
playSound('gameover'); | |
gameOverScreen.style.display = 'flex'; | |
} | |
} | |
// Rotate the current piece | |
function rotatePiece() { | |
if (isPaused || gameOver) return; | |
const rotated = Array(piece.width).fill().map(() => Array(piece.height).fill(0)); | |
for (let y = 0; y < piece.height; y++) { | |
for (let x = 0; x < piece.width; x++) { | |
rotated[x][piece.height - 1 - y] = piece.shape[y][x]; | |
} | |
} | |
const originalShape = piece.shape; | |
const originalWidth = piece.width; | |
const originalHeight = piece.height; | |
piece.shape = rotated; | |
piece.width = rotated[0].length; | |
piece.height = rotated.length; | |
// Wall kick - adjust position if rotation causes collision | |
if (collision()) { | |
// Try moving left | |
piece.pos.x -= 1; | |
if (collision()) { | |
// Try moving right | |
piece.pos.x += 2; | |
if (collision()) { | |
// Revert rotation | |
piece.shape = originalShape; | |
piece.width = originalWidth; | |
piece.height = originalHeight; | |
return; | |
} | |
} | |
} | |
playSound('rotate'); | |
} | |
// Move the current piece | |
function movePiece(dir) { | |
if (isPaused || gameOver) return; | |
piece.pos.x += dir; | |
if (collision()) { | |
piece.pos.x -= dir; | |
return false; | |
} | |
playSound('move'); | |
return true; | |
} | |
// Hard drop the current piece | |
function hardDrop() { | |
if (isPaused || gameOver) return; | |
while (!collision()) { | |
piece.pos.y++; | |
} | |
piece.pos.y--; | |
dropCounter = dropInterval; | |
merge(); | |
spawnPiece(); | |
playSound('drop'); | |
} | |
// Merge the current piece with the board | |
function merge() { | |
for (let y = 0; y < piece.height; y++) { | |
for (let x = 0; x < piece.width; x++) { | |
if (piece.shape[y][x]) { | |
board[y + piece.pos.y][x + piece.pos.x] = piece.shapeId; | |
} | |
} | |
} | |
// Check for cleared lines | |
clearLines(); | |
updateScore(); | |
} | |
// Clear completed lines | |
function clearLines() { | |
let linesCleared = 0; | |
for (let y = ROWS - 1; y >= 0; y--) { | |
if (board[y].every(cell => cell > 0)) { | |
// Remove the line | |
board.splice(y, 1); | |
// Add a new empty line at the top | |
board.unshift(Array(COLS).fill(0)); | |
linesCleared++; | |
y++; // Check this row again since rows shift down | |
} | |
} | |
if (linesCleared > 0) { | |
lines += linesCleared; | |
playSound('clear'); | |
// Level up every 10 lines | |
level = Math.floor(lines / 10) + 1; | |
dropInterval = Math.max(100, 1000 - (level - 1) * 100); | |
} | |
} | |
// Check if the current piece collides with the board boundaries or other pieces | |
function collision() { | |
for (let y = 0; y < piece.height; y++) { | |
for (let x = 0; x < piece.width; x++) { | |
if (piece.shape[y][x]) { | |
const boardX = piece.pos.x + x; | |
const boardY = piece.pos.y + y; | |
if ( | |
boardX < 0 || | |
boardX >= COLS || | |
boardY >= ROWS || | |
(boardY >= 0 && board[boardY][boardX]) | |
) { | |
return true; | |
} | |
} | |
} | |
} | |
return false; | |
} | |
// Update game score and level display | |
function updateScore() { | |
// Simple scoring system | |
const newScore = lines * 100 * level; | |
scoreElement.textContent = String(newScore).padStart(5, '0'); | |
levelElement.textContent = level; | |
linesElement.textContent = lines; | |
} | |
// Draw a single block with neon glow effect | |
function drawBlock(ctx, x, y, size, color, isGhost = false) { | |
const realX = x * size; | |
const realY = y * size; | |
// Inner block | |
if (isGhost) { | |
ctx.fillStyle = color.replace('1)', '0.3)'); | |
} else { | |
ctx.fillStyle = color; | |
} | |
ctx.fillRect(realX, realY, size, size); | |
// Neon border effect | |
ctx.strokeStyle = isGhost ? color.replace('1)', '0.5)') : color; | |
ctx.lineWidth = isGhost ? 1 : 2; | |
ctx.strokeRect(realX, realY, size, size); | |
// Glow effect | |
if (!isGhost) { | |
ctx.shadowColor = color; | |
ctx.shadowBlur = 15; | |
ctx.fillRect(realX, realY, size, size); | |
ctx.shadowBlur = 0; | |
// Inner highlight | |
ctx.fillStyle = color.replace('1)', '0.7)'); | |
ctx.fillRect(realX + size * 0.2, realY + size * 0.2, size * 0.6, size * 0.6); | |
} | |
} | |
// Draw the game board and current piece | |
function draw() { | |
// Clear the canvas | |
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(ctx, x, y, BLOCK_SIZE, COLORS[board[y][x]]); | |
} | |
} | |
} | |
// Draw the current piece | |
if (piece) { | |
// Draw ghost piece | |
const ghostY = piece.pos.y; | |
while (!collision()) { | |
piece.pos.y++; | |
} | |
piece.pos.y--; | |
const currentY = piece.pos.y; | |
piece.pos.y = ghostY; | |
for (let y = 0; y < piece.height; y++) { | |
for (let x = 0; x < piece.width; x++) { | |
if (piece.shape[y][x]) { | |
drawBlock(ctx, | |
x + piece.pos.x, | |
y + currentY, | |
BLOCK_SIZE, | |
COLORS[piece.shapeId], | |
true | |
); | |
} | |
} | |
} | |
// Draw actual piece | |
for (let y = 0; y < piece.height; y++) { | |
for (let x = 0; x < piece.width; x++) { | |
if (piece.shape[y][x]) { | |
drawBlock(ctx, | |
x + piece.pos.x, | |
y + piece.pos.y, | |
BLOCK_SIZE, | |
COLORS[piece.shapeId] | |
); | |
} | |
} | |
} | |
} | |
} | |
// Game loop | |
function update(time = 0) { | |
if (gameOver) return; | |
if (isPaused) { | |
window.requestAnimationFrame(update); | |
return; | |
} | |
const deltaTime = time - lastTime; | |
lastTime = time; | |
dropCounter += deltaTime; | |
if (dropCounter > dropInterval) { | |
piece.pos.y++; | |
if (collision()) { | |
piece.pos.y--; | |
merge(); | |
spawnPiece(); | |
} | |
dropCounter = 0; | |
} | |
draw(); | |
window.requestAnimationFrame(update); | |
} | |
// Event listeners | |
document.addEventListener('keydown', event => { | |
if (gameOver) return; | |
switch (event.key) { | |
case 'ArrowLeft': | |
movePiece(-1); | |
break; | |
case 'ArrowRight': | |
movePiece(1); | |
break; | |
case 'ArrowDown': | |
piece.pos.y++; | |
if (collision()) { | |
piece.pos.y--; | |
merge(); | |
spawnPiece(); | |
} | |
playSound('move'); | |
dropCounter = 0; | |
break; | |
case 'ArrowUp': | |
rotatePiece(); | |
break; | |
case ' ': | |
hardDrop(); | |
break; | |
case 'p': | |
togglePause(); | |
break; | |
} | |
}); | |
function togglePause() { | |
if (gameOver) return; | |
isPaused = !isPaused; | |
pauseButton.textContent = isPaused ? 'RESUME' : 'PAUSE'; | |
if (!isPaused) { | |
lastTime = performance.now(); | |
update(); | |
} | |
} | |
// Button event listeners | |
startButton.addEventListener('click', () => { | |
init(); | |
lastTime = performance.now(); | |
update(); | |
}); | |
pauseButton.addEventListener('click', togglePause); | |
restartButton.addEventListener('click', () => { | |
init(); | |
gameOverScreen.style.display = 'none'; | |
lastTime = performance.now(); | |
update(); | |
}); | |
// Start the game | |
init(); | |
draw(); | |
}); | |
</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> |