Escribiendo un microservicio de ajedrez usando Node.js y Seneca, Parte 1

(Esta es la Parte 1 de una serie de tres partes [Parte 2, Parte 3])

Empecé a pensar en los microservicios. Hasta este momento lo consideré como un patrón de escalabilidad y pasé por alto los principios de programación funcional detrás de él.

Las reglas del ajedrez se pueden descomponer fácilmente en microservicios. No son aleatorios ni ambiguos, lo que es perfecto para escribir pequeños servicios sin estado que se ocupan de movimientos de varias piezas.

En esta publicación, analizaré varios servicios que creé que determinan cuáles son los movimientos legales para piezas solitarias en un tablero de ajedrez vacío. Usaremos el marco Seneca, un conjunto de herramientas de microservicios para Node.js, porque es intuitivo y está bien documentado.

Configuración de Seneca

Seneca es un módulo de Node.js que se instala usando npm:

npm install seneca

Además, nos basaremos en módulos mocha / chai instalados globalmente para las pruebas que ilustrarán la funcionalidad.

Encuentra todos los movimientos legales

En realidad, no es necesario mantener una representación en memoria de un tablero de ajedrez, solo las piezas y su ubicación en una cuadrícula de coordenadas de 8x8. La notación algebraica se usa comúnmente para describir las coordenadas en un tablero de ajedrez, donde los archivos se indican con letras y los rangos con números:

Para el jugador blanco, la esquina inferior derecha es h1; para las negras es a8. Una torre en b2, moviéndose al cuadrado f2, se denotaría como Rb2-f2.

Movimientos crudos

Estoy definiendo movimientos crudos como los movimientos que haría una pieza si no estuviera obstaculizada por otras piezas o el borde del tablero . Ese último bit puede parecer extraño, pero me permite construir una máscara de movimiento de 15x15, que luego se trunca para adaptarse a la placa de 8x8. A un tipo llamado Procusto se le ocurrió una idea similar hace mucho tiempo.

Los reyes, reinas, alfiles y torres se mueven a lo largo de diagonales y / o filas, así que usaré un servicio para los movimientos de esas cuatro piezas. Los peones tienen características de movimiento únicas, por lo que se utilizará un servicio especial para ellos. Lo mismo ocurre con los Caballeros, ya que pueden saltar piezas y no moverse a lo largo de archivos o filas.

Por ejemplo, una torre puede mover 7 casillas a lo largo de cualquier fila o fila en un tablero de 15x15 en el que la torre está centrada. Se aplican reglas similares para el alfil y la reina. El rey está limitado a un rango de un cuadrado en cualquier dirección (la excepción es el enroque, del que me ocuparé en una publicación futura).

Usaré una ChessPiececlase para contener información sobre el tipo y la ubicación de cada pieza de ajedrez. No jugará un papel muy importante por ahora, pero lo hará más adelante cuando amplíe el alcance de las reglas cubiertas por los servicios.

Primer servicio: Movimientos de torre, alfil, reina y rey

En Séneca, los servicios se invocan a través de roley cmd. El rolees similar a una categoría y cmdnombres de un servicio específico. Como veremos más adelante, un servicio se puede especificar más mediante parámetros adicionales.

Los servicios se agregan mediante seneca.add()y se invocan mediante seneca.act(). Veamos primero el servicio (de Movement.js):

 this.add({ role: "movement", cmd: "rawMoves", }, (msg, reply) => { var err = null; var rawMoves = []; var pos = msg.piece.position; switch (msg.piece.piece) { case 'R': rawMoves = rankAndFile(pos); break; case 'B': rawMoves = diagonal(pos); break; case 'Q': rawMoves = rankAndFile(pos) .concat(diagonal(pos)); break; case 'K': rawMoves = rankAndFile(pos, 1) .concat(diagonal(pos, 1)) break; default: err = "unhandled " + msg.piece; break; }; reply(err, rawMoves); });

Ahora veamos cómo la prueba invoca el servicio (moveTest.js):

 var Ba1 = new ChessPiece('Ba1'); seneca.act({ role: "movement", cmd: "rawMoves", piece: Ba1 }, (err, msg) => {...});

Tenga en cuenta que además de roley cmdhay unpieceargumento. Esto, junto con roley cmd, son propiedades delmsgargumento recibido por el servicio. Sin embargo, antes de poder invocar el servicio, debe indicarle a Seneca qué servicios utilizar:

var movement = require(‘../services/Movement’) const seneca = require('seneca')({ log: 'silent' }) .use(movement);

Los movimientos crudos para un alfil en la casilla a1 están en el msgrecibido de vuelta desde el servicio:

[{archivo: '`', rango: '0'},

{archivo: 'b', rango: '2'},

{archivo: '`', rango: '2'},

{archivo: 'b', rango: '0'},

{archivo: '_', rango: '/'},

{archivo: 'c', rango: '3'},

{archivo: '_', rango: '3'},

{archivo: 'c', rango: '/'},

{archivo: '^', rango: '.' },

{archivo: 'd', rango: '4'},

{archivo: '^', rango: '4'},

{archivo: 'd', rango: '.' },

{archivo: ']', rango: '-'},

{archivo: 'e', ​​rango: '5'},

{archivo: ']', rango: '5'},

{archivo: 'e', ​​rango: '-'},

{archivo: '\\', rango: ','},

{archivo: 'f', rango: '6'},

{archivo: '\\', rango: '6'},

{archivo: 'f', rango: ','},

{archivo: '[', rango: '+'},

{archivo: 'g', rango: '7'},

{archivo: '[', rango: '7'},

{archivo: 'g', rango: '+'},

{archivo: 'Z', rango: '*'},

{archivo: 'h', rango: '8'},

{archivo: 'Z', rango: '8'},

{archivo: 'h', rango: '*'}]

¡Tenga en cuenta que hay algunos cuadrados extraños en la lista! Estas son las posiciones que “se desprenden” del tablero 8x8 y serán eliminadas posteriormente por otro servicio.

¿Lo que acaba de suceder?

Un servicio se definió con role=”movement”y cmd=”rawMoves”. Cuando act()se invoca posteriormente, los parámetros de la solicitud de acto se comparan con un servicio que maneja esos parámetros (esto se denomina patrón del servicio ). Como se mencionó anteriormente y como se mostrará en el siguiente ejemplo,roley cmdno son necesariamente los únicos parámetros que determinan el servicio invocado.

Próximos servicios: Peones y Caballeros

Los peones avanzan una casilla a menos que estén en su casilla original, en cuyo caso pueden avanzar una o dos casillas. Hay otros movimientos que puede hacer un peón cuando no es la única pieza en un tablero vacío, pero eso se debe considerar en el futuro. Los peones siempre comienzan en la segunda fila y nunca pueden retroceder.

Los caballeros se mueven en forma de L. En nuestro tablero imaginario de 15x15 con el caballo centrado, siempre habrá ocho movimientos posibles.

Escribiré dos servicios (uno para peones, el otro para caballeros) y colocaré ambos en un módulo (SpecialMovements.js):

module.exports = function specialMovement(options) { //... this.add({ role: "movement", cmd: "rawMoves", isPawn: true }, (msg, reply) => { if (msg.piece.piece !== 'P') { return ("piece was not a pawn") } var pos = msg.piece.position; const rawMoves = pawnMoves(pos); reply(null, rawMoves); }); this.add({ role: "movement", cmd: "rawMoves", isKnight: true }, (msg, reply) => { if (msg.piece.piece !== 'N') { return ("piece was not a knight") } var rawMoves = []; var pos = msg.piece.position; rawMoves = knightMoves(pos); reply(null, rawMoves); }); }

Ver el isPawny isKnightparámetros en los servicios? El primer objeto que se pasa a Séneca add()se denomina patrón de servicio . Lo que sucede es que Seneca invocará el servicio con la coincidencia de patrón más específica . Para invocar el servicio correcto, necesito agregarisPawn:trueo isKnight:truea la solicitud de acto:

var movement = require('../services/Movement') var specialMovement = require('../services/SpecialMovement') const seneca = require('seneca')({ log: 'silent' }) .use(specialMovement) ... var p = new ChessPiece('Pe2'); seneca.act({ role: "movement", cmd: "rawMoves", piece: p, ... isPawn: true }, (err, msg) => {...} ... var p = new ChessPiece('Nd4'); seneca.act({ role: "movement", cmd: "rawMoves", piece: p, isKnight: true }, (err, msg) => {

Movimientos legales

Our rudimentary legal move service will just filter out all the square positions that are not on files a-h or ranks 1–8. The legal move service will be called directly with a ChessPiece instance as part of the service payload. The legal move service will then invoke the raw move service to get the movement mask. The mask will be truncated to the edges of the board, and the result will be the square positions that can legally be played.

 this.add({ role: "movement", cmd: "legalSquares", }, (msg, reply) => { const isPawn = msg.piece.piece === 'P'; const isKnight = msg.piece.piece === 'N'; this.act({ role: "movement", cmd: "rawMoves", piece: msg.piece, isPawn: isPawn, isKnight: isKnight }, (err, msg) => { const squared = []; msg.forEach((move) => { if (move.file >= 'a' && move.file = 1 && move.rank <= 8) { squared.push(move) } } }) reply(null, squared); }); })

The legalSquaresservice first invokes the rawMovesservice. This gets us the 15x15 movement mask for whatever piece is passed via the msg parameter. It is important, though, that the right service is invoked by setting the isKnightor isPawnpattern field to true for either of those two pieces… if both are false, then the “regular” rawMovesservice for K,Q,B,R will be invoked.

Once the raw moves are retrieved, then the legalSquaresservice removes the invalid positions and returns what is left. So if I invoke the service with the piece at Na1, I get:

[ { file: ‘c’, rank: ‘2’ }, { file: ‘b’, rank: ‘3’ } ]

If instead I pass in Rd4, legalSquares returns:

[ { file: ‘c’, rank: ‘4’ },

{ file: ‘d’, rank: ‘5’ },

{ file: ‘e’, rank: ‘4’ },

{ file: ‘d’, rank: ‘3’ },

{ file: ‘b’, rank: ‘4’ },

{ file: ‘d’, rank: ‘6’ },

{ file: ‘f’, rank: ‘4’ },

{ file: ‘d’, rank: ‘2’ },

{ file: ‘a’, rank: ‘4’ },

{ file: ‘d’, rank: ‘7’ },

{ file: ‘g’, rank: ‘4’ },

{ file: ‘d’, rank: ‘1’ },

{ file: ‘d’, rank: ‘8’ },

{ file: ‘h’, rank: ‘4’ } ]

which is a little harder to decipher, but contains all files along the 4th rank and all ranks along the d-file (trust me!).

That’s it for now! In a future post I’ll go over services that deal with friendly pieces impeding movement, then dealing with the potential capture of hostile pieces. Further services will handle rules for castling, en passant, check, checkmate, and stalemate.

All source code can be found here.

Continue to Part 2 of this series.