Skip to content

Instantly share code, notes, and snippets.

@shricodev
Created May 24, 2025 13:15
Show Gist options
  • Save shricodev/6303887f7a4071a05929785bc3c50d52 to your computer and use it in GitHub Desktop.
Save shricodev/6303887f7a4071a05929785bc3c50d52 to your computer and use it in GitHub Desktop.
Blog - Tetris (Claude Opus 4)
<!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