Created
March 22, 2025 19:53
-
-
Save mgroves/e9bc10e9b4ecff767be4faa28c9c3063 to your computer and use it in GitHub Desktop.
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 name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Block Drop</title> | |
<style> | |
* { | |
-webkit-user-select: none; | |
-ms-user-select: none; | |
user-select: none; | |
} | |
body { | |
margin: 0; | |
background: #111; | |
color: #fff; | |
font-family: sans-serif; | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
touch-action: manipulation; | |
} | |
canvas { | |
background: #222; | |
margin-top: 10px; | |
touch-action: none; | |
border: 2px solid #444; | |
} | |
.controls { | |
display: flex; | |
flex-wrap: wrap; | |
justify-content: center; | |
gap: 10px; | |
margin: 10px; | |
} | |
button { | |
font-size: 1.2em; | |
padding: 10px; | |
width: 100px; | |
height: 60px; | |
border: none; | |
border-radius: 10px; | |
background: #444; | |
color: white; | |
} | |
button:active { | |
background: #666; | |
} | |
</style> | |
</head> | |
<body> | |
<h1>Block Drop</h1> | |
<canvas id="game"></canvas> | |
<div class="controls"> | |
<button onclick="move(-1)">Left</button> | |
<button onclick="rotate()">Rotate</button> | |
<button onclick="move(1)">Right</button> | |
<button onclick="drop()">Drop</button> | |
</div> | |
<script> | |
const canvas = document.getElementById('game'); | |
const ctx = canvas.getContext('2d'); | |
const gridWidth = 10; | |
const gridHeight = 20; | |
const cellSize = 24; | |
canvas.width = gridWidth * cellSize; | |
canvas.height = gridHeight * cellSize; | |
const colors = ['#f00', '#0f0', '#00f', '#ff0', '#f0f', '#0ff', '#aaa']; | |
const tetrominoes = [ | |
[[1, 1, 1, 1]], | |
[[1, 1], [1, 1]], | |
[[0, 1, 0], [1, 1, 1]], | |
[[1, 0, 0], [1, 1, 1]], | |
[[0, 0, 1], [1, 1, 1]], | |
[[1, 1, 0], [0, 1, 1]], | |
[[0, 1, 1], [1, 1, 0]] | |
]; | |
let grid = Array.from({length: gridHeight}, () => Array(gridWidth).fill(0)); | |
let piece, pos, color; | |
const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); | |
function beep(freq = 440, dur = 0.1) { | |
const osc = audioCtx.createOscillator(); | |
const gain = audioCtx.createGain(); | |
osc.connect(gain); | |
gain.connect(audioCtx.destination); | |
osc.type = 'square'; | |
osc.frequency.value = freq; | |
gain.gain.setValueAtTime(0.1, audioCtx.currentTime); | |
osc.start(); | |
osc.stop(audioCtx.currentTime + dur); | |
} | |
function newPiece() { | |
const index = Math.floor(Math.random() * tetrominoes.length); | |
piece = tetrominoes[index]; | |
color = index + 1; | |
pos = {x: 3, y: 0}; | |
if (collide()) { | |
alert('Game Over!'); | |
grid = Array.from({length: gridHeight}, () => Array(gridWidth).fill(0)); | |
} | |
} | |
function draw() { | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
grid.forEach((row, y) => | |
row.forEach((val, x) => { | |
if (val) drawCell(x, y, colors[val - 1]); | |
})); | |
piece.forEach((row, y) => | |
row.forEach((val, x) => { | |
if (val) drawCell(x + pos.x, y + pos.y, colors[color - 1]); | |
})); | |
} | |
function drawCell(x, y, fill) { | |
ctx.fillStyle = fill; | |
ctx.fillRect(x * cellSize, y * cellSize, cellSize - 1, cellSize - 1); | |
} | |
function collide(offsetX = 0, offsetY = 0, testPiece = piece) { | |
return testPiece.some((row, y) => | |
row.some((val, x) => { | |
if (val) { | |
const nx = x + pos.x + offsetX; | |
const ny = y + pos.y + offsetY; | |
return nx < 0 || nx >= gridWidth || ny >= gridHeight || (ny >= 0 && grid[ny][nx]); | |
} | |
return false; | |
}) | |
); | |
} | |
function merge() { | |
piece.forEach((row, y) => | |
row.forEach((val, x) => { | |
if (val) grid[y + pos.y][x + pos.x] = color; | |
})); | |
beep(220); | |
clearLines(); | |
newPiece(); | |
} | |
function clearLines() { | |
for (let y = gridHeight - 1; y >= 0; y--) { | |
if (grid[y].every(v => v)) { | |
grid.splice(y, 1); | |
grid.unshift(Array(gridWidth).fill(0)); | |
beep(880); | |
y++; | |
} | |
} | |
} | |
function rotate() { | |
const rotated = piece[0].map((_, i) => piece.map(row => row[i]).reverse()); | |
if (!collide(0, 0, rotated)) { | |
piece = rotated; | |
beep(660); | |
} | |
} | |
function move(dir) { | |
if (!collide(dir, 0)) { | |
pos.x += dir; | |
beep(550); | |
} | |
} | |
function drop() { | |
if (!collide(0, 1)) { | |
pos.y++; | |
} else { | |
merge(); | |
} | |
} | |
let dropCounter = 0; | |
let lastTime = 0; | |
function update(time = 0) { | |
const delta = time - lastTime; | |
lastTime = time; | |
dropCounter += delta; | |
if (dropCounter > 500) { | |
drop(); | |
dropCounter = 0; | |
} | |
draw(); | |
requestAnimationFrame(update); | |
} | |
newPiece(); | |
update(); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment