A few small fixes + add Monster variant
[vchess.git] / client / src / variants / Monster.js
diff --git a/client/src/variants/Monster.js b/client/src/variants/Monster.js
new file mode 100644 (file)
index 0000000..177f71d
--- /dev/null
@@ -0,0 +1,186 @@
+import { ChessRules } from "@/base_rules";
+import { randInt } from "@/utils/alea";
+
+export class MonsterRules extends ChessRules {
+  static IsGoodFlags(flags) {
+    // Only black can castle
+    return !!flags.match(/^[a-z]{2,2}$/);
+  }
+
+  static GenRandInitFen(randomness) {
+    if (randomness == 2) randomness--;
+    const fen = ChessRules.GenRandInitFen(randomness);
+    return (
+      // 26 first chars are 6 rows + 6 slashes
+      fen.substr(0, 26)
+      // En passant available, and "half-castle"
+      .concat("2PPPP2/4K3 w 0 ")
+      .concat(fen.substr(-6, 2))
+      .concat(" -")
+    );
+  }
+
+  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));
+  }
+
+  setOtherVariables(fen) {
+    super.setOtherVariables(fen);
+    this.subTurn = 1;
+  }
+
+  getPotentialKingMoves([x, y]) {
+    if (this.getColor(x, y) == 'b') return super.getPotentialKingMoves([x, y]);
+    // White doesn't castle:
+    return this.getSlideNJumpMoves(
+      [x, y],
+      V.steps[V.ROOK].concat(V.steps[V.BISHOP]),
+      "oneStep"
+    );
+  }
+
+  isAttacked(sq, color, castling) {
+    const singleMoveAttack = super.isAttacked(sq, color);
+    if (singleMoveAttack) return true;
+    if (color == 'b' || !!castling) return singleMoveAttack;
+    // Attacks by white: double-move allowed
+    const curTurn = this.turn;
+    this.turn = 'w';
+    const w1Moves = super.getAllPotentialMoves();
+    this.turn = curTurn;
+    for (let move of w1Moves) {
+      this.play(move);
+      const res = super.isAttacked(sq, 'w');
+      this.undo(move);
+      if (res) return res;
+    }
+    return false;
+  }
+
+  play(move) {
+    move.flags = JSON.stringify(this.aggregateFlags());
+    if (this.turn == 'b' || this.subTurn == 2)
+      this.epSquares.push(this.getEpSquare(move));
+    else this.epSquares.push(null);
+    V.PlayOnBoard(this.board, move);
+    if (this.turn == 'w') {
+      if (this.subTurn == 1) this.movesCount++;
+      else this.turn = 'b';
+      this.subTurn = 3 - this.subTurn;
+    } else {
+      this.turn = 'w';
+      this.movesCount++;
+    }
+    this.postPlay(move);
+  }
+
+  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;
+    }
+  }
+
+  postPlay(move) {
+    // Definition of 'c' in base class doesn't work:
+    const c = move.vanish[0].c;
+    const piece = move.vanish[0].p;
+    if (piece == V.KING && move.appear.length > 0) {
+      this.kingPos[c][0] = move.appear[0].x;
+      this.kingPos[c][1] = move.appear[0].y;
+      return;
+    }
+    this.updateCastleFlags(move, piece);
+  }
+
+  undo(move) {
+    this.epSquares.pop();
+    this.disaggregateFlags(JSON.parse(move.flags));
+    V.UndoOnBoard(this.board, move);
+    if (this.turn == 'w') {
+      if (this.subTurn == 2) this.subTurn = 1;
+      else this.turn = 'b';
+      this.movesCount--;
+    } else {
+      this.turn = 'w';
+      this.subTurn = 2;
+    }
+    this.postUndo(move);
+  }
+
+  filterValid(moves) {
+    if (this.turn == 'w' && this.subTurn == 1) {
+      return moves.filter(m1 => {
+        this.play(m1);
+        // NOTE: no recursion because next call will see subTurn == 2
+        const res = super.atLeastOneMove();
+        this.undo(m1);
+        return res;
+      });
+    }
+    return super.filterValid(moves);
+  }
+
+  static get SEARCH_DEPTH() {
+    return 1;
+  }
+
+  getComputerMove() {
+    const color = this.turn;
+    if (color == 'w') {
+      // Generate all sequences of 2-moves
+      const moves1 = this.getAllValidMoves();
+      moves1.forEach(m1 => {
+        m1.eval = -V.INFINITY;
+        m1.move2 = null;
+        this.play(m1);
+        const moves2 = this.getAllValidMoves();
+        moves2.forEach(m2 => {
+          this.play(m2);
+          const eval2 = this.evalPosition();
+          this.undo(m2);
+          if (eval2 > m1.eval) {
+            m1.eval = eval2;
+            m1.move2 = m2;
+          }
+        });
+        this.undo(m1);
+      });
+      moves1.sort((a, b) => b.eval - a.eval);
+      let candidates = [0];
+      for (
+        let i = 1;
+        i < moves1.length && moves1[i].eval == moves1[0].eval;
+        i++
+      ) {
+        candidates.push(i);
+      }
+      const idx = candidates[randInt(candidates.length)];
+      const move2 = moves1[idx].move2;
+      delete moves1[idx]["move2"];
+      return [moves1[idx], move2];
+    }
+    // For black at depth 1, super method is fine:
+    return super.getComputerMove();
+  }
+};