Change castle flags. Eightpieces still not OK, but almost
[vchess.git] / client / src / base_rules.js
index 8b49436..890601d 100644 (file)
@@ -37,6 +37,11 @@ export const ChessRules = class ChessRules {
     return true;
   }
 
+  // Or castle
+  static get HasCastle() {
+    return V.HasFlags;
+  }
+
   // Some variants don't have en-passant
   static get HasEnpassant() {
     return true;
@@ -134,7 +139,8 @@ export const ChessRules = class ChessRules {
 
   // For FEN checking
   static IsGoodFlags(flags) {
-    return !!flags.match(/^[01]{4,4}$/);
+    // NOTE: a little too permissive to work with more variants
+    return !!flags.match(/^[a-z]{4,4}$/);
   }
 
   static IsGoodEnpassant(enpassant) {
@@ -175,6 +181,11 @@ export const ChessRules = class ChessRules {
     return b; //usual pieces in pieces/ folder
   }
 
+  // Path to promotion pieces (usually the same)
+  getPPpath(b) {
+    return this.getPpath(b);
+  }
+
   // Aggregates flags into one object
   aggregateFlags() {
     return this.castleFlags;
@@ -197,13 +208,10 @@ export const ChessRules = class ChessRules {
     const move = moveOrSquare;
     const s = move.start,
           e = move.end;
-    // NOTE: next conditions are first for Atomic, and last for Checkered
     if (
-      move.appear.length > 0 &&
       Math.abs(s.x - e.x) == 2 &&
       s.y == e.y &&
-      move.appear[0].p == V.PAWN &&
-      ["w", "b"].includes(move.appear[0].c)
+      move.appear[0].p == V.PAWN
     ) {
       return {
         x: (s.x + e.x) / 2,
@@ -238,11 +246,22 @@ export const ChessRules = class ChessRules {
   /////////////
   // FEN UTILS
 
-  // Setup the initial random (assymetric) position
-  static GenRandInitFen() {
+  // Setup the initial random (asymmetric) position
+  static GenRandInitFen(randomness) {
+    if (randomness == 0)
+      // Deterministic:
+      return "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w 0 ahah -";
+
     let pieces = { w: new Array(8), b: new Array(8) };
-    // Shuffle pieces on first and last rank
+    let flags = "";
+    // Shuffle pieces on first (and last rank if randomness == 2)
     for (let c of ["w", "b"]) {
+      if (c == 'b' && randomness == 1) {
+        pieces['b'] = pieces['w'];
+        flags += flags;
+        break;
+      }
+
       let positions = ArrayFun.range(8);
 
       // Get random squares for bishops
@@ -283,13 +302,14 @@ export const ChessRules = class ChessRules {
       pieces[c][bishop2Pos] = "b";
       pieces[c][knight2Pos] = "n";
       pieces[c][rook2Pos] = "r";
+      flags += V.CoordToColumn(rook1Pos) + V.CoordToColumn(rook2Pos);
     }
     // Add turn + flags + enpassant
     return (
       pieces["b"].join("") +
       "/pppppppp/8/8/8/8/PPPPPPPP/" +
       pieces["w"].join("").toUpperCase() +
-      " w 0 1111 -"
+      " w 0 " + flags + " -"
     );
   }
 
@@ -310,16 +330,24 @@ export const ChessRules = class ChessRules {
   // Return current fen (game state)
   getFen() {
     return (
-      this.getBaseFen() +
-      " " +
-      this.getTurnFen() +
-      " " +
+      this.getBaseFen() + " " +
+      this.getTurnFen() + " " +
       this.movesCount +
       (V.HasFlags ? " " + this.getFlagsFen() : "") +
       (V.HasEnpassant ? " " + this.getEnpassantFen() : "")
     );
   }
 
+  getFenForRepeat() {
+    // Omit movesCount, only variable allowed to differ
+    return (
+      this.getBaseFen() + "_" +
+      this.getTurnFen() +
+      (V.HasFlags ? "_" + this.getFlagsFen() : "") +
+      (V.HasEnpassant ? "_" + this.getEnpassantFen() : "")
+    );
+  }
+
   // Position part of the FEN string
   getBaseFen() {
     let position = "";
@@ -352,10 +380,9 @@ export const ChessRules = class ChessRules {
   // Flags part of the FEN string
   getFlagsFen() {
     let flags = "";
-    // Add castling flags
-    for (let i of ["w", "b"]) {
-      for (let j = 0; j < 2; j++) flags += this.castleFlags[i][j] ? "1" : "0";
-    }
+    // Castling flags
+    for (let c of ["w", "b"])
+      flags += this.castleFlags[c].map(V.CoordToColumn).join("");
     return flags;
   }
 
@@ -388,21 +415,21 @@ export const ChessRules = class ChessRules {
   setFlags(fenflags) {
     // white a-castle, h-castle, black a-castle, h-castle
     this.castleFlags = { w: [true, true], b: [true, true] };
-    for (let i = 0; i < 4; i++)
-      this.castleFlags[i < 2 ? "w" : "b"][i % 2] = fenflags.charAt(i) == "1";
+    for (let i = 0; i < 4; i++) {
+      this.castleFlags[i < 2 ? "w" : "b"][i % 2] =
+        V.ColumnToCoord(fenflags.charAt(i));
+    }
   }
 
   //////////////////
   // INITIALIZATION
 
-  constructor(fen) {
-    // In printDiagram() fen isn't supply because only getPpath() is used
-    if (fen)
-      this.re_init(fen);
-  }
-
   // Fen string fully describes the game state
-  re_init(fen) {
+  constructor(fen) {
+    if (!fen)
+      // In printDiagram() fen isn't supply because only getPpath() is used
+      // TODO: find a better solution!
+      return;
     const fenParsed = V.ParseFen(fen);
     this.board = V.GetBoard(fenParsed.position);
     this.turn = fenParsed.turn[0]; //[0] to work with MarseilleRules
@@ -410,12 +437,12 @@ export const ChessRules = class ChessRules {
     this.setOtherVariables(fen);
   }
 
-  // Scan board for kings and rooks positions
-  scanKingsRooks(fen) {
+  // Scan board for kings positions
+  scanKings(fen) {
     this.INIT_COL_KING = { w: -1, b: -1 };
-    this.INIT_COL_ROOK = { w: [-1, -1], b: [-1, -1] };
     this.kingPos = { w: [-1, -1], b: [-1, -1] }; //squares of white and black king
     const fenRows = V.ParseFen(fen).position.split("/");
+    const startRow = { 'w': V.size.x - 1, 'b': 0 };
     for (let i = 0; i < fenRows.length; i++) {
       let k = 0; //column index on board
       for (let j = 0; j < fenRows[i].length; j++) {
@@ -428,14 +455,6 @@ export const ChessRules = class ChessRules {
             this.kingPos["w"] = [i, k];
             this.INIT_COL_KING["w"] = k;
             break;
-          case "r":
-            if (this.INIT_COL_ROOK["b"][0] < 0) this.INIT_COL_ROOK["b"][0] = k;
-            else this.INIT_COL_ROOK["b"][1] = k;
-            break;
-          case "R":
-            if (this.INIT_COL_ROOK["w"][0] < 0) this.INIT_COL_ROOK["w"][0] = k;
-            else this.INIT_COL_ROOK["w"][1] = k;
-            break;
           default: {
             const num = parseInt(fenRows[i].charAt(j));
             if (!isNaN(num)) k += num - 1;
@@ -458,8 +477,8 @@ export const ChessRules = class ChessRules {
           : undefined;
       this.epSquares = [epSq];
     }
-    // Search for king and rooks positions:
-    this.scanKingsRooks(fen);
+    // Search for kings positions:
+    this.scanKings(fen);
   }
 
   /////////////////////
@@ -629,7 +648,6 @@ export const ChessRules = class ChessRules {
     const firstRank = color == "w" ? sizeX - 1 : 0;
     const startRank = color == "w" ? sizeX - 2 : 1;
     const lastRank = color == "w" ? 0 : sizeX - 1;
-    const pawnColor = this.getColor(x, y); //can be different for checkered
 
     // NOTE: next condition is generally true (no pawn on last rank)
     if (x + shiftX >= 0 && x + shiftX < sizeX) {
@@ -642,7 +660,7 @@ export const ChessRules = class ChessRules {
         for (let piece of finalPieces) {
           moves.push(
             this.getBasicMove([x, y], [x + shiftX, y], {
-              c: pawnColor,
+              c: color,
               p: piece
             })
           );
@@ -667,7 +685,7 @@ export const ChessRules = class ChessRules {
           for (let piece of finalPieces) {
             moves.push(
               this.getBasicMove([x, y], [x + shiftX, y + shiftY], {
-                c: pawnColor,
+                c: color,
                 p: piece
               })
             );
@@ -725,7 +743,7 @@ export const ChessRules = class ChessRules {
   // What are the king moves from square x,y ?
   getPotentialKingMoves(sq) {
     // Initialize with normal moves
-    let moves = this.getSlideNJumpMoves(
+    const moves = this.getSlideNJumpMoves(
       sq,
       V.steps[V.ROOK].concat(V.steps[V.BISHOP]),
       "oneStep"
@@ -752,7 +770,7 @@ export const ChessRules = class ChessRules {
       castleSide < 2;
       castleSide++ //large, then small
     ) {
-      if (!this.castleFlags[c][castleSide]) continue;
+      if (this.castleFlags[c][castleSide] >= V.size.y) continue;
       // If this code is reached, rooks and king are on initial position
 
       // Nothing on the path of the king ? (and no checks)
@@ -774,10 +792,10 @@ export const ChessRules = class ChessRules {
 
       // Nothing on the path to the rook?
       step = castleSide == 0 ? -1 : 1;
-      for (i = y + step; i != this.INIT_COL_ROOK[c][castleSide]; i += step) {
+      const rookPos = this.castleFlags[c][castleSide];
+      for (i = y + step; i != rookPos; i += step) {
         if (this.board[x][i] != V.EMPTY) continue castlingCheck;
       }
-      const rookPos = this.INIT_COL_ROOK[c][castleSide];
 
       // Nothing on final squares, except maybe king and castling rook?
       for (i = 0; i < 2; i++) {
@@ -982,91 +1000,88 @@ export const ChessRules = class ChessRules {
     for (let psq of move.vanish) board[psq.x][psq.y] = psq.c + psq.p;
   }
 
+  prePlay() {}
+
+  play(move) {
+    // DEBUG:
+//    if (!this.states) this.states = [];
+//    const stateFen = this.getBaseFen() + this.getTurnFen();// + this.getFlagsFen();
+//    this.states.push(stateFen);
+
+    this.prePlay(move);
+    if (V.HasFlags) move.flags = JSON.stringify(this.aggregateFlags()); //save flags (for undo)
+    if (V.HasEnpassant) this.epSquares.push(this.getEpSquare(move));
+    V.PlayOnBoard(this.board, move);
+    this.turn = V.GetOppCol(this.turn);
+    this.movesCount++;
+    this.postPlay(move);
+  }
+
   // After move is played, update variables + flags
-  updateVariables(move) {
+  postPlay(move) {
+    const c = V.GetOppCol(this.turn);
     let piece = undefined;
-    // TODO: update variables before move is played, and just use this.turn ?
-    // (doesn't work in general, think MarseilleChess)
-    let c = undefined;
-    if (move.vanish.length >= 1) {
+    if (move.vanish.length >= 1)
       // Usual case, something is moved
       piece = move.vanish[0].p;
-      c = move.vanish[0].c;
-    } else {
+    else
       // Crazyhouse-like variants
       piece = move.appear[0].p;
-      c = move.appear[0].c;
-    }
-    if (!['w','b'].includes(c)) {
-      // Checkered, for example
-      c = V.GetOppCol(this.turn);
-    }
     const firstRank = c == "w" ? V.size.x - 1 : 0;
 
     // Update king position + flags
     if (piece == V.KING && move.appear.length > 0) {
       this.kingPos[c][0] = move.appear[0].x;
       this.kingPos[c][1] = move.appear[0].y;
-      if (V.HasFlags) this.castleFlags[c] = [false, false];
+      if (V.HasCastle) this.castleFlags[c] = [V.size.y, V.size.y];
       return;
     }
-    if (V.HasFlags) {
+    if (V.HasCastle) {
       // Update castling flags if rooks are moved
       const oppCol = V.GetOppCol(c);
       const oppFirstRank = V.size.x - 1 - firstRank;
       if (
         move.start.x == firstRank && //our rook moves?
-        this.INIT_COL_ROOK[c].includes(move.start.y)
+        this.castleFlags[c].includes(move.start.y)
       ) {
-        const flagIdx = move.start.y == this.INIT_COL_ROOK[c][0] ? 0 : 1;
-        this.castleFlags[c][flagIdx] = false;
+        const flagIdx = (move.start.y == this.castleFlags[c][0] ? 0 : 1);
+        this.castleFlags[c][flagIdx] = V.size.y;
       } else if (
         move.end.x == oppFirstRank && //we took opponent rook?
-        this.INIT_COL_ROOK[oppCol].includes(move.end.y)
+        this.castleFlags[oppCol].includes(move.end.y)
       ) {
-        const flagIdx = move.end.y == this.INIT_COL_ROOK[oppCol][0] ? 0 : 1;
-        this.castleFlags[oppCol][flagIdx] = false;
+        const flagIdx = (move.end.y == this.castleFlags[oppCol][0] ? 0 : 1);
+        this.castleFlags[oppCol][flagIdx] = V.size.y;
       }
     }
   }
 
-  // After move is undo-ed *and flags resetted*, un-update other variables
-  // TODO: more symmetry, by storing flags increment in move (?!)
-  unupdateVariables(move) {
-    // (Potentially) Reset king position
-    const c = this.getColor(move.start.x, move.start.y);
-    if (this.getPiece(move.start.x, move.start.y) == V.KING)
-      this.kingPos[c] = [move.start.x, move.start.y];
-  }
-
-  play(move) {
-    // DEBUG:
-//    if (!this.states) this.states = [];
-//    const stateFen = this.getBaseFen() + this.getTurnFen() + this.getFlagsFen();
-//    this.states.push(stateFen);
-
-    if (V.HasFlags) move.flags = JSON.stringify(this.aggregateFlags()); //save flags (for undo)
-    if (V.HasEnpassant) this.epSquares.push(this.getEpSquare(move));
-    V.PlayOnBoard(this.board, move);
-    this.turn = V.GetOppCol(this.turn);
-    this.movesCount++;
-    this.updateVariables(move);
-  }
+  preUndo() {}
 
   undo(move) {
+    this.preUndo(move);
     if (V.HasEnpassant) this.epSquares.pop();
     if (V.HasFlags) this.disaggregateFlags(JSON.parse(move.flags));
     V.UndoOnBoard(this.board, move);
     this.turn = V.GetOppCol(this.turn);
     this.movesCount--;
-    this.unupdateVariables(move);
+    this.postUndo(move);
 
     // DEBUG:
-//    const stateFen = this.getBaseFen() + this.getTurnFen() + this.getFlagsFen();
+//    const stateFen = this.getBaseFen() + this.getTurnFen();// + this.getFlagsFen();
 //    if (stateFen != this.states[this.states.length-1]) debugger;
 //    this.states.pop();
   }
 
+  // After move is undo-ed *and flags resetted*, un-update other variables
+  // TODO: more symmetry, by storing flags increment in move (?!)
+  postUndo(move) {
+    // (Potentially) Reset king position
+    const c = this.getColor(move.start.x, move.start.y);
+    if (this.getPiece(move.start.x, move.start.y) == V.KING)
+      this.kingPos[c] = [move.start.x, move.start.y];
+  }
+
   ///////////////
   // END OF GAME
 
@@ -1114,66 +1129,59 @@ export const ChessRules = class ChessRules {
     return 3;
   }
 
-  // NOTE: works also for extinction chess because depth is 3...
   getComputerMove() {
     const maxeval = V.INFINITY;
     const color = this.turn;
-    // Some variants may show a bigger moves list to the human (Switching),
-    // thus the argument "computer" below (which is generally ignored)
-    let moves1 = this.getAllValidMoves("computer");
+    let moves1 = this.getAllValidMoves();
 
     if (moves1.length == 0)
       // TODO: this situation should not happen
       return null;
 
-    // Can I mate in 1 ? (for Magnetic & Extinction)
-    for (let i of shuffle(ArrayFun.range(moves1.length))) {
+    // Rank moves using a min-max at depth 2 (if search_depth >= 2!)
+    for (let i = 0; i < moves1.length; i++) {
       this.play(moves1[i]);
-      let finish = Math.abs(this.evalPosition()) >= V.THRESHOLD_MATE;
-      if (!finish) {
-        const score = this.getCurrentScore();
-        if (["1-0", "0-1"].includes(score)) finish = true;
+      const score1 = this.getCurrentScore();
+      if (score1 != "*") {
+        moves1[i].eval =
+          score1 == "1/2"
+            ? 0
+            : (score1 == "1-0" ? 1 : -1) * maxeval;
+      }
+      if (V.SEARCH_DEPTH == 1 || score1 != "*") {
+        if (!moves1[i].eval) moves1[i].eval = this.evalPosition();
+        this.undo(moves1[i]);
+        continue;
       }
-      this.undo(moves1[i]);
-      if (finish) return moves1[i];
-    }
-
-    // Rank moves using a min-max at depth 2
-    for (let i = 0; i < moves1.length; i++) {
       // Initial self evaluation is very low: "I'm checkmated"
       moves1[i].eval = (color == "w" ? -1 : 1) * maxeval;
-      this.play(moves1[i]);
-      const score1 = this.getCurrentScore();
-      let eval2 = undefined;
-      if (score1 == "*") {
-        // Initial enemy evaluation is very low too, for him
-        eval2 = (color == "w" ? 1 : -1) * maxeval;
-        // Second half-move:
-        let moves2 = this.getAllValidMoves("computer");
-        for (let j = 0; j < moves2.length; j++) {
-          this.play(moves2[j]);
-          const score2 = this.getCurrentScore();
-          let evalPos = 0; //1/2 value
-          switch (score2) {
-            case "*":
-              evalPos = this.evalPosition();
-              break;
-            case "1-0":
-              evalPos = maxeval;
-              break;
-            case "0-1":
-              evalPos = -maxeval;
-              break;
-          }
-          if (
-            (color == "w" && evalPos < eval2) ||
-            (color == "b" && evalPos > eval2)
-          ) {
-            eval2 = evalPos;
-          }
-          this.undo(moves2[j]);
+      // Initial enemy evaluation is very low too, for him
+      let eval2 = (color == "w" ? 1 : -1) * maxeval;
+      // Second half-move:
+      let moves2 = this.getAllValidMoves();
+      for (let j = 0; j < moves2.length; j++) {
+        this.play(moves2[j]);
+        const score2 = this.getCurrentScore();
+        let evalPos = 0; //1/2 value
+        switch (score2) {
+          case "*":
+            evalPos = this.evalPosition();
+            break;
+          case "1-0":
+            evalPos = maxeval;
+            break;
+          case "0-1":
+            evalPos = -maxeval;
+            break;
+        }
+        if (
+          (color == "w" && evalPos < eval2) ||
+          (color == "b" && evalPos > eval2)
+        ) {
+          eval2 = evalPos;
         }
-      } else eval2 = score1 == "1/2" ? 0 : (score1 == "1-0" ? 1 : -1) * maxeval;
+        this.undo(moves2[j]);
+      }
       if (
         (color == "w" && eval2 > moves1[i].eval) ||
         (color == "b" && eval2 < moves1[i].eval)
@@ -1185,20 +1193,11 @@ export const ChessRules = class ChessRules {
     moves1.sort((a, b) => {
       return (color == "w" ? 1 : -1) * (b.eval - a.eval);
     });
-
-    let candidates = [0]; //indices of candidates moves
-    for (let j = 1; j < moves1.length && moves1[j].eval == moves1[0].eval; j++)
-      candidates.push(j);
-    let currentBest = moves1[candidates[randInt(candidates.length)]];
+//    console.log(moves1.map(m => { return [this.getNotation(m), m.eval]; }));
 
     // Skip depth 3+ if we found a checkmate (or if we are checkmated in 1...)
     if (V.SEARCH_DEPTH >= 3 && Math.abs(moves1[0].eval) < V.THRESHOLD_MATE) {
-      // From here, depth >= 3: may take a while, so we control time
-      const timeStart = Date.now();
       for (let i = 0; i < moves1.length; i++) {
-        if (Date.now() - timeStart >= 5000)
-          //more than 5 seconds
-          return currentBest; //depth 2 at least
         this.play(moves1[i]);
         // 0.1 * oldEval : heuristic to avoid some bad moves (not all...)
         moves1[i].eval =
@@ -1209,10 +1208,9 @@ export const ChessRules = class ChessRules {
       moves1.sort((a, b) => {
         return (color == "w" ? 1 : -1) * (b.eval - a.eval);
       });
-    } else return currentBest;
-//    console.log(moves1.map(m => { return [this.getNotation(m), m.eval]; }));
+    }
 
-    candidates = [0];
+    let candidates = [0];
     for (let j = 1; j < moves1.length && moves1[j].eval == moves1[0].eval; j++)
       candidates.push(j);
     return moves1[candidates[randInt(candidates.length)]];
@@ -1225,7 +1223,7 @@ export const ChessRules = class ChessRules {
     if (score != "*")
       return score == "1/2" ? 0 : (score == "1-0" ? 1 : -1) * maxeval;
     if (depth == 0) return this.evalPosition();
-    const moves = this.getAllValidMoves("computer");
+    const moves = this.getAllValidMoves();
     let v = color == "w" ? -maxeval : maxeval;
     if (color == "w") {
       for (let i = 0; i < moves.length; i++) {