312 lines
7.8 KiB
JavaScript
312 lines
7.8 KiB
JavaScript
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 });
|
|
|
|
|