Some Chakart fixes/adjustments
[vchess.git] / client / src / variants / Chakart.js
index 9a02663..86a867a 100644 (file)
@@ -4,6 +4,7 @@ import { ArrayFun } from "@/utils/array";
 import { randInt } from "@/utils/alea";
 
 export class ChakartRules extends ChessRules {
+
   static get PawnSpecs() {
     return SuicideRules.PawnSpecs;
   }
@@ -12,38 +13,23 @@ export class ChakartRules extends ChessRules {
     return false;
   }
 
+  static get HasEnpassant() {
+    return false;
+  }
+
   static get CorrConfirm() {
     // Because of bonus effects
     return false;
   }
 
   static get CanAnalyze() {
-    return true; //false;
+    return false;
   }
 
   static get SomeHiddenMoves() {
     return true;
   }
 
-  hoverHighlight(x, y) {
-    if (this.subTurn == 1) return false;
-    const L = this.firstMove.length;
-    const fm = this.firstMove[L-1];
-    if (fm.end.effect != 0) return false;
-    const deltaX = Math.abs(fm.appear[0].x - x);
-    const deltaY = Math.abs(fm.appear[0].y - y);
-    return (
-      (deltaX == 0 && deltaY == 0) ||
-      (
-        this.board[x][y] == V.EMPTY &&
-        (
-          (fm.vanish[0].p == V.ROOK && deltaX == 1 && deltaY == 1) ||
-          (fm.vanish[0].p == V.BISHOP && deltaX + deltaY == 1)
-        )
-      )
-    );
-  }
-
   static get IMMOBILIZE_CODE() {
     return {
       'p': 's',
@@ -113,8 +99,13 @@ export class ChakartRules extends ChessRules {
   }
 
   getPPpath(m) {
+    if (!!m.promoteInto) return m.promoteInto;
+    if (m.appear.length == 0 && m.vanish.length == 1)
+      // King 'remote shell capture', on an adjacent square:
+      return this.getPpath(m.vanish[0].c + m.vanish[0].p);
     let piece = m.appear[0].p;
     if (Object.keys(V.IMMOBILIZE_DECODE).includes(piece))
+      // Promotion by capture into immobilized piece: do not reveal!
       piece = V.IMMOBILIZE_DECODE[piece];
     return this.getPpath(m.appear[0].c + piece);
   }
@@ -123,10 +114,17 @@ export class ChakartRules extends ChessRules {
     const fenParts = fen.split(" ");
     return Object.assign(
       ChessRules.ParseFen(fen),
-      { captured: fenParts[5] }
+      { captured: fenParts[4] }
     );
   }
 
+  static IsGoodFen(fen) {
+    if (!ChessRules.IsGoodFen(fen)) return false;
+    const captured = V.ParseFen(fen).captured;
+    if (!captured || !captured.match(/^[0-9]{12,12}$/)) return false;
+    return true;
+  }
+
   // King can be l or L (immobilized) --> similar to Alice variant
   static IsGoodPosition(position) {
     if (position.length == 0) return false;
@@ -139,14 +137,14 @@ export class ChakartRules extends ChessRules {
         if (['K', 'k', 'L', 'l'].includes(row[i])) kings[row[i]]++;
         if (V.PIECES.includes(row[i].toLowerCase())) sumElts++;
         else {
-          const num = parseInt(row[i]);
+          const num = parseInt(row[i], 10);
           if (isNaN(num)) return false;
           sumElts += num;
         }
       }
       if (sumElts != V.size.y) return false;
     }
-    if (kings['k'] + kings['l'] != 1 || kings['K'] + kings['L'] != 1)
+    if (kings['k'] + kings['l'] == 0 || kings['K'] + kings['L'] == 0)
       return false;
     return true;
   }
@@ -187,11 +185,11 @@ export class ChakartRules extends ChessRules {
   }
 
   getCapturedFen() {
-    let counts = [...Array(10).fill(0)];
+    let counts = [...Array(12).fill(0)];
     let i = 0;
-    for (let p of [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN, V.PAWN]) {
+    for (let p of V.RESERVE_PIECES) {
       counts[i] = this.captured["w"][p];
-      counts[5 + i] = this.captured["b"][p];
+      counts[6 + i] = this.captured["b"][p];
       i++;
     }
     return counts.join("");
@@ -201,25 +199,28 @@ export class ChakartRules extends ChessRules {
 
   setOtherVariables(fen) {
     super.setOtherVariables(fen);
-    const fenParsed = V.ParseFen(fen);
     // Initialize captured pieces' counts from FEN
+    const captured =
+      V.ParseFen(fen).captured.split("").map(x => parseInt(x, 10));
     this.captured = {
       w: {
-        [V.ROOK]: parseInt(fenParsed.captured[0]),
-        [V.KNIGHT]: parseInt(fenParsed.captured[1]),
-        [V.BISHOP]: parseInt(fenParsed.captured[2]),
-        [V.QUEEN]: parseInt(fenParsed.captured[3]),
-        [V.PAWN]: parseInt(fenParsed.captured[4]),
+        [V.PAWN]: captured[0],
+        [V.ROOK]: captured[1],
+        [V.KNIGHT]: captured[2],
+        [V.BISHOP]: captured[3],
+        [V.QUEEN]: captured[4],
+        [V.KING]: captured[5]
       },
       b: {
-        [V.ROOK]: parseInt(fenParsed.captured[5]),
-        [V.KNIGHT]: parseInt(fenParsed.captured[6]),
-        [V.BISHOP]: parseInt(fenParsed.captured[7]),
-        [V.QUEEN]: parseInt(fenParsed.captured[8]),
-        [V.PAWN]: parseInt(fenParsed.captured[9]),
+        [V.PAWN]: captured[6],
+        [V.ROOK]: captured[7],
+        [V.KNIGHT]: captured[8],
+        [V.BISHOP]: captured[9],
+        [V.QUEEN]: captured[10],
+        [V.KING]: captured[11]
       }
     };
-    this.firstMove = [];
+    this.effects = [];
     this.subTurn = 1;
   }
 
@@ -231,12 +232,22 @@ export class ChakartRules extends ChessRules {
     return fen;
   }
 
+  getColor(i, j) {
+    if (i >= V.size.x) return i == V.size.x ? "w" : "b";
+    return this.board[i][j].charAt(0);
+  }
+
+  getPiece(i, j) {
+    if (i >= V.size.x) return V.RESERVE_PIECES[j];
+    return this.board[i][j].charAt(1);
+  }
+
   getReservePpath(index, color) {
     return color + V.RESERVE_PIECES[index];
   }
 
   static get RESERVE_PIECES() {
-    return [V.PAWN, V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN];
+    return [V.PAWN, V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN, V.KING];
   }
 
   getReserveMoves([x, y]) {
@@ -248,30 +259,14 @@ export class ChakartRules extends ChessRules {
     const end = (color == 'b' && p == V.PAWN ? 7 : 8);
     for (let i = start; i < end; i++) {
       for (let j = 0; j < V.size.y; j++) {
-        // TODO: allow also to drop on bonus square?
-        // No effect if mushroom, but normal (recursive) effect otherwise.
-        //
-        //Koopa --> shift()
-        //
-        //Egg: no effect on subTurn == 2 => OK
-        //Juste bootstrap banan or bomb effect => getBasicMove otherwise
-        //
-        //
-        if (this.board[i][j] == V.EMPTY) {
-          let mv = new Move({
-            appear: [
-              new PiPo({
-                x: i,
-                y: j,
-                c: color,
-                p: p
-              })
-            ],
-            vanish: [],
-            start: { x: x, y: y }, //a bit artificial...
-            end: { x: i, y: j }
-          });
-          moves.push(mv);
+        if (
+          this.board[i][j] == V.EMPTY ||
+          this.getColor(i, j) == 'a' ||
+          this.getPiece(i, j) == V.INVISIBLE_QUEEN
+        ) {
+          let m = this.getBasicMove({ p: p, x: i, y: j});
+          m.start = { x: x, y: y };
+          moves.push(m);
         }
       }
     }
@@ -312,12 +307,10 @@ export class ChakartRules extends ChessRules {
     }
     else {
       // Subturn == 2
-      const L = this.firstMove.length;
-      const fm = this.firstMove[L-1];
-      switch (fm.end.effect) {
-        // case 0: a click is required (banana or bomb)
+      const L = this.effects.length;
+      switch (this.effects[L-1]) {
         case "kingboo":
-          // Exchange position with any piece,
+          // Exchange position with any visible piece,
           // except pawns if arriving on last rank.
           const lastRank = { 'w': 0, 'b': 7 };
           const color = this.turn;
@@ -325,12 +318,13 @@ export class ChakartRules extends ChessRules {
           for (let i=0; i<8; i++) {
             for (let j=0; j<8; j++) {
               const colIJ = this.getColor(i, j);
+              const pieceIJ = this.getPiece(i, j);
               if (
                 (i != x || j != y) &&
                 this.board[i][j] != V.EMPTY &&
+                pieceIJ != V.INVISIBLE_QUEEN &&
                 colIJ != 'a'
               ) {
-                const pieceIJ = this.getPiece(i, j);
                 if (
                   (pieceIJ != V.PAWN || x != lastRank[colIJ]) &&
                   (allowLastRank || i != lastRank[color])
@@ -341,7 +335,7 @@ export class ChakartRules extends ChessRules {
                     c: colIJ,
                     p: this.getPiece(i, j)
                   });
-                  let mMove = this.getBasicMove([x, y], [i, j]);
+                  let mMove = this.getBasicMove({ x: x, y: y }, [i, j]);
                   mMove.appear.push(movedUnit);
                   moves.push(mMove);
                 }
@@ -354,42 +348,70 @@ export class ChakartRules extends ChessRules {
           if (x >= V.size.x) moves = this.getReserveMoves([x, y]);
           break;
         case "daisy":
-          // Play again with the same piece
-          if (fm.appear[0].x == x && fm.appear[0].y == y)
-            moves = super.getPotentialMovesFrom([x, y]);
+          // Play again with any piece
+          moves = super.getPotentialMovesFrom([x, y]);
           break;
       }
     }
     return moves;
   }
 
-  // Helper for getBasicMove()
+  // Helper for getBasicMove(): banana/bomb effect
   getRandomSquare([x, y], steps) {
-    const validSteps = steps.filter(s => {
-      const [i, j] = [x + s[0], y + s[1]];
-      return (
-        V.OnBoard(i, j) &&
-        (this.board[i][j] == V.EMPTY || this.getColor(i, j) == 'a')
-      );
-    });
-    if (validSteps.length == 0)
-      // Can happen after mushroom jump
-      return [x, y];
+    const validSteps = steps.filter(s => V.OnBoard(x + s[0], y + s[1]));
     const step = validSteps[randInt(validSteps.length)];
     return [x + step[0], y + step[1]];
   }
 
-  getBasicMove([x1, y1], [x2, y2], tr) {
-    // Apply mushroom, bomb or banana effect (hidden to the player).
-    // Determine egg effect, too, and apply its first part if possible.
-    let move = super.getBasicMove([x1, y1], [x2, y2], tr);
-    const color1 = this.getColor(x1, y1);
-    const color2 = this.getColor(x2, y2);
-    const piece1 = this.getPiece(x1, y1);
-    const piece2 = this.getPiece(x2, y2);
+  // Apply mushroom, bomb or banana effect (hidden to the player).
+  // Determine egg effect, too, and apply its first part if possible.
+  getBasicMove_aux(psq1, sq2, tr, initMove) {
+    const [x1, y1] = [psq1.x, psq1.y];
+    const color1 = this.turn;
+    const piece1 = (!!tr ? tr.p : (psq1.p || this.getPiece(x1, y1)));
     const oppCol = V.GetOppCol(color1);
-    if ([V.PAWN, V.KNIGHT].includes(piece1)) {
-      //&& (color2 != 'a' || !([V.BANANA, V.BOMB].includes(piece2)))
+    if (!sq2) {
+      let move = {
+        appear: [],
+        vanish: []
+      };
+      // banana or bomb defines next square, or the move ends there
+      move.appear = [
+        new PiPo({
+          x: x1,
+          y: y1,
+          c: color1,
+          p: piece1
+        })
+      ];
+      if (this.board[x1][y1] != V.EMPTY) {
+        const initP1 = this.getPiece(x1, y1);
+        move.vanish = [
+          new PiPo({
+            x: x1,
+            y: y1,
+            c: this.getColor(x1, y1),
+            p: initP1
+          })
+        ];
+        if ([V.BANANA, V.BOMB].includes(initP1)) {
+          const steps = V.steps[initP1 == V.BANANA ? V.ROOK : V.BISHOP];
+          move.next = this.getRandomSquare([x1, y1], steps);
+        }
+      }
+      move.end = { x: x1, y: y1 };
+      return move;
+    }
+    const [x2, y2] = [sq2[0], sq2[1]];
+    // The move starts normally, on board:
+    let move = super.getBasicMove([x1, y1], [x2, y2], tr);
+    if (!!tr) move.promoteInto = tr.c + tr.p; //in case of (chomped...)
+    const L = this.effects.length;
+    if (
+      [V.PAWN, V.KNIGHT].includes(piece1) &&
+      !!initMove &&
+      (this.subTurn == 1 || this.effects[L-1] == "daisy")
+    ) {
       switch (piece1) {
         case V.PAWN: {
           const twoSquaresMove = (Math.abs(x2 - x1) == 2);
@@ -450,65 +472,39 @@ export class ChakartRules extends ChessRules {
         }
       }
     }
-    // Avoid looping back on effect already applied:
-    let usedEffect = ArrayFun.init(8, 8, false);
-    const applyBeffect = (steps) => {
-      const [x, y] = [move.appear[0].x, move.appear[0].y];
-      if (usedEffect[x][y]) return;
-      usedEffect[x][y] = true;
-      const moveTo = this.getRandomSquare([x, y], steps);
-      move.appear[0].x = moveTo[0];
-      move.appear[0].y = moveTo[1];
-      if (
-        this.board[moveTo[0]][moveTo[1]] != V.EMPTY &&
-        this.getColor(moveTo[0], moveTo[1]) == 'a'
-      ) {
-        // Artificially change direction, before applying new effects:
-        x1 = x;
-        y1 = y;
-        x2 = moveTo[0];
-        x2 = moveTo[1];
-        switch (this.getPiece(moveTo[0], moveTo[1])) {
-          case V.BANANA:
-            applyBeffect(V.steps[V.ROOK]);
-            break;
-          case V.BOMB:
-            applyBeffect(V.steps[V.BISHOP]);
-            break;
-          case V.MUSHROOM:
-            applyMushroomEffect();
-            break;
-          case V.EGG:
-            applyEggEffect();
-            break;
-        }
-      }
-    };
     // For (wa)luigi effect:
     const changePieceColor = (color) => {
       let pieces = [];
       const oppLastRank = (color == 'w' ? 7 : 0);
       for (let i=0; i<8; i++) {
         for (let j=0; j<8; j++) {
+          const piece = this.getPiece(i, j);
           if (
+            (i != move.vanish[0].x || j != move.vanish[0].y) &&
             this.board[i][j] != V.EMPTY &&
+            piece != V.INVISIBLE_QUEEN &&
             this.getColor(i, j) == color
           ) {
-            const piece = this.getPiece(i, j);
             if (piece != V.KING && (piece != V.PAWN || i != oppLastRank))
               pieces.push({ x: i, y: j, p: piece });
           }
         }
       }
+      // Special case of the current piece (still at its initial position)
+      if (color == color1)
+        pieces.push({ x: move.appear[0].x, y: move.appear[0].y, p: piece1 });
       const cp = pieces[randInt(pieces.length)];
-      move.vanish.push(
-        new PiPo({
-          x: cp.x,
-          y: cp.y,
-          c: color,
-          p: cp.p
-        })
-      );
+      if (move.appear[0].x != cp.x || move.appear[0].y != cp.y) {
+        move.vanish.push(
+          new PiPo({
+            x: cp.x,
+            y: cp.y,
+            c: color,
+            p: cp.p
+          })
+        );
+      }
+      else move.appear.shift();
       move.appear.push(
         new PiPo({
           x: cp.x,
@@ -523,33 +519,17 @@ export class ChakartRules extends ChessRules {
         // No egg effects at subTurn 2
         return;
       // 1) Determine the effect (some may be impossible)
-      let effects = ["kingboo", "koopa", "chomp", "bowser"];
+      let effects = ["kingboo", "koopa", "chomp", "bowser", "daisy"];
       if (Object.values(this.captured[color1]).some(c => c >= 1))
         effects.push("toadette");
       const lastRank = { 'w': 0, 'b': 7 };
-      let canPlayAgain = undefined;
-      if (
-        move.appear[0].p == V.PAWN &&
-        move.appear[0].x == lastRank[color1]
-      ) {
-        // Always possible: promote into a queen rook or king
-        canPlayAgain = true;
-      }
-      else {
-        move.end.effect = "toadette";
-        this.play(move);
-        canPlayAgain = this.getPotentialMovesFrom([x2, y2]).length > 0;
-        this.undo(move);
-        delete move.end["effect"];
-      }
-      if (canPlayAgain) effects.push("daisy");
       if (
         this.board.some((b,i) =>
           b.some(cell => {
             return (
               cell[0] == oppCol &&
               cell[1] != V.KING &&
-              (cell[1] != V.PAWN || i != lastRank[oppCol])
+              (cell[1] != V.PAWN || i != lastRank[color1])
             );
           })
         )
@@ -557,12 +537,16 @@ export class ChakartRules extends ChessRules {
         effects.push("luigi");
       }
       if (
+        (
+          piece1 != V.KING &&
+          (piece1 != V.PAWN || move.appear[0].x != lastRank[oppCol])
+        ) ||
         this.board.some((b,i) =>
           b.some(cell => {
             return (
               cell[0] == color1 &&
               cell[1] != V.KING &&
-              (cell[1] != V.PAWN || i != lastRank[color1])
+              (cell[1] != V.PAWN || i != lastRank[oppCol])
             );
           })
         )
@@ -596,8 +580,6 @@ export class ChakartRules extends ChessRules {
       }
     };
     const applyMushroomEffect = () => {
-      if (usedEffect[move.appear[0].x][move.appear[0].y]) return;
-      usedEffect[move.appear[0].x][move.appear[0].y] = true;
       if ([V.PAWN, V.KING, V.KNIGHT].includes(piece1)) {
         // Just make another similar step, if possible (non-capturing)
         const [i, j] = [
@@ -606,26 +588,30 @@ export class ChakartRules extends ChessRules {
         ];
         if (
           V.OnBoard(i, j) &&
-          (this.board[i][j] == V.EMPTY || this.getColor(i, j) == 'a')
+          (
+            this.board[i][j] == V.EMPTY ||
+            this.getPiece(i, j) == V.INVISIBLE_QUEEN ||
+            this.getColor(i, j) == 'a'
+          )
         ) {
           move.appear[0].x = i;
           move.appear[0].y = j;
           if (this.board[i][j] != V.EMPTY) {
             const object = this.getPiece(i, j);
+            const color = this.getColor(i, j);
             move.vanish.push(
               new PiPo({
                 x: i,
                 y: j,
-                c: 'a',
+                c: color,
                 p: object
               })
             );
             switch (object) {
               case V.BANANA:
-                applyBeffect(V.steps[V.ROOK]);
-                break;
               case V.BOMB:
-                applyBeffect(V.steps[V.BISHOP]);
+                const steps = V.steps[object == V.BANANA ? V.ROOK : V.BISHOP];
+                move.next = this.getRandomSquare([i, j], steps);
                 break;
               case V.EGG:
                 applyEggEffect();
@@ -647,110 +633,183 @@ export class ChakartRules extends ChessRules {
         if (
           V.OnBoard(next[0], next[1]) &&
           this.board[next[0]][next[1]] != V.EMPTY &&
+          this.getPiece(next[0], next[1]) != V.INVISIBLE_QUEEN &&
           this.getColor(next[0], next[1]) != 'a'
         ) {
           const afterNext = [next[0] + step[0], next[1] + step[1]];
-          if (
-            V.OnBoard(afterNext[0], afterNext[1]) &&
-            (
+          if (V.OnBoard(afterNext[0], afterNext[1])) {
+            const afterColor = this.getColor(afterNext[0], afterNext[1]);
+            if (
               this.board[afterNext[0]][afterNext[1]] == V.EMPTY ||
-              this.getColor(afterNext[0], afterNext[1]) == 'a'
-            )
-          ) {
-            move.appear[0].x = afterNext[0];
-            move.appear[0].y = afterNext[1];
-            if (this.board[afterNext[0]][afterNext[1]] != V.EMPTY) {
-              const object = this.getPiece(afterNext[0], afterNext[1]);
-              move.vanish.push(
-                new PiPo({
-                  x: afterNext[0],
-                  y: afterNext[0],
-                  c: 'a',
-                  p: object
-                })
-              );
-              switch (object) {
-                case V.BANANA:
-                  applyBeffect(V.steps[V.ROOK]);
-                  break;
-                case V.BOMB:
-                  applyBeffect(V.steps[V.BISHOP]);
-                  break;
-                case V.EGG:
-                  applyEggEffect();
-                  break;
-                case V.MUSHROOM:
-                  applyMushroomEffect();
-                  break;
+              afterColor != color1
+            ) {
+              move.appear[0].x = afterNext[0];
+              move.appear[0].y = afterNext[1];
+              if (this.board[afterNext[0]][afterNext[1]] != V.EMPTY) {
+                // The "object" could also be an opponent's piece
+                const object = this.getPiece(afterNext[0], afterNext[1]);
+                move.vanish.push(
+                  new PiPo({
+                    x: afterNext[0],
+                    y: afterNext[1],
+                    c: afterColor,
+                    p: object
+                  })
+                );
+                switch (object) {
+                  case V.BANANA:
+                  case V.BOMB:
+                    const steps =
+                      V.steps[object == V.BANANA ? V.ROOK : V.BISHOP];
+                    move.next = this.getRandomSquare(
+                      [afterNext[0], afterNext[1]], steps);
+                    break;
+                  case V.EGG:
+                    applyEggEffect();
+                    break;
+                  case V.MUSHROOM:
+                    applyMushroomEffect();
+                    break;
+                }
               }
             }
           }
         }
       }
     };
+    const color2 = this.getColor(x2, y2);
+    const piece2 = this.getPiece(x2, y2);
     if (color2 == 'a') {
       switch (piece2) {
         case V.BANANA:
-          applyBeffect(V.steps[V.ROOK]);
-          break;
         case V.BOMB:
-          applyBeffect(V.steps[V.BISHOP]);
+          const steps = V.steps[piece2 == V.BANANA ? V.ROOK : V.BISHOP];
+          move.next = this.getRandomSquare([x2, y2], steps);
           break;
         case V.MUSHROOM:
           applyMushroomEffect();
           break;
         case V.EGG:
-          if (this.subTurn == 1) {
+          if (this.subTurn == 1)
             // No egg effect at subTurn 2
-            if ([V.ROOK, V.BISHOP].includes(piece1)) {
-              // Drop a bomb or banana at random, because even if bonus is
-              // "play again" nothing can be done after next move.
-              const steps = V.steps[piece1 == V.ROOK ? V.BISHOP : V.ROOK];
-              const object = (piece1 == V.ROOK ? V.BANANA : V.BOMB);
-              const dropOn = this.getRandomSquare([x2, y2], steps);
-              move.appear.push(
-                new PiPo({
-                  x: dropOn[0],
-                  y: dropOn[1],
-                  c: 'a',
-                  p: object
-                })
-              );
-            }
             applyEggEffect();
-          }
           break;
       }
     }
     if (
       this.subTurn == 1 &&
-      (color2 != 'a' || piece2 != V.EGG) &&
+      !move.next &&
+      move.appear.length > 0 &&
       [V.ROOK, V.BISHOP].includes(piece1)
     ) {
-      move.end.effect = 0;
+      const finalSquare = [move.appear[0].x, move.appear[0].y];
+      if (
+        color2 != 'a' ||
+        this.getColor(finalSquare[0], finalSquare[1]) != 'a' ||
+        this.getPiece(finalSquare[0], finalSquare[1]) != V.EGG
+      ) {
+        const validSteps =
+          V.steps[piece1 == V.ROOK ? V.BISHOP : V.ROOK].filter(s => {
+            const [i, j] = [finalSquare[0] + s[0], finalSquare[1] + s[1]];
+            return (
+              V.OnBoard(i, j) &&
+              // NOTE: do not place a bomb or banana on the invisible queen!
+              (this.board[i][j] == V.EMPTY || this.getColor(i, j) == 'a')
+            );
+          });
+        if (validSteps.length >= 1) {
+          const randIdx = randInt(validSteps.length);
+          const [x, y] = [
+            finalSquare[0] + validSteps[randIdx][0],
+            finalSquare[1] + validSteps[randIdx][1]
+          ];
+          move.appear.push(
+            new PiPo({
+              x: x,
+              y: y,
+              c: 'a',
+              p: (piece1 == V.ROOK ? V.BANANA : V.BOMB)
+            })
+          );
+          if (this.board[x][y] != V.EMPTY) {
+            move.vanish.push(
+              new PiPo({ x: x, y: y, c: 'a', p: this.getPiece(x, y) }));
+          }
+        }
+      }
     }
     return move;
   }
 
-  getEnpassantCaptures([x, y], shiftX) {
-    const Lep = this.epSquares.length;
-    const epSquare = this.epSquares[Lep - 1]; //always at least one element
-    let enpassantMove = null;
-    if (
-      !!epSquare &&
-      epSquare.x == x + shiftX &&
-      Math.abs(epSquare.y - y) == 1
-    ) {
-      // Not using this.getBasicMove() because the mushroom has no effect
-      enpassantMove = super.getBasicMove([x, y], [epSquare.x, epSquare.y]);
-      enpassantMove.vanish.push({
-        x: x,
-        y: epSquare.y,
-        p: V.PAWN,
-        c: this.getColor(x, epSquare.y)
-      });
+  getBasicMove(psq1, sq2, tr) {
+    let moves = [];
+    if (Array.isArray(psq1)) psq1 = { x: psq1[0], y: psq1[1] };
+    let m = this.getBasicMove_aux(psq1, sq2, tr, "initMove");
+    while (!!m.next) {
+      // Last move ended on bomb or banana, direction change
+      V.PlayOnBoard(this.board, m);
+      moves.push(m);
+      m = this.getBasicMove_aux(
+        { x: m.appear[0].x, y: m.appear[0].y }, m.next);
+    }
+    for (let i=moves.length-1; i>=0; i--) V.UndoOnBoard(this.board, moves[i]);
+    moves.push(m);
+    // Now merge moves into one
+    let move = {};
+    // start is wrong for Toadette moves --> it's fixed later
+    move.start = { x: psq1.x, y: psq1.y };
+    move.end = !!sq2 ? { x: sq2[0], y: sq2[1] } : { x: psq1.x, y: psq1.y };
+    if (!!tr) move.promoteInto = moves[0].promoteInto;
+    let lm = moves[moves.length-1];
+    if (this.subTurn == 1 && !!lm.end.effect)
+      move.end.effect = lm.end.effect;
+    if (moves.length == 1) {
+      move.appear = moves[0].appear;
+      move.vanish = moves[0].vanish;
     }
-    return !!enpassantMove ? [enpassantMove] : [];
+    else {
+      // Keep first vanish and last appear (if any)
+      move.appear = lm.appear;
+      move.vanish = moves[0].vanish;
+      if (
+        move.vanish.length >= 1 &&
+        move.appear.length >= 1 &&
+        move.vanish[0].x == move.appear[0].x &&
+        move.vanish[0].y == move.appear[0].y
+      ) {
+        // Loopback on initial square:
+        move.vanish.shift();
+        move.appear.shift();
+      }
+      for (let i=1; i < moves.length - 1; i++) {
+        for (let v of moves[i].vanish) {
+          // Only vanishing objects, not appearing at init move
+          if (
+            v.c == 'a' &&
+            (
+              moves[0].appear.length == 1 ||
+              moves[0].appear[1].x != v.x ||
+              moves[0].appear[1].y != v.y
+            )
+          ) {
+            move.vanish.push(v);
+          }
+        }
+      }
+      // Final vanish is our piece, but others might be relevant
+      // (for some egg bonuses at least).
+      for (let i=1; i < lm.vanish.length; i++) {
+        if (
+          lm.vanish[i].c != 'a' ||
+          moves[0].appear.length == 1 ||
+          moves[0].appear[1].x != lm.vanish[i].x ||
+          moves[0].appear[1].y != lm.vanish[i].y
+        ) {
+          move.vanish.push(lm.vanish[i]);
+        }
+      }
+    }
+    return move;
   }
 
   getPotentialPawnMoves([x, y]) {
@@ -762,17 +821,19 @@ export class ChakartRules extends ChessRules {
     let moves = [];
     if (
       this.board[x + shiftX][y] == V.EMPTY ||
-      this.getColor(x + shiftX, y) == 'a'
+      this.getColor(x + shiftX, y) == 'a' ||
+      this.getPiece(x + shiftX, y) == V.INVISIBLE_QUEEN
     ) {
       this.addPawnMoves([x, y], [x + shiftX, y], moves);
       if (
         [firstRank, firstRank + shiftX].includes(x) &&
         (
           this.board[x + 2 * shiftX][y] == V.EMPTY ||
-          this.getColor(x + 2 * shiftX, y) == 'a'
+          this.getColor(x + 2 * shiftX, y) == 'a' ||
+          this.getPiece(x + 2 * shiftX, y) == V.INVISIBLE_QUEEN
         )
       ) {
-        moves.push(this.getBasicMove([x, y], [x + 2 * shiftX, y]));
+        moves.push(this.getBasicMove({ x: x, y: y }, [x + 2 * shiftX, y]));
       }
     }
     for (let shiftY of [-1, 1]) {
@@ -780,15 +841,13 @@ export class ChakartRules extends ChessRules {
         y + shiftY >= 0 &&
         y + shiftY < sizeY &&
         this.board[x + shiftX][y + shiftY] != V.EMPTY &&
-        this.getColor(x + shiftX, y + shiftY) == oppCol
+        // Pawns cannot capture invisible queen this way!
+        this.getPiece(x + shiftX, y + shiftY) != V.INVISIBLE_QUEEN &&
+        ['a', oppCol].includes(this.getColor(x + shiftX, y + shiftY))
       ) {
         this.addPawnMoves([x, y], [x + shiftX, y + shiftY], moves);
       }
     }
-    Array.prototype.push.apply(
-      moves,
-      this.getEnpassantCaptures([x, y], shiftX)
-    );
     return moves;
   }
 
@@ -798,7 +857,12 @@ export class ChakartRules extends ChessRules {
     let invisibleMoves = [];
     if (this.powerFlags[this.turn][V.QUEEN]) {
       normalMoves.forEach(m => {
-        if (m.vanish.length == 1) {
+        if (
+          m.appear.length == 1 &&
+          m.vanish.length == 1 &&
+          // Only simple non-capturing moves:
+          m.vanish[0].c != 'a'
+        ) {
           let im = JSON.parse(JSON.stringify(m));
           im.appear[0].p = V.INVISIBLE_QUEEN;
           im.end.noHighlight = true;
@@ -815,41 +879,37 @@ export class ChakartRules extends ChessRules {
     // If flag allows it, add 'remote shell captures'
     if (this.powerFlags[this.turn][V.KING]) {
       V.steps[V.ROOK].concat(V.steps[V.BISHOP]).forEach(step => {
-        if (
-          V.OnBoard(x + step[0], y + step[1]) &&
-          this.board[x + step[0]][y + step[1]] == V.EMPTY
-        ) {
-          let [i, j] = [x + 2 * step[0], y + 2 * step[1]];
-          while (
-            V.OnBoard(i, j) &&
+        let [i, j] = [x + step[0], y + step[1]];
+        while (
+          V.OnBoard(i, j) &&
+          (
+            this.board[i][j] == V.EMPTY ||
+            this.getPiece(i, j) == V.INVISIBLE_QUEEN ||
             (
-              this.board[i][j] == V.EMPTY ||
-              (
-                this.getColor(i, j) == 'a' &&
-                [V.EGG, V.MUSHROOM].includes(this.getPiece(i, j))
-              )
+              this.getColor(i, j) == 'a' &&
+              [V.EGG, V.MUSHROOM].includes(this.getPiece(i, j))
             )
-          ) {
-            i += step[0];
-            j += step[1];
-          }
-          if (V.OnBoard(i, j)) {
-            const colIJ = this.getColor(i, j);
-            if (colIJ != color) {
-              // May just destroy a bomb or banana:
-              moves.push(
-                new Move({
-                  start: { x: x, y: y},
-                  end: { x: i, y: j },
-                  appear: [],
-                  vanish: [
-                    new PiPo({
-                      x: i, y: j, c: colIJ, p: this.getPiece(i, j)
-                    })
-                  ]
-                })
-              );
-            }
+          )
+        ) {
+          i += step[0];
+          j += step[1];
+        }
+        if (V.OnBoard(i, j)) {
+          const colIJ = this.getColor(i, j);
+          if (colIJ != color) {
+            // May just destroy a bomb or banana:
+            moves.push(
+              new Move({
+                start: { x: x, y: y},
+                end: { x: i, y: j },
+                appear: [],
+                vanish: [
+                  new PiPo({
+                    x: i, y: j, c: colIJ, p: this.getPiece(i, j)
+                  })
+                ]
+              })
+            );
           }
         }
       });
@@ -866,19 +926,20 @@ export class ChakartRules extends ChessRules {
         V.OnBoard(i, j) &&
         (
           this.board[i][j] == V.EMPTY ||
+          this.getPiece(i, j) == V.INVISIBLE_QUEEN ||
           (
             this.getColor(i, j) == 'a' &&
             [V.EGG, V.MUSHROOM].includes(this.getPiece(i, j))
           )
         )
       ) {
-        moves.push(this.getBasicMove([x, y], [i, j]));
+        moves.push(this.getBasicMove({ x: x, y: y }, [i, j]));
         if (oneStep) continue outerLoop;
         i += step[0];
         j += step[1];
       }
       if (V.OnBoard(i, j) && this.canTake([x, y], [i, j]))
-        moves.push(this.getBasicMove([x, y], [i, j]));
+        moves.push(this.getBasicMove({ x: x, y: y }, [i, j]));
     }
     return moves;
   }
@@ -886,66 +947,42 @@ export class ChakartRules extends ChessRules {
   getAllPotentialMoves() {
     if (this.subTurn == 1) return super.getAllPotentialMoves();
     let moves = [];
-    const L = this.firstMove.length;
-    const fm = this.firstMove[L-1];
-    switch (fm.end.effect) {
-      case 0:
-        moves.push({
-          start: { x: -1, y: -1 },
-          end: { x: -1, y: -1 },
-          appear: [],
-          vanish: []
-        });
-        for (
-          let step of
-          (fm.vanish[0].p == V.ROOK ? V.steps[V.BISHOP] : V.steps[V.ROOK])
-        ) {
-          const [i, j] = [fm.appear[0].x + step[0], fm.appear[0].y + step[1]];
-          if (
-            V.OnBoard(i, j) &&
-            (this.board[i][j] == V.EMPTY || this.getColor(i, j) == 'a')
-          ) {
-            let m = new Move({
-              start: { x: -1, y: -1 },
-              end: { x: i, y: j },
-              appear: [
-                new PiPo({
-                  x: i,
-                  y: j,
-                  c: 'a',
-                  p: (fm.vanish[0].p == V.ROOK ? V.BANANA : V.BOMB)
-                })
-              ],
-              vanish: []
-            });
-            if (this.board[i][j] != V.EMPTY) {
-              m.vanish.push(
-                new PiPo({ x: i, y: j, c: 'a', p: this.getPiece(i, j) }));
-            }
-            moves.push(m);
-          }
-        }
-        break;
+    const color = this.turn;
+    const L = this.effects.length;
+    switch (this.effects[L-1]) {
       case "kingboo": {
-        const [x, y] = [fm.appear[0].x, fm.appear[0].y];
+        let allPieces = [];
         for (let i=0; i<8; i++) {
           for (let j=0; j<8; j++) {
             const colIJ = this.getColor(i, j);
+            const pieceIJ = this.getPiece(i, j);
             if (
-              i != x &&
-              j != y &&
+              i != x && j != y &&
               this.board[i][j] != V.EMPTY &&
-              colIJ != 'a'
+              colIJ != 'a' &&
+              pieceIJ != V.INVISIBLE_QUEEN
             ) {
-              const movedUnit = new PiPo({
-                x: x,
-                y: y,
-                c: colIJ,
-                p: this.getPiece(i, j)
+              allPieces.push({ x: i, y: j, c: colIJ, p: pieceIJ });
+            }
+          }
+        }
+        for (let x=0; x<8; x++) {
+          for (let y=0; y<8; y++) {
+            if (this.getColor(i, j) == color) {
+              // Add exchange with something
+              allPieces.forEach(pp => {
+                if (pp.x != i || pp.y != j) {
+                  const movedUnit = new PiPo({
+                    x: x,
+                    y: y,
+                    c: pp.c,
+                    p: pp.p
+                  });
+                  let mMove = this.getBasicMove({ x: x, y: y }, [pp.x, pp.y]);
+                  mMove.appear.push(movedUnit);
+                  moves.push(mMove);
+                }
               });
-              let mMove = this.getBasicMove([x, y], [i, j]);
-              mMove.appear.push(movedUnit);
-              moves.push(mMove);
             }
           }
         }
@@ -958,66 +995,22 @@ export class ChakartRules extends ChessRules {
         break;
       }
       case "daisy":
-        moves = super.getPotentialMovesFrom([fm.appear[0].x, fm.appear[0].y]);
+        moves = super.getAllPotentialMoves();
         break;
     }
     return moves;
   }
 
-  doClick(square) {
-    if (isNaN(square[0])) return null;
-    if (this.subTurn == 1) return null;
-    const L = this.firstMove.length;
-    const fm = this.firstMove[L-1];
-    if (fm.end.effect != 0) return null;
-    const [x, y] = [square[0], square[1]];
-    const deltaX = Math.abs(fm.appear[0].x - x);
-    const deltaY = Math.abs(fm.appear[0].y - y);
-    if (deltaX == 0 && deltaY == 0) {
-      // Empty move:
-      return {
-        start: { x: -1, y: -1 },
-        end: { x: -1, y: -1 },
-        appear: [],
-        vanish: []
-      };
-    }
-    if (
-      (this.board[x][y] == V.EMPTY || this.getColor(x, y) == 'a') &&
-      (
-        (fm.vanish[0].p == V.ROOK && deltaX == 1 && deltaY == 1) ||
-        (fm.vanish[0].p == V.BISHOP && deltaX + deltaY == 1)
-      )
-    ) {
-      let m = new Move({
-        start: { x: -1, y: -1 },
-        end: { x: x, y: y },
-        appear: [
-          new PiPo({
-            x: x,
-            y: y,
-            c: 'a',
-            p: (fm.vanish[0].p == V.ROOK ? V.BANANA : V.BOMB)
-          })
-        ],
-        vanish: []
-      });
-      if (this.board[x][y] != V.EMPTY) {
-        m.vanish.push(
-          new PiPo({ x: x, y: y, c: 'a', p: this.getPiece(x, y) }));
-      }
-      return m;
-    }
-    return null;
-  }
-
   play(move) {
+//    if (!this.states) this.states = [];
+//    const stateFen = this.getFen();
+//    this.states.push(stateFen);
+
     move.flags = JSON.stringify(this.aggregateFlags());
-    this.epSquares.push(this.getEpSquare(move));
     V.PlayOnBoard(this.board, move);
     move.turn = [this.turn, this.subTurn];
-    if ([0, "kingboo", "toadette", "daisy"].includes(move.end.effect)) {
-      this.firstMove.push(move);
+    if (["kingboo", "toadette", "daisy"].includes(move.end.effect)) {
+      this.effects.push(move.end.effect);
       this.subTurn = 2;
     }
     else {
@@ -1031,82 +1024,85 @@ export class ChakartRules extends ChessRules {
   postPlay(move) {
     if (move.end.effect == "toadette") this.reserve = this.captured;
     else this.reserve = undefined;
-    if (move.vanish.length == 2 && move.vanish[1].c != 'a')
+    const color = move.turn[0];
+    if (
+      move.vanish.length == 2 &&
+      move.vanish[1].c != 'a' &&
+      move.appear.length == 1 //avoid king Boo!
+    ) {
       // Capture: update this.captured
-      this.captured[move.vanish[1].c][move.vanish[1].p]++;
+      let capturedPiece = move.vanish[1].p;
+      if (capturedPiece == V.INVISIBLE_QUEEN) capturedPiece = V.QUEEN;
+      else if (Object.keys(V.IMMOBILIZE_DECODE).includes(capturedPiece))
+        capturedPiece = V.IMMOBILIZE_DECODE[capturedPiece];
+      this.captured[move.vanish[1].c][capturedPiece]++;
+    }
     else if (move.vanish.length == 0) {
       if (move.appear.length == 0 || move.appear[0].c == 'a') return;
       // A piece is back on board
       this.captured[move.appear[0].c][move.appear[0].p]--;
     }
-
-
-
-
-// TODO: simplify and fix this
-
-
-
-    if (this.subTurn == 1) {
-      // Update flags:
-      if (
-        this.board[move.start.x][move.start.y] != V.EMPTY &&
-        this.getPiece(move.start.x, move.start.y) == V.KING &&
-        (
-          Math.abs(move.end.x - move.start.x) >= 2 ||
-          Math.abs(move.end.y - move.start.y) >= 2
-        )
-      ) {
-        const myColor = this.getColor(move.start.x, move.start.y);
-        this.powerFlags[myColor][V.KING] = false;
-      }
-      else if (
-        move.vanish[0].p == V.QUEEN &&
-        this.getPiece(move.end.x, move.end.y) == V.INVISIBLE_QUEEN
-      ) {
-        this.powerFlags[move.vanish[0].c][V.QUEEN] = false;
-      }
-      const color = move.vanish[0].c;
-      const oppCol = V.GetOppCol(color);
-      if (!(Object.keys(V.IMMOBILIZE_DECODE).includes(move.appear[0].p))) {
-        // Look for an immobilized piece of my color: it can now move
-        // Also make opponent invisible queen visible again, if any
-        for (let i=0; i<8; i++) {
-          for (let j=0; j<8; j++) {
-            if (this.board[i][j] != V.EMPTY) {
-              const colIJ = this.getColor(i, j);
-              const piece = this.getPiece(i, j);
-              if (
-                colIJ == color &&
-                Object.keys(V.IMMOBILIZE_DECODE).includes(piece)
-              ) {
-                this.board[i][j] = color + V.IMMOBILIZE_DECODE[piece];
-                move.wasImmobilized = [i, j];
-              }
-              else if (
-                colIJ == oppCol &&
-                piece == V.INVISIBLE_QUEEN
-              ) {
-                this.board[i][j] = oppCol + V.QUEEN;
-                move.wasInvisible = [i, j];
-              }
+    if (move.appear.length == 0) {
+      // Three cases: king "shell capture", Chomp or Koopa
+      if (this.getPiece(move.start.x, move.start.y) == V.KING)
+        // King remote capture:
+        this.powerFlags[color][V.KING] = false;
+      else if (move.end.effect == "chomp")
+        this.captured[color][move.vanish[0].p]++;
+    }
+    else if (move.appear[0].p == V.INVISIBLE_QUEEN)
+      this.powerFlags[move.appear[0].c][V.QUEEN] = false;
+    if (this.subTurn == 2) return;
+    if (
+      move.turn[1] == 1 &&
+      move.appear.length == 0 ||
+      !(Object.keys(V.IMMOBILIZE_DECODE).includes(move.appear[0].p))
+    ) {
+      // Look for an immobilized piece of my color: it can now move
+      for (let i=0; i<8; i++) {
+        for (let j=0; j<8; j++) {
+          if (this.board[i][j] != V.EMPTY) {
+            const piece = this.getPiece(i, j);
+            if (
+              this.getColor(i, j) == color &&
+              Object.keys(V.IMMOBILIZE_DECODE).includes(piece)
+            ) {
+              this.board[i][j] = color + V.IMMOBILIZE_DECODE[piece];
+              move.wasImmobilized = [i, j];
             }
           }
         }
       }
     }
+    // Also make opponent invisible queen visible again, if any
+    const oppCol = V.GetOppCol(color);
+    for (let i=0; i<8; i++) {
+      for (let j=0; j<8; j++) {
+        if (
+          this.board[i][j] != V.EMPTY &&
+          this.getColor(i, j) == oppCol &&
+          this.getPiece(i, j) == V.INVISIBLE_QUEEN
+        ) {
+          this.board[i][j] = oppCol + V.QUEEN;
+          move.wasInvisible = [i, j];
+        }
+      }
+    }
   }
 
   undo(move) {
-    this.epSquares.pop();
     this.disaggregateFlags(JSON.parse(move.flags));
     V.UndoOnBoard(this.board, move);
-    if ([0, "kingboo", "toadette", "daisy"].includes(move.end.effect))
-      this.firstMove.pop();
+    if (["kingboo", "toadette", "daisy"].includes(move.end.effect))
+      this.effects.pop();
     else this.movesCount--;
     this.turn = move.turn[0];
     this.subTurn = move.turn[1];
     this.postUndo(move);
+
+//    const stateFen = this.getFen();
+//    if (stateFen != this.states[this.states.length-1]) debugger;
+//    this.states.pop();
   }
 
   postUndo(move) {
@@ -1117,16 +1113,22 @@ export class ChakartRules extends ChessRules {
     }
     if (!!move.wasInvisible) {
       const [i, j] = move.wasInvisible;
-      this.board[i][j] =
-        this.getColor(i, j) + V.INVISIBLE_QUEEN;
+      this.board[i][j] = this.getColor(i, j) + V.INVISIBLE_QUEEN;
+    }
+    if (move.vanish.length == 2 && move.vanish[1].c != 'a') {
+      let capturedPiece = move.vanish[1].p;
+      if (capturedPiece == V.INVISIBLE_QUEEN) capturedPiece = V.QUEEN;
+      else if (Object.keys(V.IMMOBILIZE_DECODE).includes(capturedPiece))
+        capturedPiece = V.IMMOBILIZE_DECODE[capturedPiece];
+      this.captured[move.vanish[1].c][capturedPiece]--;
     }
-    if (move.vanish.length == 2 && move.vanish[1].c != 'a')
-      this.captured[move.vanish[1].c][move.vanish[1].p]--;
     else if (move.vanish.length == 0) {
       if (move.appear.length == 0 || move.appear[0].c == 'a') return;
       // A piece was back on board
       this.captured[move.appear[0].c][move.appear[0].p]++;
     }
+    else if (move.appear.length == 0 && move.end.effect == "chomp")
+      this.captured[move.vanish[0].c][move.vanish[0].p]--;
     if (move.vanish.length == 0) this.reserve = this.captured;
     else this.reserve = undefined;
   }
@@ -1154,11 +1156,11 @@ export class ChakartRules extends ChessRules {
     return "*";
   }
 
-  static GenRandInitFen(randomness) {
+  static GenRandInitFen(options) {
     return (
-      SuicideRules.GenRandInitFen(randomness).slice(0, -1) +
-      // Add Peach + Mario flags, re-add en-passant + capture counts
-      "1111 0000000000"
+      SuicideRules.GenRandInitFen(options).slice(0, -1) +
+      // Add Peach + Mario flags + capture counts
+      "1111 000000000000"
     );
   }
 
@@ -1166,17 +1168,50 @@ export class ChakartRules extends ChessRules {
     return moves;
   }
 
+  static get VALUES() {
+    return Object.assign(
+      {},
+      ChessRules.VALUES,
+      {
+        s: 1,
+        u: 5,
+        o: 3,
+        c: 3,
+        t: 9,
+        l: 1000,
+        e: 0,
+        d: 0,
+        w: 0,
+        m: 0
+      }
+    );
+  }
+
+  static get SEARCH_DEPTH() {
+    return 1;
+  }
+
   getComputerMove() {
-    // Random mover:
     const moves = this.getAllValidMoves();
-    let move1 = moves[randInt(moves.length)];
+    // Split into "normal" and "random" moves:
+    // (Next splitting condition is OK because cannot take self object
+    // without a banana or bomb on the way).
+    const deterministicMoves = moves.filter(m => {
+      return m.vanish.every(a => a.c != 'a' || a.p == V.MUSHROOM);
+    });
+    const randomMoves = moves.filter(m => {
+      return m.vanish.some(a => a.c == 'a' && a.p != V.MUSHROOM);
+    });
+    if (Math.random() < deterministicMoves.length / randomMoves.length)
+      // Play a deterministic one: capture king or material if possible
+      return super.getComputerMove(deterministicMoves);
+    // Play a random effect move, at random:
+    let move1 = randomMoves[randInt(randomMoves.length)];
     this.play(move1);
     let move2 = undefined;
     if (this.subTurn == 2) {
       const moves2 = this.getAllValidMoves();
       move2 = moves2[randInt(moves2.length)];
-console.log(move2);
-
     }
     this.undo(move1);
     if (!move2) return move1;
@@ -1184,12 +1219,7 @@ console.log(move2);
   }
 
   getNotation(move) {
-    if (move.vanish.length == 0) {
-      if (move.appear.length == 0) return "-";
-      const piece =
-        move.appear[0].p != V.PAWN ? move.appear[0].p.toUpperCase() : "";
-      return piece + "@" + V.CoordsToSquare(move.end);
-    }
+    if (move.vanish.length == 0 && move.appear.length == 0) return "-";
     if (
       !move.end.effect &&
       move.appear.length > 0 &&
@@ -1198,18 +1228,55 @@ console.log(move2);
       return "Q??";
     }
     const finalSquare = V.CoordsToSquare(move.end);
-    const piece = move.vanish[0].p;
-    if (move.appear.length == 0) {
-      // Koopa or Chomp
+    // Next condition also includes Toadette placements:
+    if (move.appear.length > 0 && move.vanish.every(a => a.c == 'a')) {
+      const piece =
+        move.appear[0].p != V.PAWN ? move.appear[0].p.toUpperCase() : "";
+      return piece + "@" + finalSquare;
+    }
+    else if (move.appear.length == 0) {
+      const piece = this.getPiece(move.start.x, move.start.y);
+      if (piece == V.KING && !move.end.effect)
+        // King remote capture
+        return "Kx" + finalSquare;
+      // Koopa or Chomp, or loopback after bananas, bombs & mushrooms:
       return (
         piece.toUpperCase() + "x" + finalSquare +
-        "*" + (move.end.effect == "koopa" ? "K" : "C")
+        (
+          !!move.end.effect
+            ? "*" + (move.end.effect == "koopa" ? "K" : "C")
+            : ""
+        )
       );
     }
+    if (move.appear.length == 1 && move.vanish.length == 1) {
+      const moveStart = move.appear[0].p.toUpperCase() + "@";
+      if (move.appear[0].c == 'a' && move.vanish[0].c == 'a')
+        // Bonus replacement:
+        return moveStart + finalSquare;
+      if (
+        move.vanish[0].p == V.INVISIBLE_QUEEN &&
+        move.appear[0].x == move.vanish[0].x &&
+        move.appear[0].y == move.vanish[0].y
+      ) {
+        // Toadette takes invisible queen
+        return moveStart + "Q" + finalSquare;
+      }
+    }
+    if (
+      move.appear.length == 2 &&
+      move.vanish.length == 2 &&
+      move.appear.every(a => a.c != 'a') &&
+      move.vanish.every(v => v.c != 'a')
+    ) {
+      // King Boo exchange
+      return V.CoordsToSquare(move.start) + finalSquare;
+    }
+    const piece = move.vanish[0].p;
     let notation = undefined;
     if (piece == V.PAWN) {
       // Pawn move
-      if (move.vanish.length >= 2) {
+      if (this.board[move.end.x][move.end.y] != V.EMPTY) {
         // Capture
         const startColumn = V.CoordToColumn(move.start.y);
         notation = startColumn + "x" + finalSquare;
@@ -1222,7 +1289,7 @@ console.log(move2);
     else {
       notation =
         piece.toUpperCase() +
-        (move.vanish.length >= 2 ? "x" : "") +
+        (this.board[move.end.x][move.end.y] != V.EMPTY ? "x" : "") +
         finalSquare;
     }
     if (!!move.end.effect) {
@@ -1240,28 +1307,15 @@ console.log(move2);
           notation += "*M";
           break;
         case "luigi":
-          notation += "*L";
-          break;
         case "waluigi":
-          notation += "*W";
-          break;
-      }
-    }
-    else if (move.vanish.length >= 2 && move.vanish[1].c == 'a') {
-      let symbol = "";
-      switch (move.vanish[1].p) {
-        case V.MUSHROOM:
-          symbol = 'M';
-          break;
-        case V.BANANA:
-          symbol = 'B';
-          break;
-        case V.BOMB:
-          symbol = 'X';
+          const lastAppear = move.appear[move.appear.length - 1];
+          const effectOn =
+            V.CoordsToSquare({ x: lastAppear.x, y : lastAppear.y });
+          notation += "*" + move.end.effect[0].toUpperCase() + effectOn;
           break;
       }
-      notation.replace('x', 'x' + symbol);
     }
     return notation;
   }
+
 };