node seneca_使用Node.js和Seneca编写国际象棋微服务,第3部分

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} ,该服务的上游客户端可以选择要注意的内容。 小心,必须考虑,虽然, movesmoveVectors是同步的时刻。

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 cycles
    boardAndPiece.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 in
        boardAndPiece.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 needed

        Object.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 piece

    legalMoves服务被称为友好作品

  • if the friendly piece is a King, squaresControlledBy is called for opposing side

    如果友善的棋子是国王, squaresControlledBy称为对面

  • squaresControlledBy requests legalMoves for all opposing sides pieces

    squaresControlledBy要求为所有相对的边块legalMoves

  • if legalMoves is requested for the opposing King, it will call service squaresControlledBy 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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值