Get rid of livereload dependency. Draft Baroque (still some issues)
[xogo.git] / variants / Baroque / class.js
diff --git a/variants/Baroque/class.js b/variants/Baroque/class.js
new file mode 100644 (file)
index 0000000..62904e8
--- /dev/null
@@ -0,0 +1,402 @@
+import ChessRules from "/base_rules.js";
+import GiveawayRules from "/variants/Giveaway/class.js";
+import {Random} from "/utils/alea.js";
+import PiPo from "/utils/PiPo.js";
+import Move from "/utils/Move.js";
+
+export default class BaroqueRules extends ChessRules {
+
+  static get Options() {
+    return {
+      select: C.Options.Select,
+      input: [
+        {
+          label: "Capture king",
+          variable: "taking",
+          type: "checkbox",
+          defaut: false
+        }
+      ],
+      styles: [
+        "balance",
+        "capture",
+        "crazyhouse",
+        "cylinder",
+        "doublemove",
+        "progressive",
+        "recycle",
+        "teleport"
+      ]
+    };
+  }
+
+  get hasFlags() {
+    return false;
+  }
+  get hasEnpassant() {
+    return false;
+  }
+
+  genRandInitBaseFen() {
+    if (this.options["randomness"] == 0)
+      return "rnbkqbnm/pppppppp/8/8/8/8/PPPPPPPP/MNBQKBNR";
+    const options = Object.assign({mode: "suicide"}, this.options);
+    const gr = new GiveawayRules({options: options, genFenOnly: true});
+    let res = gr.genRandInitBaseFen();
+    let immPos = {};
+    for (let c of ['w', 'b']) {
+      const rookChar = (c == 'w' ? 'R' : 'r');
+      switch (Random.randInt(2)) {
+        case 0:
+          immPos[c] = res.fen.indexOf(rookChar);
+          break;
+        case 1:
+          immPos[c] = res.fen.lastIndexOf(rookChar);
+          break;
+      }
+    }
+    res.fen = res.fen.substring(0, immPos['b']) + 'i' +
+              res.fen.substring(immPos['b'] + 1, immPos['w']) + 'I' +
+              res.fen.substring(immPos['w'] + 1);
+    return res;
+  }
+
+  // Although other pieces keep their names here for coding simplicity,
+  // keep in mind that:
+  //  - a "rook" is a coordinator, capturing by coordinating with the king
+  //  - a "knight" is a long-leaper, capturing as in draughts
+  //  - a "bishop" is a chameleon, capturing as its prey
+  //  - a "queen" is a withdrawer, capturing by moving away from pieces
+
+  pieces() {
+    return Object.assign({},
+      super.pieces(),
+      {
+        'p': {
+          "class": "pawn",
+          moves: [
+            {steps: [[0, 1], [0, -1], [1, 0], [-1, 0]]}
+          ]
+        },
+        'r': {
+          "class": "rook",
+          moves: [
+            {
+              steps: [
+                [1, 0], [0, 1], [-1, 0], [0, -1],
+                [1, 1], [1, -1], [-1, 1], [-1, -1]
+              ]
+            }
+          ]
+        },
+        'n': {
+          "class": "knight",
+          moveas: 'r'
+        },
+        'b': {
+          "class": "bishop",
+          moveas: 'r'
+        },
+        'q': {
+          "class": "queen",
+          moveas: 'r'
+        },
+        'i': {
+          "class": "immobilizer",
+          moveas: 'q'
+        }
+      }
+    );
+  }
+
+  // Is piece on square (x,y) immobilized?
+  isImmobilized([x, y]) {
+    const piece = this.getPiece(x, y);
+    const color = this.getColor(x, y);
+    const oppCol = C.GetOppCol(color);
+    const adjacentSteps = this.pieces()['k'].moves[0].steps;
+    for (let step of adjacentSteps) {
+      const [i, j] = [x + step[0], this.getY(y + step[1])];
+      if (
+        this.onBoard(i, j) &&
+        this.board[i][j] != "" &&
+        this.getColor(i, j) == oppCol
+      ) {
+        const oppPiece = this.getPiece(i, j);
+        if (oppPiece == 'i') {
+          // Moving is possible only if this immobilizer is neutralized
+          for (let step2 of adjacentSteps) {
+            const [i2, j2] = [i + step2[0], this.getY(j + step2[1])];
+            if (i2 == x && j2 == y)
+              continue; //skip initial piece!
+            if (
+              this.onBoard(i2, j2) &&
+              this.board[i2][j2] != "" &&
+              this.getColor(i2, j2) == color
+            ) {
+              if (['b', 'i'].includes(this.getPiece(i2, j2)))
+                return false;
+            }
+          }
+          return true; //immobilizer isn't neutralized
+        }
+        // Chameleons can't be immobilized twice,
+        // because there is only one immobilizer
+        if (oppPiece == 'b' && piece == 'i')
+          return true;
+      }
+    }
+    return false;
+  }
+
+  canTake([x1, y1], [x2, y2]) {
+    // Deactivate standard captures, except for king:
+    return (
+      this.getPiece(x1, y1) == 'k' &&
+      this.getColor(x1, y1) != this.getColor(x2, y2)
+    );
+  }
+
+  postProcessPotentialMoves(moves) {
+    if (moves.length == 0)
+      return [];
+    switch (moves[0].vanish[0].p) {
+      case 'p':
+        this.addPawnCaptures(moves);
+        break;
+      case 'r':
+        this.addRookCaptures(moves);
+        break;
+      case 'n':
+        const [x, y] = [moves[0].start.x, moves[0].start.y];
+        moves = moves.concat(this.getKnightCaptures([x, y]));
+        break;
+      case 'b':
+        moves = this.getBishopCaptures(moves);
+        break;
+      case 'q':
+        this.addPawnCaptures(moves);
+        break;
+    }
+    return moves;
+  }
+
+  // Modify capturing moves among listed pawn moves
+  addPawnCaptures(moves, byChameleon) {
+    const steps = this.pieces()['p'].moves[0].steps;
+    const color = this.turn;
+    const oppCol = C.GetOppCol(color);
+    moves.forEach(m => {
+      if (byChameleon && m.start.x != m.end.x && m.start.y != m.end.y)
+        // Chameleon not moving as pawn
+        return;
+      // Try capturing in every direction
+      for (let step of steps) {
+        const sq2 = [m.end.x + 2 * step[0], this.getY(m.end.y + 2 * step[1])];
+        if (
+          this.onBoard(sq2[0], sq2[1]) &&
+          this.board[sq2[0]][sq2[1]] != "" &&
+          this.getColor(sq2[0], sq2[1]) == color
+        ) {
+          // Potential capture
+          const sq1 = [m.end.x + step[0], this.getY(m.end.y + step[1])];
+          if (
+            this.board[sq1[0]][sq1[1]] != "" &&
+            this.getColor(sq1[0], sq1[1]) == oppCol
+          ) {
+            const piece1 = this.getPiece(sq1[0], sq1[1]);
+            if (!byChameleon || piece1 == 'p') {
+              m.vanish.push(
+                new PiPo({
+                  x: sq1[0],
+                  y: sq1[1],
+                  c: oppCol,
+                  p: piece1
+                })
+              );
+            }
+          }
+        }
+      }
+    });
+  }
+
+  addRookCaptures(moves, byChameleon) {
+    const color = this.turn;
+    const oppCol = V.GetOppCol(color);
+    const kp = this.searchKingPos(color)[0];
+    moves.forEach(m => {
+      // Check piece-king rectangle (if any) corners for enemy pieces
+      if (m.end.x == kp[0] || m.end.y == kp[1])
+        return; //"flat rectangle"
+      const corner1 = [m.end.x, kp[1]];
+      const corner2 = [kp[0], m.end.y];
+      for (let [i, j] of [corner1, corner2]) {
+        if (this.board[i][j] != "" && this.getColor(i, j) == oppCol) {
+          const piece = this.getPiece(i, j);
+          if (!byChameleon || piece == 'r') {
+            m.vanish.push(
+              new PiPo({
+                x: i,
+                y: j,
+                p: piece,
+                c: oppCol
+              })
+            );
+          }
+        }
+      }
+    });
+  }
+
+  getKnightCaptures(startSquare, byChameleon) {
+    // Look in every direction for captures
+    const steps = this.pieces()['r'].moves[0].steps;
+    const color = this.turn;
+    const oppCol = C.GetOppCol(color);
+    let moves = [];
+    const [x, y] = [startSquare[0], startSquare[1]];
+    const piece = this.getPiece(x, y); //might be a chameleon!
+    outerLoop: for (let step of steps) {
+      let [i, j] = [x + step[0], this.getY(y + step[1])];
+      while (this.onBoard(i, j) && this.board[i][j] == "")
+        [i, j] = [i + step[0], this.getY(j + step[1])];
+      if (
+        !this.onBoard(i, j) ||
+        this.getColor(i, j) == color ||
+        (byChameleon && this.getPiece(i, j) != 'n')
+      ) {
+        continue;
+      }
+      // last(thing), cur(thing) : stop if "cur" is our color,
+      // or beyond board limits, or if "last" isn't empty and cur neither.
+      // Otherwise, if cur is empty then add move until cur square;
+      // if cur is occupied then stop if !!byChameleon and the square not
+      // occupied by a leaper.
+      let last = [i, j];
+      let cur = [i + step[0], this.getY(j + step[1])];
+      let vanished = [new PiPo({x: x, y: y, c: color, p: piece})];
+      while (this.onBoard(cur[0], cur[1])) {
+        if (this.board[last[0]][last[1]] != "") {
+          const oppPiece = this.getPiece(last[0], last[1]);
+          if (!!byChameleon && oppPiece != 'n')
+            continue outerLoop;
+          // Something to eat:
+          vanished.push(
+            new PiPo({x: last[0], y: last[1], c: oppCol, p: oppPiece})
+          );
+        }
+        if (this.board[cur[0]][cur[1]] != "") {
+          if (
+            this.getColor(cur[0], cur[1]) == color ||
+            this.board[last[0]][last[1]] != ""
+          ) {
+            //TODO: redundant test
+            continue outerLoop;
+          }
+        }
+        else {
+          moves.push(
+            new Move({
+              appear: [new PiPo({x: cur[0], y: cur[1], c: color, p: piece})],
+              vanish: JSON.parse(JSON.stringify(vanished)), //TODO: required?
+              start: {x: x, y: y},
+              end: {x: cur[0], y: cur[1]}
+            })
+          );
+        }
+        last = [last[0] + step[0], this.getY(last[1] + step[1])];
+        cur = [cur[0] + step[0], this.getY(cur[1] + step[1])];
+      }
+    }
+    return moves;
+  }
+
+  // Chameleon
+  getBishopCaptures(moves) {
+    const [x, y] = [moves[0].start.x, moves[0].start.y];
+    moves = moves.concat(this.getKnightCaptures([x, y], "asChameleon"));
+    // No "king capture" because king cannot remain under check
+    this.addPawnCaptures(moves, "asChameleon");
+    this.addRookCaptures(moves, "asChameleon");
+    this.addQueenCaptures(moves, "asChameleon");
+    // Post-processing: merge similar moves, concatenating vanish arrays
+    let mergedMoves = {};
+    moves.forEach(m => {
+      const key = m.end.x + this.size.x * m.end.y;
+      if (!mergedMoves[key])
+        mergedMoves[key] = m;
+      else {
+        for (let i = 1; i < m.vanish.length; i++)
+          mergedMoves[key].vanish.push(m.vanish[i]);
+      }
+    });
+    return Object.values(mergedMoves);
+  }
+
+  addQueenCaptures(moves, byChameleon) {
+    if (moves.length == 0) return;
+    const [x, y] = [moves[0].start.x, moves[0].start.y];
+    const adjacentSteps = this.pieces()['r'].moves[0].steps;
+    let capturingDirections = [];
+    const color = this.turn;
+    const oppCol = C.GetOppCol(color);
+    adjacentSteps.forEach(step => {
+      const [i, j] = [x + step[0], this.getY(y + step[1])];
+      if (
+        this.onBoard(i, j) &&
+        this.board[i][j] != "" &&
+        this.getColor(i, j) == oppCol &&
+        (!byChameleon || this.getPiece(i, j) == 'q')
+      ) {
+        capturingDirections.push(step);
+      }
+    });
+    moves.forEach(m => {
+      const step = [
+        m.end.x != x ? (m.end.x - x) / Math.abs(m.end.x - x) : 0,
+        m.end.y != y ? (m.end.y - y) / Math.abs(m.end.y - y) : 0
+      ];
+      // NOTE: includes() and even _.isEqual() functions fail...
+      // TODO: this test should be done only once per direction
+      if (
+        capturingDirections.some(dir => {
+          return dir[0] == -step[0] && dir[1] == -step[1];
+        })
+      ) {
+        const [i, j] = [x - step[0], this.getY(y - step[1])];
+        m.vanish.push(
+          new PiPo({
+            x: i,
+            y: j,
+            p: this.getPiece(i, j),
+            c: oppCol
+          })
+        );
+      }
+    });
+  }
+
+  underAttack([x, y], oppCol) {
+    // Generate all potential opponent moves, check if king captured.
+    // TODO: do it more efficiently.
+    const color = this.getColor(x, y);
+    for (let i = 0; i < this.size.x; i++) {
+      for (let j = 0; j < this.size.y; j++) {
+        if (
+          this.board[i][j] != "" && this.getColor(i, j) == oppCol &&
+          this.getPotentialMovesFrom([i, j]).some(m => {
+            return (
+              m.vanish.length >= 2 &&
+              [1, m.vanish.length - 1].some(k => m.vanish[k].p == 'k')
+            );
+          })
+        ) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+};