Initial commit
This commit is contained in:
@@ -0,0 +1,445 @@
|
||||
<!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>2048</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body { width: 100%; height: 100%; overflow: hidden; background: #faf8ef; 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();
|
||||
}
|
||||
|
||||
// Calm puzzle melody
|
||||
const melody = [
|
||||
{n: 392, d: 0.4}, {n: 440, d: 0.4}, {n: 494, d: 0.4}, {n: 523, d: 0.8},
|
||||
{n: 494, d: 0.4}, {n: 440, d: 0.4}, {n: 392, d: 0.8},
|
||||
{n: 330, d: 0.4}, {n: 349, d: 0.4}, {n: 392, d: 0.4}, {n: 440, d: 0.8},
|
||||
{n: 392, d: 0.4}, {n: 349, d: 0.4}, {n: 330, d: 0.8}, {n: 0, d: 0.4}
|
||||
];
|
||||
|
||||
const bass = [
|
||||
{n: 98, d: 0.8}, {n: 110, d: 0.8}, {n: 131, d: 0.8}, {n: 110, d: 0.8},
|
||||
{n: 82, d: 0.8}, {n: 87, d: 0.8}, {n: 98, d: 0.8}, {n: 110, d: 0.8}
|
||||
];
|
||||
|
||||
let melodyIdx = 0, bassIdx = 0, nextMelody = 0, nextBass = 0;
|
||||
|
||||
function playTone(freq, dur, type = 'square', vol = 0.08) {
|
||||
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 = 300;
|
||||
gain.gain.setValueAtTime(0.1, audioCtx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.05);
|
||||
} else if (type === 'merge') {
|
||||
osc.frequency.setValueAtTime(400, audioCtx.currentTime);
|
||||
osc.frequency.setValueAtTime(600, audioCtx.currentTime + 0.05);
|
||||
gain.gain.setValueAtTime(0.15, audioCtx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.1);
|
||||
} else if (type === 'win') {
|
||||
osc.frequency.setValueAtTime(523, audioCtx.currentTime);
|
||||
osc.frequency.setValueAtTime(659, audioCtx.currentTime + 0.15);
|
||||
osc.frequency.setValueAtTime(784, audioCtx.currentTime + 0.3);
|
||||
osc.frequency.setValueAtTime(1047, audioCtx.currentTime + 0.45);
|
||||
gain.gain.setValueAtTime(0.2, audioCtx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.6);
|
||||
} else if (type === 'lose') {
|
||||
osc.frequency.setValueAtTime(300, audioCtx.currentTime);
|
||||
osc.frequency.exponentialRampToValueAtTime(100, audioCtx.currentTime + 0.4);
|
||||
gain.gain.setValueAtTime(0.2, audioCtx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.4);
|
||||
}
|
||||
osc.type = 'square';
|
||||
osc.connect(gain);
|
||||
gain.connect(audioCtx.destination);
|
||||
osc.start();
|
||||
osc.stop(audioCtx.currentTime + 0.7);
|
||||
}
|
||||
|
||||
function updateMusic() {
|
||||
if (!audioCtx || !musicEnabled || !musicPlaying) return;
|
||||
const now = audioCtx.currentTime;
|
||||
if (now >= nextMelody) {
|
||||
const note = melody[melodyIdx];
|
||||
playTone(note.n, note.d, 'triangle', 0.06);
|
||||
nextMelody = now + note.d;
|
||||
melodyIdx = (melodyIdx + 1) % melody.length;
|
||||
}
|
||||
if (now >= nextBass) {
|
||||
const note = bass[bassIdx];
|
||||
playTone(note.n, note.d, 'triangle', 0.1);
|
||||
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 SIZE = 4;
|
||||
let grid = [];
|
||||
let score = 0;
|
||||
let bestScore = 0;
|
||||
let gameOver = false;
|
||||
let won = false;
|
||||
let gameRunning = false;
|
||||
|
||||
const COLORS = {
|
||||
0: '#cdc1b4',
|
||||
2: '#eee4da',
|
||||
4: '#ede0c8',
|
||||
8: '#f2b179',
|
||||
16: '#f59563',
|
||||
32: '#f67c5f',
|
||||
64: '#f65e3b',
|
||||
128: '#edcf72',
|
||||
256: '#edcc61',
|
||||
512: '#edc850',
|
||||
1024: '#edc53f',
|
||||
2048: '#edc22e'
|
||||
};
|
||||
|
||||
const TEXT_COLORS = {
|
||||
2: '#776e65',
|
||||
4: '#776e65'
|
||||
};
|
||||
|
||||
function createGrid() {
|
||||
grid = [];
|
||||
for (let r = 0; r < SIZE; r++) {
|
||||
grid.push(new Array(SIZE).fill(0));
|
||||
}
|
||||
}
|
||||
|
||||
function addRandomTile() {
|
||||
const empty = [];
|
||||
for (let r = 0; r < SIZE; r++) {
|
||||
for (let c = 0; c < SIZE; c++) {
|
||||
if (grid[r][c] === 0) empty.push({r, c});
|
||||
}
|
||||
}
|
||||
if (empty.length > 0) {
|
||||
const {r, c} = empty[Math.floor(Math.random() * empty.length)];
|
||||
grid[r][c] = Math.random() < 0.9 ? 2 : 4;
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
createGrid();
|
||||
score = 0;
|
||||
gameOver = false;
|
||||
won = false;
|
||||
gameRunning = true;
|
||||
addRandomTile();
|
||||
addRandomTile();
|
||||
startMusic();
|
||||
}
|
||||
|
||||
function canMove() {
|
||||
for (let r = 0; r < SIZE; r++) {
|
||||
for (let c = 0; c < SIZE; c++) {
|
||||
if (grid[r][c] === 0) return true;
|
||||
if (c < SIZE - 1 && grid[r][c] === grid[r][c + 1]) return true;
|
||||
if (r < SIZE - 1 && grid[r][c] === grid[r + 1][c]) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function move(direction) {
|
||||
if (gameOver) return false;
|
||||
|
||||
let moved = false;
|
||||
let merged = false;
|
||||
|
||||
const rotateGrid = (times) => {
|
||||
for (let t = 0; t < times; t++) {
|
||||
const newGrid = [];
|
||||
for (let r = 0; r < SIZE; r++) {
|
||||
newGrid.push([]);
|
||||
for (let c = 0; c < SIZE; c++) {
|
||||
newGrid[r].push(grid[SIZE - 1 - c][r]);
|
||||
}
|
||||
}
|
||||
grid = newGrid;
|
||||
}
|
||||
};
|
||||
|
||||
// Rotate so we always move left
|
||||
const rotations = {left: 0, up: 1, right: 2, down: 3};
|
||||
rotateGrid(rotations[direction]);
|
||||
|
||||
// Move and merge left
|
||||
for (let r = 0; r < SIZE; r++) {
|
||||
const row = grid[r].filter(v => v !== 0);
|
||||
const newRow = [];
|
||||
|
||||
for (let i = 0; i < row.length; i++) {
|
||||
if (i < row.length - 1 && row[i] === row[i + 1]) {
|
||||
const mergedValue = row[i] * 2;
|
||||
newRow.push(mergedValue);
|
||||
score += mergedValue;
|
||||
if (mergedValue === 2048 && !won) {
|
||||
won = true;
|
||||
playSfx('win');
|
||||
}
|
||||
i++;
|
||||
merged = true;
|
||||
} else {
|
||||
newRow.push(row[i]);
|
||||
}
|
||||
}
|
||||
|
||||
while (newRow.length < SIZE) newRow.push(0);
|
||||
|
||||
for (let c = 0; c < SIZE; c++) {
|
||||
if (grid[r][c] !== newRow[c]) moved = true;
|
||||
grid[r][c] = newRow[c];
|
||||
}
|
||||
}
|
||||
|
||||
// Rotate back
|
||||
rotateGrid((4 - rotations[direction]) % 4);
|
||||
|
||||
if (moved) {
|
||||
addRandomTile();
|
||||
if (merged) playSfx('merge');
|
||||
else playSfx('move');
|
||||
|
||||
if (!canMove()) {
|
||||
gameOver = true;
|
||||
gameRunning = false;
|
||||
playSfx('lose');
|
||||
}
|
||||
|
||||
if (score > bestScore) bestScore = score;
|
||||
}
|
||||
|
||||
return moved;
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
initAudio();
|
||||
if (e.key === 'ArrowLeft' || e.key === 'a') { move('left'); e.preventDefault(); }
|
||||
if (e.key === 'ArrowRight' || e.key === 'd') { move('right'); e.preventDefault(); }
|
||||
if (e.key === 'ArrowUp' || e.key === 'w') { move('up'); e.preventDefault(); }
|
||||
if (e.key === 'ArrowDown' || e.key === 's') { move('down'); e.preventDefault(); }
|
||||
if ((e.key === ' ' || e.key === 'Enter') && gameOver) init();
|
||||
});
|
||||
|
||||
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('touchend', e => {
|
||||
e.preventDefault();
|
||||
const dx = e.changedTouches[0].clientX - touchStartX;
|
||||
const dy = e.changedTouches[0].clientY - touchStartY;
|
||||
|
||||
if (Math.abs(dx) < 20 && Math.abs(dy) < 20) return;
|
||||
|
||||
if (Math.abs(dx) > Math.abs(dy)) {
|
||||
move(dx > 0 ? 'right' : 'left');
|
||||
} else {
|
||||
move(dy > 0 ? 'down' : 'up');
|
||||
}
|
||||
});
|
||||
|
||||
canvas.addEventListener('mousedown', e => { touchStartX = e.clientX; touchStartY = e.clientY; });
|
||||
canvas.addEventListener('mouseup', e => {
|
||||
const dx = e.clientX - touchStartX;
|
||||
const dy = e.clientY - touchStartY;
|
||||
if (Math.abs(dx) < 20 && Math.abs(dy) < 20) return;
|
||||
if (Math.abs(dx) > Math.abs(dy)) {
|
||||
move(dx > 0 ? 'right' : 'left');
|
||||
} else {
|
||||
move(dy > 0 ? 'down' : 'up');
|
||||
}
|
||||
});
|
||||
|
||||
function update() {
|
||||
updateMusic();
|
||||
}
|
||||
|
||||
function draw() {
|
||||
// Background
|
||||
ctx.fillStyle = '#faf8ef';
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
|
||||
// Safe zones
|
||||
ctx.fillStyle = '#f0e6d3';
|
||||
ctx.fillRect(0, 0, W, safeTop);
|
||||
ctx.fillRect(0, safeBottom, W, H - safeBottom);
|
||||
|
||||
// Header
|
||||
ctx.fillStyle = '#776e65';
|
||||
ctx.font = 'bold 28px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('2048', W / 2, 40);
|
||||
|
||||
ctx.font = 'bold 18px Arial';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText(`Score: ${score}`, 20, 35);
|
||||
ctx.textAlign = 'right';
|
||||
ctx.fillText(`Best: ${bestScore}`, W - 20, 35);
|
||||
|
||||
// Calculate board size
|
||||
const availH = safeBottom - safeTop - 40;
|
||||
const availW = W - 40;
|
||||
const boardSize = Math.min(availW, availH);
|
||||
const cellPadding = 8;
|
||||
const cellSize = (boardSize - cellPadding * (SIZE + 1)) / SIZE;
|
||||
const boardX = (W - boardSize) / 2;
|
||||
const boardY = safeTop + 20 + (availH - boardSize) / 2;
|
||||
|
||||
// Board background
|
||||
ctx.fillStyle = '#bbada0';
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(boardX, boardY, boardSize, boardSize, 10);
|
||||
ctx.fill();
|
||||
|
||||
// Tiles
|
||||
for (let r = 0; r < SIZE; r++) {
|
||||
for (let c = 0; c < SIZE; c++) {
|
||||
const x = boardX + cellPadding + c * (cellSize + cellPadding);
|
||||
const y = boardY + cellPadding + r * (cellSize + cellPadding);
|
||||
const value = grid[r][c];
|
||||
|
||||
ctx.fillStyle = COLORS[value] || '#3c3a32';
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(x, y, cellSize, cellSize, 6);
|
||||
ctx.fill();
|
||||
|
||||
if (value > 0) {
|
||||
ctx.fillStyle = TEXT_COLORS[value] || '#f9f6f2';
|
||||
const fontSize = value >= 1024 ? cellSize * 0.35 : value >= 128 ? cellSize * 0.4 : cellSize * 0.5;
|
||||
ctx.font = `bold ${fontSize}px Arial`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(value, x + cellSize / 2, y + cellSize / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Controls hint
|
||||
ctx.fillStyle = '#776e65';
|
||||
ctx.font = '14px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'alphabetic';
|
||||
ctx.fillText('Swipe or Arrow Keys to move tiles', W / 2, safeBottom + 25);
|
||||
|
||||
// Win message
|
||||
if (won && !gameOver) {
|
||||
ctx.fillStyle = 'rgba(237, 194, 46, 0.5)';
|
||||
ctx.fillRect(boardX, boardY, boardSize, boardSize);
|
||||
ctx.fillStyle = '#f9f6f2';
|
||||
ctx.font = 'bold 36px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText('You Win!', W / 2, (safeTop + safeBottom) / 2);
|
||||
ctx.font = '18px Arial';
|
||||
ctx.fillText('Keep playing!', W / 2, (safeTop + safeBottom) / 2 + 40);
|
||||
}
|
||||
|
||||
// Game over
|
||||
if (gameOver) {
|
||||
ctx.fillStyle = 'rgba(238, 228, 218, 0.85)';
|
||||
ctx.fillRect(boardX, boardY, boardSize, boardSize);
|
||||
ctx.fillStyle = '#776e65';
|
||||
ctx.font = 'bold 36px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText('Game Over!', W / 2, (safeTop + safeBottom) / 2 - 30);
|
||||
ctx.font = '24px Arial';
|
||||
ctx.fillText(`Score: ${score}`, W / 2, (safeTop + safeBottom) / 2 + 10);
|
||||
|
||||
ctx.fillStyle = '#8f7a66';
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(W / 2 - 80, (safeTop + safeBottom) / 2 + 40, 160, 45, 8);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#f9f6f2';
|
||||
ctx.font = 'bold 16px Arial';
|
||||
ctx.fillText('TAP TO RESTART', W / 2, (safeTop + safeBottom) / 2 + 68);
|
||||
}
|
||||
}
|
||||
|
||||
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