diff --git a/assets/css/style.css b/assets/css/style.css new file mode 100644 index 0000000..564ea61 --- /dev/null +++ b/assets/css/style.css @@ -0,0 +1,114 @@ +body { + font-family: Inter, system-ui, Arial, sans-serif; + margin: 0; + background: #f2f6f9; + color: #111; +} + +.topbar { + width: 100%; + padding: 12px 0; + background: #061d1d; + color: white; + text-align: center; + font-size: 17px; + font-weight: bold; +} + +.auth-card { + max-width: 360px; + margin: 6vh auto; + padding: 20px; + background: #fff; + border-radius: 8px; + box-shadow: 0 6px 18px rgba(2,6,23,0.08); +} + +.auth-card h1 { + margin: 0 0 12px; +} + +.auth-card label { + display: block; + margin: 8px 0; +} + +.auth-card input { + width: 95%; + padding: 8px; + border: 1px solid #d1d5db; + border-radius: 6px; +} + +.auth-card button { + width: 100%; + padding: 10px; + margin-top: 12px; + border: 0; + border-radius: 8px; + background: #0ea5a4; + color: #fff; + font-weight: 600; +} + +.error { + background: #fee2e2; + color: #991b1b; + padding: 8px; + border-radius: 6px; + margin-bottom: 8px; +} + +.success { + background: #ecfccb; + color: #365314; + padding: 8px; + border-radius: 6px; + margin-bottom: 8px; +} + +/* GAME AREA */ +.game-wrap { + display: flex; + justify-content: center; + padding: 24px; +} + +.board-wrap { + position: relative !important; + z-index: 1 !important; +} + +.controls { + display: flex; + gap: 12px; + flex-wrap: wrap; + justify-content: center; +} + +button { + padding: 10px 16px; + font-size: 15px; + cursor: pointer; + border-radius: 6px; + border: none; + background: #334155; + color: white; +} + +button:hover { + background: #1e293b; +} + +canvas#board { + position: relative !important; + z-index: 999999 !important; + background: #000 !important; + border: 2px solid red !important; + + width: 640px !important; + height: 640px !important; + + display: block !important; + flex-shrink: 0 !important; +} diff --git a/assets/js/game.js b/assets/js/game.js new file mode 100644 index 0000000..7ee8af4 --- /dev/null +++ b/assets/js/game.js @@ -0,0 +1,431 @@ +const canvas = document.getElementById("board"); +if (!canvas) alert("Canvas tidak ditemukan! ID harus 'board'"); +const ctx = canvas.getContext("2d"); + +// ensure canvas actual pixel size used for tileSize calculation +const CANVAS_W = canvas.width; +const CANVAS_H = canvas.height; + +const size = 8; +const tileSize = CANVAS_W / size; + +let board = []; +let currentTurn = 1; // 1 = red, 2 = white +let selected = null; +let history = []; +let gameOver = false; + +let aiEnabled = (typeof GAME_MODE !== "undefined" && GAME_MODE === "pvai"); +let aiPlays = 2; +let aiThinkingDelay = 300; + +let hintsEnabled = (typeof ENABLE_HINTS !== "undefined") ? !!ENABLE_HINTS : true; +/********** SOUND EFFECT **********/ +const SFX = { + move: new Audio("assets/sound/move.mp3"), + capture: new Audio("assets/sound/capture.mp3") +}; + +let soundEnabled = true; +function playSound(name){ + if(!soundEnabled || !SFX[name]) return; + SFX[name].currentTime = 0; + SFX[name].play().catch(()=>{}); +} + + +/********** TIMER (unchanged) **********/ +let timerSeconds = 0; +let timerInterval = null; +let timerRunning = false; +function startTimer(){ if(timerRunning) return; timerRunning=true; timerInterval=setInterval(()=>{ timerSeconds++; const min=String(Math.floor(timerSeconds/60)).padStart(2,'0'); const sec=String(timerSeconds%60).padStart(2,'0'); const dom=document.getElementById('timer'); if(dom) dom.textContent = `${min}:${sec}`; },1000); } +function stopTimer(){ timerRunning=false; clearInterval(timerInterval); } +function resetTimer(){ stopTimer(); timerSeconds=0; const dom=document.getElementById('timer'); if(dom) dom.textContent='00:00'; } + +/********** HELPERS **********/ +function opponentOf(p){ return p===1?2:1; } +function inside(r,c){ return Number.isInteger(r) && Number.isInteger(c) && r>=0 && r=0 && cArray(size).fill(0)); + for(let r=0;r<3;r++) for(let c=0;c triggerAI(), 120); + } +} + +/********** DRAW **********/ +function draw(){ + // safety: re-compute tileSize if canvas resized via CSS (keep consistent) + // (we keep original tileSize computed from initial canvas.width) + ctx.clearRect(0,0,CANVAS_W,CANVAS_H); + + for(let r=0;r60) history.shift(); } + +/********** GENERATE MOVES **********/ +function generateMovesForPiece(r,c,player){ + const v = board[r][c]; + if(v===0) return []; + const isKing = (v===3||v===4); + const dir = player===1? -1 : 1; + const res = []; + + const dirs = isKing ? [[1,1],[1,-1],[-1,1],[-1,-1]] : [[dir,1],[dir,-1]]; + for(const [dr,dc] of dirs){ + const r2 = r+dr, c2 = c+dc; + if(inside(r2,c2) && board[r2][c2]===0) res.push({r1:r,c1:c,r2:r2,c2:c2,capture:false}); + } + + const capDirs = [[1,1],[1,-1],[-1,1],[-1,-1]]; + for(const [dr,dc] of capDirs){ + const mr = r+dr, mc = c+dc; + const r2 = r+dr*2, c2 = c+dc*2; + if(!inside(mr,mc) || !inside(r2,c2)) continue; + if(board[r2][c2] !== 0) continue; + const mid = board[mr][mc]; + if(mid !== 0 && !belongsTo(mid, player)){ + if(isKing || dr === dir) res.push({r1:r,c1:c,r2:r2,c2:c2,capture:true, mr, mc}); + } + } + + return res; +} + +function getAllMovesFor(player){ + const all = []; + for(let r=0;r !!m.capture); + const nonCaptureMoves = moves.filter(m => !m.capture); + + // jika ada capture -> pilih di antara capture (prioritas tetap di sini) + if(captureMoves.length > 0){ + let bestScore = -Infinity, best = []; + for(const m of captureMoves){ + // skor sederhana untuk capture: dasar tinggi + prefer capture yang mengarah ke king sedikit + let s = 200; + // small preference untuk multi-direction / maju (lebih baik) + s += (Math.abs(m.dr || 0) + Math.abs(m.dc || 0)) * 5; + // sedikit randomness untuk variasi + s += Math.random() * 20; + if(s > bestScore){ bestScore = s; best = [m]; } else if(s === bestScore){ best.push(m); } + } + const chosen = best[Math.floor(Math.random()*best.length)]; + setTimeout(()=>{ tryMove(chosen.r1, chosen.c1, chosen.r2, chosen.c2); draw(); checkWinCondition(); }, aiThinkingDelay); + return; + } + + // jika tidak ada capture -> prioritaskan non-promotion moves bila memungkinkan + const nonPromotion = nonCaptureMoves.filter(m => m.r2 !== promotionRow); + let pool = nonPromotion.length > 0 ? nonPromotion : nonCaptureMoves; // jika semua move adalah promotion, gunakan semua + + // beri skor supaya AI tidak selalu memilih move yang mempromote: prefer pusat & variasi + let bestScore = -Infinity, best = []; + const centerR = (size - 1) / 2, centerC = (size - 1) / 2; + for(const m of pool){ + // prefer bergerak menuju pusat papan (lebih "aman" / seimbang) + const distToCenter = Math.abs(m.r2 - centerR) + Math.abs(m.c2 - centerC); + // kecil penalti jika langkah ini menuju promosi (agar tidak selalu memilih king) + const promoPenalty = (m.r2 === promotionRow) ? 40 : 0; + // prefer maju sedikit (untuk white: r increasing; for red: r decreasing) + const advanceBonus = (aiPlays === 2) ? (m.r2) : (size - 1 - m.r2); + + // skor komposit + let s = 50; // baseline + s += (100 - distToCenter*10); // lebih kecil jarak ke pusat => lebih besar skor + s += advanceBonus * 0.5; // sedikit nilai untuk maju + s -= promoPenalty; // penalti promosi agar tidak selalu memilih king + s += Math.random() * 20; // variasi randomisasi + + if(s > bestScore){ bestScore = s; best = [m]; } else if(s === bestScore){ best.push(m); } + } + + const chosen = best[Math.floor(Math.random()*best.length)]; + setTimeout(()=>{ tryMove(chosen.r1, chosen.c1, chosen.r2, chosen.c2); draw(); checkWinCondition(); }, aiThinkingDelay); +} + + + +function triggerAI(){ setTimeout(()=>{ if(aiEnabled && currentTurn===aiPlays && !gameOver) aiMakeMove(); }, 120); } + +/********** MOUSE **********/ +canvas.addEventListener("click", (e)=>{ + console.debug("canvas click", {gameOver, aiEnabled, currentTurn}); + if(gameOver) return; + if(aiEnabled && currentTurn === aiPlays) { console.debug("click ignored: AI turn"); return; } + + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left, y = e.clientY - rect.top; + const c = Math.floor(x / tileSize), r = Math.floor(y / tileSize); + + console.debug("click coords", {x,y, r, c}); + + if(!inside(r,c)) return; + const v = board[r][c]; + + if(selected){ + if(tryMove(selected.r, selected.c, r, c)){ + selected = null; + } else if(v!==0 && belongsTo(v, currentTurn)){ + selected = {r,c}; + } else { + selected = null; + } + } else { + if(v!==0 && belongsTo(v, currentTurn)){ + selected = {r,c}; + } + } + draw(); +}); + +/********** KEY H TOGGLE HINTS **********/ +document.addEventListener("keydown", (e)=>{ if(e.key.toLowerCase()==='h'){ hintsEnabled = !hintsEnabled; draw(); } }); + +/********** BUTTONS **********/ +const newBtn = document.getElementById("newBtn"); +if(newBtn) newBtn.onclick = ()=> resetBoard(); + +const undoBtn = document.getElementById("undoBtn"); +if(undoBtn) undoBtn.onclick = ()=>{ if(history.length>0){ board = history.pop(); draw(); } }; + +/********** LEADERBOARD (unchanged) **********/ +async function loadLeaderboard(){ + const box = document.getElementById("leaderboardList"); + if(!box) return; + try{ + const res = await fetch("load_leaderboard.php"); + if(!res.ok) throw new Error("Network"); + const data = await res.json(); + box.innerHTML = ""; + if(!data || data.length===0){ box.innerHTML = "

Belum ada data

"; return; } + data.forEach((row,i)=>{ + const div = document.createElement("div"); + div.className = "lb-row" + (typeof CURRENT_USERNAME!=='undefined' && row.username === CURRENT_USERNAME ? " me" : ""); + div.innerHTML = `${i+1}. ${escapeHtml(row.username)}${row.wins}W | ${row.losses}L`; + box.appendChild(div); + }); + }catch(err){ console.error("loadLeaderboard error", err); } +} +function escapeHtml(s){ if(!s) return ''; return String(s).replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''})[m]); } +window.addEventListener("load", ()=>{ loadLeaderboard(); setInterval(loadLeaderboard,10000); }); + +async function saveResult(result){ + if(typeof CURRENT_USER_ID === 'undefined') return; + try{ + const res = await fetch("save_result.php",{ method:'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({result}) }); + if(res.ok) await loadLeaderboard(); + }catch(err){ console.error("saveResult error", err); } +} + +/********** POPUP **********/ +function showEnd(title, msg, result){ + if(document.getElementById("endPopup")) return; + gameOver = true; + stopTimer(); + + const div = document.createElement("div"); + div.id = "endPopup"; + div.style = `position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.75);display:flex;justify-content:center;align-items:center;z-index:999999999;pointer-events:auto;`; + div.innerHTML = `

${title}

${msg}

`; + document.body.appendChild(div); + // save result async + saveResult(result).catch(()=>{}); + document.getElementById("popupNew").onclick = ()=>{ div.remove(); resetBoard(); }; + document.getElementById("popupClose").onclick = ()=> div.remove(); +} + +/********** START **********/ +resetBoard(); \ No newline at end of file diff --git a/assets/sound/capture.mp3 b/assets/sound/capture.mp3 new file mode 100644 index 0000000..9971ae8 Binary files /dev/null and b/assets/sound/capture.mp3 differ diff --git a/assets/sound/move.mp3 b/assets/sound/move.mp3 new file mode 100644 index 0000000..50e883b Binary files /dev/null and b/assets/sound/move.mp3 differ diff --git a/dam_db.sql b/dam_db.sql new file mode 100644 index 0000000..13ff8a5 --- /dev/null +++ b/dam_db.sql @@ -0,0 +1,8 @@ +USE dam_db; + +CREATE TABLE IF NOT EXISTS users ( +id INT AUTO_INCREMENT PRIMARY KEY, +username VARCHAR(50) NOT NULL UNIQUE, +password VARCHAR(255) NOT NULL, +created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB; \ No newline at end of file diff --git a/db.php b/db.php new file mode 100644 index 0000000..ee01fec --- /dev/null +++ b/db.php @@ -0,0 +1,23 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + ] + ); +} catch (Exception $e) { + die('DB ERROR: ' . $e->getMessage()); +} diff --git a/game.php b/game.php new file mode 100644 index 0000000..57d897b --- /dev/null +++ b/game.php @@ -0,0 +1,189 @@ + + + + + + + +Dam Inggris - Main + + + + + + + + +
+ Halo, | Pilih Mode +
+ Credits: Alvin, Basilius & Gray - Kelompok 11 +
+
+ + +
+ + +
+

🏆 Leaderboard

+
Memuat...
+
+ + +
+ + + +
+ + +
+ +
+ ⏱ 00:00 +
+ + + + + + +
+
+ + + + + + + + + + + + + diff --git a/index.php b/index.php new file mode 100644 index 0000000..b02ef56 --- /dev/null +++ b/index.php @@ -0,0 +1,50 @@ +prepare('SELECT id, password FROM users WHERE username = ? LIMIT 1'); +$stmt->execute([$username]); +$user = $stmt->fetch(); +if ($user && password_verify($password, $user['password'])) { +session_regenerate_id(true); +$_SESSION['user_id'] = $user['id']; +$_SESSION['username'] = $username; +header('Location: menu.php'); +exit; +} else { +$err = 'Username atau password salah.'; + } + } +} +?> + + + + + +Login - Dam Inggris + + + +
+

Login - Ke Main Menu

+
+
+ + + +
+

Belum punya akun? Daftar di sini

+
+ + \ No newline at end of file diff --git a/load_leaderboard.php b/load_leaderboard.php new file mode 100644 index 0000000..7ff9c3b --- /dev/null +++ b/load_leaderboard.php @@ -0,0 +1,23 @@ +query(" + SELECT u.id AS user_id, u.username, s.wins, s.losses + FROM users u + LEFT JOIN users_stats s ON s.user_id = u.id + GROUP BY u.id + ORDER BY s.wins DESC, s.losses ASC + LIMIT 20 + "); + + $rows = $stmt->fetchAll(); + echo json_encode($rows); + +} catch (Exception $e) { + echo json_encode([ + "error" => $e->getMessage() + ]); +} diff --git a/logout.php b/logout.php new file mode 100644 index 0000000..674f08d --- /dev/null +++ b/logout.php @@ -0,0 +1,14 @@ + \ No newline at end of file diff --git a/menu.php b/menu.php new file mode 100644 index 0000000..002163f --- /dev/null +++ b/menu.php @@ -0,0 +1,94 @@ + + + + + + +Menu Utama - Dam Inggris + + + + + + + + + + + diff --git a/mode.php b/mode.php new file mode 100644 index 0000000..f227721 --- /dev/null +++ b/mode.php @@ -0,0 +1,97 @@ + + + + + +Dam Inggris - Menu + + + + + + + + + diff --git a/register.php b/register.php new file mode 100644 index 0000000..23679cb --- /dev/null +++ b/register.php @@ -0,0 +1,83 @@ +prepare('SELECT id FROM users WHERE username = ?'); + $stmt->execute([$username]); + + if ($stmt->fetch()) { + $err = 'Username sudah digunakan.'; + } else { + // Insert ke tabel users + $hash = password_hash($password, PASSWORD_DEFAULT); + $ins = $pdo->prepare('INSERT INTO users (username, password) VALUES (?, ?)'); + $ins->execute([$username, $hash]); + + // AMBIL user_id baru + $user_id = $pdo->lastInsertId(); + + // ⬇⬇ TAMBAHKAN users_stats OTOMATIS DI SINI ⬇⬇ + $stmt = $pdo->prepare("INSERT INTO users_stats (user_id) VALUES (?)"); + $stmt->execute([$user_id]); + // ⬆⬆ TAMBAHAN WAJIB ADA ⬆⬆ + + $success = 'Pendaftaran berhasil. Silakan login.'; + } + } +} +?> + + + + + +Register - Dam Inggris + + + +
+

Daftar

+ + +
+ + + +
+ + +
+ + + + + + + +
+ +

Sudah punya akun? Login

+
+ + diff --git a/save_result.php b/save_result.php new file mode 100644 index 0000000..23d4c93 --- /dev/null +++ b/save_result.php @@ -0,0 +1,44 @@ + "Not logged in"]); + exit; +} + +$user_id = $_SESSION['user_id']; + +$input = json_decode(file_get_contents("php://input"), true); +$result = $input['result'] ?? ''; + +if (!in_array($result, ['win','loss','draw'])) { + echo json_encode(["error" => "Invalid result"]); + exit; +} + +// Pastikan row user di users_stats ADA +$stmt = $pdo->prepare("SELECT id FROM users_stats WHERE user_id = ?"); +$stmt->execute([$user_id]); + +// Jika belum ada, buat kosong +if (!$stmt->fetch()) { + $pdo->prepare("INSERT INTO users_stats (user_id, wins, losses, draws) VALUES (?, 0, 0, 0)") + ->execute([$user_id]); +} + +// Update sesuai hasil +if ($result === 'win') { + $sql = "UPDATE users_stats SET wins = wins + 1, updated_at = NOW() WHERE user_id = ?"; +} +else if ($result === 'loss') { + $sql = "UPDATE users_stats SET losses = losses + 1, updated_at = NOW() WHERE user_id = ?"; +} +else if ($result === 'draw') { + $sql = "UPDATE users_stats SET draws = draws + 1, updated_at = NOW() WHERE user_id = ?"; +} + +$pdo->prepare($sql)->execute([$user_id]); + +echo json_encode(["success" => true]); diff --git a/settings.php b/settings.php new file mode 100644 index 0000000..147a45b --- /dev/null +++ b/settings.php @@ -0,0 +1,127 @@ + + + + + + +Settings - Dam Inggris + + + + + + + +
+

Settings

+ + + +
+ + +
+ + + +
+ + ⬅ Kembali +
+ + +