Fixed Checkered variant
[xogo.git] / variants / Checkered / class.js
index 6ef3353..f73ed0e 100644 (file)
@@ -12,10 +12,9 @@ export default class CheckeredRules extends ChessRules {
           label: "Allow switching",
           variable: "withswitch",
           type: "checkbox",
-          defaut: false
+          defaut: true
         }
       ],
-      // Game modifiers (using "elementary variants"). Default: false
       styles: [
         "balance",
         "capture",
@@ -29,7 +28,13 @@ export default class CheckeredRules extends ChessRules {
     };
   }
 
-  static board2fen(b) {
+  static GetColorClass(c) {
+    if (c == 'c')
+      return "checkered";
+    return C.GetColorClass(c);
+  }
+
+  board2fen(b) {
     const checkered_codes = {
       p: "s",
       q: "t",
@@ -37,11 +42,12 @@ export default class CheckeredRules extends ChessRules {
       b: "c",
       n: "o"
     };
-    if (b[0] == "c") return checkered_codes[b[1]];
+    if (b[0] == "c")
+      return checkered_codes[b[1]];
     return super.board2fen(b);
   }
 
-  static fen2board(f) {
+  fen2board(f) {
     // Tolerate upper-case versions of checkered pieces (why not?)
     const checkered_pieces = {
       s: "p",
@@ -60,7 +66,100 @@ export default class CheckeredRules extends ChessRules {
     return super.fen2board(f);
   }
 
-  // TODO: pieces()
+  genRandInitBaseFen() {
+    let res = super.genRandInitBaseFen();
+    res.o.flags += "1".repeat(16); //pawns flags
+    return res;
+  }
+
+  getPartFen(o) {
+    return Object.assign(
+      {
+        "cmove": o.init ? "-" : this.getCmoveFen(),
+        "stage": o.init ? "1" : this.getStageFen()
+      },
+      super.getPartFen(o)
+    );
+  }
+
+  getCmoveFen() {
+    if (!this.cmove)
+      return "-";
+    return (
+      C.CoordsToSquare(this.cmove.start) + C.CoordsToSquare(this.cmove.end)
+    );
+  }
+
+  getStageFen() {
+    return (this.stage + this.sideCheckered);
+  }
+
+  getFlagsFen() {
+    let fen = super.getFlagsFen();
+    // Add pawns flags
+    for (let c of ["w", "b"]) {
+      for (let i = 0; i < 8; i++)
+        fen += (this.pawnFlags[c][i] ? "1" : "0");
+    }
+    return fen;
+  }
+
+  getPawnShift(color) {
+    return super.getPawnShift(color == 'c' ? this.turn : color);
+  }
+
+  getOppCols(color) {
+    if (this.stage == 1)
+      return super.getOppCols(color).concat(['c']);
+    // Stage 2: depends if color is w+b or checkered
+    if (color == this.sideCheckered)
+      return ['w', 'b'];
+    return ['c'];
+  }
+
+  pieces(color, x, y) {
+    let baseRes = super.pieces(color, x, y);
+    if (this.getPiece(x, y) == 'p' && color == 'c') {
+      const pawnShift = this.getPawnShift(this.turn); //cannot trust color
+      const initRank = (
+        (this.stage == 2 && [1, 6].includes(x)) ||
+        (this.stage == 1 &&
+          ((x == 1 && this.turn == 'b') || (x == 6 && this.turn == 'w'))
+        )
+      );
+      // Checkered pawns on stage 2 are bidirectional
+      let moveSteps = [[pawnShift, 0]],
+          attackSteps = [[pawnShift, 1], [pawnShift, -1]];
+      if (this.stage == 2) {
+        moveSteps.push([-pawnShift, 0]);
+        Array.prototype.push.apply(attackSteps,
+          [[-pawnShift, 1], [-pawnShift, -1]]);
+      }
+      baseRes['p'] = {
+        "class": "pawn",
+        moves: [
+          {
+            steps: moveSteps,
+            range: (initRank ? 2 : 1)
+          }
+        ],
+        attack: [
+          {
+            steps: attackSteps,
+            range: 1
+          }
+        ]
+      };
+    }
+    const checkered = {
+      's': {"class": "checkered-pawn", moveas: 'p'},
+      'u': {"class": "checkered-rook", moveas: 'r'},
+      'o': {"class": "checkered-knight", moveas: 'n'},
+      'c': {"class": "checkered-bishop", moveas: 'b'},
+      't': {"class": "checkered-queen", moveas: 'q'}
+    };
+    return Object.assign(baseRes, checkered);
+  }
 
   setOtherVariables(fenParsed) {
     super.setOtherVariables(fenParsed);
@@ -76,8 +175,7 @@ export default class CheckeredRules extends ChessRules {
     // Stage 1: as Checkered2. Stage 2: checkered pieces are autonomous
     const stageInfo = fenParsed.stage;
     this.stage = parseInt(stageInfo[0], 10);
-    this.canSwitch = (this.stage == 1 && stageInfo[1] != '-');
-    this.sideCheckered = (this.stage == 2 ? stageInfo[1] : undefined);
+    this.sideCheckered = (this.stage == 2 ? stageInfo[1] : "");
   }
 
   setFlags(fenflags) {
@@ -93,15 +191,6 @@ export default class CheckeredRules extends ChessRules {
     }
   }
 
-  aggregateFlags() {
-    return [this.castleFlags, this.pawnFlags];
-  }
-
-  disaggregateFlags(flags) {
-    this.castleFlags = flags[0];
-    this.pawnFlags = flags[1];
-  }
-
   getEpSquare(moveOrSquare) {
     // At stage 2, all pawns can be captured en-passant
     if (
@@ -117,7 +206,7 @@ export default class CheckeredRules extends ChessRules {
   getCmove(move) {
     // No checkered move to undo at stage 2:
     if (this.stage == 1 && move.vanish.length == 1 && move.appear[0].c == "c")
-      return { start: move.start, end: move.end };
+      return {start: move.start, end: move.end};
     return null;
   }
 
@@ -130,113 +219,83 @@ export default class CheckeredRules extends ChessRules {
       const color2 = this.getColor(x2, y2);
       return color1 != color2 && [color1, color2].includes('c');
     }
-    // Checkered aren't captured
     return (
       color1 != color2 &&
-      color2 != "c" &&
+      color2 != "c" && //checkered aren't captured
       (color1 != "c" || color2 != this.turn)
     );
   }
 
-  // TODO::::
-  getPotentialMovesFrom([x, y], noswitch) {
-    let standardMoves = super.getPotentialMovesFrom([x, y]);
-    if (this.stage == 1) {
-      const color = this.turn;
-      // Post-processing: apply "checkerization" of standard moves
-      const lastRank = (color == "w" ? 0 : 7);
-      let moves = [];
-      // King is treated differently: it never turn checkered
-      if (this.getPiece(x, y) == V.KING) {
-        // If at least one checkered piece, allow switching:
-        if (
-          this.canSwitch && !noswitch &&
-          this.board.some(b => b.some(cell => cell[0] == 'c'))
-        ) {
-          const oppCol = V.GetOppCol(color);
-          moves.push(
-            new Move({
-              start: { x: x, y: y },
-              end: { x: this.kingPos[oppCol][0], y: this.kingPos[oppCol][1] },
-              appear: [],
-              vanish: []
-            })
-          );
-        }
-        return standardMoves.concat(moves);
+  postProcessPotentialMoves(moves) {
+    moves = super.postProcessPotentialMoves(moves);
+    if (this.stage == 2 || moves.length == 0)
+      return moves;
+    const color = this.turn;
+    // Apply "checkerization" of standard moves
+    const lastRank = (color == "w" ? 0 : 7);
+    const [x, y] = [moves[0].start.x, moves[0].start.y];
+    const piece = this.getPiece(x, y);
+    // King is treated differently: it never turn checkered
+    if (piece == 'k' && this.stage == 1) {
+      // If at least one checkered piece, allow switching:
+      if (
+        this.options["withswitch"] &&
+        this.board.some(b => b.some(cell => cell[0] == 'c'))
+      ) {
+        const oppKingPos = this.searchKingPos(C.GetOppTurn(this.turn))[0];
+        moves.push(
+          new Move({
+            start: { x: x, y: y },
+            end: {x: oppKingPos[0], y: oppKingPos[1]},
+            appear: [],
+            vanish: []
+          })
+        );
       }
-      standardMoves.forEach(m => {
-        if (m.vanish[0].p == V.PAWN) {
+      return moves;
+    }
+    if (piece == 'p') {
+      // Filter out forbidden pawn moves
+      moves = moves.filter(m => {
+        if (m.vanish.length > 0 && m.vanish[0].p == 'p') {
           if (
             Math.abs(m.end.x - m.start.x) == 2 &&
             !this.pawnFlags[this.turn][m.start.y]
           ) {
-            return; //skip forbidden 2-squares jumps
+            return false; //forbidden 2-squares jumps
           }
           if (
-            this.board[m.end.x][m.end.y] == V.EMPTY &&
+            this.board[m.end.x][m.end.y] == "" &&
             m.vanish.length == 2 &&
             this.getColor(m.start.x, m.start.y) == "c"
           ) {
-            return; //checkered pawns cannot take en-passant
-          }
-        }
-        if (m.vanish.length == 1)
-          // No capture
-          moves.push(m);
-        else {
-          // A capture occured (m.vanish.length == 2)
-          m.appear[0].c = "c";
-          moves.push(m);
-          if (
-            // Avoid promotions (already treated):
-            m.appear[0].p != m.vanish[1].p &&
-            (m.vanish[0].p != V.PAWN || m.end.x != lastRank)
-          ) {
-            // Add transformation into captured piece
-            let m2 = JSON.parse(JSON.stringify(m));
-            m2.appear[0].p = m.vanish[1].p;
-            moves.push(m2);
+            return false; //checkered pawns cannot take en-passant
           }
         }
+        return true;
       });
-      return moves;
     }
-    return standardMoves;
-  }
-
-  // TODO: merge this into pieces() method
-  getPotentialPawnMoves([x, y]) {
-    const color = this.getColor(x, y);
-    if (this.stage == 2) {
-      const saveTurn = this.turn;
-      if (this.sideCheckered == this.turn) {
-        // Cannot change PawnSpecs.bidirectional, so cheat a little:
-        this.turn = 'w';
-        const wMoves = super.getPotentialPawnMoves([x, y]);
-        this.turn = 'b';
-        const bMoves = super.getPotentialPawnMoves([x, y]);
-        this.turn = saveTurn;
-        return wMoves.concat(bMoves);
+    let extraMoves = [];
+    moves.forEach(m => {
+      if (m.vanish.length == 2 && m.appear.length == 1) {
+        // A capture occured
+        m.appear[0].c = "c";
+        if (
+          m.appear[0].p != m.vanish[1].p &&
+          // No choice if promotion:
+          (m.vanish[0].p != 'p' || m.end.x != lastRank)
+        ) {
+          // Add transformation into captured piece
+          let m2 = JSON.parse(JSON.stringify(m));
+          m2.appear[0].p = m.vanish[1].p;
+          extraMoves.push(m2);
+        }
       }
-      // Playing with both colors:
-      this.turn = color;
-      const moves = super.getPotentialPawnMoves([x, y]);
-      this.turn = saveTurn;
-      return moves;
-    }
-    let moves = super.getPotentialPawnMoves([x, y]);
-    // Post-process: set right color for checkered moves
-    if (color == 'c') {
-      moves.forEach(m => {
-        m.appear[0].c = 'c'; //may be done twice if capture
-        m.vanish[0].c = 'c';
-      });
-    }
-    return moves;
+    });
+    return moves.concat(extraMoves);
   }
 
-  canIplay(side, [x, y]) {
+  canIplay(x, y) {
     if (this.stage == 2) {
       const color = this.getColor(x, y);
       return (
@@ -245,16 +304,19 @@ export default class CheckeredRules extends ChessRules {
           : ['w', 'b'].includes(color)
       );
     }
-    return side == this.turn && [side, "c"].includes(this.getColor(x, y));
+    return (
+      this.playerColor == this.turn &&
+      [this.turn, "c"].includes(this.getColor(x, y))
+    );
   }
 
   // Does m2 un-do m1 ? (to disallow undoing checkered moves)
   oppositeMoves(m1, m2) {
     return (
       !!m1 &&
-      m2.appear[0].c == "c" &&
       m2.appear.length == 1 &&
       m2.vanish.length == 1 &&
+      m2.appear[0].c == "c" &&
       m1.start.x == m2.end.x &&
       m1.end.x == m2.start.x &&
       m1.start.y == m2.end.y &&
@@ -262,48 +324,44 @@ export default class CheckeredRules extends ChessRules {
     );
   }
 
-  // TODO: adapt, merge
   filterValid(moves) {
-    if (moves.length == 0) return [];
     const color = this.turn;
-    const oppCol = V.GetOppCol(color);
-    const L = this.cmoves.length; //at least 1: init from FEN
-    const stage = this.stage; //may change if switch
-    return moves.filter(m => {
+    if (this.stage == 2 && this.sideCheckered == color)
       // Checkered cannot be under check (no king)
-      if (stage == 2 && this.sideCheckered == color) return true;
-      this.play(m);
+      return moves;
+    let kingPos = super.searchKingPos(color);
+    if (this.stage == 2)
+      // Must consider both kings (attacked by checkered side)
+      kingPos.push(super.searchKingPos(C.GetOppTurn(this.turn))[0]);
+    const oppCols = this.getOppCols(color);
+    const filteredMoves = moves.filter(m => {
+      if (m.vanish.length == 0 && m.appear.length == 0)
+        return true; //switch move
+      this.playOnBoard(m);
+      if (m.vanish[0].p == 'k')
+        kingPos[0] = [m.appear[0].x, m.appear[0].y];
       let res = true;
-      if (stage == 1) {
-        if (m.appear.length == 0 && m.vanish.length == 0) {
-          // Special "switch" move: kings must not be attacked by checkered.
-          // Not checking for oppositeMoves here: checkered are autonomous
-          res = (
-            !this.isAttacked(this.kingPos['w'], ['c']) &&
-            !this.isAttacked(this.kingPos['b'], ['c']) &&
-            this.getAllPotentialMoves().length > 0
-          );
-        }
-        else res = !this.oppositeMoves(this.cmoves[L - 1], m);
-      }
-      if (res && m.appear.length > 0) res = !this.underCheck(color);
-      // At stage 2, side with B & W can be undercheck with both kings:
-      if (res && stage == 2) res = !this.underCheck(oppCol);
-      this.undo(m);
+      if (this.stage == 1)
+        res = !this.oppositeMoves(this.cmove, m);
+      if (res && m.appear.length > 0)
+        res = !this.underCheck(kingPos, oppCols);
+      if (m.vanish[0].p == 'k')
+        kingPos[0] = [m.vanish[0].x, m.vanish[0].y];
+      this.undoOnBoard(m);
       return res;
     });
+    return filteredMoves;
   }
 
-  atLeastOneMove() {
-    const color = this.turn;
-    const oppCol = V.GetOppCol(color);
-    for (let i = 0; i < V.size.x; i++) {
-      for (let j = 0; j < V.size.y; j++) {
+  atLeastOneMove(color) {
+    const myCols = [color, 'c'];
+    for (let i = 0; i < this.size.x; i++) {
+      for (let j = 0; j < this.size.y; j++) {
         const colIJ = this.getColor(i, j);
         if (
-          this.board[i][j] != V.EMPTY &&
+          this.board[i][j] != "" &&
           (
-            (this.stage == 1 && colIJ != oppCol) ||
+            (this.stage == 1 && myCols.includes(colIJ)) ||
             (this.stage == 2 &&
               (
                 (this.sideCheckered == color && colIJ == 'c') ||
@@ -312,138 +370,90 @@ export default class CheckeredRules extends ChessRules {
             )
           )
         ) {
-          const moves = this.getPotentialMovesFrom([i, j], "noswitch");
-          if (moves.length > 0) {
-            for (let k = 0; k < moves.length; k++)
-              if (this.filterValid([moves[k]]).length > 0) return true;
-          }
+          const moves = this.getPotentialMovesFrom([i, j]);
+          if (moves.some(m => this.filterValid([m]).length >= 1))
+            return true;
         }
       }
     }
     return false;
   }
 
-  // TODO: adapt
-  underCheck(color) {
-    if (this.stage == 1)
-      return this.isAttacked(this.kingPos[color], [V.GetOppCol(color), "c"]);
-    if (color == this.sideCheckered) return false;
-    return (
-      this.isAttacked(this.kingPos['w'], ["c"]) ||
-      this.isAttacked(this.kingPos['b'], ["c"])
-    );
-  }
-
-  play(move) {
-    move.flags = JSON.stringify(this.aggregateFlags());
-    this.epSquares.push(this.getEpSquare(move));
-    V.PlayOnBoard(this.board, move);
-    if (move.appear.length > 0 || move.vanish.length > 0)
-    {
-      this.turn = V.GetOppCol(this.turn);
-      this.movesCount++;
-    }
-    this.postPlay(move);
+  underCheck(square_s, oppCols) {
+    if (this.stage == 2 && this.turn == this.sideCheckered)
+      return false; //checkered pieces is me, I'm not under check
+    // Artificial turn change required, because canTake uses turn info.
+    // canTake is called from underCheck --> ... --> findDestSquares
+    this.turn = C.GetOppTurn(this.turn);
+    const res = square_s.some(sq => super.underAttack(sq, oppCols));
+    this.turn = C.GetOppTurn(this.turn);
+    return res;
   }
 
-  postPlay(move) {
-    if (move.appear.length == 0 && move.vanish.length == 0) {
-      this.stage = 2;
-      this.sideCheckered = this.turn;
-    }
-    else {
-      const c = move.vanish[0].c;
-      const piece = move.vanish[0].p;
-      if (piece == V.KING) {
-        this.kingPos[c][0] = move.appear[0].x;
-        this.kingPos[c][1] = move.appear[0].y;
-      }
-      super.updateCastleFlags(move, piece);
+  prePlay(move) {
+    if (move.appear.length > 0 && move.vanish.length > 0) {
+      super.prePlay(move);
       if (
         [1, 6].includes(move.start.x) &&
-        move.vanish[0].p == V.PAWN &&
+        move.vanish[0].p == 'p' &&
         Math.abs(move.end.x - move.start.x) == 2
       ) {
         // This move turns off a 2-squares pawn flag
         this.pawnFlags[move.start.x == 6 ? "w" : "b"][move.start.y] = false;
       }
     }
+  }
+
+  postPlay(move) {
+    if (move.appear.length == 0 && move.vanish.length == 0) {
+      this.stage = 2;
+      this.sideCheckered = this.turn;
+      if (this.playerColor != this.turn)
+        super.displayMessage(null, "Autonomous checkered!", "info-text", 2000);
+    }
+    else
+      super.postPlay(move);
     this.cmove = this.getCmove(move);
   }
 
+  tryChangeTurn(move) {
+    if (move.appear.length > 0 && move.vanish.length > 0)
+      super.tryChangeTurn(move);
+  }
+
   getCurrentScore() {
     const color = this.turn;
     if (this.stage == 1) {
-      if (this.atLeastOneMove()) return "*";
+      if (this.atLeastOneMove(color))
+        return "*";
       // Artifically change turn, for checkered pawns
-      this.turn = V.GetOppCol(this.turn);
-      const res =
-        this.isAttacked(this.kingPos[color], [V.GetOppCol(color), "c"])
-          ? color == "w"
-            ? "0-1"
-            : "1-0"
-          : "1/2";
-      this.turn = V.GetOppCol(this.turn);
+      const oppTurn = C.GetOppTurn(color);
+      this.turn = oppTurn;
+      const kingPos = super.searchKingPos(color)[0];
+      let res = "1/2";
+      if (super.underAttack(kingPos, [oppTurn, 'c']))
+        res = (color == "w" ? "0-1" : "1-0");
+      this.turn = color;
       return res;
     }
     // Stage == 2:
-    if (this.sideCheckered == this.turn) {
+    if (this.sideCheckered == color) {
       // Check if remaining checkered pieces: if none, I lost
       if (this.board.some(b => b.some(cell => cell[0] == 'c'))) {
-        if (!this.atLeastOneMove()) return "1/2";
+        if (!this.atLeastOneMove(color))
+          return "1/2";
         return "*";
       }
-      return color == 'w' ? "0-1" : "1-0";
+      return (color == 'w' ? "0-1" : "1-0");
     }
-    if (this.atLeastOneMove()) return "*";
-    let res = this.isAttacked(this.kingPos['w'], ["c"]);
-    if (!res) res = this.isAttacked(this.kingPos['b'], ["c"]);
-    if (res) return color == 'w' ? "0-1" : "1-0";
+    if (this.atLeastOneMove(color))
+      return "*";
+    let res = super.underAttack(super.searchKingPos(color)[0], ['c']);
+    if (!res)
+      res = super.underAttack(super.searchKingPos(oppCol)[0], ['c']);
+    if (res)
+      return (color == 'w' ? "0-1" : "1-0");
     return "1/2";
   }
 
-  // TODO: adapt
-  static GenRandInitFen(options) {
-    const baseFen = ChessRules.GenRandInitFen(options);
-    return (
-      // Add 16 pawns flags + empty cmove + stage == 1:
-      baseFen.slice(0, -2) + "1111111111111111 - - 1" +
-      (!options["switch"] ? '-' : "")
-    );
-  }
-      {
-        cmove: fenParts[5],
-        stage: fenParts[6]
-      }
-
-  getCmoveFen() {
-    const L = this.cmoves.length;
-    return (
-      !this.cmoves[L - 1]
-        ? "-"
-        : ChessRules.CoordsToSquare(this.cmoves[L - 1].start) +
-          ChessRules.CoordsToSquare(this.cmoves[L - 1].end)
-    );
-  }
-
-  getStageFen() {
-    if (this.stage == 1) return "1" + (!this.canSwitch ? '-' : "");
-    // Stage == 2:
-    return "2" + this.sideCheckered;
-  }
-
-  getFen() {
-    return (
-      super.getFen() + " " + this.getCmoveFen() + " " + this.getStageFen()
-    );
-  }
-
-  getFlagsFen() {
-    let fen = super.getFlagsFen();
-    // Add pawns flags
-    for (let c of ["w", "b"])
-      for (let i = 0; i < 8; i++) fen += (this.pawnFlags[c][i] ? "1" : "0");
-    return fen;
-  }
-
 };