First draft of Emergo - still buggish. Fix Fanorona, avoid underscore in FEN and...
[vchess.git] / client / src / variants / Emergo.js
index 264186b..caa2e98 100644 (file)
-import { ChessRules } from "@/base_rules";
+import { ChessRules, Move, PiPo } from "@/base_rules";
+import { randInt } from "@/utils/alea";
+import { ArrayFun } from "@/utils/array";
 
-export class YoteRules extends ChessRules {
+export class EmergoRules extends ChessRules {
 
-  // TODO
-  //If (as white) a pile W1/B1 jumps over another pile W2/B2, it lets on the intermediate square exactly W2 men, to end as W1/(B1+B2).
-  //In the first case in the video, W1=1, B1=0, W2=0, B2=1 ==> 1/1 and finally 1/2 with nothing on intermediate squares since W2 is always 0.
-  //In the second case, W1=1, B1=0, W2=1, B2=1 ==> 1 man left on intermediate square, end as 1/1.
-  //...I think it's that (?). Not very well explained either on Wikipedia or mindsports.nl :/
-  //Found this link: http://www.iggamecenter.com/info/en/emergo.html - so it's all clear now ! I'll add the game soon.
-  //Btw, I'm not a big fan of this naming "men" for pieces, but, won't contradict the author on that 
+  // 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 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(randomness) {
+    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: { [V.PAWN]: reserve[0] },
+      b: { [V.PAWN]: reserve[1] }
+    };
+    // Local stack of captures during a turn (squares + directions)
+    this.captures = [ [] ];
+  }
+
+  atLeastOneCaptureFrom([x, y], color) {
+    for (let s of V.steps[V.BISHOP]) {
+      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);
+    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;
+  };
+
+  getLongestCapturesFrom([x, y], color, locSteps) {
+    //
+    // TODO: debug here, from
+    // 9/9/2a@1a@4/5A@3/9/3aa1A@3/9/9/8A@ w 10 8,9
+    // White to move, double capture.
+    //
+    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 sRes = this.getLongestCapturesFrom(
+                       [i + s[0], j + s[1]], color, locSteps);
+        res.push({
+          step: s,
+          length: 1 + (sRes.length == 0 ? 0 : sRes[0].length)
+        });
+        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;
+    if (L > 0) {
+      let locSteps = [];
+      const caps = Object.assign(
+        { square: captures[L-1].square },
+        this.getLongestCapturesFrom(captures[L-1].square, color, locSteps)
+      );
+      return this.maxLengthIndices(caps).map(i => caps[i]);
+    }
+    let caps = [];
+    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))
+          );
+        }
+      }
+    }
+
+console.log(caps);
+
+    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 cpCapt = this.board[capt[0]][capt[1]];
+    const newAtCapt = cpCapt.charCodeAt(0) - 1;
+    const newAtDest =
+      cp1[1] == '@'
+        ? (cp1.charCodeAt(0) < 97 ? 65 : 97)
+        : (cp1.charCodeAt(1) + 1);
+    const color = this.turn;
+    let mv = new Move({
+      appear: [
+        new PiPo({
+          x: x2,
+          y: y2,
+          c: cp1[0],
+          p: String.fromCharCode(newAtDest)
+        })
+      ],
+      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 ([64, 96].includes(newAtCapt)) {
+      // Enemy units vanish from capturing square
+      if (cpCapt.charAt(1) != '@') {
+        // Out units remain:
+        mv.appear.push(
+          new PiPo({
+            x: capt[0],
+            y: capt[1],
+            c: cpCapt[0],
+            p: '@'
+          })
+        );
+      }
+    }
+    else {
+      mv.appear.push(
+        new PiPo({
+          x: capt[0],
+          y: capt[1],
+          c: String.fromCharCode(newAtCapt),
+          p: 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;
+    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.start.x, move.start.y],
+        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;
+    }
+    else if (move.vanish == 0) {
+      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');
+      if (!this.reserve[color]) this.reserve[color] = { [V.PAWN]: 1 };
+      else this.reserve[color][V.PAWN]++;
+    }
+    else if (move.vanish.length == 2) {
+      const L0 = this.captures.length;
+      let captures = this.captures[L0 - 1];
+      captures.pop();
+    }
+  }
+
+  atLeastOneMove() {
+    if (this.atLeastOneCapture()) return true;
+    const color = this.turn;
+    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] &&
+      this.board.every(b => {
+        return b.every(cell => {
+          return (cell == "" || cell[0] != color);
+        });
+      })
+    ) {
+      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);
+    return V.CoordsToSquare(move.start) + V.CoordsToSquare(move.end);
+  }
 
 };