-
-
Save shricodev/6303887f7a4071a05929785bc3c50d52 to your computer and use it in GitHub Desktop.
Blog - Tetris (Claude Opus 4)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" width=device-width, initial-scale=1.0"> | |
<title>Tetris</title> | |
<style> | |
* { | |
margin: 0; | |
padding: 0; | |
box-sizing: border-box; | |
} | |
body { | |
background: #0a0a0a; | |
color: #fff; | |
font-family: 'Arial', sans-serif; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
min-height: 100vh; | |
overflow: hidden; | |
} | |
#gameContainer { | |
display: flex; | |
gap: 20px; | |
align-items: center; | |
padding: 20px; | |
background: rgba(20, 20, 20, 0.9); | |
border-radius: 20px; | |
box-shadow: 0 0 40px rgba(0, 255, 255, 0.3); | |
} | |
.sidePanel { | |
display: flex; | |
flex-direction: column; | |
gap: 20px; | |
} | |
.infoBox { | |
background: rgba(0, 0, 0, 0.5); | |
border: 2px solid rgba(255, 255, 255, 0.1); | |
border-radius: 10px; | |
padding: 15px; | |
text-align: center; | |
min-width: 150px; | |
} | |
.infoBox h3 { | |
color: #0ff; | |
margin-bottom: 10px; | |
font-size: 14px; | |
text-transform: uppercase; | |
letter-spacing: 2px; | |
} | |
#holdCanvas, .nextCanvas { | |
background: rgba(0, 0, 0, 0.3); | |
border-radius: 5px; | |
margin: 10px auto; | |
} | |
#gameCanvas { | |
border: 2px solid #333; | |
border-radius: 10px; | |
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); | |
} | |
.score { | |
font-size: 24px; | |
font-weight: bold; | |
color: #fff; | |
text-shadow: 0 0 10px rgba(255, 255, 255, 0.5); | |
} | |
.controls { | |
margin-top: 20px; | |
font-size: 12px; | |
color: #888; | |
line-height: 1.8; | |
} | |
button { | |
background: #0ff; | |
color: #000; | |
border: none; | |
padding: 10px 20px; | |
border-radius: 5px; | |
font-weight: bold; | |
cursor: pointer; | |
transition: all 0.3s; | |
text-transform: uppercase; | |
letter-spacing: 1px; | |
margin: 5px; | |
} | |
button:hover { | |
background: #fff; | |
box-shadow: 0 0 15px rgba(0, 255, 255, 0.8); | |
} | |
.gameOver { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
background: rgba(0, 0, 0, 0.9); | |
padding: 40px; | |
border-radius: 20px; | |
text-align: center; | |
z-index: 1000; | |
display: none; | |
} | |
.gameOver h2 { | |
color: #f00; | |
font-size: 48px; | |
margin-bottom: 20px; | |
text-shadow: 0 0 20px rgba(255, 0, 0, 0.8); | |
} | |
#pauseOverlay { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
font-size: 48px; | |
color: #0ff; | |
text-shadow: 0 0 30px rgba(0, 255, 255, 0.8); | |
display: none; | |
z-index: 999; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="gameContainer"> | |
<div class="sidePanel"> | |
<div class="infoBox"> | |
<h3>Hold</h3> | |
<canvas id="holdCanvas" width="100" height="100"></canvas> | |
</div> | |
<div class="controls"> | |
<strong>Controls:</strong><br> | |
← → Move<br> | |
↑ Rotate<br> | |
↓ Soft Drop<br> | |
Space Hard Drop<br> | |
Shift Hold<br> | |
P Pause<br> | |
R Reset<br> | |
F Fullscreen | |
</div> | |
</div> | |
<canvas id="gameCanvas" width="300" height="600"></canvas> | |
<div class="sidePanel"> | |
<div class="infoBox"> | |
<h3>Score</h3> | |
<div class="score" id="score">0</div> | |
</div> | |
<div class="infoBox"> | |
<h3>Level</h3> | |
<div class="score" id="level">1</div> | |
</div> | |
<div class="infoBox"> | |
<h3>Lines</h3> | |
<div class="score" id="lines">0</div> | |
</div> | |
<div class="infoBox"> | |
<h3>Next</h3> | |
<canvas class="nextCanvas" width="100" height="60"></canvas> | |
<canvas class="nextCanvas" width="100" height="60"></canvas> | |
<canvas class="nextCanvas" width="100" height="60"></canvas> | |
</div> | |
<div> | |
<button onclick="togglePause()">Pause</button> | |
<button onclick="resetGame()">Reset</button> | |
</div> | |
</div> | |
</div> | |
<div id="pauseOverlay">PAUSED</div> | |
<div class="gameOver" id="gameOverScreen"> | |
<h2>GAME OVER</h2> | |
<div class="score" id="finalScore">0</div> | |
<br> | |
<button onclick="resetGame()">Play Again</button> | |
</div> | |
<script> | |
// Canvas and context setup | |
const canvas = document.getElementById('gameCanvas'); | |
const ctx = canvas.getContext('2d'); | |
const holdCanvas = document.getElementById('holdCanvas'); | |
const holdCtx = holdCanvas.getContext('2d'); | |
const nextCanvases = document.querySelectorAll('.nextCanvas'); | |
const nextCtxs = Array.from(nextCanvases).map(c => c.getContext('2d')); | |
// Game constants | |
const COLS = 10; | |
const ROWS = 20; | |
const BLOCK_SIZE = 30; | |
const COLORS = { | |
I: '#0ff', | |
O: '#ff0', | |
T: '#f0f', | |
S: '#0f0', | |
Z: '#f00', | |
J: '#00f', | |
L: '#fa0' | |
}; | |
// Tetromino definitions | |
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]] | |
}; | |
// Game state | |
let board = []; | |
let currentPiece = null; | |
let heldPiece = null; | |
let canHold = true; | |
let nextQueue = []; | |
let score = 0; | |
let level = 1; | |
let linesCleared = 0; | |
let dropTime = 0; | |
let lastTime = 0; | |
let isPaused = false; | |
let isGameOver = false; | |
let combo = 0; | |
let lockDelay = 0; | |
let clearedLines = []; | |
// Sound effects using Web Audio API | |
const audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
function playSound(frequency, duration, type = 'square') { | |
const oscillator = audioContext.createOscillator(); | |
const gainNode = audioContext.createGain(); | |
oscillator.connect(gainNode); | |
gainNode.connect(audioContext.destination); | |
oscillator.frequency.value = frequency; | |
oscillator.type = type; | |
gainNode.gain.setValueAtTime(0.1, audioContext.currentTime); | |
gainNode.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + duration); | |
oscillator.start(audioContext.currentTime); | |
oscillator.stop(audioContext.currentTime + duration); | |
} | |
// Initialize game | |
function init() { | |
// Create empty board | |
board = Array(ROWS).fill().map(() => Array(COLS).fill(0)); | |
// Fill next queue | |
nextQueue = []; | |
for (let i = 0; i < 5; i++) { | |
nextQueue.push(randomPiece()); | |
} | |
// Spawn first piece | |
spawnPiece(); | |
// Start game loop | |
lastTime = 0; | |
requestAnimationFrame(gameLoop); | |
} | |
// Create a random piece | |
function randomPiece() { | |
const pieces = Object.keys(SHAPES); | |
const type = pieces[Math.floor(Math.random() * pieces.length)]; | |
return { | |
type: type, | |
shape: SHAPES[type], | |
color: COLORS[type], | |
x: Math.floor((COLS - SHAPES[type][0].length) / 2), | |
y: 0, | |
rotation: 0 | |
}; | |
} | |
// Spawn a new piece | |
function spawnPiece() { | |
currentPiece = nextQueue.shift(); | |
nextQueue.push(randomPiece()); | |
canHold = true; | |
lockDelay = 0; | |
// Check game over | |
if (collision(currentPiece)) { | |
gameOver(); | |
} | |
} | |
// Rotate matrix | |
function rotateMatrix(matrix) { | |
const n = matrix.length; | |
const rotated = Array(n).fill().map(() => Array(n).fill(0)); | |
for (let i = 0; i < n; i++) { | |
for (let j = 0; j < n; j++) { | |
rotated[j][n - 1 - i] = matrix[i][j]; | |
} | |
} | |
return rotated; | |
} | |
// Check collision | |
function collision(piece, boardToCheck = board) { | |
for (let y = 0; y < piece.shape.length; y++) { | |
for (let x = 0; x < piece.shape[y].length; x++) { | |
if (piece.shape[y][x]) { | |
const boardX = piece.x + x; | |
const boardY = piece.y + y; | |
if (boardX < 0 || boardX >= COLS || | |
boardY >= ROWS || | |
(boardY >= 0 && boardToCheck[boardY][boardX])) { | |
return true; | |
} | |
} | |
} | |
} | |
return false; | |
} | |
// Lock piece to board | |
function lockPiece() { | |
for (let y = 0; y < currentPiece.shape.length; y++) { | |
for (let x = 0; x < currentPiece.shape[y].length; x++) { | |
if (currentPiece.shape[y][x]) { | |
const boardY = currentPiece.y + y; | |
const boardX = currentPiece.x + x; | |
if (boardY >= 0) { | |
board[boardY][boardX] = currentPiece.type; | |
} | |
} | |
} | |
} | |
playSound(200, 0.1); | |
checkLines(); | |
spawnPiece(); | |
} | |
// Check and clear lines | |
function checkLines() { | |
clearedLines = []; | |
for (let y = ROWS - 1; y >= 0; y--) { | |
if (board[y].every(cell => cell !== 0)) { | |
clearedLines.push(y); | |
} | |
} | |
if (clearedLines.length > 0) { | |
animateLineClear(); | |
// Update score | |
const lineScores = [0, 100, 300, 500, 800]; | |
score += lineScores[clearedLines.length] * level; | |
linesCleared += clearedLines.length; | |
// Level up every 10 lines | |
if (Math.floor(linesCleared / 10) + 1 > level) { | |
level++; | |
playSound(880, 0.3); | |
} | |
// Play clear sound | |
playSound(440 + clearedLines.length * 110, 0.2); | |
updateUI(); | |
} | |
} | |
// Animate line clearing | |
function animateLineClear() { | |
// Flash animation | |
setTimeout(() => { | |
clearedLines.forEach(y => { | |
board.splice(y, 1); | |
board.unshift(Array(COLS).fill(0)); | |
}); | |
}, 100); | |
} | |
// Move piece | |
function movePiece(dx, dy) { | |
if (isPaused || isGameOver) return; | |
currentPiece.x += dx; | |
currentPiece.y += dy; | |
if (collision(currentPiece)) { | |
currentPiece.x -= dx; | |
currentPiece.y -= dy; | |
if (dy > 0) { | |
lockDelay++; | |
if (lockDelay > 30) { | |
lockPiece(); | |
} | |
} | |
return false; | |
} | |
if (dx !== 0) { | |
playSound(150, 0.05); | |
lockDelay = 0; | |
} | |
return true; | |
} | |
// Rotate piece with wall kicks | |
function rotatePiece() { | |
if (isPaused || isGameOver) return; | |
const originalRotation = currentPiece.rotation; | |
const originalShape = currentPiece.shape; | |
currentPiece.shape = rotateMatrix(currentPiece.shape); | |
currentPiece.rotation = (currentPiece.rotation + 1) % 4; | |
// Wall kick offsets for SRS | |
const kicks = currentPiece.type === 'I' ? | |
[ | |
[0, 0], [-2, 0], [1, 0], [-2, -1], [1, 2] | |
] : [ | |
[0, 0], [-1, 0], [-1, 1], [0, -2], [-1, -2] | |
]; | |
for (let [dx, dy] of kicks) { | |
currentPiece.x += dx; | |
currentPiece.y += dy; | |
if (!collision(currentPiece)) { | |
playSound(300, 0.05); | |
lockDelay = 0; | |
return; | |
} | |
currentPiece.x -= dx; | |
currentPiece.y -= dy; | |
} | |
// Revert if no valid position found | |
currentPiece.shape = originalShape; | |
currentPiece.rotation = originalRotation; | |
} | |
// Hard drop | |
function hardDrop() { | |
if (isPaused || isGameOver) return; | |
let dropped = 0; | |
while (movePiece(0, 1)) { | |
dropped++; | |
} | |
score += dropped * 2; | |
lockPiece(); | |
playSound(100, 0.1, 'sawtooth'); | |
updateUI(); | |
} | |
// Soft drop | |
function softDrop() { | |
if (movePiece(0, 1)) { | |
score++; | |
updateUI(); | |
} | |
} | |
// Hold piece | |
function holdPiece() { | |
if (!canHold || isPaused || isGameOver) return; | |
canHold = false; | |
if (heldPiece === null) { | |
heldPiece = { | |
type: currentPiece.type, | |
shape: SHAPES[currentPiece.type], | |
color: currentPiece.color | |
}; | |
spawnPiece(); | |
} else { | |
const temp = { | |
type: currentPiece.type, | |
shape: SHAPES[currentPiece.type], | |
color: currentPiece.color | |
}; | |
currentPiece = { | |
type: heldPiece.type, | |
shape: SHAPES[heldPiece.type], | |
color: heldPiece.color, | |
x: Math.floor((COLS - SHAPES[heldPiece.type][0].length) / 2), | |
y: 0, | |
rotation: 0 | |
}; | |
heldPiece = temp; | |
} | |
playSound(400, 0.1); | |
} | |
// Draw functions | |
function drawBlock(ctx, x, y, color, size = BLOCK_SIZE) { | |
// Main block with gradient | |
const gradient = ctx.createLinearGradient(x, y, x + size, y + size); | |
gradient.addColorStop(0, color); | |
gradient.addColorStop(1, adjustColor(color, -30)); | |
ctx.fillStyle = gradient; | |
ctx.fillRect(x + 1, y + 1, size - 2, size - 2); | |
// Highlight | |
ctx.fillStyle = adjustColor(color, 50); | |
ctx.fillRect(x + 2, y + 2, size - 4, 2); | |
ctx.fillRect(x + 2, y + 2, 2, size - 4); | |
// Shadow | |
ctx.fillStyle = adjustColor(color, -50); | |
ctx.fillRect(x + size - 3, y + 2, 2, size - 3); | |
ctx.fillRect(x + 2, y + size - 3, size - 3, 2); | |
} | |
function adjustColor(color, amount) { | |
const num = parseInt(color.replace('#', ''), 16); | |
const r = Math.max(0, Math.min(255, (num >> 16) + amount)); | |
const g = Math.max(0, Math.min(255, ((num >> 8) & 0x00FF) + amount)); | |
const b = Math.max(0, Math.min(255, (num & 0x0000FF) + amount)); | |
return '#' + ((r << 16) | (g << 8) | b).toString(16).padStart(6, '0'); | |
} | |
function drawBoard() { | |
// Background | |
ctx.fillStyle = '#111'; | |
ctx.fillRect(0, 0, canvas.width, canvas.height); | |
// Grid | |
ctx.strokeStyle = '#222'; | |
ctx.lineWidth = 1; | |
for (let x = 0; x <= COLS; x++) { | |
ctx.beginPath(); | |
ctx.moveTo(x * BLOCK_SIZE, 0); | |
ctx.lineTo(x * BLOCK_SIZE, canvas.height); | |
ctx.stroke(); | |
} | |
for (let y = 0; y <= ROWS; y++) { | |
ctx.beginPath(); | |
ctx.moveTo(0, y * BLOCK_SIZE); | |
ctx.lineTo(canvas.width, y * BLOCK_SIZE); | |
ctx.stroke(); | |
} | |
// Draw locked pieces | |
for (let y = 0; y < ROWS; y++) { | |
for (let x = 0; x < COLS; x++) { | |
if (board[y][x]) { | |
// Flash effect for clearing lines | |
if (clearedLines.includes(y)) { | |
ctx.fillStyle = '#fff'; | |
ctx.fillRect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE); | |
} else { | |
drawBlock(ctx, x * BLOCK_SIZE, y * BLOCK_SIZE, COLORS[board[y][x]]); | |
} | |
} | |
} | |
} | |
} | |
function drawPiece(piece, opacity = 1) { | |
ctx.save(); | |
ctx.globalAlpha = opacity; | |
for (let y = 0; y < piece.shape.length; y++) { | |
for (let x = 0; x < piece.shape[y].length; x++) { | |
if (piece.shape[y][x]) { | |
drawBlock( | |
ctx, | |
(piece.x + x) * BLOCK_SIZE, | |
(piece.y + y) * BLOCK_SIZE, | |
piece.color | |
); | |
} | |
} | |
} | |
ctx.restore(); | |
} | |
function drawGhostPiece() { | |
const ghost = { | |
...currentPiece, | |
y: currentPiece.y | |
}; | |
while (!collision(ghost)) { | |
ghost.y++; | |
} | |
ghost.y--; | |
drawPiece(ghost, 0.3); | |
} | |
function drawHoldBox() { | |
holdCtx.fillStyle = '#000'; | |
holdCtx.fillRect(0, 0, holdCanvas.width, holdCanvas.height); | |
if (heldPiece) { | |
const blockSize = 20; | |
const offsetX = (holdCanvas.width - heldPiece.shape[0].length * blockSize) / 2; | |
const offsetY = (holdCanvas.height - heldPiece.shape.length * blockSize) / 2; | |
for (let y = 0; y < heldPiece.shape.length; y++) { | |
for (let x = 0; x < heldPiece.shape[y].length; x++) { | |
if (heldPiece.shape[y][x]) { | |
drawBlock( | |
holdCtx, | |
offsetX + x * blockSize, | |
offsetY + y * blockSize, | |
heldPiece.color, | |
blockSize | |
); | |
} | |
} | |
} | |
} | |
} | |
function drawNextQueue() { | |
nextCtxs.forEach((ctx, index) => { | |
ctx.fillStyle = '#000'; | |
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); | |
if (nextQueue[index]) { | |
const piece = nextQueue[index]; | |
const blockSize = 15; | |
const offsetX = (ctx.canvas.width - piece.shape[0].length * blockSize) / 2; | |
const offsetY = (ctx.canvas.height - piece.shape.length * blockSize) / 2; | |
for (let y = 0; y < piece.shape.length; y++) { | |
for (let x = 0; x < piece.shape[y].length; x++) { | |
if (piece.shape[y][x]) { | |
drawBlock( | |
ctx, | |
offsetX + x * blockSize, | |
offsetY + y * blockSize, | |
piece.color, | |
blockSize | |
); | |
} | |
} | |
} | |
} | |
}); | |
} | |
// Game loop | |
function gameLoop(time) { | |
if (!isPaused && !isGameOver) { | |
const deltaTime = time - lastTime; | |
dropTime += deltaTime; | |
// Drop speed based on level | |
const dropSpeed = Math.max(100, 1000 - (level - 1) * 100); | |
if (dropTime > dropSpeed) { | |
softDrop(); | |
dropTime = 0; | |
} | |
// Draw everything | |
drawBoard(); | |
drawGhostPiece(); | |
drawPiece(currentPiece); | |
drawHoldBox(); | |
drawNextQueue(); | |
} | |
lastTime = time; | |
requestAnimationFrame(gameLoop); | |
} | |
// UI updates | |
function updateUI() { | |
document.getElementById('score').textContent = score; | |
document.getElementById('level').textContent = level; | |
document.getElementById('lines').textContent = linesCleared; | |
} | |
// Game controls | |
function togglePause() { | |
isPaused = !isPaused; | |
document.getElementById('pauseOverlay').style.display = isPaused ? 'block' : 'none'; | |
if (!isPaused) { | |
lastTime = performance.now(); | |
} | |
} | |
function gameOver() { | |
isGameOver = true; | |
document.getElementById('finalScore').textContent = `Score: ${score}`; | |
document.getElementById('gameOverScreen').style.display = 'block'; | |
// Save high score | |
const highScore = localStorage.getItem('tetrisHighScore') || 0; | |
if (score > highScore) { | |
localStorage.setItem('tetrisHighScore', score); | |
} | |
playSound(200, 0.5, 'sawtooth'); | |
} | |
function resetGame() { | |
board = Array(ROWS).fill().map(() => Array(COLS).fill(0)); | |
score = 0; | |
level = 1; | |
linesCleared = 0; | |
heldPiece = null; | |
nextQueue = []; | |
isGameOver = false; | |
isPaused = false; | |
clearedLines = []; | |
document.getElementById('gameOverScreen').style.display = 'none'; | |
document.getElementById('pauseOverlay').style.display = 'none'; | |
updateUI(); | |
init(); | |
} | |
// Keyboard controls | |
document.addEventListener('keydown', (e) => { | |
if (isGameOver && e.key !== 'r' && e.key !== 'R') return; | |
switch(e.key) { | |
case 'ArrowLeft': | |
movePiece(-1, 0); | |
break; | |
case 'ArrowRight': | |
movePiece(1, 0); | |
break; | |
case 'ArrowDown': | |
softDrop(); | |
break; | |
case 'ArrowUp': | |
rotatePiece(); | |
break; | |
case ' ': | |
e.preventDefault(); | |
hardDrop(); | |
break; | |
case 'Shift': | |
holdPiece(); | |
break; | |
case 'p': | |
case 'P': | |
togglePause(); | |
break; | |
case 'r': | |
case 'R': | |
resetGame(); | |
break; | |
case 'f': | |
case 'F': | |
if (document.fullscreenElement) { | |
document.exitFullscreen(); | |
} else { | |
document.documentElement.requestFullscreen(); | |
} | |
break; | |
} | |
}); | |
// Touch controls for mobile | |
let touchStartX = 0; | |
let touchStartY = 0; | |
canvas.addEventListener('touchstart', (e) => { | |
touchStartX = e.touches[0].clientX; | |
touchStartY = e.touches[0].clientY; | |
}); | |
canvas.addEventListener('touchend', (e) => { | |
const touchEndX = e.changedTouches[0].clientX; | |
const touchEndY = e.changedTouches[0].clientY; | |
const dx = touchEndX - touchStartX; | |
const dy = touchEndY - touchStartY; | |
if (Math.abs(dx) > Math.abs(dy)) { | |
if (dx > 50) movePiece(1, 0); | |
else if (dx < -50) movePiece(-1, 0); | |
} else { | |
if (dy > 50) hardDrop(); | |
else if (dy < -50) rotatePiece(); | |
} | |
}); | |
canvas.addEventListener('touchmove', (e) => { | |
e.preventDefault(); | |
}); | |
// Start game | |
init(); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment