+ const oppCol = V.GetOppCol(c);
+ let moves = [];
+ let i = 0;
+ // King, then rook or jailer:
+ const finalSquares = [
+ [2, 3],
+ [V.size.y - 2, V.size.y - 3]
+ ];
+ castlingCheck: for (
+ let castleSide = 0;
+ castleSide < 2;
+ castleSide++
+ ) {
+ if (!this.castleFlags[c][castleSide]) continue;
+ // Rook (or jailer) and king are on initial position
+
+ const finDist = finalSquares[castleSide][0] - y;
+ let step = finDist / Math.max(1, Math.abs(finDist));
+ i = y;
+ do {
+ if (
+ this.isAttacked([x, i], [oppCol]) ||
+ (this.board[x][i] != V.EMPTY &&
+ (this.getColor(x, i) != c ||
+ ![V.KING, V.ROOK].includes(this.getPiece(x, i))))
+ ) {
+ continue castlingCheck;
+ }
+ i += step;
+ } while (i != finalSquares[castleSide][0]);
+
+ step = castleSide == 0 ? -1 : 1;
+ const rookOrJailerPos =
+ castleSide == 0
+ ? Math.min(this.INIT_COL_ROOK[c], this.INIT_COL_JAILER[c])
+ : Math.max(this.INIT_COL_ROOK[c], this.INIT_COL_JAILER[c]);
+ for (i = y + step; i != rookOrJailerPos; i += step)
+ if (this.board[x][i] != V.EMPTY) continue castlingCheck;
+
+ // Nothing on final squares, except maybe king and castling rook or jailer?
+ for (i = 0; i < 2; i++) {
+ if (
+ this.board[x][finalSquares[castleSide][i]] != V.EMPTY &&
+ this.getPiece(x, finalSquares[castleSide][i]) != V.KING &&
+ finalSquares[castleSide][i] != rookOrJailerPos
+ ) {
+ continue castlingCheck;
+ }
+ }
+
+ // If this code is reached, castle is valid
+ const castlingPiece = this.getPiece(firstRank, rookOrJailerPos);
+ moves.push(
+ new Move({
+ appear: [
+ new PiPo({ x: x, y: finalSquares[castleSide][0], p: V.KING, c: c }),
+ new PiPo({ x: x, y: finalSquares[castleSide][1], p: castlingPiece, c: c })
+ ],
+ vanish: [
+ new PiPo({ x: x, y: y, p: V.KING, c: c }),
+ new PiPo({ x: x, y: rookOrJailerPos, p: castlingPiece, c: c })
+ ],
+ end:
+ Math.abs(y - rookOrJailerPos) <= 2
+ ? { x: x, y: rookOrJailerPos }
+ : { x: x, y: y + 2 * (castleSide == 0 ? -1 : 1) }
+ })
+ );
+ }
+
+ return moves;
+ }
+
+ filterValid(moves) {
+ // Disable check tests for sentry pushes,
+ // because in this case the move isn't finished
+ let movesWithoutSentryPushes = [];
+ let movesWithSentryPushes = [];
+ moves.forEach(m => {
+ if (m.appear.length > 0) movesWithoutSentryPushes.push(m);
+ else movesWithSentryPushes.push(m);
+ });
+ const filteredMoves = super.filterValid(movesWithoutSentryPushes);
+ // If at least one full move made, everything is allowed:
+ if (this.movesCount >= 2)
+ return filteredMoves.concat(movesWithSentryPushes);
+ // Else, forbid checks and captures:
+ const oppCol = V.GetOppCol(this.turn);
+ return filteredMoves.filter(m => {
+ if (m.vanish.length == 2 && m.appear.length == 1) return false;
+ this.play(m);
+ const res = !this.underCheck(oppCol);
+ this.undo(m);
+ return res;
+ }).concat(movesWithSentryPushes);
+ }
+
+ getAllValidMoves() {
+ if (this.subTurn == 1) return super.getAllValidMoves();
+ // Sentry push:
+ const sentrySq = [this.sentryPos.x, this.sentryPos.y];
+ return this.filterValid(this.getPotentialMovesFrom(sentrySq));
+ }
+
+ updateVariables(move) {
+ const c = this.turn;
+ const piece = move.vanish[0].p;
+ const firstRank = (c == "w" ? V.size.x - 1 : 0);
+
+ // Update king position + flags
+ if (piece == V.KING) {
+ this.kingPos[c][0] = move.appear[0].x;
+ this.kingPos[c][1] = move.appear[0].y;
+ this.castleFlags[c] = [false, false];
+ return;
+ }
+
+ // Update castling flags if rook or jailer moved (or is captured)
+ const oppCol = V.GetOppCol(c);
+ const oppFirstRank = V.size.x - 1 - firstRank;
+ let flagIdx = 0;
+ if (
+ // Our rook moves?
+ move.start.x == firstRank &&
+ this.INIT_COL_ROOK[c] == move.start.y
+ ) {
+ if (this.INIT_COL_ROOK[c] > this.INIT_COL_JAILER[c]) flagIdx++;
+ this.castleFlags[c][flagIdx] = false;
+ } else if (
+ // Our jailer moves?
+ move.start.x == firstRank &&
+ this.INIT_COL_JAILER[c] == move.start.y
+ ) {
+ if (this.INIT_COL_JAILER[c] > this.INIT_COL_ROOK[c]) flagIdx++;
+ this.castleFlags[c][flagIdx] = false;
+ } else if (
+ // We took opponent's rook?
+ move.end.x == oppFirstRank &&
+ this.INIT_COL_ROOK[oppCol] == move.end.y
+ ) {
+ if (this.INIT_COL_ROOK[oppCol] > this.INIT_COL_JAILER[oppCol]) flagIdx++;
+ this.castleFlags[oppCol][flagIdx] = false;
+ } else if (
+ // We took opponent's jailer?
+ move.end.x == oppFirstRank &&
+ this.INIT_COL_JAILER[oppCol] == move.end.y
+ ) {
+ if (this.INIT_COL_JAILER[oppCol] > this.INIT_COL_ROOK[oppCol]) flagIdx++;
+ this.castleFlags[oppCol][flagIdx] = false;
+ }
+
+ if (move.appear.length == 0 && move.vanish.length == 1) {
+ // The sentry is about to push a piece: subTurn goes from 1 to 2
+ this.sentryPos = { x: move.end.x, y: move.end.y };
+ } else if (this.subTurn == 2) {
+ // A piece is pushed: forbid array of squares between start and end
+ // of move, included (except if it's a pawn)
+ let squares = [];
+ if (move.vanish[0].p != V.PAWN) {
+ if ([V.KNIGHT,V.KING].includes(move.vanish[0].p))
+ // short-range pieces: just forbid initial square
+ squares.push(move.start);
+ else {
+ const deltaX = move.end.x - move.start.x;
+ const deltaY = move.end.y - move.start.y;
+ const step = [
+ deltaX / Math.abs(deltaX) || 0,
+ deltaY / Math.abs(deltaY) || 0
+ ];
+ for (
+ let sq = {x: move.start.x, y: move.start.y};
+ sq.x != move.end.x && sq.y != move.end.y;
+ sq.x += step[0], sq.y += step[1]
+ ) {
+ squares.push(sq);
+ }
+ }
+ // Add end square as well, to know if I was pushed (useful for lancers)
+ squares.push(move.end);
+ }
+ this.sentryPush.push(squares);
+ } else this.sentryPush.push(null);
+ }
+
+ // TODO: cleaner (global) update/unupdate variables logic, rename...
+ unupdateVariables(move) {
+ super.unupdateVariables(move);
+ this.sentryPush.pop();
+ }
+
+ play(move) {
+ move.flags = JSON.stringify(this.aggregateFlags());
+ this.epSquares.push(this.getEpSquare(move));
+ V.PlayOnBoard(this.board, move);
+ this.updateVariables(move);
+ // Is it a sentry push? (useful for undo)
+ move.sentryPush = (this.subTurn == 2);
+ if (this.subTurn == 1) this.movesCount++;
+ if (move.appear.length == 0 && move.vanish.length == 1) this.subTurn = 2;
+ else {
+ // Turn changes only if not a sentry "pre-push"
+ this.turn = V.GetOppCol(this.turn);
+ this.subTurn = 1;
+ }
+ }
+
+ undo(move) {
+ this.epSquares.pop();
+ this.disaggregateFlags(JSON.parse(move.flags));
+ V.UndoOnBoard(this.board, move);
+ const L = this.sentryPush.length;
+ // Decrement movesCount except if the move is a sentry push
+ if (!move.sentryPush) this.movesCount--;
+ // Turn changes only if not undoing second part of a sentry push
+ if (!move.sentryPush || this.subTurn == 1)
+ this.turn = V.GetOppCol(this.turn);
+ this.unupdateVariables(move);
+ }
+
+ isAttacked(sq, colors) {
+ return (
+ super.isAttacked(sq, colors) ||
+ this.isAttackedByLancer(sq, colors) ||
+ this.isAttackedBySentry(sq, colors)
+ );
+ }
+
+ isAttackedByLancer([x, y], colors) {
+ for (let step of V.steps[V.ROOK].concat(V.steps[V.BISHOP])) {
+ // If in this direction there are only enemy pieces and empty squares,
+ // and we meet a lancer: can he reach us?
+ // NOTE: do not stop at first lancer, there might be several!
+ let coord = { x: x + step[0], y: y + step[1] };
+ let lancerPos = [];
+ while (
+ V.OnBoard(coord.x, coord.y) &&
+ (
+ this.board[coord.x][coord.y] == V.EMPTY ||
+ colors.includes(this.getColor(coord.x, coord.y))
+ )
+ ) {
+ lancerPos.push(coord);
+ }
+ for (let xy of lancerPos) {
+ const dir = V.LANCER_DIRS[this.board[xy.x][xy.y].charAt(1)];
+ if (dir[0] == -step[0] && dir[1] == -step[1]) return true;
+ }
+ }
+ return false;
+ }
+
+ // Helper to check sentries attacks:
+ selfAttack([x1, y1], [x2, y2]) {
+ const color = this.getColor(x1, y1);
+ const sliderAttack = (allowedSteps, lancer) => {
+ const deltaX = x2 - x1;
+ const deltaY = y2 - y1;
+ const step = [ deltaX / Math.abs(deltaX), deltaY / Math.abs(deltaY) ];
+ if (allowedStep.every(st => st[0] != step[0] || st[1] != step[1]))
+ return false;
+ let sq = [ x1 = step[0], y1 + step[1] ];
+ while (sq[0] != x2 && sq[1] != y2) {
+ if (
+ (!lancer && this.board[sq[0]][sq[1]] != V.EMPTY) ||
+ (!!lancer && this.getColor(sq[0], sq[1]) != color)
+ ) {
+ return false;
+ }
+ }
+ return true;
+ };
+ switch (this.getPiece(x1, y1)) {
+ case V.PAWN: {
+ // Pushed pawns move as enemy pawns
+ const shift = (color == 'w' ? 1 : -1);
+ return (x1 + shift == x2 && Math.abs(y1 - y2) == 1);
+ }
+ case V.KNIGHT: {
+ const deltaX = Math.abs(x1 - x2);
+ const deltaY = Math.abs(y1 - y2);
+ return (
+ deltaX + deltaY == 3 &&
+ [1, 2].includes(deltaX) &&
+ [1, 2].includes(deltaY)
+ );
+ }
+ case V.ROOK:
+ return sliderAttack(V.steps[V.ROOK]);
+ case V.BISHOP:
+ return sliderAttack(V.steps[V.BISHOP]);
+ case V.QUEEN:
+ return sliderAttack(V.steps[V.ROOK].concat(V.steps[V.BISHOP]));
+ case V.LANCER: {
+ // Special case: as long as no enemy units stands in-between, it attacks
+ // (if it points toward the king).
+ const allowedStep = V.LANCER_DIRS[this.board[x1][y1].charAt(1)];
+ return sliderAttack([allowedStep], "lancer");
+ }
+ // No sentries or jailer tests: they cannot self-capture
+ }
+ return false;
+ }
+
+ isAttackedBySentry([x, y], colors) {
+ // Attacked by sentry means it can self-take our king.
+ // Just check diagonals of enemy sentry(ies), and if it reaches
+ // one of our pieces: can I self-take?
+ const color = V.GetOppCol(colors[0]);
+ let candidates = [];
+ for (let i=0; i<V.size.x; i++) {
+ for (let j=0; j<V.size.y; j++) {
+ if (
+ this.getPiece(i,j) == V.SENTRY &&
+ colors.includes(this.getColor(i,j))
+ ) {
+ for (let step of V.steps[V.BISHOP]) {
+ let sq = [ i + step[0], j + step[1] ];
+ while (
+ V.OnBoard(sq[0], sq[1]) &&
+ this.board[sq[0]][sq[1]] == V.EMPTY
+ ) {
+ sq[0] += step[0];
+ sq[1] += step[1];
+ }
+ if (
+ V.OnBoard(sq[0], sq[1]) &&
+ this.getColor(sq[0], sq[1]) == color
+ ) {
+ candidates.push(sq);
+ }
+ }
+ }
+ }
+ }
+ for (let c of candidates)
+ if (this.selfAttack(c, [x, y])) return true;
+ return false;
+ }
+
+ // Jailer doesn't capture or give check