From: Benjamin Auder <benjamin.auder@somewhere>
Date: Mon, 18 Jan 2021 19:25:19 +0000 (+0100)
Subject: Add Fanorona
X-Git-Url: https://git.auder.net/variants/current/doc/css/assets/rpsls.css?a=commitdiff_plain;h=cdab566355412821c9187078ee0864ceb30545de;p=vchess.git

Add Fanorona
---

diff --git a/TODO b/TODO
index 7a50f280..10788bd0 100644
--- a/TODO
+++ b/TODO
@@ -2,7 +2,6 @@ PROBABLY WON'T FIX:
 Embedded rules language not updated when language is set (in Analyse, Game and Problems)
 If new live game starts in background, "new game" notify OK but not first move.
 
-NEW VARIANTS, Non-chess ( won't add draughts: https://lidraughts.org/ )
-Gomoku, Konane
-Fanorona https://fr.wikipedia.org/wiki/Fanorona
-Yoté https://fr.wikipedia.org/wiki/Yot%C3%A9 http://www.zillionsofgames.com/cgi-bin/zilligames/submissions.cgi/92187?do=show;id=960
+"FreeBoard", re-using a lot of Board logic, but with SVG (empty) board + SVG (empty) reserves.
+Will be used for variants with custom non-rectangular board (Hex, at least)
+Or, with other board shapes (see greenchess.net for example)
diff --git a/client/public/images/pieces/Fanorona/arrow_-1_-1.svg b/client/public/images/pieces/Fanorona/arrow_-1_-1.svg
new file mode 100644
index 00000000..b6cd7893
--- /dev/null
+++ b/client/public/images/pieces/Fanorona/arrow_-1_-1.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="100" height="100">
+  <line x1="25" y1="25" x2="75" y2="75" stroke="black" stroke-width="3"/>
+  <circle cx="25" cy="25" r="10" fill="red" stroke="none"/>
+</svg>
diff --git a/client/public/images/pieces/Fanorona/arrow_-1_0.svg b/client/public/images/pieces/Fanorona/arrow_-1_0.svg
new file mode 100644
index 00000000..bb22f867
--- /dev/null
+++ b/client/public/images/pieces/Fanorona/arrow_-1_0.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="100" height="100">
+  <line x1="50" y1="25" x2="50" y2="75" stroke="black" stroke-width="3"/>
+  <circle cx="50" cy="25" r="10" fill="red" stroke="none"/>
+</svg>
diff --git a/client/public/images/pieces/Fanorona/arrow_-1_1.svg b/client/public/images/pieces/Fanorona/arrow_-1_1.svg
new file mode 100644
index 00000000..c84fdfd8
--- /dev/null
+++ b/client/public/images/pieces/Fanorona/arrow_-1_1.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="100" height="100">
+  <line x1="75" y1="25" x2="25" y2="75" stroke="black" stroke-width="3"/>
+  <circle cx="75" cy="25" r="10" fill="red" stroke="none"/>
+</svg>
diff --git a/client/public/images/pieces/Fanorona/arrow_0_-1.svg b/client/public/images/pieces/Fanorona/arrow_0_-1.svg
new file mode 100644
index 00000000..95699dad
--- /dev/null
+++ b/client/public/images/pieces/Fanorona/arrow_0_-1.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="100" height="100">
+  <line x1="25" y1="50" x2="75" y2="50" stroke="black" stroke-width="3"/>
+  <circle cx="25" cy="50" r="10" fill="red" stroke="none"/>
+</svg>
diff --git a/client/public/images/pieces/Fanorona/arrow_0_1.svg b/client/public/images/pieces/Fanorona/arrow_0_1.svg
new file mode 100644
index 00000000..960fc9eb
--- /dev/null
+++ b/client/public/images/pieces/Fanorona/arrow_0_1.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="100" height="100">
+  <line x1="25" y1="50" x2="75" y2="50" stroke="black" stroke-width="3"/>
+  <circle cx="75" cy="50" r="10" fill="red" stroke="none"/>
+</svg>
diff --git a/client/public/images/pieces/Fanorona/arrow_1_-1.svg b/client/public/images/pieces/Fanorona/arrow_1_-1.svg
new file mode 100644
index 00000000..a25fea34
--- /dev/null
+++ b/client/public/images/pieces/Fanorona/arrow_1_-1.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="100" height="100">
+  <line x1="75" y1="25" x2="25" y2="75" stroke="black" stroke-width="3"/>
+  <circle cx="25" cy="75" r="10" fill="red" stroke="none"/>
+</svg>
diff --git a/client/public/images/pieces/Fanorona/arrow_1_0.svg b/client/public/images/pieces/Fanorona/arrow_1_0.svg
new file mode 100644
index 00000000..08948dd7
--- /dev/null
+++ b/client/public/images/pieces/Fanorona/arrow_1_0.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="100" height="100">
+  <line x1="50" y1="25" x2="50" y2="75" stroke="black" stroke-width="3"/>
+  <circle cx="50" cy="75" r="10" fill="red" stroke="none"/>
+</svg>
diff --git a/client/public/images/pieces/Fanorona/arrow_1_1.svg b/client/public/images/pieces/Fanorona/arrow_1_1.svg
new file mode 100644
index 00000000..702706e3
--- /dev/null
+++ b/client/public/images/pieces/Fanorona/arrow_1_1.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="100" height="100">
+  <line x1="25" y1="25" x2="75" y2="75" stroke="black" stroke-width="3"/>
+  <circle cx="75" cy="75" r="10" fill="red" stroke="none"/>
+</svg>
diff --git a/client/src/base_rules.js b/client/src/base_rules.js
index 05491398..e8a82f93 100644
--- a/client/src/base_rules.js
+++ b/client/src/base_rules.js
@@ -1035,6 +1035,9 @@ export const ChessRules = class ChessRules {
 
   // Stop at the first move found
   // TODO: not really, it explores all moves from a square (one is enough).
+  // Possible fix: add extra arg "oneMove" to getPotentialMovesFrom,
+  // and then return only boolean true at first move found
+  // (in all getPotentialXXXMoves() ... for all variants ...)
   atLeastOneMove() {
     const color = this.turn;
     for (let i = 0; i < V.size.x; i++) {
diff --git a/client/src/components/Board.vue b/client/src/components/Board.vue
index b176bd73..b4528082 100644
--- a/client/src/components/Board.vue
+++ b/client/src/components/Board.vue
@@ -365,7 +365,7 @@ export default {
       const squareWidth = boardElt.offsetWidth / sizeY;
       const offset = [boardElt.offsetTop, boardElt.offsetLeft];
       const maxNbeltsPerRow = Math.min(this.choices.length, sizeY);
-      let topOffset = offset[0] + (sizeY / 2) * squareWidth - squareWidth / 2;
+      let topOffset = offset[0] + ((sizeX - 1) / 2) * squareWidth;
       let choicesHeight = squareWidth;
       if (this.choices.length >= sizeY) {
         // A second row is required (Eightpieces variant)
diff --git a/client/src/translations/rules/Fanorona/en.pug b/client/src/translations/rules/Fanorona/en.pug
index 3a33838b..cc397c60 100644
--- a/client/src/translations/rules/Fanorona/en.pug
+++ b/client/src/translations/rules/Fanorona/en.pug
@@ -1,2 +1,60 @@
 p.boxed
-  | TODO
+  | Capture stones by approach or withdrawal.
+
+p The following is summarized from Wikipedia.
+
+p.
+  The Fanorona board consists of 5 rows and 9 columns.
+  A stone can only move to an adjacent intersection, following the lines.
+  This movement can lead to captures.
+
+h3 General rules
+
+ul
+  li Players alternate turns, starting with White.
+  li There are two kinds of moves: non-capturing ("paika") and capturing.
+  li.
+    If at the beginning of a turn a capturing move is possible,
+    then it has to be played.
+  li
+    | Capturing implies removing one or more pieces of the opponent, in one of
+    | two ways:
+    ul
+      li.
+        Approach &mdash; moving the capturing stone to a point adjacent to an
+        opponent's stone, located right after in the movement's direction.
+      li.
+        Withdrawal &mdash; the capturing stone moves away from an opponent's
+        stone, initially adjacent.
+  li.
+    When an opponent stone is captured, all opponent pieces in line beyond
+    that stone (and connected to it) are captured as well.
+    Another capture can then be achieved with the same stone.
+
+figure.diagram-container
+  .diagram.diag12
+    | fen:pppp1p1pp/pppppPppp/pP1PPp1pP/PPPpPPPPP/PPPP1PPPP:
+  .diagram.diag22
+    | fen:pppp1p1pp/pppppP1pp/pP1PPpppP/PPPpPP1PP/PPPP1P1PP:
+  figcaption Before and after g4g3 (capturing g2 and g1).
+
+h3 Some restrictions
+
+ul
+  li.
+    An approach capture and a withdrawal capture cannot be made at the same
+    time. The two locations will appear on the interface and you'll have
+    to make a choice.
+  li
+    | The capturing piece is allowed to continue making successive captures,
+    | with these restrictions:
+    ul
+      li The piece is not allowed to arrive at the same position twice.
+      li.
+        It is not permitted to capture twice consecutively in the same
+        direction (first by withdrawal, and then by approach).
+    | However, continuing the capturing sequence is optional.
+
+p.
+  The game ends when one player captures all stones of the opponent.
+  If neither player can achieve this, then the game is a draw.
diff --git a/client/src/translations/rules/Fanorona/es.pug b/client/src/translations/rules/Fanorona/es.pug
index 3a33838b..0ee83499 100644
--- a/client/src/translations/rules/Fanorona/es.pug
+++ b/client/src/translations/rules/Fanorona/es.pug
@@ -1,2 +1,61 @@
 p.boxed
-  | TODO
+  | Captura piedras por percusión o succión.
+
+p El resto se resume en Wikipedia.
+
+p.
+  El tablero de Fanorona tiene 5 filas y 9 columnas.
+  Una piedra solo puede moverse a una intersección adyacente,
+  siguiendo las líneas. Este movimiento puede dar lugar a capturas.
+
+h3 Reglas generales
+
+ul
+  li Los jugadores alternan turnos, comenzando con las blancas.
+  li Hay dos tipos de jugadas: no capturadoras ("paika") y capturadoras.
+  li.
+    Si al comienzo de un turno es posible un movimiento de captura,
+    entonces debe jugarse.
+  li
+    | Capturar implica eliminar una o más piezas opuestas,
+    | una de las dos formas siguientes:
+    li.
+      Percusión &mdash; moviendo la piedra de captura a un punto
+      adyacente a una piedra enemiga, ubicada justo después en la dirección
+      del movimiento.
+    li.
+      Succión &mdash; la piedra capturadora se aleja de una piedra enemiga,
+      inicialmente adyacente.
+  li.
+    Cuando se captura una piedra enemiga, todas las demás piezas enemigas
+    en línea más allá de esta piedra (y conectados a ella) están
+    también capturado. Entonces es posible otra captura
+    con la misma piedra.
+
+figure.diagram-container
+  .diagram.diag12
+    | fen:pppp1p1pp/pppppPppp/pP1PPp1pP/PPPpPPPPP/PPPP1PPPP:
+  .diagram.diag22
+    | fen:pppp1p1pp/pppppP1pp/pP1PPpppP/PPPpPP1PP/PPPP1P1PP:
+  figcaption Antes y después de g4g3 (capturando g2 y g1).
+
+h3 Algunas restricciones
+
+ul
+  li.
+    No se puede realizar la captura por succión y percusión
+    al mismo tiempo. Ambas ubicaciones aparecerán en la pantalla y deberá
+    hacer una elección.
+  li
+    | Se permite que la pieza de captura continúe capturando, módulo el
+    | siguientes restricciones:
+    ul
+      li La pieza no se debe planchar dos veces en el mismo lugar.
+      li.
+        No puedes capturar dos veces consecutivas en la misma dirección
+        (primero por succión, luego por percusión).
+    | Sin embargo, continuar con la secuencia de captura es opcional.
+
+p.
+  El juego termina cuando uno de los jugadores ha capturado todos los
+  del otro. Si ninguno de ellos tiene éxito, entonces es un empate.
diff --git a/client/src/translations/rules/Fanorona/fr.pug b/client/src/translations/rules/Fanorona/fr.pug
index 3a33838b..8f13edb5 100644
--- a/client/src/translations/rules/Fanorona/fr.pug
+++ b/client/src/translations/rules/Fanorona/fr.pug
@@ -1,2 +1,61 @@
 p.boxed
-  | TODO
+  | Capturez des pierres par percussion ou aspiration.
+
+p La suite est résumée depuis Wikipedia.
+
+p.
+  Le plateau de Fanorona comporte 5 rangées et 9 colonnes.
+  Une pierre peut seulement se déplacer vers une intersection adjacente,
+  suivant les lignes. Ce mouvement peut donner lieu à des captures.
+
+h3 Règles générales
+
+ul
+  li Les joueurs alternent les tours, commençant avec les blancs.
+  li Il y a deux types de coups : non-capturants ("paika") et capturants.
+  li.
+    Si au début d'un tour un coup capturant est possible,
+    alors il doit être joué.
+  li
+    | Capturer implique retirer une ou plusieurs pièces adverses,
+    | d'une des deux manières suivantes :
+    li.
+      Percussion &mdash; en déplaçant la pierre capturante vers un point
+      adjacent à une pierre ennemie, située juste après dans la direction du
+      mouvement.
+    li.
+      Aspiration &mdash; la pierre capturante s'éloigne d'une pierre ennemie,
+      initialement adjacente.
+  li.
+    Quand une pierre ennemie est capturée, toutes les autres pièces adverses
+    en ligne au-delà de cette pierre (et connectées à celle-ci) sont
+    capturées également. Une autre capture est ensuite envisageable
+    avec la même pierre.
+
+figure.diagram-container
+  .diagram.diag12
+    | fen:pppp1p1pp/pppppPppp/pP1PPp1pP/PPPpPPPPP/PPPP1PPPP:
+  .diagram.diag22
+    | fen:pppp1p1pp/pppppP1pp/pP1PPpppP/PPPpPP1PP/PPPP1P1PP:
+  figcaption Avant et après g4g3 (capturant g2 et g1).
+
+h3 Quelques restrictions
+
+ul
+  li.
+    Une capture par percussion et une par aspiration ne peuvent être réalisées
+    en même temps. Les deux emplacements apparaitront à l'écran et vous devrez
+    faire un choix.
+  li
+    | La pièce capturante est autorisée à continuer de capturer, modulo les
+    | restrictions suivantes :
+    ul
+      li La pièce ne doit pas repasser deux fois au même endroit.
+      li.
+        On ne peut pas capturer deux fois consécutives dans la même direction
+        (d'abord par aspiration, puis par percussion).
+    | Cependant, continuer la séquence de capture est optionnel.
+
+p.
+  La partie s'achève quand un des joueurs a capturé toutes les pierres de
+  l'autre. Si aucun des deux n'y parvient, alors c'est un match nul.
diff --git a/client/src/variants/Fanorona.js b/client/src/variants/Fanorona.js
index bd92c859..04eea2c3 100644
--- a/client/src/variants/Fanorona.js
+++ b/client/src/variants/Fanorona.js
@@ -46,38 +46,192 @@ export class FanoronaRules extends ChessRules {
 
   setOtherVariables(fen) {
     // Local stack of captures during a turn (squares + directions)
-    this.captures = [];
+    this.captures = [ [] ];
   }
 
   static get size() {
     return { x: 5, y: 9 };
   }
 
-  static get PIECES() {
-    return [V.PAWN];
-  }
-
   getPiece() {
     return V.PAWN;
   }
 
+  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++) {
+        if (row[i].toLowerCase() == V.PAWN) sumElts++;
+        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;
+  }
+
   getPpath(b) {
     return "Fanorona/" + b;
   }
 
-  //TODO
-  //getPPpath() {}
+  getPPpath(m) {
+    // m.vanish.length >= 2, first capture gives direction
+    const ref = (Math.abs(m.vanish[1].x - m.start.x) == 1 ? m.start : m.end);
+    const step = [m.vanish[1].x - ref.x, m.vanish[1].y - ref.y];
+    const normalizedStep = [
+      step[0] / Math.abs(step[0]),
+      step[1] / Math.abs(step[1])
+    ];
+    return (
+      "Fanorona/arrow_" +
+      (normalizedStep[0] || 0) + "_" + (normalizedStep[1] || 0)
+    );
+  }
+
+  // After moving, add stones captured in "step" direction from new location
+  // [x, y] to mv.vanish (if any captured stone!)
+  addCapture([x, y], step, move) {
+    let [i, j] = [x + step[0], y + step[1]];
+    const oppCol = V.GetOppCol(move.vanish[0].c);
+    while (
+      V.OnBoard(i, j) &&
+      this.board[i][j] != V.EMPTY &&
+      this.getColor(i, j) == oppCol
+    ) {
+      move.vanish.push(new PiPo({ x: i, y: j, c: oppCol, p: V.PAWN }));
+      i += step[0];
+      j += step[1];
+    }
+    return (move.vanish.length >= 2);
+  }
 
   getPotentialMovesFrom([x, y]) {
-    // NOTE: (x + y) % 2 == 0 ==> has diagonals
-    // TODO
-    // Même stratégie que Yote, revenir sur ses pas si stop avant de tout capturer
-    // Mais première capture obligatoire (si this.captures.length == 0).
-    // After a capture: allow only capturing.
-    // Warning: case 3 on Wikipedia page, if both percussion & aspiration,
-    // two different moves, cannot take all ==> adjust getPPpath showing arrows.
-    // nice looking arrows, with something representing a capture at its end...
-    return [];
+    const L0 = this.captures.length;
+    const captures = this.captures[L0 - 1];
+    const L = captures.length;
+    if (L > 0) {
+      var c = captures[L-1];
+      if (x != c.square.x + c.step[0] || y != c.square.y + c.step[1])
+        return [];
+    }
+    const oppCol = V.GetOppCol(this.turn);
+    let steps = V.steps[V.ROOK];
+    if ((x + y) % 2 == 0) steps = steps.concat(V.steps[V.BISHOP]);
+    let moves = [];
+    for (let s of steps) {
+      if (L > 0 && c.step[0] == s[0] && c.step[1] == s[1]) {
+        // Add a move to say "I'm done capturing"
+        moves.push(
+          new Move({
+            appear: [],
+            vanish: [],
+            start: { x: x, y: y },
+            end: { x: x - s[0], y: y - s[1] }
+          })
+        );
+        continue;
+      }
+      let [i, j] = [x + s[0], y + s[1]];
+      if (captures.some(c => c.square.x == i && c.square.y == j)) continue;
+      if (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
+        // The move is potentially allowed. Might lead to 2 different captures
+        let mv = super.getBasicMove([x, y], [i, j]);
+        const capt = this.addCapture([i, j], s, mv);
+        if (capt) {
+          moves.push(mv);
+          mv = super.getBasicMove([x, y], [i, j]);
+        }
+        const capt_bw = this.addCapture([x, y], [-s[0], -s[1]], mv);
+        if (capt_bw) moves.push(mv);
+        // Captures take priority (if available)
+        if (!capt && !capt_bw && L == 0) moves.push(mv);
+      }
+    }
+    return moves;
+  }
+
+  atLeastOneCapture() {
+    const color = this.turn;
+    const oppCol = V.GetOppCol(color);
+    const L0 = this.captures.length;
+    const captures = this.captures[L0 - 1];
+    const L = captures.length;
+    if (L > 0) {
+      // If some adjacent enemy stone, with free space to capture it,
+      // toward a square not already visited, through a different step
+      // from last one: then yes.
+      const c = captures[L-1];
+      const [x, y] = [c.square.x + c.step[0], c.square.y + c.step[1]];
+      let steps = V.steps[V.ROOK];
+      if ((x + y) % 2 == 0) steps = steps.concat(V.steps[V.BISHOP]);
+      // TODO: half of the steps explored are redundant
+      for (let s of steps) {
+        if (s[0] == c.step[0] && s[1] == c.step[1]) continue;
+        const [i, j] = [x + s[0], y + s[1]];
+        if (
+          !V.OnBoard(i, j) ||
+          this.board[i][j] != V.EMPTY ||
+          captures.some(c => c.square.x == i && c.square.y == j)
+        ) {
+          continue;
+        }
+        if (
+          V.OnBoard(i + s[0], j + s[1]) &&
+          this.board[i + s[0]][j + s[1]] != V.EMPTY &&
+          this.getColor(i + s[0], j + s[1]) == oppCol
+        ) {
+          return true;
+        }
+        if (
+          V.OnBoard(x - s[0], y - s[1]) &&
+          this.board[x - s[0]][y - s[1]] != V.EMPTY &&
+          this.getColor(x - s[0], y - s[1]) == oppCol
+        ) {
+          return true;
+        }
+      }
+      return false;
+    }
+    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 &&
+          // TODO: this could be more efficient
+          this.getPotentialMovesFrom([i, j]).some(m => m.vanish.length >= 2)
+        ) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  static KeepCaptures(moves) {
+    return moves.filter(m => m.vanish.length >= 2);
+  }
+
+  getPossibleMovesFrom(sq) {
+    let moves = this.getPotentialMovesFrom(sq);
+    const L0 = this.captures.length;
+    const captures = this.captures[L0 - 1];
+    if (captures.length > 0) return this.getPotentialMovesFrom(sq);
+    const captureMoves = V.KeepCaptures(moves);
+    if (captureMoves.length > 0) return captureMoves;
+    if (this.atLeastOneCapture()) return [];
+    return moves;
+  }
+
+  getAllValidMoves() {
+    const moves = super.getAllValidMoves();
+    if (moves.some(m => m.vanish.length >= 2)) return V.KeepCaptures(moves);
+    return moves;
   }
 
   filterValid(moves) {
@@ -88,34 +242,42 @@ export class FanoronaRules extends ChessRules {
     return [];
   }
 
-  //TODO: function aux to detect if continuation captures
-  //(not trivial, but not difficult)
-
   play(move) {
     const color = this.turn;
     move.turn = color; //for undo
-    const captureNotEnding = (
-      move.vanish.length >= 2 &&
-      true //TODO: detect if there are continuation captures
-    );
-    this.captures.push(captureNotEnding); //TODO: something more structured
-                                          //with square + direction of capture
-    if (captureNotEnding) move.notTheEnd = true;
-    else {
-      this.turn = oppCol;
+    V.PlayOnBoard(this.board, move);
+    const L0 = this.captures.length;
+    let captures = this.captures[L0 - 1];
+    if (move.vanish.length >= 2) {
+      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);
       this.movesCount++;
+      this.captures.push([]);
     }
-    this.postPlay(move);
   }
 
   undo(move) {
     V.UndoOnBoard(this.board, move);
-    this.captures.pop();
     if (move.turn != this.turn) {
       this.turn = move.turn;
       this.movesCount--;
+      this.captures.pop();
+    }
+    else {
+      const L0 = this.captures.length;
+      let captures = this.captures[L0 - 1];
+      captures.pop();
     }
-    this.postUndo(move);
   }
 
   getCurrentScore() {
@@ -134,7 +296,7 @@ export class FanoronaRules extends ChessRules {
   }
 
   getComputerMove() {
-    const moves = super.getAllValidMoves();
+    const moves = this.getAllValidMoves();
     if (moves.length == 0) return null;
     const color = this.turn;
     // Capture available? If yes, play it
@@ -147,19 +309,18 @@ export class FanoronaRules extends ChessRules {
       if (candidates.length >= 1) mv = candidates[randInt(candidates.length)];
       else mv = captures[randInt(captures.length)];
       this.play(mv);
-      captures = (this.turn == color ? super.getAllValidMoves() : []);
+      mvArray.push(mv);
+      captures = (this.turn == color ? this.getAllValidMoves() : []);
     }
     if (mvArray.length >= 1) {
       for (let i = mvArray.length - 1; i >= 0; i--) this.undo(mvArray[i]);
       return mvArray;
     }
-    // Just play a random move, which if possible do not let a capture
+    // Just play a random move, which if possible does not let a capture
     let candidates = [];
     for (let m of moves) {
       this.play(m);
-      const moves2 = super.getAllValidMoves();
-      if (moves2.every(m2 => m2.vanish.length <= 1))
-        candidates.push(m);
+      if (!this.atLeastOneCapture()) candidates.push(m);
       this.undo(m);
     }
     if (candidates.length >= 1) return candidates[randInt(candidates.length)];
@@ -167,10 +328,11 @@ export class FanoronaRules extends ChessRules {
   }
 
   getNotation(move) {
+    if (move.appear.length == 0) return "stop";
     return (
       V.CoordsToSquare(move.start) +
-      (move.vanish.length >= 2 ? "x" : "") +
-      V.CoordsToSquare(move.end)
+      V.CoordsToSquare(move.end) +
+      (move.vanish.length >= 2 ? "X" : "")
     );
   }
 
diff --git a/client/src/variants/Konane.js b/client/src/variants/Konane.js
index 6acc54ba..357bae75 100644
--- a/client/src/variants/Konane.js
+++ b/client/src/variants/Konane.js
@@ -15,10 +15,6 @@ export class KonaneRules extends ChessRules {
     return true;
   }
 
-  static get PIECES() {
-    return V.PAWN;
-  }
-
   getPiece() {
     return V.PAWN;
   }
@@ -34,7 +30,7 @@ export class KonaneRules extends ChessRules {
     for (let row of rows) {
       let sumElts = 0;
       for (let i = 0; i < row.length; i++) {
-        if (V.PIECES.includes(row[i].toLowerCase())) sumElts++;
+        if (row[i].toLowerCase() == V.PAWN) sumElts++;
         else {
           const num = parseInt(row[i], 10);
           if (isNaN(num) || num <= 0) return false;
diff --git a/client/src/variants/Yote.js b/client/src/variants/Yote.js
index 7158d4f4..76d7c2e9 100644
--- a/client/src/variants/Yote.js
+++ b/client/src/variants/Yote.js
@@ -23,6 +23,25 @@ export class YoteRules extends ChessRules {
     return true;
   }
 
+  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++) {
+        if (row[i].toLowerCase() == V.PAWN) sumElts++;
+        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 IsGoodFen(fen) {
     if (!ChessRules.IsGoodFen(fen)) return false;
     const fenParsed = V.ParseFen(fen);
@@ -120,10 +139,6 @@ export class YoteRules extends ChessRules {
     return { x: 5, y: 6 };
   }
 
-  static get PIECES() {
-    return [V.PAWN];
-  }
-
   getColor(i, j) {
     if (i >= V.size.x) return i == V.size.x ? "w" : "b";
     return this.board[i][j].charAt(0);