node seneca
Finishing up a three-part series on writing a rules engine with Seneca microservices.
完成有關使用Seneca微服務編寫規則引擎的三部分系列文章。
Parts 1 & 2 of this series covered:
本系列的第1部分和第2部分涉及:
- The Seneca microservices Node.js module Seneca微服務Node.js模塊
- How to write a service, how to identify it by pattern and how to call it 如何編寫服務,如何通過模式識別服務以及如何調用服務
- How to string service calls together 如何將服務電話串在一起
- How to enhance an existing service 如何增強現有服務
Along the way, I pondered what a service should return. I came to the conclusion that returning a data object (JSON in this case) was the most flexible. It allows services to embellish the output without affecting existing clients of the service.
在此過程中,我考慮了服務應返回的內容。 我得出的結論是,返回數據對象(在這種情況下為JSON)是最靈活的。 它可以修飾服務 輸出而不會影響該服務的現有客戶。
Embellish? By that I mean intermediate results can be maintained as a means of tracking information that might be useful later, to a service not yet written. In the present case, I had a rawMoves
service that returned a list of moves. That was immediately sufficient for the clients I had. The service calculated moves along movement vectors, and combined them into a 1-dimensional array.
潤? 我的意思是說,中間結果可以作為一種跟蹤信息的方式來維護,該信息以后可能對尚未編寫的服務有用。 在目前的情況下,我有一個rawMoves
服務返回了動作列表。 對于我所擁有的客戶而言,這立即就足夠了。 計算出的服務沿著運動矢量移動,并將它們組合成一維數組。
Later though, I discovered that those movement vectors would have come in handy when a legalMoves
service written later needed to take into account friendly pieces that were blocking movement. Vectors would have made those calculations simpler and more efficient, but they were “tossed out” by the rawMoves
service.
不過,后來我發現,當legalMoves
時,這些運動向量會派上用場 稍后編寫的服務需要考慮阻礙移動的友好部分。 向量本可以使這些計算更簡單,更有效,但是rawMoves
它們“拋棄”了 服務。
To go back and add the vectors (in addition to the move list) meant changing the clients of the original service to accept an object, not an array. True, I could have made the original service stateful, but that would have been overkill. I had a choice: refactor the service and its clients, or Deal with It??. In Part 2, I chose the latter.
返回并添加矢量(除了移動列表之外)意味著更改原始服務的客戶端以接受對象,而不是數組。 沒錯,我本可以使原始服務具有狀態,但是那會太過分了。 我有一個選擇:重構服務及其客戶,或者處理It?? 。 在第2部分中,我選擇了后者。
Yet in this installment, time has come to refactor. rawMoves
now returns {moves, moveVectors}
, and the upstream clients of the service can choose what to pay attention to. Care has to be taken, though, that moves
and moveVectors
are in sync at all times.
然而在本期中,時間已經到了重構的時候了。 rawMoves
現在返回{moves, moveVectors}
,該服務的上游客戶端可以選擇要注意的內容。 小心,必須考慮,雖然, moves
和moveVectors
是同步的時刻。
Let’s see what the advantage is. In the original code, finding legalMoves
was an involved process if given just a piece, move list, and friendly pieces elsewhere on the board (example). Compare that code to one that uses moveVectors
:
讓我們看看有什么好處。 在原始代碼中,如果只給出一塊,移動列表和板上其他位置的友好塊,則找到legalMoves
是一個涉及過程( 示例) 。 將代碼與使用moveVectors
代碼進行比較:
module.exports = function (boardAndPiece, candidateMoves) {if (!boardAndPiece.board) return candidateMoves;const rangeChecks = {B: vectorChecks,R: vectorChecks,K: vectorChecks,Q: vectorChecks,P: pawnChecks,N: knightChecks};var rangeCheck = rangeChecks[boardAndPiece.piece.piece];return rangeCheck(boardAndPiece, candidateMoves)
}//...function vectorChecks(boardAndPiece, candidateMoves) {for (const [j, v] of candidateMoves.moveVectors.entries()) {for (const [i, m] of v.entries()) {const p = boardAndPiece.board.pieceAt(m);if (p) {if (p.color === boardAndPiece.piece.color) {candidateMoves.moveVectors[j] = v.slice(0, i);break;} else {candidateMoves.moveVectors[j] = v.slice(0, i + 1);Object.assign(candidateMoves.moveVectors[j].slice(-1)[0], {hasCaptured: p})break;}}}}return {moveVectors: candidateMoves.moveVectors,moves: Array.prototype.concat(...candidateMoves.moveVectors)}
}
Much, much simpler…and more efficient. The wrapping function is exported and used by the legalMoves
service.
更簡單,更高效。 包裝功能由legalMoves
服務導出并使用。
const legalMovesWithBoard = require("./helpers/legalMovesWithBoard")
//...this.add('role:movement,cmd:legalMoves', function (msg, reply) {this.prior(msg, function (err, result) {if (msg.board) {const result2 = legalMovesWithBoard(msg, result);//...
回到游戲 (Back to the Game)
服務總覽 (Service Overview)
All movement requests are handled by the legalMoves
service, which relies on several other services and helper methods:
所有移動請求均由legalMoves
處理 服務,它依賴于其他幾種服務和輔助方法:
Call the
rawMoves
service調用
rawMoves
服務This will return all moves of a lone piece on a virtual 15x15 chessboard (referred to as the
這將返回虛擬15x15棋盤上的單個棋子的所有移動(稱為
movement mask). Explained in Part 1
運動面具 )。 在第1部分中解釋
Call the base
legalMoves
service叫基地
legalMoves
服務This will clip the
這將剪輯
movement mask at the edge of the “real” 8x8 board, with proper algebraic coordinates. Explained in Part 2
位于“真實” 8x8電路板邊緣的運動遮罩 ,具有適當的代數坐標 。 在第2部分中解釋
Call the overriding
legalMoves
service呼叫最重要的
legalMoves
服務If there is a board as part of the incoming message (the service pattern), then a series of checks is done to account for the presence of friendly and opposing pieces, because these will affect movement. Explained in this part (Part 3).
如果傳入消息(服務模式)中有一塊板,則要進行一系列檢查以檢查是否存在友好的相對的部件,因為它們會影響運動。 在本部分(第3部分)中進行解釋。
So Part 2 took care of friendly pieces blocking other friendly pieces, but now there are those annoying enemy pieces to deal with. Like friendly pieces, enemy pieces can block movement, but they can also be captured. Under some conditions, enemy pieces may even increase our movement options.
因此, 第2部分照顧了友軍,阻止了其他友軍,但現在有那些煩人的敵軍要處理。 像友軍碎片一樣,敵方碎片可以阻止移動,但也可以將其捕獲。 在某些情況下,敵方部隊甚至可能增加我們的行動選擇。
Then there’s castling: the only move where two pieces can shift their position at once. Special considerations apply, some of which involve enemy pieces.
然后就是cast聲:唯一的動作,兩個棋子可以一次移動其位置。 需要特別考慮,其中一些涉及敵人。
女王,白嘴鴉和主教 (Queen, Rook, & Bishop)
The new rules involving enemy pieces extend or modify the original legalMoves
service in Part 2 that dealt with friendly pieces only. The new microservice extension will need to know if the blocking piece is friend or foe. If friend, then movement is blocked at the square before. If foe, then movement is blocked by the square of the opposing piece (by capture). In the list of legal moves returned by a piece, we will denote captures by setting a hasCaptured
flag, along with the type of enemy piece to be captured.
涉及敵方碎片的新規則擴展或修改了第2部分中僅處理友好碎片的原始legalMoves
服務。 新的微服務擴展將需要知道阻塞塊是敵還是友。 如果是朋友,則運動被阻止在廣場之前。 如果是敵人,則移動被對方棋子的方塊阻止(通過捕獲)。 在棋子返回的合法動作列表中,我們將通過設置hasCaptured
標志以及要捕捉的敵軍棋子的類型來表示捕捉。
The vectorChecks
helper method shown in the previous gist listing handles all vector-based movement for Queen, Rook, and Bishop.
前面要點清單中顯示的vectorChecks
幫助器方法可以處理Queen,Rook和Bishop的所有基于矢量的移動。
騎士 (Knight)
Knights jump around the board, so are only blocked by friendly pieces that are on one of its potential landing squares. An enemy piece does not block, but would be captured if a Knight landed on it. The method used by the legalMoves
service is easy to write.
騎士在棋盤上跳來跳去,因此只被其潛在著陸廣場之一上的友軍所阻擋。 敵人的碎片不會阻擋,但如果有騎士降落,它將被捕獲。 legalMoves
使用的方法 服務很容易寫。
function knightChecks(boardAndPiece, candidateMoves) {const newMoves = [];for (const m of candidateMoves.moves) {const p = boardAndPiece.board.pieceAt(m)if (!p) {newMoves.push(m)} else if (p.color !== boardAndPiece.piece.color) {m.hasCaptured = p;newMoves.push(m)}}return {moves: newMoves,moveVectors: [newMoves]};
}
典當 (Pawn)
Pawns at first seem like a pretty simple case. The pawn is blocked if any piece whether friend or enemy stands in front of it. But it can to move one square diagonally forward to capture an enemy that sits in that square.
最初的典當似乎很簡單。 如果任何棋子(無論是朋友還是敵人)站在它的前面,它都會被阻止。 但是它可以對角線向前移動一個正方形,以俘獲一個坐在那個正方形中的敵人。
There is also the en passant rule, where a pawn can capture an adjacent enemy pawn that just moved two squares on the previous turn:
也有順便規則,其中的棋子可以捕獲相鄰的敵人棋子, 只是移動上一轉兩個格:
And then there’s the issue of mandatory promotion once a pawn reaches the 8th rank. Confusingly, this refers to the eighth rank in front of the pawn, which would be the first rank of the board coordinates if playing Black.
當棋子達到第8位時,就會出現強制升級的問題。 令人困惑的是,這指的是棋子前面的第8位,如果玩Black,那將是棋盤坐標的第1位。
All these considerations make for a rather involved set of rules to determine the pawn’s movement options. These can be found in the accompanying source code at GitHub.
所有這些考慮因素都構成了一套相當復雜的規則來確定棋子的移動選項。 這些可以發現在所附的源代碼在GitHub上。
國王 (King)
The Pawn was a bit of work, but the king even more so. There are several conditions:
Pawn有點工作,但國王更是如此。 有幾個條件:
Is a potential move square controlled by an enemy piece?
潛在的移動方塊是否受到敵方控制?
Eliminate that option.
消除該選項。
Is the king in check?
國王在檢查嗎?
If so, it
如果是這樣
must move this turn
必須轉彎
* If it is in check, and can’t move out of check, game over! Checkmate!
*如果在檢查中,并且不能移出檢查,則游戲結束! 將死!
* If it is not in check, but there are no other legal moves by any friendly piece on the board, stalemate!
*如果不在檢查范圍之內,但董事會上任何友好的行動都沒有其他合法行動,就此成為僵局!
Can the King castle (queen side or king side)?
國王城堡(女王側還是國王側)可以嗎?
* King is in check: No.
*國王在檢查:不。
* King has previously moved: No.
* King之前已搬家:否。
* Rook has previously moved: No.
* Rook之前已搬家:否。
* Intervening squares between K and R occupied: No.
*占據K和R之間的中間方格:否。
* Intervening squares empty, but controlled by enemy piece: No.
*中間方塊為空,但由敵方控制:否
* Otherwise: Yes.
*否則:是。
This service I will break down into detail. As you may recall, the legalMoves
service is broken into two parts. One part treats a piece as if it is alone on the board. The other part deals with friendly and opposing pieces. Let’s look at the listing:
我將詳細介紹這項服務。 您可能還記得, legalMoves
服務分為兩部分。 一部分將棋子視為單獨在板上。 另一部分涉及友好和對立的部分。 讓我們看一下清單:
this.add('role:movement,cmd:legalMoves', function (msg, reply) {this.prior(msg, function (err, result) {if (msg.board) {const result2 = legalMovesWithBoard(msg, result);if (msg.piece.piece === 'K') {legalMovesWithKing.call(this, msg, result2, reply)} else {reply(err, result2);}} else {reply(err, result);}});});
For every piece but the King, we simply call the base service (via the Seneca framework’s prior()
method) followed by the helper method legalMovesWithBoard()
, parts of which were listed in the previous gists of this post.
對于每一塊,但王,我們只需調用基本服務(通過塞內卡框架的prior()
方法),其次是輔助方法legalMovesWithBoard()
這部分在這篇文章的前學家上市。
If the piece is a King, the additional helper method legalMovesWithKing()
is called. The calling parameters are the this
reference, a msg
object containing board and the piece being moved (the King), the result2
which was came from the base legalMoves
service call (this contains movement info), and the reply
callback.
如果作品是國王,則調用附加的輔助方法legalMovesWithKing()
。 調用參數是this
引用,一個包含木板和正在移動的棋子(國王)的msg
對象, result2
來自基礎legalMoves
服務呼叫(其中包含移動信息)和reply
回調。
There’s a bit of code to slog through, so I will refer to sections by line number:
有一些代碼可以通過,所以我將按行號引用各節:
module.exports = function (boardAndPiece, candidateMoves, reply) {const opposingColor = boardAndPiece.piece.color === 'W' ? 'black' : 'white';//temporarily remove the K to avoid cyclesboardAndPiece.board.removePiece(boardAndPiece.piece);function canCastle(king, rook, intervening, opposing) {// console.log("canCastle", arguments)const opposingControlled = [...opposing.controlled]const board = boardAndPiece.board;const canCastle = !candidateMoves.inCheck &&!king.hasMoved &&rook &&rook.color === king.color &&!rook.hasMoved;if (!canCastle) return false;const pieceInTheWay = !!intervening.find(sq => board.pieceAt(sq));if (pieceInTheWay) return false;const passThruCheck = !!intervening.find(sq =>opposingControlled.find(opp => (opp.rank === sq.rank && opp.file == sq.file)))if (passThruCheck) return false;return true;}this.use(require('../SquareControl'))this.act({role: "board",cmd: "squaresControlledBy",board: boardAndPiece.board,color: opposingColor,}, (err, opposing) => {if (err) {reply(err);return;}const king = boardAndPiece.piece;// console.log(opposing.controlled)// add the removed K back inboardAndPiece.board.addPiece(king);const filteredMoves = candidateMoves.moves.filter(m =>!!!opposing.controlled.find(o => o.rank === m.rank && o.file === m.file))const kingSq = king.position;const inCheck = !!opposing.controlled.find(o => o.rank === kingSq.rank && o.file === kingSq.file)const additional = {}additional.inCheck = inCheck;additional.checkMated = (inCheck && filteredMoves.length === 0)const rank = additional.color === 'W' ? 1 : 8;let rook = boardAndPiece.board.pieceAt(`a${rank}`);let intervening = [`b${rank}`, `c${rank}`, `d${rank}`]additional.canQSideCastle = canCastle(king, rook, intervening, opposing)rook = boardAndPiece.board.pieceAt(`h${rank}`);intervening = [`f${rank}`, `g${rank}`]additional.canKSideCastle = canCastle(king, rook, intervening, opposing)candidateMoves.moves = filteredMoves;delete candidateMoves.moveVectors; // no longer valid, and no longer neededObject.assign(candidateMoves, additional);console.log(candidateMoves)reply(null, candidateMoves)});
};
Let start from the middle, at line 30. A service called squaresControlledBy
is imported into the framework from SquareControl.js. It gathers all legal moves of the opposing side and calls those the controlled squares. We need this information because the King cannot move into a square ‘controlled’ by the enemy. The King cannot move into check.
讓我們從中間的第30行開始。一個名為squaresControlledBy
的服務 從SquareControl.js導入到框架中。 它收集了對方的所有合法舉動,并稱這些為受控方。 我們需要這些信息,因為國王無法進入被敵人“控制”的廣場。 國王無法阻止。
There’s a tricky bit to this, and that is because the squaresControlledBy
service relies on the legalMoves
service. What can happen is that:
這有一個棘手的問題,這是因為squaresControlledBy
服務依賴legalMoves
服務。 可能發生的情況是:
legalMoves
service is called for friendly piecelegalMoves
服務被稱為友好作品if the friendly piece is a King,
squaresControlledBy
is called for opposing side如果友善的棋子是國王,
squaresControlledBy
稱為對面squaresControlledBy
requestslegalMoves
for all opposing sides piecessquaresControlledBy
要求為所有相對的邊塊legalMoves
if
legalMoves
is requested for the opposing King, it will call servicesquaresControlledBy
for its opposing side (our side).如果
legalMoves
要求為反對國王,它會調用服務squaresControlledBy
其相反側(我方)。- we’ve come full circle, and round and round we go… 我們走了整整一個圈,然后又走了……
These cycles are one of the gotchas of microservices, and have to be carefully accounted for. I won’t go into the various strategies for dealing with this, but Seneca provides trace options for actions ( — seneca.print.tree)
and service invocations ( — seneca.log.all)
that can be helpful in debugging.
這些周期是微服務的陷阱之一,必須仔細考慮。 我不會討論用于解決此問題的各種策略,但是Seneca提供了對操作( — seneca.print.tree)
和服務調用( — seneca.log.all)
跟蹤選項,這些選項可能有助于調試。
The trick I used to avoid endless cycling was to temporarily remove the friendly king from the board (line 5) and later add it back in (line 46). I would say that best practice would be to not modify incoming service action data. There are potential hard-to-track side-effects. For purposes of finishing this series in a reasonable time frame, though, I will overlook a bit of fudging.
我用來避免無休止循環的訣竅是從板上暫時刪除友好的國王(第5行),然后將其重新添加到第46行。 我會說,最佳實踐是不修改傳入的服務操作數據。 存在潛在的難以追蹤的副作用。 但是,為了在合理的時間范圍內完成本系列文章,我會忽略一些麻煩。
We push additional information (inCheck
, castle options [lines 7–28], checkmate
) to the reply
by storing it in a local data structure and then using Object.assign()
to merge it into the candidateMoves
structure. The candidateMoves
object will now have moves long with new properties provided by the additional object (lines 54–73).
通過將其他信息存儲在本地數據結構中,然后使用Object.assign()
將其合并到candidateMoves
結構中,我們將其他信息( inCheck
,城堡選項[7–28行, checkmate
)推送到reply
。 現在,帶有附加對象提供的新屬性的candidateMoves
移動對象將移動很長時間(第54-73行)。
That wraps it up! Remember, if you found this series useful and engaging, please don’t forget to recommend it (click that little heart icon). Feedback always welcome.
結束了! 請記住,如果您發現本系列有用且引人入勝,請不要忘記推薦它(單擊該小心臟圖標)。 反饋隨時歡迎。
Full source (including tests) for this Part 3 of the series can be found here.
該系列第3部分的完整資源(包括測試)可以在這里找到。
翻譯自: https://www.freecodecamp.org/news/writing-a-chess-microservice-using-node-js-and-seneca-part-3-ab38b8ef9b0a/
node seneca