Ajedrez y JavaScript con ChessJs

Averigua cuanto tienen en común el ajedrez y JavaScript con ChessJs. La librería para el desarrollo de partidas de ajedrez en el navegador.

https://github.com/jhlywa/chess.js A Javascript chess library for chess move generation/validation, piece placement/movement, and check/checkmate/draw detection

Este artículo va acompañado de un ejercicio práctico. Haz click en la siguiente imágen para ver el resultado terminado de la práctica de hoy:

Ejercicio terminado ChessJs
Ejercicio terminado ChessJs, haz click en la imagen para abrirlo en una ventana nueva

Si quieres saltarte la teoria e ir directamente a la parte práctica, haz click aquí

Programación JavaScript y ajedrez, dos pasiones con mucho en común

Lo reconozco, además del desarrollo web, otra de mis pasiones es el ajedrez

A pesar de que nunca he sido muy bueno, llevo jugando a este juego, prácticamente, desde que tengo memoria.

De modo que, tarde o temprano, quería dedicar un artículo a relacionar estas dos grandes aficiones.

Puede que pienses que son dos mundos muy alejados entre sí. Pero pronto descubrirás que tienen más en común, de lo que parece a simple vista. 

Navegando por Internet encontré no una, sinó dos librerías de ajedrez excelentes, programadas en JavaScript. De modo que ya no tenía excusas para posponer la publicación de hoy.

Los recursos de los que voy a hablar son chessJs y cm-chessboard.

Dos librerías JavaScript de código abierto, creadas para resolver necesidades de desarrollo propias del ajedrez

En líneas generales, chessJs se ocupa de manejar la lógica intrínseca del juego.

Siendo capaz de comprender las normas del tablero, el movimiento de las piezas o el turno de cada jugador.

No obstante, esta herramienta no renderiza nada. De eso se encarga cm-chessboard. 

Por su lado, ésta segunda dependencia muestra en pantalla todos los elementos gráficos, necesarios en una partida de ajedrez.

Desde el tablero, hasta, por supuesto, las piezas.

Sin embargo, cm-chessboard ignora por completo qué está pasando, o quién va ganando.

Como ya imaginarás, combinando las dos, se pueden asentar las bases frontend de aplicaciones tan espectaculares como chess.com o lichess.org. Dos de las grandes plataformas de ajedrez en línea.

Doma el ajedrez con JavaScript y ChessJs
Doma el ajedrez con JavaScript y ChessJs

Antes de entrar de lleno a ver cómo se implementan, es importante tener en cuenta dos puntos.

Por un lado, no debemos confundir chess.js con una base de datos, ni con un motor de juego, como lo podrían ser Stockfish o AlphaZero.

Por el otro, es muy recomendable estar familiarizado con notaciones propias del ajedrez. Existen varias, pero las más conocidas son FEN (Forsyth–Edwards Notation), SAN (Standard Algebraic Notation) y PGN (Portable Game Notation). 

No es imprescindible conocerlas al dedillo, pero entender estas formas de escritura, te ayudará a trabajar mejor con chess.js.

Una librería para el “core” y la otra para la UI

Tras esta breve introducción, ha llegado el momento de ver en detalle las dos herramientas de hoy.

Empezaré analizando el uso y popularidad de chessJs, creado por Jeff Hlywa.

Tras una ojeada rápida a su repositorio en Github, vemos que el código fuente se ampara bajo la licencia BSD 2-Clause.

En el momento de redactar estas líneas, está valorado con más de 2.9K estrellas, y se descarga unas 16K veces al mes.

Como es habitual, se puede instalar rápidamente con el comando:

npm install chess.js

E importarla en nuestro script con:

import { Chess } from 'chess.js';

El objeto Chess es una clase, que al instanciarla, devolverá un objeto con métodos y propiedades, para controlar el estado y evolución de una partida.

Su constructor admite un único parámetro opcional. Se trata de una cadena de texto en formato FEN, que describe la posición inicial de las piezas en el tablero.

En caso de no proporcionar ningún argumento, las piezas se ubicaran en su posición de inicio, por defecto.

Un ejemplo de ambos casos sería este:

import { Chess } from 'chess.js'
const chess = new Chess(); //partida con las piezas en sus posiciones iniciales
const chessMiddleGame = new Chess(‘r1k4r/p2nb1p1/2b4p/1p1n1p2/2PP4/3Q1NB1/1P3PPP/R5K1 b - c3 0 19’); //nueva partida, con una posición inicial determinada

A continuación, voy a destacar algunos de los métodos más interesantes del objeto “chess”.

  • .move(move, [options]): Seguramente una de las funciones más importantes. El método “chess.move()” tratará de mover la pieza indicada en el argumento, mediante notación SAN. También acepta un objeto con las propiedades “from”, “to” y “promotion”.
  • .reset(): Resetea el tablero a su posición inicial.
  • .clear(): Retira todas las piezas del tablero.
  • .turn(): Devolverá el color del jugador que le toca mover.
  • .fen(): Extrae la posición del tablero, en formato FEN.
  • .game_over(): Al llamar este método obtienes una booleana “true” o “false” dependiendo de si el juego ha terminado por jaque mate, ahogo o similar.
  • .in_check(): Comprueba si hay una situación de jaque.
  • .moves(): Retorna un array de todos los movimientos válidos posibles.
  • .pgn(): Devuelve el juego completo en formato PGN

Estas solo son algunas de las acciones, pero sugiero que visites su documentación, para ver el resto de ellas.

https://github.com/jhlywa/chess.js#api

Si deseas ver una representación rápida del estado del tablero, también puedes ejecutar el método «.ascii()». La función generará un diagrama ASCII, que puedes «imprimir», por ejemplo, en la consola. 

Mostrando un resultado parecido al siguiente:

chess.ascii()
// -> '   +------------------------+
//      8 | r  n  b  q  k  b  n  r |
//      7 | p  p  p  p  .  p  p  p |
//      6 | .  .  .  .  .  .  .  . |
//      5 | .  .  .  .  p  .  .  . |
//      4 | .  .  .  .  P  P  .  . |
//      3 | .  .  .  .  .  .  .  . |
//      2 | P  P  P  P  .  .  P  P |
//      1 | R  N  B  Q  K  B  N  R |
//        +------------------------+
//          a  b  c  d  e  f  g  h'

Mostrar el tablero en forma de carácteres ASCII es muy interesante, pero siendo honestos, muy poco «user friendly». 

Así que ha llegado el turno de ver la librería que se encargará de la UI.

Según npmjs.com, cm-chessboard es un proyecto de modesta popularidad, pero de gran calidad. Así que lo he escogido frente a otras alternativas (que no son pocas).

Cm-chessboard está basado en módulos ES6, es responsive y usa tecnología SVG para el renderizado gráfico.

Stefan Haack es su creador original, y conjuntamente con un equipo de 9 personas, mantiene el repositorio actualizado desde hace más de dos años.

En este momento “solo” tiene 123 estrellas, pero no te dejes engañar, es un programa con un enorme potencial. Un rápido vistazo a su API será suficiente para convencerte.

Puedes incluir la librería en tu proyecto mediante el comando:

npm install cm-chessboard

Para, posteriormente, importar la clase principal con:

import { Chessboard } from "cm-chessboard";

El constructor de esta clase acepta dos argumentos. 

Un elemento del DOM, que actúa de contenedor dónde montar el tablero. Y un objeto opcional, para configurar algunos parámetros de inicio.

Estas son algunas de las propiedades configurables:

  • position: Posiciona las piezas en el tablero según una cadena de texto en notación FEN
  • orientation: Decide qué color se coloca en la parte inferior del tablero.
  • responsive: Pasa una booleana para forzar que el tablero se ajuste al tamaño máximo de pantalla.
  • style: Determina a través de un objeto, las características de diseño de las casillas, los elementos de marcaje o las coordenadas de los laterales.
  • sprite: Puedes personalizar el diseño de las piezas, entregando un archivo .svg que actúe de “sprite”.
  • extensions: Array para agregar funcionalidades de terceros al tablero.

Tienes el listado completo de parámetros en este link:

https://github.com/shaack/cm-chessboard#readme

Seguidamente analizaremos brevemente los métodos de los que dispone el objeto “chessboard”.

Nuevamente, destacaré solo las más relevantes.

  • setPiece(square, piece, animated = false): Ubica la pieza que deseas en la casilla que quieras. Devolverá una “promise” que se resolverá al finalizar la animación.
  • getPiece(square): Recupera qué pieza se encuentra colocada en un punto dado.
  • movePiece(squareFrom, squareTo, animated = false): De nuevo, quizá, el método más importante. Decide qué movimiento realizar, enviando un punto de inicio y uno final.
  • setPosition(fen, animation): Setea la posición completa de todas las piezas del juego.
  • getPosition(): Recupera la posición completa de todas las piezas.
  • addMarker(type, square): Coloca una marca sobre una coordenada determinada.
  • getMarkers(): El método devuelve todas las marcas activas.
  • enableMoveInput(eventHandler, color = undefined): Mediante este método, se puede activar la interacción del usuario, ya sea a través del ratón, o táctil. El parámetro “eventHandler” es una función de respuesta que se ejecutará en momentos específicos de la interacción. En el ejercicio práctico lo veremos en marcha.
  • disableMoveInput(): Deshabilita la interactividad sobre el tablero.

Tienes el resto de métodos en este enlace:

https://github.com/shaack/cm-chessboard#api

Pero basta de teoría, ha llegado el momento de crear nuestro propio proyecto de ajedrez.

Combina javaScript y ajedrez con chessJs para crear tu propia app

Con todo lo aprendido, vamos a crear una pequeña aplicación web para aficionados al ajedrez, a la que llamaremos “Appjedrez”. El programa ofrecerá estas funcionalidades:

  • Jugar contra la máquina (aunque hará movimientos válidos, serán totalmente aleatorios). 
  • Realizar una partida por turnos para dos jugadores (no en línea, sinó sobre el mismo tablero).
  • Cargar una partida a partir de un archivo “.pgn” y visualizarla.
  • Descargar la partida jugada en un documento “.pgn”.
  • Rotar el tablero a petición.

Puedes encontrar el código completo del ejercicio en el link que te dejo a continuación. Te resultará útil, si te pierdes en algún momento.

https://github.com/Danivalldo/libreriasjs/tree/master/ChessJs

Iniciamos esta guía preparando una estructura HTML básica. Este «layout» está compuesto por un «sidebar«, un contenedor para el tablero de ajedrez y una modal.

Verás que todos los elementos tienen asignadas múltiples clases y atributos. Eso es porque, más adelante, utilizaremos dos librerías CSS que hacen uso de estas.

En el “sidebar” incluiremos un único botón para iniciar una nueva partida.

<nav class="bg-blue-500 p-3">
  <button
    type="button"
    data-modal-toggle="defaultModal"
    data-tooltip-target="tooltip-right"
    data-tooltip-placement="right"
    class="text-white bg-white hover:bg-grey-100 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-full text-sm p-2.5 text-center inline-flex items-center shadow-md"
  >
    <img src="assets/images/plus.svg" class="w-6 h-6" alt="" />
  </button>
  <div
    id="tooltip-right"
    role="tooltip"
    class="inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-white bg-gray-900 rounded-lg shadow-sm opacity-0 tooltip dark:bg-gray-700"
  >
    Nueva partida
    <div class="tooltip-arrow" data-popper-arrow></div>
  </div>
</nav>

En el caso que te muestro, además incluyo un “tooltip”, pero es opcional.

Seguimos con el área principal. En ella añadimos una cabecera, un contenedor donde cargar el tablero, y unos botones para activar funciones específicas.

<main class="bg-indigo-10 flex-1 overflow-x-auto">
  <div
    class="bg-gradient-to-r from-blue-500 to-chessblue p-2 mb-2 text-white shadow-md"
  >
    <img
      src="assets/images/chess-pawn.svg"
      alt=""
      class="w-10 h-10 inline-block"
    />
    <h1 class="inline-block font-bold">Appjedrez</h1>
  </div>
  <div class="px-2">
    <div id="board1" class="chessboard aspect-square shadow-md"></div>
  </div>
  <div class="flex flex-row justify-center pt-3">
    <button
      type="button"
      class="text-white bg-blue-500 hover:bg-blue-600 focus:ring-4 font-medium text-sm p-2.5 text-center items-center mr-2 rounded-md export-png-btn"
    >
      Exportar PGN
    </button>
    <button
      type="button"
      class="text-white bg-blue-500 hover:bg-blue-600 focus:ring-4 font-medium text-sm p-2.5 text-center items-center mr-2 rounded-md rotate-board-btn"
    >
      Rotar el tablero
    </button>
    <button
      type="button"
      class="text-white bg-red-700 hover:bg-red-800 focus:ring-4 font-medium text-sm p-2.5 text-center items-center mr-2 rounded-md reset-btn"
    >
      Reiniciar
    </button>
  </div>
</main>

Cerraremos con una “modal” que contiene las opciones de inicio de partida. Aparecerá al inicio, y cuando el usuario haga click en el botón del sidebar.

<div
id="defaultModal"
data-modal-show="true"
tabindex="-1"
aria-hidden="true"
class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 w-full md:inset-0 h-modal md:h-full"
>
  <div class="relative p-4 w-full max-w-2xl h-full md:h-auto">
    <!-- Modal content -->
    <div class="relative bg-white rounded-lg shadow">
      <!-- Modal header -->
      <div class="flex justify-between items-start p-4 rounded-t border-b">
        <h3 class="text-xl font-semibold text-gray-900">Nueva partida</h3>
        <button
          type="button"
          class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center"
          data-modal-toggle="defaultModal"
        >
          <img src="assets/images/cross.svg" alt="" class="w-5 h-5" />
          <span class="sr-only">Close modal</span>
        </button>
      </div>
      <!-- Modal body -->
      <div class="p-6 space-y-6">
        <ul class="grid gap-6 w-full md:grid-cols-2">
          <li>
            <input
              type="radio"
              id="vs-computer"
              name="game-vs"
              checked="checked"
              value="vs-computer"
              class="hidden peer"
            />
            <label
              for="vs-computer"
              class="inline-flex justify-between items-center p-5 w-full text-gray-500 bg-white rounded-lg border border-gray-200 cursor-pointer peer-checked:border-blue-600 peer-checked:text-blue-600 hover:text-gray-600 hover:bg-gray-100"
            >
              <div class="block">
                <div class="w-full text-lg font-semibold">
                  Contra la máquina
                </div>
                <div class="w-full">
                  Solo hace movimientos aleatorios. Es fácil ganarla.
                </div>
              </div>
              <img
                src="assets/images/arrow.svg"
                class="ml-3 w-6 h-6"
                alt=""
              />
            </label>
          </li>
          <li>
            <input
              type="radio"
              id="vs-human"
              name="game-vs"
              value="vs-human"
              class="hidden peer"
              required=""
            />
            <label
              for="vs-human"
              class="inline-flex justify-between items-center p-5 w-full text-gray-500 bg-white rounded-lg border border-gray-200 cursor-pointer peer-checked:border-blue-600 peer-checked:text-blue-600 hover:text-gray-600 hover:bg-gray-100"
            >
              <div class="block">
                <div class="w-full text-lg font-semibold">
                  Contra otra persona
                </div>
                <div class="w-full">
                  Sobre el mismo tablero. Puedes rotarlo a cada turno.
                </div>
              </div>
              <img
                src="assets/images/arrow.svg"
                class="ml-3 w-6 h-6"
                alt=""
              />
            </label>
          </li>
        </ul>
        <hr />
        <h2 class="mt-0">Cargar PGN</h2>
        <textarea
          id="png-loader"
          rows="4"
          name="png-input-loader"
          class="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700"
          placeholder="Pega un texto en formato PGN para visualizar y continuar con una partida empezada"
        ></textarea>
      </div>
      <!-- Modal footer -->
      <div
        class="flex items-center p-6 space-x-2 rounded-b border-t border-gray-200"
      >
        <button
          data-modal-toggle="defaultModal"
          type="button"
          class="text-white bg-blue-500 hover:bg-blue-700 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center start-game-btn"
        >
          Empezar
        </button>
        <button
          data-modal-toggle="defaultModal"
          type="button"
          class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10"
        >
          Cancelar
        </button>
      </div>
    </div>
  </div>
</div>

Aunque no es muy complejo, te sugiero que le dediques unos minutos a estudiar detenidamente el código. 

También dotaremos de estilos CSS a nuestra maqueta. Como dije, para ello instalaremos tailwind y flowbite.

Tailwind es una biblioteca de clases CSS. Mientras que Flowbite, resuelve la creación de componentes UI más complejos, como «tooltips» o modales.

Instalamos todas las librerías necesarias con nuestro CLI:

npm install tailwind flowbite chess.js cm-chessboard

Importamos tailwind, y los estilos base de cm-chessboard en nuestra hoja de estilos SASS

@tailwind base
@tailwind components
@tailwind utilities
@import 'cm-chessboard/assets/styles/cm-chessboard.scss'

Aprovechamos también para definir unos estilos adicionales, que ajustarán el tablero a lo largo y ancho de la pantalla.

Ten en cuenta que, para que Tailwind y Flowbite funcionen, debes incluir el archivo «tailwind.config.cjs» en la raíz de tu proyecto. Consulta el enlace al repositorio, para ver su contenido.

Acto seguido programamos una clase JavaScript, a la que llamaremos «ChessGame». Actuará de controlador, combinando las dos librerías que hemos estudiado.

Definimos esta nueva clase en el archivo «./ChessGame.js». 

import { Chess } from "chess.js";
import { Chessboard } from "cm-chessboard";
import {
  COLOR,
  MARKER_TYPE,
  INPUT_EVENT_TYPE,
} from "cm-chessboard/src/cm-chessboard/Chessboard";
class ChessGame {
  constructor(element, fen) {
    this.chess = new Chess(fen);
    this.board = new Chessboard(element, {
      responsive: true,
      position: this.chess.fen(),
      style: {
        cssClass: "blue", //default
      },
      sprite: {
        url: "./assets/images/chessboard-sprite.svg",
      },
    });
    this.isReplayingGame = false;
    this.onPlayerMoveCb = undefined;
    this.changeBoardOrientation(this.chess.turn());
    this.enablePlayerMove();
  }
}
export default ChessGame;

Declaramos el constructor, de modo que acepte dos argumentos. El primero será el elemento contenedor del tablero. Y el segundo un «string» en formato FEN.

En base a esos parámetros, instanciamos un objeto de cada librería, y los guardamos como propiedades «this.chess» y «this.board».

En el momento de crear «board», puedes decidir características de estilos. Asegúrate de que la opción “sprite”, tenga referenciada correctamente la URL hacia el archivo “svg”. Nuevamente, puedes descargar este material directamente del repositorio de Github.

https://github.com/Danivalldo/libreriasjs/tree/master/ChessJs/public/assets/images

Continuamos preparando dos variables más. Una booleana con el nombre «this.isReplayingGame». Y otra indefinida con el nombre «this.onPlayerMoveCb». Más adelante veremos para qué las necesitamos. 

Finalizamos el constructor orientando el tablero con «changeBoardOrientation()», y llamando a «enablePlayerMove()»,ambos métodos los definimos a continuación.

changeBoardOrientation(color) {
  this.board.setOrientation(
    color
      ? color
      : this.board.getOrientation() === COLOR.white
      ? COLOR.black
      : COLOR.white
  );
}

Un código simple, que hace uso de la API de cm-chessboard.

  enablePlayerMove() {
    this.board.enableMoveInput(this.inputHandler.bind(this), this.chess.turn());
  }

Esta instrucción habilita la posibilidad de que el usuario interactúe con el tablero. Es necesario asignar una función de respuesta, y el color del turno.

La función de «callback» asociada será la «this.inputHandler». 

async inputHandler(event) {
  this.board.removeMarkers(MARKER_TYPE.dot);
  if (event.type === INPUT_EVENT_TYPE.moveInputStarted) {
    const moves = this.chess.moves({ square: event.square, verbose: true });
    for (const move of moves) {
      this.board.addMarker(MARKER_TYPE.dot, move.to);
    }
    return moves.length > 0;
  }
  if (event.type === INPUT_EVENT_TYPE.validateMoveInput) {
    const move = {
      from: event.squareFrom,
      to: event.squareTo,
      promotion: this.isPromotingMove({
        from: event.squareFrom,
        to: event.squareTo,
      })
        ? "q"
        : undefined,
    };
    const result = this.chess.move(move);
    this.disablePlayerMove();
    this.board.state.moveInputProcess.then(async () => {
      await this.board.setPosition(this.chess.fen(), true);
      if (typeof this.onPlayerMoveCb === "function" && result) {
        this.onPlayerMoveCb();
      }
      this.enablePlayerMove();
    });
    return result;
  }
}

Parece muy compleja, pero en realidad no lo es tanto, vayamos por partes.

Cada vez que el usuario interactúa con el tablero, se lanza esta función. A través del argumento «event» identificamos la fase de la acción.

Me explico, cuando un jugador mueve, coje la pieza y luego la coloca. Bien, pues «event.type» nos dirá si se trata del momento de seleccionar la pieza, o el de soltarla.

En la primera fase, añadimos marcas sobre el tablero, en función de todos los movimientos válidos posibles.

En la segunda generamos un objeto «move» que describe desde dónde se ha movido, hacía dónde va la pieza y si se trata de un peón que promociona a dama.

Con esa información, actualizamos «this.chess» y obtenemos si es un movimiento válido. Luego deshabilitamos temporalmente la interactividad con el tablero.

La variable «this.board.state.moveInputProcess» es una promesa que se resuelve cuando termina la animación de la pieza. Debemos esperar a que finalice para ejecutar las siguientes instrucciones.

Actualizamos la posición de todas las piezas del tablero segun «this.chess.fen()». Ejecutamos el «callback» guardado en «this.onPlayerMoveCb», si existe. Y habilitamos de nuevo la interacción. 

A lo largo de estas líneas de código hemos visto «this.disablePlayerMove()». Es momento de ver qué debe hacer éste método. 

disablePlayerMove() {
  this.board.disableMoveInput();
}

Sencillamente llama a la función «disableMoveInput» del objeto board, para desactivar la interactividad con el tablero.

También es necesario definir una forma de asignar el callback «onPlayerMoveCb». Lo haremos mediante el método «afterMove(cb)»

afterMove(cb) {
  this.onPlayerMoveCb = cb;
}

Como ves, solo guarda la función enviada por parámetro, en la propiedad llamada “onPlayerMoveCb”.

Seguimos ampliando la interfaz de nuestra clase con otros métodos adicionales.

Para la lógica del programa, en ocasiones es necesario conocer de antemano, si en el movimiento propuesto por un jugador, trata de coronar un peón.

Por ese motivo, he preparado el método la función “isPromotingMove”. 

isPromotingMove(move) {
  const piece = this.chess.get(move.from);
  if (piece?.type !== "p") {
    return false;
  }
  if (piece.color !== this.chess.turn()) {
    return false;
  }
  if (!["1", "8"].some((it) => move.to.endsWith(it))) {
    return false;
  }
  return this.chess
    .moves({ square: move.from, verbose: true })
    .map((it) => it.to)
    .includes(move.to);
}

Ésta recibe un parámetro que describe el movimiento. Mediante las capacidades de ChessJs, comprobamos si se trata de un peón, si es del color del turno adecuado, y si es un movimiento reglamentario para coronar.

Con “loadPGN” y “exportPGN”, expondremos las funciones de carga y descarga de partidas con forma PGN.

loadPGN(pgn) {
  return this.chess.loadPgn(pgn);
}
exportPGN() {
  return this.chess.pgn();
}

Una partida importada con “loadPGN” debería mostrar una animación de los movimientos realizados. Así pues, creamos una función dedicada a ello.

replayGameFromHistory(onEndReplay) {
  this.isReplayingGame = true;
  const history = this.chess.history();
  this.disablePlayerMove();
  this.chess.reset();
  const recursiveFunction = (pointer) => {
    const nextMove = history[pointer];
    if (!nextMove) {
      this.isReplayingGame = false;
      this.enablePlayerMove();
      if (typeof onEndReplay === "function") {
        onEndReplay();
      }
      return;
    }
    this.chess.move(nextMove);
    this.board.setPosition(this.chess.fen(), true);
    window.setTimeout(recursiveFunction.bind(this, ++pointer), 500);
  };
  recursiveFunction(0);
}

Las instrucciones que conforman “replayGameFromHistory”, dictan que se recorra el histórico de movimientos de la partida de forma recurrente.

A cada iteración, se actualizará el tablero a intervalos de 0.5 segundos. Cuando no queden movimientos, se ejecutará una función de respuesta enviada por parámetro.

Para controlar que no sucedan otras acciones mientras se ejecuta este método, generamos un “getter” que obtiene el estado de la variable “isReplayingGame” en todo momento.

getIsReplaying() {
  return this.isReplayingGame;
}

Asimismo, conectamos la función de recuperar el turno de la librería, con nuestro propio método “getTurn()”

getTurn() {
  return this.chess.turn();
}

Para implementar la función de jugar contra la máquina, habilitamos un método de movimiento aleatorio. Eso sí, es importante que respete las normas de juego.

randomMove() {
  this.disablePlayerMove();
  const possibleMoves = this.chess.moves({ verbose: true });
  if (possibleMoves.length > 0) {
    const randomIndex = Math.floor(Math.random() * possibleMoves.length);
    const randomMove = possibleMoves[randomIndex];
    this.chess.move({ from: randomMove.from, to: randomMove.to });
    this.board.setPosition(this.chess.fen(), true);
  }
  this.enablePlayerMove();
}

ChessJs devolverá un listado de posibles movimientos válidos con “this.chess.moves()”, si hay más de uno, pediremos al sistema que elija uno al azar y actualice el tablero con él.

Vamos a concluir la definición de la clase “ChessGame” con los dos últimos métodos.

Uno para reiniciar la posición de la partida.

reset() {
  this.disablePlayerMove();
  this.chess.reset();
  this.board.setPosition(this.chess.fen(), true);
  this.enablePlayerMove();
}

Y otro para comprobar si se ha llegado a un estado de final de partida.

isGameOver() {
  return this.chess.isGameOver();
}

Con la clase terminada, solo queda conectar sus capacidades a la interfaz gráfica HTML.

Antes de nada, comenzamos instalando una nueva dependencia. En esta ocasión incluiremos la librería “file-saver”, para cubrir la necesidad de descarga del archivo PGN generado.

npm install file-saver

Hablamos de esta herramienta en este otro post, por si la quieres estudiar en profundidad.

Descarga de archivos con JavaScript y FileSaver

Iniciamos el script “main.js” importando estilos y dependencias.

import "./style.sass";
import "flowbite";
import { saveAs } from "file-saver";
import ChessGame from "./ChessGame";

Guardamos referencias a un conjunto de botones de la interfaz, haciendo uso de “document.querySelector”.

const exportPGNBtn = document.querySelector(".export-png-btn");
const resetBtn = document.querySelector(".reset-btn");
const rotateBoardBtn = document.querySelector(".rotate-board-btn");
const startGameBtn = document.querySelector(".start-game-btn");

Creamos una variable de control, para determinar qué modalidad de juego ha escogido el usuario.

let vsMode = "vs-computer";

Instanciamos un objeto “chessGame” de nuestra clase. Como primer argumento pasamos el contenedor del tablero, y como segundo, la posición de inicio típica de ajedrez, en formato FEN.

const chessGame = new ChessGame(
  document.querySelector("#board1"),
  `rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1`
);

Asignamos el “callback” de respuesta, que se ejecuta a cada movimiento del jugador. Lo hacemos mediante el método “afterMove” que ya vimos.

chessGame.afterMove(() => {
  const gameOver = chessGame.isGameOver();
  if (gameOver) {
    return alert("Game over!");
  }
  if (vsMode === "vs-computer") {
    chessGame.randomMove();
    if (gameOver) {
      alert("Game over!");
    }
  }
});

Tras cada movimiento, comprobamos si se ha terminado la partida. En caso contrario, realizamos un movimiento aleatorio, solo si la modalidad de juego es “vs-computer”.

Finalmente, asignamos los eventos de tipo click a cada botón.

Con ExportPGNBtn generamos un archivo “.pgn” de la partida actual.

exportPGNBtn.addEventListener("click", () => {
  if (chessGame.getIsReplaying()) {
    return;
  }
  const pgn = chessGame.exportPGN();
  var blob = new Blob([pgn], { type: "text/plain;charset=utf-8" });
  saveAs(blob, `game.pgn`);
});

Luego, con “resetBtn”, reiniciamos dicha partida.

resetBtn.addEventListener("click", () => {
  if (chessGame.getIsReplaying()) {
    return;
  }
  chessGame.reset();
});

Como ya supondrás “rotateBoardBtn” se encarga de reorientar el tablero a petición.

rotateBoardBtn.addEventListener("click", () => {
  chessGame.changeBoardOrientation();
});

Y por último, pero no por ello menos importante, “startGameBtn”

startGameBtn.addEventListener("click", () => {
  if (chessGame.getIsReplaying()) {
    return;
  }
  vsMode = document.querySelector('input[name="game-vs"]:checked').value;
  const pgnInputLoader = document.querySelector('[name="png-input-loader"]');
  if (pgnInputLoader.value) {
    const validPGN = chessGame.loadPGN(pgnInputLoader.value);
    if (!validPGN) {
      return;
    }
    exportPGNBtn.classList.add("opacity-25", "cursor-not-allowed");
    resetBtn.classList.add("opacity-25", "cursor-not-allowed");
    return chessGame.replayGameFromHistory(() => {
      exportPGNBtn.classList.remove("opacity-25", "cursor-not-allowed");
      resetBtn.classList.remove("opacity-25", "cursor-not-allowed");
      if (chessGame.getTurn() === "b") {
        chessGame.randomMove();
      }
    });
  }
  chessGame.reset();
});

Su objetivo será actualizar “vsMode”, cargar y reproducir una partida si el usuario la ha incluido mediante notación PGN, y reiniciar el tablero.

Para hacer pruebas de carga de partidas, te he dejado una de prueba en el directorio “assets”. Eso sí, no te esperes un duelo a la altura de Nakamura contra Carlsen

Si has llegado a este punto, solo puedo decirte, ¡enhorabuena!. Ha sido un tutorial bastante completo, solo espero que lo hayas disfrutado. 

Puedes compartir conmigo tu opinión escribiéndome a través del canal de Instagram, agradeceré muchísimo tu feedback.

Antes de terminar, te lanzo un pequeño reto. El ejercicio de hoy pide a gritos que se desarrolle la función de juego en línea. ¿Serías capaz de hacerlo?

Hace un tiempo preparé una guía sobre “websockets”, una tecnología excelente para ese propósito, seguro que ese artículo despeja muchas dudas para abordar el reto.

Comunicación a tiempo real con JavaScript y websockets

También te listo algún material de apoyo más, por si te es de interés.

Una vez más, muchas gracias por leerme. Hasta la próxima.

5 comentarios en «Ajedrez y JavaScript con ChessJs»

Deja un comentario