let board = []; let score = 0; // --- Audio setup --- const audio = { bg: new Audio("bgmusic.mp3"), pop: new Audio("pop.mp3"), merge: new Audio("merge.wav") }; // lower default volumes audio.bg.volume = 0.25; audio.pop.volume = 0.9; audio.merge.volume = 0.9; audio.bg.loop = true; // try to play background music; may be blocked until user interaction function tryPlayBg() { audio.bg.play().catch(() => { // autoplay blocked — will try again on first user keypress or click const unlock = () => { audio.bg.play().catch(() => {}); window.removeEventListener("keydown", unlock); window.removeEventListener("click", unlock); }; window.addEventListener("keydown", unlock, { once: true }); window.addEventListener("click", unlock, { once: true }); }); } // --- DOM ready: setup board and initial tiles --- document.addEventListener("DOMContentLoaded", () => { setupBoard(); addNewTile(); addNewTile(); tryPlayBg(); // keyboard controls (also used to unlock audio) document.addEventListener("keydown", handleKey); }); // ---------------------------- // SETUP BOARD (render tiles ONCE) // ---------------------------- function setupBoard() { board = []; score = 0; updateScore(); const container = document.getElementById("board"); if (!container) { console.error("Board element not found (#board)."); return; } // empty container and create fixed 4x4 cells container.innerHTML = ""; for (let r = 0; r < 4; r++) { board[r] = []; for (let c = 0; c < 4; c++) { board[r][c] = 0; const tile = document.createElement("div"); tile.id = `${r}-${c}`; tile.className = "tile"; // base class only // ensure box sizing not influenced by text nodes tile.style.boxSizing = "border-box"; container.appendChild(tile); } } } // ---------------------------- // UPDATE UI FOR SINGLE TILE // ---------------------------- function updateTile(row, col, num) { const tile = document.getElementById(`${row}-${col}`); if (!tile) return; // reset classes to base tile.className = "tile"; // force reflow to allow re-adding 'new' animation class reliably void tile.offsetWidth; if (num > 0) { tile.textContent = num; // add tile class for color (expects classes like tile-2, tile-4, ...) tile.classList.add("tile-" + num); // make numbers visually white-neon glow if desired: // ensure text is centered by CSS; no inline style needed } else { tile.textContent = ""; } } // updates entire board DOM from board array function refreshBoard() { for (let r = 0; r < 4; r++) { for (let c = 0; c < 4; c++) { updateTile(r, c, board[r][c]); } } updateScore(); } // ---------------------------- // SCORE UI // ---------------------------- function updateScore() { const el = document.getElementById("score"); if (el) el.textContent = score; } // ---------------------------- // ADD NEW TILE (value 2, play pop sound + animation) // ---------------------------- function addNewTile() { const empty = []; for (let r = 0; r < 4; r++) { for (let c = 0; c < 4; c++) { if (board[r][c] === 0) empty.push({ r, c }); } } if (empty.length === 0) return false; const spot = empty[Math.floor(Math.random() * empty.length)]; board[spot.r][spot.c] = 2; const tile = document.getElementById(`${spot.r}-${spot.c}`); if (tile) { tile.classList.add("new"); // play pop sound playSound(audio.pop); // remove 'new' class after animation completes to keep DOM tidy tile.addEventListener("animationend", function handler() { tile.classList.remove("new"); tile.removeEventListener("animationend", handler); }); // update text/color updateTile(spot.r, spot.c, 2); } else { // fallback updateTile(spot.r, spot.c, 2); } return true; } // helper to play sound safely function playSound(soundObj) { try { soundObj.currentTime = 0; soundObj.play().catch(() => { // suppressed (autoplay or other) }); } catch (e) { /* ignore */ } } // ---------------------------- // INPUT HANDLING // ---------------------------- function handleKey(e) { let moved = false; if (e.key === "ArrowLeft") moved = moveLeft(); else if (e.key === "ArrowRight") moved = moveRight(); else if (e.key === "ArrowUp") moved = moveUp(); else if (e.key === "ArrowDown") moved = moveDown(); if (moved) { // after a successful move: add tile, refresh board, play bg or unlock addNewTile(); refreshBoard(); } } // ---------- MOVE HELPERS ---------- function filterZero(row) { return row.filter(n => n !== 0); } function slide(row) { // row is an array of length 4 row = filterZero(row); let mergedThisMove = false; for (let i = 0; i < row.length - 1; i++) { if (row[i] === row[i + 1]) { row[i] = row[i] * 2; // play merge sound and vibrate playSound(audio.merge); if (navigator.vibrate) navigator.vibrate(30); score += row[i]; row[i + 1] = 0; mergedThisMove = true; } } row = filterZero(row); while (row.length < 4) row.push(0); return { row, merged: mergedThisMove }; } // Compare arrays helper function arraysEqual(a, b) { return a.length === b.length && a.every((v, i) => v === b[i]); } function moveLeft() { let moved = false; for (let r = 0; r < 4; r++) { const { row: newRow } = slide(board[r]); if (!arraysEqual(newRow, board[r])) moved = true; board[r] = newRow; } if (moved) updateAfterMove(); return moved; } function moveRight() { let moved = false; for (let r = 0; r < 4; r++) { let reversed = [...board[r]].reverse(); const { row: slid } = slide(reversed); let newRow = slid.reverse(); if (!arraysEqual(newRow, board[r])) moved = true; board[r] = newRow; } if (moved) updateAfterMove(); return moved; } function moveUp() { let moved = false; for (let c = 0; c < 4; c++) { const col = [board[0][c], board[1][c], board[2][c], board[3][c]]; const { row: newCol } = slide(col); for (let r = 0; r < 4; r++) { if (board[r][c] !== newCol[r]) moved = true; board[r][c] = newCol[r]; } } if (moved) updateAfterMove(); return moved; } function moveDown() { let moved = false; for (let c = 0; c < 4; c++) { const col = [board[3][c], board[2][c], board[1][c], board[0][c]]; const { row: slid } = slide(col); const newCol = slid.reverse(); for (let r = 0; r < 4; r++) { if (board[r][c] !== newCol[r]) moved = true; board[r][c] = newCol[r]; } } if (moved) updateAfterMove(); return moved; } function updateAfterMove() { // update all tiles now (this keeps sizes stable) refreshBoard(); // update score DOM updateScore(); // small debounce is not required—moves are sequential } // ---------------------------- // RESTART & HOME // ---------------------------- function restartGame() { setupBoard(); addNewTile(); addNewTile(); refreshBoard(); } function goHome() { // stops music to prevent continuing on homepage try { audio.bg.pause(); audio.bg.currentTime = 0; } catch (e) {} window.location.href = "Homepage.html"; } // ---------------------------- // OPTIONAL: touch swipe for mobile // ---------------------------- let touchStartX = 0; let touchStartY = 0; document.addEventListener("touchstart", function (e) { const t = e.touches[0]; touchStartX = t.clientX; touchStartY = t.clientY; }, { passive: true }); document.addEventListener("touchend", function (e) { const t = e.changedTouches[0]; const dx = t.clientX - touchStartX; const dy = t.clientY - touchStartY; if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 30) { if (dx > 0) moveRight() && addNewTile() && refreshBoard(); else moveLeft() && addNewTile() && refreshBoard(); } else if (Math.abs(dy) > 30) { if (dy > 0) moveDown() && addNewTile() && refreshBoard(); else moveUp() && addNewTile() && refreshBoard(); } }, { passive: true }); /* End of file */