Fix parseInt() usage, rename Doubleorda --> Ordamirror, implement Clorange variant
[vchess.git] / client / src / variants / Wormhole.js
index 964c5e4..1f3ecd2 100644 (file)
@@ -1,10 +1,6 @@
-import { ChessRules, PiPo, Move } from "@/base_rules";
-import { ArrayFun } from "@/utils/array";
-import { randInt } from "@/utils/alea";
+import { ChessRules } from "@/base_rules";
 
-// TODO:
-
-export const VariantRules = class HiddenRules extends ChessRules {
+export class WormholeRules extends ChessRules {
   static get HasFlags() {
     return false;
   }
@@ -13,321 +9,322 @@ export const VariantRules = class HiddenRules extends ChessRules {
     return false;
   }
 
-  // Analyse in Hidden mode makes no sense
-  static get CanAnalyze() {
-    return false;
-  }
-
-  // Moves are revealed only when game ends
-  static get ShowMoves() {
-    return "none";
-  }
-
-  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 get HOLE() {
+    return "xx";
   }
 
-  // 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;
+  static board2fen(b) {
+    if (b[0] == 'x') return 'x';
+    return ChessRules.board2fen(b);
   }
 
-  static get PIECES() {
-    return ChessRules.PIECES.concat(Object.values(V.HIDDEN_CODE));
+  static fen2board(f) {
+    if (f == 'x') return V.HOLE;
+    return ChessRules.fen2board(f);
   }
 
-  // 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;
+  getPpath(b) {
+    if (b[0] == 'x') return "Wormhole/hole";
+    return b;
   }
 
-  // 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;
-          }
+  static IsGoodPosition(position) {
+    if (position.length == 0) return false;
+    const rows = position.split("/");
+    if (rows.length != V.size.x) return false;
+    let kings = { "k": 0, "K": 0 };
+    for (let row of rows) {
+      let sumElts = 0;
+      for (let i = 0; i < row.length; i++) {
+        if (['K','k'].includes(row[i])) kings[row[i]]++;
+        if (['x'].concat(V.PIECES).includes(row[i].toLowerCase())) sumElts++;
+        else {
+          const num = parseInt(row[i], 10);
+          if (isNaN(num)) return false;
+          sumElts += num;
         }
-        k++;
       }
+      if (sumElts != V.size.y) return false;
     }
+    if (Object.values(kings).some(v => v != 1)) return false;
+    return true;
   }
 
-  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]];
+  getSquareAfter(square, movement) {
+    let shift1, shift2;
+    if (Array.isArray(movement[0])) {
+      // A knight
+      shift1 = movement[0];
+      shift2 = movement[1];
+    } else {
+      shift1 = movement;
+      shift2 = null;
     }
-    // The piece is already not supposed to be hidden:
-    return b;
+    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;
   }
 
-  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)
-        })
+  // 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]
       ],
-      vanish: [
-        new PiPo({
-          x: sx,
-          y: sy,
-          c: this.getColor(sx, sy),
-          p: this.board[sx][sy].charAt(1)
-        })
+      k: [
+        [-1, 0],
+        [1, 0],
+        [0, -1],
+        [0, 1],
+        [-1, -1],
+        [-1, 1],
+        [1, -1],
+        [1, 1]
       ]
-    });
+    };
+  }
+
+  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;
+  }
 
-    // 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);
+  // 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]));
+      }
+    }
+    // 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 mv;
+    return moves;
   }
 
-  // What are the king moves from square x,y ?
-  getPotentialKingMoves(sq) {
-    // No castling:
-    return this.getSlideNJumpMoves(
-      sq,
-      V.steps[V.ROOK].concat(V.steps[V.BISHOP]),
-      "oneStep"
-    );
+  getPotentialRookMoves(sq) {
+    return this.getJumpMoves(sq, V.steps[V.ROOK]);
   }
 
-  filterValid(moves) {
-    return moves;
+  getPotentialKnightMoves(sq) {
+    return this.getJumpMoves(sq, V.steps[V.KNIGHT]);
   }
 
-  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);
+  getPotentialBishopMoves(sq) {
+    return this.getJumpMoves(sq, V.steps[V.BISHOP]);
+  }
 
-      // 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);
+  getPotentialQueenMoves(sq) {
+    return this.getJumpMoves(
+      sq,
+      V.steps[V.ROOK].concat(V.steps[V.BISHOP])
+    );
+  }
 
-      // 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);
+  getPotentialKingMoves(sq) {
+    return this.getJumpMoves(sq, V.steps[V.KING]);
+  }
 
-      // 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], color, piece, steps) {
+    for (let step of steps) {
+      const sq = this.getSquareAfter([x,y], step);
+      if (
+        sq &&
+        this.getPiece(sq[0], sq[1]) == piece &&
+        this.getColor(sq[0], sq[1]) == color
+      ) {
+        return true;
+      }
+    }
+    return false;
+  }
 
-      // Get random square for queen
-      randIndex = randInt(10);
-      const queenPos = positions[randIndex];
-      positions.splice(randIndex, 1);
+  isAttackedByPawn([x, y], color) {
+    const pawnShift = (color == "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]) == color
+      ) {
+        return true;
+      }
+    }
+    return false;
+  }
 
-      // Get random square for king
-      randIndex = randInt(9);
-      const kingPos = positions[randIndex];
-      positions.splice(randIndex, 1);
+  isAttackedByRook(sq, color) {
+    return this.isAttackedByJump(sq, color, V.ROOK, V.steps[V.ROOK]);
+  }
 
-      // Pawns position are all remaining slots:
-      for (let p of positions)
-        pieces[c][p] = "s";
+  isAttackedByKnight(sq, color) {
+    // NOTE: knight attack is not symmetric in this variant:
+    // steps order need to be reversed.
+    return this.isAttackedByJump(
+      sq,
+      color,
+      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, color) {
+    return this.isAttackedByJump(sq, color, V.BISHOP, V.steps[V.BISHOP]);
   }
 
-  getCheckSquares() {
-    return [];
+  isAttackedByQueen(sq, color) {
+    return this.isAttackedByJump(
+      sq,
+      color,
+      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, color) {
+    return this.isAttackedByJump(sq, color, 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;
   }
 };