From 42435847062448dad1977d084684406a85e52b21 Mon Sep 17 00:00:00 2001 From: Jevinca Marvella Date: Tue, 16 Dec 2025 01:25:10 +0700 Subject: [PATCH] Coment section --- 2048_Controls.js | 95 +++++++++++++++++++++++---------- 2048_Logic.js | 134 ++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 174 insertions(+), 55 deletions(-) diff --git a/2048_Controls.js b/2048_Controls.js index 3aa7108..77a647f 100644 --- a/2048_Controls.js +++ b/2048_Controls.js @@ -1,33 +1,58 @@ -/* ------------------------ - 6. INPUT HANDLER - ------------------------ - */ +/* ========================================== + 2048 CONTROLS + ========================================== + fungsi utama: + 1. handleKey() - mendeteksi keyboard (Arrow/WASD) + 2. Touch Events - meneteksi swipe mobile + 3. inputLocked - prevent konflik saat panel volume buka + ========================================== */ +// flag untuk blokir input game saat setting panel aktif let inputLocked = false; +/* ========================================== + KEYBOARD INPUT HANDLER + ========================================== */ function handleKey(e) { - if (isMoving || inputLocked) return; // ⛔ BLOK kalau sedang buka sound panel + // Blokir input kalau: + // 1. Tile sedang bergerak (isMoving = true) + // 2. Panel volume sedang dibuka (inputLocked = true) + if (isMoving || inputLocked) return; - let moved = false; const k = e.key; - if (k === "ArrowLeft" || k === "a" || k === "A") { e.preventDefault(); moved = moveLeft(); } - else if (k === "ArrowRight" || k === "d" || k === "D") { e.preventDefault(); moved = moveRight(); } - else if (k === "ArrowUp" || k === "w" || k === "W") { e.preventDefault(); moved = moveUp(); } - else if (k === "ArrowDown" || k === "s" || k === "S") { e.preventDefault(); moved = moveDown(); } + // Deteksi 4 arah: Arrow keys atau WASD + if (k === "ArrowLeft" || k === "a" || k === "A") { + e.preventDefault(); // Cegah scroll halaman + moved = moveLeft(); } + else if (k === "ArrowRight" || k === "d" || k === "D") { + e.preventDefault(); + moved = moveRight(); } + else if (k === "ArrowUp" || k === "w" || k === "W") { + e.preventDefault(); + moved = moveUp(); } + else if (k === "ArrowDown" || k === "s" || k === "S") { + e.preventDefault(); + moved = moveDown(); } - if (moved) { - isMoving = true; + // Kalau tile berhasil bergerak + if (moved) { + isMoving = true; // Lock input biar nggak spam + + // Delay 100ms untuk animasi selesai setTimeout(() => { - const added = addNewTile(); + const added = addNewTile(); // Spawn tile baru + + // Cek game over: board penuh ATAU nggak bisa move if (!added || !canMove()) { setTimeout(() => showGameOver(), 300); } - isMoving = false; + isMoving = false; // Unlock input }, 100); + } else { - // Shake effect on invalid move + //Invalid move - Kasih feedback shake const b = document.getElementById("board"); if (b) { b.classList.add("shake"); @@ -36,37 +61,49 @@ function handleKey(e) { } } -/* Touch Swipe */ +/* ========================================== + TOUCH/SWIPE INPUT HANDLER (Mobile) + ========================================== */ + +// Simpan posisi saat awal sentuhan let touchStartX = 0; let touchStartY = 0; - +// Event 1: Catat posisi awal saat jari menyentuh layar document.addEventListener("touchstart", function (e) { - if (inputLocked) return; // ⛔ jangan catat swipe + if (inputLocked) return; // Jangan catat swipe kalau panel volume aktif const t = e.touches[0]; - touchStartX = t.clientX; - touchStartY = t.clientY; -}, { passive: true }); - + touchStartX = t.clientX; // Posisi X awal + touchStartY = t.clientY; // Posisi Y awal +}, { passive: true }); // Passive = performa lebih smooth +// Event 2: Deteksi arah swipe saat jari diangkat document.addEventListener("touchend", function (e) { - if (isMoving || inputLocked) return; // ⛔ swipe diblok saat sound panel aktif + if (isMoving || inputLocked) return; // cek isMoving DAN inputLocked const t = e.changedTouches[0]; - const dx = t.clientX - touchStartX; - const dy = t.clientY - touchStartY; + + // Hitung selisih posisi (delta) + const dx = t.clientX - touchStartX; //Horizontal movement + const dy = t.clientY - touchStartY; //Vertical movement let moved = false; + // Tentukan arah swipe berdasarkan delta terbesar if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 30) { - if (dx > 0) moved = moveRight(); - else moved = moveLeft(); + // Swipe HORIZONTAL (kiri/kanan) + // Minimal 30px biar nggak terlalu sensitif + if (dx > 0) moved = moveRight(); // swipe kanan + else moved = moveLeft(); // swipe kiri + } else if (Math.abs(dy) > 30) { - if (dy > 0) moved = moveDown(); - else moved = moveUp(); + // Swipe VERTICAL (atas/bawah) + if (dy > 0) moved = moveDown(); // swipe bawah + else moved = moveUp(); // swipe atas } + // Logic sama seperti keyboard handler if (moved) { isMoving = true; setTimeout(() => { diff --git a/2048_Logic.js b/2048_Logic.js index 2d7bf53..708be7b 100644 --- a/2048_Logic.js +++ b/2048_Logic.js @@ -1,79 +1,138 @@ -/* ------------------------ - 5. GAME LOGIC - ------------------------ */ +/* ========================================== + 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; + 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"); - playSound(audio.pop); + tile.classList.add("new"); // Trigger animasi pop + playSound(audio.pop); // Sound effect setTimeout(() => tile.classList.remove("new"), 300); } - updateTile(spot.r, spot.c, 2); + + 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 = []; - let mergeCount = 0; + 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]) { - row[i] = row[i] * 2; + // MERGE! Tile sama ketemu + row[i] = row[i] * 2; // Tile pertama jadi 2x lipat - playSound(audio.merge); + playSound(audio.merge); // Sound effect merge + // Haptic feedback (getaran) di mobile if (navigator.vibrate) { navigator.vibrate([80, 20, 80]); } - currentScore += row[i]; - row[i + 1] = 0; + currentScore += row[i]; // Tambah score + row[i + 1] = 0; // Tile kedua hilang (jadi 0) + mergedThisMove = true; - mergedPositions.push(i); + 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); - return { row, merged: mergedThisMove, mergedPositions, mergeCount }; + 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 */ +/* ========================================== + 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 = []; - mergesInCurrentMove = 0; + 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 }); @@ -81,6 +140,7 @@ function moveLeft() { } } + // Kalau ada pergerakan, update visual + effect if (moved) { refreshBoard(); triggerComboEffect(mergedCells, mergesInCurrentMove); @@ -88,23 +148,28 @@ function moveLeft() { 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(); + 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; + const c = 3 - pos; // Mirror position mergedCells.push({ r, c }); }); } @@ -117,14 +182,19 @@ function moveRight() { 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]; @@ -146,15 +216,18 @@ function moveUp() { 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(); + 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]; @@ -162,9 +235,10 @@ function moveDown() { mergesInCurrentMove += mergeCount; + // Convert posisi merge ke koordinat asli (dari bawah) if (mergedPositions && mergedPositions.length > 0) { mergedPositions.forEach(pos => { - const r = 3 - pos; + const r = 3 - pos; // Mirror position mergedCells.push({ r, c }); }); } @@ -177,19 +251,27 @@ function moveDown() { 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; + 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; } \ No newline at end of file