2025-12-15 20:28:29 +07:00

379 lines
13 KiB
JavaScript

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 PERMAINAN
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<size && c>=0 && c<size; }
function belongsTo(v, player){
return (player===1 && (v===1||v===3)) || (player===2 && (v===2||v===4));
}
// RESET BOARD
function resetBoard(){
console.debug("resetBoard()");
board = Array.from({length:size}, ()=>Array(size).fill(0));
for(let r=0;r<3;r++) for(let c=0;c<size;c++) if((r+c)%2===1) board[r][c] = 2;
for(let r=size-3;r<size;r++) for(let c=0;c<size;c++) if((r+c)%2===1) board[r][c] = 1;
currentTurn = 1;
selected = null;
history = [];
gameOver = false;
const old = document.getElementById("endPopup");
if(old) old.remove();
resetTimer();
startTimer();
draw();
// small defer so UI painted before AI moves
if(aiEnabled && currentTurn === aiPlays){
setTimeout(()=> 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;r<size;r++){
for(let c=0;c<size;c++){
const x = c * tileSize;
const y = r * tileSize;
ctx.fillStyle = (r+c)%2===0? "#f3f4f6" : "#111827";
ctx.fillRect(x,y,tileSize,tileSize);
if(selected && selected.r===r && selected.c===c){
ctx.fillStyle = "rgba(152, 253, 0, 0.58)";
ctx.fillRect(x,y,tileSize,tileSize);
}
const v = board[r][c];
if(v!==0) drawPiece(x+tileSize/2, y+tileSize/2, v);
}
}
if(selected && hintsEnabled && !gameOver){
const moves = generateMovesForPiece(selected.r, selected.c, currentTurn);
drawMoveHints(moves);
}
}
function drawPiece(cx,cy,v){
ctx.beginPath();
ctx.arc(cx,cy,tileSize*0.35,0,Math.PI*2);
ctx.fillStyle = (v===1||v===3) ? "#ef4444" : "#ffffff";
ctx.fill(); ctx.closePath();
if(v===3||v===4){
ctx.beginPath();
ctx.arc(cx,cy,tileSize*0.15,0,Math.PI*2);
ctx.fillStyle = "#fbbf24"; ctx.fill(); ctx.closePath();
}
}
/********** HINTS DRAW **********/
function drawMoveHints(moves){
if(!moves || moves.length===0) return;
for(const m of moves){
if(!inside(m.r2,m.c2)) continue;
const x = m.c2*tileSize, y = m.r2*tileSize;
ctx.fillStyle = m.capture ? "rgba(220,38,38,0.25)" : "rgba(34,197,94,0.25)";
ctx.fillRect(x,y,tileSize,tileSize);
ctx.beginPath();
ctx.arc(x+tileSize/2, y+tileSize/2, tileSize*0.10, 0, Math.PI*2);
ctx.fillStyle = m.capture ? "#dc2626" : "#22c55e";
ctx.fill(); ctx.closePath();
// line
const sx = selected.c*tileSize + tileSize/2;
const sy = selected.r*tileSize + tileSize/2;
const tx = x+tileSize/2, ty = y+tileSize/2;
ctx.beginPath(); ctx.moveTo(sx,sy); ctx.lineWidth = 2; ctx.strokeStyle = "rgba(255,255,255,0.3)"; ctx.lineTo(tx,ty); ctx.stroke(); ctx.closePath();
}
}
// LOGIKA UNTUK MOVE
function tryMove(r1,c1,r2,c2){
console.debug("tryMove", {r1,c1,r2,c2, gameOver, currentTurn, aiEnabled});
if(gameOver) { console.debug("move blocked: gameOver"); return false; }
if(!inside(r1,c1) || !inside(r2,c2)) { console.debug("move blocked: outside"); return false; }
const v = board[r1][c1];
if(!v) { console.debug("move blocked: no piece at source"); return false; }
if(!belongsTo(v, currentTurn)) { console.debug("move blocked: piece does not belong to currentTurn", currentTurn); return false; }
if(board[r2][c2] !== 0) { console.debug("move blocked: dest occupied"); return false; }
const dr = r2 - r1, dc = c2 - c1;
const isKing = (v===3||v===4);
const dir = (v===1||v===3) ? -1 : 1;
// move
if(Math.abs(dr)===1 && Math.abs(dc)===1){
if(isKing || dr === dir){
saveHistory();
board[r2][c2] = v;
board[r1][c1] = 0;
const before = v;
crownIfNeeded(r2,c2);
playSound("move");
if((before === 1 && board[r2][c2] === 3) || (before === 2 && board[r2][c2] === 4)){
playSound("king");
}
switchTurn();
draw();
if(!checkWinCondition()) triggerAI();
return true;
} else { console.debug("move blocked: wrong direction for non-king"); return false; }
}
// MEMAKAN PION
if(Math.abs(dr)===2 && Math.abs(dc)===2){
const mr = r1 + dr/2, mc = c1 + dc/2;
if(!inside(mr,mc)) { console.debug("capture blocked: middle outside"); return false; }
const mid = board[mr][mc];
if(mid===0) { console.debug("capture blocked: no mid piece"); return false; }
if(belongsTo(mid, currentTurn)) { console.debug("capture blocked: mid belongs to same player"); return false; }
if(isKing || (dr/2) === dir){
saveHistory();
board[r2][c2] = v;
board[r1][c1] = 0;
board[mr][mc] = 0;
const before = v;
crownIfNeeded(r2,c2);
playSound("capture");
if((before === 1 && board[r2][c2] === 3) || (before === 2 && board[r2][c2] === 4)){
playSound("king");
}
switchTurn();
draw();
if(!checkWinCondition()) triggerAI();
return true;
} else { console.debug("capture blocked: wrong direction for non-king"); return false; }
}
console.debug("move blocked: invalid delta");
return false;
}
function crownIfNeeded(r,c){
if(board[r][c] === 1 && r === 0) board[r][c] = 3;
if(board[r][c] === 2 && r === size - 1) board[r][c] = 4;
}
function switchTurn(){ currentTurn = (currentTurn===1?2:1); }
function saveHistory(){ history.push(JSON.parse(JSON.stringify(board))); if(history.length>60) history.shift(); }
// MEMBUAT GERAKAN
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<size;r++) for(let c=0;c<size;c++) if(belongsTo(board[r][c], player)) all.push(...generateMovesForPiece(r,c,player));
return all;
}
// CEK KONDISI MENANG
function checkWinCondition(){
if(gameOver) return true;
let red=0, white=0;
for(let r=0;r<size;r++) for(let c=0;c<size;c++){
const v = board[r][c];
if(v===1||v===3) red++;
if(v===2||v===4) white++;
}
if(red===0){ showEnd("KALAH 😢","Semua pion merah hilang","loss"); return true; }
if(white===0){ showEnd("MENANG 🎉","Semua pion putih hilang","win"); return true; }
const redMoves = getAllMovesFor(1), whiteMoves = getAllMovesFor(2);
if(currentTurn===1 && redMoves.length===0){ showEnd("KALAH 😢","Merah tidak dapat bergerak","loss"); return true; }
if(currentTurn===2 && whiteMoves.length===0){ showEnd("MENANG 🎉","Putih tidak dapat bergerak","win"); return true; }
return false;
}
// AI MOVE
function aiMakeMove(){
if(!aiEnabled || currentTurn !== aiPlays || gameOver) { console.debug("aiMakeMove blocked", {aiEnabled, currentTurn, aiPlays, gameOver}); return; }
const moves = getAllMovesFor(aiPlays);
if(moves.length === 0) { checkWinCondition(); return; }
let bestScore = -Infinity, bestMoves = [];
for(const m of moves){
let s = 0;
if(m.capture) s += 100;
if(aiPlays===2 && m.r2 === size-1) s += 50;
s += (m.r2 || 0);
if(s > bestScore){ bestScore = s; bestMoves = [m]; } else if(s === bestScore) bestMoves.push(m);
}
const chosen = bestMoves[Math.floor(Math.random()*bestMoves.length)];
setTimeout(()=>{ tryMove(chosen.r1, chosen.c1, chosen.r2, chosen.c2); draw(); checkWinCondition(); }, aiThinkingDelay);
}
function triggerAI(){ setTimeout(()=>{ if(aiEnabled && currentTurn===aiPlays && !gameOver) aiMakeMove(); }, 120); }
// MENGKLIK PADA KANVAS
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();
});
// TOGGLE HINTS DENGAN 'H'
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 LOAD & SAVE
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 = "<p>Belum ada data</p>"; 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 = `<span>${i+1}. ${escapeHtml(row.username)}</span><span>${row.wins}W | ${row.losses}L</span>`;
box.appendChild(div);
});
}catch(err){ console.error("loadLeaderboard error", err); }
}
function escapeHtml(s){ if(!s) return ''; return String(s).replace(/[&<>"']/g,m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'})[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); }
}
// END GAME 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 = `<div style="background:#1e293b;padding:24px;border-radius:12px;width:310px;text-align:center;color:white;"><h2 style="margin:0 0 8px">${title}</h2><p style="margin:0 0 16px">${msg}</p><div style="display:flex;gap:8px;justify-content:center;"><button id="popupNew" style="background:#0ea5a4;color:white;border:none;padding:10px 18px;border-radius:8px;">New Game</button><button id="popupClose" style="background:#64748b;color:white;border:none;padding:10px 18px;border-radius:8px;">Close</button></div></div>`;
document.body.appendChild(div);
// save result async
saveResult(result).catch(()=>{});
document.getElementById("popupNew").onclick = ()=>{ div.remove(); resetBoard(); };
document.getElementById("popupClose").onclick = ()=> div.remove();
}
// START GAME
resetBoard();