import { ArrayFun } from "@/utils/array";
-import { randInt, shuffle } from "@/utils/alea";
+import { randInt } from "@/utils/alea";
import { ChessRules, PiPo, Move } from "@/base_rules";
export const VariantRules = class EightpiecesRules extends ChessRules {
super.setOtherVariables(fen);
// subTurn == 2 only when a sentry moved, and is about to push something
this.subTurn = 1;
- // Pushing sentry position, updated after each push (subTurn == 1)
- this.sentryPos = { x: -1, y: -1 };
+ // Sentry position just after a "capture" (subTurn from 1 to 2)
+ this.sentryPos = null;
// Stack pieces' forbidden squares after a sentry move at each turn
const parsedFen = V.ParseFen(fen);
if (parsedFen.sentrypush == "-") this.sentryPush = [null];
}
}
- canTake([x1,y1], [x2, y2]) {
- if (this.subTurn == 2)
- // Sentry push: pieces can capture own color (only)
- return this.getColor(x1, y1) == this.getColor(x2, y2);
- return super.canTake([x1,y1], [x2, y2]);
- }
-
static GenRandInitFen(randomness) {
if (randomness == 0)
// Deterministic:
this.board[i][j] != V.EMPTY &&
this.getColor(i, j) == oppCol
) {
- const oppPiece = this.getPiece(i, j);
- if (oppPiece == V.JAILER) return [i, j];
+ if (this.getPiece(i, j) == V.JAILER) return [i, j];
}
}
return null;
x: ex,
y: ey,
c: tr ? tr.c : this.getColor(sx, sy),
- p: tr ? tr.p : this.board[sx][sy][1]
+ p: tr ? tr.p : this.board[sx][sy].charAt(1)
})
],
vanish: [
x: sx,
y: sy,
c: this.getColor(sx, sy),
- p: this.board[sx][sy][1]
+ p: this.board[sx][sy].charAt(1)
})
]
});
x: ex,
y: ey,
c: this.getColor(ex, ey),
- p: this.board[ex][ey][1]
+ p: this.board[ex][ey].charAt(1)
})
);
}
return mv;
}
- getPotentialMovesFrom_aux([x, y]) {
+ canIplay(side, [x, y]) {
+ return (
+ (this.subTurn == 1 && this.turn == side && this.getColor(x, y) == side) ||
+ (this.subTurn == 2 && x == this.sentryPos.x && y == this.sentryPos.y)
+ );
+ }
+
+ getPotentialMovesFrom([x,y]) {
+ // At subTurn == 2, jailers aren't effective (Jeff K)
+ if (this.subTurn == 1 && !!this.isImmobilized([x, y])) return [];
+ if (this.subTurn == 2) {
+ // Temporarily change pushed piece color.
+ // (Not using getPiece() because of lancers)
+ var oppCol = this.getColor(x, y);
+ var color = V.GetOppCol(oppCol);
+ var saveXYstate = this.board[x][y];
+ this.board[x][y] = color + this.board[x][y].charAt(1);
+ }
+ let moves = [];
switch (this.getPiece(x, y)) {
case V.JAILER:
- return this.getPotentialJailerMoves([x, y]);
+ moves = this.getPotentialJailerMoves([x, y]);
+ break;
case V.SENTRY:
- return this.getPotentialSentryMoves([x, y]);
+ moves = this.getPotentialSentryMoves([x, y]);
+ break;
case V.LANCER:
- return this.getPotentialLancerMoves([x, y]);
+ moves = this.getPotentialLancerMoves([x, y]);
+ break;
default:
- return super.getPotentialMovesFrom([x, y]);
+ moves = super.getPotentialMovesFrom([x, y]);
+ break;
}
- }
-
- getPotentialMovesFrom([x,y]) {
- if (this.subTurn == 1) {
- if (!!this.isImmobilized([x, y])) return [];
- let moves = this.getPotentialMovesFrom_aux([x, y]);
- const L = this.sentryPush.length;
- if (!!this.sentryPush[L-1]) {
- // Delete moves walking back on sentry push path
- moves = moves.filter(m => {
- if (
- m.vanish[0].p != V.PAWN &&
- this.sentryPush[L-1].some(sq => sq.x == m.end.x && sq.y == m.end.y)
- ) {
- return false;
- }
- return true;
- });
- }
- return moves;
+ const L = this.sentryPush.length;
+ if (!!this.sentryPush[L-1]) {
+ // Delete moves walking back on sentry push path
+ moves = moves.filter(m => {
+ if (
+ m.vanish[0].p != V.PAWN &&
+ this.sentryPush[L-1].some(sq => sq.x == m.end.x && sq.y == m.end.y)
+ ) {
+ return false;
+ }
+ return true;
+ });
}
- // subTurn == 2: only the piece pushed by the sentry is allowed to move,
- // as if the sentry didn't exist
- if (x != this.sentryPos.x && y != this.sentryPos.y) return [];
- const moves2 = this.getPotentialMovesFrom_aux([x, y]);
- // Don't forget to re-add the sentry on the board:
- const oppCol = V.GetOppCol(this.turn);
- return moves2.map(m => {
- m.appear.push({x: x, y: y, p: V.SENTRY, c: oppCol});
- return m;
- });
+ else if (this.subTurn == 2) {
+ // Don't forget to re-add the sentry on the board:
+ // Also fix color of pushed piece afterward:
+ moves.forEach(m => {
+ m.appear.push({x: x, y: y, p: V.SENTRY, c: color});
+ m.appear[0].c = oppCol;
+ m.vanish[0].c = oppCol;
+ });
+ }
+ return moves;
}
getPotentialPawnMoves([x, y]) {
- const color = this.turn;
+ const color = this.getColor(x, y);
let moves = [];
const [sizeX, sizeY] = [V.size.x, V.size.y];
- let shiftX = color == "w" ? -1 : 1;
- // Special case of a sentry push: pawn goes in the capturer direction
- if (this.subTurn == 2) shiftX *= -1;
+ const shiftX = color == "w" ? -1 : 1;
const startRank = color == "w" ? sizeX - 2 : 1;
const lastRank = color == "w" ? 0 : sizeX - 1;
const finalPieces =
- x + shiftX == lastRank
+ // No promotions after pushes!
+ x + shiftX == lastRank && this.subTurn == 1
?
- [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN, V.SENTRY, V.JAILER]
- .concat(Object.keys(V.LANCER_DIRS))
+ Object.keys(V.LANCER_DIRS).concat(
+ [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN, V.SENTRY, V.JAILER])
: [V.PAWN];
if (this.board[x + shiftX][y] == V.EMPTY) {
// One square forward
}
}
- // En passant: no subTurn consideration here (always == 1)
+ // En passant:
const Lep = this.epSquares.length;
const epSquare = this.epSquares[Lep - 1]; //always at least one element
if (
// Obtain all lancer moves in "step" direction,
// without final re-orientation.
- getPotentialLancerMoves_aux([x, y], step) {
+ getPotentialLancerMoves_aux([x, y], step, tr) {
let moves = [];
// Add all moves to vacant squares until opponent is met:
- const oppCol = V.GetOppCol(this.turn);
+ const oppCol = V.GetOppCol(this.getColor(x, y));
let sq = [x + step[0], y + step[1]];
while (V.OnBoard(sq[0], sq[1]) && this.getColor(sq[0], sq[1]) != oppCol) {
if (this.board[sq[0]][sq[1]] == V.EMPTY)
- moves.push(this.getBasicMove([x, y], sq));
+ moves.push(this.getBasicMove([x, y], sq, tr));
sq[0] += step[0];
sq[1] += step[1];
}
if (V.OnBoard(sq[0], sq[1]))
// Add capturing move
- moves.push(this.getBasicMove([x, y], sq));
+ moves.push(this.getBasicMove([x, y], sq, tr));
return moves;
}
) {
// I was pushed: allow all directions (for this move only), but
// do not change direction after moving.
+ const color = this.getColor(x, y);
Object.values(V.LANCER_DIRS).forEach(step => {
+ const dirCode = Object.keys(V.LANCER_DIRS).find(k => {
+ return (V.LANCER_DIRS[k][0] == step[0] && V.LANCER_DIRS[k][1] == step[1]);
+ });
Array.prototype.push.apply(
moves,
- this.getPotentialLancerMoves_aux([x, y], step)
+ this.getPotentialLancerMoves_aux([x, y], step, { p: dirCode, c: color })
);
});
return moves;
const dirCode = this.board[x][y][1];
const monodirMoves =
this.getPotentialLancerMoves_aux([x, y], V.LANCER_DIRS[dirCode]);
- // Add all possible orientations aftermove:
- monodirMoves.forEach(m => {
- Object.keys(V.LANCER_DIRS).forEach(k => {
- let mk = JSON.parse(JSON.stringify(m));
- mk.appear[0].p = k;
- moves.push(mk);
+ // Add all possible orientations aftermove except if I'm being pushed
+ if (this.subTurn == 1) {
+ monodirMoves.forEach(m => {
+ Object.keys(V.LANCER_DIRS).forEach(k => {
+ let mk = JSON.parse(JSON.stringify(m));
+ mk.appear[0].p = k;
+ moves.push(mk);
+ });
});
- });
- return moves;
+ return moves;
+ } else return monodirMoves;
}
getPotentialSentryMoves([x, y]) {
let moves = super.getPotentialBishopMoves([x, y]);
// ...but captures are replaced by special move, if and only if
// "captured" piece can move now, considered as the capturer unit.
+ // --> except is subTurn == 2, in this case I don't push anything.
+ if (this.subTurn == 2) return moves.filter(m => m.vanish.length == 1);
moves.forEach(m => {
if (m.vanish.length == 2) {
// Temporarily cancel the sentry capture:
m.vanish.pop();
}
});
- // Can the pushed unit make any move?
- this.subTurn = 2;
+ // Can the pushed unit make any move? ...resulting in a non-self-check?
+ const color = this.getColor(x, y);
const fMoves = moves.filter(m => {
- V.PlayOnBoard(this.board, m);
- let res =
- (this.filterValid(this.getPotentialMovesFrom([x, y])).length > 0);
- V.UndoOnBoard(this.board, m);
- return res;
+ // Sentry push?
+ if (m.appear.length == 0) {
+ let res = false;
+ this.play(m);
+ let moves2 = this.filterValid(
+ this.getPotentialMovesFrom([m.end.x, m.end.y]));
+ for (let m2 of moves2) {
+ this.play(m2);
+ res = !this.underCheck(color);
+ this.undo(m2);
+ if (res) break;
+ }
+ this.undo(m);
+ return res;
+ }
+ return true;
});
- this.subTurn = 1;
return fMoves;
}
}
filterValid(moves) {
- // Disable check tests when subTurn == 2, because the move isn't finished
- if (this.subTurn == 2) return moves;
- const filteredMoves = super.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;
- // Else, forbid check and captures:
+ 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;
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;
+ const firstRank = (c == "w" ? V.size.x - 1 : 0);
// Update king position + flags
if (piece == V.KING) {
this.castleFlags[oppCol][flagIdx] = false;
}
- if (this.subTurn == 2) {
+ 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].insludes(move.vanish[0].p))
+ if ([V.KNIGHT,V.KING].includes(move.vanish[0].p))
// short-range pieces: just forbid initial square
squares.push(move.start);
else {
deltaY / Math.abs(deltaY) || 0
];
for (
- let sq = {x: x, y: y};
+ 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]
) {
move.flags = JSON.stringify(this.aggregateFlags());
this.epSquares.push(this.getEpSquare(move));
V.PlayOnBoard(this.board, move);
- if (this.subTurn == 1) this.movesCount++;
this.updateVariables(move);
- if (move.appear.length == 0 && move.vanish.length == 1) {
- // The sentry is about to push a piece:
- this.sentryPos = { x: move.end.x, y: move.end.y };
- this.subTurn = 2;
- } else {
+ // 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;
- const L = this.sentryPush.length;
- // Is it a sentry push? (useful for undo)
- move.sentryPush = !!this.sentryPush[L-1];
}
}
const L = this.sentryPush.length;
// Decrement movesCount except if the move is a sentry push
if (!move.sentryPush) this.movesCount--;
- this.unupdateVariables(move);
// 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
+
static get VALUES() {
return Object.assign(
{ l: 4.8, s: 2.8, j: 3.8 }, //Jeff K. estimations
);
}
+ getComputerMove() {
+ const maxeval = V.INFINITY;
+ const color = this.turn;
+ let moves1 = this.getAllValidMoves();
+
+ if (moves1.length == 0)
+ // TODO: this situation should not happen
+ return null;
+
+ const setEval = (move) => {
+ const score = this.getCurrentScore();
+ if (score != "*") {
+ move.eval =
+ score == "1/2"
+ ? 0
+ : (score == "1-0" ? 1 : -1) * maxeval;
+ } else move[i].eval = this.evalPosition();
+ };
+
+ // Just search_depth == 1 (because of sentries. TODO: can do better...)
+ moves1.forEach(m1 => {
+ this.play(m1);
+ if (this.subTurn == 1) setEval(m1);
+ else {
+ // Need to play every pushes and count:
+ const moves2 = this.getAllValidMoves();
+ moves2.forEach(m2 => {
+ this.play(m2);
+ setEval(m1);
+ this.undo(m2);
+ });
+ }
+ this.undo(m1);
+ });
+
+ moves1.sort((a, b) => {
+ return (color == "w" ? 1 : -1) * (b.eval - a.eval);
+ });
+ let candidates = [0];
+ for (let j = 1; j < moves1.length && moves1[j].eval == moves1[0].eval; j++)
+ candidates.push(j);
+ return moves1[candidates[randInt(candidates.length)]];
+ }
+
getNotation(move) {
// Special case "king takes jailer" is a pass move
if (move.appear.length == 0 && move.vanish.length == 0) return "pass";