From a19caec0e72dd3b37af961daabc35eb789476ab1 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Sat, 5 Dec 2020 16:07:37 +0100
Subject: [PATCH] Fix Fullcavalry + complete rules. Also complete Atomic2 rules

---
 client/src/base_rules.js                      |   2 +-
 client/src/translations/rules/Atomic2/en.pug  |   5 +-
 client/src/translations/rules/Atomic2/es.pug  |  24 ++-
 client/src/translations/rules/Atomic2/fr.pug  |  24 ++-
 .../src/translations/rules/Fullcavalry/en.pug |  27 ++-
 .../src/translations/rules/Fullcavalry/es.pug |  33 +++-
 .../src/translations/rules/Fullcavalry/fr.pug |  28 ++-
 client/src/variants/Fullcavalry.js            | 175 +++++++++++++++++-
 8 files changed, 288 insertions(+), 30 deletions(-)

diff --git a/client/src/base_rules.js b/client/src/base_rules.js
index d17ebe67..46c09a5c 100644
--- a/client/src/base_rules.js
+++ b/client/src/base_rules.js
@@ -969,7 +969,7 @@ export const ChessRules = class ChessRules {
           ],
           vanish: [
             // King might be initially disguised (Titan...)
-            new PiPo({ x: x, y: y, p: this.board[x][y][1], c: c }),
+            new PiPo({ x: x, y: y, p: castlingKing, c: c }),
             new PiPo({ x: x, y: rookPos, p: castlingPiece, c: c })
           ],
           end:
diff --git a/client/src/translations/rules/Atomic2/en.pug b/client/src/translations/rules/Atomic2/en.pug
index dc0e87a0..9311018e 100644
--- a/client/src/translations/rules/Atomic2/en.pug
+++ b/client/src/translations/rules/Atomic2/en.pug
@@ -9,15 +9,14 @@ p.
 figure.diagram-container
   .diagram
     | fen:rnbqkbnr/pppppppp/8/8/8/8/PPPP1PPP/RNBQKBNR:
-  figcaption.
-    After 1.e2X, pawn removal at e2.
+  figcaption After 1.e2X, pawn removal at e2.
 
 h3 More information
 
 p
   | This variant aims at balancing the big white's advantage in Atomic1.
   | It was suggested (relayed?) and analyzed recently (2020) by a strong
-  | Atomic player (Gannet on Discord). See
+  | Atomic player (Gannet on Discord). See 
   a(href="https://discord.com/channels/686736099959504907/687076744095858762/762398439043498046")
     | the messages
   | &nbsp;on Discord vchess server.
diff --git a/client/src/translations/rules/Atomic2/es.pug b/client/src/translations/rules/Atomic2/es.pug
index 21203baa..56cdaff2 100644
--- a/client/src/translations/rules/Atomic2/es.pug
+++ b/client/src/translations/rules/Atomic2/es.pug
@@ -1 +1,23 @@
-p.boxed TODO
+p.boxed.
+  Atomic1, pero el primer movimiento de las blancas es quitar un peón.
+
+p.
+  En el primer movimiento del juego, las blancas deben elegir un peón para
+  borrar, blanco o negro.
+  Entonces las negras responden, luego el juego continúa normalmente.
+
+figure.diagram-container
+  .diagram
+    | fen:rnbqkbnr/pppppppp/8/8/8/8/PPPP1PPP/RNBQKBNR:
+  figcaption Después de 1.e2X, remoción de peones en e2.
+
+h3 Más información
+
+p
+  | Esta variante tiene como objetivo reequilibrar la gran ventaja de las
+  | blancas en Atomic1. Ha sido sugerido (¿retransmitido?) y analizado
+  | recientemente (2020) por un fuerte jugador de ajedrez atómico
+  | (Gannet en Discord). Ver 
+  a(href="https://discord.com/channels/686736099959504907/687076744095858762/762398439043498046")
+    | los mensajes
+  | &nbsp;en el servidor Discord de vchess.
diff --git a/client/src/translations/rules/Atomic2/fr.pug b/client/src/translations/rules/Atomic2/fr.pug
index 21203baa..d072c7b1 100644
--- a/client/src/translations/rules/Atomic2/fr.pug
+++ b/client/src/translations/rules/Atomic2/fr.pug
@@ -1 +1,23 @@
-p.boxed TODO
+p.boxed.
+  Atomic1, mais le premier coup des blancs consiste à supprimer un pion.
+
+p.
+  Au tout premier coup de la partie, les blancs doivent choisir un pion à
+  supprimer, blanc ou noir.
+  Les noirs répondent alors, puis la partie continue normalement.
+
+figure.diagram-container
+  .diagram
+    | fen:rnbqkbnr/pppppppp/8/8/8/8/PPPP1PPP/RNBQKBNR:
+  figcaption.
+    Après 1.e2X, suppression de pion en e2.
+
+h3 Plus d'information
+
+p
+  | Cette variante vise à rééquilibrer le gros avantage blanc en Atomic1.
+  | Elle a été suggérée (relayée ?) et analysée récemment (2020) par un fort
+  | joueur d'échecs atomiques (Gannet sur Discord). Voir 
+  a(href="https://discord.com/channels/686736099959504907/687076744095858762/762398439043498046")
+    | les messages
+  | &nbsp;sur le serveur Discord de vchess.
diff --git a/client/src/translations/rules/Fullcavalry/en.pug b/client/src/translations/rules/Fullcavalry/en.pug
index 6bb88da3..98110364 100644
--- a/client/src/translations/rules/Fullcavalry/en.pug
+++ b/client/src/translations/rules/Fullcavalry/en.pug
@@ -8,16 +8,25 @@ p
   | .
 
 p.
-  White lancers are facing diagonally upward, while black lancers point
-  diagonally down in the deterministic initial setup.
-  In the randomized version, to avoid complex setup rules,
-  all lancers are facing up.
+  Lancers are initially facing each other.
+  They can be reoriented after castling, if they effectively moved.
+  While castling, some pieces can stand on the lancer's path,
+  since it can jump over them.
 
 figure.diagram-container
-  .diagram
-    | fen:efbqkbnm/pppppppp/8/8/8/8/PPPPPPPP/EDBQKBNM:
-  figcaption Initial deterministic position.
+  .diagram.diag12
+    | fen:nn1ekm2/p1ppppqp/bp3bp1/8/8/BPPN2N1/P1QPPPPP/EB4KM:
+  .diagram.diag22
+    | fen:nn1e1mk1/p1ppppqp/bp3bp1/8/8/BPPN2N1/P1QPPPPP/1BKC3M:
+  figcaption Before and after 0-0-0+L:N 0-0 (no reorientation)
 
-h3 More information
+p.
+  Pawn cannot promote in rooks, and, since they are absent of this game,
+  neither do they promote in jailer or sentry.
+
+h3 Source
 
-p TODO
+p
+  | This variant was imagined by the creator of 
+  a(href="/variants/Eightpieces") Eightpieces
+  | .
diff --git a/client/src/translations/rules/Fullcavalry/es.pug b/client/src/translations/rules/Fullcavalry/es.pug
index 21203baa..2067a1d1 100644
--- a/client/src/translations/rules/Fullcavalry/es.pug
+++ b/client/src/translations/rules/Fullcavalry/es.pug
@@ -1 +1,32 @@
-p.boxed TODO
+p.boxed
+  | Las torres son reemplazadas por lanceros de la variante Eightpieces.
+
+p
+  | Todo procede como en el ajedrez ortodoxo, excepto por
+  | torres que son reemplazadas por lanceros como se define en la 
+  a(href="variants/Eightpieces") variante Eightpieces
+  | .
+
+p.
+  Los Lancers inicialmente se enfrentan entre sí.
+  Se pueden reorientar después del enroque, si realmente se movieron.
+  Durante el enroque, se pueden encontrar piezas en el camino de la lanza,
+  ya que puede saltarlo.
+
+figure.diagram-container
+  .diagram.diag12
+    | fen:nn1ekm2/p1ppppqp/bp3bp1/8/8/BPPN2N1/P1QPPPPP/EB4KM:
+  .diagram.diag22
+    | fen:nn1e1mk1/p1ppppqp/bp3bp1/8/8/BPPN2N1/P1QPPPPP/1BKC3M:
+  figcaption Antes y después 0-0-0+L:N 0-0 (sin reorientación)
+
+p.
+  Los peones no pueden promoverse en torres y, dado que están ausentes de
+  este juego, no se promocionan a sí mismos como carceleros o centinelas.
+
+h3 Fuente
+
+p
+  | Esta variante fue imaginada por el creador de 
+  a(href="/variants/Eightpieces") Eightpieces
+  | .
diff --git a/client/src/translations/rules/Fullcavalry/fr.pug b/client/src/translations/rules/Fullcavalry/fr.pug
index 2605db40..e92a1afd 100644
--- a/client/src/translations/rules/Fullcavalry/fr.pug
+++ b/client/src/translations/rules/Fullcavalry/fr.pug
@@ -8,17 +8,25 @@ p
   | .
 
 p.
-  Les lanciers blancs regarde vers l'avant en diagonale, tandis que les
-  lanciers noirs pointent vers le sud (toujours en diagonale) dans
-  l'arrangement initial déterministe.
-  Dans la version aléatoire, tous les lanciers regardent droit devant afin
-  d'éviter des règles d'initialisation complexes.
+  Les lanciers se font initialement face.
+  Ils peuvent être réorientés après le roque, s'ils ont effectivement bougé.
+  Pendant le roque, des pièces peuvent se trouver sur le chemin du lancier,
+  puisqu'il peut sauter par dessus.
 
 figure.diagram-container
-  .diagram
-    | fen:efbqkbnm/pppppppp/8/8/8/8/PPPPPPPP/EDBQKBNM:
-  figcaption Position initiale déterministe.
+  .diagram.diag12
+    | fen:nn1ekm2/p1ppppqp/bp3bp1/8/8/BPPN2N1/P1QPPPPP/EB4KM:
+  .diagram.diag22
+    | fen:nn1e1mk1/p1ppppqp/bp3bp1/8/8/BPPN2N1/P1QPPPPP/1BKC3M:
+  figcaption Avant et après 0-0-0+L:N 0-0 (pas de réorientation)
 
-h3 Plus d'information
+p.
+  Les pions ne peuvent être promus en tours, et, puisqu'ils sont absents de
+  ce jeu, ils ne se promeuvent pas non plus en geôlier ou sentinelle.
+
+h3 Source
 
-p TODO
+p
+  | Cette variante a été imaginée par le créateur de 
+  a(href="/variants/Eightpieces") Eightpieces
+  | .
diff --git a/client/src/variants/Fullcavalry.js b/client/src/variants/Fullcavalry.js
index c25f3400..672ebc8a 100644
--- a/client/src/variants/Fullcavalry.js
+++ b/client/src/variants/Fullcavalry.js
@@ -76,9 +76,11 @@ export class FullcavalryRules extends ChessRules {
   }
 
   getPPpath(m, orientation) {
+    // If castle, show choices on m.appear[1]:
+    const index = (m.appear.length == 2 ? 1 : 0);
     return (
       this.getPpath(
-        m.appear[0].c + m.appear[0].p,
+        m.appear[index].c + m.appear[index].p,
         null,
         null,
         orientation
@@ -92,9 +94,18 @@ export class FullcavalryRules extends ChessRules {
       return "efbqkbnm/pppppppp/8/8/8/8/PPPPPPPP/EDBQKBNM w 0 ahah -";
 
     const baseFen = ChessRules.GenRandInitFen(randomness);
-    // Replace black rooks by lancers oriented south,
-    // and white rooks by lancers oriented north:
-    return baseFen.replace(/r/g, 'g').replace(/R/g, 'C');
+    // Replace rooks by lancers with expected orientation:
+    const firstBlackRook = baseFen.indexOf('r'),
+          lastBlackRook = baseFen.lastIndexOf('r'),
+          firstWhiteRook = baseFen.indexOf('R'),
+          lastWhiteRook = baseFen.lastIndexOf('R');
+    return (
+      baseFen.substring(0, firstBlackRook) + 'e' +
+      baseFen.substring(firstBlackRook + 1, lastBlackRook) + 'm' +
+      baseFen.substring(lastBlackRook + 1, firstWhiteRook) + 'E' +
+      baseFen.substring(firstWhiteRook + 1, lastWhiteRook) + 'M' +
+      baseFen.substring(lastWhiteRook + 1)
+    );
   }
 
   // Because of the lancers, getPiece() could be wrong:
@@ -143,6 +154,65 @@ export class FullcavalryRules extends ChessRules {
     return super.getPotentialMovesFrom([x, y]);
   }
 
+  getPotentialPawnMoves([x, y]) {
+    const color = this.getColor(x, y);
+    let moves = [];
+    const [sizeX, sizeY] = [V.size.x, V.size.y];
+    let shiftX = (color == "w" ? -1 : 1);
+    const startRank = color == "w" ? sizeX - 2 : 1;
+    const lastRank = color == "w" ? 0 : sizeX - 1;
+
+    let finalPieces = [V.PAWN];
+    if (x + shiftX == lastRank) {
+      // Only allow direction facing inside board:
+      const allowedLancerDirs =
+        lastRank == 0
+          ? ['e', 'f', 'g', 'h', 'm']
+          : ['c', 'd', 'e', 'm', 'o'];
+      finalPieces = allowedLancerDirs.concat([V.KNIGHT, V.BISHOP, V.QUEEN]);
+    }
+    if (this.board[x + shiftX][y] == V.EMPTY) {
+      // One square forward
+      for (let piece of finalPieces) {
+        moves.push(
+          this.getBasicMove([x, y], [x + shiftX, y], {
+            c: color,
+            p: piece
+          })
+        );
+      }
+      if (x == startRank && this.board[x + 2 * shiftX][y] == V.EMPTY)
+        // Two squares jump
+        moves.push(this.getBasicMove([x, y], [x + 2 * shiftX, y]));
+    }
+    // Captures
+    for (let shiftY of [-1, 1]) {
+      if (
+        y + shiftY >= 0 &&
+        y + shiftY < sizeY &&
+        this.board[x + shiftX][y + shiftY] != V.EMPTY &&
+        this.canTake([x, y], [x + shiftX, y + shiftY])
+      ) {
+        for (let piece of finalPieces) {
+          moves.push(
+            this.getBasicMove([x, y], [x + shiftX, y + shiftY], {
+              c: color,
+              p: piece
+            })
+          );
+        }
+      }
+    }
+
+    // Add en-passant captures
+    Array.prototype.push.apply(
+      moves,
+      this.getEnpassantCaptures([x, y], shiftX)
+    );
+
+    return moves;
+  }
+
   // Obtain all lancer moves in "step" direction
   getPotentialLancerMoves_aux([x, y], step, tr) {
     let moves = [];
@@ -184,6 +254,100 @@ export class FullcavalryRules extends ChessRules {
     return moves;
   }
 
+  getCastleMoves([x, y]) {
+    const c = this.getColor(x, y);
+
+    // Castling ?
+    const oppCol = V.GetOppCol(c);
+    let moves = [];
+    let i = 0;
+    // King, then lancer:
+    const finalSquares = [ [2, 3], [V.size.y - 2, V.size.y - 3] ];
+    castlingCheck: for (
+      let castleSide = 0;
+      castleSide < 2;
+      castleSide++ //large, then small
+    ) {
+      if (this.castleFlags[c][castleSide] >= V.size.y) continue;
+      // If this code is reached, lancer and king are on initial position
+
+      const lancerPos = this.castleFlags[c][castleSide];
+      const castlingPiece = this.board[x][lancerPos].charAt(1);
+
+      // Nothing on the path of the king ? (and no checks)
+      const finDist = finalSquares[castleSide][0] - y;
+      let step = finDist / Math.max(1, Math.abs(finDist));
+      i = y;
+      do {
+        if (
+          (this.isAttacked([x, i], oppCol)) ||
+          (
+            this.board[x][i] != V.EMPTY &&
+            // NOTE: next check is enough, because of chessboard constraints
+            (this.getColor(x, i) != c || ![y, lancerPos].includes(i))
+          )
+        ) {
+          continue castlingCheck;
+        }
+        i += step;
+      } while (i != finalSquares[castleSide][0]);
+
+      // Nothing on final squares, except maybe king and castling lancer?
+      for (i = 0; i < 2; i++) {
+        if (
+          finalSquares[castleSide][i] != lancerPos &&
+          this.board[x][finalSquares[castleSide][i]] != V.EMPTY &&
+          (
+            finalSquares[castleSide][i] != y ||
+            this.getColor(x, finalSquares[castleSide][i]) != c
+          )
+        ) {
+          continue castlingCheck;
+        }
+      }
+
+      // If this code is reached, castle is valid
+      let allowedLancerDirs = [castlingPiece];
+      if (finalSquares[castleSide][1] != lancerPos) {
+        // It moved: allow reorientation
+        allowedLancerDirs =
+          x == 0
+            ? ['e', 'f', 'g', 'h', 'm']
+            : ['c', 'd', 'e', 'm', 'o'];
+      }
+      allowedLancerDirs.forEach(dir => {
+        moves.push(
+          new Move({
+            appear: [
+              new PiPo({
+                x: x,
+                y: finalSquares[castleSide][0],
+                p: V.KING,
+                c: c
+              }),
+              new PiPo({
+                x: x,
+                y: finalSquares[castleSide][1],
+                p: dir,
+                c: c
+              })
+            ],
+            vanish: [
+              new PiPo({ x: x, y: y, p: V.KING, c: c }),
+              new PiPo({ x: x, y: lancerPos, p: castlingPiece, c: c })
+            ],
+            end:
+              Math.abs(y - lancerPos) <= 2
+                ? { x: x, y: lancerPos }
+                : { x: x, y: y + 2 * (castleSide == 0 ? -1 : 1) }
+          })
+        );
+      });
+    }
+
+    return moves;
+  }
+
   isAttacked(sq, color) {
     return (
       super.isAttacked(sq, color) ||
@@ -254,6 +418,9 @@ export class FullcavalryRules extends ChessRules {
     if (Object.keys(V.LANCER_DIRNAMES).includes(move.vanish[0].p))
       // Lancer: add direction info
       notation += "=" + V.LANCER_DIRNAMES[move.appear[0].p];
+    else if (move.appear.length == 2 && move.vanish[1].p != move.appear[1].p)
+      // Same after castle:
+      notation += "+L:" + V.LANCER_DIRNAMES[move.appear[1].p];
     else if (
       move.vanish[0].p == V.PAWN &&
       Object.keys(V.LANCER_DIRNAMES).includes(move.appear[0].p)
-- 
2.44.0