♟️ Joc d'escacs amb chessboard.js i chess.js

Tutorial pas a pas · Basat en el codi del Prof. F. Pérez · Responsiu per a PC i mòbil

Aprendrem a crear un joc d'escacs jugable al navegador utilitzant les llibreries chessboard.js (per al tauler interactiu) i chess.js (per a la lògica del joc). El jugador controla les peces blanques arrossegant-les, i l'ordinador respon amb un moviment aleatori de les negres.

Pas 1: Estructura bàsica del document

Definim el tipus de document HTML5, l'idioma català, el joc de caràcters UTF-8, i la meta etiqueta viewport per garantir que el disseny s'adapti a qualsevol dispositiu.

<!DOCTYPE html>
<html lang="ca">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Play Chess</title>
</head>

Pas 2: Incloure llibreries CSS i JavaScript

Carreguem des d'un CDN el CSS de chessboard.js, la llibreria jQuery (necessària per chessboard.js), el JavaScript de chessboard.js i chess.js per gestionar la lògica dels escacs (moviments, torns, escac i mat, etc.).

<link rel="stylesheet" href="https://unpkg.com/@chrisoakman/chessboardjs@1.0.0/dist/chessboard-1.0.0.min.css">
<script src="https://unpkg.com/jquery@3.6.0/dist/jquery.min.js"></script>
<script src="https://unpkg.com/@chrisoakman/chessboardjs@1.0.0/dist/chessboard-1.0.0.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chess.js/0.12.1/chess.min.js"></script>
⚠️ Ordre important: jQuery ha de carregar-se abans que chessboard.js, perquè chessboard.js en depèn.

Pas 3: Estils CSS per al tauler i missatges

Donem estil al contenidor del tauler (#board) perquè es centri i tingui una amplada fixa de 400px (suficient per a la majoria de pantalles). Els divs de missatges (#game-status, #error-message, #moves) es mostren centrats amb colors distintius.

#board {
    width: 400px;
    margin: 20px auto;
}
.messages {
    text-align: center;
    margin-top: 20px;
}
#game-status {
    color: green;
}
#error-message {
    color: red;
}
#moves {
    color: blue;
}

Pas 4: Estructura del cos del document

Dins del body afegim un títol, un div#board on chessboard.js renderitzarà el tauler, i tres div per als missatges d'estat, error i llista de moviments legals.

<body>
    <h1>Play Chess</h1>
    <div id="board"></div>
    <div class="messages">
        <div id="game-status"></div>
        <div id="error-message"></div>
        <div id="moves"></div>
    </div>
</body>
</html>

Pas 5: Inicialitzar el tauler d'escacs

Quan el DOM està completament carregat, creem el tauler amb Chessboard('board', { ... }). L'opció draggable: true permet arrossegar les peces. position: 'start' col·loca la posició inicial. pieceTheme defineix la ruta de les imatges de les peces (en aquest exemple, des d'una carpeta local; a la demo farem servir imatges en línia). onDrop associa la funció que gestionarà cada moviment.

document.addEventListener("DOMContentLoaded", function() {
    let board, game;

    function initChessboard() {
        board = Chessboard('board', {
            draggable: true,
            position: 'start',
            pieceTheme: 'img/chesspieces/wikipedia/{piece}.png',
            onDrop: handleMove,
        });
        game = new Chess();
        updateMoves();
    }
    initChessboard();
});
💡 Nota: pieceTheme accepta el comodí {piece}, que se substitueix per codis com wP (peó blanc), bK (rei negre), etc.

Pas 6: Gestionar els moviments de les peces

La funció handleMove(source, target) s'executa cada cop que s'arrossega una peça. Primer neteja els missatges anteriors. Després intenta fer el moviment amb game.move(); si no és vàlid, mostra un error i retorna 'snapback' (la peça torna al lloc original). Si el moviment és vàlid, actualitza el tauler amb board.position(game.fen()). Si el joc ha acabat, mostra "Game over". Si és el torn de les negres, l'ordinador tria un moviment aleatori i el juga automàticament.

function handleMove(source, target) {
    clearMessages();
    const move = game.move({
        from: source,
        to: target,
        promotion: 'q'  // promoció automàtica a reina
    });
    if (move === null) {
        displayErrorMessage('Invalid move!');
        return 'snapback';
    }
    board.position(game.fen());
    if (game.game_over()) {
        displayGameStatus('Game over');
        return;
    }
    if (game.turn() === 'b') {
        // L'ordinador (negres) juga un moviment aleatori
        const blackMoves = game.moves({ verbose: true });
        const randomIndex = Math.floor(Math.random() * blackMoves.length);
        const randomBlackMove = blackMoves[randomIndex];
        game.move(randomBlackMove);
        board.position(game.fen());
        if (game.game_over()) {
            displayGameStatus('Game over');
            return;
        }
        updateMoves();
    } else {
        updateMoves();
    }
}

Fixa't que s'utilitza promotion: 'q' per promocionar automàticament a reina. En una versió més avançada es podria preguntar a l'usuari quina peça vol.

Pas 7: Actualitzar moviments i mostrar missatges

updateMoves() obté tots els moviments legals, en filtra els que deixarien el rei en escac, i mostra la llista en notació algebraica (SAN). Les altres funcions s'encarreguen de mostrar o netejar els missatges d'estat i error.

function updateMoves() {
    const moves = game.moves({ verbose: true });
    const legalMoves = moves
        .filter(move => {
            game.move(move);
            const isLegal = !game.in_check();
            game.undo();
            return isLegal;
        })
        .map(move => move.san);
    document.getElementById('moves').innerText = 'Legal moves: ' + legalMoves.join(', ');
}

function displayGameStatus(message) {
    document.getElementById('game-status').innerText = message;
}

function displayErrorMessage(message) {
    document.getElementById('error-message').innerText = message;
}

function clearMessages() {
    document.getElementById('game-status').innerText = '';
    document.getElementById('error-message').innerText = '';
    document.getElementById('moves').innerText = '';
}

Pas 8: Gestionar errors globals

Amb window.onerror capturem qualsevol error no controlat i el mostrem a l'usuari mitjançant handleError, que escriu el missatge al div d'error i el registra a la consola.

function handleError(error) {
    displayErrorMessage('Error: ' + error.message);
    console.error(error);
}

window.onerror = function (message, source, lineno, colno, error) {
    handleError(error || new Error(message + ' at ' + source + ':' + lineno + ':' + colno));
};

Pas 9: Codi complet del joc d'escacs

A continuació tens el codi complet, adaptat perquè les imatges de les peces es carreguin des d'un CDN públic i el tauler sigui responsiu. Desa'l com a fitxer .html i obre'l al navegador per jugar.

<!DOCTYPE html>
<html lang="ca">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Play Chess</title>
    <link rel="stylesheet" href="https://unpkg.com/@chrisoakman/chessboardjs@1.0.0/dist/chessboard-1.0.0.min.css">
    <script src="https://unpkg.com/jquery@3.6.0/dist/jquery.min.js"></script>
    <script src="https://unpkg.com/@chrisoakman/chessboardjs@1.0.0/dist/chessboard-1.0.0.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/chess.js/0.12.1/chess.min.js"></script>
    <style>
        #board {
            width: 400px;
            margin: 20px auto;
        }
        .messages {
            text-align: center;
            margin-top: 20px;
        }
        #game-status {
            color: green;
        }
        #error-message {
            color: red;
        }
        #moves {
            color: blue;
        }
    </style>
</head>
<body>
    <h1>Play Chess</h1>
    <div id="board"></div>
    <div class="messages">
        <div id="game-status"></div>
        <div id="error-message"></div>
        <div id="moves"></div>
    </div>
    <script>
        document.addEventListener("DOMContentLoaded", function() {
            let board, game;

            function initChessboard() {
                board = Chessboard('board', {
                    draggable: true,
                    position: 'start',
                    pieceTheme: 'https://chessboardjs.com/img/chesspieces/wikipedia/{piece}.png',
                    onDrop: handleMove,
                });
                game = new Chess();
                updateMoves();
            }

            function handleMove(source, target) {
                clearMessages();
                const move = game.move({
                    from: source,
                    to: target,
                    promotion: 'q'
                });
                if (move === null) {
                    displayErrorMessage('Invalid move!');
                    return 'snapback';
                }
                board.position(game.fen());
                if (game.game_over()) {
                    displayGameStatus('Game over');
                    return;
                }
                if (game.turn() === 'b') {
                    const blackMoves = game.moves({ verbose: true });
                    const randomIndex = Math.floor(Math.random() * blackMoves.length);
                    const randomBlackMove = blackMoves[randomIndex];
                    game.move(randomBlackMove);
                    board.position(game.fen());
                    if (game.game_over()) {
                        displayGameStatus('Game over');
                        return;
                    }
                    updateMoves();
                } else {
                    updateMoves();
                }
            }

            function updateMoves() {
                const moves = game.moves({ verbose: true });
                const legalMoves = moves
                    .filter(move => {
                        game.move(move);
                        const isLegal = !game.in_check();
                        game.undo();
                        return isLegal;
                    })
                    .map(move => move.san);
                document.getElementById('moves').innerText = 'Legal moves: ' + legalMoves.join(', ');
            }

            function displayGameStatus(message) {
                document.getElementById('game-status').innerText = message;
            }

            function displayErrorMessage(message) {
                document.getElementById('error-message').innerText = message;
            }

            function clearMessages() {
                document.getElementById('game-status').innerText = '';
                document.getElementById('error-message').innerText = '';
                document.getElementById('moves').innerText = '';
            }

            function handleError(error) {
                displayErrorMessage('Error: ' + error.message);
                console.error(error);
            }

            window.onerror = function (message, source, lineno, colno, error) {
                handleError(error || new Error(message + ' at ' + source + ':' + lineno + ':' + colno));
            };

            initChessboard();
        });
    </script>
</body>
</html>
🎯 La principal diferència amb el codi original és que hem canviat el pieceTheme a un CDN públic (https://chessboardjs.com/img/chesspieces/wikipedia/{piece}.png) perquè les imatges es carreguin correctament sense necessitat de carpeta local.

🎮 Prova el joc d'escacs aquí mateix!

Arrossega les peces blanques per jugar. L'ordinador mourà una peça negra aleatòriament. El tauler és interactiu i funciona tant amb ratolí com amb pantalla tàctil.