(Ovo je 1. dio trodijelne serije [2. dio, 3. dio])
Počeo sam omatati glavu oko mikroservisa. Do ovog trenutka smatrao sam ga uzorkom skalabilnosti i previdio principe funkcionalnog programiranja koji stoje iza njega.
Pravila šaha mogu se lako razgraditi na mikroservise. Nisu ni slučajni ni dvosmisleni, što je savršeno za pisanje malih službi bez državljanstva koje se bave pokretima raznih dijelova.
U ovom postu proći ću kroz nekoliko usluga koje sam stvorio i koje utvrđuju koji su pravni potezi za usamljene figure na praznoj šahovskoj ploči. Upotrijebit ćemo okvir Seneca, alat za mikrousluge za Node.js, jer je intuitivan i dobro dokumentiran.
Postavljanje Seneke
Seneca je Node.js modul koji se instalira pomoću npm:
npm install seneca
Također, oslanjat ćemo se na globalno instalirane mocha / chai module za testove koji će ilustrirati funkcionalnost.
Pronađite sve pravne poteze
Zapravo nije potrebno održavati prikaz šahovske ploče u memoriji, već figure i njihovo mjesto na koordinatnoj mreži 8x8. Algebarska notacija obično se koristi za opisivanje koordinata na šahovskoj ploči, gdje su datoteke označene slovima, a redovi brojevima:

Za igrača koji je bijeli, krajnji desni donji kut je h1; za Crnu je a8. Točak na b2, koji se kreće u kvadrat f2, označavao bi se kao Rb2-f2.
Sirovi potezi
Definiram sirove poteze kao poteze koje bi komad napravio ako ih ne bi ometali drugi dijelovi ili rub ploče . Taj zadnji bit može se činiti čudnim, ali omogućuje mi konstrukciju maske pokreta 15x15, koja se zatim skraćuje da stane na ploču 8x8. Momak po imenu Procrustes na sličnu je ideju došao prije mnogo vremena.
Kraljevi, kraljice, biskupi i topovi kreću se po dijagonalama i / ili datotekama, pa ću za kretanje ta četiri dijela koristiti jednu uslugu. Pioni imaju jedinstvene karakteristike kretanja, pa će se za njih koristiti posebna usluga. Isto vrijedi i za vitezove, jer oni mogu preskakati komade i ne kretati se po datotekama ili redovima.
Na primjer, topot može pomicati 7 kvadrata duž bilo kojeg ranga ili datoteke na ploči 15x15 u kojoj je tok usredotočen. Slična pravila vrijede za biskupa i kraljicu. Kralj je ograničen na raspon od jednog kvadrata u bilo kojem smjeru (iznimka je zamka, kojom ću se pozabaviti u budućem postu).
Koristit ću ChessPiece
nastavu da čuvam informacije o vrsti i mjestu svakog šahovskog komada. Zasad to neće igrati previše važnu ulogu, ali kasnije, kad proširim opseg pravila obuhvaćenih uslugama.
Prva usluga: Rook, biskup, kraljica i kralj
U Seneci se usluge pozivaju putem role
i cmd
. To role
je slično kategoriji i cmd
imenuje određenu uslugu. Kao što ćemo vidjeti kasnije, usluga se može dodatno odrediti dodatnim parametrima.
Usluge se dodaju seneca.add()
i pozivaju putem seneca.act()
. Pogledajmo prvo uslugu (s 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); });
Sada da vidimo kako test poziva uslugu (moveTest.js):
var Ba1 = new ChessPiece('Ba1'); seneca.act({ role: "movement", cmd: "rawMoves", piece: Ba1 }, (err, msg) => {...});
Imajte na umu da pored role
i cmd
, postojipiece
argument. To su, zajedno s role
i cmd
, svojstva datotekemsg
argument primljen od službe. Prije nego što se možete pozvati na uslugu, morate reći Seneci koje usluge koristiti:
var movement = require(‘../services/Movement’) const seneca = require('seneca')({ log: 'silent' }) .use(movement);
Sirovi potezi za biskupa na kvadratu a1 su u msg
primljeno natrag od usluge:
[{datoteka: '' ', rang:' 0 '},
{datoteka: 'b', rang: '2'},
{datoteka: '' ', rang:' 2 '},
{datoteka: 'b', rang: '0'},
{datoteka: '_', rang: '/'},
{datoteka: 'c', rang: '3'},
{datoteka: '_', rang: '3'},
{datoteka: 'c', rang: '/'},
{datoteka: '^', rang: '.' },
{datoteka: 'd', rang: '4'},
{datoteka: '^', rang: '4'},
{datoteka: 'd', rang: '.' },
{datoteka: ']', rang: '-'},
{datoteka: 'e', rang: '5'},
{datoteka: ']', rang: '5'},
{datoteka: 'e', rang: '-'},
{datoteka: '\\', rang: ','},
{datoteka: 'f', rang: '6'},
{datoteka: '\\', rang: '6'},
{datoteka: 'f', rang: ','},
{datoteka: '[', rang: '+'},
{datoteka: 'g', rang: '7'},
{datoteka: '[', rang: '7'},
{datoteka: 'g', rang: '+'},
{datoteka: 'Z', rang: '*'},
{datoteka: 'h', rang: '8'},
{datoteka: 'Z', rang: '8'},
{datoteka: 'h', rang: '*'}]
Imajte na umu da su navedeni neki čudni kvadrati! To su položaji koji "otpadaju" s ploče 8x8 i kasnije će ih eliminirati druga usluga.
Što se upravo dogodilo?
Usluga je definirana s role=”movement”
i cmd=”rawMoves”
. Kad act()
se kasnije pozove, parametri zahtjeva za činom podudaraju se sa uslugom koja obrađuje te parametre (to se naziva obrazac usluge ). Kao što je prethodno spomenuto i kao što će biti prikazano u sljedećem primjeru,role
i cmd
nisu nužno jedini parametri koji određuju uslugu koja se poziva.
Sljedeće usluge: Pawns and Knights
Pioni se pomiču za jedan kvadrat naprijed, osim ako nisu na svom izvornom kvadratu, u tom slučaju mogu pomaknuti jedan ili dva kvadrata prema naprijed. Postoje i drugi potezi koje pijun može povući kada to nije usamljeni komad na praznoj ploči, ali to je za buduće razmatranje. Pioni uvijek počinju s drugog ranga i nikad se ne mogu pomaknuti unatrag.
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 isPawn
and isKnight
parameters 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 legalSquares
service first invokes the rawMoves
service. 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 isKnight
or isPawn
pattern field to true for either of those two pieces… if both are false, then the “regular” rawMoves
service for K,Q,B,R will be invoked.
Once the raw moves are retrieved, then the legalSquares
service 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.