Skrivning af en skakmikrotjeneste ved hjælp af Node.js og Seneca, del 1

(Dette er del 1 af en tredelt serie [del 2, del 3])

Jeg er begyndt at pakke mit hoved omkring mikrotjenester. Indtil dette tidspunkt betragtede jeg det som et skalerbarhedsmønster og overså de funktionelle programmeringsprincipper bag det.

Skakreglerne kan let nedbrydes i mikrotjenester. De er hverken tilfældige eller tvetydige, hvilket er perfekt til at skrive små, statsløse tjenester, der beskæftiger sig med bevægelser af forskellige stykker.

I dette indlæg vil jeg gennemgå flere tjenester, jeg oprettede, der bestemmer, hvad de lovlige træk er for ensomme brikker på et tomt skakbræt. Vi bruger Seneca-rammen, et mikroserviceværktøjssæt til Node.js, fordi det er intuitivt og veldokumenteret.

Opsætning af Seneca

Seneca er et Node.js-modul, der er installeret ved hjælp af npm:

npm install seneca

Vi vil også stole på globalt installerede mocha / chai-moduler til testene, der illustrerer funktionalitet.

Find alle de juridiske træk

Det er faktisk ikke nødvendigt at opretholde en hukommelsesrepræsentation af et skakbræt, bare brikkerne og deres placering på et 8x8 koordinatgitter. Algebraisk notation bruges ofte til at beskrive koordinaterne på et skakbræt, hvor filerne er angivet med bogstaver og rækken med tal:

For spilleren, der er hvid, er nederste højre hjørne h1; for sort er det a8. Et tårn på b2, der flytter til kvadrat f2, betegnes som Rb2-f2.

Rå bevægelser

Jeg definerer rå bevægelser som de bevægelser et stykke ville gøre, hvis det ikke er hindret af andre stykker eller kanten af ​​brættet . Den sidste bit kan virke underlig, men det giver mig mulighed for at konstruere en 15x15 bevægelsesmaske, som derefter afkortes til at passe til 8x8-kortet. En fyr ved navn Procrustes kom med en lignende idé for evigt siden.

Kings, Queens, Bishops and Rooks bevæger sig langs diagonaler og / eller filer, så jeg vil bruge en tjeneste til bevægelsen af ​​disse fire stykker. Bønder har unikke bevægelsesegenskaber, så en særlig tjeneste vil blive brugt til dem. Det samme gælder for riddere, da de kan springe over stykker og ikke bevæge sig langs filer eller rækker.

For eksempel kan en tårn flytte 7 firkanter langs en hvilken som helst rang eller fil på et 15x15 bord, hvor tårnet er centreret. Lignende regler gælder for biskop og dronning. Kongen er begrænset til et firkantet interval i enhver retning (undtagelsen er castling, som jeg vil behandle i et fremtidigt indlæg).

Jeg vil bruge en ChessPieceklasse til at holde information om skakstykkets type og placering. Det spiller ikke en alt for vigtig rolle for nu, men det vil senere, når jeg udvider anvendelsesområdet for de regler, der er omfattet af tjenesterne.

Første gudstjeneste: Rook, Bishop, Queen og King flytter

I Seneca påkaldes tjenester via roleog cmd. Den roleligner en kategori og cmdnavngiver en bestemt tjeneste. Som vi vil se senere, kan en tjeneste specificeres yderligere af yderligere parametre.

Tjenester tilføjes ved hjælp af seneca.add()og påberåbes via seneca.act(). Lad os først se på tjenesten (fra 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); });

Lad os nu se, hvordan testen påkalder tjenesten (movesTest.js):

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

Bemærk, at ud over roleog cmdder er enpieceargument. Dette sammen med roleog cmder egenskaber formsgargument modtaget af tjenesten. Før du kan påberåbe dig tjenesten, skal du dog fortælle Seneca, hvilke tjenester du skal bruge:

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

De rå bevægelser for en biskop ved firkant a1 er i msgmodtaget tilbage fra tjenesten:

[{fil: '' ', rang:' 0 '},

{fil: 'b', rang: '2'},

{fil: '' ', rang:' 2 '},

{fil: 'b', rang: '0'},

{fil: '_', rang: '/'},

{fil: 'c', rang: '3'},

{fil: '_', rang: '3'},

{fil: 'c', rang: '/'},

{fil: '^', rang: '.' },

{fil: 'd', rang: '4'},

{fil: '^', rang: '4'},

{fil: 'd', rang: '.' },

{fil: ']', rang: '-'},

{fil: 'e', ​​rang: '5'},

{fil: ']', rang: '5'},

{fil: 'e', ​​rang: '-'},

{fil: '\\', rang: ','},

{fil: 'f', rang: '6'},

{fil: '\\', rang: '6'},

{fil: 'f', rang: ','},

{fil: '[', rang: '+'},

{fil: 'g', rang: '7'},

{fil: '[', rang: '7'},

{fil: 'g', rang: '+'},

{fil: 'Z', rang: '*'},

{fil: 'h', rang: '8'},

{fil: 'Z', rang: '8'},

{fil: 'h', rang: '*'}]

Bemærk, at der er nogle underlige firkanter opført! Dette er de positioner, der "falder af" 8x8-kortet og vil blive fjernet senere af en anden service.

Hvad skete der lige?

En tjeneste blev defineret med role=”movement”og cmd=”rawMoves”. Når act()der senere påkaldes, matches parametrene for handlingsanmodningen mod en tjeneste, der håndterer disse parametre (dette kaldes tjenestens mønster ). Som tidligere nævnt og som vil blive vist i det næste eksempel,roleog cmder ikke nødvendigvis de eneste parametre, der bestemmer den påkaldte tjeneste.

Næste tjenester: Bøn og riddere

Bønne bevæger sig en firkant fremad, medmindre de er på deres oprindelige firkant, i hvilket tilfælde de kan flytte en eller to firkanter fremad. Der er andre træk, som en bonde kan foretage, når det ikke er det eneste stykke på et tomt bræt, men det er til fremtidig overvejelse. Bøn starter altid på anden rang og kan aldrig bevæge sig baglæns.

Knights move in an L-shape pattern. In our imaginary 15x15 board with the knight centered, there will always be eight possible moves.

I’ll write two services (one for pawns, the other for knights) and place both in one module (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); }); }

See the isPawnand isKnightparameters in the services? The first object passed to Seneca add() is called the service pattern. What happens is that Seneca will invoke the service with the most specific pattern match. In order to invoke the right service, I need to addisPawn:true or isKnight:true to the act request:

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) => {

Legal Moves

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!).

Det er det for nu! I et fremtidigt indlæg vil jeg gå over tjenester, der beskæftiger sig med venlige brikker, der hindrer bevægelse, og derefter beskæftige mig med den potentielle fangst af fjendtlige brikker. Yderligere tjenester vil håndtere regler for castling, en passant, check, skakmat og dødvande.

Al kildekode kan findes her.

Fortsæt til del 2 i denne serie.