Add Fanorona
authorBenjamin Auder <benjamin.auder@somewhere>
Mon, 18 Jan 2021 19:25:19 +0000 (20:25 +0100)
committerBenjamin Auder <benjamin.auder@somewhere>
Mon, 18 Jan 2021 19:25:19 +0000 (20:25 +0100)
17 files changed:
TODO
client/public/images/pieces/Fanorona/arrow_-1_-1.svg [new file with mode: 0644]
client/public/images/pieces/Fanorona/arrow_-1_0.svg [new file with mode: 0644]
client/public/images/pieces/Fanorona/arrow_-1_1.svg [new file with mode: 0644]
client/public/images/pieces/Fanorona/arrow_0_-1.svg [new file with mode: 0644]
client/public/images/pieces/Fanorona/arrow_0_1.svg [new file with mode: 0644]
client/public/images/pieces/Fanorona/arrow_1_-1.svg [new file with mode: 0644]
client/public/images/pieces/Fanorona/arrow_1_0.svg [new file with mode: 0644]
client/public/images/pieces/Fanorona/arrow_1_1.svg [new file with mode: 0644]
client/src/base_rules.js
client/src/components/Board.vue
client/src/translations/rules/Fanorona/en.pug
client/src/translations/rules/Fanorona/es.pug
client/src/translations/rules/Fanorona/fr.pug
client/src/variants/Fanorona.js
client/src/variants/Konane.js
client/src/variants/Yote.js

diff --git a/TODO b/TODO
index 7a50f28..10788bd 100644 (file)
--- 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.
 
 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 (file)
index 0000000..b6cd789
--- /dev/null
@@ -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 (file)
index 0000000..bb22f86
--- /dev/null
@@ -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 (file)
index 0000000..c84fdfd
--- /dev/null
@@ -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 (file)
index 0000000..95699da
--- /dev/null
@@ -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 (file)
index 0000000..960fc9e
--- /dev/null
@@ -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 (file)
index 0000000..a25fea3
--- /dev/null
@@ -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 (file)
index 0000000..08948dd
--- /dev/null
@@ -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 (file)
index 0000000..702706e
--- /dev/null
@@ -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>
index 0549139..e8a82f9 100644 (file)
@@ -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).
 
   // 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++) {
   atLeastOneMove() {
     const color = this.turn;
     for (let i = 0; i < V.size.x; i++) {
index b176bd7..b452808 100644 (file)
@@ -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);
       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)
       let choicesHeight = squareWidth;
       if (this.choices.length >= sizeY) {
         // A second row is required (Eightpieces variant)
index 3a33838..cc397c6 100644 (file)
@@ -1,2 +1,60 @@
 p.boxed
 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.
index 3a33838..0ee8349 100644 (file)
@@ -1,2 +1,61 @@
 p.boxed
 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.
index 3a33838..8f13edb 100644 (file)
@@ -1,2 +1,61 @@
 p.boxed
 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.
index bd92c85..04eea2c 100644 (file)
@@ -46,38 +46,192 @@ export class FanoronaRules extends ChessRules {
 
   setOtherVariables(fen) {
     // Local stack of captures during a turn (squares + directions)
 
   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 size() {
     return { x: 5, y: 9 };
   }
 
-  static get PIECES() {
-    return [V.PAWN];
-  }
-
   getPiece() {
     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;
   }
 
   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]) {
 
   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) {
   }
 
   filterValid(moves) {
@@ -88,34 +242,42 @@ export class FanoronaRules extends ChessRules {
     return [];
   }
 
     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
   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.movesCount++;
+      this.captures.push([]);
     }
     }
-    this.postPlay(move);
   }
 
   undo(move) {
     V.UndoOnBoard(this.board, move);
   }
 
   undo(move) {
     V.UndoOnBoard(this.board, move);
-    this.captures.pop();
     if (move.turn != this.turn) {
       this.turn = move.turn;
       this.movesCount--;
     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() {
   }
 
   getCurrentScore() {
@@ -134,7 +296,7 @@ export class FanoronaRules extends ChessRules {
   }
 
   getComputerMove() {
   }
 
   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
     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);
       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;
     }
     }
     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);
     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)];
       this.undo(m);
     }
     if (candidates.length >= 1) return candidates[randInt(candidates.length)];
@@ -167,10 +328,11 @@ export class FanoronaRules extends ChessRules {
   }
 
   getNotation(move) {
   }
 
   getNotation(move) {
+    if (move.appear.length == 0) return "stop";
     return (
       V.CoordsToSquare(move.start) +
     return (
       V.CoordsToSquare(move.start) +
-      (move.vanish.length >= 2 ? "x" : "") +
-      V.CoordsToSquare(move.end)
+      V.CoordsToSquare(move.end) +
+      (move.vanish.length >= 2 ? "X" : "")
     );
   }
 
     );
   }
 
index 6acc54b..357bae7 100644 (file)
@@ -15,10 +15,6 @@ export class KonaneRules extends ChessRules {
     return true;
   }
 
     return true;
   }
 
-  static get PIECES() {
-    return V.PAWN;
-  }
-
   getPiece() {
     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++) {
     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;
         else {
           const num = parseInt(row[i], 10);
           if (isNaN(num) || num <= 0) return false;
index 7158d4f..76d7c2e 100644 (file)
@@ -23,6 +23,25 @@ export class YoteRules extends ChessRules {
     return true;
   }
 
     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);
   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 };
   }
 
     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);
   getColor(i, j) {
     if (i >= V.size.x) return i == V.size.x ? "w" : "b";
     return this.board[i][j].charAt(0);