const dir = this.getNormalizedDirection(
[fm.start.x - x, fm.start.y - y]);
const nbSteps =
- [V.PAWN, V.KING, V.KNIGHT].includes(piece)
+ ['p', 'k', 'n'].includes(piece)
? 1
: null;
return this.getMovesInDirection([x, y], dir, nbSteps);
const deltaX = Math.abs(fm.start.x - x);
const deltaY = Math.abs(fm.start.y - y);
switch (piece) {
- case V.PAWN:
+ case 'p':
if (x == pawnStartRank) {
if (
(fm.start.x - x) * pawnShift < 0 ||
}
}
break;
- case V.KNIGHT:
+ case 'n':
if (
(deltaX + deltaY != 3 || (deltaX == 0 && deltaY == 0)) ||
(fm.end.x - fm.start.x != fm.start.x - x) ||
return [];
}
break;
- case V.KING:
+ case 'k':
if (
(deltaX >= 2 || deltaY >= 2) ||
(fm.end.x - fm.start.x != fm.start.x - x) ||
return [];
}
break;
- case V.BISHOP:
+ case 'b':
if (deltaX != deltaY)
return [];
break;
- case V.ROOK:
+ case 'r':
if (deltaX != 0 && deltaY != 0)
return [];
break;
- case V.QUEEN:
+ case 'q':
if (deltaX != deltaY && deltaX != 0 && deltaY != 0)
return [];
break;
// Note: kings cannot suicide, so fm.vanish[0].p is not KING.
// Could be PAWN though, if a pawn was pushed out of board.
if (
- fm.vanish[0].p != V.PAWN && //pawns cannot pull
+ fm.vanish[0].p != 'p' && //pawns cannot pull
this.isAprioriValidExit(
[x, y],
[fm.start.x, fm.start.y],
// Seems so:
const dir = this.getNormalizedDirection(
[fm.start.x - x, fm.start.y - y]);
- const nbSteps = (fm.vanish[0].p == V.KNIGHT ? 1 : null);
+ const nbSteps = (fm.vanish[0].p == 'n' ? 1 : null);
return this.getMovesInDirection([x, y], dir, nbSteps);
}
return [];
};
const getPullMoves = () => {
- if (fm.vanish[0].p == V.PAWN)
+ if (fm.vanish[0].p == 'p')
// pawns cannot pull
return [];
const dirM = this.getNormalizedDirection(
const deltaX = Math.abs(x - fm.start.x);
const deltaY = Math.abs(y - fm.start.y);
if (
- (fm.vanish[0].p == V.KING && (deltaX > 1 || deltaY > 1)) ||
- (fm.vanish[0].p == V.KNIGHT &&
+ (fm.vanish[0].p == 'k' && (deltaX > 1 || deltaY > 1)) ||
+ (fm.vanish[0].p == 'n' &&
(deltaX + deltaY != 3 || deltaX == 0 || deltaY == 0))
) {
return [];
let [i, j] = [x + dir[0], y + dir[1]];
while (
(i != fm.start.x || j != fm.start.y) &&
- this.board[i][j] == V.EMPTY
+ this.board[i][j] == ""
) {
i += dir[0];
j += dir[1];
outerLoop: for (let step of steps) {
let i = x + step[0];
let j = y + step[1];
- while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
+ while (this.onBoard(i, j) && this.board[i][j] == "") {
moves.push(this.getBasicMove([x, y], [i, j]));
if (oneStep)
continue outerLoop;
i += step[0];
j += step[1];
}
- if (V.OnBoard(i, j)) {
+ if (this.onBoard(i, j)) {
if (this.canTake([x, y], [i, j]))
moves.push(this.getBasicMove([x, y], [i, j]));
}
else {
// Add potential board exit (suicide), except for the king
- if (piece != V.KING) {
+ if (piece != 'k') {
moves.push({
start: { x: x, y: y},
end: { x: this.kingPos[c][0], y: this.kingPos[c][1] },
}
}
- // TODO ::
+// TODO: re-write just for here getAllPotentialMoves() ?
+
filterValid(moves) {
const color = this.turn;
const La = this.amoves.length;
// A move is valid either if it doesn't result in a check,
// or if a second move is possible to counter the check
// (not undoing a potential move + action of the opponent)
- this.play(m);
+ this.playOnBoard(m);
let res = this.underCheck(color);
if (this.subTurn == 2) {
let isOpposite = La > 0 && this.oppositeMoves(this.amoves[La-1], m);
const moves2 = this.getAllPotentialMoves();
for (let m2 of moves2) {
this.play(m2);
- const res2 = this.underCheck(color);
+ const res2 = this.underCheck(color); //TODO: + square
const amove = this.getAmove(m, m2);
isOpposite =
La > 0 && this.oppositeMoves(this.amoves[La-1], amove);
}
}
}
- this.undo(m);
+ this.undoOnBoard(m);
return !res;
});
}
--- /dev/null
+import { randInt, sample } from "@/utils/alea";
+import { ChessRules, PiPo, Move } from "@/base_rules";
+
+export class EightpiecesRules extends ChessRules {
+
+ static get JAILER() {
+ return "j";
+ }
+ static get SENTRY() {
+ return "s";
+ }
+ static get LANCER() {
+ return "l";
+ }
+
+ static get IMAGE_EXTENSION() {
+ // Temporarily, for the time SVG pieces are being designed:
+ return ".png";
+ }
+
+ // Lancer directions *from white perspective*
+ static get LANCER_DIRS() {
+ return {
+ 'c': [-1, 0], //north
+ 'd': [-1, 1], //N-E
+ 'e': [0, 1], //east
+ 'f': [1, 1], //S-E
+ 'g': [1, 0], //south
+ 'h': [1, -1], //S-W
+ 'm': [0, -1], //west
+ 'o': [-1, -1] //N-W
+ };
+ }
+
+ static get PIECES() {
+ return ChessRules.PIECES
+ .concat([V.JAILER, V.SENTRY])
+ .concat(Object.keys(V.LANCER_DIRS));
+ }
+
+ getPiece(i, j) {
+ const piece = this.board[i][j].charAt(1);
+ // Special lancer case: 8 possible orientations
+ if (Object.keys(V.LANCER_DIRS).includes(piece)) return V.LANCER;
+ return piece;
+ }
+
+ getPpath(b, color, score, orientation) {
+ if ([V.JAILER, V.SENTRY].includes(b[1])) return "Eightpieces/tmp_png/" + b;
+ if (Object.keys(V.LANCER_DIRS).includes(b[1])) {
+ if (orientation == 'w') return "Eightpieces/tmp_png/" + b;
+ // Find opposite direction for adequate display:
+ let oppDir = '';
+ switch (b[1]) {
+ case 'c':
+ oppDir = 'g';
+ break;
+ case 'g':
+ oppDir = 'c';
+ break;
+ case 'd':
+ oppDir = 'h';
+ break;
+ case 'h':
+ oppDir = 'd';
+ break;
+ case 'e':
+ oppDir = 'm';
+ break;
+ case 'm':
+ oppDir = 'e';
+ break;
+ case 'f':
+ oppDir = 'o';
+ break;
+ case 'o':
+ oppDir = 'f';
+ break;
+ }
+ return "Eightpieces/tmp_png/" + b[0] + oppDir;
+ }
+ // TODO: after we have SVG pieces, remove the folder and next prefix:
+ return "Eightpieces/tmp_png/" + b;
+ }
+
+ getPPpath(m, orientation) {
+ return (
+ this.getPpath(
+ m.appear[0].c + m.appear[0].p,
+ null,
+ null,
+ orientation
+ )
+ );
+ }
+
+ static ParseFen(fen) {
+ const fenParts = fen.split(" ");
+ return Object.assign(
+ ChessRules.ParseFen(fen),
+ { sentrypush: fenParts[5] }
+ );
+ }
+
+ static IsGoodFen(fen) {
+ if (!ChessRules.IsGoodFen(fen)) return false;
+ const fenParsed = V.ParseFen(fen);
+ // 5) Check sentry push (if any)
+ if (
+ fenParsed.sentrypush != "-" &&
+ !fenParsed.sentrypush.match(/^([a-h][1-8]){2,2}$/)
+ ) {
+ return false;
+ }
+ return true;
+ }
+
+ getFen() {
+ return super.getFen() + " " + this.getSentrypushFen();
+ }
+
+ getFenForRepeat() {
+ return super.getFenForRepeat() + "_" + this.getSentrypushFen();
+ }
+
+ getSentrypushFen() {
+ const L = this.sentryPush.length;
+ if (!this.sentryPush[L-1]) return "-";
+ let res = "";
+ const spL = this.sentryPush[L-1].length;
+ // Condensate path: just need initial and final squares:
+ return [0, spL - 1]
+ .map(i => V.CoordsToSquare(this.sentryPush[L-1][i]))
+ .join("");
+ }
+
+ setOtherVariables(fen) {
+ super.setOtherVariables(fen);
+ // subTurn == 2 only when a sentry moved, and is about to push something
+ this.subTurn = 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];
+ else {
+ // Expand init + dest squares into a full path:
+ const init = V.SquareToCoords(parsedFen.sentrypush.substr(0, 2)),
+ dest = V.SquareToCoords(parsedFen.sentrypush.substr(2));
+ let newPath = [init];
+ const delta = ['x', 'y'].map(i => Math.abs(dest[i] - init[i]));
+ // Check that it's not a knight movement:
+ if (delta[0] == 0 || delta[1] == 0 || delta[0] == delta[1]) {
+ const step = ['x', 'y'].map((i, idx) => {
+ return (dest[i] - init[i]) / delta[idx] || 0
+ });
+ let x = init.x + step[0],
+ y = init.y + step[1];
+ while (x != dest.x || y != dest.y) {
+ newPath.push({ x: x, y: y });
+ x += step[0];
+ y += step[1];
+ }
+ }
+ newPath.push(dest);
+ this.sentryPush = [newPath];
+ }
+ }
+
+ static GenRandInitFen(options) {
+ if (options.randomness == 0)
+ return "jfsqkbnr/pppppppp/8/8/8/8/PPPPPPPP/JDSQKBNR w 0 ahah - -";
+
+ const baseFen = ChessRules.GenRandInitFen(options);
+ const fenParts = baseFen.split(' ');
+ const posParts = fenParts[0].split('/');
+
+ // Replace one bishop by sentry, so that sentries on different colors
+ // Also replace one random rook by jailer,
+ // and one random knight by lancer (facing north/south)
+ let pieceLine = { b: posParts[0], w: posParts[7].toLowerCase() };
+ let posBlack = { r: -1, n: -1, b: -1 };
+ const mapP = { r: 'j', n: 'l', b: 's' };
+ ['b', 'w'].forEach(c => {
+ ['r', 'n', 'b'].forEach(p => {
+ let pl = pieceLine[c];
+ let pos = -1;
+ if (options.randomness == 2 || c == 'b')
+ pos = (randInt(2) == 0 ? pl.indexOf(p) : pl.lastIndexOf(p));
+ else pos = posBlack[p];
+ pieceLine[c] =
+ pieceLine[c].substr(0, pos) + mapP[p] + pieceLine[c].substr(pos+1);
+ if (options.randomness == 1 && c == 'b') posBlack[p] = pos;
+ });
+ });
+ // Rename 'l' into 'g' (black) or 'c' (white)
+ pieceLine['w'] = pieceLine['w'].replace('l', 'c');
+ pieceLine['b'] = pieceLine['b'].replace('l', 'g');
+ if (options.randomness == 2) {
+ const ws = pieceLine['w'].indexOf('s');
+ const bs = pieceLine['b'].indexOf('s');
+ if (ws % 2 != bs % 2) {
+ // Fix sentry: should be on different colors.
+ // => move sentry on other bishop for random color
+ const c = sample(['w', 'b'], 1);
+ pieceLine[c] = pieceLine[c]
+ .replace('b', 't') //tmp
+ .replace('s', 'b')
+ .replace('t', 's');
+ }
+ }
+
+ return (
+ pieceLine['b'] + "/" +
+ posParts.slice(1, 7).join('/') + "/" +
+ pieceLine['w'].toUpperCase() + " " +
+ fenParts.slice(1, 5).join(' ') + " -"
+ );
+ }
+
+ canTake([x1, y1], [x2, y2]) {
+ if (this.subTurn == 2)
+ // Only self captures on this subturn:
+ return this.getColor(x1, y1) == this.getColor(x2, y2);
+ return super.canTake([x1, y1], [x2, y2]);
+ }
+
+ // Is piece on square (x,y) immobilized?
+ isImmobilized([x, y]) {
+ const color = this.getColor(x, y);
+ const oppCol = V.GetOppCol(color);
+ for (let step of V.steps[V.ROOK]) {
+ const [i, j] = [x + step[0], y + step[1]];
+ if (
+ V.OnBoard(i, j) &&
+ this.board[i][j] != V.EMPTY &&
+ this.getColor(i, j) == oppCol
+ ) {
+ if (this.getPiece(i, j) == V.JAILER) return [i, j];
+ }
+ }
+ return null;
+ }
+
+ 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]) {
+ const piece = this.getPiece(x, y);
+ const L = this.sentryPush.length;
+ // At subTurn == 2, jailers aren't effective (Jeff K)
+ if (this.subTurn == 1) {
+ const jsq = this.isImmobilized([x, y]);
+ if (!!jsq) {
+ let moves = [];
+ // Special pass move if king:
+ if (piece == V.KING) {
+ moves.push(
+ new Move({
+ appear: [],
+ vanish: [],
+ start: { x: x, y: y },
+ end: { x: jsq[0], y: jsq[1] }
+ })
+ );
+ }
+ else if (piece == V.LANCER && !!this.sentryPush[L-1]) {
+ // A pushed lancer next to the jailer: reorient
+ const color = this.getColor(x, y);
+ const curDir = this.board[x][y].charAt(1);
+ Object.keys(V.LANCER_DIRS).forEach(k => {
+ moves.push(
+ new Move({
+ appear: [{ x: x, y: y, c: color, p: k }],
+ vanish: [{ x: x, y: y, c: color, p: curDir }],
+ start: { x: x, y: y },
+ end: { x: jsq[0], y: jsq[1] }
+ })
+ );
+ });
+ }
+ return moves;
+ }
+ }
+ let moves = [];
+ switch (piece) {
+ case V.JAILER:
+ moves = this.getPotentialJailerMoves([x, y]);
+ break;
+ case V.SENTRY:
+ moves = this.getPotentialSentryMoves([x, y]);
+ break;
+ case V.LANCER:
+ moves = this.getPotentialLancerMoves([x, y]);
+ break;
+ default:
+ moves = super.getPotentialMovesFrom([x, y]);
+ break;
+ }
+ if (!!this.sentryPush[L-1]) {
+ // Delete moves walking back on sentry push path,
+ // only if not a pawn, and the piece is the pushed one.
+ const pl = this.sentryPush[L-1].length;
+ const finalPushedSq = this.sentryPush[L-1][pl-1];
+ moves = moves.filter(m => {
+ if (
+ m.vanish[0].p != V.PAWN &&
+ m.start.x == finalPushedSq.x && m.start.y == finalPushedSq.y &&
+ this.sentryPush[L-1].some(sq => sq.x == m.end.x && sq.y == m.end.y)
+ ) {
+ return false;
+ }
+ return true;
+ });
+ }
+ else if (this.subTurn == 2) {
+ // Put back the sentinel on board:
+ const color = this.turn;
+ moves.forEach(m => {
+ m.appear.push({x: x, y: y, p: V.SENTRY, c: color});
+ });
+ }
+ return moves;
+ }
+
+ getPotentialPawnMoves([x, y]) {
+ const color = this.getColor(x, y);
+ let moves = [];
+ const [sizeX, sizeY] = [V.size.x, V.size.y];
+ let shiftX = (color == "w" ? -1 : 1);
+ if (this.subTurn == 2) shiftX *= -1;
+ const firstRank = color == "w" ? sizeX - 1 : 0;
+ const startRank = color == "w" ? sizeX - 2 : 1;
+ const lastRank = color == "w" ? 0 : sizeX - 1;
+
+ // Pawns might be pushed on 1st rank and attempt to move again:
+ if (!V.OnBoard(x + shiftX, y)) return [];
+
+ // A push cannot put a pawn on last rank (it goes backward)
+ let finalPieces = [V.PAWN];
+ if (x + shiftX == lastRank) {
+ // Only allow direction facing inside board:
+ const allowedLancerDirs =
+ lastRank == 0
+ ? ['e', 'f', 'g', 'h', 'm']
+ : ['c', 'd', 'e', 'm', 'o'];
+ finalPieces =
+ allowedLancerDirs
+ .concat([V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN, V.SENTRY, V.JAILER]);
+ }
+ if (this.board[x + shiftX][y] == V.EMPTY) {
+ // One square forward
+ for (let piece of finalPieces) {
+ moves.push(
+ this.getBasicMove([x, y], [x + shiftX, y], {
+ c: color,
+ p: piece
+ })
+ );
+ }
+ if (
+ // 2-squares jumps forbidden if pawn push
+ this.subTurn == 1 &&
+ [startRank, firstRank].includes(x) &&
+ this.board[x + 2 * shiftX][y] == V.EMPTY
+ ) {
+ // Two squares jump
+ moves.push(this.getBasicMove([x, y], [x + 2 * shiftX, y]));
+ }
+ }
+ // Captures
+ for (let shiftY of [-1, 1]) {
+ if (
+ y + shiftY >= 0 &&
+ y + shiftY < sizeY &&
+ this.board[x + shiftX][y + shiftY] != V.EMPTY &&
+ this.canTake([x, y], [x + shiftX, y + shiftY])
+ ) {
+ for (let piece of finalPieces) {
+ moves.push(
+ this.getBasicMove([x, y], [x + shiftX, y + shiftY], {
+ c: color,
+ p: piece
+ })
+ );
+ }
+ }
+ }
+
+ // En passant: only on subTurn == 1
+ const Lep = this.epSquares.length;
+ const epSquare = this.epSquares[Lep - 1];
+ if (
+ this.subTurn == 1 &&
+ !!epSquare &&
+ epSquare.x == x + shiftX &&
+ Math.abs(epSquare.y - y) == 1
+ ) {
+ let enpassantMove = this.getBasicMove([x, y], [epSquare.x, epSquare.y]);
+ enpassantMove.vanish.push({
+ x: x,
+ y: epSquare.y,
+ p: "p",
+ c: this.getColor(x, epSquare.y)
+ });
+ moves.push(enpassantMove);
+ }
+
+ return moves;
+ }
+
+ doClick(square) {
+ if (isNaN(square[0])) return null;
+ const L = this.sentryPush.length;
+ const [x, y] = [square[0], square[1]];
+ const color = this.turn;
+ if (
+ this.subTurn == 2 ||
+ this.board[x][y] == V.EMPTY ||
+ this.getPiece(x, y) != V.LANCER ||
+ this.getColor(x, y) != color ||
+ !!this.sentryPush[L-1]
+ ) {
+ return null;
+ }
+ // Stuck lancer?
+ const orientation = this.board[x][y][1];
+ const step = V.LANCER_DIRS[orientation];
+ if (!V.OnBoard(x + step[0], y + step[1])) {
+ let choices = [];
+ Object.keys(V.LANCER_DIRS).forEach(k => {
+ const dir = V.LANCER_DIRS[k];
+ if (
+ (dir[0] != step[0] || dir[1] != step[1]) &&
+ V.OnBoard(x + dir[0], y + dir[1])
+ ) {
+ choices.push(
+ new Move({
+ vanish: [
+ new PiPo({
+ x: x,
+ y: y,
+ c: color,
+ p: orientation
+ })
+ ],
+ appear: [
+ new PiPo({
+ x: x,
+ y: y,
+ c: color,
+ p: k
+ })
+ ],
+ start: { x: x, y : y },
+ end: { x: -1, y: -1 }
+ })
+ );
+ }
+ });
+ return choices;
+ }
+ return null;
+ }
+
+ // Obtain all lancer moves in "step" direction
+ getPotentialLancerMoves_aux([x, y], step, tr) {
+ let moves = [];
+ // Add all moves to vacant squares until opponent is met:
+ const color = this.getColor(x, y);
+ const oppCol =
+ this.subTurn == 1
+ ? V.GetOppCol(color)
+ // at subTurn == 2, consider own pieces as opponent
+ : color;
+ 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, 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, tr));
+ return moves;
+ }
+
+ getPotentialLancerMoves([x, y]) {
+ let moves = [];
+ // Add all lancer possible orientations, similar to pawn promotions.
+ // Except if just after a push: allow all movements from init square then
+ const L = this.sentryPush.length;
+ const color = this.getColor(x, y);
+ const dirCode = this.board[x][y][1];
+ const curDir = V.LANCER_DIRS[dirCode];
+ if (!!this.sentryPush[L-1]) {
+ // Maybe I was pushed
+ const pl = this.sentryPush[L-1].length;
+ if (
+ this.sentryPush[L-1][pl-1].x == x &&
+ this.sentryPush[L-1][pl-1].y == y
+ ) {
+ // I was pushed: allow all directions (for this move only), but
+ // do not change direction after moving, *except* if I keep the
+ // same orientation in which I was pushed.
+ // Also allow simple reorientation ("capturing king"):
+ if (!V.OnBoard(x + curDir[0], y + curDir[1])) {
+ const kp = this.kingPos[color];
+ let reorientMoves = [];
+ Object.keys(V.LANCER_DIRS).forEach(k => {
+ const dir = V.LANCER_DIRS[k];
+ if (
+ (dir[0] != curDir[0] || dir[1] != curDir[1]) &&
+ V.OnBoard(x + dir[0], y + dir[1])
+ ) {
+ reorientMoves.push(
+ new Move({
+ vanish: [
+ new PiPo({
+ x: x,
+ y: y,
+ c: color,
+ p: dirCode
+ })
+ ],
+ appear: [
+ new PiPo({
+ x: x,
+ y: y,
+ c: color,
+ p: k
+ })
+ ],
+ start: { x: x, y : y },
+ end: { x: kp[0], y: kp[1] }
+ })
+ );
+ }
+ });
+ Array.prototype.push.apply(moves, reorientMoves);
+ }
+ 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]
+ );
+ });
+ const dirMoves =
+ this.getPotentialLancerMoves_aux(
+ [x, y],
+ step,
+ { p: dirCode, c: color }
+ );
+ if (curDir[0] == step[0] && curDir[1] == step[1]) {
+ // Keeping same orientation: can choose after
+ let chooseMoves = [];
+ dirMoves.forEach(m => {
+ Object.keys(V.LANCER_DIRS).forEach(k => {
+ const newDir = V.LANCER_DIRS[k];
+ // Prevent orientations toward outer board:
+ if (V.OnBoard(m.end.x + newDir[0], m.end.y + newDir[1])) {
+ let mk = JSON.parse(JSON.stringify(m));
+ mk.appear[0].p = k;
+ chooseMoves.push(mk);
+ }
+ });
+ });
+ Array.prototype.push.apply(moves, chooseMoves);
+ }
+ else Array.prototype.push.apply(moves, dirMoves);
+ });
+ return moves;
+ }
+ }
+ // I wasn't pushed: standard lancer move
+ const monodirMoves =
+ this.getPotentialLancerMoves_aux([x, y], V.LANCER_DIRS[dirCode]);
+ // 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 => {
+ const newDir = V.LANCER_DIRS[k];
+ // Prevent orientations toward outer board:
+ if (V.OnBoard(m.end.x + newDir[0], m.end.y + newDir[1])) {
+ let mk = JSON.parse(JSON.stringify(m));
+ mk.appear[0].p = k;
+ moves.push(mk);
+ }
+ });
+ });
+ return moves;
+ }
+ else {
+ // I'm pushed: add potential nudges, except for current orientation
+ let potentialNudges = [];
+ for (let step of V.steps[V.ROOK].concat(V.steps[V.BISHOP])) {
+ if (
+ (step[0] != curDir[0] || step[1] != curDir[1]) &&
+ V.OnBoard(x + step[0], y + step[1]) &&
+ this.board[x + step[0]][y + step[1]] == V.EMPTY
+ ) {
+ const newDirCode = Object.keys(V.LANCER_DIRS).find(k => {
+ const codeStep = V.LANCER_DIRS[k];
+ return (codeStep[0] == step[0] && codeStep[1] == step[1]);
+ });
+ potentialNudges.push(
+ this.getBasicMove(
+ [x, y],
+ [x + step[0], y + step[1]],
+ { c: color, p: newDirCode }
+ )
+ );
+ }
+ }
+ return monodirMoves.concat(potentialNudges);
+ }
+ }
+
+ getPotentialSentryMoves([x, y]) {
+ // The sentry moves a priori like a bishop:
+ 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.appear.pop();
+ m.vanish.pop();
+ }
+ });
+ const color = this.getColor(x, y);
+ const fMoves = moves.filter(m => {
+ // Can the pushed unit make any move? ...resulting in a non-self-check?
+ if (m.appear.length == 0) {
+ let res = false;
+ this.play(m);
+ let moves2 = 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;
+ });
+ return fMoves;
+ }
+
+ getPotentialJailerMoves([x, y]) {
+ return super.getPotentialRookMoves([x, y]).filter(m => {
+ // Remove jailer captures
+ return m.vanish[0].p != V.JAILER || m.vanish.length == 1;
+ });
+ }
+
+ getPotentialKingMoves(sq) {
+ const moves = this.getSlideNJumpMoves(
+ sq, V.steps[V.ROOK].concat(V.steps[V.BISHOP]), 1);
+ return (
+ this.subTurn == 1
+ ? moves.concat(this.getCastleMoves(sq))
+ : moves
+ );
+ }
+
+ atLeastOneMove() {
+ // If in second-half of a move, we already know that a move is possible
+ if (this.subTurn == 2) return true;
+ return super.atLeastOneMove();
+ }
+
+ filterValid(moves) {
+ if (moves.length == 0) return [];
+ const basicFilter = (m, c) => {
+ this.play(m);
+ const res = !this.underCheck(c);
+ this.undo(m);
+ return res;
+ };
+ // Disable check tests for sentry pushes,
+ // because in this case the move isn't finished
+ let movesWithoutSentryPushes = [];
+ let movesWithSentryPushes = [];
+ moves.forEach(m => {
+ // Second condition below for special king "pass" moves
+ if (m.appear.length > 0 || m.vanish.length == 0)
+ movesWithoutSentryPushes.push(m);
+ else movesWithSentryPushes.push(m);
+ });
+ const color = this.turn;
+ const oppCol = V.GetOppCol(color);
+ const filteredMoves =
+ movesWithoutSentryPushes.filter(m => basicFilter(m, color));
+ // If at least one full move made, everything is allowed.
+ // Else: forbid checks and captures.
+ return (
+ this.movesCount >= 2
+ ? filteredMoves
+ : filteredMoves.filter(m => {
+ return (m.vanish.length <= 1 && basicFilter(m, oppCol));
+ })
+ ).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));
+ }
+
+ isAttacked(sq, color) {
+ return (
+ super.isAttacked(sq, color) ||
+ this.isAttackedByLancer(sq, color) ||
+ this.isAttackedBySentry(sq, color)
+ // The jailer doesn't capture.
+ );
+ }
+
+ isAttackedBySlideNJump([x, y], color, piece, steps, oneStep) {
+ for (let step of steps) {
+ let rx = x + step[0],
+ ry = y + step[1];
+ while (V.OnBoard(rx, ry) && this.board[rx][ry] == V.EMPTY && !oneStep) {
+ rx += step[0];
+ ry += step[1];
+ }
+ if (
+ V.OnBoard(rx, ry) &&
+ this.getPiece(rx, ry) == piece &&
+ this.getColor(rx, ry) == color &&
+ !this.isImmobilized([rx, ry])
+ ) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ isAttackedByPawn([x, y], color) {
+ const pawnShift = (color == "w" ? 1 : -1);
+ if (x + pawnShift >= 0 && x + pawnShift < V.size.x) {
+ for (let i of [-1, 1]) {
+ if (
+ y + i >= 0 &&
+ y + i < V.size.y &&
+ this.getPiece(x + pawnShift, y + i) == V.PAWN &&
+ this.getColor(x + pawnShift, y + i) == color &&
+ !this.isImmobilized([x + pawnShift, y + i])
+ ) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ isAttackedByLancer([x, y], color) {
+ 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 ||
+ this.getColor(coord.x, coord.y) == color
+ )
+ ) {
+ if (
+ this.getPiece(coord.x, coord.y) == V.LANCER &&
+ !this.isImmobilized([coord.x, coord.y])
+ ) {
+ lancerPos.push({x: coord.x, y: coord.y});
+ }
+ coord.x += step[0];
+ coord.y += step[1];
+ }
+ const L = this.sentryPush.length;
+ const pl = (!!this.sentryPush[L-1] ? this.sentryPush[L-1].length : 0);
+ 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]) ||
+ // If the lancer was just pushed, this is an attack too:
+ (
+ !!this.sentryPush[L-1] &&
+ this.sentryPush[L-1][pl-1].x == xy.x &&
+ this.sentryPush[L-1][pl-1].y == xy.y
+ )
+ ) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ // Helper to check sentries attacks:
+ selfAttack([x1, y1], [x2, y2]) {
+ const color = this.getColor(x1, y1);
+ const oppCol = V.GetOppCol(color);
+ const sliderAttack = (allowedSteps, lancer) => {
+ const deltaX = x2 - x1,
+ deltaY = y2 - y1;
+ const absDeltaX = Math.abs(deltaX),
+ absDeltaY = Math.abs(deltaY);
+ const step = [ deltaX / absDeltaX || 0, deltaY / absDeltaY || 0 ];
+ if (
+ // Check that the step is a priori valid:
+ (absDeltaX != absDeltaY && deltaX != 0 && deltaY != 0) ||
+ allowedSteps.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) {
+ // NOTE: no need to check OnBoard in this special case
+ if (this.board[sq[0]][sq[1]] != V.EMPTY) {
+ const p = this.getPiece(sq[0], sq[1]);
+ const pc = this.getColor(sq[0], sq[1]);
+ if (
+ // Enemy sentry on the way will be gone:
+ (p != V.SENTRY || pc != oppCol) &&
+ // Lancer temporarily "changed color":
+ (!lancer || pc == color)
+ ) {
+ return false;
+ }
+ }
+ sq[0] += step[0];
+ sq[1] += step[1];
+ }
+ 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], color) {
+ // 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 myColor = V.GetOppCol(color);
+ 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 &&
+ this.getColor(i,j) == color &&
+ !this.isImmobilized([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]) == myColor
+ ) {
+ candidates.push([ sq[0], sq[1] ]);
+ }
+ }
+ }
+ }
+ }
+ for (let c of candidates)
+ if (this.selfAttack(c, [x, y])) return true;
+ return false;
+ }
+
+ // Jailer doesn't capture or give check
+
+ prePlay(move) {
+ 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 };
+ if (this.subTurn == 2 && move.vanish[0].p != V.PAWN) {
+ // A piece is pushed: forbid array of squares between start and end
+ // of move, included (except if it's a pawn)
+ let squares = [];
+ if ([V.KNIGHT,V.KING].includes(move.vanish[0].p))
+ // short-range pieces: just forbid initial square
+ squares.push({ x: move.start.x, y: move.start.y });
+ 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({ x: sq.x, y: sq.y });
+ }
+ }
+ // Add end square as well, to know if I was pushed (useful for lancers)
+ squares.push({ x: move.end.x, y: move.end.y });
+ this.sentryPush.push(squares);
+ } else this.sentryPush.push(null);
+ }
+
+ play(move) {
+ this.prePlay(move);
+ move.flags = JSON.stringify(this.aggregateFlags());
+ this.epSquares.push(this.getEpSquare(move));
+ V.PlayOnBoard(this.board, 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;
+ }
+ this.postPlay(move);
+ }
+
+ postPlay(move) {
+ if (move.vanish.length == 0 || this.subTurn == 2)
+ // Special pass move of the king, or sentry pre-push: nothing to update
+ return;
+ const c = move.vanish[0].c;
+ const piece = move.vanish[0].p;
+ const firstRank = c == "w" ? V.size.x - 1 : 0;
+
+ if (piece == V.KING) {
+ this.kingPos[c][0] = move.appear[0].x;
+ this.kingPos[c][1] = move.appear[0].y;
+ this.castleFlags[c] = [V.size.y, V.size.y];
+ return;
+ }
+ // Update castling flags if rooks are moved
+ const oppCol = V.GetOppCol(c);
+ const oppFirstRank = V.size.x - 1 - firstRank;
+ if (
+ move.start.x == firstRank && //our rook moves?
+ this.castleFlags[c].includes(move.start.y)
+ ) {
+ const flagIdx = (move.start.y == this.castleFlags[c][0] ? 0 : 1);
+ this.castleFlags[c][flagIdx] = V.size.y;
+ } else if (
+ move.end.x == oppFirstRank && //we took opponent rook?
+ this.castleFlags[oppCol].includes(move.end.y)
+ ) {
+ const flagIdx = (move.end.y == this.castleFlags[oppCol][0] ? 0 : 1);
+ this.castleFlags[oppCol][flagIdx] = V.size.y;
+ }
+ }
+
+ undo(move) {
+ this.epSquares.pop();
+ this.disaggregateFlags(JSON.parse(move.flags));
+ V.UndoOnBoard(this.board, move);
+ // Decrement movesCount except if the move is a sentry push
+ if (!move.sentryPush) this.movesCount--;
+ if (this.subTurn == 2) this.subTurn = 1;
+ else {
+ this.turn = V.GetOppCol(this.turn);
+ if (move.sentryPush) this.subTurn = 2;
+ }
+ this.postUndo(move);
+ }
+
+ postUndo(move) {
+ super.postUndo(move);
+ this.sentryPush.pop();
+ }
+
+ static get VALUES() {
+ return Object.assign(
+ { l: 4.8, s: 2.8, j: 3.8 }, //Jeff K. estimations
+ ChessRules.VALUES
+ );
+ }
+
+ 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, next) => {
+ const score = this.getCurrentScore();
+ const curEval = move.eval;
+ if (score != "*") {
+ move.eval =
+ score == "1/2"
+ ? 0
+ : (score == "1-0" ? 1 : -1) * maxeval;
+ } else move.eval = this.evalPosition();
+ if (
+ // "next" is defined after sentry pushes
+ !!next && (
+ !curEval ||
+ color == 'w' && move.eval > curEval ||
+ color == 'b' && move.eval < curEval
+ )
+ ) {
+ move.second = next;
+ }
+ };
+
+ // 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, m2);
+ 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);
+ const choice = moves1[candidates[randInt(candidates.length)]];
+ return (!choice.second ? choice : [choice, choice.second]);
+ }
+
+ // For moves notation:
+ static get LANCER_DIRNAMES() {
+ return {
+ 'c': "N",
+ 'd': "NE",
+ 'e': "E",
+ 'f': "SE",
+ 'g': "S",
+ 'h': "SW",
+ 'm': "W",
+ 'o': "NW"
+ };
+ }
+
+ getNotation(move) {
+ // Special case "king takes jailer" is a pass move
+ if (move.appear.length == 0 && move.vanish.length == 0) return "pass";
+ let notation = undefined;
+ if (this.subTurn == 2) {
+ // Do not consider appear[1] (sentry) for sentry pushes
+ const simpleMove = {
+ appear: [move.appear[0]],
+ vanish: move.vanish,
+ start: move.start,
+ end: move.end
+ };
+ notation = super.getNotation(simpleMove);
+ }
+ else if (
+ move.appear.length > 0 &&
+ move.vanish[0].x == move.appear[0].x &&
+ move.vanish[0].y == move.appear[0].y
+ ) {
+ // Lancer in-place reorientation:
+ notation = "L" + V.CoordsToSquare(move.start) + ":R";
+ }
+ else notation = super.getNotation(move);
+ if (Object.keys(V.LANCER_DIRNAMES).includes(move.vanish[0].p))
+ // Lancer: add direction info
+ notation += "=" + V.LANCER_DIRNAMES[move.appear[0].p];
+ else if (
+ move.vanish[0].p == V.PAWN &&
+ Object.keys(V.LANCER_DIRNAMES).includes(move.appear[0].p)
+ ) {
+ // Fix promotions in lancer:
+ notation = notation.slice(0, -1) +
+ "L:" + V.LANCER_DIRNAMES[move.appear[0].p];
+ }
+ return notation;
+ }
+
+};
--- /dev/null
+p.boxed
+ | Three new pieces appear. All pieces are unique.
+
+p.
+ There are only one rook, one bishop and one knight per side in this variant.
+ That explains the name. The king and queen are still there,
+ and the three remaining slots are taken by new pieces:
+
+ul
+ li.
+ The lancer 'L' is oriented and can only move in the direction it points,
+ by any number of squares as long as an enemy isn't met
+ (it can jump over friendly pieces). If an opponent' piece is found,
+ it can be captured. After moving you can reorient the lancer.
+ li.
+ The sentry 'S' moves like a bishop but doesn't capture directly.
+ It "pushes" enemy pieces instead, either on an empty square or on other
+ enemy pieces which are thus (self-)captured.
+ li.
+ The jailer 'J' moves like a rook but also doesn't capture.
+ It immobilizes enemy pieces which are vertically or horizontally adjacent.
+
+p.
+ On the following diagram the white sentry can push the black lancer to
+ capture the black pawn on b4. The lancer is then immobilized
+ by the white jailer at a4.
+
+figure.diagram-container
+ .diagram.diag12
+ | fen:7k/8/8/8/Jp3m2/8/3S4/K7:
+ .diagram.diag22
+ | fen:7k/8/8/8/Jm3S2/8/8/K7:
+ figcaption Left: before white move S"push"f4. Right: after this move.
+
+p To reorient a stuck lancer,
+ul
+ li Just after being pushed: play a move which 'capture your king".
+ li Later in the game: click on the lancer.
+
+h3 Complete rules
+
+p
+ | The rules were invented by Jeff Kubach (2020), who described them much
+ | more precisely on the
+ a(href="https://www.chessvariants.com/rules/8-piece-chess")
+ | chessvariants page
+ | . While the summary given above may suffice to start playing,
+ | you should read the complete rules to fully understand this variant.
--- /dev/null
+Three new pieces appear: the lancer is a rook with a constrained direction, the sentry moves like a bishop and pushes pieces, and the jailer immobilizes pieces orthogonally adjacent.
+
+The goal is still to checkmate.
--- /dev/null
+@import url("/base_pieces.css");
--- /dev/null
+import { ChessRules, Move, PiPo } from "@/base_rules";
+import { randInt } from "@/utils/alea";
+import { ArrayFun } from "@/utils/array";
+
+export class EmergoRules extends ChessRules {
+
+ // Simple encoding: A to L = 1 to 12, from left to right, if white controls.
+ // Lowercase if black controls.
+ // Single piece (no prisoners): A@ to L@ (+ lowercase)
+
+ static get Options() {
+ return null;
+ }
+
+ static get HasFlags() {
+ return false;
+ }
+
+ static get HasEnpassant() {
+ return false;
+ }
+
+ static get DarkBottomRight() {
+ return true;
+ }
+
+ // board element == file name:
+ static board2fen(b) {
+ return b;
+ }
+ static fen2board(f) {
+ return f;
+ }
+
+ static IsGoodPosition(position) {
+ if (position.length == 0) return false;
+ const rows = position.split("/");
+ if (rows.length != V.size.x) return false;
+ for (let row of rows) {
+ let sumElts = 0;
+ for (let i = 0; i < row.length; i++) {
+ // Add only 0.5 per symbol because 2 per piece
+ if (row[i].toLowerCase().match(/^[a-lA-L@]$/)) sumElts += 0.5;
+ else {
+ const num = parseInt(row[i], 10);
+ if (isNaN(num) || num <= 0) return false;
+ sumElts += num;
+ }
+ }
+ if (sumElts != V.size.y) return false;
+ }
+ return true;
+ }
+
+ static GetBoard(position) {
+ const rows = position.split("/");
+ let board = ArrayFun.init(V.size.x, V.size.y, "");
+ for (let i = 0; i < rows.length; i++) {
+ let j = 0;
+ for (let indexInRow = 0; indexInRow < rows[i].length; indexInRow++) {
+ const character = rows[i][indexInRow];
+ const num = parseInt(character, 10);
+ // If num is a number, just shift j:
+ if (!isNaN(num)) j += num;
+ else
+ // Something at position i,j
+ board[i][j++] = V.fen2board(character + rows[i][++indexInRow]);
+ }
+ }
+ return board;
+ }
+
+ getPpath(b) {
+ return "Emergo/" + b;
+ }
+
+ getColor(x, y) {
+ if (x >= V.size.x) return x == V.size.x ? "w" : "b";
+ if (this.board[x][y].charCodeAt(0) < 97) return 'w';
+ return 'b';
+ }
+
+ getPiece() {
+ return V.PAWN; //unused
+ }
+
+ static IsGoodFen(fen) {
+ if (!ChessRules.IsGoodFen(fen)) return false;
+ const fenParsed = V.ParseFen(fen);
+ // 3) Check reserves
+ if (
+ !fenParsed.reserve ||
+ !fenParsed.reserve.match(/^([0-9]{1,2},?){2,2}$/)
+ ) {
+ return false;
+ }
+ return true;
+ }
+
+ static ParseFen(fen) {
+ const fenParts = fen.split(" ");
+ return Object.assign(
+ ChessRules.ParseFen(fen),
+ { reserve: fenParts[3] }
+ );
+ }
+
+ static get size() {
+ return { x: 9, y: 9 };
+ }
+
+ static GenRandInitFen() {
+ return "9/9/9/9/9/9/9/9/9 w 0 12,12";
+ }
+
+ getFen() {
+ return super.getFen() + " " + this.getReserveFen();
+ }
+
+ getFenForRepeat() {
+ return super.getFenForRepeat() + "_" + this.getReserveFen();
+ }
+
+ getReserveFen() {
+ return (
+ (!this.reserve["w"] ? 0 : this.reserve["w"][V.PAWN]) + "," +
+ (!this.reserve["b"] ? 0 : this.reserve["b"][V.PAWN])
+ );
+ }
+
+ getReservePpath(index, color) {
+ return "Emergo/" + (color == 'w' ? 'A' : 'a') + '@';
+ }
+
+ static get RESERVE_PIECES() {
+ return [V.PAWN]; //only array length matters
+ }
+
+ setOtherVariables(fen) {
+ const reserve =
+ V.ParseFen(fen).reserve.split(",").map(x => parseInt(x, 10));
+ this.reserve = { w: null, b: null };
+ if (reserve[0] > 0) this.reserve['w'] = { [V.PAWN]: reserve[0] };
+ if (reserve[1] > 0) this.reserve['b'] = { [V.PAWN]: reserve[1] };
+ // Local stack of captures during a turn (squares + directions)
+ this.captures = [ [] ];
+ }
+
+ atLeastOneCaptureFrom([x, y], color, forbiddenStep) {
+ for (let s of V.steps[V.BISHOP]) {
+ if (
+ !forbiddenStep ||
+ (s[0] != -forbiddenStep[0] || s[1] != -forbiddenStep[1])
+ ) {
+ const [i, j] = [x + s[0], y + s[1]];
+ if (
+ V.OnBoard(i + s[0], j + s[1]) &&
+ this.board[i][j] != V.EMPTY &&
+ this.getColor(i, j) != color &&
+ this.board[i + s[0]][j + s[1]] == V.EMPTY
+ ) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ atLeastOneCapture(color) {
+ const L0 = this.captures.length;
+ const captures = this.captures[L0 - 1];
+ const L = captures.length;
+ if (L > 0) {
+ return (
+ this.atLeastOneCaptureFrom(
+ captures[L-1].square, color, captures[L-1].step)
+ );
+ }
+ for (let i = 0; i < V.size.x; i++) {
+ for (let j=0; j< V.size.y; j++) {
+ if (
+ this.board[i][j] != V.EMPTY &&
+ this.getColor(i, j) == color &&
+ this.atLeastOneCaptureFrom([i, j], color)
+ ) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ maxLengthIndices(caps) {
+ let maxLength = 0;
+ let res = [];
+ for (let i = 0; i < caps.length; i++) {
+ if (caps[i].length > maxLength) {
+ res = [i];
+ maxLength = caps[i].length;
+ }
+ else if (caps[i].length == maxLength) res.push(i);
+ }
+ return res;
+ };
+
+ getLongestCaptures_aux([x, y], color, locSteps) {
+ let res = [];
+ const L = locSteps.length;
+ const lastStep = (L > 0 ? locSteps[L-1] : null);
+ for (let s of V.steps[V.BISHOP]) {
+ if (!!lastStep && s[0] == -lastStep[0] && s[1] == -lastStep[1]) continue;
+ const [i, j] = [x + s[0], y + s[1]];
+ if (
+ V.OnBoard(i + s[0], j + s[1]) &&
+ this.board[i + s[0]][j + s[1]] == V.EMPTY &&
+ this.board[i][j] != V.EMPTY &&
+ this.getColor(i, j) != color
+ ) {
+ const move = this.getBasicMove([x, y], [i + s[0], j + s[1]], [i, j]);
+ locSteps.push(s);
+ V.PlayOnBoard(this.board, move);
+ const nextRes =
+ this.getLongestCaptures_aux([i + s[0], j + s[1]], color, locSteps);
+ res.push(1 + nextRes);
+ locSteps.pop();
+ V.UndoOnBoard(this.board, move);
+ }
+ }
+ if (res.length == 0) return 0;
+ return Math.max(...res);
+ }
+
+ getLongestCapturesFrom([x, y], color, locSteps) {
+ let res = [];
+ const L = locSteps.length;
+ const lastStep = (L > 0 ? locSteps[L-1] : null);
+ for (let s of V.steps[V.BISHOP]) {
+ if (!!lastStep && s[0] == -lastStep[0] && s[1] == -lastStep[1]) continue;
+ const [i, j] = [x + s[0], y + s[1]];
+ if (
+ V.OnBoard(i + s[0], j + s[1]) &&
+ this.board[i + s[0]][j + s[1]] == V.EMPTY &&
+ this.board[i][j] != V.EMPTY &&
+ this.getColor(i, j) != color
+ ) {
+ const move = this.getBasicMove([x, y], [i + s[0], j + s[1]], [i, j]);
+ locSteps.push(s);
+ V.PlayOnBoard(this.board, move);
+ const stepRes =
+ this.getLongestCaptures_aux([i + s[0], j + s[1]], color, locSteps);
+ res.push({ step: s, length: 1 + stepRes });
+ locSteps.pop();
+ V.UndoOnBoard(this.board, move);
+ }
+ }
+ return this.maxLengthIndices(res).map(i => res[i]);;
+ }
+
+ getAllLongestCaptures(color) {
+ const L0 = this.captures.length;
+ const captures = this.captures[L0 - 1];
+ const L = captures.length;
+ let caps = [];
+ if (L > 0) {
+ let locSteps = [ captures[L-1].step ];
+ let res =
+ this.getLongestCapturesFrom(captures[L-1].square, color, locSteps);
+ Array.prototype.push.apply(
+ caps,
+ res.map(r => Object.assign({ square: captures[L-1].square }, r))
+ );
+ }
+ else {
+ for (let i = 0; i < V.size.x; i++) {
+ for (let j=0; j < V.size.y; j++) {
+ if (
+ this.board[i][j] != V.EMPTY &&
+ this.getColor(i, j) == color
+ ) {
+ let locSteps = [];
+ let res = this.getLongestCapturesFrom([i, j], color, locSteps);
+ Array.prototype.push.apply(
+ caps,
+ res.map(r => Object.assign({ square: [i, j] }, r))
+ );
+ }
+ }
+ }
+ }
+ return this.maxLengthIndices(caps).map(i => caps[i]);
+ }
+
+ getBasicMove([x1, y1], [x2, y2], capt) {
+ const cp1 = this.board[x1][y1];
+ if (!capt) {
+ return new Move({
+ appear: [ new PiPo({ x: x2, y: y2, c: cp1[0], p: cp1[1] }) ],
+ vanish: [ new PiPo({ x: x1, y: y1, c: cp1[0], p: cp1[1] }) ]
+ });
+ }
+ // Compute resulting types based on jumped + jumping pieces
+ const color = this.getColor(x1, y1);
+ const firstCodes = (color == 'w' ? [65, 97] : [97, 65]);
+ const cpCapt = this.board[capt[0]][capt[1]];
+ let count1 = [cp1.charCodeAt(0) - firstCodes[0], -1];
+ if (cp1[1] != '@') count1[1] = cp1.charCodeAt(1) - firstCodes[0];
+ let countC = [cpCapt.charCodeAt(0) - firstCodes[1], -1];
+ if (cpCapt[1] != '@') countC[1] = cpCapt.charCodeAt(1) - firstCodes[1];
+ count1[1]++;
+ countC[0]--;
+ let colorChange = false,
+ captVanish = false;
+ if (countC[0] < 0) {
+ if (countC[1] >= 0) {
+ colorChange = true;
+ countC = [countC[1], -1];
+ }
+ else captVanish = true;
+ }
+ const incPrisoners = String.fromCharCode(firstCodes[0] + count1[1]);
+ let mv = new Move({
+ appear: [
+ new PiPo({
+ x: x2,
+ y: y2,
+ c: cp1[0],
+ p: incPrisoners
+ })
+ ],
+ vanish: [
+ new PiPo({ x: x1, y: y1, c: cp1[0], p: cp1[1] }),
+ new PiPo({ x: capt[0], y: capt[1], c: cpCapt[0], p: cpCapt[1] })
+ ]
+ });
+ if (!captVanish) {
+ mv.appear.push(
+ new PiPo({
+ x: capt[0],
+ y: capt[1],
+ c: String.fromCharCode(
+ firstCodes[(colorChange ? 0 : 1)] + countC[0]),
+ p: (colorChange ? '@' : cpCapt[1]),
+ })
+ );
+ }
+ return mv;
+ }
+
+ getReserveMoves(x) {
+ const color = this.turn;
+ if (!this.reserve[color] || this.atLeastOneCapture(color)) return [];
+ let moves = [];
+ const shadowPiece =
+ this.reserve[V.GetOppCol(color)] == null
+ ? this.reserve[color][V.PAWN] - 1
+ : 0;
+ const appearColor = String.fromCharCode(
+ (color == 'w' ? 'A' : 'a').charCodeAt(0) + shadowPiece);
+ const addMove = ([i, j]) => {
+ moves.push(
+ new Move({
+ appear: [ new PiPo({ x: i, y: j, c: appearColor, p: '@' }) ],
+ vanish: [],
+ start: { x: V.size.x + (color == 'w' ? 0 : 1), y: 0 }
+ })
+ );
+ };
+ const oppCol = V.GetOppCol(color);
+ const opponentCanCapture = this.atLeastOneCapture(oppCol);
+ for (let i = 0; i < V.size.x; i++) {
+ for (let j = i % 2; j < V.size.y; j += 2) {
+ if (
+ this.board[i][j] == V.EMPTY &&
+ // prevent playing on central square at move 1:
+ (this.movesCount >= 1 || i != 4 || j != 4)
+ ) {
+ if (opponentCanCapture) addMove([i, j]);
+ else {
+ let canAddMove = true;
+ for (let s of V.steps[V.BISHOP]) {
+ if (
+ V.OnBoard(i + s[0], j + s[1]) &&
+ V.OnBoard(i - s[0], j - s[1]) &&
+ this.board[i + s[0]][j + s[1]] != V.EMPTY &&
+ this.board[i - s[0]][j - s[1]] == V.EMPTY &&
+ this.getColor(i + s[0], j + s[1]) == oppCol
+ ) {
+ canAddMove = false;
+ break;
+ }
+ }
+ if (canAddMove) addMove([i, j]);
+ }
+ }
+ }
+ }
+ return moves;
+ }
+
+ getPotentialMovesFrom([x, y], longestCaptures) {
+ if (x >= V.size.x) {
+ if (longestCaptures.length == 0) return this.getReserveMoves(x);
+ return [];
+ }
+ const color = this.turn;
+ if (!!this.reserve[color] && !this.atLeastOneCapture(color)) return [];
+ const L0 = this.captures.length;
+ const captures = this.captures[L0 - 1];
+ const L = captures.length;
+ let moves = [];
+ if (longestCaptures.length > 0) {
+ if (
+ L > 0 &&
+ (x != captures[L-1].square[0] || y != captures[L-1].square[1])
+ ) {
+ return [];
+ }
+ longestCaptures.forEach(lc => {
+ if (lc.square[0] == x && lc.square[1] == y) {
+ const s = lc.step;
+ const [i, j] = [x + s[0], y + s[1]];
+ moves.push(this.getBasicMove([x, y], [i + s[0], j + s[1]], [i, j]));
+ }
+ });
+ return moves;
+ }
+ // Just search simple moves:
+ for (let s of V.steps[V.BISHOP]) {
+ const [i, j] = [x + s[0], y + s[1]];
+ if (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY)
+ moves.push(this.getBasicMove([x, y], [i, j]));
+ }
+ return moves;
+ }
+
+ getAllValidMoves() {
+ const color = this.turn;
+ const longestCaptures = this.getAllLongestCaptures(color);
+ let potentialMoves = [];
+ for (let i = 0; i < V.size.x; i++) {
+ for (let j = 0; j < V.size.y; j++) {
+ if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) {
+ Array.prototype.push.apply(
+ potentialMoves,
+ this.getPotentialMovesFrom([i, j], longestCaptures)
+ );
+ }
+ }
+ }
+ // Add reserve moves
+ potentialMoves = potentialMoves.concat(
+ this.getReserveMoves(V.size.x + (color == "w" ? 0 : 1))
+ );
+ return potentialMoves;
+ }
+
+ getPossibleMovesFrom([x, y]) {
+ const longestCaptures = this.getAllLongestCaptures(this.getColor(x, y));
+ return this.getPotentialMovesFrom([x, y], longestCaptures);
+ }
+
+ filterValid(moves) {
+ return moves;
+ }
+
+ getCheckSquares() {
+ return [];
+ }
+
+ play(move) {
+ const color = this.turn;
+ move.turn = color; //for undo
+ V.PlayOnBoard(this.board, move);
+ if (move.vanish.length == 2) {
+ const L0 = this.captures.length;
+ let captures = this.captures[L0 - 1];
+ captures.push({
+ square: [move.end.x, move.end.y],
+ step: [(move.end.x - move.start.x)/2, (move.end.y - move.start.y)/2]
+ });
+ if (this.atLeastOneCapture(color))
+ // There could be other captures (mandatory)
+ move.notTheEnd = true;
+ }
+ else if (move.vanish == 0) {
+ const firstCode = (color == 'w' ? 65 : 97);
+ // Generally, reserveCount == 1 (except for shadow piece)
+ const reserveCount = move.appear[0].c.charCodeAt() - firstCode + 1;
+ this.reserve[color][V.PAWN] -= reserveCount;
+ if (this.reserve[color][V.PAWN] == 0) this.reserve[color] = null;
+ }
+ if (!move.notTheEnd) {
+ this.turn = V.GetOppCol(color);
+ this.movesCount++;
+ this.captures.push([]);
+ }
+ }
+
+ undo(move) {
+ V.UndoOnBoard(this.board, move);
+ if (!move.notTheEnd) {
+ this.turn = move.turn;
+ this.movesCount--;
+ this.captures.pop();
+ }
+ if (move.vanish.length == 0) {
+ const color = (move.appear[0].c == 'A' ? 'w' : 'b');
+ const firstCode = (color == 'w' ? 65 : 97);
+ const reserveCount = move.appear[0].c.charCodeAt() - firstCode + 1;
+ if (!this.reserve[color]) this.reserve[color] = { [V.PAWN]: 0 };
+ this.reserve[color][V.PAWN] += reserveCount;
+ }
+ else if (move.vanish.length == 2) {
+ const L0 = this.captures.length;
+ let captures = this.captures[L0 - 1];
+ captures.pop();
+ }
+ }
+
+ atLeastOneMove() {
+ const color = this.turn;
+ if (this.atLeastOneCapture(color)) return true;
+ for (let i = 0; i < V.size.x; i++) {
+ for (let j = 0; j < V.size.y; j++) {
+ if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) {
+ const moves = this.getPotentialMovesFrom([i, j], []);
+ if (moves.length > 0) return true;
+ }
+ }
+ }
+ const reserveMoves =
+ this.getReserveMoves(V.size.x + (this.turn == "w" ? 0 : 1));
+ return (reserveMoves.length > 0);
+ }
+
+ getCurrentScore() {
+ const color = this.turn;
+ // If no pieces on board + reserve, I lose
+ if (!!this.reserve[color]) return "*";
+ let atLeastOnePiece = false;
+ outerLoop: for (let i=0; i < V.size.x; i++) {
+ for (let j=0; j < V.size.y; j++) {
+ if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) {
+ atLeastOnePiece = true;
+ break outerLoop;
+ }
+ }
+ }
+ if (!atLeastOnePiece) return (color == 'w' ? "0-1" : "1-0");
+ if (!this.atLeastOneMove()) return "1/2";
+ return "*";
+ }
+
+ getComputerMove() {
+ // Random mover for now (TODO)
+ const color = this.turn;
+ let mvArray = [];
+ let mv = null;
+ while (this.turn == color) {
+ const moves = this.getAllValidMoves();
+ mv = moves[randInt(moves.length)];
+ mvArray.push(mv);
+ this.play(mv);
+ }
+ for (let i = mvArray.length - 1; i >= 0; i--) this.undo(mvArray[i]);
+ return (mvArray.length > 1 ? mvArray : mvArray[0]);
+ }
+
+ getNotation(move) {
+ if (move.vanish.length == 0) return "@" + V.CoordsToSquare(move.end);
+ const L0 = this.captures.length;
+ if (this.captures[L0 - 1].length > 0) return V.CoordsToSquare(move.end);
+ return V.CoordsToSquare(move.start) + V.CoordsToSquare(move.end);
+ }
+
+};
--- /dev/null
+import { ChessRules } from "@/base_rules";
+
+export class EmpireRules extends ChessRules {
+
+ static get PawnSpecs() {
+ return Object.assign(
+ {},
+ ChessRules.PawnSpecs,
+ { promotions: [V.QUEEN] }
+ );
+ }
+
+ static get LoseOnRepetition() {
+ return true;
+ }
+
+ static IsGoodFlags(flags) {
+ // Only black can castle
+ return !!flags.match(/^[a-z]{2,2}$/);
+ }
+
+ getPpath(b) {
+ return (b[0] == 'w' ? "Empire/" : "") + b;
+ }
+
+ static GenRandInitFen(options) {
+ if (options.randomness == 0)
+ return "rnbqkbnr/pppppppp/8/8/8/PPPSSPPP/8/TECDKCET w 0 ah -";
+
+ // Mapping kingdom --> empire:
+ const piecesMap = {
+ 'R': 'T',
+ 'N': 'E',
+ 'B': 'C',
+ 'Q': 'D',
+ 'K': 'K'
+ };
+
+ const baseFen = ChessRules.GenRandInitFen(options);
+ return (
+ baseFen.substr(0, 24) + "PPPSSPPP/8/" +
+ baseFen.substr(35, 8).split('').map(p => piecesMap[p]).join('') +
+ baseFen.substr(43, 5) + baseFen.substr(50)
+ );
+ }
+
+ getFlagsFen() {
+ return this.castleFlags['b'].map(V.CoordToColumn).join("");
+ }
+
+ setFlags(fenflags) {
+ this.castleFlags = { 'b': [-1, -1] };
+ for (let i = 0; i < 2; i++)
+ this.castleFlags['b'][i] = V.ColumnToCoord(fenflags.charAt(i));
+ }
+
+ static get TOWER() {
+ return 't';
+ }
+ static get EAGLE() {
+ return 'e';
+ }
+ static get CARDINAL() {
+ return 'c';
+ }
+ static get DUKE() {
+ return 'd';
+ }
+ static get SOLDIER() {
+ return 's';
+ }
+ // Kaiser is technically a King, so let's keep things simple.
+
+ static get PIECES() {
+ return ChessRules.PIECES.concat(
+ [V.TOWER, V.EAGLE, V.CARDINAL, V.DUKE, V.SOLDIER]);
+ }
+
+ getPotentialMovesFrom(sq) {
+ let moves = [];
+ const piece = this.getPiece(sq[0], sq[1]);
+ switch (piece) {
+ case V.TOWER:
+ moves = this.getPotentialTowerMoves(sq);
+ break;
+ case V.EAGLE:
+ moves = this.getPotentialEagleMoves(sq);
+ break;
+ case V.CARDINAL:
+ moves = this.getPotentialCardinalMoves(sq);
+ break;
+ case V.DUKE:
+ moves = this.getPotentialDukeMoves(sq);
+ break;
+ case V.SOLDIER:
+ moves = this.getPotentialSoldierMoves(sq);
+ break;
+ default:
+ moves = super.getPotentialMovesFrom(sq);
+ }
+ if (
+ piece != V.KING &&
+ this.kingPos['w'][0] != this.kingPos['b'][0] &&
+ this.kingPos['w'][1] != this.kingPos['b'][1]
+ ) {
+ return moves;
+ }
+ // TODO: factor two next "if" into one (rank/column...)
+ if (this.kingPos['w'][1] == this.kingPos['b'][1]) {
+ const colKing = this.kingPos['w'][1];
+ let intercept = 0; //count intercepting pieces
+ let [kingPos1, kingPos2] = [this.kingPos['w'][0], this.kingPos['b'][0]];
+ if (kingPos1 > kingPos2) [kingPos1, kingPos2] = [kingPos2, kingPos1];
+ for (let i = kingPos1 + 1; i < kingPos2; i++) {
+ if (this.board[i][colKing] != V.EMPTY) intercept++;
+ }
+ if (intercept >= 2) return moves;
+ // intercept == 1 (0 is impossible):
+ // Any move not removing intercept is OK
+ return moves.filter(m => {
+ return (
+ // From another column?
+ m.start.y != colKing ||
+ // From behind a king? (including kings themselves!)
+ m.start.x <= kingPos1 ||
+ m.start.x >= kingPos2 ||
+ // Intercept piece moving: must remain in-between
+ (
+ m.end.y == colKing &&
+ m.end.x > kingPos1 &&
+ m.end.x < kingPos2
+ )
+ );
+ });
+ }
+ if (this.kingPos['w'][0] == this.kingPos['b'][0]) {
+ const rowKing = this.kingPos['w'][0];
+ let intercept = 0; //count intercepting pieces
+ let [kingPos1, kingPos2] = [this.kingPos['w'][1], this.kingPos['b'][1]];
+ if (kingPos1 > kingPos2) [kingPos1, kingPos2] = [kingPos2, kingPos1];
+ for (let i = kingPos1 + 1; i < kingPos2; i++) {
+ if (this.board[rowKing][i] != V.EMPTY) intercept++;
+ }
+ if (intercept >= 2) return moves;
+ // intercept == 1 (0 is impossible):
+ // Any move not removing intercept is OK
+ return moves.filter(m => {
+ return (
+ // From another row?
+ m.start.x != rowKing ||
+ // From "behind" a king? (including kings themselves!)
+ m.start.y <= kingPos1 ||
+ m.start.y >= kingPos2 ||
+ // Intercept piece moving: must remain in-between
+ (
+ m.end.x == rowKing &&
+ m.end.y > kingPos1 &&
+ m.end.y < kingPos2
+ )
+ );
+ });
+ }
+ // piece == king: check only if move.end.y == enemy king column,
+ // or if move.end.x == enemy king rank.
+ const color = this.getColor(sq[0], sq[1]);
+ const oppCol = V.GetOppCol(color);
+ return moves.filter(m => {
+ if (
+ m.end.y != this.kingPos[oppCol][1] &&
+ m.end.x != this.kingPos[oppCol][0]
+ ) {
+ return true;
+ }
+ // check == -1 if (row, or col) unchecked, 1 if checked and occupied,
+ // 0 if checked and clear
+ let check = [-1, -1];
+ // TODO: factor two next "if"...
+ if (m.end.x == this.kingPos[oppCol][0]) {
+ if (check[0] < 0) {
+ // Do the check:
+ check[0] = 0;
+ let [kingPos1, kingPos2] = [m.end.y, this.kingPos[oppCol][1]];
+ if (kingPos1 > kingPos2) [kingPos1, kingPos2] = [kingPos2, kingPos1];
+ for (let i = kingPos1 + 1; i < kingPos2; i++) {
+ if (this.board[m.end.x][i] != V.EMPTY) {
+ check[0]++;
+ break;
+ }
+ }
+ return check[0] == 1;
+ }
+ // Check already done:
+ return check[0] == 1;
+ }
+ //if (m.end.y == this.kingPos[oppCol][1]) //true...
+ if (check[1] < 0) {
+ // Do the check:
+ check[1] = 0;
+ let [kingPos1, kingPos2] = [m.end.x, this.kingPos[oppCol][0]];
+ if (kingPos1 > kingPos2) [kingPos1, kingPos2] = [kingPos2, kingPos1];
+ for (let i = kingPos1 + 1; i < kingPos2; i++) {
+ if (this.board[i][m.end.y] != V.EMPTY) {
+ check[1]++;
+ break;
+ }
+ }
+ return check[1] == 1;
+ }
+ // Check already done:
+ return check[1] == 1;
+ });
+ }
+
+ // TODO: some merging to do with Orda method (and into base_rules.js)
+ getSlideNJumpMoves_([x, y], steps, oneStep) {
+ let moves = [];
+ outerLoop: for (let step of steps) {
+ const s = step.s;
+ let i = x + s[0];
+ let j = y + s[1];
+ while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
+ if (!step.onlyTake) moves.push(this.getBasicMove([x, y], [i, j]));
+ // NOTE: (bad) HACK here, since onlyTake is true only for Eagle
+ // capturing moves, which are oneStep...
+ if (oneStep || step.onlyTake) continue outerLoop;
+ i += s[0];
+ j += s[1];
+ }
+ if (V.OnBoard(i, j) && this.canTake([x, y], [i, j]) && !step.onlyMove)
+ moves.push(this.getBasicMove([x, y], [i, j]));
+ }
+ return moves;
+ }
+
+ static get steps() {
+ return (
+ Object.assign(
+ {
+ t: [
+ { s: [-1, 0] },
+ { s: [1, 0] },
+ { s: [0, -1] },
+ { s: [0, 1] },
+ { s: [-1, -1], onlyMove: true },
+ { s: [-1, 1], onlyMove: true },
+ { s: [1, -1], onlyMove: true },
+ { s: [1, 1], onlyMove: true }
+ ],
+ c: [
+ { s: [-1, 0], onlyMove: true },
+ { s: [1, 0], onlyMove: true },
+ { s: [0, -1], onlyMove: true },
+ { s: [0, 1], onlyMove: true },
+ { s: [-1, -1] },
+ { s: [-1, 1] },
+ { s: [1, -1] },
+ { s: [1, 1] }
+ ],
+ e: [
+ { s: [-1, 0], onlyMove: true },
+ { s: [1, 0], onlyMove: true },
+ { s: [0, -1], onlyMove: true },
+ { s: [0, 1], onlyMove: true },
+ { s: [-1, -1], onlyMove: true },
+ { s: [-1, 1], onlyMove: true },
+ { s: [1, -1], onlyMove: true },
+ { s: [1, 1], onlyMove: true },
+ { s: [-2, -1], onlyTake: true },
+ { s: [-2, 1], onlyTake: true },
+ { s: [-1, -2], onlyTake: true },
+ { s: [-1, 2], onlyTake: true },
+ { s: [1, -2], onlyTake: true },
+ { s: [1, 2], onlyTake: true },
+ { s: [2, -1], onlyTake: true },
+ { s: [2, 1], onlyTake: true }
+ ]
+ },
+ ChessRules.steps
+ )
+ );
+ }
+
+ getPotentialTowerMoves(sq) {
+ return this.getSlideNJumpMoves_(sq, V.steps[V.TOWER]);
+ }
+
+ getPotentialCardinalMoves(sq) {
+ return this.getSlideNJumpMoves_(sq, V.steps[V.CARDINAL]);
+ }
+
+ getPotentialEagleMoves(sq) {
+ return this.getSlideNJumpMoves_(sq, V.steps[V.EAGLE]);
+ }
+
+ getPotentialDukeMoves([x, y]) {
+ // Anything to capture around? mark other steps to explore after
+ let steps = [];
+ const oppCol = V.GetOppCol(this.getColor(x, y));
+ let moves = [];
+ for (let s of V.steps[V.ROOK].concat(V.steps[V.BISHOP])) {
+ const [i, j] = [x + s[0], y + s[1]];
+ if (
+ V.OnBoard(i, j) &&
+ this.board[i][j] != V.EMPTY &&
+ this.getColor(i, j) == oppCol
+ ) {
+ moves.push(super.getBasicMove([x, y], [i, j]));
+ }
+ else steps.push({ s: s, onlyMove: true });
+ }
+ if (steps.length > 0) {
+ const noncapturingMoves = this.getSlideNJumpMoves_([x, y], steps);
+ Array.prototype.push.apply(moves, noncapturingMoves);
+ }
+ return moves;
+ }
+
+ getPotentialKingMoves([x, y]) {
+ if (this.getColor(x, y) == 'b') return super.getPotentialKingMoves([x, y]);
+ // Empire doesn't castle:
+ return super.getSlideNJumpMoves(
+ [x, y], V.steps[V.ROOK].concat(V.steps[V.BISHOP]), 1);
+ }
+
+ getPotentialSoldierMoves([x, y]) {
+ const c = this.getColor(x, y);
+ const shiftX = (c == 'w' ? -1 : 1);
+ const lastRank = (c == 'w' && x == 0 || c == 'b' && x == 9);
+ let steps = [];
+ if (!lastRank) steps.push([shiftX, 0]);
+ if (y > 0) steps.push([0, -1]);
+ if (y < 9) steps.push([0, 1]);
+ return super.getSlideNJumpMoves([x, y], steps, 1);
+ }
+
+ isAttacked(sq, color) {
+ if (color == 'b') return super.isAttacked(sq, color);
+ // Empire: only pawn and king (+ queen if promotion) in common:
+ return (
+ super.isAttackedByPawn(sq, color) ||
+ this.isAttackedByTower(sq, color) ||
+ this.isAttackedByEagle(sq, color) ||
+ this.isAttackedByCardinal(sq, color) ||
+ this.isAttackedByDuke(sq, color) ||
+ this.isAttackedBySoldier(sq, color) ||
+ super.isAttackedByKing(sq, color) ||
+ super.isAttackedByQueen(sq, color)
+ );
+ }
+
+ isAttackedByTower(sq, color) {
+ return super.isAttackedBySlideNJump(sq, color, V.TOWER, V.steps[V.ROOK]);
+ }
+
+ isAttackedByEagle(sq, color) {
+ return super.isAttackedBySlideNJump(
+ sq, color, V.EAGLE, V.steps[V.KNIGHT], 1);
+ }
+
+ isAttackedByCardinal(sq, color) {
+ return super.isAttackedBySlideNJump(
+ sq, color, V.CARDINAL, V.steps[V.BISHOP]);
+ }
+
+ isAttackedByDuke(sq, color) {
+ return (
+ super.isAttackedBySlideNJump(
+ sq, color, V.DUKE,
+ V.steps[V.ROOK].concat(V.steps[V.BISHOP]), 1
+ )
+ );
+ }
+
+ isAttackedBySoldier([x, y], color) {
+ const shiftX = (color == 'w' ? 1 : -1); //shift from king
+ return super.isAttackedBySlideNJump(
+ [x, y], color, V.SOLDIER, [[shiftX, 0], [0, 1], [0, -1]], 1);
+ }
+
+ updateCastleFlags(move, piece) {
+ // Only black can castle:
+ const firstRank = 0;
+ if (piece == V.KING && move.appear[0].c == 'b')
+ this.castleFlags['b'] = [8, 8];
+ else if (
+ move.start.x == firstRank &&
+ this.castleFlags['b'].includes(move.start.y)
+ ) {
+ const flagIdx = (move.start.y == this.castleFlags['b'][0] ? 0 : 1);
+ this.castleFlags['b'][flagIdx] = 8;
+ }
+ else if (
+ move.end.x == firstRank &&
+ this.castleFlags['b'].includes(move.end.y)
+ ) {
+ const flagIdx = (move.end.y == this.castleFlags['b'][0] ? 0 : 1);
+ this.castleFlags['b'][flagIdx] = 8;
+ }
+ }
+
+ getCurrentScore() {
+ // Turn has changed:
+ const color = V.GetOppCol(this.turn);
+ const lastRank = (color == 'w' ? 0 : 7);
+ if (this.kingPos[color][0] == lastRank)
+ // The opposing edge is reached!
+ return color == "w" ? "1-0" : "0-1";
+ if (this.atLeastOneMove()) return "*";
+ // Game over
+ const oppCol = this.turn;
+ return (oppCol == "w" ? "0-1" : "1-0");
+ }
+
+ static get VALUES() {
+ return Object.assign(
+ {},
+ ChessRules.VALUES,
+ {
+ t: 7,
+ e: 7,
+ c: 4,
+ d: 4,
+ s: 2
+ }
+ );
+ }
+
+ static get SEARCH_DEPTH() {
+ return 2;
+ }
+
+};
--- /dev/null
+import { ChessRules, PiPo, Move } from "@/base_rules";
+
+export class EnpassantRules extends ChessRules {
+
+ static IsGoodEnpassant(enpassant) {
+ if (enpassant != "-") {
+ const squares = enpassant.split(",");
+ if (squares.length > 2) return false;
+ for (let sq of squares) {
+ const ep = V.SquareToCoords(sq);
+ if (isNaN(ep.x) || !V.OnBoard(ep)) return false;
+ }
+ }
+ return true;
+ }
+
+ getPpath(b) {
+ return (b[1] == V.KNIGHT ? "Enpassant/" : "") + b;
+ }
+
+ getEpSquare(moveOrSquare) {
+ if (!moveOrSquare) return undefined;
+ if (typeof moveOrSquare === "string") {
+ const square = moveOrSquare;
+ if (square == "-") return undefined;
+ // Expand init + dest squares into a full path:
+ const init = V.SquareToCoords(square.substr(0, 2));
+ let newPath = [init];
+ if (square.length == 2) return newPath;
+ const dest = V.SquareToCoords(square.substr(2));
+ const delta = ['x', 'y'].map(i => Math.abs(dest[i] - init[i]));
+ // Check if it's a knight(rider) movement:
+ let step = [0, 0];
+ if (delta[0] > 0 && delta[1] > 0 && delta[0] != delta[1]) {
+ // Knightrider
+ const minShift = Math.min(delta[0], delta[1]);
+ step[0] = (dest.x - init.x) / minShift;
+ step[1] = (dest.y - init.y) / minShift;
+ } else {
+ // "Sliders"
+ step = ['x', 'y'].map((i, idx) => {
+ return (dest[i] - init[i]) / delta[idx] || 0
+ });
+ }
+ let x = init.x + step[0],
+ y = init.y + step[1];
+ while (x != dest.x || y != dest.y) {
+ newPath.push({ x: x, y: y });
+ x += step[0];
+ y += step[1];
+ }
+ newPath.push(dest);
+ return newPath;
+ }
+ // Argument is a move: all intermediate squares are en-passant candidates,
+ // except if the moving piece is a king.
+ const move = moveOrSquare;
+ const piece = move.appear[0].p;
+ if (piece == V.KING ||
+ (
+ Math.abs(move.end.x-move.start.x) <= 1 &&
+ Math.abs(move.end.y-move.start.y) <= 1
+ )
+ ) {
+ return undefined;
+ }
+ const delta = [move.end.x-move.start.x, move.end.y-move.start.y];
+ let step = undefined;
+ if (piece == V.KNIGHT) {
+ const divisor = Math.min(Math.abs(delta[0]), Math.abs(delta[1]));
+ step = [delta[0]/divisor || 0, delta[1]/divisor || 0];
+ } else {
+ step = [
+ delta[0]/Math.abs(delta[0]) || 0,
+ delta[1]/Math.abs(delta[1]) || 0
+ ];
+ }
+ let res = [];
+ for (
+ let [x,y] = [move.start.x+step[0],move.start.y+step[1]];
+ x != move.end.x || y != move.end.y;
+ x += step[0], y += step[1]
+ ) {
+ res.push({ x: x, y: y });
+ }
+ // Add final square to know which piece is taken en passant:
+ res.push(move.end);
+ return res;
+ }
+
+ getEnpassantFen() {
+ const L = this.epSquares.length;
+ if (!this.epSquares[L - 1]) return "-"; //no en-passant
+ const epsq = this.epSquares[L - 1];
+ if (epsq.length <= 2) return epsq.map(V.CoordsToSquare).join("");
+ // Condensate path: just need initial and final squares:
+ return V.CoordsToSquare(epsq[0]) + V.CoordsToSquare(epsq[epsq.length - 1]);
+ }
+
+ getPotentialMovesFrom([x, y]) {
+ let moves = super.getPotentialMovesFrom([x,y]);
+ // Add en-passant captures from this square:
+ const L = this.epSquares.length;
+ if (!this.epSquares[L - 1]) return moves;
+ const squares = this.epSquares[L - 1];
+ const S = squares.length;
+ // Object describing the removed opponent's piece:
+ const pipoV = new PiPo({
+ x: squares[S-1].x,
+ y: squares[S-1].y,
+ c: V.GetOppCol(this.turn),
+ p: this.getPiece(squares[S-1].x, squares[S-1].y)
+ });
+ // Check if existing non-capturing moves could also capture en passant
+ moves.forEach(m => {
+ if (
+ m.appear[0].p != V.PAWN && //special pawn case is handled elsewhere
+ m.vanish.length <= 1 &&
+ [...Array(S-1).keys()].some(i => {
+ return m.end.x == squares[i].x && m.end.y == squares[i].y;
+ })
+ ) {
+ m.vanish.push(pipoV);
+ }
+ });
+ // Special case of the king knight's movement:
+ if (this.getPiece(x, y) == V.KING) {
+ V.steps[V.KNIGHT].forEach(step => {
+ const endX = x + step[0];
+ const endY = y + step[1];
+ if (
+ V.OnBoard(endX, endY) &&
+ [...Array(S-1).keys()].some(i => {
+ return endX == squares[i].x && endY == squares[i].y;
+ })
+ ) {
+ let enpassantMove = this.getBasicMove([x, y], [endX, endY]);
+ enpassantMove.vanish.push(pipoV);
+ moves.push(enpassantMove);
+ }
+ });
+ }
+ return moves;
+ }
+
+ getEnpassantCaptures([x, y], shiftX) {
+ const Lep = this.epSquares.length;
+ const squares = this.epSquares[Lep - 1];
+ let moves = [];
+ if (!!squares) {
+ const S = squares.length;
+ const taken = squares[S-1];
+ const pipoV = new PiPo({
+ x: taken.x,
+ y: taken.y,
+ p: this.getPiece(taken.x, taken.y),
+ c: this.getColor(taken.x, taken.y)
+ });
+ [...Array(S-1).keys()].forEach(i => {
+ const sq = squares[i];
+ if (sq.x == x + shiftX && Math.abs(sq.y - y) == 1) {
+ let enpassantMove = this.getBasicMove([x, y], [sq.x, sq.y]);
+ enpassantMove.vanish.push(pipoV);
+ moves.push(enpassantMove);
+ }
+ });
+ }
+ return moves;
+ }
+
+ // Remove the "onestep" condition: knight promote to knightrider:
+ getPotentialKnightMoves(sq) {
+ return this.getSlideNJumpMoves(sq, V.steps[V.KNIGHT]);
+ }
+
+ filterValid(moves) {
+ const filteredMoves = super.filterValid(moves);
+ // If at least one full move made, everything is allowed:
+ if (this.movesCount >= 2)
+ return filteredMoves;
+ // Else, forbid captures:
+ return filteredMoves.filter(m => m.vanish.length == 1);
+ }
+
+ isAttackedByKnight(sq, color) {
+ return this.isAttackedBySlideNJump(
+ sq,
+ color,
+ V.KNIGHT,
+ V.steps[V.KNIGHT]
+ );
+ }
+
+ static get SEARCH_DEPTH() {
+ return 2;
+ }
+
+ static get VALUES() {
+ return {
+ p: 1,
+ r: 5,
+ n: 4,
+ b: 3,
+ q: 9,
+ k: 1000
+ };
+ }
+
+};
--- /dev/null
+import { ChessRules } from "@/base_rules";
+
+export class EvolutionRules extends ChessRules {
+
+ getPotentialMovesFrom([x, y]) {
+ let moves = super.getPotentialMovesFrom([x, y]);
+ const c = this.getColor(x, y);
+ const piece = this.getPiece(x, y);
+ if (
+ [V.BISHOP, V.ROOK, V.QUEEN].includes(piece) &&
+ (c == 'w' && x == 7) || (c == 'b' && x == 0)
+ ) {
+ // Move from first rank
+ const forward = (c == 'w' ? -1 : 1);
+ for (let shift of [-2, 0, 2]) {
+ if (
+ (piece == V.ROOK && shift != 0) ||
+ (piece == V.BISHOP && shift == 0)
+ ) {
+ continue;
+ }
+ if (
+ V.OnBoard(x+2*forward, y+shift) &&
+ this.board[x+forward][y+shift/2] != V.EMPTY &&
+ this.getColor(x+2*forward, y+shift) != c
+ ) {
+ moves.push(this.getBasicMove([x,y], [x+2*forward,y+shift]));
+ }
+ }
+ }
+ return moves;
+ }
+
+};
--- /dev/null
+import { ChessRules } from "@/base_rules";
+
+export class ExtinctionRules extends ChessRules {
+
+ static get PawnSpecs() {
+ return Object.assign(
+ {},
+ ChessRules.PawnSpecs,
+ { promotions: ChessRules.PawnSpecs.promotions.concat([V.KING]) }
+ );
+ }
+
+ static IsGoodPosition(position) {
+ if (!ChessRules.IsGoodPosition(position)) return false;
+ // Also check that each piece type is present
+ const rows = position.split("/");
+ let pieces = {};
+ for (let row of rows) {
+ for (let i = 0; i < row.length; i++) {
+ if (isNaN(parseInt(row[i], 10)) && !pieces[row[i]])
+ pieces[row[i]] = true;
+ }
+ }
+ if (Object.keys(pieces).length != 12) return false;
+ return true;
+ }
+
+ setOtherVariables(fen) {
+ super.setOtherVariables(fen);
+ const pos = V.ParseFen(fen).position;
+ // NOTE: no need for safety "|| []", because each piece type is present
+ // (otherwise game is already over!)
+ this.material = {
+ w: {
+ [V.KING]: pos.match(/K/g).length,
+ [V.QUEEN]: pos.match(/Q/g).length,
+ [V.ROOK]: pos.match(/R/g).length,
+ [V.KNIGHT]: pos.match(/N/g).length,
+ [V.BISHOP]: pos.match(/B/g).length,
+ [V.PAWN]: pos.match(/P/g).length
+ },
+ b: {
+ [V.KING]: pos.match(/k/g).length,
+ [V.QUEEN]: pos.match(/q/g).length,
+ [V.ROOK]: pos.match(/r/g).length,
+ [V.KNIGHT]: pos.match(/n/g).length,
+ [V.BISHOP]: pos.match(/b/g).length,
+ [V.PAWN]: pos.match(/p/g).length
+ }
+ };
+ }
+
+ // TODO: verify this assertion
+ atLeastOneMove() {
+ return true; //always at least one possible move
+ }
+
+ filterValid(moves) {
+ return moves; //there is no check
+ }
+
+ getCheckSquares() {
+ return [];
+ }
+
+ postPlay(move) {
+ super.postPlay(move);
+ // Treat the promotion case: (not the capture part)
+ if (move.appear[0].p != move.vanish[0].p) {
+ this.material[move.appear[0].c][move.appear[0].p]++;
+ this.material[move.appear[0].c][V.PAWN]--;
+ }
+ if (move.vanish.length == 2 && move.appear.length == 1)
+ //capture
+ this.material[move.vanish[1].c][move.vanish[1].p]--;
+ }
+
+ postUndo(move) {
+ super.postUndo(move);
+ if (move.appear[0].p != move.vanish[0].p) {
+ this.material[move.appear[0].c][move.appear[0].p]--;
+ this.material[move.appear[0].c][V.PAWN]++;
+ }
+ if (move.vanish.length == 2 && move.appear.length == 1)
+ this.material[move.vanish[1].c][move.vanish[1].p]++;
+ }
+
+ getCurrentScore() {
+ if (this.atLeastOneMove()) {
+ // Game not over?
+ const color = this.turn;
+ if (
+ Object.keys(this.material[color]).some(p => {
+ return this.material[color][p] == 0;
+ })
+ ) {
+ return this.turn == "w" ? "0-1" : "1-0";
+ }
+ return "*";
+ }
+ return this.turn == "w" ? "0-1" : "1-0"; //NOTE: currently unreachable...
+ }
+
+ evalPosition() {
+ const color = this.turn;
+ if (
+ Object.keys(this.material[color]).some(p => {
+ return this.material[color][p] == 0;
+ })
+ ) {
+ // Very negative (resp. positive)
+ // if white (reps. black) pieces set is incomplete
+ return (color == "w" ? -1 : 1) * V.INFINITY;
+ }
+ return super.evalPosition();
+ }
+
+};
--- /dev/null
+import { ChessRules, Move, PiPo } from "@/base_rules";
+import { randInt } from "@/utils/alea";
+
+export class FanoronaRules extends ChessRules {
+
+ static get Options() {
+ return null;
+ }
+
+ static get HasFlags() {
+ return false;
+ }
+
+ static get HasEnpassant() {
+ return false;
+ }
+
+ static get Monochrome() {
+ return true;
+ }
+
+ static get Lines() {
+ let lines = [];
+ // Draw all inter-squares lines, shifted:
+ for (let i = 0; i < V.size.x; i++)
+ lines.push([[i+0.5, 0.5], [i+0.5, V.size.y-0.5]]);
+ for (let j = 0; j < V.size.y; j++)
+ lines.push([[0.5, j+0.5], [V.size.x-0.5, j+0.5]]);
+ const columnDiags = [
+ [[0.5, 0.5], [2.5, 2.5]],
+ [[0.5, 2.5], [2.5, 0.5]],
+ [[2.5, 0.5], [4.5, 2.5]],
+ [[4.5, 0.5], [2.5, 2.5]]
+ ];
+ for (let j of [0, 2, 4, 6]) {
+ lines = lines.concat(
+ columnDiags.map(L => [[L[0][0], L[0][1] + j], [L[1][0], L[1][1] + j]])
+ );
+ }
+ return lines;
+ }
+
+ static get Notoodark() {
+ return true;
+ }
+
+ static GenRandInitFen() {
+ return "ppppppppp/ppppppppp/pPpP1pPpP/PPPPPPPPP/PPPPPPPPP w 0";
+ }
+
+ setOtherVariables(fen) {
+ // Local stack of captures during a turn (squares + directions)
+ this.captures = [ [] ];
+ }
+
+ static get size() {
+ return { x: 5, y: 9 };
+ }
+
+ getPiece() {
+ return V.PAWN;
+ }
+
+ static IsGoodPosition(position) {
+ if (position.length == 0) return false;
+ const rows = position.split("/");
+ if (rows.length != V.size.x) return false;
+ for (let row of rows) {
+ let sumElts = 0;
+ for (let i = 0; i < row.length; i++) {
+ if (row[i].toLowerCase() == V.PAWN) sumElts++;
+ else {
+ const num = parseInt(row[i], 10);
+ if (isNaN(num) || num <= 0) return false;
+ sumElts += num;
+ }
+ }
+ if (sumElts != V.size.y) return false;
+ }
+ return true;
+ }
+
+ getPpath(b) {
+ return "Fanorona/" + b;
+ }
+
+ getPPpath(m, orientation) {
+ // m.vanish.length >= 2, first capture gives direction
+ const ref = (Math.abs(m.vanish[1].x - m.start.x) == 1 ? m.start : m.end);
+ const step = [m.vanish[1].x - ref.x, m.vanish[1].y - ref.y];
+ const multStep = (orientation == 'w' ? 1 : -1);
+ const normalizedStep = [
+ multStep * step[0] / Math.abs(step[0]),
+ multStep * step[1] / Math.abs(step[1])
+ ];
+ return (
+ "Fanorona/arrow_" +
+ (normalizedStep[0] || 0) + "_" + (normalizedStep[1] || 0)
+ );
+ }
+
+ // After moving, add stones captured in "step" direction from new location
+ // [x, y] to mv.vanish (if any captured stone!)
+ addCapture([x, y], step, move) {
+ let [i, j] = [x + step[0], y + step[1]];
+ const oppCol = V.GetOppCol(move.vanish[0].c);
+ while (
+ V.OnBoard(i, j) &&
+ this.board[i][j] != V.EMPTY &&
+ this.getColor(i, j) == oppCol
+ ) {
+ move.vanish.push(new PiPo({ x: i, y: j, c: oppCol, p: V.PAWN }));
+ i += step[0];
+ j += step[1];
+ }
+ return (move.vanish.length >= 2);
+ }
+
+ getPotentialMovesFrom([x, y]) {
+ const L0 = this.captures.length;
+ const captures = this.captures[L0 - 1];
+ const L = captures.length;
+ if (L > 0) {
+ var c = captures[L-1];
+ if (x != c.square.x + c.step[0] || y != c.square.y + c.step[1])
+ return [];
+ }
+ const oppCol = V.GetOppCol(this.turn);
+ let steps = V.steps[V.ROOK];
+ if ((x + y) % 2 == 0) steps = steps.concat(V.steps[V.BISHOP]);
+ let moves = [];
+ for (let s of steps) {
+ if (L > 0 && c.step[0] == s[0] && c.step[1] == s[1]) {
+ // Add a move to say "I'm done capturing"
+ moves.push(
+ new Move({
+ appear: [],
+ vanish: [],
+ start: { x: x, y: y },
+ end: { x: x - s[0], y: y - s[1] }
+ })
+ );
+ continue;
+ }
+ let [i, j] = [x + s[0], y + s[1]];
+ if (captures.some(c => c.square.x == i && c.square.y == j)) continue;
+ if (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
+ // The move is potentially allowed. Might lead to 2 different captures
+ let mv = super.getBasicMove([x, y], [i, j]);
+ const capt = this.addCapture([i, j], s, mv);
+ if (capt) {
+ moves.push(mv);
+ mv = super.getBasicMove([x, y], [i, j]);
+ }
+ const capt_bw = this.addCapture([x, y], [-s[0], -s[1]], mv);
+ if (capt_bw) moves.push(mv);
+ // Captures take priority (if available)
+ if (!capt && !capt_bw && L == 0) moves.push(mv);
+ }
+ }
+ return moves;
+ }
+
+ atLeastOneCapture() {
+ const color = this.turn;
+ const oppCol = V.GetOppCol(color);
+ const L0 = this.captures.length;
+ const captures = this.captures[L0 - 1];
+ const L = captures.length;
+ if (L > 0) {
+ // If some adjacent enemy stone, with free space to capture it,
+ // toward a square not already visited, through a different step
+ // from last one: then yes.
+ const c = captures[L-1];
+ const [x, y] = [c.square.x + c.step[0], c.square.y + c.step[1]];
+ let steps = V.steps[V.ROOK];
+ if ((x + y) % 2 == 0) steps = steps.concat(V.steps[V.BISHOP]);
+ // TODO: half of the steps explored are redundant
+ for (let s of steps) {
+ if (s[0] == c.step[0] && s[1] == c.step[1]) continue;
+ const [i, j] = [x + s[0], y + s[1]];
+ if (
+ !V.OnBoard(i, j) ||
+ this.board[i][j] != V.EMPTY ||
+ captures.some(c => c.square.x == i && c.square.y == j)
+ ) {
+ continue;
+ }
+ if (
+ V.OnBoard(i + s[0], j + s[1]) &&
+ this.board[i + s[0]][j + s[1]] != V.EMPTY &&
+ this.getColor(i + s[0], j + s[1]) == oppCol
+ ) {
+ return true;
+ }
+ if (
+ V.OnBoard(x - s[0], y - s[1]) &&
+ this.board[x - s[0]][y - s[1]] != V.EMPTY &&
+ this.getColor(x - s[0], y - s[1]) == oppCol
+ ) {
+ return true;
+ }
+ }
+ return false;
+ }
+ for (let i = 0; i < V.size.x; i++) {
+ for (let j = 0; j < V.size.y; j++) {
+ if (
+ this.board[i][j] != V.EMPTY &&
+ this.getColor(i, j) == color &&
+ // TODO: this could be more efficient
+ this.getPotentialMovesFrom([i, j]).some(m => m.vanish.length >= 2)
+ ) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ static KeepCaptures(moves) {
+ return moves.filter(m => m.vanish.length >= 2);
+ }
+
+ getPossibleMovesFrom(sq) {
+ let moves = this.getPotentialMovesFrom(sq);
+ const L0 = this.captures.length;
+ const captures = this.captures[L0 - 1];
+ if (captures.length > 0) return this.getPotentialMovesFrom(sq);
+ const captureMoves = V.KeepCaptures(moves);
+ if (captureMoves.length > 0) return captureMoves;
+ if (this.atLeastOneCapture()) return [];
+ return moves;
+ }
+
+ getAllValidMoves() {
+ const moves = super.getAllValidMoves();
+ if (moves.some(m => m.vanish.length >= 2)) return V.KeepCaptures(moves);
+ return moves;
+ }
+
+ filterValid(moves) {
+ return moves;
+ }
+
+ getCheckSquares() {
+ return [];
+ }
+
+ play(move) {
+ const color = this.turn;
+ move.turn = color; //for undo
+ V.PlayOnBoard(this.board, move);
+ if (move.vanish.length >= 2) {
+ const L0 = this.captures.length;
+ let captures = this.captures[L0 - 1];
+ captures.push({
+ square: move.start,
+ step: [move.end.x - move.start.x, move.end.y - move.start.y]
+ });
+ if (this.atLeastOneCapture())
+ // There could be other captures (optional)
+ move.notTheEnd = true;
+ }
+ if (!move.notTheEnd) {
+ this.turn = V.GetOppCol(color);
+ this.movesCount++;
+ this.captures.push([]);
+ }
+ }
+
+ undo(move) {
+ V.UndoOnBoard(this.board, move);
+ if (!move.notTheEnd) {
+ this.turn = move.turn;
+ this.movesCount--;
+ this.captures.pop();
+ }
+ if (move.vanish.length >= 2) {
+ const L0 = this.captures.length;
+ let captures = this.captures[L0 - 1];
+ captures.pop();
+ }
+ }
+
+ getCurrentScore() {
+ const color = this.turn;
+ // If no stones on board, I lose
+ if (
+ this.board.every(b => {
+ return b.every(cell => {
+ return (cell == "" || cell[0] != color);
+ });
+ })
+ ) {
+ return (color == 'w' ? "0-1" : "1-0");
+ }
+ return "*";
+ }
+
+ getComputerMove() {
+ const moves = this.getAllValidMoves();
+ if (moves.length == 0) return null;
+ const color = this.turn;
+ // Capture available? If yes, play it
+ let captures = moves.filter(m => m.vanish.length >= 2);
+ let mvArray = [];
+ while (captures.length >= 1) {
+ // Then just pick random captures (trying to maximize)
+ let candidates = captures.filter(c => !!c.notTheEnd);
+ let mv = null;
+ if (candidates.length >= 1) mv = candidates[randInt(candidates.length)];
+ else mv = captures[randInt(captures.length)];
+ this.play(mv);
+ mvArray.push(mv);
+ captures = (this.turn == color ? this.getAllValidMoves() : []);
+ }
+ if (mvArray.length >= 1) {
+ for (let i = mvArray.length - 1; i >= 0; i--) this.undo(mvArray[i]);
+ return mvArray;
+ }
+ // Just play a random move, which if possible does not let a capture
+ let candidates = [];
+ for (let m of moves) {
+ this.play(m);
+ if (!this.atLeastOneCapture()) candidates.push(m);
+ this.undo(m);
+ }
+ if (candidates.length >= 1) return candidates[randInt(candidates.length)];
+ return moves[randInt(moves.length)];
+ }
+
+ getNotation(move) {
+ if (move.appear.length == 0) return "stop";
+ return (
+ V.CoordsToSquare(move.start) +
+ V.CoordsToSquare(move.end) +
+ (move.vanish.length >= 2 ? "X" : "")
+ );
+ }
+
+};
--- /dev/null
+import ChessRules from "/base_rules.js";
+import PiPo from "/utils/PiPo.js";
+import Move from "/utils/Move.js";
+
+export default class SleepyRules extends ChessRules {
+
+ static get Options() {
+ return {
+ select: C.Options.select,
+ input: {},
+ styles: ["cylinder"] //TODO
+ };
+ }
+
+ setOtherVariables(fenParsed) {
+ super.setOtherVariables(fenParsed);
+ // Stack of "last move" only for intermediate chaining
+ this.lastMoveEnd = [];
+ }
+
+ getBasicMove([sx, sy], [ex, ey], tr) {
+ const L = this.lastMoveEnd.length;
+ const piece = (L >= 1 ? this.lastMoveEnd[L-1].p : null);
+ if (
+ this.board[ex][ey] == "" ||
+ this.getColor(ex, ey) == C.GetOppTurn(this.turn)
+ ) {
+ if (piece && !tr)
+ tr = {c: this.turn, p: piece};
+ let mv = super.getBasicMove([sx, sy], [ex, ey], tr);
+ if (piece)
+ mv.vanish.pop(); //end of a chain: initial piece remains
+ return mv;
+ }
+ // (Self)Capture: initial, or inside a chain
+ const initPiece = (piece || this.getPiece(sx, sy)),
+ destPiece = this.getPiece(ex, ey);
+ let mv = new Move({
+ start: {x: sx, y: sy},
+ end: {x: ex, y: ey},
+ appear: [
+ new PiPo({
+ x: ex,
+ y: ey,
+ c: this.turn,
+ p: (!!tr ? tr.p : initPiece)
+ })
+ ],
+ vanish: [
+ new PiPo({
+ x: ex,
+ y: ey,
+ c: this.turn,
+ p: destPiece
+ })
+ ]
+ });
+ if (!piece) {
+ // Initial capture
+ mv.vanish.unshift(
+ new PiPo({
+ x: sx,
+ y: sy,
+ c: this.turn,
+ p: initPiece
+ })
+ );
+ }
+ mv.chained = destPiece; //easier (no need to detect it)
+// mv.drag = {c: this.turn, p: initPiece}; //TODO: doesn't work
+ return mv;
+ }
+
+ getPiece(x, y) {
+ const L = this.lastMoveEnd.length;
+ if (L >= 1 && this.lastMoveEnd[L-1].x == x && this.lastMoveEnd[L-1].y == y)
+ return this.lastMoveEnd[L-1].p;
+ return super.getPiece(x, y);
+ }
+
+ getPotentialMovesFrom([x, y], color) {
+ const L = this.lastMoveEnd.length;
+ if (
+ L >= 1 &&
+ (x != this.lastMoveEnd[L-1].x || y != this.lastMoveEnd[L-1].y)
+ ) {
+ // A self-capture was played: wrong square
+ return [];
+ }
+ return super.getPotentialMovesFrom([x, y], color);
+ }
+
+ isLastMove(move) {
+ return !move.chained;
+ }
+
+ postPlay(move) {
+ super.postPlay(move);
+ if (!!move.chained) {
+ this.lastMoveEnd.push({
+ x: move.end.x,
+ y: move.end.y,
+ p: move.chained
+ });
+ }
+ else
+ this.lastMoveEnd = [];
+ }
+
+};
--- /dev/null
+<p>
+ You can "capture" your own pieces, and then move them from the capturing
+ square in the same turn, with potential chaining if the captured unit
+ makes a self-capture too.
+</p>
+
+<p class="author">Benjamin Auder (2021).</p>
--- /dev/null
+@import url("/base_pieces.css");