Draft Checkless. Add 'trackKingWrap' method, untested. Use 'canSelfTake'
[xogo.git] / base_rules.js
index dc25e54..0c316eb 100644 (file)
@@ -1,5 +1,6 @@
 import {Random} from "/utils/alea.js";
 import {ArrayFun} from "/utils/array.js";
+import {FenUtil} from "/utils/setupPieces.js";
 import PiPo from "/utils/PiPo.js";
 import Move from "/utils/Move.js";
 
@@ -215,66 +216,20 @@ export default class ChessRules {
 
   // Setup the initial random-or-not (asymmetric-or-not) position
   genRandInitBaseFen() {
-    let fen, flags = "0707";
-    if (!this.options.randomness)
-      // Deterministic:
-      fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR";
-
-    else {
-      // Randomize
-      let pieces = {w: new Array(8), b: new Array(8)};
-      flags = "";
-      // Shuffle pieces on first (and last rank if randomness == 2)
-      for (let c of ["w", "b"]) {
-        if (c == 'b' && this.options.randomness == 1) {
-          pieces['b'] = pieces['w'];
-          flags += flags;
-          break;
-        }
-        let positions = ArrayFun.range(8);
-        // Get random squares for bishops
-        let randIndex = 2 * Random.randInt(4);
-        const bishop1Pos = positions[randIndex];
-        // The second bishop must be on a square of different color
-        let randIndex_tmp = 2 * Random.randInt(4) + 1;
-        const bishop2Pos = positions[randIndex_tmp];
-        // Remove chosen squares
-        positions.splice(Math.max(randIndex, randIndex_tmp), 1);
-        positions.splice(Math.min(randIndex, randIndex_tmp), 1);
-        // Get random squares for knights
-        randIndex = Random.randInt(6);
-        const knight1Pos = positions[randIndex];
-        positions.splice(randIndex, 1);
-        randIndex = Random.randInt(5);
-        const knight2Pos = positions[randIndex];
-        positions.splice(randIndex, 1);
-        // Get random square for queen
-        randIndex = Random.randInt(4);
-        const queenPos = positions[randIndex];
-        positions.splice(randIndex, 1);
-        // Rooks and king positions are now fixed,
-        // because of the ordering rook-king-rook
-        const rook1Pos = positions[0];
-        const kingPos = positions[1];
-        const rook2Pos = positions[2];
-        // Finally put the shuffled pieces in the board array
-        pieces[c][rook1Pos] = "r";
-        pieces[c][knight1Pos] = "n";
-        pieces[c][bishop1Pos] = "b";
-        pieces[c][queenPos] = "q";
-        pieces[c][kingPos] = "k";
-        pieces[c][bishop2Pos] = "b";
-        pieces[c][knight2Pos] = "n";
-        pieces[c][rook2Pos] = "r";
-        flags += rook1Pos.toString() + rook2Pos.toString();
+    const s = FenUtil.setupPieces(
+      ['r', 'n', 'b', 'q', 'k', 'b', 'n', 'r'],
+      {
+        randomness: this.options["randomness"],
+        between: {p1: 'k', p2: 'r'},
+        diffCol: ['b'],
+        flags: ['r']
       }
-      fen = (
-        pieces["b"].join("") +
-        "/pppppppp/8/8/8/8/PPPPPPPP/" +
-        pieces["w"].join("").toUpperCase()
-      );
-    }
-    return { fen: fen, o: {flags: flags} };
+    );
+    return {
+      fen: s.b.join("") + "/pppppppp/8/8/8/8/PPPPPPPP/" +
+           s.w.join("").toUpperCase(),
+      o: {flags: s.flags}
+    };
   }
 
   // "Parse" FEN: just return untransformed string data
@@ -331,9 +286,9 @@ export default class ChessRules {
   // Position part of the FEN string
   getPosition() {
     let position = "";
-    for (let i = 0; i < this.size.y; i++) {
+    for (let i = 0; i < this.size.x; i++) {
       let emptyCount = 0;
-      for (let j = 0; j < this.size.x; j++) {
+      for (let j = 0; j < this.size.y; j++) {
         if (this.board[i][j] == "")
           emptyCount++;
         else {
@@ -348,7 +303,7 @@ export default class ChessRules {
       if (emptyCount > 0)
         // "Flush remainder"
         position += C.FenEmptySquares(emptyCount);
-      if (i < this.size.y - 1)
+      if (i < this.size.x - 1)
         position += "/"; //separate rows
     }
     return position;
@@ -404,9 +359,6 @@ export default class ChessRules {
       if (this.options[opt.variable] === undefined)
         this.options[opt.variable] = opt.defaut;
     });
-    if (o.genFenOnly)
-      // This object will be used only for initial FEN generation
-      return;
 
     // Some variables
     this.playerColor = o.color;
@@ -518,7 +470,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(
@@ -567,7 +519,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");
@@ -605,7 +556,7 @@ export default class ChessRules {
     chessboard.style.top = spaceTop + "px";
     // Give sizes instead of recomputing them,
     // because chessboard might not be drawn yet.
-    this.setupPieces({
+    this.setupVisualPieces({
       width: cbWidth,
       height: cbHeight,
       x: spaceLeft,
@@ -645,7 +596,7 @@ export default class ChessRules {
     return board;
   }
 
-  setupPieces(r) {
+  setupVisualPieces(r) {
     let chessboard =
       document.getElementById(this.containerId).querySelector(".chessboard");
     if (!r)
@@ -689,7 +640,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");
         }
@@ -756,7 +707,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;
@@ -779,8 +730,8 @@ export default class ChessRules {
       piece = "k"; //capturing cannibal king: back to king form
     const oldCount = this.reserve[color][piece];
     this.reserve[color][piece] = count;
-    // Redrawing is much easier if count==0
-    if ([oldCount, count].includes(0))
+    // Redrawing is much easier if count==0 (or undefined)
+    if ([oldCount, count].some(item => !item))
       this.re_drawReserve([color]);
     else {
       const numId = this.getReserveNumId(color, piece);
@@ -1013,11 +964,10 @@ export default class ChessRules {
     // TODO: onpointerdown/move/up ? See reveal.js /controllers/touch.js
   }
 
+  // NOTE: not called if isDiagram
   removeListeners() {
     let container = document.getElementById(this.containerId);
     this.windowResizeObs.unobserve(container);
-    if (this.isDiagram)
-      return; //no listeners in this case
     if ('onmousedown' in window) {
       this.mouseListeners.forEach(ml => {
         document.removeEventListener(ml.type, ml.listener);
@@ -1066,7 +1016,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);
@@ -1114,7 +1064,8 @@ 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],
             y = this.getY(this.epSquare.y - step[1]);
@@ -1190,7 +1141,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) {
@@ -1201,8 +1152,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");
   }
 
@@ -1220,8 +1175,12 @@ export default class ChessRules {
   ////////////////////////
   // PIECES SPECIFICATIONS
 
+  getPawnShift(color) {
+    return (color == "w" ? -1 : 1);
+  }
+
   pieces(color, x, y) {
-    const pawnShift = (color == "w" ? -1 : 1);
+    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));
     return {
@@ -1242,13 +1201,13 @@ export default class ChessRules {
       },
       'r': {
         "class": "rook",
-        moves: [
+        both: [
           {steps: [[0, 1], [0, -1], [1, 0], [-1, 0]]}
         ]
       },
       'n': {
         "class": "knight",
-        moves: [
+        both: [
           {
             steps: [
               [1, 2], [1, -2], [-1, 2], [-1, -2],
@@ -1260,13 +1219,13 @@ export default class ChessRules {
       },
       'b': {
         "class": "bishop",
-        moves: [
+        both: [
           {steps: [[1, 1], [1, -1], [-1, 1], [-1, -1]]}
         ]
       },
       'q': {
         "class": "queen",
-        moves: [
+        both: [
           {
             steps: [
               [0, 1], [0, -1], [1, 0], [-1, 0],
@@ -1277,7 +1236,7 @@ export default class ChessRules {
       },
       'k': {
         "class": "king",
-        moves: [
+        both: [
           {
             steps: [
               [0, 1], [0, -1], [1, 0], [-1, 0],
@@ -1341,7 +1300,20 @@ export default class ChessRules {
   }
 
   getStepSpec(color, x, y, piece) {
-    return this.pieces(color, x, y)[piece || this.getPieceType(x, y)];
+    let pieceType = piece;
+    let allSpecs = this.pieces(color, x, y);
+    if (!piece)
+      pieceType = this.getPieceType(x, y);
+    else if (allSpecs[piece].moveas)
+      pieceType = allSpecs[piece].moveas;
+    let res = allSpecs[pieceType];
+    if (!res["both"])
+      res.both = [];
+    if (!res["moves"])
+      res.moves = [];
+    if (!res["attack"])
+      res.attack = [];
+    return res;
   }
 
   // Can thing on square1 capture thing on square2?
@@ -1349,6 +1321,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] == "";
@@ -1372,10 +1349,10 @@ 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.attack || stepSpec.moves;
+    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]];
@@ -1388,7 +1365,7 @@ export default class ChessRules {
         }
         if (
           this.onBoard(i, j) &&
-          this.getColor(i, j) == oppCol &&
+          oppCols.includes(this.getColor(i, j)) &&
           this.getPieceType(i, j) == piece
         ) {
           return true;
@@ -1400,7 +1377,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.
@@ -1512,6 +1488,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 [];
@@ -1536,19 +1513,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"])
@@ -1558,22 +1535,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 = [
@@ -1628,7 +1605,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);
@@ -1649,7 +1626,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)];
       }
@@ -1746,14 +1723,18 @@ export default class ChessRules {
           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 (this.options["cylinder"] && !!s.segments && s.segments.length >= 2)
         mv.segments = s.segments;
       return mv;
     });
@@ -1771,10 +1752,10 @@ export default class ChessRules {
     const addSquare = ([i, j]) => {
       let elt = {sq: [i, j]};
       if (o.segments)
-        elt.segments = this.getSegments(segments, segStart, end);
+        elt.segments = this.getSegments(segments, segStart, [i, j]);
       res.push(elt);
     };
-    const exploreSteps = (stepArray) => {
+    const exploreSteps = (stepArray, mode) => {
       for (let s of stepArray) {
         outerLoop: for (let step of s.steps) {
           if (o.segments) {
@@ -1793,9 +1774,9 @@ export default class ChessRules {
                 !o.captureTarget ||
                 (o.captureTarget[0] == i && o.captureTarget[1] == j)
               ) {
-                if (o.one && !o.attackOnly)
+                if (o.one && mode != "attack")
                   return true;
-                if (!o.attackOnly)
+                if (mode != "attack")
                   addSquare(!o.captureTarget ? [i, j] : [x, y]);
                 if (o.captureTarget)
                   return res[0];
@@ -1818,9 +1799,9 @@ export default class ChessRules {
           if (!explored[i + "." + j]) {
             explored[i + "." + j] = true;
             if (allowed([x, y], [i, j])) {
-              if (o.one && !o.moveOnly)
+              if (o.one && mode != "moves")
                 return true;
-              if (!o.moveOnly)
+              if (mode != "moves")
                 addSquare(!o.captureTarget ? [i, j] : [x, y]);
               if (
                 o.captureTarget &&
@@ -1835,17 +1816,15 @@ export default class ChessRules {
       return undefined; //default, but let's explicit it
     };
     if (o.captureTarget)
-      return exploreSteps(o.captureSteps)
+      return exploreSteps(o.captureSteps, "attack");
     else {
       const stepSpec =
         o.stepSpec || this.getStepSpec(this.getColor(x, y), x, y);
       let outOne = false;
-      if (!o.attackOnly || !stepSpec.attack)
-        outOne = exploreSteps(stepSpec.moves);
-      if (!outOne && !o.moveOnly && !!stepSpec.attack) {
-        o.attackOnly = true; //ok because o is always a temporary object
-        outOne = exploreSteps(stepSpec.attack);
-      }
+      if (!o.attackOnly)
+        outOne = exploreSteps(stepSpec.both.concat(stepSpec.moves), "moves");
+      if (!outOne && !o.moveOnly)
+        outOne = exploreSteps(stepSpec.both.concat(stepSpec.attack), "attack");
       return (o.one ? outOne : res);
     }
   }
@@ -1853,7 +1832,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++) {
@@ -1868,7 +1847,7 @@ export default class ChessRules {
           if (this.canStepOver(x, y, apparentPiece))
             continue;
           const stepSpec = this.getStepSpec(colIJ, i, j);
-          const attacks = stepSpec.attack || stepSpec.moves;
+          const attacks = stepSpec.attack.concat(stepSpec.both);
           for (let a of attacks) {
             for (let s of a.steps) {
               // Quick check: if step isn't compatible, don't even try
@@ -1880,9 +1859,7 @@ 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
+                  segments: o.segments
                 },
                 allowed
               );
@@ -2010,17 +1987,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 &&
       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
@@ -2034,7 +2011,7 @@ export default class ChessRules {
     const c = this.getColor(x, y);
 
     // Castling ?
-    const oppCol = C.GetOppCol(c);
+    const oppCols = this.getOppCols(c);
     let moves = [];
     // King, then rook:
     finalSquares =
@@ -2070,7 +2047,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)
           )
           ||
           (
@@ -2140,8 +2117,8 @@ export default class ChessRules {
   ////////////////////
   // MOVES VALIDATION
 
-  // Is piece (or square) at given position attacked by "oppCol" ?
-  underAttack([x, y], oppCol) {
+  // Is piece (or square) at given position attacked by "oppCol(s)" ?
+  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);
@@ -2151,7 +2128,7 @@ export default class ChessRules {
         this.findCapturesOn(
           [x, y],
           {
-            byCol: [oppCol],
+            byCol: oppCols,
             segments: this.options["cylinder"],
             one: true
           }
@@ -2167,19 +2144,17 @@ export default class ChessRules {
             segments: this.options["cylinder"],
             one: true
           },
-          ([i1, j1], [i2, j2]) => this.getColor(i2, j2) == oppCol
+          ([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;
-    if (!Array.isArray(square_s[0]))
-      square_s = [square_s];
-    return square_s.some(sq => this.underAttack(sq, oppCol));
+    return square_s.some(sq => this.underAttack(sq, oppCols));
   }
 
   // Scan board for king(s)
@@ -2194,42 +2169,44 @@ export default class ChessRules {
     return res;
   }
 
+  // cb: callback returning a boolean (false if king missing)
+  trackKingWrap(move, kingPos, cb) {
+    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;
     });
   }
 
@@ -2340,14 +2317,13 @@ export default class ChessRules {
   }
 
   postPlay(move) {
-    const color = this.turn;
     if (this.options["dark"])
       this.updateEnlightened();
     if (this.options["teleport"]) {
       if (
         this.subTurnTeleport == 1 &&
         move.vanish.length > move.appear.length &&
-        move.vanish[1].c == color
+        move.vanish[1].c == this.turn
       ) {
         const v = move.vanish[move.vanish.length - 1];
         this.captured = {x: v.x, y: v.y, c: v.c, p: v.p};
@@ -2357,8 +2333,12 @@ export default class ChessRules {
       this.subTurnTeleport = 1;
       this.captured = null;
     }
+    this.tryChangeTurn(move);
+  }
+
+  tryChangeTurn(move) {
     if (this.isLastMove(move)) {
-      this.turn = C.GetOppCol(color);
+      this.turn = C.GetOppTurn(this.turn);
       this.movesCount++;
       this.subTurn = 1;
     }
@@ -2370,8 +2350,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 (
       (
@@ -2400,7 +2380,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;
         }
@@ -2422,30 +2402,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 =
@@ -2457,7 +2437,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);
@@ -2476,32 +2456,37 @@ export default class ChessRules {
   buildMoveStack(move, r) {
     this.moveStack.push(move);
     this.computeNextMove(move);
-    this.play(move);
-    const newTurn = this.turn;
-    if (this.moveStack.length == 1 && !this.hideMoves)
-      this.playVisual(move, r);
-    if (move.next) {
-      this.gameState = {
-        fen: this.getFen(),
-        board: JSON.parse(JSON.stringify(this.board)) //easier
-      };
-      this.buildMoveStack(move.next, r);
-    }
-    else {
-      if (this.moveStack.length == 1) {
-        // Usual case (one normal move)
-        this.afterPlay(this.moveStack, newTurn, {send: true, res: true});
-        this.moveStack = []
+    const then = () => {
+      const newTurn = this.turn;
+      if (this.moveStack.length == 1 && !this.hideMoves)
+        this.playVisual(move, r);
+      if (move.next) {
+        this.gameState = {
+          fen: this.getFen(),
+          board: JSON.parse(JSON.stringify(this.board)) //easier
+        };
+        this.buildMoveStack(move.next, r);
       }
       else {
-        this.afterPlay(this.moveStack, newTurn, {send: true, res: false});
-        this.re_initFromFen(this.gameState.fen, this.gameState.board);
-        this.playReceivedMove(this.moveStack.slice(1), () => {
-          this.afterPlay(this.moveStack, newTurn, {send: false, res: true});
-          this.moveStack = []
-        });
+        if (this.moveStack.length == 1) {
+          // Usual case (one normal move)
+          this.afterPlay(this.moveStack, newTurn, {send: true, res: true});
+          this.moveStack = [];
+        }
+        else {
+          this.afterPlay(this.moveStack, newTurn, {send: true, res: false});
+          this.re_initFromFen(this.gameState.fen, this.gameState.board);
+          this.playReceivedMove(this.moveStack.slice(1), () => {
+            this.afterPlay(this.moveStack, newTurn, {send: false, res: true});
+            this.moveStack = [];
+          });
+        }
       }
-    }
+    };
+    // If hiding moves, then they are revealed in play() with callback
+    this.play(move, this.hideMoves ? then : null);
+    if (!this.hideMoves)
+      then();
   }
 
   // Implemented in variants using (automatic) moveStack
@@ -2509,6 +2494,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";
@@ -2529,8 +2518,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);
@@ -2632,8 +2621,9 @@ export default class ChessRules {
 
   launchAnimation(moves, container, callback) {
     if (this.hideMoves) {
-      moves.forEach(m => this.play(m));
-      callback();
+      for (let i=0; i<moves.length; i++)
+        // If hiding moves, they are revealed into play():
+        this.play(moves[i], i == moves.length - 1 ? callback : () => {});
       return;
     }
     const r = container.querySelector(".chessboard").getBoundingClientRect();
@@ -2672,6 +2662,7 @@ export default class ChessRules {
     let container = document.getElementById(this.containerId);
     if (document.hidden) {
       document.onvisibilitychange = () => {
+        // TODO here: page reload ?! (some issues if tab changed...)
         document.onvisibilitychange = undefined;
         checkDisplayThenAnimate(700);
       };