From: Benjamin Auder <benjamin.auder@somewhere>
Date: Thu, 21 Jan 2021 02:22:20 +0000 (+0100)
Subject: First draft of Emergo - still buggish. Fix Fanorona, avoid underscore in FEN and... 
X-Git-Url: https://git.auder.net/%7B%7B%20asset%28%27mixstore/images/assets/current/doc/%7B%7B%20targetUrl%20%7D%7D?a=commitdiff_plain;h=d982fffc441a0443be90ba6f57a94d1d60b702c8;p=vchess.git

First draft of Emergo - still buggish. Fix Fanorona, avoid underscore in FEN and file names
---

diff --git a/client/public/images/pieces/Emergo/generateSVG_simple.py b/client/public/images/pieces/Emergo/generateSVG_simple.py
index 1437626c..5cb53c9e 100755
--- a/client/public/images/pieces/Emergo/generateSVG_simple.py
+++ b/client/public/images/pieces/Emergo/generateSVG_simple.py
@@ -43,7 +43,7 @@ final = "</svg>"
 for color in ["white", "black"]:
     chrShift = 0 if color == "white" else 32
     for number in range(12):
-        filename = chr(65 + number + chrShift) + "_.svg"
+        filename = chr(65 + number + chrShift) + "@.svg"
         f = open(filename, "w")
         f.write(preamble)
         f.write("\n");
diff --git a/client/public/images/pieces/Otage/b_.png b/client/public/images/pieces/Otage/b@.png
similarity index 100%
rename from client/public/images/pieces/Otage/b_.png
rename to client/public/images/pieces/Otage/b@.png
diff --git a/client/public/images/pieces/Otage/w_.png b/client/public/images/pieces/Otage/w@.png
similarity index 100%
rename from client/public/images/pieces/Otage/w_.png
rename to client/public/images/pieces/Otage/w@.png
diff --git a/client/public/images/pieces/Pacosako/w_.png b/client/public/images/pieces/Pacosako/w@.png
similarity index 100%
rename from client/public/images/pieces/Pacosako/w_.png
rename to client/public/images/pieces/Pacosako/w@.png
diff --git a/client/src/translations/rules/Emergo/en.pug b/client/src/translations/rules/Emergo/en.pug
index 3a33838b..8c24bd6a 100644
--- a/client/src/translations/rules/Emergo/en.pug
+++ b/client/src/translations/rules/Emergo/en.pug
@@ -1,2 +1,62 @@
 p.boxed
-  | TODO
+  | Similar to Checkers, with prisoners stacked below capturers.
+
+p
+  | The 9x9 board is initially empty.
+  | Each player receives 12 stackable pieces, "in hand".
+  | At each turn, a player must either
+  ul
+    li.
+      Enter a new piece on the board such that the opponent cannot capture it.
+      However, if a capture is already possible before the move, then
+      the piece can be dropped anywhere.
+      White cannot place a piece in the center at move 1.
+    li Play a move on the board, along diagonals.
+
+p.
+  Simple moves are Ferz moves: one step diagonally.
+  Captures work exactly as in Checkers: by jumping over a diagonally adjacent
+  piece to land on a free square just behind.
+  However, the resulting situation is more complex. See below.
+  If a capture is possible, then it must be played; in this case no piece can
+  be introduced on the board.
+
+p TODO: diagram
+
+p.
+  Let us consider each unit as a compound entity containing W white pieces
+  and B black ones (initially W = 1 and B = 0 for white units,
+  and vice-versa for black).
+  Captures can then be described formally as follows.
+
+p.
+  As white:
+  If W1/B1 jumps over W2/B2 at square S2 to land on S1', then
+  W1/(B1+1) arrives on S1' and W2/(B2-1) remains on S2.
+  If W2 = B2 - 1 = 0, nothing remains at the captured unit location.
+  As black: exchange W and B above.
+
+p.
+  In other words, each unit is a stack of friendly and enemy pieces, with
+  friendly pieces on top. After each capture, the prisoners part of the
+  stack is incremented, while the "jailers" counterpart at the captured
+  location decreases by one.
+
+p.
+  When several capturing chains are available,
+  the player has to select one of the longest (as in Checkers).
+
+p TODO: diagram (from mindsports.nl)
+
+h3 More information
+
+p
+  | See the 
+  a(href="https://www.mindsports.nl/index.php/arena/emergo/88-rules")
+    | Emergo page
+  | &nbsp;on the author's website.
+  | Rules are also described on 
+  a(href="http://www.iggamecenter.com/info/en/emergo.html") iggamecenter
+  | , where you can also play this game.
+
+p Inventors: Christian Freeling and Ed van Zon (1986)
diff --git a/client/src/variants/Crazyhouse.js b/client/src/variants/Crazyhouse.js
index 64ce48b3..7fa47b72 100644
--- a/client/src/variants/Crazyhouse.js
+++ b/client/src/variants/Crazyhouse.js
@@ -185,17 +185,15 @@ export class CrazyhouseRules extends ChessRules {
   }
 
   atLeastOneMove() {
-    if (!super.atLeastOneMove()) {
-      // Search one reserve move
-      for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
-        const moves = this.filterValid(
-          this.getReserveMoves([V.size.x + (this.turn == "w" ? 0 : 1), i])
-        );
-        if (moves.length > 0) return true;
-      }
-      return false;
+    if (super.atLeastOneMove()) return true;
+    // Search one reserve move
+    for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
+      const moves = this.filterValid(
+        this.getReserveMoves([V.size.x + (this.turn == "w" ? 0 : 1), i])
+      );
+      if (moves.length > 0) return true;
     }
-    return true;
+    return false;
   }
 
   postPlay(move) {
diff --git a/client/src/variants/Emergo.js b/client/src/variants/Emergo.js
index 264186b3..caa2e982 100644
--- a/client/src/variants/Emergo.js
+++ b/client/src/variants/Emergo.js
@@ -1,13 +1,535 @@
-import { ChessRules } from "@/base_rules";
+import { ChessRules, Move, PiPo } from "@/base_rules";
+import { randInt } from "@/utils/alea";
+import { ArrayFun } from "@/utils/array";
 
-export class YoteRules extends ChessRules {
+export class EmergoRules extends ChessRules {
 
-  // TODO
-  //If (as white) a pile W1/B1 jumps over another pile W2/B2, it lets on the intermediate square exactly W2 men, to end as W1/(B1+B2).
-  //In the first case in the video, W1=1, B1=0, W2=0, B2=1 ==> 1/1 and finally 1/2 with nothing on intermediate squares since W2 is always 0.
-  //In the second case, W1=1, B1=0, W2=1, B2=1 ==> 1 man left on intermediate square, end as 1/1.
-  //...I think it's that (?). Not very well explained either on Wikipedia or mindsports.nl :/
-  //Found this link: http://www.iggamecenter.com/info/en/emergo.html - so it's all clear now ! I'll add the game soon.
-  //Btw, I'm not a big fan of this naming "men" for pieces, but, won't contradict the author on that 
+  // Simple encoding: A to L = 1 to 12, from left to right, if white controls.
+  // Lowercase if black controls.
+  // Single piece (no prisoners): A@ to L@ (+ lowercase)
+
+  static get HasFlags() {
+    return false;
+  }
+
+  static get HasEnpassant() {
+    return false;
+  }
+
+  static get DarkBottomRight() {
+    return true;
+  }
+
+  // board element == file name:
+  static board2fen(b) {
+    return b;
+  }
+  static fen2board(f) {
+    return f;
+  }
+
+  static IsGoodPosition(position) {
+    if (position.length == 0) return false;
+    const rows = position.split("/");
+    if (rows.length != V.size.x) return false;
+    for (let row of rows) {
+      let sumElts = 0;
+      for (let i = 0; i < row.length; i++) {
+        // Add only 0.5 per symbol because 2 per piece
+        if (row[i].toLowerCase().match(/^[a-lA-L@]$/)) sumElts += 0.5;
+        else {
+          const num = parseInt(row[i], 10);
+          if (isNaN(num) || num <= 0) return false;
+          sumElts += num;
+        }
+      }
+      if (sumElts != V.size.y) return false;
+    }
+    return true;
+  }
+
+  static GetBoard(position) {
+    const rows = position.split("/");
+    let board = ArrayFun.init(V.size.x, V.size.y, "");
+    for (let i = 0; i < rows.length; i++) {
+      let j = 0;
+      for (let indexInRow = 0; indexInRow < rows[i].length; indexInRow++) {
+        const character = rows[i][indexInRow];
+        const num = parseInt(character, 10);
+        // If num is a number, just shift j:
+        if (!isNaN(num)) j += num;
+        else
+          // Something at position i,j
+          board[i][j++] = V.fen2board(character + rows[i][++indexInRow]);
+      }
+    }
+    return board;
+  }
+
+  getPpath(b) {
+    return "Emergo/" + b;
+  }
+
+  getColor(x, y) {
+    if (x >= V.size.x) return x == V.size.x ? "w" : "b";
+    if (this.board[x][y].charCodeAt(0) < 97) return 'w';
+    return 'b';
+  }
+
+  getPiece() {
+    return V.PAWN; //unused
+  }
+
+  static IsGoodFen(fen) {
+    if (!ChessRules.IsGoodFen(fen)) return false;
+    const fenParsed = V.ParseFen(fen);
+    // 3) Check reserves
+    if (
+      !fenParsed.reserve ||
+      !fenParsed.reserve.match(/^([0-9]{1,2},?){2,2}$/)
+    ) {
+      return false;
+    }
+    return true;
+  }
+
+  static ParseFen(fen) {
+    const fenParts = fen.split(" ");
+    return Object.assign(
+      ChessRules.ParseFen(fen),
+      { reserve: fenParts[3] }
+    );
+  }
+
+  static get size() {
+    return { x: 9, y: 9 };
+  }
+
+  static GenRandInitFen(randomness) {
+    return "9/9/9/9/9/9/9/9/9 w 0 12,12";
+  }
+
+  getFen() {
+    return super.getFen() + " " + this.getReserveFen();
+  }
+
+  getFenForRepeat() {
+    return super.getFenForRepeat() + "_" + this.getReserveFen();
+  }
+
+  getReserveFen() {
+    return (
+      (!this.reserve["w"] ? 0 : this.reserve["w"][V.PAWN]) + "," +
+      (!this.reserve["b"] ? 0 : this.reserve["b"][V.PAWN])
+    );
+  }
+
+  getReservePpath(index, color) {
+    return "Emergo/" + (color == 'w' ? 'A' : 'a') + '@';
+  }
+
+  static get RESERVE_PIECES() {
+    return [V.PAWN]; //only array length matters
+  }
+
+  setOtherVariables(fen) {
+    const reserve =
+      V.ParseFen(fen).reserve.split(",").map(x => parseInt(x, 10));
+    this.reserve = {
+      w: { [V.PAWN]: reserve[0] },
+      b: { [V.PAWN]: reserve[1] }
+    };
+    // Local stack of captures during a turn (squares + directions)
+    this.captures = [ [] ];
+  }
+
+  atLeastOneCaptureFrom([x, y], color) {
+    for (let s of V.steps[V.BISHOP]) {
+      const [i, j] = [x + s[0], y + s[1]];
+      if (
+        V.OnBoard(i + s[0], j + s[1]) &&
+        this.board[i][j] != V.EMPTY &&
+        this.getColor(i, j) != color &&
+        this.board[i + s[0]][j + s[1]] == V.EMPTY
+      ) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  atLeastOneCapture(color) {
+    const L0 = this.captures.length;
+    const captures = this.captures[L0 - 1];
+    const L = captures.length;
+    if (L > 0) return this.atLeastOneCaptureFrom(captures[L-1].square, color);
+    for (let i = 0; i < V.size.x; i++) {
+      for (let j=0; j< V.size.y; j++) {
+        if (
+          this.board[i][j] != V.EMPTY &&
+          this.getColor(i, j) == color &&
+          this.atLeastOneCaptureFrom([i, j], color)
+        ) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  maxLengthIndices(caps) {
+    let maxLength = 0;
+    let res = [];
+    for (let i = 0; i < caps.length; i++) {
+      if (caps[i].length > maxLength) {
+        res = [i];
+        maxLength = caps[i].length;
+      }
+      else if (caps[i].length == maxLength) res.push(i);
+    }
+    return res;
+  };
+
+  getLongestCapturesFrom([x, y], color, locSteps) {
+    //
+    // TODO: debug here, from
+    // 9/9/2a@1a@4/5A@3/9/3aa1A@3/9/9/8A@ w 10 8,9
+    // White to move, double capture.
+    //
+    let res = [];
+    const L = locSteps.length;
+    const lastStep = (L > 0 ? locSteps[L-1] : null);
+    for (let s of V.steps[V.BISHOP]) {
+      if (!!lastStep && s[0] == -lastStep[0] && s[1] == -lastStep[1]) continue;
+      const [i, j] = [x + s[0], y + s[1]];
+      if (
+        V.OnBoard(i + s[0], j + s[1]) &&
+        this.board[i + s[0]][j + s[1]] == V.EMPTY &&
+        this.board[i][j] != V.EMPTY &&
+        this.getColor(i, j) != color
+      ) {
+        const move = this.getBasicMove([x, y], [i + s[0], j + s[1]], [i, j]);
+        locSteps.push(s);
+        V.PlayOnBoard(this.board, move);
+        const sRes = this.getLongestCapturesFrom(
+                       [i + s[0], j + s[1]], color, locSteps);
+        res.push({
+          step: s,
+          length: 1 + (sRes.length == 0 ? 0 : sRes[0].length)
+        });
+        locSteps.pop();
+        V.UndoOnBoard(this.board, move);
+      }
+    }
+    return this.maxLengthIndices(res).map(i => res[i]);
+  }
+
+  getAllLongestCaptures(color) {
+    const L0 = this.captures.length;
+    const captures = this.captures[L0 - 1];
+    const L = captures.length;
+    if (L > 0) {
+      let locSteps = [];
+      const caps = Object.assign(
+        { square: captures[L-1].square },
+        this.getLongestCapturesFrom(captures[L-1].square, color, locSteps)
+      );
+      return this.maxLengthIndices(caps).map(i => caps[i]);
+    }
+    let caps = [];
+    for (let i = 0; i < V.size.x; i++) {
+      for (let j=0; j < V.size.y; j++) {
+        if (
+          this.board[i][j] != V.EMPTY &&
+          this.getColor(i, j) == color
+        ) {
+          let locSteps = [];
+          let res = this.getLongestCapturesFrom([i, j], color, locSteps);
+          Array.prototype.push.apply(
+            caps,
+            res.map(r => Object.assign({ square: [i, j] }, r))
+          );
+        }
+      }
+    }
+
+console.log(caps);
+
+    return this.maxLengthIndices(caps).map(i => caps[i]);
+  }
+
+  getBasicMove([x1, y1], [x2, y2], capt) {
+    const cp1 = this.board[x1][y1];
+    if (!capt) {
+      return new Move({
+        appear: [ new PiPo({ x: x2, y: y2, c: cp1[0], p: cp1[1] }) ],
+        vanish: [ new PiPo({ x: x1, y: y1, c: cp1[0], p: cp1[1] }) ]
+      });
+    }
+    // Compute resulting types based on jumped + jumping pieces
+    const cpCapt = this.board[capt[0]][capt[1]];
+    const newAtCapt = cpCapt.charCodeAt(0) - 1;
+    const newAtDest =
+      cp1[1] == '@'
+        ? (cp1.charCodeAt(0) < 97 ? 65 : 97)
+        : (cp1.charCodeAt(1) + 1);
+    const color = this.turn;
+    let mv = new Move({
+      appear: [
+        new PiPo({
+          x: x2,
+          y: y2,
+          c: cp1[0],
+          p: String.fromCharCode(newAtDest)
+        })
+      ],
+      vanish: [
+        new PiPo({ x: x1, y: y1, c: cp1[0], p: cp1[1] }),
+        new PiPo({ x: capt[0], y: capt[1], c: cpCapt[0], p: cpCapt[1] })
+      ]
+    });
+    if ([64, 96].includes(newAtCapt)) {
+      // Enemy units vanish from capturing square
+      if (cpCapt.charAt(1) != '@') {
+        // Out units remain:
+        mv.appear.push(
+          new PiPo({
+            x: capt[0],
+            y: capt[1],
+            c: cpCapt[0],
+            p: '@'
+          })
+        );
+      }
+    }
+    else {
+      mv.appear.push(
+        new PiPo({
+          x: capt[0],
+          y: capt[1],
+          c: String.fromCharCode(newAtCapt),
+          p: cpCapt[1]
+        })
+      );
+    }
+    return mv;
+  }
+
+  getReserveMoves(x) {
+    const color = this.turn;
+    if (!this.reserve[color] || this.atLeastOneCapture(color)) return [];
+    let moves = [];
+    const shadowPiece =
+      this.reserve[V.GetOppCol(color)] == null
+        ? this.reserve[color][V.PAWN] - 1
+        : 0;
+    const appearColor = String.fromCharCode(
+      (color == 'w' ? 'A' : 'a').charCodeAt(0) + shadowPiece);
+    const addMove = ([i, j]) => {
+      moves.push(
+        new Move({
+          appear: [ new PiPo({ x: i, y: j, c: appearColor, p: '@' }) ],
+          vanish: [],
+          start: { x: V.size.x + (color == 'w' ? 0 : 1), y: 0 }
+        })
+      );
+    };
+    const oppCol = V.GetOppCol(color);
+    const opponentCanCapture = this.atLeastOneCapture(oppCol);
+    for (let i = 0; i < V.size.x; i++) {
+      for (let j = i % 2; j < V.size.y; j += 2) {
+        if (
+          this.board[i][j] == V.EMPTY &&
+          // prevent playing on central square at move 1:
+          (this.movesCount >= 1 || i != 4 || j != 4)
+        ) {
+          if (opponentCanCapture) addMove([i, j]);
+          else {
+            let canAddMove = true;
+            for (let s of V.steps[V.BISHOP]) {
+              if (
+                V.OnBoard(i + s[0], j + s[1]) &&
+                V.OnBoard(i - s[0], j - s[1]) &&
+                this.board[i + s[0]][j + s[1]] != V.EMPTY &&
+                this.board[i - s[0]][j - s[1]] == V.EMPTY &&
+                this.getColor(i + s[0], j + s[1]) == oppCol
+              ) {
+                canAddMove = false;
+                break;
+              }
+            }
+            if (canAddMove) addMove([i, j]);
+          }
+        }
+      }
+    }
+    return moves;
+  }
+
+  getPotentialMovesFrom([x, y], longestCaptures) {
+    if (x >= V.size.x) {
+      if (longestCaptures.length == 0) return this.getReserveMoves(x);
+      return [];
+    }
+    const color = this.turn;
+    const L0 = this.captures.length;
+    const captures = this.captures[L0 - 1];
+    const L = captures.length;
+    let moves = [];
+    if (longestCaptures.length > 0) {
+      if (
+        L > 0 &&
+        (x != captures[L-1].square[0] || y != captures[L-1].square[1])
+      ) {
+        return [];
+      }
+      longestCaptures.forEach(lc => {
+        if (lc.square[0] == x && lc.square[1] == y) {
+          const s = lc.step;
+          const [i, j] = [x + s[0], y + s[1]];
+          moves.push(this.getBasicMove([x, y], [i + s[0], j + s[1]], [i, j]));
+        }
+      });
+      return moves;
+    }
+    // Just search simple moves:
+    for (let s of V.steps[V.BISHOP]) {
+      const [i, j] = [x + s[0], y + s[1]];
+      if (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY)
+        moves.push(this.getBasicMove([x, y], [i, j]));
+    }
+    return moves;
+  }
+
+  getAllValidMoves() {
+    const color = this.turn;
+    const longestCaptures = this.getAllLongestCaptures(color);
+    let potentialMoves = [];
+    for (let i = 0; i < V.size.x; i++) {
+      for (let j = 0; j < V.size.y; j++) {
+        if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) {
+          Array.prototype.push.apply(
+            potentialMoves,
+            this.getPotentialMovesFrom([i, j], longestCaptures)
+          );
+        }
+      }
+    }
+    // Add reserve moves
+    potentialMoves = potentialMoves.concat(
+      this.getReserveMoves(V.size.x + (color == "w" ? 0 : 1))
+    );
+    return potentialMoves;
+  }
+
+  getPossibleMovesFrom([x, y]) {
+    const longestCaptures = this.getAllLongestCaptures(this.getColor(x, y));
+    return this.getPotentialMovesFrom([x, y], longestCaptures);
+  }
+
+  filterValid(moves) {
+    return moves;
+  }
+
+  getCheckSquares() {
+    return [];
+  }
+
+  play(move) {
+    const color = this.turn;
+    move.turn = color; //for undo
+    V.PlayOnBoard(this.board, move);
+    if (move.vanish.length == 2) {
+      const L0 = this.captures.length;
+      let captures = this.captures[L0 - 1];
+      captures.push({
+        square: [move.start.x, move.start.y],
+        step: [move.end.x - move.start.x, move.end.y - move.start.y]
+      });
+      if (this.atLeastOneCapture())
+        // There could be other captures (optional)
+        move.notTheEnd = true;
+    }
+    else if (move.vanish == 0) {
+      if (--this.reserve[color][V.PAWN] == 0) this.reserve[color] = null;
+    }
+    if (!move.notTheEnd) {
+      this.turn = V.GetOppCol(color);
+      this.movesCount++;
+      this.captures.push([]);
+    }
+  }
+
+  undo(move) {
+    V.UndoOnBoard(this.board, move);
+    if (!move.notTheEnd) {
+      this.turn = move.turn;
+      this.movesCount--;
+      this.captures.pop();
+    }
+    if (move.vanish.length == 0) {
+      const color = (move.appear[0].c == 'A' ? 'w' : 'b');
+      if (!this.reserve[color]) this.reserve[color] = { [V.PAWN]: 1 };
+      else this.reserve[color][V.PAWN]++;
+    }
+    else if (move.vanish.length == 2) {
+      const L0 = this.captures.length;
+      let captures = this.captures[L0 - 1];
+      captures.pop();
+    }
+  }
+
+  atLeastOneMove() {
+    if (this.atLeastOneCapture()) return true;
+    const color = this.turn;
+    for (let i = 0; i < V.size.x; i++) {
+      for (let j = 0; j < V.size.y; j++) {
+        if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) {
+          const moves = this.getPotentialMovesFrom([i, j], []);
+          if (moves.length > 0) return true;
+        }
+      }
+    }
+    const reserveMoves =
+      this.getReserveMoves(V.size.x + (this.turn == "w" ? 0 : 1));
+    return (reserveMoves.length > 0);
+  }
+
+  getCurrentScore() {
+    const color = this.turn;
+    // If no pieces on board + reserve, I lose
+    if (
+      !this.reserve[color] &&
+      this.board.every(b => {
+        return b.every(cell => {
+          return (cell == "" || cell[0] != color);
+        });
+      })
+    ) {
+      return (color == 'w' ? "0-1" : "1-0");
+    }
+    if (!this.atLeastOneMove()) return "1/2";
+    return "*";
+  }
+
+  getComputerMove() {
+    // Random mover for now (TODO)
+    const color = this.turn;
+    let mvArray = [];
+    let mv = null;
+    while (this.turn == color) {
+      const moves = this.getAllValidMoves();
+      mv = moves[randInt(moves.length)];
+      mvArray.push(mv);
+      this.play(mv);
+    }
+    for (let i = mvArray.length - 1; i >= 0; i--) this.undo(mvArray[i]);
+    return (mvArray.length > 1 ? mvArray : mvArray[0]);
+  }
+
+  getNotation(move) {
+    if (move.vanish.length == 0) return "@" + V.CoordsToSquare(move.end);
+    return V.CoordsToSquare(move.start) + V.CoordsToSquare(move.end);
+  }
 
 };
diff --git a/client/src/variants/Fanorona.js b/client/src/variants/Fanorona.js
index 04eea2c3..39911635 100644
--- a/client/src/variants/Fanorona.js
+++ b/client/src/variants/Fanorona.js
@@ -246,18 +246,16 @@ export class FanoronaRules extends ChessRules {
     const color = this.turn;
     move.turn = color; //for undo
     V.PlayOnBoard(this.board, move);
-    const L0 = this.captures.length;
-    let captures = this.captures[L0 - 1];
     if (move.vanish.length >= 2) {
+      const L0 = this.captures.length;
+      let captures = this.captures[L0 - 1];
       captures.push({
         square: move.start,
         step: [move.end.x - move.start.x, move.end.y - move.start.y]
       });
       if (this.atLeastOneCapture())
         // There could be other captures (optional)
-        // This field is mostly useful for computer play.
         move.notTheEnd = true;
-      else captures.pop(); //useless now
     }
     if (!move.notTheEnd) {
       this.turn = V.GetOppCol(color);
@@ -268,12 +266,12 @@ export class FanoronaRules extends ChessRules {
 
   undo(move) {
     V.UndoOnBoard(this.board, move);
-    if (move.turn != this.turn) {
+    if (!move.notTheEnd) {
       this.turn = move.turn;
       this.movesCount--;
       this.captures.pop();
     }
-    else {
+    if (move.vanish.length >= 2) {
       const L0 = this.captures.length;
       let captures = this.captures[L0 - 1];
       captures.pop();
diff --git a/client/src/variants/Otage.js b/client/src/variants/Otage.js
index 9eb900a4..2a8891ff 100644
--- a/client/src/variants/Otage.js
+++ b/client/src/variants/Otage.js
@@ -36,7 +36,7 @@ export class OtageRules extends ChessRules {
       x: ['b', 'k'],
       y: ['q', 'q'],
       z: ['q', 'k'],
-      '_': ['k', 'k']
+      '@': ['k', 'k']
     };
   }
 
@@ -61,7 +61,7 @@ export class OtageRules extends ChessRules {
       for (let i = 0; i < row.length; i++) {
         const lowR = row[i].toLowerCase();
         const readNext = !(ChessRules.PIECES.includes(lowR));
-        if (!!(lowR.match(/[a-z_]/))) {
+        if (!!(lowR.match(/[a-z@]/))) {
           sumElts++;
           if (lowR == 'k') kings[row[i]]++;
           else if (readNext) {
@@ -146,7 +146,7 @@ export class OtageRules extends ChessRules {
         const c = fenRows[i].charAt(j);
         const lowR = c.toLowerCase();
         const readNext = !(ChessRules.PIECES.includes(lowR));
-        if (!!(lowR.match(/[a-z_]/))) {
+        if (!!(lowR.match(/[a-z@]/))) {
           if (lowR == 'k') this.kingPos[c == 'k' ? 'b' : 'w'] = [i, k];
           else if (readNext) {
             const up = this.getUnionPieces(fenRows[i][++j], lowR);
diff --git a/client/src/variants/Pacosako.js b/client/src/variants/Pacosako.js
index b5f703f8..158abf22 100644
--- a/client/src/variants/Pacosako.js
+++ b/client/src/variants/Pacosako.js
@@ -30,25 +30,25 @@ export class PacosakoRules extends ChessRules {
       x: ['b', 'k'],
       y: ['q', 'q'],
       z: ['q', 'k'],
-      '_': ['k', 'k']
+      '@': ['k', 'k']
     };
   }
 
   static fen2board(f) {
-    // Underscore is character 95, in file w_
-    return f.charCodeAt() <= 95 ? "w" + f.toLowerCase() : "b" + f;
+    // Arobase is character 64
+    return f.charCodeAt() <= 90 ? "w" + f.toLowerCase() : "b" + f;
   }
 
   static IsGoodPosition(position) {
     if (position.length == 0) return false;
     const rows = position.split("/");
     if (rows.length != V.size.x) return false;
-    let kingSymb = ['k', 'g', 'm', 'u', 'x', 'z', '_'];
+    let kingSymb = ['k', 'g', 'm', 'u', 'x', 'z', '@'];
     let kings = { 'k': 0, 'K': 0 };
     for (let row of rows) {
       let sumElts = 0;
       for (let i = 0; i < row.length; i++) {
-        if (!!(row[i].toLowerCase().match(/[a-z_]/))) {
+        if (!!(row[i].toLowerCase().match(/[a-z@]/))) {
           sumElts++;
           if (kingSymb.includes(row[i])) kings['k']++;
           // Not "else if", if two kings dancing together
@@ -104,12 +104,12 @@ export class PacosakoRules extends ChessRules {
     this.kingPos = { w: [-1, -1], b: [-1, -1] };
     const fenRows = V.ParseFen(fen).position.split("/");
     const startRow = { 'w': V.size.x - 1, 'b': 0 };
-    const kingSymb = ['k', 'g', 'm', 'u', 'x', 'z', '_'];
+    const kingSymb = ['k', 'g', 'm', 'u', 'x', 'z', '@'];
     for (let i = 0; i < fenRows.length; i++) {
       let k = 0;
       for (let j = 0; j < fenRows[i].length; j++) {
         const c = fenRows[i].charAt(j);
-        if (!!(c.toLowerCase().match(/[a-z_]/))) {
+        if (!!(c.toLowerCase().match(/[a-z@]/))) {
           if (kingSymb.includes(c))
             this.kingPos["b"] = [i, k];
           // Not "else if", in case of two kings dancing together