-import { ChessRules, PiPo, Move } from "@/base_rules";
-import { ArrayFun } from "@/utils/array";
-import { randInt } from "@/utils/alea";
-
-// TODO:
+import { ChessRules } from "@/base_rules";
export const VariantRules = class WormholeRules extends ChessRules {
static get HasFlags() {
return false;
}
- // Analyse in Hidden mode makes no sense
- static get CanAnalyze() {
- return false;
+ static get HOLE() {
+ return "xx";
}
- // Moves are revealed only when game ends
- static get ShowMoves() {
- return "none";
+ static board2fen(b) {
+ if (b[0] == 'x') return 'x';
+ return ChessRules.board2fen(b);
}
- static get HIDDEN_DECODE() {
- return {
- s: "p",
- t: "q",
- u: "r",
- c: "b",
- o: "n",
- l: "k"
- };
- }
- static get HIDDEN_CODE() {
- return {
- p: "s",
- q: "t",
- r: "u",
- b: "c",
- n: "o",
- k: "l"
- };
+ static fen2board(f) {
+ if (f == 'x') return V.HOLE;
+ return ChessRules.fen2board(f);
}
- // Turn a hidden piece or revealed piece into revealed piece:
- static Decode(p) {
- if (Object.keys(V.HIDDEN_DECODE).includes(p))
- return V.HIDDEN_DECODE[p];
- return p;
+ getPpath(b) {
+ if (b[0] == 'x') return "Wormhole/hole";
+ return b;
}
- static get PIECES() {
- return ChessRules.PIECES.concat(Object.values(V.HIDDEN_CODE));
+ getSquareAfter(square, movement) {
+ let shift1, shift2;
+ if (Array.isArray(movement[0])) {
+ // A knight
+ shift1 = movement[0];
+ shift2 = movement[1];
+ } else {
+ shift1 = movement;
+ shift2 = null;
+ }
+ const tryMove = (init, shift) => {
+ let step = [
+ shift[0] / Math.abs(shift[0]) || 0,
+ shift[1] / Math.abs(shift[1]) || 0,
+ ];
+ const nbSteps = Math.max(Math.abs(shift[0]), Math.abs(shift[1]));
+ let stepsAchieved = 0;
+ let sq = [init[0] + step[0], init[1] + step[1]];
+ while (V.OnBoard(sq[0],sq[1])) {
+ if (this.board[sq[0]][sq[1]] != V.HOLE)
+ stepsAchieved++;
+ if (stepsAchieved < nbSteps) {
+ sq[0] += step[0];
+ sq[1] += step[1];
+ }
+ else break;
+ }
+ if (stepsAchieved < nbSteps)
+ // The move is impossible
+ return null;
+ return sq;
+ };
+ // First, apply shift1
+ let dest = tryMove(square, shift1);
+ if (dest && shift2)
+ // A knight: apply second shift
+ dest = tryMove(dest, shift2);
+ return dest;
}
- // Pieces can be hidden :)
- getPiece(i, j) {
- const piece = this.board[i][j].charAt(1);
- if (Object.keys(V.HIDDEN_DECODE).includes(piece))
- return V.HIDDEN_DECODE[piece];
- return piece;
+ // NOTE (TODO?): some extra work done in some function because informations
+ // on one step should ease the computation for a step in the same direction.
+ static get steps() {
+ return {
+ r: [
+ [-1, 0],
+ [1, 0],
+ [0, -1],
+ [0, 1],
+ [-2, 0],
+ [2, 0],
+ [0, -2],
+ [0, 2]
+ ],
+ // Decompose knight movements into one step orthogonal + one diagonal
+ n: [
+ [[0, -1], [-1, -1]],
+ [[0, -1], [1, -1]],
+ [[-1, 0], [-1,-1]],
+ [[-1, 0], [-1, 1]],
+ [[0, 1], [-1, 1]],
+ [[0, 1], [1, 1]],
+ [[1, 0], [1, -1]],
+ [[1, 0], [1, 1]]
+ ],
+ b: [
+ [-1, -1],
+ [-1, 1],
+ [1, -1],
+ [1, 1],
+ [-2, -2],
+ [-2, 2],
+ [2, -2],
+ [2, 2]
+ ],
+ k: [
+ [-1, 0],
+ [1, 0],
+ [0, -1],
+ [0, 1],
+ [-1, -1],
+ [-1, 1],
+ [1, -1],
+ [1, 1]
+ ]
+ };
}
- // Scan board for kings positions (no castling)
- scanKingsRooks(fen) {
- this.kingPos = { w: [-1, -1], b: [-1, -1] };
- const fenRows = V.ParseFen(fen).position.split("/");
- for (let i = 0; i < fenRows.length; i++) {
- let k = 0; //column index on board
- for (let j = 0; j < fenRows[i].length; j++) {
- switch (fenRows[i].charAt(j)) {
- case "k":
- case "l":
- this.kingPos["b"] = [i, k];
- break;
- case "K":
- case "L":
- this.kingPos["w"] = [i, k];
- break;
- default: {
- const num = parseInt(fenRows[i].charAt(j));
- if (!isNaN(num)) k += num - 1;
- }
- }
- k++;
+ getJumpMoves([x, y], steps) {
+ let moves = [];
+ for (let step of steps) {
+ const sq = this.getSquareAfter([x,y], step);
+ if (sq &&
+ (
+ this.board[sq[0]][sq[1]] == V.EMPTY ||
+ this.canTake([x, y], sq)
+ )
+ ) {
+ moves.push(this.getBasicMove([x, y], sq));
}
}
+ return moves;
}
- getPpath(b, color, score) {
- if (Object.keys(V.HIDDEN_DECODE).includes(b[1])) {
- // Supposed to be hidden.
- if (score == "*" && (!color || color != b[0]))
- return "Hidden/" + b[0] + "p";
- // Else: condition OK to show the piece
- return b[0] + V.HIDDEN_DECODE[b[1]];
+ // What are the pawn moves from square x,y ?
+ getPotentialPawnMoves([x, y]) {
+ const color = this.turn;
+ let moves = [];
+ const [sizeX, sizeY] = [V.size.x, V.size.y];
+ const shiftX = color == "w" ? -1 : 1;
+ const startRank = color == "w" ? sizeX - 2 : 1;
+ const lastRank = color == "w" ? 0 : sizeX - 1;
+
+ const sq1 = this.getSquareAfter([x,y], [shiftX,0]);
+ if (sq1 && this.board[sq1[0]][y] == V.EMPTY) {
+ // One square forward (cannot be a promotion)
+ moves.push(this.getBasicMove([x, y], [sq1[0], y]));
+ if (x == startRank) {
+ // If two squares after is available, then move is possible
+ const sq2 = this.getSquareAfter([x,y], [2*shiftX,0]);
+ if (sq2 && this.board[sq2[0]][y] == V.EMPTY)
+ // Two squares jump
+ moves.push(this.getBasicMove([x, y], [sq2[0], y]));
+ }
}
- // The piece is already not supposed to be hidden:
- return b;
+ // Captures
+ for (let shiftY of [-1, 1]) {
+ const sq = this.getSquareAfter([x,y], [shiftX,shiftY]);
+ if (
+ !!sq &&
+ this.board[sq[0]][sq[1]] != V.EMPTY &&
+ this.canTake([x, y], [sq[0], sq[1]])
+ ) {
+ const finalPieces = sq[0] == lastRank
+ ? [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN]
+ : [V.PAWN];
+ for (let piece of finalPieces) {
+ moves.push(
+ this.getBasicMove([x, y], [sq[0], sq[1]], {
+ c: color,
+ p: piece
+ })
+ );
+ }
+ }
+ }
+
+ return moves;
}
- getBasicMove([sx, sy], [ex, ey], tr) {
- if (
- tr &&
- Object.keys(V.HIDDEN_DECODE).includes(this.board[sx][sy].charAt(1))
- ) {
- // The transformed piece is a priori hidden
- tr.p = V.HIDDEN_CODE[tr.p];
- }
- let mv = new Move({
- appear: [
- new PiPo({
- x: ex,
- y: ey,
- c: tr ? tr.c : this.getColor(sx, sy),
- p: tr ? tr.p : this.board[sx][sy].charAt(1)
- })
- ],
- vanish: [
- new PiPo({
- x: sx,
- y: sy,
- c: this.getColor(sx, sy),
- p: this.board[sx][sy].charAt(1)
- })
- ]
- });
+ getPotentialRookMoves(sq) {
+ return this.getJumpMoves(sq, V.steps[V.ROOK]);
+ }
- // The opponent piece disappears if we take it
- if (this.board[ex][ey] != V.EMPTY) {
- mv.vanish.push(
- new PiPo({
- x: ex,
- y: ey,
- c: this.getColor(ex, ey),
- p: this.board[ex][ey].charAt(1)
- })
- );
- // Pieces are revealed when they capture
- mv.appear[0].p = V.Decode(mv.appear[0].p);
- }
+ getPotentialKnightMoves(sq) {
+ return this.getJumpMoves(sq, V.steps[V.KNIGHT]);
+ }
- return mv;
+ getPotentialBishopMoves(sq) {
+ return this.getJumpMoves(sq, V.steps[V.BISHOP]);
}
- // What are the king moves from square x,y ?
- getPotentialKingMoves(sq) {
- // No castling:
- return this.getSlideNJumpMoves(
+ getPotentialQueenMoves(sq) {
+ return this.getJumpMoves(
sq,
- V.steps[V.ROOK].concat(V.steps[V.BISHOP]),
- "oneStep"
+ V.steps[V.ROOK].concat(V.steps[V.BISHOP])
);
}
- filterValid(moves) {
- return moves;
+ getPotentialKingMoves(sq) {
+ return this.getJumpMoves(sq, V.steps[V.KING]);
}
- static GenRandInitFen() {
- let pieces = { w: new Array(8), b: new Array(8) };
- // Shuffle pieces + pawns on two first ranks
- for (let c of ["w", "b"]) {
- let positions = ArrayFun.range(16);
-
- // Get random squares for bishops
- let randIndex = 2 * randInt(8);
- const bishop1Pos = positions[randIndex];
- // The second bishop must be on a square of different color
- let randIndex_tmp = 2 * randInt(8) + 1;
- const bishop2Pos = positions[randIndex_tmp];
- // Remove chosen squares
- positions.splice(Math.max(randIndex, randIndex_tmp), 1);
- positions.splice(Math.min(randIndex, randIndex_tmp), 1);
-
- // Get random squares for knights
- randIndex = randInt(14);
- const knight1Pos = positions[randIndex];
- positions.splice(randIndex, 1);
- randIndex = randInt(13);
- const knight2Pos = positions[randIndex];
- positions.splice(randIndex, 1);
-
- // Get random squares for rooks
- randIndex = randInt(12);
- const rook1Pos = positions[randIndex];
- positions.splice(randIndex, 1);
- randIndex = randInt(11);
- const rook2Pos = positions[randIndex];
- positions.splice(randIndex, 1);
+ isAttackedByJump([x, y], colors, piece, steps) {
+ for (let step of steps) {
+ const sq = this.getSquareAfter([x,y], step);
+ if (
+ sq &&
+ this.getPiece(sq[0], sq[1]) === piece &&
+ colors.includes(this.getColor(sq[0], sq[1]))
+ ) {
+ return true;
+ }
+ }
+ return false;
+ }
- // Get random square for queen
- randIndex = randInt(10);
- const queenPos = positions[randIndex];
- positions.splice(randIndex, 1);
+ isAttackedByPawn([x, y], colors) {
+ for (let c of colors) {
+ const pawnShift = c == "w" ? 1 : -1;
+ for (let i of [-1, 1]) {
+ const sq = this.getSquareAfter([x,y], [pawnShift,i]);
+ if (
+ sq &&
+ this.getPiece(sq[0], sq[1]) == V.PAWN &&
+ this.getColor(sq[0], sq[1]) == c
+ ) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
- // Get random square for king
- randIndex = randInt(9);
- const kingPos = positions[randIndex];
- positions.splice(randIndex, 1);
+ isAttackedByRook(sq, colors) {
+ return this.isAttackedByJump(sq, colors, V.ROOK, V.steps[V.ROOK]);
+ }
- // Pawns position are all remaining slots:
- for (let p of positions)
- pieces[c][p] = "s";
+ isAttackedByKnight(sq, colors) {
+ // NOTE: knight attack is not symmetric in this variant:
+ // steps order need to be reversed.
+ return this.isAttackedByJump(
+ sq,
+ colors,
+ V.KNIGHT,
+ V.steps[V.KNIGHT].map(s => s.reverse())
+ );
+ }
- // Finally put the shuffled pieces in the board array
- pieces[c][rook1Pos] = "u";
- pieces[c][knight1Pos] = "o";
- pieces[c][bishop1Pos] = "c";
- pieces[c][queenPos] = "t";
- pieces[c][kingPos] = "l";
- pieces[c][bishop2Pos] = "c";
- pieces[c][knight2Pos] = "o";
- pieces[c][rook2Pos] = "u";
- }
- let upFen = pieces["b"].join("");
- upFen = upFen.substr(0,8) + "/" + upFen.substr(8).split("").reverse().join("");
- let downFen = pieces["b"].join("").toUpperCase();
- downFen = downFen.substr(0,8) + "/" + downFen.substr(8).split("").reverse().join("");
- return upFen + "/8/8/8/8/" + downFen + " w 0";
+ isAttackedByBishop(sq, colors) {
+ return this.isAttackedByJump(sq, colors, V.BISHOP, V.steps[V.BISHOP]);
}
- getCheckSquares() {
- return [];
+ isAttackedByQueen(sq, colors) {
+ return this.isAttackedByJump(
+ sq,
+ colors,
+ V.QUEEN,
+ V.steps[V.ROOK].concat(V.steps[V.BISHOP])
+ );
}
- updateVariables(move) {
- super.updateVariables(move);
- if (
- move.vanish.length >= 2 &&
- [V.KING,V.HIDDEN_CODE[V.KING]].includes(move.vanish[1].p)
- ) {
- // We took opponent king
- this.kingPos[this.turn] = [-1, -1];
- }
+ isAttackedByKing(sq, colors) {
+ return this.isAttackedByJump(sq, colors, V.KING, V.steps[V.KING]);
}
- unupdateVariables(move) {
- super.unupdateVariables(move);
- const c = move.vanish[0].c;
- const oppCol = V.GetOppCol(c);
- if (this.kingPos[oppCol][0] < 0)
- // Last move took opponent's king:
- this.kingPos[oppCol] = [move.vanish[1].x, move.vanish[1].y];
+ // NOTE: altering move in getBasicMove doesn't work and wouldn't be logical.
+ // This is a side-effect on board generated by the move.
+ static PlayOnBoard(board, move) {
+ board[move.vanish[0].x][move.vanish[0].y] = V.HOLE;
+ for (let psq of move.appear) board[psq.x][psq.y] = psq.c + psq.p;
}
getCurrentScore() {
- const color = this.turn;
- const kp = this.kingPos[color];
- if (kp[0] < 0)
- // King disappeared
- return color == "w" ? "0-1" : "1-0";
- // Assume that stalemate is impossible:
- return "*";
+ if (this.atLeastOneMove())
+ return "*";
+ // No valid move: I lose
+ return this.turn == "w" ? "0-1" : "1-0";
}
- getComputerMove() {
- const color = this.turn;
- let moves = this.getAllValidMoves();
- for (let move of moves) {
- move.eval = 0; //a priori...
+ static get SEARCH_DEPTH() {
+ return 2;
+ }
- // Can I take something ? If yes, do it with some probability
- if (move.vanish.length == 2 && move.vanish[1].c != color) {
- // OK this isn't a castling move
- const myPieceVal = V.VALUES[move.appear[0].p];
- const hisPieceVal = Object.keys(V.HIDDEN_DECODE).includes(move.vanish[1].p)
- ? undefined
- : V.VALUES[move.vanish[1].p];
- if (!hisPieceVal) {
- // Opponent's piece is unknown: do not take too much risk
- move.eval = -myPieceVal + 1.5; //so that pawns always take
+ evalPosition() {
+ let evaluation = 0;
+ for (let i = 0; i < V.size.x; i++) {
+ for (let j = 0; j < V.size.y; j++) {
+ if (![V.EMPTY,V.HOLE].includes(this.board[i][j])) {
+ const sign = this.getColor(i, j) == "w" ? 1 : -1;
+ evaluation += sign * V.VALUES[this.getPiece(i, j)];
}
- // Favor captures
- else if (myPieceVal <= hisPieceVal)
- move.eval = hisPieceVal - myPieceVal + 1;
- else {
- // Taking a pawn with minor piece,
- // or minor piece or pawn with a rook,
- // or anything but a queen with a queen,
- // or anything with a king.
- move.eval = hisPieceVal - myPieceVal;
- }
- } else {
- // If no capture, favor small step moves,
- // but sometimes move the knight anyway
- const penalty = V.Decode(move.vanish[0].p) != V.KNIGHT
- ? Math.abs(move.end.x - move.start.x) + Math.abs(move.end.y - move.start.y)
- : (Math.random() < 0.5 ? 3 : 1);
- move.eval -= penalty / (V.size.x + V.size.y - 1);
}
-
- // TODO: also favor movements toward the center?
}
-
- moves.sort((a, b) => b.eval - a.eval);
- let candidates = [0];
- for (let j = 1; j < moves.length && moves[j].eval == moves[0].eval; j++)
- candidates.push(j);
- return moves[candidates[randInt(candidates.length)]];
+ return evaluation;
}
getNotation(move) {
- // Translate final square
- const finalSquare = V.CoordsToSquare(move.end);
-
const piece = this.getPiece(move.start.x, move.start.y);
- if (piece == V.PAWN) {
- // Pawn move
- let notation = "";
- if (move.vanish.length > move.appear.length) {
- // Capture
- const startColumn = V.CoordToColumn(move.start.y);
- notation = startColumn + "x" + finalSquare;
- }
- else notation = finalSquare;
- if (move.appear.length > 0 && !["p","s"].includes(move.appear[0].p)) {
- // Promotion
- const appearPiece = V.Decode(move.appear[0].p);
- notation += "=" + appearPiece.toUpperCase();
- }
- return notation;
- }
- // Piece movement
- return (
- piece.toUpperCase() +
+ // Indicate start square + dest square, because holes distort the board
+ let notation =
+ (piece != V.PAWN ? piece.toUpperCase() : "") +
+ V.CoordsToSquare(move.start) +
(move.vanish.length > move.appear.length ? "x" : "") +
- finalSquare
- );
+ V.CoordsToSquare(move.end);
+ if (piece == V.PAWN && move.appear[0].p != V.PAWN)
+ // Promotion
+ notation += "=" + move.appear[0].p.toUpperCase();
+ return notation;
}
};