Experimental: improve animation, reduce lags in stack moves sending. Add Allmate
[xogo.git] / base_rules.js
index 4b387b6..6562673 100644 (file)
@@ -3,6 +3,21 @@ import { ArrayFun } from "/utils/array.js";
 import PiPo from "/utils/PiPo.js";
 import Move from "/utils/Move.js";
 
+// Helper class for move animation
+class TargetObj {
+
+  constructor(callOnComplete) {
+    this.value = 0;
+    this.target = 0;
+    this.callOnComplete = callOnComplete;
+  }
+  increment() {
+    if (++this.value == this.target)
+      this.callOnComplete();
+  }
+
+};
+
 // NOTE: x coords: top to bottom (white perspective); y: left to right
 // NOTE: ChessRules is aliased as window.C, and variants as window.V
 export default class ChessRules {
@@ -96,6 +111,10 @@ export default class ChessRules {
     return !!this.options["dark"];
   }
 
+  get hasMoveStack() {
+    return false;
+  }
+
   // Some variants use click infos:
   doClick(coords) {
     if (typeof coords.x != "number")
@@ -252,7 +271,7 @@ export default class ChessRules {
       parts.push(`"flags":"${flags}"`);
     if (this.hasEnpassant)
       parts.push('"enpassant":"-"');
-    if (this.hasReserve)
+    if (this.hasReserveFen)
       parts.push('"reserve":"000000000000"');
     if (this.options["crazyhouse"])
       parts.push('"ispawn":"-"');
@@ -391,17 +410,21 @@ export default class ChessRules {
     // Fen string fully describes the game state
     if (!o.fen)
       o.fen = this.genRandInitFen(o.seed);
-    const fenParsed = this.parseFen(o.fen);
-    this.board = this.getBoard(fenParsed.position);
-    this.turn = fenParsed.turn;
-    this.movesCount = parseInt(fenParsed.movesCount, 10);
-    this.setOtherVariables(fenParsed);
+    this.re_initFromFen(o.fen);
 
     // Graphical (can use variables defined above)
     this.containerId = o.element;
     this.graphicalInit();
   }
 
+  re_initFromFen(fen, oldBoard) {
+    const fenParsed = this.parseFen(fen);
+    this.board = oldBoard || this.getBoard(fenParsed.position);
+    this.turn = fenParsed.turn;
+    this.movesCount = parseInt(fenParsed.movesCount, 10);
+    this.setOtherVariables(fenParsed);
+  }
+
   // Turn position fen into double array ["wb","wp","bk",...]
   getBoard(position) {
     const rows = position.split("/");
@@ -433,7 +456,6 @@ export default class ChessRules {
       this.initReserves(fenParsed.reserve);
     if (this.options["crazyhouse"])
       this.initIspawn(fenParsed.ispawn);
-    this.subTurn = 1; //may be unused
     if (this.options["teleport"]) {
       this.subTurnTeleport = 1;
       this.captured = null;
@@ -443,6 +465,9 @@ export default class ChessRules {
       this.enlightened = ArrayFun.init(this.size.x, this.size.y, false);
       this.updateEnlightened();
     }
+    this.subTurn = 1; //may be unused
+    if (!this.moveStack) //avoid resetting (unwanted)
+      this.moveStack = [];
   }
 
   updateEnlightened() {
@@ -1082,8 +1107,9 @@ export default class ChessRules {
   }
 
   // Piece type on square (i,j)
-  getPieceType(i, j) {
-    const p = this.getPiece(i, j);
+  getPieceType(i, j, p) {
+    if (!p)
+      p = this.getPiece(i, j);
     return C.CannibalKings[p] || p; //a cannibal king move as...
   }
 
@@ -1092,7 +1118,7 @@ export default class ChessRules {
     return (color == "w" ? "b" : "w");
   }
 
-  // Can thing on square1 capture (no return) thing on square2?
+  // Can thing on square1 capture (enemy) thing on square2?
   canTake([x1, y1], [x2, y2]) {
     return (this.getColor(x1, y1) !== this.getColor(x2, y2));
   }
@@ -1214,6 +1240,8 @@ export default class ChessRules {
         if (this.board[i][j] != "" && this.getColor(i, j) == color) {
           const allSpecs = this.pieces(color, i, j)
           let specs = allSpecs[this.getPieceType(i, j)];
+          if (specs.moveas)
+            specs = allSpecs[specs.moveas];
           const attacks = specs.attack || specs.moves;
           for (let a of attacks) {
             outerLoop: for (let step of a.steps) {
@@ -1482,7 +1510,10 @@ export default class ChessRules {
     const color = this.getColor(x, y);
     const oppCol = C.GetOppCol(color);
     const piece = this.getPieceType(x, y); //ok not cannibal king
-    const stepSpec = this.pieces(color, x, y)[piece];
+    const allSpecs = this.pieces(color, x, y);
+    let stepSpec = allSpecs[piece];
+    if (stepSpec.moveas)
+      stepSpec = allSpecs[stepSpec.moveas];
     const attacks = stepSpec.attack || stepSpec.moves;
     for (let a of attacks) {
       outerLoop: for (let step of a.steps) {
@@ -1506,7 +1537,7 @@ export default class ChessRules {
     return false;
   }
 
-  canStepOver(i, j) {
+  canStepOver(i, j, p) {
     // In some variants, objects on boards don't stop movement (Chakart)
     return this.board[i][j] == "";
   }
@@ -1514,7 +1545,11 @@ export default class ChessRules {
   // Generic method to find possible moves of "sliding or jumping" pieces
   getPotentialMovesOf(piece, [x, y]) {
     const color = this.getColor(x, y);
-    const stepSpec = this.pieces(color, x, y)[piece];
+    const apparentPiece = this.getPiece(x, y); //how it looks
+    const allSpecs = this.pieces(color, x, y);
+    let stepSpec = allSpecs[piece];
+    if (stepSpec.moveas)
+      stepSpec = allSpecs[stepSpec.moveas];
     let moves = [];
     // Next 3 for Cylinder mode:
     let explored = {};
@@ -1539,7 +1574,7 @@ export default class ChessRules {
           let stepCounter = 0;
           while (
             this.onBoard(i, j) &&
-            (this.canStepOver(i, j) || (i == x && j == y))
+            ((i == x && j == y) || this.canStepOver(i, j, apparentPiece))
           ) {
             if (
               type != "attack" &&
@@ -1610,8 +1645,14 @@ export default class ChessRules {
         ) {
           if (args.zen && this.isKing(this.getPiece(i, j)))
             continue; //king not captured in this way
-          const stepSpec =
-            this.pieces(args.oppCol, i, j)[this.getPieceType(i, j)];
+          const apparentPiece = this.getPiece(i, j);
+          // Quick check: does this potential attacker target x,y ?
+          if (this.canStepOver(x, y, apparentPiece))
+            continue;
+          const allSpecs = this.pieces(args.oppCol, i, j);
+          let stepSpec = allSpecs[this.getPieceType(i, j)];
+          if (stepSpec.moveas)
+            stepSpec = allSpecs[stepSpec.moveas];
           const attacks = stepSpec.attack || stepSpec.moves;
           for (let a of attacks) {
             for (let s of a.steps) {
@@ -1754,8 +1795,15 @@ export default class ChessRules {
       s.y == e.y &&
       Math.abs(s.x - e.x) == 2 &&
       // Next conditions for variants like Atomic or Rifle, Recycle...
-      (move.appear.length > 0 && move.appear[0].p == "p") &&
-      (move.vanish.length > 0 && move.vanish[0].p == "p")
+      (
+        move.appear.length > 0 &&
+        this.getPieceType(0, 0, move.appear[0].p) == "p"
+      )
+      &&
+      (
+        move.vanish.length > 0 &&
+        this.getPieceType(0, 0, move.vanish[0].p) == "p"
+      )
     ) {
       return {
         x: (s.x + e.x) / 2,
@@ -1945,12 +1993,12 @@ export default class ChessRules {
         let square = kingPos,
             res = true; //a priori valid
         if (m.vanish.some(v => {
-          return C.CannibalKings[v.p] && v.c == color;
+          return this.isKing(v.p) && v.c == color;
         })) {
           // Search king in appear array:
           const newKingIdx =
             m.appear.findIndex(a => {
-              return C.CannibalKings[a.p] && a.c == color;
+              return this.isKing(a.p) && a.c == color;
             });
           if (newKingIdx >= 0)
             square = [m.appear[newKingIdx].x, m.appear[newKingIdx].y];
@@ -2105,35 +2153,38 @@ export default class ChessRules {
       this.subTurnTeleport = 1;
       this.captured = null;
     }
-    if (this.options["balance"]) {
-      if (![1, 3].includes(this.movesCount))
-        this.turn = oppCol;
-    }
-    else {
+    if (
+      (
+        this.options["doublemove"] &&
+        this.movesCount >= 1 &&
+        this.subTurn == 1
+      ) ||
+      (this.options["progressive"] && this.subTurn <= this.movesCount)
+    ) {
+      const oppKingPos = this.searchKingPos(oppCol);
       if (
+        oppKingPos[0] >= 0 &&
         (
-          this.options["doublemove"] &&
-          this.movesCount >= 1 &&
-          this.subTurn == 1
-        ) ||
-        (this.options["progressive"] && this.subTurn <= this.movesCount)
+          this.options["taking"] ||
+          !this.underCheck(oppKingPos, color)
+        )
       ) {
-        const oppKingPos = this.searchKingPos(oppCol);
-        if (
-          oppKingPos[0] >= 0 &&
-          (
-            this.options["taking"] ||
-            !this.underCheck(oppKingPos, color)
-          )
-        ) {
-          this.subTurn++;
-          return;
-        }
+        this.subTurn++;
+        return;
       }
+    }
+    if (this.isLastMove(move)) {
       this.turn = oppCol;
+      this.movesCount++;
+      this.subTurn = 1;
     }
-    this.movesCount++;
-    this.subTurn = 1;
+  }
+
+  isLastMove(move) {
+    return (
+      (this.options["balance"] && ![1, 3].includes(this.movesCount)) ||
+      !move.next
+    );
   }
 
   // "Stop at the first move found"
@@ -2210,11 +2261,50 @@ export default class ChessRules {
   }
 
   playPlusVisual(move, r) {
+    if (this.hasMoveStack)
+      this.buildMoveStack(move);
+    else {
+      this.play(move);
+      this.playVisual(move, r);
+      this.afterPlay(move, this.turn, {send: true, res: true}); //user method
+    }
+  }
+
+  // TODO: send stack receive stack, or allow incremental? (good/bad points)
+  buildMoveStack(move) {
+    this.moveStack.push(move);
+    this.computeNextMove(move);
     this.play(move);
-    this.playVisual(move, r);
-    this.afterPlay(move); //user method
+    const newTurn = this.turn;
+    if (this.moveStack.length == 1) {
+      this.playVisual(move);
+      this.gameState = {
+        fen: this.getFen(),
+        board: JSON.parse(JSON.stringify(this.board)) //easier
+      };
+    }
+    if (move.next)
+      this.buildMoveStack(move.next);
+    else {
+      // Send, animate + play until here
+      if (this.moveStack.length == 1) {
+        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 = []
+        });
+      }
+    }
   }
 
+  // Implemented in variants using (automatic) moveStack
+  computeNextMove(move) {}
+
   getMaxDistance(r) {
     // Works for all rectangular boards:
     return Math.sqrt(r.width ** 2 + r.height ** 2);
@@ -2224,41 +2314,37 @@ export default class ChessRules {
     return (typeof x == "string" ? this.r_pieces : this.g_pieces)[x][y];
   }
 
-  animate(move, callback) {
-    if (this.noAnimate || move.noAnimate) {
-      callback();
-      return;
-    }
-    let initPiece = this.getDomPiece(move.start.x, move.start.y);
-    // NOTE: cloning generally not required, but light enough, and simpler
+  animateMoving(start, end, drag, segments, cb) {
+    let initPiece = this.getDomPiece(start.x, start.y);
+    // NOTE: cloning often not required, but light enough, and simpler
     let movingPiece = initPiece.cloneNode();
     initPiece.style.opacity = "0";
     let container =
       document.getElementById(this.containerId)
     const r = container.querySelector(".chessboard").getBoundingClientRect();
-    if (typeof move.start.x == "string") {
+    if (typeof start.x == "string") {
       // Need to bound width/height (was 100% for reserve pieces)
       const pieceWidth = this.getPieceWidth(r.width);
       movingPiece.style.width = pieceWidth + "px";
       movingPiece.style.height = pieceWidth + "px";
     }
     const maxDist = this.getMaxDistance(r);
-    const apparentColor = this.getColor(move.start.x, move.start.y);
-    const pieces = this.pieces(apparentColor, move.start.x, move.start.y);
-    if (move.drag) {
-      const startCode = this.getPiece(move.start.x, move.start.y);
+    const apparentColor = this.getColor(start.x, start.y);
+    const pieces = this.pieces(apparentColor, start.x, start.y);
+    if (drag) {
+      const startCode = this.getPiece(start.x, start.y);
       C.RemoveClass_es(movingPiece, pieces[startCode]["class"]);
-      C.AddClass_es(movingPiece, pieces[move.drag.p]["class"]);
-      if (apparentColor != move.drag.c) {
+      C.AddClass_es(movingPiece, pieces[drag.p]["class"]);
+      if (apparentColor != drag.c) {
         movingPiece.classList.remove(C.GetColorClass(apparentColor));
-        movingPiece.classList.add(C.GetColorClass(move.drag.c));
+        movingPiece.classList.add(C.GetColorClass(drag.c));
       }
     }
     container.appendChild(movingPiece);
     const animateSegment = (index, cb) => {
       // NOTE: move.drag could be generalized per-segment (usage?)
-      const [i1, j1] = move.segments[index][0];
-      const [i2, j2] = move.segments[index][1];
+      const [i1, j1] = segments[index][0];
+      const [i2, j2] = segments[index][1];
       const dep = this.getPixelPosition(i1, j1, r);
       const arr = this.getPixelPosition(i2, j2, r);
       movingPiece.style.transitionDuration = "0s";
@@ -2274,24 +2360,63 @@ export default class ChessRules {
         setTimeout(cb, duration * 1000);
       }, 50);
     };
-    if (!move.segments) {
-      move.segments = [
-        [[move.start.x, move.start.y], [move.end.x, move.end.y]]
-      ];
-    }
     let index = 0;
     const animateSegmentCallback = () => {
-      if (index < move.segments.length)
+      if (index < segments.length)
         animateSegment(index++, animateSegmentCallback);
       else {
         movingPiece.remove();
         initPiece.style.opacity = "1";
-        callback();
+        cb();
       }
     };
     animateSegmentCallback();
   }
 
+  // Input array of objects with at least fields x,y (e.g. PiPo)
+  animateFading(arr, cb) {
+    const animLength = 350; //TODO: 350ms? More? Less?
+    arr.forEach(v => {
+      let fadingPiece = this.getDomPiece(v.x, v.y);
+      fadingPiece.style.transitionDuration = (animLength / 1000) + "s";
+      fadingPiece.style.opacity = "0";
+    });
+    setTimeout(cb, animLength);
+  }
+
+  animate(move, callback) {
+    if (this.noAnimate || move.noAnimate) {
+      callback();
+      return;
+    }
+    let segments = move.segments;
+    if (!segments)
+      segments = [ [[move.start.x, move.start.y], [move.end.x, move.end.y]] ];
+    let targetObj = new TargetObj(callback);
+    if (move.start.x != move.end.x || move.start.y != move.end.y) {
+      targetObj.target++;
+      this.animateMoving(move.start, move.end, move.drag, segments,
+                         () => targetObj.increment());
+    }
+    if (move.vanish.length > move.appear.length) {
+      const arr = move.vanish.slice(move.appear.length)
+                    .filter(v => v.x != move.end.x || v.y != move.end.y);
+      if (arr.length > 0) {
+        targetObj.target++;
+        this.animateFading(arr, () => targetObj.increment());
+      }
+    }
+    targetObj.target +=
+      this.customAnimate(move, segments, () => targetObj.increment());
+    if (targetObj.target == 0)
+      callback();
+  }
+
+  // Potential other animations (e.g. for Suction variant)
+  customAnimate(move, segments, cb) {
+    return 0; //nb of targets
+  }
+
   playReceivedMove(moves, callback) {
     const launchAnimation = () => {
       const r = container.querySelector(".chessboard").getBoundingClientRect();