New variant idea
[xogo.git] / base_rules.js
index a0defd1..3d8e463 100644 (file)
@@ -97,6 +97,14 @@ export default class ChessRules {
     return true;
   }
 
+  // Allow to take (moving: not disappearing) own pieces?
+  get hasSelfCaptures() {
+    return (
+      this.options["recycle"] ||
+      (this.options["teleport"] && this.subTurnTeleport == 1)
+    );
+  }
+
   get hasReserve() {
     return (
       !!this.options["crazyhouse"] ||
@@ -122,6 +130,11 @@ export default class ChessRules {
     return false;
   }
 
+  // Some variants do not flip board as black
+  get flippedBoard() {
+    return (this.playerColor == 'b');
+  }
+
   // Some variants use click infos:
   doClick(coords) {
     if (typeof coords.x != "number")
@@ -162,6 +175,22 @@ export default class ChessRules {
     return Object.values(cd).map(c => c.toString(36)).join("");
   }
 
+  // c10 --> 02 (assuming 10 rows)
+  static SquareFromUsual(sq) {
+    return (
+      (this.size.x - parseInt(sq.substring(1), 10)).toString(36) +
+      (sq.charCodeAt(0) - 97).toString(36)
+    );
+  }
+
+  // 02 --> c10
+  static UsualFromSquare(sq) {
+    return (
+      String.fromCharCode(parseInt(sq.charAt(1), 36) + 97) +
+      (this.size.x - parseInt(sq.charAt(0), 36)).toString(10)
+    );
+  }
+
   coordsToId(cd) {
     if (typeof cd.x == "number") {
       return (
@@ -220,7 +249,7 @@ export default class ChessRules {
       ['r', 'n', 'b', 'q', 'k', 'b', 'n', 'r'],
       {
         randomness: this.options["randomness"],
-        between: {p1: 'k', p2: 'r'},
+        between: [{p1: 'k', p2: 'r'}],
         diffCol: ['b'],
         flags: ['r']
       }
@@ -311,7 +340,7 @@ export default class ChessRules {
 
   // Flags part of the FEN string
   getFlagsFen() {
-    return ["w", "b"].map(c => {
+    return ['w', 'b'].map(c => {
       return this.castleFlags[c].map(x => x.toString(36)).join("");
     }).join("");
   }
@@ -325,9 +354,9 @@ export default class ChessRules {
 
   getReserveFen(o) {
     if (o.init)
-      return "000000000000";
+      return Array(2 * V.ReserveArray.length).fill('0').join("");
     return (
-      ["w","b"].map(c => Object.values(this.reserve[c]).join("")).join("")
+      ['w', 'b'].map(c => Object.values(this.reserve[c]).join("")).join("")
     );
   }
 
@@ -428,14 +457,16 @@ export default class ChessRules {
   }
 
   // ordering as in pieces() p,r,n,b,q,k
-  initReserves(reserveStr, pieceArray) {
-    if (!pieceArray)
-      pieceArray = ['p', 'r', 'n', 'b', 'q', 'k'];
+  static get ReserveArray() {
+    return ['p', 'r', 'n', 'b', 'q', 'k'];
+  }
+
+  initReserves(reserveStr) {
     const counts = reserveStr.split("").map(c => parseInt(c, 36));
-    const L = pieceArray.length;
+    const L = V.ReserveArray.length;
     this.reserve = {
-      w: ArrayFun.toObject(pieceArray, counts.slice(0, L)),
-      b: ArrayFun.toObject(pieceArray, counts.slice(L, 2 * L))
+      w: ArrayFun.toObject(V.ReserveArray, counts.slice(0, L)),
+      b: ArrayFun.toObject(V.ReserveArray, counts.slice(L, 2 * L))
     };
   }
 
@@ -450,7 +481,7 @@ export default class ChessRules {
   // VISUAL UTILS
 
   getPieceWidth(rwidth) {
-    return (rwidth / this.size.y);
+    return (rwidth / Math.max(this.size.x, this.size.y));
   }
 
   getReserveSquareSize(rwidth, nbR) {
@@ -470,7 +501,7 @@ export default class ChessRules {
   }
 
   getRankInReserve(c, p) {
-    const pieces = Object.keys(this.pieces());
+    const pieces = Object.keys(this.pieces(c, c, p));
     const lastIndex = pieces.findIndex(pp => pp == p)
     let toTest = pieces.slice(0, lastIndex);
     return toTest.reduce(
@@ -519,7 +550,6 @@ export default class ChessRules {
 
   re_drawBoardElements() {
     const board = this.getSvgChessboard();
-    const oppCol = C.GetOppCol(this.playerColor);
     const container = document.getElementById(this.containerId);
     const rc = container.getBoundingClientRect();
     let chessboard = container.querySelector(".chessboard");
@@ -567,11 +597,18 @@ export default class ChessRules {
 
   // Get SVG board (background, no pieces)
   getSvgChessboard() {
-    const flipped = (this.playerColor == 'b');
     let board = `
       <svg
         viewBox="0 0 ${10*this.size.y} ${10*this.size.x}"
         class="chessboard_SVG">`;
+    board += this.getBaseSvgChessboard();
+    board += "</svg>";
+    return board;
+  }
+
+  getBaseSvgChessboard() {
+    let board = "";
+    const flipped = this.flippedBoard;
     for (let i=0; i < this.size.x; i++) {
       for (let j=0; j < this.size.y; j++) {
         if (!this.onBoard(i, j))
@@ -593,7 +630,6 @@ export default class ChessRules {
           />`;
       }
     }
-    board += "</svg>";
     return board;
   }
 
@@ -630,7 +666,11 @@ export default class ChessRules {
       else
         this[arrName] = ArrayFun.init(this.size.x, this.size.y, null);
       if (arrName == "d_pieces")
-        this.marks.forEach(([i, j]) => addPiece(i, j, arrName, "mark"));
+        this.marks.forEach((m) => {
+          const formattedSquare = C.SquareFromUsual(m);
+          const mCoords = C.SquareToCoords(formattedSquare);
+          addPiece(mCoords.x, mCoords.y, arrName, "mark");
+        });
     };
     if (this.marks)
       conditionalReset("d_pieces");
@@ -641,7 +681,7 @@ export default class ChessRules {
           const color = this.getColor(i, j);
           const piece = this.getPiece(i, j);
           addPiece(i, j, "g_pieces", this.pieces(color, i, j)[piece]["class"]);
-          this.g_pieces[i][j].classList.add(C.GetColorClass(color));
+          this.g_pieces[i][j].classList.add(V.GetColorClass(color));
           if (this.enlightened && !this.enlightened[i][j])
             this.g_pieces[i][j].classList.add("hidden");
         }
@@ -708,7 +748,7 @@ export default class ChessRules {
         rcontainer.appendChild(r_cell);
         let piece = document.createElement("piece");
         C.AddClass_es(piece, this.pieces(c, c, p)[p]["class"]);
-        piece.classList.add(C.GetColorClass(c));
+        piece.classList.add(V.GetColorClass(c));
         piece.style.width = "100%";
         piece.style.height = "100%";
         this.r_pieces[c][p] = piece;
@@ -830,9 +870,10 @@ export default class ChessRules {
       y = (this.playerColor == i ? y = r.height + 5 : - 5 - rsqSize);
     }
     else {
-      const sqSize = r.width / this.size.y;
-      const flipped = (this.playerColor == 'b');
-      x = (flipped ? this.size.y - 1 - j : j) * sqSize;
+      const sqSize = r.width / Math.max(this.size.x, this.size.y);
+      const flipped = this.flippedBoard;
+      x = (flipped ? this.size.y - 1 - j : j) * sqSize +
+          Math.abs(this.size.x - this.size.y) * sqSize / 2;
       y = (flipped ? this.size.x - 1 - i : i) * sqSize;
     }
     return [r.x + x, r.y + y];
@@ -1017,7 +1058,7 @@ export default class ChessRules {
       const cdisp = moves[i].choice || moves[i].appear[0].p;
       C.AddClass_es(piece,
         this.pieces(color, moves[i].end.x, moves[i].end.y)[cdisp]["class"]);
-      piece.classList.add(C.GetColorClass(color));
+      piece.classList.add(V.GetColorClass(color));
       piece.style.width = "100%";
       piece.style.height = "100%";
       choice.appendChild(piece);
@@ -1065,9 +1106,10 @@ export default class ChessRules {
   // Include square of the en-passant capturing square:
   enlightEnpassant() {
     // NOTE: shortcut, pawn has only one attack type, doesn't depend on square
-    const steps = this.pieces(this.playerColor)["p"].attack[0].steps;
+    // TODO: (0, 0) is wrong, would need to place an attacker here...
+    const steps = this.pieces(this.playerColor, 0, 0)["p"].attack[0].steps;
     for (let step of steps) {
-      const x = this.epSquare.x - step[0],
+      const x = this.epSquare.x - step[0], //NOTE: epSquare.x not on edge
             y = this.getY(this.epSquare.y - step[1]);
       if (
         this.onBoard(x, y) &&
@@ -1141,7 +1183,7 @@ export default class ChessRules {
   getPieceType(x, y, p) {
     if (!p)
       p = this.getPiece(x, y);
-    return this.pieces()[p].moveas || p;
+    return this.pieces(this.getColor(x, y), x, y)[p].moveas || p;
   }
 
   isKing(x, y, p) {
@@ -1152,8 +1194,12 @@ export default class ChessRules {
     return !!C.CannibalKings[p];
   }
 
-  // Get opponent color
-  static GetOppCol(color) {
+  static GetOppTurn(color) {
+    return (color == 'w' ? 'b' : 'w');
+  }
+
+  // Get opponent color(s): may differ from turn (e.g. Checkered)
+  getOppCols(color) {
     return (color == "w" ? "b" : "w");
   }
 
@@ -1174,18 +1220,19 @@ export default class ChessRules {
   getPawnShift(color) {
     return (color == "w" ? -1 : 1);
   }
+  isPawnInitRank(x, color) {
+    return (color == 'w' && x >= 6) || (color == 'b' && x <= 1);
+  }
 
   pieces(color, x, y) {
-    const pawnShift = this.getPawnShift(color);
-    // NOTE: jump 2 squares from first rank (pawns can be here sometimes)
-    const initRank = ((color == 'w' && x >= 6) || (color == 'b' && x <= 1));
+    const pawnShift = this.getPawnShift(color || 'w');
     return {
       'p': {
         "class": "pawn",
         moves: [
           {
             steps: [[pawnShift, 0]],
-            range: (initRank ? 2 : 1)
+            range: (this.isPawnInitRank(x, color) ? 2 : 1)
           }
         ],
         attack: [
@@ -1286,6 +1333,17 @@ export default class ChessRules {
       res += this.size.y;
     return res;
   }
+  // Circular?
+  getX(x) {
+    return x; //generally, no
+  }
+
+  increment([x, y], step) {
+    return [
+      this.getX(x + step[0]),
+      this.getY(y + step[1])
+    ];
+  }
 
   getSegments(curSeg, segStart, segEnd) {
     if (curSeg.length == 0)
@@ -1317,6 +1375,11 @@ export default class ChessRules {
     return this.getColor(x1, y1) !== this.getColor(x2, y2);
   }
 
+  // Teleport & Recycle. Assumption: color(x1,y1) == color(x2,y2)
+  canSelfTake([x1, y1], [x2, y2]) {
+    return !this.isKing(x2, y2);
+  }
+
   canStepOver(i, j, p) {
     // In some variants, objects on boards don't stop movement (Chakart)
     return this.board[i][j] == "";
@@ -1340,23 +1403,22 @@ export default class ChessRules {
     if (!this.options["madrasi"])
       return false;
     const color = this.getColor(x, y);
-    const oppCol = C.GetOppCol(color);
+    const oppCols = this.getOppCols(color);
     const piece = this.getPieceType(x, y);
     const stepSpec = this.getStepSpec(color, x, y, piece);
     const attacks = stepSpec.both.concat(stepSpec.attack);
     for (let a of attacks) {
       outerLoop: for (let step of a.steps) {
-        let [i, j] = [x + step[0], y + step[1]];
-        let stepCounter = 1;
+        let [i, j] = this.increment([x, y], step);
+        let stepCounter = 0;
         while (this.onBoard(i, j) && this.board[i][j] == "") {
           if (a.range <= stepCounter++)
             continue outerLoop;
-          i += step[0];
-          j = this.getY(j + step[1]);
+          [i, j] = this.increment([i, j], step);
         }
         if (
           this.onBoard(i, j) &&
-          this.getColor(i, j) == oppCol &&
+          oppCols.includes(this.getColor(i, j)) &&
           this.getPieceType(i, j) == piece
         ) {
           return true;
@@ -1368,7 +1430,6 @@ export default class ChessRules {
 
   // Stop at the first capture found
   atLeastOneCapture(color) {
-    const oppCol = C.GetOppCol(color);
     const allowed = (sq1, sq2) => {
       return (
         // NOTE: canTake is reversed for Zen.
@@ -1389,7 +1450,6 @@ export default class ChessRules {
                 {
                   attackOnly: true,
                   one: true,
-                  segments: this.options["cylinder"]
                 },
                 allowed
               )
@@ -1400,10 +1460,7 @@ export default class ChessRules {
                 this.options["zen"] &&
                 this.findCapturesOn(
                   [i, j],
-                  {
-                    one: true,
-                    segments: this.options["cylinder"]
-                  },
+                  {one: true},
                   allowed
                 )
               )
@@ -1480,6 +1537,7 @@ export default class ChessRules {
   }
 
   // All possible moves from selected square
+  // TODO: generalize usage if arg "color" (e.g. Checkered)
   getPotentialMovesFrom([x, y], color) {
     if (this.subTurnTeleport == 2)
       return [];
@@ -1491,12 +1549,8 @@ export default class ChessRules {
     let moves = this.getPotentialMovesOf(piece, [x, y]);
     if (piece == "p" && this.hasEnpassant && this.epSquare)
       Array.prototype.push.apply(moves, this.getEnpassantCaptures([x, y]));
-    if (
-      this.isKing(0, 0, piece) && this.hasCastle &&
-      this.castleFlags[color || this.turn].some(v => v < this.size.y)
-    ) {
+    if (this.isKing(0, 0, piece) && this.hasCastle)
       Array.prototype.push.apply(moves, this.getCastleMoves([x, y]));
-    }
     return this.postProcessPotentialMoves(moves);
   }
 
@@ -1504,19 +1558,19 @@ export default class ChessRules {
     if (moves.length == 0)
       return [];
     const color = this.getColor(moves[0].start.x, moves[0].start.y);
-    const oppCol = C.GetOppCol(color);
+    const oppCols = this.getOppCols(color);
 
     if (this.options["capture"] && this.atLeastOneCapture(color))
-      moves = this.capturePostProcess(moves, oppCol);
+      moves = this.capturePostProcess(moves, oppCols);
 
     if (this.options["atomic"])
-      moves = this.atomicPostProcess(moves, color, oppCol);
+      moves = this.atomicPostProcess(moves, color, oppCols);
 
     if (
       moves.length > 0 &&
       this.getPieceType(moves[0].start.x, moves[0].start.y) == "p"
     ) {
-      moves = this.pawnPostProcess(moves, color, oppCol);
+      moves = this.pawnPostProcess(moves, color, oppCols);
     }
 
     if (this.options["cannibal"] && this.options["rifle"])
@@ -1526,22 +1580,22 @@ export default class ChessRules {
     return moves;
   }
 
-  capturePostProcess(moves, oppCol) {
+  capturePostProcess(moves, oppCols) {
     // Filter out non-capturing moves (not using m.vanish because of
     // self captures of Recycle and Teleport).
     return moves.filter(m => {
       return (
         this.board[m.end.x][m.end.y] != "" &&
-        this.getColor(m.end.x, m.end.y) == oppCol
+        oppCols.includes(this.getColor(m.end.x, m.end.y))
       );
     });
   }
 
-  atomicPostProcess(moves, color, oppCol) {
+  atomicPostProcess(moves, color, oppCols) {
     moves.forEach(m => {
       if (
         this.board[m.end.x][m.end.y] != "" &&
-        this.getColor(m.end.x, m.end.y) == oppCol
+        oppCols.includes(this.getColor(m.end.x, m.end.y))
       ) {
         // Explosion!
         let steps = [
@@ -1561,8 +1615,7 @@ export default class ChessRules {
           vanish: []
         });
         for (let step of steps) {
-          let x = m.end.x + step[0];
-          let y = this.getY(m.end.y + step[1]);
+          let [x, y] = this.increment([m.end.x, m.end.y], step);
           if (
             this.onBoard(x, y) &&
             this.board[x][y] != "" &&
@@ -1596,7 +1649,7 @@ export default class ChessRules {
     return moves;
   }
 
-  pawnPostProcess(moves, color, oppCol) {
+  pawnPostProcess(moves, color, oppCols) {
     let moreMoves = [];
     const lastRank = (color == "w" ? 0 : this.size.x - 1);
     const initPiece = this.getPiece(moves[0].start.x, moves[0].start.y);
@@ -1617,7 +1670,7 @@ export default class ChessRules {
       if (
         this.options["cannibal"] &&
         this.board[x2][y2] != "" &&
-        this.getColor(x2, y2) == oppCol
+        oppCols.includes(this.getColor(x2, y2))
       ) {
         finalPieces = [this.getPieceType(x2, y2)];
       }
@@ -1650,7 +1703,7 @@ export default class ChessRules {
         m.appear[0].p = this.pawnPromotions[0];
         for (let i=1; i<this.pawnPromotions.length; i++) {
           let newMv = JSON.parse(JSON.stringify(m));
-          newMv.appear[0].p = this.pawnSpecs.promotions[i];
+          newMv.appear[0].p = this.pawnPromotions[i];
           newMoves.push(newMv);
         }
       }
@@ -1668,7 +1721,6 @@ export default class ChessRules {
         [x, y],
         {
           attackOnly: true,
-          segments: this.options["cylinder"],
           stepSpec: stepSpec
         },
         ([i1, j1], [i2, j2]) => {
@@ -1683,7 +1735,6 @@ export default class ChessRules {
       [x, y],
       {
         moveOnly: !!stepSpec.attack || this.options["zen"],
-        segments: this.options["cylinder"],
         stepSpec: stepSpec
       }
     );
@@ -1696,32 +1747,31 @@ export default class ChessRules {
           !this.isKing(i1, j1) && this.canTake([i2, j2], [i1, j1])
       );
       // Technical step: segments (if any) are reversed
-      if (this.options["cylinder"]) {
-        zenCaptures.forEach(z => {
+      zenCaptures.forEach(z => {
+        if (!!z.segments)
           z.segments = z.segments.reverse().map(s => s.reverse())
-        });
-      }
+      });
       Array.prototype.push.apply(squares, zenCaptures);
     }
-    if (
-      this.options["recycle"] ||
-      (this.options["teleport"] && this.subTurnTeleport == 1)
-    ) {
+    if (this.hasSelfCaptures) {
       const selfCaptures = this.findDestSquares(
         [x, y],
         {
           attackOnly: true,
-          segments: this.options["cylinder"],
           stepSpec: stepSpec
         },
-        ([i1, j1], [i2, j2]) =>
-          this.getColor(i2, j2) == color && !this.isKing(i2, j2)
+        ([i1, j1], [i2, j2]) => {
+          return (
+            this.getColor(i2, j2) == color &&
+            this.canSelfTake([i1, j1], [i2, j2])
+          );
+        }
       );
       Array.prototype.push.apply(squares, selfCaptures);
     }
     return squares.map(s => {
       let mv = this.getBasicMove([x, y], s.sq);
-      if (this.options["cylinder"] && s.segments.length >= 2)
+      if (!!s.segments)
         mv.segments = s.segments;
       return mv;
     });
@@ -1732,23 +1782,21 @@ export default class ChessRules {
       allowed = (sq1, sq2) => this.canTake(sq1, sq2);
     const apparentPiece = this.getPiece(x, y); //how it looks
     let res = [];
-    // Next 3 for Cylinder mode: (unused if !o.segments)
+    // Next 3 for Cylinder mode or circular (useless otherwise)
     let explored = {};
     let segments = [];
     let segStart = [];
     const addSquare = ([i, j]) => {
       let elt = {sq: [i, j]};
-      if (o.segments)
-        elt.segments = this.getSegments(segments, segStart, end);
+      if (segments.length >= 1)
+        elt.segments = this.getSegments(segments, segStart, [i, j]);
       res.push(elt);
     };
     const exploreSteps = (stepArray, mode) => {
       for (let s of stepArray) {
         outerLoop: for (let step of s.steps) {
-          if (o.segments) {
-            segments = [];
-            segStart = [x, y];
-          }
+          segments = [];
+          segStart = [x, y];
           let [i, j] = [x, y];
           let stepCounter = 0;
           while (
@@ -1772,10 +1820,12 @@ export default class ChessRules {
             if (s.range <= stepCounter++)
               continue outerLoop;
             const oldIJ = [i, j];
-            i += step[0];
-            j = this.getY(j + step[1]);
-            if (o.segments && Math.abs(j - oldIJ[1]) > 1) {
-              // Boundary between segments (cylinder mode)
+            [i, j] = this.increment([i, j], step);
+            if (
+              Math.abs(i - oldIJ[0]) != Math.abs(step[0]) ||
+              Math.abs(j - oldIJ[1]) != Math.abs(step[1])
+            ) {
+              // Boundary between segments (cylinder or circular mode)
               segments.push([[segStart[0], segStart[1]], oldIJ]);
               segStart = [i, j];
             }
@@ -1819,7 +1869,7 @@ export default class ChessRules {
   // Search for enemy (or not) pieces attacking [x, y]
   findCapturesOn([x, y], o, allowed) {
     if (!o.byCol)
-      o.byCol = [C.GetOppCol(this.getColor(x, y) || this.turn)];
+      o.byCol = this.getOppCols(this.getColor(x, y) || this.turn);
     let res = [];
     for (let i=0; i<this.size.x; i++) {
       for (let j=0; j<this.size.y; j++) {
@@ -1846,9 +1896,6 @@ export default class ChessRules {
                 {
                   captureTarget: [x, y],
                   captureSteps: [{steps: [s], range: a.range}],
-                  segments: o.segments,
-                  attackOnly: true,
-                  one: false //one and captureTarget are mutually exclusive
                 },
                 allowed
               );
@@ -1976,17 +2023,17 @@ export default class ChessRules {
   getEnpassantCaptures([x, y]) {
     const color = this.getColor(x, y);
     const shiftX = (color == 'w' ? -1 : 1);
-    const oppCol = C.GetOppCol(color);
+    const oppCols = this.getOppCols(color);
     if (
       this.epSquare &&
-      this.epSquare.x == x + shiftX &&
+      this.epSquare.x == x + shiftX && //NOTE: epSquare.x not on edge
       Math.abs(this.getY(this.epSquare.y - y)) == 1 &&
       // Doublemove (and Progressive?) guards:
       this.board[this.epSquare.x][this.epSquare.y] == "" &&
-      this.getColor(x, this.epSquare.y) == oppCol
+      oppCols.includes(this.getColor(x, this.epSquare.y))
     ) {
       const [epx, epy] = [this.epSquare.x, this.epSquare.y];
-      this.board[epx][epy] = oppCol + 'p';
+      this.board[epx][epy] = this.board[x][this.epSquare.y];
       let enpassantMove = this.getBasicMove([x, y], [epx, epy]);
       this.board[epx][epy] = "";
       const lastIdx = enpassantMove.vanish.length - 1; //think Rifle
@@ -1996,11 +2043,12 @@ export default class ChessRules {
     return [];
   }
 
-  getCastleMoves([x, y], finalSquares, castleWith) {
+  getCastleMoves([x, y], finalSquares, castleWith, castleFlags) {
     const c = this.getColor(x, y);
+    castleFlags = castleFlags || this.castleFlags[c];
 
     // Castling ?
-    const oppCol = C.GetOppCol(c);
+    const oppCols = this.getOppCols(c);
     let moves = [];
     // King, then rook:
     finalSquares =
@@ -2011,12 +2059,12 @@ export default class ChessRules {
       castleSide < 2;
       castleSide++ //large, then small
     ) {
-      if (this.castleFlags[c][castleSide] >= this.size.y)
+      if (castleFlags[castleSide] >= this.size.y)
         continue;
       // If this code is reached, rook and king are on initial position
 
       // NOTE: in some variants this is not a rook
-      const rookPos = this.castleFlags[c][castleSide];
+      const rookPos = castleFlags[castleSide];
       const castlingPiece = this.getPiece(x, rookPos);
       if (
         this.board[x][rookPos] == "" ||
@@ -2036,7 +2084,7 @@ export default class ChessRules {
           // will be executed in filterValid() later.
           (
             i != finalSquares[castleSide][0] &&
-            this.underCheck([[x, i]], oppCol)
+            this.underCheck([[x, i]], oppCols)
           )
           ||
           (
@@ -2107,9 +2155,7 @@ export default class ChessRules {
   // MOVES VALIDATION
 
   // Is piece (or square) at given position attacked by "oppCol(s)" ?
-  underAttack([x, y], oppCol) {
-    if (!Array.isArray(oppCol))
-      oppCol = [oppCol];
+  underAttack([x, y], oppCols) {
     // An empty square is considered as king,
     // since it's used only in getCastleMoves (TODO?)
     const king = this.board[x][y] == "" || this.isKing(x, y);
@@ -2119,8 +2165,7 @@ export default class ChessRules {
         this.findCapturesOn(
           [x, y],
           {
-            byCol: oppCol,
-            segments: this.options["cylinder"],
+            byCol: oppCols,
             one: true
           }
         )
@@ -2132,20 +2177,19 @@ export default class ChessRules {
           [x, y],
           {
             attackOnly: true,
-            segments: this.options["cylinder"],
             one: true
           },
-          ([i1, j1], [i2, j2]) => oppCol.includes(this.getColor(i2, j2))
+          ([i1, j1], [i2, j2]) => oppCols.includes(this.getColor(i2, j2))
         )
       )
     );
   }
 
   // Argument is (very generally) an array of squares (= arrays)
-  underCheck(square_s, oppCol) {
+  underCheck(square_s, oppCols) {
     if (this.options["taking"] || this.options["dark"])
       return false;
-    return square_s.some(sq => this.underAttack(sq, oppCol));
+    return square_s.some(sq => this.underAttack(sq, oppCols));
   }
 
   // Scan board for king(s)
@@ -2160,42 +2204,48 @@ export default class ChessRules {
     return res;
   }
 
+  // cb: callback returning a boolean (false if king missing)
+  trackKingWrap(move, kingPos, cb) {
+    if (move.appear.length == 0 && move.vanish.length == 0)
+      return true;
+    const color =
+      (move.vanish.length > 0 ? move.vanish[0].c : move.appear[0].c);
+    let newKingPP = null,
+        sqIdx = 0,
+        res = true; //a priori valid
+    const oldKingPP =
+      move.vanish.find(v => this.isKing(0, 0, v.p) && v.c == color);
+    if (oldKingPP) {
+      // Search king in appear array:
+      newKingPP =
+        move.appear.find(a => this.isKing(0, 0, a.p) && a.c == color);
+      if (newKingPP) {
+        sqIdx = kingPos.findIndex(kp =>
+          kp[0] == oldKingPP.x && kp[1] == oldKingPP.y);
+        kingPos[sqIdx] = [newKingPP.x, newKingPP.y];
+      }
+      else
+        res = false; //king vanished
+    }
+    res &&= cb(kingPos);
+    if (oldKingPP && newKingPP)
+      kingPos[sqIdx] = [oldKingPP.x, oldKingPP.y];
+    return res;
+  }
+
   // 'color' arg because some variants (e.g. Refusal) check opponent moves
   filterValid(moves, color) {
     if (!color)
       color = this.turn;
-    const oppCol = C.GetOppCol(color);
+    const oppCols = this.getOppCols(color);
     let kingPos = this.searchKingPos(color);
-    let filtered = {}; //avoid re-checking similar moves (promotions...)
     return moves.filter(m => {
-      const key = m.start.x + m.start.y + '.' + m.end.x + m.end.y;
-      if (!filtered[key]) {
-        this.playOnBoard(m);
-        let newKingPP = null,
-            sqIdx = 0,
-            res = true; //a priori valid
-        const oldKingPP =
-          m.vanish.find(v => this.isKing(0, 0, v.p) && v.c == color);
-        if (oldKingPP) {
-          // Search king in appear array:
-          newKingPP =
-            m.appear.find(a => this.isKing(0, 0, a.p) && a.c == color);
-          if (newKingPP) {
-            sqIdx = kingPos.findIndex(kp =>
-              kp[0] == oldKingPP.x && kp[1] == oldKingPP.y);
-            kingPos[sqIdx] = [newKingPP.x, newKingPP.y];
-          }
-          else
-            res = false; //king vanished
-        }
-        res &&= !this.underCheck(kingPos, oppCol);
-        if (oldKingPP && newKingPP)
-          kingPos[sqIdx] = [oldKingPP.x, oldKingPP.y];
-        this.undoOnBoard(m);
-        filtered[key] = res;
-        return res;
-      }
-      return filtered[key];
+      this.playOnBoard(m);
+      const res = this.trackKingWrap(m, kingPos, (kp) => {
+        return !this.underCheck(kp, oppCols);
+      });
+      this.undoOnBoard(m);
+      return res;
     });
   }
 
@@ -2217,11 +2267,20 @@ export default class ChessRules {
       this.board[psq.x][psq.y] = psq.c + psq.p;
   }
 
-  updateCastleFlags(move) {
+  // NOTE: arg "castleFlags" for Coregal or Twokings
+  updateCastleFlags(move, castleFlags, king) {
+    castleFlags = castleFlags || this.castleFlags;
+    // If flags already off, no need to re-check:
+    if (
+      Object.values(castleFlags).every(cvals =>
+        cvals.every(val => val >= this.size.y))
+    ) {
+      return;
+    }
     // Update castling flags if start or arrive from/at rook/king locations
     move.appear.concat(move.vanish).forEach(psq => {
-      if (this.isKing(0, 0, psq.p))
-        this.castleFlags[psq.c] = [this.size.y, this.size.y];
+      if ((king && psq.p == king) || (!king && this.isKing(0, 0, psq.p)))
+        castleFlags[psq.c] = [this.size.y, this.size.y];
       // NOTE: not "else if" because king can capture enemy rook...
       let c = "";
       if (psq.x == 0)
@@ -2229,28 +2288,17 @@ export default class ChessRules {
       else if (psq.x == this.size.x - 1)
         c = "w";
       if (c != "") {
-        const fidx = this.castleFlags[c].findIndex(f => f == psq.y);
+        const fidx = castleFlags[c].findIndex(f => f == psq.y);
         if (fidx >= 0)
-          this.castleFlags[c][fidx] = this.size.y;
+          castleFlags[c][fidx] = this.size.y;
       }
     });
   }
 
   prePlay(move) {
-    if (
-      this.hasCastle &&
-      // If flags already off, no need to re-check:
-      Object.values(this.castleFlags).some(cvals =>
-        cvals.some(val => val < this.size.y))
-    ) {
+    if (this.hasCastle)
       this.updateCastleFlags(move);
-    }
     if (this.options["crazyhouse"]) {
-      move.vanish.forEach(v => {
-        const square = C.CoordsToSquare({x: v.x, y: v.y});
-        if (this.ispawn[square])
-          delete this.ispawn[square];
-      });
       if (move.appear.length > 0 && move.vanish.length > 0) {
         // Assumption: something is moving
         const initSquare = C.CoordsToSquare(move.start);
@@ -2269,6 +2317,11 @@ export default class ChessRules {
           delete this.ispawn[destSquare];
         }
       }
+      move.vanish.forEach(v => {
+        const square = C.CoordsToSquare({x: v.x, y: v.y});
+        if (this.ispawn[square])
+          delete this.ispawn[square];
+      });
     }
     const minSize = Math.min(move.appear.length, move.vanish.length);
     if (
@@ -2311,10 +2364,11 @@ export default class ChessRules {
     if (this.options["teleport"]) {
       if (
         this.subTurnTeleport == 1 &&
-        move.vanish.length > move.appear.length &&
+        move.vanish.length == 2 &&
+        move.appear.length == 1 &&
         move.vanish[1].c == this.turn
       ) {
-        const v = move.vanish[move.vanish.length - 1];
+        const v = move.vanish[1];
         this.captured = {x: v.x, y: v.y, c: v.c, p: v.p};
         this.subTurnTeleport = 2;
         return;
@@ -2327,7 +2381,7 @@ export default class ChessRules {
 
   tryChangeTurn(move) {
     if (this.isLastMove(move)) {
-      this.turn = C.GetOppCol(this.turn);
+      this.turn = C.GetOppTurn(this.turn);
       this.movesCount++;
       this.subTurn = 1;
     }
@@ -2339,8 +2393,8 @@ export default class ChessRules {
     if (move.next)
       return false;
     const color = this.turn;
-    const oppKingPos = this.searchKingPos(C.GetOppCol(color));
-    if (oppKingPos.length == 0 || this.underCheck(oppKingPos, color))
+    const oppKingPos = this.searchKingPos(C.GetOppTurn(color));
+    if (oppKingPos.length == 0 || this.underCheck(oppKingPos, [color]))
       return true;
     return (
       (
@@ -2369,7 +2423,7 @@ export default class ChessRules {
         if (this.board[i][j] != "" && this.getColor(i, j) == color) {
           // NOTE: in fact searching for all potential moves from i,j.
           //       I don't believe this is an issue, for now at least.
-          const moves = this.getPotentialMovesFrom([i, j]);
+          const moves = this.getPotentialMovesFrom([i, j], color);
           if (moves.some(m => this.filterValid([m]).length >= 1))
             return true;
         }
@@ -2391,30 +2445,30 @@ export default class ChessRules {
     // Shortcut in case the score was computed before:
     if (move.result)
       return move.result;
-    const color = this.turn;
-    const oppCol = C.GetOppCol(color);
+    const oppTurn = C.GetOppTurn(this.turn);
     const kingPos = {
-      [color]: this.searchKingPos(color),
-      [oppCol]: this.searchKingPos(oppCol)
+      w: this.searchKingPos('w'),
+      b: this.searchKingPos('b')
     };
-    if (kingPos[color].length == 0 && kingPos[oppCol].length == 0)
+    if (kingPos[this.turn].length == 0 && kingPos[oppTurn].length == 0)
       return "1/2";
-    if (kingPos[color].length == 0)
-      return (color == "w" ? "0-1" : "1-0");
-    if (kingPos[oppCol].length == 0)
-      return (color == "w" ? "1-0" : "0-1");
-    if (this.atLeastOneMove(color))
+    if (kingPos[this.turn].length == 0)
+      return (this.turn == "w" ? "0-1" : "1-0");
+    if (kingPos[oppTurn].length == 0)
+      return (this.turn == "w" ? "1-0" : "0-1");
+    if (this.atLeastOneMove(this.turn))
       return "*";
     // No valid move: stalemate or checkmate?
-    if (!this.underCheck(kingPos[color], oppCol))
+    if (!this.underCheck(kingPos[this.turn], this.getOppCols(this.turn)))
       return "1/2";
     // OK, checkmate
-    return (color == "w" ? "0-1" : "1-0");
+    return (this.turn == "w" ? "0-1" : "1-0");
   }
 
   playVisual(move, r) {
     move.vanish.forEach(v => {
-      this.g_pieces[v.x][v.y].remove();
+      if (this.g_pieces[v.x][v.y]) //can be null (e.g. Apocalypse)
+        this.g_pieces[v.x][v.y].remove();
       this.g_pieces[v.x][v.y] = null;
     });
     let chessboard =
@@ -2426,7 +2480,7 @@ export default class ChessRules {
       this.g_pieces[a.x][a.y] = document.createElement("piece");
       C.AddClass_es(this.g_pieces[a.x][a.y],
                     this.pieces(a.c, a.x, a.y)[a.p]["class"]);
-      this.g_pieces[a.x][a.y].classList.add(C.GetColorClass(a.c));
+      this.g_pieces[a.x][a.y].classList.add(V.GetColorClass(a.c));
       this.g_pieces[a.x][a.y].style.width = pieceWidth + "px";
       this.g_pieces[a.x][a.y].style.height = pieceWidth + "px";
       const [ip, jp] = this.getPixelPosition(a.x, a.y, r);
@@ -2483,6 +2537,10 @@ export default class ChessRules {
 
   animateMoving(start, end, drag, segments, cb) {
     let initPiece = this.getDomPiece(start.x, start.y);
+    if (!initPiece) { //TODO: shouldn't occur!
+      cb();
+      return;
+    }
     // NOTE: cloning often not required, but light enough, and simpler
     let movingPiece = initPiece.cloneNode();
     initPiece.style.opacity = "0";
@@ -2503,8 +2561,8 @@ export default class ChessRules {
       C.RemoveClass_es(movingPiece, pieces[startCode]["class"]);
       C.AddClass_es(movingPiece, pieces[drag.p]["class"]);
       if (apparentColor != drag.c) {
-        movingPiece.classList.remove(C.GetColorClass(apparentColor));
-        movingPiece.classList.add(C.GetColorClass(drag.c));
+        movingPiece.classList.remove(V.GetColorClass(apparentColor));
+        movingPiece.classList.add(V.GetColorClass(drag.c));
       }
     }
     container.appendChild(movingPiece);