Experimental: improve animation, reduce lags in stack moves sending. Add Allmate
authorBenjamin Auder <benjamin.auder@somewhere>
Sat, 25 Jun 2022 10:22:15 +0000 (12:22 +0200)
committerBenjamin Auder <benjamin.auder@somewhere>
Sat, 25 Jun 2022 10:22:15 +0000 (12:22 +0200)
app.js
base_rules.js
variants.js
variants/Allmate/class.js [new file with mode: 0644]
variants/Allmate/rules.html [new file with mode: 0644]
variants/Allmate/style.css [new file with mode: 0644]
variants/Chakart/class.js
variants/Suction/class.js

diff --git a/app.js b/app.js
index 4c0b5e4..6f17c64 100644 (file)
--- a/app.js
+++ b/app.js
@@ -456,9 +456,28 @@ function notifyMe(code) {
 
 let curMoves = [],
     lastFen;
 
 let curMoves = [],
     lastFen;
-const afterPlay = (move_s) => {
-  const callbackAfterSend = () => {
-    curMoves = [];
+const afterPlay = (move_s, newTurn, ops) => {
+  if (ops.send) {
+    // Pack into one moves array, then send (if turn changed)
+    if (Array.isArray(move_s))
+      // Array of simple moves (e.g. Chakart)
+      Array.prototype.push.apply(curMoves, move_s);
+    else
+      // Usual case
+      curMoves.push(move_s);
+    if (newTurn != playerColor) {
+      send("newmove",
+           {gid: gid, moves: curMoves, fen: vr.getFen()},
+           {
+             retry: true,
+             success: () => curMoves = [],
+             error: () => alert("Move not sent: reload page")
+           }
+      );
+    }
+  }
+  if (ops.res && newTurn != playerColor) {
+    toggleTurnIndicator(false); //now all moves are sent and animated
     const result = vr.getCurrentScore(move_s);
     if (result != "*") {
       setTimeout(() => {
     const result = vr.getCurrentScore(move_s);
     if (result != "*") {
       setTimeout(() => {
@@ -466,23 +485,6 @@ const afterPlay = (move_s) => {
         send("gameover", {gid: gid});
       }, 2000);
     }
         send("gameover", {gid: gid});
       }, 2000);
     }
-  };
-  // Pack into one moves array, then send
-  if (Array.isArray(move_s))
-    // Array of simple moves (e.g. Chakart)
-    Array.prototype.push.apply(curMoves, move_s);
-  else
-    // Usual case
-    curMoves.push(move_s);
-  if (vr.turn != playerColor) {
-    toggleTurnIndicator(false);
-    send("newmove",
-         {gid: gid, moves: curMoves, fen: vr.getFen()},
-         {
-           retry: true,
-           success: callbackAfterSend,
-           error: () => alert("Move not sent: reload page")
-         });
   }
 };
 
   }
 };
 
@@ -532,8 +534,9 @@ function initializeGame(obj) {
       afterPlay: afterPlay,
       options: options
     });
       afterPlay: afterPlay,
       options: options
     });
-    if (!obj.fen) {
-      // Game creation: both players set FEN, in case of one is offline
+    const gameCreation = !obj.fen;
+    if (gameCreation) {
+      // Both players set FEN, in case of one is offline
       send("setfen", {gid: obj.gid, fen: vr.getFen()});
       localStorage.setItem("gid", obj.gid);
     }
       send("setfen", {gid: obj.gid, fen: vr.getFen()});
       localStorage.setItem("gid", obj.gid);
     }
@@ -547,7 +550,7 @@ function initializeGame(obj) {
     }
     const playerIndex = (playerColor == "w" ? 0 : 1);
     fillGameInfos(obj, 1 - playerIndex);
     }
     const playerIndex = (playerColor == "w" ? 0 : 1);
     fillGameInfos(obj, 1 - playerIndex);
-    if (obj.players[playerIndex].randvar)
+    if (obj.players[playerIndex].randvar && gameCreation)
       toggleVisible("gameInfos");
     else
       toggleVisible("boardContainer");
       toggleVisible("gameInfos");
     else
       toggleVisible("boardContainer");
index 3fac51b..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";
 
 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 {
 // 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"];
   }
 
     return !!this.options["dark"];
   }
 
+  get hasMoveStack() {
+    return false;
+  }
+
   // Some variants use click infos:
   doClick(coords) {
     if (typeof coords.x != "number")
   // Some variants use click infos:
   doClick(coords) {
     if (typeof coords.x != "number")
@@ -391,17 +410,21 @@ export default class ChessRules {
     // Fen string fully describes the game state
     if (!o.fen)
       o.fen = this.genRandInitFen(o.seed);
     // 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();
   }
 
 
     // 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("/");
   // 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.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;
     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.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() {
   }
 
   updateEnlightened() {
@@ -2128,35 +2153,38 @@ export default class ChessRules {
       this.subTurnTeleport = 1;
       this.captured = null;
     }
       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 (
       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.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"
   }
 
   // "Stop at the first move found"
@@ -2233,11 +2261,50 @@ export default class ChessRules {
   }
 
   playPlusVisual(move, r) {
   }
 
   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.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);
   getMaxDistance(r) {
     // Works for all rectangular boards:
     return Math.sqrt(r.width ** 2 + r.height ** 2);
@@ -2247,41 +2314,37 @@ export default class ChessRules {
     return (typeof x == "string" ? this.r_pieces : this.g_pieces)[x][y];
   }
 
     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();
     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);
       // 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.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.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?)
       }
     }
     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";
       const dep = this.getPixelPosition(i1, j1, r);
       const arr = this.getPixelPosition(i2, j2, r);
       movingPiece.style.transitionDuration = "0s";
@@ -2297,24 +2360,63 @@ export default class ChessRules {
         setTimeout(cb, duration * 1000);
       }, 50);
     };
         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 = () => {
     let index = 0;
     const animateSegmentCallback = () => {
-      if (index < move.segments.length)
+      if (index < segments.length)
         animateSegment(index++, animateSegmentCallback);
       else {
         movingPiece.remove();
         initPiece.style.opacity = "1";
         animateSegment(index++, animateSegmentCallback);
       else {
         movingPiece.remove();
         initPiece.style.opacity = "1";
-        callback();
+        cb();
       }
     };
     animateSegmentCallback();
   }
 
       }
     };
     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();
   playReceivedMove(moves, callback) {
     const launchAnimation = () => {
       const r = container.querySelector(".chessboard").getBoundingClientRect();
index 5d04f45..18d5891 100644 (file)
@@ -3,7 +3,7 @@ const variants = [
   {name: 'Alapo', desc: 'Geometric Chess'},
   {name: 'Alice', desc: 'Both sides of the mirror'},
   {name: 'Align4', desc: 'Align four pawns'},
   {name: 'Alapo', desc: 'Geometric Chess'},
   {name: 'Alice', desc: 'Both sides of the mirror'},
   {name: 'Align4', desc: 'Align four pawns'},
-//  {name: 'Allmate', desc: 'Mate any piece'},
+  {name: 'Allmate', desc: 'Mate any piece'},
   {name: 'Ambiguous', desc: "Play opponent's pieces"},
 //  {name: 'Antiking1', desc: 'Keep antiking in check', disp: 'Anti-King'},
 //  {name: 'Antimatter', desc: 'Dangerous collisions'},
   {name: 'Ambiguous', desc: "Play opponent's pieces"},
 //  {name: 'Antiking1', desc: 'Keep antiking in check', disp: 'Anti-King'},
 //  {name: 'Antimatter', desc: 'Dangerous collisions'},
diff --git a/variants/Allmate/class.js b/variants/Allmate/class.js
new file mode 100644 (file)
index 0000000..8c5a92c
--- /dev/null
@@ -0,0 +1,97 @@
+import ChessRules from "/base_rules.js";
+import PiPo from "/utils/PiPo.js";
+import Move from "/utils/Move.js";
+
+export default class AllmateRules extends ChessRules {
+
+  static get Options() {
+    return {
+      select: C.Options.select,
+      styles: [
+        "cylinder",
+        "madrasi",
+        "zen"
+      ]
+    };
+  }
+
+  get hasEnpassant() {
+    return false;
+  }
+  get hasMoveStack() {
+    return true;
+  }
+
+  setOtherVariables(fenParsed) {
+    super.setOtherVariables(fenParsed);
+    this.curMove = null;
+  }
+
+  getPotentialMovesFrom(sq) {
+    // Remove direct captures:
+    return super.getPotentialMovesFrom(sq)
+      .filter(m => m.vanish.length == m.appear.length);
+  }
+
+  // Called "recursively" before a move is played, until no effect
+  computeNextMove(move) {
+    if (move.appear.length > 0)
+      this.curMove = move;
+    const color = this.turn;
+    const oppCol = C.GetOppCol(this.turn);
+    let mv = new Move({
+      start: this.curMove.end,
+      end: this.curMove.end,
+      appear: [],
+      vanish: []
+    });
+    this.playOnBoard(move);
+    for (let i=0; i<this.size.x; i++) {
+      for (let j=0; j<this.size.y; j++) {
+        if (this.getColor(i, j) == oppCol && this.isMated(i, j, color)) {
+          mv.vanish.push(
+            new PiPo({x: i, y: j, c: oppCol, p: this.getPiece(i, j)})
+          );
+        }
+      }
+    }
+    this.undoOnBoard(move);
+    move.next = (mv.vanish.length > 0 ? mv : null);
+  }
+
+  // is piece on square x,y mated by color?
+  isMated(x, y, color) {
+    const myColor = C.GetOppCol(color);
+    if (!this.underCheck([x, y], color))
+      return false;
+    for (let i=0; i<this.size.x; i++) {
+      for (let j=0; j<this.size.y; j++) {
+        if (this.getColor(i, j) == myColor) {
+          const movesIJ = super.getPotentialMovesFrom([i, j], myColor);
+          for (let move of movesIJ) {
+            this.playOnBoard(move);
+            let testSquare = [x, y];
+            if (i == x && j == y) {
+              // The mated-candidate has moved itself
+              testSquare = [move.end.x, move.end.y]; }
+            const res = this.underCheck(testSquare, color);
+            this.undoOnBoard(move);
+            if (!res)
+              return false;
+          }
+        }
+      }
+    }
+    return true;
+  }
+
+  // No "under check" conditions in castling
+  getCastleMoves(sq) {
+    return super.getCastleMoves(sq, null, "castleInCheck");
+  }
+
+  filterValid(moves) {
+    return moves; //TODO?: over-simplification to be fixed later
+  }
+
+};
diff --git a/variants/Allmate/rules.html b/variants/Allmate/rules.html
new file mode 100644 (file)
index 0000000..0ccbfdc
--- /dev/null
@@ -0,0 +1,9 @@
+<p>TODO</p>
+
+<p>Win by mate-capturing the enemy king.</p>
+
+<a href="https://www.chessvariants.com/difftaking.dir/allmate.html">
+  chessvariants page.
+</a>
+
+<p class="author">Dr. Chris Taylor (1979).</p>
diff --git a/variants/Allmate/style.css b/variants/Allmate/style.css
new file mode 100644 (file)
index 0000000..a3550bc
--- /dev/null
@@ -0,0 +1 @@
+@import url("/base_pieces.css");
index 24bbd3e..abd695b 100644 (file)
@@ -41,6 +41,9 @@ export default class ChakartRules extends ChessRules {
   get hasReserveFen() {
     return false;
   }
   get hasReserveFen() {
     return false;
   }
+  get hasMoveStack() {
+    return true;
+  }
 
   static get IMMOBILIZE_CODE() {
     return {
 
   static get IMMOBILIZE_CODE() {
     return {
@@ -178,15 +181,17 @@ export default class ChakartRules extends ChessRules {
   }
 
   setOtherVariables(fenParsed) {
   }
 
   setOtherVariables(fenParsed) {
-    this.setFlags(fenParsed.flags);
-    this.reserve = {}; //to be filled later
+    super.setOtherVariables(fenParsed);
     this.egg = null;
     this.egg = null;
-    this.moveStack = [];
     // Change seed (after FEN generation!!)
     // so that further calls differ between players:
     Random.setSeed(Math.floor(19840 * Math.random()));
   }
 
     // Change seed (after FEN generation!!)
     // so that further calls differ between players:
     Random.setSeed(Math.floor(19840 * Math.random()));
   }
 
+  initReserves() {
+    this.reserve = {}; //to be filled later
+  }
+
   // For Toadette bonus
   getDropMovesFrom([c, p]) {
     if (typeof c != "string" || this.reserve[c][p] == 0)
   // For Toadette bonus
   getDropMovesFrom([c, p]) {
     if (typeof c != "string" || this.reserve[c][p] == 0)
@@ -415,57 +420,11 @@ export default class ChakartRules extends ChessRules {
       super.showChoices(moves);
       return false;
     }
       super.showChoices(moves);
       return false;
     }
-    if (!move.nextComputed) {
-      // Set potential random effects, so that play() is deterministic
-      // from opponent viewpoint:
-      const endPiece = this.getPiece(move.end.x, move.end.y);
-      switch (endPiece) {
-        case V.EGG:
-          move.egg = Random.sample(V.EGG_SURPRISE);
-          move.next = this.getEggEffect(move);
-          break;
-        case V.MUSHROOM:
-          move.next = this.getMushroomEffect(move);
-          break;
-        case V.BANANA:
-        case V.BOMB:
-          move.next = this.getBombBananaEffect(move, endPiece);
-          break;
-      }
-      if (!move.next && move.appear.length > 0 && !move.kingboo) {
-        const movingPiece = move.appear[0].p;
-        if (['b', 'r'].includes(movingPiece)) {
-          // Drop a banana or bomb:
-          const bs =
-            this.getRandomSquare([move.end.x, move.end.y],
-              movingPiece == 'r'
-                ? [[1, 1], [1, -1], [-1, 1], [-1, -1]]
-                : [[1, 0], [-1, 0], [0, 1], [0, -1]],
-              "freeSquare");
-          if (bs) {
-            move.appear.push(
-              new PiPo({
-                x: bs[0],
-                y: bs[1],
-                c: 'a',
-                p: movingPiece == 'r' ? 'd' : 'w'
-              })
-            );
-            if (this.board[bs[0]][bs[1]] != "") {
-              move.vanish.push(
-                new PiPo({
-                  x: bs[0],
-                  y: bs[1],
-                  c: this.getColor(bs[0], bs[1]),
-                  p: this.getPiece(bs[0], bs[1])
-                })
-              );
-            }
-          }
-        }
-      }
-      move.nextComputed = true;
-    }
+    this.postPlay(move, color, oppCol);
+    return true;
+  }
+
+  postPlay(move, color, oppCol) {
     this.egg = move.egg;
     if (move.egg == "toadette") {
       this.reserve = { w: {}, b: {} };
     this.egg = move.egg;
     if (move.egg == "toadette") {
       this.reserve = { w: {}, b: {} };
@@ -526,15 +485,73 @@ export default class ChakartRules extends ChessRules {
         }
       }
     }
         }
       }
     }
-    if (!move.next && !["daisy", "toadette", "kingboo"].includes(move.egg)) {
-      this.turn = oppCol;
-      this.movesCount++;
-    }
+    this.playOnBoard(move);
+    super.postPlay(move);
+  }
+
+  playVisual(move, r) {
+    super.playVisual(move, r);
     if (move.egg)
       this.displayBonus(move);
     if (move.egg)
       this.displayBonus(move);
-    this.playOnBoard(move);
-    this.nextMove = move.next;
-    return true;
+  }
+
+  computeNextMove(move) {
+    // Set potential random effects, so that play() is deterministic
+    // from opponent viewpoint:
+    const endPiece = this.getPiece(move.end.x, move.end.y);
+    switch (endPiece) {
+      case V.EGG:
+        move.egg = Random.sample(V.EGG_SURPRISE);
+        move.next = this.getEggEffect(move);
+        break;
+      case V.MUSHROOM:
+        move.next = this.getMushroomEffect(move);
+        break;
+      case V.BANANA:
+      case V.BOMB:
+        move.next = this.getBombBananaEffect(move, endPiece);
+        break;
+    }
+    // NOTE: Chakart has also some side-effects:
+    if (
+      !move.next && move.appear.length > 0 &&
+      !move.kingboo && !move.luigiEffect
+    ) {
+      const movingPiece = move.appear[0].p;
+      if (['b', 'r'].includes(movingPiece)) {
+        // Drop a banana or bomb:
+        const bs =
+          this.getRandomSquare([move.end.x, move.end.y],
+            movingPiece == 'r'
+              ? [[1, 1], [1, -1], [-1, 1], [-1, -1]]
+              : [[1, 0], [-1, 0], [0, 1], [0, -1]],
+            "freeSquare");
+        if (bs) {
+          move.appear.push(
+            new PiPo({
+              x: bs[0],
+              y: bs[1],
+              c: 'a',
+              p: movingPiece == 'r' ? 'd' : 'w'
+            })
+          );
+          if (this.board[bs[0]][bs[1]] != "") {
+            move.vanish.push(
+              new PiPo({
+                x: bs[0],
+                y: bs[1],
+                c: this.getColor(bs[0], bs[1]),
+                p: this.getPiece(bs[0], bs[1])
+              })
+            );
+          }
+        }
+      }
+    }
+  }
+
+  isLastMove(move) {
+    return !move.next && !["daisy", "toadette", "kingboo"].includes(move.egg);
   }
 
   // Helper to set and apply banana/bomb effect
   }
 
   // Helper to set and apply banana/bomb effect
@@ -584,6 +601,7 @@ export default class ChakartRules extends ChessRules {
               new PiPo({x: coords[0], y: coords[1], c: oldColor, p: piece})
             ]
           });
               new PiPo({x: coords[0], y: coords[1], c: oldColor, p: piece})
             ]
           });
+          em.luigiEffect = true; //avoid dropping bomb/banana by mistake
         }
         break;
       case "bowser":
         }
         break;
       case "bowser":
@@ -703,23 +721,13 @@ export default class ChakartRules extends ChessRules {
     return moves;
   }
 
     return moves;
   }
 
-  playPlusVisual(move, r) {
-    const nextLines = () => {
-      if (!this.play(move))
-        return;
-      this.moveStack.push(move);
-      this.playVisual(move, r);
-      if (this.nextMove)
-        this.playPlusVisual(this.nextMove, r);
-      else {
-        this.afterPlay(this.moveStack);
-        this.moveStack = [];
-      }
-    };
-    if (this.moveStack.length == 0)
-      nextLines();
-    else
-      this.animate(move, nextLines);
+  // Kingboo bonus can be animated better:
+  customAnimate(move, segments, cb) {
+    if (!move.kingboo)
+      return 0;
+    super.animateMoving(move.end, move.start, null,
+                        segments.reverse().map(s => s.reverse()), cb);
+    return 1;
   }
 
 };
   }
 
 };
index e2e36c8..dbaefa0 100644 (file)
@@ -110,4 +110,13 @@ export default class SuctionRules extends ChessRules {
     return "*";
   }
 
     return "*";
   }
 
+  // Better animation for swaps
+  customAnimate(move, segments, cb) {
+    if (move.vanish.length < 2)
+      return 0;
+    super.animateMoving(move.end, move.start, null,
+                        segments.reverse().map(s => s.reverse()), cb);
+    return 1;
+  }
+
 };
 };