Initial commit
This commit is contained in:
@@ -0,0 +1,536 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>Block Drop</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body { width: 100%; height: 100%; overflow: hidden; background: #1a1a2e; touch-action: none; font-family: Arial, sans-serif; }
|
||||
canvas { display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="game"></canvas>
|
||||
<script>
|
||||
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||
let audioCtx = null;
|
||||
let musicEnabled = true;
|
||||
let sfxEnabled = true;
|
||||
let musicPlaying = false;
|
||||
|
||||
function initAudio() {
|
||||
if (!audioCtx) audioCtx = new AudioContext();
|
||||
if (audioCtx.state === 'suspended') audioCtx.resume();
|
||||
}
|
||||
|
||||
// Classic puzzle melody
|
||||
const melody = [
|
||||
{n: 659, d: 0.2}, {n: 494, d: 0.1}, {n: 523, d: 0.1}, {n: 587, d: 0.2},
|
||||
{n: 523, d: 0.1}, {n: 494, d: 0.1}, {n: 440, d: 0.2}, {n: 440, d: 0.1},
|
||||
{n: 523, d: 0.1}, {n: 659, d: 0.2}, {n: 587, d: 0.1}, {n: 523, d: 0.1},
|
||||
{n: 494, d: 0.3}, {n: 523, d: 0.1}, {n: 587, d: 0.2}, {n: 659, d: 0.2},
|
||||
{n: 523, d: 0.2}, {n: 440, d: 0.2}, {n: 440, d: 0.3}, {n: 0, d: 0.2}
|
||||
];
|
||||
|
||||
const bass = [
|
||||
{n: 165, d: 0.4}, {n: 110, d: 0.4}, {n: 131, d: 0.4}, {n: 165, d: 0.4},
|
||||
{n: 147, d: 0.4}, {n: 110, d: 0.4}, {n: 131, d: 0.4}, {n: 165, d: 0.4}
|
||||
];
|
||||
|
||||
let melodyIdx = 0, bassIdx = 0, nextMelody = 0, nextBass = 0;
|
||||
|
||||
function playTone(freq, dur, type = 'square', vol = 0.1) {
|
||||
if (!audioCtx || !musicEnabled || freq === 0) return;
|
||||
const osc = audioCtx.createOscillator();
|
||||
const gain = audioCtx.createGain();
|
||||
osc.type = type;
|
||||
osc.frequency.value = freq;
|
||||
gain.gain.setValueAtTime(vol, audioCtx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + dur * 0.9);
|
||||
osc.connect(gain);
|
||||
gain.connect(audioCtx.destination);
|
||||
osc.start();
|
||||
osc.stop(audioCtx.currentTime + dur);
|
||||
}
|
||||
|
||||
function playSfx(type) {
|
||||
if (!audioCtx || !sfxEnabled) return;
|
||||
const osc = audioCtx.createOscillator();
|
||||
const gain = audioCtx.createGain();
|
||||
if (type === 'move') {
|
||||
osc.frequency.value = 200;
|
||||
gain.gain.setValueAtTime(0.1, audioCtx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.05);
|
||||
} else if (type === 'rotate') {
|
||||
osc.frequency.value = 400;
|
||||
gain.gain.setValueAtTime(0.12, audioCtx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.08);
|
||||
} else if (type === 'drop') {
|
||||
osc.frequency.setValueAtTime(300, audioCtx.currentTime);
|
||||
osc.frequency.exponentialRampToValueAtTime(100, audioCtx.currentTime + 0.15);
|
||||
gain.gain.setValueAtTime(0.2, audioCtx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.15);
|
||||
} else if (type === 'clear') {
|
||||
osc.frequency.setValueAtTime(523, audioCtx.currentTime);
|
||||
osc.frequency.setValueAtTime(659, audioCtx.currentTime + 0.1);
|
||||
osc.frequency.setValueAtTime(784, audioCtx.currentTime + 0.2);
|
||||
gain.gain.setValueAtTime(0.25, audioCtx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.3);
|
||||
} else if (type === 'gameover') {
|
||||
osc.frequency.setValueAtTime(400, audioCtx.currentTime);
|
||||
osc.frequency.exponentialRampToValueAtTime(100, audioCtx.currentTime + 0.5);
|
||||
gain.gain.setValueAtTime(0.3, audioCtx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.5);
|
||||
osc.type = 'sawtooth';
|
||||
}
|
||||
osc.connect(gain);
|
||||
gain.connect(audioCtx.destination);
|
||||
osc.start();
|
||||
osc.stop(audioCtx.currentTime + 0.6);
|
||||
}
|
||||
|
||||
function updateMusic() {
|
||||
if (!audioCtx || !musicEnabled || !musicPlaying) return;
|
||||
const now = audioCtx.currentTime;
|
||||
if (now >= nextMelody) {
|
||||
const note = melody[melodyIdx];
|
||||
playTone(note.n, note.d, 'square', 0.08);
|
||||
nextMelody = now + note.d;
|
||||
melodyIdx = (melodyIdx + 1) % melody.length;
|
||||
}
|
||||
if (now >= nextBass) {
|
||||
const note = bass[bassIdx];
|
||||
playTone(note.n, note.d, 'triangle', 0.12);
|
||||
nextBass = now + note.d;
|
||||
bassIdx = (bassIdx + 1) % bass.length;
|
||||
}
|
||||
}
|
||||
|
||||
function startMusic() {
|
||||
if (!musicPlaying && musicEnabled) {
|
||||
initAudio();
|
||||
musicPlaying = true;
|
||||
nextMelody = audioCtx.currentTime;
|
||||
nextBass = audioCtx.currentTime;
|
||||
}
|
||||
}
|
||||
|
||||
const canvas = document.getElementById('game');
|
||||
const ctx = canvas.getContext('2d');
|
||||
let W, H, safeTop, safeBottom;
|
||||
|
||||
function resize() {
|
||||
W = window.innerWidth;
|
||||
H = window.innerHeight;
|
||||
canvas.width = W;
|
||||
canvas.height = H;
|
||||
safeTop = H * 0.1;
|
||||
safeBottom = H * 0.9;
|
||||
}
|
||||
resize();
|
||||
window.addEventListener('resize', resize);
|
||||
|
||||
const COLS = 10;
|
||||
const ROWS = 20;
|
||||
let cellSize, boardX, boardY, boardW, boardH;
|
||||
|
||||
function calcBoard() {
|
||||
const availH = safeBottom - safeTop - 40;
|
||||
const availW = W - 40;
|
||||
cellSize = Math.min(availW / COLS, availH / ROWS);
|
||||
boardW = cellSize * COLS;
|
||||
boardH = cellSize * ROWS;
|
||||
boardX = (W - boardW) / 2;
|
||||
boardY = safeTop + 20 + (availH - boardH) / 2;
|
||||
}
|
||||
|
||||
const SHAPES = [
|
||||
{ shape: [[1,1,1,1]], color: '#00f0f0' }, // I
|
||||
{ shape: [[1,1],[1,1]], color: '#f0f000' }, // O
|
||||
{ shape: [[0,1,0],[1,1,1]], color: '#a000f0' }, // T
|
||||
{ shape: [[1,0,0],[1,1,1]], color: '#0000f0' }, // J
|
||||
{ shape: [[0,0,1],[1,1,1]], color: '#f0a000' }, // L
|
||||
{ shape: [[0,1,1],[1,1,0]], color: '#00f000' }, // S
|
||||
{ shape: [[1,1,0],[0,1,1]], color: '#f00000' } // Z
|
||||
];
|
||||
|
||||
let board = [];
|
||||
let currentPiece = null;
|
||||
let pieceX = 0, pieceY = 0;
|
||||
let score = 0;
|
||||
let level = 1;
|
||||
let lines = 0;
|
||||
let gameRunning = false;
|
||||
let gameOver = false;
|
||||
let dropTimer = 0;
|
||||
let dropInterval = 60; // Very slow start (60 frames = 1 second at 60fps)
|
||||
|
||||
function createBoard() {
|
||||
board = [];
|
||||
for (let r = 0; r < ROWS; r++) {
|
||||
board.push(new Array(COLS).fill(null));
|
||||
}
|
||||
}
|
||||
|
||||
function newPiece() {
|
||||
const idx = Math.floor(Math.random() * SHAPES.length);
|
||||
currentPiece = {
|
||||
shape: SHAPES[idx].shape.map(row => [...row]),
|
||||
color: SHAPES[idx].color
|
||||
};
|
||||
pieceX = Math.floor((COLS - currentPiece.shape[0].length) / 2);
|
||||
pieceY = 0;
|
||||
|
||||
if (!isValidPosition(pieceX, pieceY, currentPiece.shape)) {
|
||||
gameOver = true;
|
||||
gameRunning = false;
|
||||
musicPlaying = false;
|
||||
playSfx('gameover');
|
||||
}
|
||||
}
|
||||
|
||||
function isValidPosition(x, y, shape) {
|
||||
for (let r = 0; r < shape.length; r++) {
|
||||
for (let c = 0; c < shape[r].length; c++) {
|
||||
if (shape[r][c]) {
|
||||
const newX = x + c;
|
||||
const newY = y + r;
|
||||
if (newX < 0 || newX >= COLS || newY >= ROWS) return false;
|
||||
if (newY >= 0 && board[newY][newX]) return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function rotatePiece() {
|
||||
const rows = currentPiece.shape.length;
|
||||
const cols = currentPiece.shape[0].length;
|
||||
const rotated = [];
|
||||
for (let c = 0; c < cols; c++) {
|
||||
rotated.push([]);
|
||||
for (let r = rows - 1; r >= 0; r--) {
|
||||
rotated[c].push(currentPiece.shape[r][c]);
|
||||
}
|
||||
}
|
||||
|
||||
// Try rotation with wall kicks
|
||||
const kicks = [0, 1, -1, 2, -2];
|
||||
for (const kick of kicks) {
|
||||
if (isValidPosition(pieceX + kick, pieceY, rotated)) {
|
||||
currentPiece.shape = rotated;
|
||||
pieceX += kick;
|
||||
playSfx('rotate');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function movePiece(dx) {
|
||||
if (isValidPosition(pieceX + dx, pieceY, currentPiece.shape)) {
|
||||
pieceX += dx;
|
||||
playSfx('move');
|
||||
}
|
||||
}
|
||||
|
||||
function dropPiece() {
|
||||
if (isValidPosition(pieceX, pieceY + 1, currentPiece.shape)) {
|
||||
pieceY++;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function hardDrop() {
|
||||
while (dropPiece()) {}
|
||||
lockPiece();
|
||||
playSfx('drop');
|
||||
}
|
||||
|
||||
function lockPiece() {
|
||||
for (let r = 0; r < currentPiece.shape.length; r++) {
|
||||
for (let c = 0; c < currentPiece.shape[r].length; c++) {
|
||||
if (currentPiece.shape[r][c]) {
|
||||
const boardY = pieceY + r;
|
||||
const boardX = pieceX + c;
|
||||
if (boardY >= 0) {
|
||||
board[boardY][boardX] = currentPiece.color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
clearLines();
|
||||
newPiece();
|
||||
}
|
||||
|
||||
function clearLines() {
|
||||
let cleared = 0;
|
||||
for (let r = ROWS - 1; r >= 0; r--) {
|
||||
if (board[r].every(cell => cell !== null)) {
|
||||
board.splice(r, 1);
|
||||
board.unshift(new Array(COLS).fill(null));
|
||||
cleared++;
|
||||
r++;
|
||||
}
|
||||
}
|
||||
|
||||
if (cleared > 0) {
|
||||
const points = [0, 100, 300, 500, 800];
|
||||
score += points[cleared] * level;
|
||||
lines += cleared;
|
||||
playSfx('clear');
|
||||
|
||||
// Level up every 10 lines
|
||||
const newLevel = Math.floor(lines / 10) + 1;
|
||||
if (newLevel > level) {
|
||||
level = newLevel;
|
||||
// Gradual speed increase - starts very slow
|
||||
dropInterval = Math.max(10, 60 - (level - 1) * 5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
calcBoard();
|
||||
createBoard();
|
||||
score = 0;
|
||||
level = 1;
|
||||
lines = 0;
|
||||
dropInterval = 60; // 1 second drop at start
|
||||
dropTimer = 0;
|
||||
gameOver = false;
|
||||
gameRunning = true;
|
||||
newPiece();
|
||||
startMusic();
|
||||
}
|
||||
|
||||
let keys = { left: false, right: false, down: false };
|
||||
let keyRepeat = { left: 0, right: 0, down: 0 };
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
initAudio();
|
||||
if (e.key === 'ArrowLeft' || e.key === 'a') keys.left = true;
|
||||
if (e.key === 'ArrowRight' || e.key === 'd') keys.right = true;
|
||||
if (e.key === 'ArrowDown' || e.key === 's') keys.down = true;
|
||||
if (e.key === 'ArrowUp' || e.key === 'w') { rotatePiece(); e.preventDefault(); }
|
||||
if (e.key === ' ') { hardDrop(); e.preventDefault(); }
|
||||
if ((e.key === ' ' || e.key === 'Enter') && gameOver) init();
|
||||
});
|
||||
|
||||
document.addEventListener('keyup', e => {
|
||||
if (e.key === 'ArrowLeft' || e.key === 'a') { keys.left = false; keyRepeat.left = 0; }
|
||||
if (e.key === 'ArrowRight' || e.key === 'd') { keys.right = false; keyRepeat.right = 0; }
|
||||
if (e.key === 'ArrowDown' || e.key === 's') { keys.down = false; keyRepeat.down = 0; }
|
||||
});
|
||||
|
||||
let touchStartX = 0, touchStartY = 0;
|
||||
canvas.addEventListener('click', e => {
|
||||
initAudio();
|
||||
if (gameOver) init();
|
||||
});
|
||||
|
||||
canvas.addEventListener('touchstart', e => {
|
||||
e.preventDefault();
|
||||
initAudio();
|
||||
if (gameOver) { init(); return; }
|
||||
touchStartX = e.touches[0].clientX;
|
||||
touchStartY = e.touches[0].clientY;
|
||||
});
|
||||
|
||||
canvas.addEventListener('touchmove', e => {
|
||||
e.preventDefault();
|
||||
const dx = e.touches[0].clientX - touchStartX;
|
||||
const dy = e.touches[0].clientY - touchStartY;
|
||||
|
||||
if (Math.abs(dx) > 30) {
|
||||
movePiece(dx > 0 ? 1 : -1);
|
||||
touchStartX = e.touches[0].clientX;
|
||||
}
|
||||
if (dy > 30) {
|
||||
dropPiece();
|
||||
touchStartY = e.touches[0].clientY;
|
||||
}
|
||||
});
|
||||
|
||||
canvas.addEventListener('touchend', e => {
|
||||
const dx = e.changedTouches[0].clientX - touchStartX;
|
||||
const dy = e.changedTouches[0].clientY - touchStartY;
|
||||
|
||||
// Tap to rotate
|
||||
if (Math.abs(dx) < 10 && Math.abs(dy) < 10) {
|
||||
rotatePiece();
|
||||
}
|
||||
// Swipe up for hard drop
|
||||
if (dy < -50 && Math.abs(dx) < 30) {
|
||||
hardDrop();
|
||||
}
|
||||
});
|
||||
|
||||
function update() {
|
||||
if (!gameRunning || gameOver) return;
|
||||
|
||||
// Key repeat
|
||||
if (keys.left) {
|
||||
keyRepeat.left++;
|
||||
if (keyRepeat.left === 1 || (keyRepeat.left > 15 && keyRepeat.left % 3 === 0)) {
|
||||
movePiece(-1);
|
||||
}
|
||||
}
|
||||
if (keys.right) {
|
||||
keyRepeat.right++;
|
||||
if (keyRepeat.right === 1 || (keyRepeat.right > 15 && keyRepeat.right % 3 === 0)) {
|
||||
movePiece(1);
|
||||
}
|
||||
}
|
||||
if (keys.down) {
|
||||
keyRepeat.down++;
|
||||
if (keyRepeat.down % 3 === 0) {
|
||||
if (!dropPiece()) lockPiece();
|
||||
}
|
||||
}
|
||||
|
||||
// Auto drop
|
||||
dropTimer++;
|
||||
if (dropTimer >= dropInterval) {
|
||||
dropTimer = 0;
|
||||
if (!dropPiece()) {
|
||||
lockPiece();
|
||||
}
|
||||
}
|
||||
|
||||
updateMusic();
|
||||
}
|
||||
|
||||
function draw() {
|
||||
calcBoard();
|
||||
|
||||
// Background
|
||||
ctx.fillStyle = '#1a1a2e';
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
|
||||
// Safe zones
|
||||
ctx.fillStyle = '#16213e';
|
||||
ctx.fillRect(0, 0, W, safeTop);
|
||||
ctx.fillRect(0, safeBottom, W, H - safeBottom);
|
||||
|
||||
// Header
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.font = 'bold 20px Arial';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText(`Score: ${score}`, 15, 35);
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(`Level ${level}`, W / 2, 35);
|
||||
ctx.textAlign = 'right';
|
||||
ctx.fillText(`Lines: ${lines}`, W - 15, 35);
|
||||
|
||||
// Board background
|
||||
ctx.fillStyle = '#0f0f23';
|
||||
ctx.fillRect(boardX, boardY, boardW, boardH);
|
||||
|
||||
// Grid
|
||||
ctx.strokeStyle = '#222';
|
||||
ctx.lineWidth = 1;
|
||||
for (let r = 0; r <= ROWS; r++) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(boardX, boardY + r * cellSize);
|
||||
ctx.lineTo(boardX + boardW, boardY + r * cellSize);
|
||||
ctx.stroke();
|
||||
}
|
||||
for (let c = 0; c <= COLS; c++) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(boardX + c * cellSize, boardY);
|
||||
ctx.lineTo(boardX + c * cellSize, boardY + boardH);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Placed blocks
|
||||
for (let r = 0; r < ROWS; r++) {
|
||||
for (let c = 0; c < COLS; c++) {
|
||||
if (board[r][c]) {
|
||||
ctx.fillStyle = board[r][c];
|
||||
ctx.fillRect(boardX + c * cellSize + 1, boardY + r * cellSize + 1, cellSize - 2, cellSize - 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Current piece
|
||||
if (currentPiece && !gameOver) {
|
||||
ctx.fillStyle = currentPiece.color;
|
||||
for (let r = 0; r < currentPiece.shape.length; r++) {
|
||||
for (let c = 0; c < currentPiece.shape[r].length; c++) {
|
||||
if (currentPiece.shape[r][c]) {
|
||||
const x = boardX + (pieceX + c) * cellSize + 1;
|
||||
const y = boardY + (pieceY + r) * cellSize + 1;
|
||||
ctx.fillRect(x, y, cellSize - 2, cellSize - 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ghost piece
|
||||
let ghostY = pieceY;
|
||||
while (isValidPosition(pieceX, ghostY + 1, currentPiece.shape)) {
|
||||
ghostY++;
|
||||
}
|
||||
if (ghostY > pieceY) {
|
||||
ctx.strokeStyle = currentPiece.color;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.globalAlpha = 0.3;
|
||||
for (let r = 0; r < currentPiece.shape.length; r++) {
|
||||
for (let c = 0; c < currentPiece.shape[r].length; c++) {
|
||||
if (currentPiece.shape[r][c]) {
|
||||
const x = boardX + (pieceX + c) * cellSize + 2;
|
||||
const y = boardY + (ghostY + r) * cellSize + 2;
|
||||
ctx.strokeRect(x, y, cellSize - 4, cellSize - 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Controls hint
|
||||
ctx.fillStyle = '#666';
|
||||
ctx.font = '14px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('Swipe/Arrows: Move | Tap/Up: Rotate | Swipe Up/Space: Drop', W / 2, safeBottom + 25);
|
||||
|
||||
if (gameOver) {
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.85)';
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.font = 'bold 40px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('GAME OVER', W / 2, H / 2 - 50);
|
||||
ctx.font = '24px Arial';
|
||||
ctx.fillText(`Score: ${score}`, W / 2, H / 2);
|
||||
ctx.fillText(`Lines: ${lines}`, W / 2, H / 2 + 35);
|
||||
|
||||
ctx.fillStyle = '#00f0f0';
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(W / 2 - 100, H / 2 + 60, 200, 50, 10);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.font = 'bold 18px Arial';
|
||||
ctx.fillText('TAP TO PLAY AGAIN', W / 2, H / 2 + 92);
|
||||
}
|
||||
}
|
||||
|
||||
function gameLoop() {
|
||||
update();
|
||||
draw();
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
window.gameScore = 0;
|
||||
Object.defineProperty(window, 'gameScore', { get: () => score });
|
||||
window.pauseGame = () => { gameRunning = false; musicPlaying = false; };
|
||||
window.resumeGame = () => { if (!gameOver) { gameRunning = true; startMusic(); } };
|
||||
window.setMusicEnabled = (v) => { musicEnabled = v; if (!v) musicPlaying = false; };
|
||||
window.setSfxEnabled = (v) => { sfxEnabled = v; };
|
||||
|
||||
init();
|
||||
gameLoop();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user