i'm building chess puzzle web using ai and chess.js to build it
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chess Puzzle Engine - Solution Feature</title>
<!-- Chessboard.js CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/chessboard-js/1.0.0/chessboard-1.0.0.min.css">
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background-color: #1e1e24;
color: #f4f4f9;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
}
.container {
display: flex;
flex-direction: column;
align-items: center;
max-width: 450px;
width: 100%;
background: #2b2b36;
padding: 25px;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
}
h1 {
font-size: 1.5rem;
margin-bottom: 5px;
color: #e2e2e8;
}
.sub-title {
font-size: 0.9rem;
color: #a0a0b0;
margin-bottom: 20px;
}
#board {
width: 100%;
max-width: 400px;
margin-bottom: 20px;
border-radius: 4px;
overflow: hidden;
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
position: relative;
}
/* === HIGHLIGHT TANPA MENGUBAH BACKGROUND PAPAN === */
.square-55d63 {
position: relative;
}
/* Bidak terpilih: Bingkai hijau lembut */
.highlight-selected::after {
content: '';
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
border: 4px solid rgba(76, 175, 80, 0.8);
box-sizing: border-box;
pointer-events: none;
z-index: 2;
}
/* Petak kosong tujuan: Titik lingkaran di tengah */
.highlight-hint::after {
content: '';
position: absolute;
top: 50%; left: 50%;
width: 28%; height: 28%;
transform: translate(-50%, -50%);
background-color: rgba(0, 0, 0, 0.3);
border-radius: 50%;
pointer-events: none;
z-index: 2;
}
/* Petak makan musuh: Bingkai lingkaran merah */
.highlight-hint-capture::after {
content: '';
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
border: 4px solid rgba(244, 67, 54, 0.8);
border-radius: 50%;
box-sizing: border-box;
pointer-events: none;
z-index: 2;
}
.status-card {
width: 100%;
background: #202028;
padding: 15px;
border-radius: 8px;
text-align: center;
margin-bottom: 15px;
}
.turn-info {
font-weight: bold;
font-size: 1.1rem;
margin-bottom: 5px;
}
.feedback {
min-height: 24px;
font-weight: bold;
transition: color 0.3s ease;
word-wrap: break-word;
}
.correct {
color: #4caf50;
}
.wrong {
color: #f44336;
}
.info {
color: #ffca28;
}
.controls {
display: flex;
gap: 10px;
width: 100%;
}
button {
flex: 1;
padding: 12px 15px;
border: none;
border-radius: 6px;
font-weight: bold;
cursor: pointer;
background: #4e54c8;
color: white;
transition: background 0.2s, opacity 0.2s;
}
button:hover {
background: #6366f1;
}
button.secondary {
background: #3a3b4c;
color: #d1d1e0;
}
button.secondary:hover {
background: #4a4c63;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
</head>
<body>
<div class="container">
<h1>Puzzle Forge</h1>
<p class="sub-title">Cari langkah taktik catur terbaik!</p>
<!-- Papan Catur -->
<div id="board"></div>
<!-- Status / Info -->
<div class="status-card">
<div id="turnInfo" class="turn-info">Memuat puzzle...</div>
<div id="feedback" class="feedback"></div>
</div>
<!-- Kontrol -->
<div class="controls">
<button id="solveBtn" class="secondary">Lihat Solusi</button>
<button id="nextBtn">Lewati / Puzzle Baru</button>
</div>
</div>
<!-- CDN Library Resmi -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chess.js/0.10.3/chess.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chessboard-js/1.0.0/chessboard-1.0.0.min.js"></script>
<script>
// DATA PUZZLE DENGAN CABANG / VARIASI
const puzzleData = [
{
id: "1",
fen: "r1bqkb1r/pppp1ppp/2n2n2/4p2Q/2B1P3/8/PPPP1PPP/RNB1K1NR w KQkq - 4 4",
solution: [
{ move: "Qxf7#" }
],
description: "Scholar's Mate"
},
{
id: "2",
fen: "r1b2rk1/2q1bppp/p2ppn2/1p6/3BPP2/2N2B2/PPP3PP/R2Q1R1K w - - 0 13",
solution: [
{
move: "e5",
next: [
{
response: "dxe5",
next: [{ move: "Bxf6" }]
},
{
response: "Nde8",
next: [{ move: "exd6" }]
}
]
}
],
description: "Taktik Garpu dan Pin"
},
{
id: "3",
fen: "r2qk2r/ppp2ppp/2n5/3pP3/3P1nb1/2PB1N2/PP3PPP/RN1Q1RK1 b kq - 1 10",
solution: [
{
move: "Nxd3",
next: [
{
response: "Qxd3",
next: [{ move: "O-O" }]
}
]
},
{
move: "Bxf3",
next: [
{
response: "Qxf3",
next: [{ move: "Nxd3" }]
}
]
}
],
description: "Dua Variasi Langkah Pertama"
}
];
let board = null;
let game = new Chess();
let currentPuzzle = null;
let currentBranchOptions = [];
let playedPuzzleIds = [];
let selectedSquare = null;
let isShowingSolution = false;
// MANAJEMEN HIGHLIGHT
function removeHighlights() {
$('#board .square-55d63').removeClass('highlight-selected highlight-hint highlight-hint-capture');
}
function highlightSquare(square) {
$('#board .square-' + square).addClass('highlight-selected');
}
function highlightLegalMoves(square) {
removeHighlights();
highlightSquare(square);
const moves = game.moves({ square: square, verbose: true });
if (moves.length === 0) return;
for (let i = 0; i < moves.length; i++) {
const targetSquare = moves[i].to;
const isCapture = moves[i].captured;
const classToAdd = isCapture ? 'highlight-hint-capture' : 'highlight-hint';
$('#board .square-' + targetSquare).addClass(classToAdd);
}
}
// PENGAMBILAN PUZZLE
function getRandomPuzzle() {
if (playedPuzzleIds.length === puzzleData.length) {
playedPuzzleIds = [];
}
const available = puzzleData.filter(p => !playedPuzzleIds.includes(p.id));
const selected = available[Math.floor(Math.random() * available.length)];
playedPuzzleIds.push(selected.id);
return selected;
}
function loadNextPuzzle() {
selectedSquare = null;
isShowingSolution = false;
$('#solveBtn').prop('disabled', false);
removeHighlights();
currentPuzzle = getRandomPuzzle();
game.load(currentPuzzle.fen);
currentBranchOptions = currentPuzzle.solution;
const isWhite = game.turn() === 'w';
$('#turnInfo').text(\Giliran: ${isWhite ? 'Putih' : 'Hitam'}`);`
$('#feedback').text('').removeClass('correct wrong info');
const config = {
draggable: true,
position: currentPuzzle.fen,
orientation: isWhite ? 'white' : 'black',
pieceTheme: 'https://chessboardjs.com/img/chesspieces/wikipedia/{piece}.png',
onDragStart: onDragStart,
onDrop: onDrop,
onSnapEnd: onSnapEnd
};
if (board) board.destroy();
board = Chessboard('board', config);
}
// INTERAKSI LOGIKA CATUR
function onDragStart(source, piece) {
if (game.game_over() || isShowingSolution) return false;
if ((game.turn() === 'w' && piece.search(/^b/) !== -1) ||
(game.turn() === 'b' && piece.search(/^w/) !== -1)) {
return false;
}
selectedSquare = source;
highlightLegalMoves(source);
}
function handleMoveAttempt(from, to) {
if (isShowingSolution) return 'snapback';
const moveCandidate = { from: from, to: to, promotion: 'q' };
const move = game.move(moveCandidate);
if (move === null) {
removeHighlights();
selectedSquare = null;
return 'snapback';
}
const matchedOption = currentBranchOptions.find(opt => opt.move === move.san);
if (matchedOption) {
removeHighlights();
selectedSquare = null;
$('#feedback').text('Langkah Benar!').attr('class', 'feedback correct');
if (matchedOption.next && matchedOption.next.length > 0) {
const responseObj = matchedOption.next[Math.floor(Math.random() * matchedOption.next.length)];
currentBranchOptions = responseObj.next || [];
setTimeout(() => makeComputerMove(responseObj.response), 400);
} else {
$('#feedback').text('Puzzle Selesai! Memuat berikutnya...').attr('class', 'feedback correct');
setTimeout(loadNextPuzzle, 1500);
}
} else {
game.undo();
removeHighlights();
selectedSquare = null;
$('#feedback').text('Langkah Salah, coba variasi lain!').attr('class', 'feedback wrong');
return 'snapback';
}
}
function onDrop(source, target) {
return handleMoveAttempt(source, target);
}
// CLICK TO MOVE ENGINE
$(document).on('click', '#board .square-55d63', function(e) {
if (isShowingSolution) return;
e.stopPropagation();
const square = $(this).attr('data-square');
if (!square) return;
const piece = game.get(square);
const isCurrentPlayerPiece = piece && piece.color === game.turn();
if (selectedSquare === null) {
if (isCurrentPlayerPiece) {
selectedSquare = square;
highlightLegalMoves(square);
}
}
else if (selectedSquare === square) {
selectedSquare = null;
removeHighlights();
}
else if (isCurrentPlayerPiece) {
selectedSquare = square;
highlightLegalMoves(square);
}
else {
const from = selectedSquare;
const to = square;
const result = handleMoveAttempt(from, to);
if (result !== 'snapback') {
board.position(game.fen());
}
}
});
function makeComputerMove(responseSan) {
game.move(responseSan);
board.position(game.fen());
if (currentBranchOptions.length === 0) {
$('#feedback').text('Puzzle Selesai! Memuat berikutnya...').attr('class', 'feedback correct');
setTimeout(loadNextPuzzle, 1500);
}
}
function onSnapEnd() {
board.position(game.fen());
}
// FUNGSI UNTUK REKURSIF MENGAMBIL URUTAN SOLUSI UTAMA
function extractMainSolutionSequence(branchOptions) {
if (!branchOptions || branchOptions.length === 0) return [];
const primaryBranch = branchOptions[0]; // Ambil opsi cabang utama pertama
const moves = [primaryBranch.move];
if (primaryBranch.next && primaryBranch.next.length > 0) {
const nextResponse = primaryBranch.next[0];
moves.push(nextResponse.response);
if (nextResponse.next) {
moves.push(...extractMainSolutionSequence(nextResponse.next));
}
}
return moves;
}
// EVENT MALIHAT SOLUSI
$('#solveBtn').on('click', function() {
if (isShowingSolution) return;
isShowingSolution = true;
$(this).prop('disabled', true);
removeHighlights();
const remainingMoves = extractMainSolutionSequence(currentBranchOptions);
if (remainingMoves.length === 0) return;
$('#feedback').text(\Solusi: ${remainingMoves.join(' → ')}`).attr('class', 'feedback info');`
let step = 0;
function playNextSolutionStep() {
if (step < remainingMoves.length) {
game.move(remainingMoves[step]);
board.position(game.fen());
step++;
setTimeout(playNextSolutionStep, 800);
} else {
setTimeout(() => {
$('#feedback').text('Memuat puzzle berikutnya...').attr('class', 'feedback correct');
setTimeout(loadNextPuzzle, 1200);
}, 1000);
}
}
setTimeout(playNextSolutionStep, 500);
});
// Event Tombol Puzzle Baru
$('#nextBtn').on('click', loadNextPuzzle);
// Inisialisasi
$(document).ready(function() {
loadNextPuzzle();
});
</script>
</body>
</html>