/* ========================================== 2048 LOGIC ========================================== fungsi inti: 1. addNewTile() - spawn tile baru (angka 2) 2. slide() - algoritma merge tile 3. move functions - 4 arah pergerakan 4. canMove() - Cek game over ========================================== */ /* ========================================== SPAWN TILE BARU ========================================== */ function addNewTile() { // Step 1: Cari semua cell kosong (value = 0) 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 }); } } // Kalau board penuh, return false (game over) if (empty.length === 0) return false; // Step 2: Pilih 1 posisi random dari cell kosong const spot = empty[Math.floor(Math.random() * empty.length)]; board[spot.r][spot.c] = 2; // Tile baru selalu angka 2 // Step 3: Animasi "new" + sound effect const tile = document.getElementById(`${spot.r}-${spot.c}`); if (tile) { tile.classList.add("new"); // Trigger animasi pop playSound(audio.pop); // Sound effect setTimeout(() => tile.classList.remove("new"), 300); } updateTile(spot.r, spot.c, 2); // Update visual tile return true; } /* ========================================== HELPER: FILTER ANGKA NOL ========================================== */ function filterZero(row) { // Buang semua 0 dari array // Contoh: [2, 0, 2, 0] → [2, 2] return row.filter(n => n !== 0); } /* ========================================== ALGORITMA SLIDE & MERGE (CORE!) ========================================== Ini yang bikin 2+2=4, 4+4=8, dst ========================================== */ function slide(row) { // Step 1: Filter zero - kumpulkan tile // [2, 0, 2, 4] → [2, 2, 4] row = filterZero(row); let mergedThisMove = false; let mergedPositions = []; // Posisi tile yang di-merge (untuk animasi) let mergeCount = 0; // Hitung berapa kali merge (untuk combo) // Step 2: Loop cek tile sebelahan for (let i = 0; i < row.length - 1; i++) { if (row[i] === row[i + 1]) { // MERGE! Tile sama ketemu row[i] = row[i] * 2; // Tile pertama jadi 2x lipat playSound(audio.merge); // Sound effect merge // Haptic feedback (getaran) di mobile if (navigator.vibrate) { navigator.vibrate([80, 20, 80]); } currentScore += row[i]; // Tambah score row[i + 1] = 0; // Tile kedua hilang (jadi 0) mergedThisMove = true; mergedPositions.push(i); // Simpan posisi untuk animasi mergeCount++; } } // Step 3: Filter zero lagi & padding // [4, 0, 4] → [4, 4] → [4, 4, 0, 0] row = filterZero(row); while (row.length < 4) row.push(0); // Padding 0 di kanan return { row, merged: mergedThisMove, mergedPositions, mergeCount }; } /* ========================================== HELPER: CEK ARRAY SAMA ATAU TIDAK ========================================== */ function arraysEqual(a, b) { // Bandingin setiap elemen // Dipakai untuk cek apakah board berubah setelah move return a.length === b.length && a.every((v, i) => v === b[i]); } /* ========================================== MOVE FUNCTIONS - 4 ARAH PERGERAKAN ========================================== Semua punya struktur yang sama: 1. Loop setiap baris/kolom 2. Panggil slide() untuk merge 3. Cek perubahan dengan arraysEqual() 4. Refresh board kalau ada perubahan ========================================== */ /* MOVE LEFT - Geser ke kiri */ function moveLeft() { let moved = false; let mergedCells = []; // Track cell yang di-merge mergesInCurrentMove = 0; // Reset combo counter // Loop setiap baris (horizontal) for (let r = 0; r < 4; r++) { const { row: newRow, mergedPositions, mergeCount } = slide(board[r]); // Cek apakah row berubah if (!arraysEqual(newRow, board[r])) moved = true; board[r] = newRow; mergesInCurrentMove += mergeCount; // Simpan posisi cell yang merge untuk combo effect if (mergedPositions && mergedPositions.length > 0) { mergedPositions.forEach(c => { mergedCells.push({ r, c }); }); } } // Kalau ada pergerakan, update visual + effect if (moved) { refreshBoard(); triggerComboEffect(mergedCells, mergesInCurrentMove); } return moved; } /* MOVE RIGHT - Geser ke kanan */ function moveRight() { let moved = false; let mergedCells = []; mergesInCurrentMove = 0; // TRICK: Reverse → slide → reverse lagi // Supaya bisa pakai logic slide yang sama for (let r = 0; r < 4; r++) { let reversed = [...board[r]].reverse(); const { row: slid, mergedPositions, mergeCount } = slide(reversed); let newRow = slid.reverse(); // Balik lagi ke posisi asli if (!arraysEqual(newRow, board[r])) moved = true; board[r] = newRow; mergesInCurrentMove += mergeCount; // Convert posisi merge ke koordinat asli (dari kanan) if (mergedPositions && mergedPositions.length > 0) { mergedPositions.forEach(pos => { const c = 3 - pos; // Mirror position mergedCells.push({ r, c }); }); } } if (moved) { refreshBoard(); triggerComboEffect(mergedCells, mergesInCurrentMove); } return moved; } /* MOVE UP - Geser ke atas */ function moveUp() { let moved = false; let mergedCells = []; mergesInCurrentMove = 0; // Loop setiap kolom (vertical) for (let c = 0; c < 4; c++) { // Ambil kolom vertikal sebagai array const col = [board[0][c], board[1][c], board[2][c], board[3][c]]; const { row: newCol, mergedPositions, mergeCount } = slide(col); // Update board per row for (let r = 0; r < 4; r++) { if (board[r][c] !== newCol[r]) moved = true; board[r][c] = newCol[r]; } mergesInCurrentMove += mergeCount; if (mergedPositions && mergedPositions.length > 0) { mergedPositions.forEach(r => { mergedCells.push({ r, c }); }); } } if (moved) { refreshBoard(); triggerComboEffect(mergedCells, mergesInCurrentMove); } return moved; } /* MOVE DOWN - Geser ke bawah */ function moveDown() { let moved = false; let mergedCells = []; mergesInCurrentMove = 0; for (let c = 0; c < 4; c++) { //TRICK: Ambil kolom dari bawah ke atas (reversed) const col = [board[3][c], board[2][c], board[1][c], board[0][c]]; const { row: slid, mergedPositions, mergeCount } = slide(col); const newCol = slid.reverse(); // Balik ke urutan normal for (let r = 0; r < 4; r++) { if (board[r][c] !== newCol[r]) moved = true; board[r][c] = newCol[r]; } mergesInCurrentMove += mergeCount; // Convert posisi merge ke koordinat asli (dari bawah) if (mergedPositions && mergedPositions.length > 0) { mergedPositions.forEach(pos => { const r = 3 - pos; // Mirror position mergedCells.push({ r, c }); }); } } if (moved) { refreshBoard(); triggerComboEffect(mergedCells, mergesInCurrentMove); } return moved; } /* ========================================== CEK GAME OVER ========================================== */ function canMove() { // Kondisi 1: Ada cell kosong? for (let r = 0; r < 4; r++) { for (let c = 0; c < 4; c++) { if (board[r][c] === 0) return true; // Masih ada ruang } } // Kondisi 2: Ada tile sebelahan yang bisa di-merge? for (let r = 0; r < 4; r++) { for (let c = 0; c < 4; c++) { const current = board[r][c]; //cek kanan if (c < 3 && board[r][c + 1] === current) return true; //cek bawah if (r < 3 && board[r + 1][c] === current) return true; } } // Kalau kedua kondisi false → STUCK = Game Over return false; }