From e3e2cc443054cfb273b28b3ba46f559117c5ceae Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Mon, 24 Feb 2020 18:53:52 +0100
Subject: [PATCH] Draft Circular Chess

---
 client/src/base_rules.js                      |   3 +-
 client/src/translations/en.js                 |   1 +
 client/src/translations/es.js                 |   1 +
 client/src/translations/fr.js                 |   1 +
 client/src/translations/rules/Circular/en.pug |  26 +++
 client/src/translations/rules/Circular/es.pug |  23 +++
 client/src/translations/rules/Circular/fr.pug |  23 +++
 client/src/variants/Circular.js               | 195 ++++++++++++++++++
 server/db/populate.sql                        |   1 +
 9 files changed, 273 insertions(+), 1 deletion(-)
 create mode 100644 client/src/translations/rules/Circular/en.pug
 create mode 100644 client/src/translations/rules/Circular/es.pug
 create mode 100644 client/src/translations/rules/Circular/fr.pug
 create mode 100644 client/src/variants/Circular.js

diff --git a/client/src/base_rules.js b/client/src/base_rules.js
index da9fab78..4b9a139f 100644
--- a/client/src/base_rules.js
+++ b/client/src/base_rules.js
@@ -274,12 +274,13 @@ export const ChessRules = class ChessRules {
       pieces[c][knight2Pos] = "n";
       pieces[c][rook2Pos] = "r";
     }
+    // Add turn + flags + enpassant
     return (
       pieces["b"].join("") +
       "/pppppppp/8/8/8/8/PPPPPPPP/" +
       pieces["w"].join("").toUpperCase() +
       " w 0 1111 -"
-    ); //add turn + flags + enpassant
+    );
   }
 
   // "Parse" FEN: just return untransformed string data
diff --git a/client/src/translations/en.js b/client/src/translations/en.js
index 920e19cb..09fa92fe 100644
--- a/client/src/translations/en.js
+++ b/client/src/translations/en.js
@@ -147,6 +147,7 @@ export const translations = {
   "Pawns move diagonally": "Pawns move diagonally",
   "Reuse pieces": "Reuse pieces",
   "Reverse captures": "Reverse captures",
+  "Run forward": "Run forward",
   "Shared pieces": "Shared pieces",
   "Standard rules": "Standard rules",
   "Unidentified pieces": "Unidentified pieces"
diff --git a/client/src/translations/es.js b/client/src/translations/es.js
index d3ead1a0..286ae428 100644
--- a/client/src/translations/es.js
+++ b/client/src/translations/es.js
@@ -147,6 +147,7 @@ export const translations = {
   "Pawns move diagonally": "Peones se mueven en diagonal",
   "Reuse pieces": "Reutilizar piezas",
   "Reverse captures": "Capturas invertidas",
+  "Run forward": "Correr hacia adelante",
   "Shared pieces": "Piezas compartidas",
   "Standard rules": "Reglas estandar",
   "Unidentified pieces": "Piezas no identificadas"
diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js
index 5abf762c..6746e708 100644
--- a/client/src/translations/fr.js
+++ b/client/src/translations/fr.js
@@ -147,6 +147,7 @@ export const translations = {
   "Pawns move diagonally": "Les pions vont en diagonale",
   "Reuse pieces": "Réutiliser les pièces",
   "Reverse captures": "Captures inversées",
+  "Run forward": "Courir vers l'avant",
   "Shared pieces": "Pièces partagées",
   "Standard rules": "Règles usuelles",
   "Unidentified pieces": "Pièces non identifiées"
diff --git a/client/src/translations/rules/Circular/en.pug b/client/src/translations/rules/Circular/en.pug
new file mode 100644
index 00000000..7e78b2f8
--- /dev/null
+++ b/client/src/translations/rules/Circular/en.pug
@@ -0,0 +1,26 @@
+p https://www.chessvariants.com/d.betza/chessvar/race.html 8x8 race chess
+
+p See also: https://www.chessvariants.com/shape.dir/x_torus.html
+
+p.boxed
+  | If a piece captures one of the same kind, both disappear.
+
+p.
+  The defensive power of pawns is thus increased, because they don't fear
+  captures (by other pawns).
+
+p.
+  Endings are also affected quite a lot, and sometimes new threats occur:
+  on the diagram, 3.Bxg7 wins a pawn because 3...Bxg7 would make both
+  bishops disappear.
+
+figure.diagram-container
+  .diagram
+    | fen:r1bqkbnr/pp1ppppp/2n5/2p5/8/1P6/PBPPPPPP/RN1QKBNR:
+  figcaption After 1.b3 c5 2.Bb2 Nc6
+
+h3 Source
+
+p
+  a(href="https://www.chessvariants.com/rules/antimatter-chess") Antimatter chess
+  | &nbsp;on chessvariants.com.
diff --git a/client/src/translations/rules/Circular/es.pug b/client/src/translations/rules/Circular/es.pug
new file mode 100644
index 00000000..b6adc808
--- /dev/null
+++ b/client/src/translations/rules/Circular/es.pug
@@ -0,0 +1,23 @@
+p.boxed
+  | Si una pieza captura otra del mismo tipo, las dos desaparecen.
+
+p.
+  El poder defensivo de los peones aumenta así, ya que no temen
+  más capturas (por otros peones).
+
+p.
+  Las finales también se ven muy afectadas y, a veces, nuevas amenazas
+  ocurren: en el diagrama, 3.Bxg7 gana un peón porque 3...Bxg7 causaría
+  la desaparición de los dos alfiles.
+
+figure.diagram-container
+  .diagram
+    | fen:r1bqkbnr/pp1ppppp/2n5/2p5/8/1P6/PBPPPPPP/RN1QKBNR:
+  figcaption Después de 1.b3 c5 2.Bb2 Nc6
+
+h3 Fuente
+
+p
+  | La 
+  a(href="https://www.chessvariants.com/rules/antimatter-chess") variante Antimateria
+  | &nbsp;en chessvariants.com.
diff --git a/client/src/translations/rules/Circular/fr.pug b/client/src/translations/rules/Circular/fr.pug
new file mode 100644
index 00000000..7b1cdfae
--- /dev/null
+++ b/client/src/translations/rules/Circular/fr.pug
@@ -0,0 +1,23 @@
+p.boxed
+  | Si une pièce en capture une autre du même type, les deux disparaissent.
+
+p.
+  Le pouvoir défensif des pions est ainsi augmenté, puisqu'ils ne craignent
+  plus les captures (par d'autres pions).
+
+p.
+  Les finales sont aussi beaucoup affectées, et parfois de nouvelles menaces
+  surviennent : sur le diagramme, 3.Bxg7 gagne un pion car 3...Bxg7 provoquerait
+  la disparition des deux fous.
+
+figure.diagram-container
+  .diagram
+    | fen:r1bqkbnr/pp1ppppp/2n5/2p5/8/1P6/PBPPPPPP/RN1QKBNR:
+  figcaption After 1.b3 c5 2.Bb2 Nc6
+
+h3 Source
+
+p
+  | La 
+  a(href="https://www.chessvariants.com/rules/antimatter-chess") variante Antimatière
+  | &nbsp;sur chessvariants.com.
diff --git a/client/src/variants/Circular.js b/client/src/variants/Circular.js
new file mode 100644
index 00000000..a5a5d644
--- /dev/null
+++ b/client/src/variants/Circular.js
@@ -0,0 +1,195 @@
+import { ChessRules, PiPo, Move } from "@/base_rules";
+import { ArrayFun } from "@/utils/array";
+import { randInt, shuffle } from "@/utils/alea";
+
+export const VariantRules = class CircularRules extends ChessRules {
+  static get HasFlags() {
+    return false;
+  }
+
+  static get HasEnpassant() {
+    return false;
+  }
+
+  // TODO: CanFlip --> also for racing kings (answer is false)
+
+  // TODO: shuffle on 1st and 5th ranks
+  static GenRandInitFen() {
+    let pieces = { w: new Array(8), b: new Array(8) };
+    // Shuffle pieces on first and last rank
+    for (let c of ["w", "b"]) {
+      let positions = ArrayFun.range(8);
+
+      // Get random squares for bishops
+      let randIndex = 2 * randInt(4);
+      const bishop1Pos = positions[randIndex];
+      // The second bishop must be on a square of different color
+      let randIndex_tmp = 2 * randInt(4) + 1;
+      const bishop2Pos = positions[randIndex_tmp];
+      // Remove chosen squares
+      positions.splice(Math.max(randIndex, randIndex_tmp), 1);
+      positions.splice(Math.min(randIndex, randIndex_tmp), 1);
+
+      // Get random squares for knights
+      randIndex = randInt(6);
+      const knight1Pos = positions[randIndex];
+      positions.splice(randIndex, 1);
+      randIndex = randInt(5);
+      const knight2Pos = positions[randIndex];
+      positions.splice(randIndex, 1);
+
+      // Get random square for queen
+      randIndex = randInt(4);
+      const queenPos = positions[randIndex];
+      positions.splice(randIndex, 1);
+
+      // Rooks and king positions are now fixed,
+      // because of the ordering rook-king-rook
+      const rook1Pos = positions[0];
+      const kingPos = positions[1];
+      const rook2Pos = positions[2];
+
+      // Finally put the shuffled pieces in the board array
+      pieces[c][rook1Pos] = "r";
+      pieces[c][knight1Pos] = "n";
+      pieces[c][bishop1Pos] = "b";
+      pieces[c][queenPos] = "q";
+      pieces[c][kingPos] = "k";
+      pieces[c][bishop2Pos] = "b";
+      pieces[c][knight2Pos] = "n";
+      pieces[c][rook2Pos] = "r";
+    }
+    return (
+      pieces["b"].join("") +
+      "/pppppppp/8/8/8/8/PPPPPPPP/" +
+      pieces["w"].join("").toUpperCase() +
+      " w 0"
+    );
+  }
+
+  // TODO: adapt this for a circular board
+  getSlideNJumpMoves([x, y], steps, oneStep) {
+    let moves = [];
+    outerLoop: for (let step of steps) {
+      let i = x + step[0];
+      let j = y + step[1];
+      while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
+        moves.push(this.getBasicMove([x, y], [i, j]));
+        if (oneStep !== undefined) continue outerLoop;
+        i += step[0];
+        j += step[1];
+      }
+      if (V.OnBoard(i, j) && this.canTake([x, y], [i, j]))
+        moves.push(this.getBasicMove([x, y], [i, j]));
+    }
+    return moves;
+  }
+
+  // TODO: adapt: all pawns go in thz same direction!
+  getPotentialPawnMoves([x, y]) {
+    const color = this.turn;
+    let moves = [];
+    const [sizeX, sizeY] = [V.size.x, V.size.y];
+    const shiftX = color == "w" ? -1 : 1;
+    const firstRank = color == "w" ? sizeX - 1 : 0;
+    const startRank = color == "w" ? sizeX - 2 : 1;
+    const lastRank = color == "w" ? 0 : sizeX - 1;
+    const pawnColor = this.getColor(x, y); //can be different for checkered
+
+    // NOTE: next condition is generally true (no pawn on last rank)
+    if (x + shiftX >= 0 && x + shiftX < sizeX) {
+      const finalPieces =
+        x + shiftX == lastRank
+          ? [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN]
+          : [V.PAWN];
+      // One square forward
+      if (this.board[x + shiftX][y] == V.EMPTY) {
+        for (let piece of finalPieces) {
+          moves.push(
+            this.getBasicMove([x, y], [x + shiftX, y], {
+              c: pawnColor,
+              p: piece
+            })
+          );
+        }
+        // Next condition because pawns on 1st rank can generally jump
+        if (
+          [startRank, firstRank].includes(x) &&
+          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: pawnColor,
+                p: piece
+              })
+            );
+          }
+        }
+      }
+    }
+
+    return moves;
+  }
+
+  // What are the king moves from square x,y ?
+  getPotentialKingMoves(sq) {
+    return this.getSlideNJumpMoves(
+      sq,
+      V.steps[V.ROOK].concat(V.steps[V.BISHOP]),
+      "oneStep"
+    );
+  }
+
+  // TODO: check boundaries here as well
+  isAttackedByPawn([x, y], colors) {
+    for (let c of colors) {
+      let pawnShift = c == "w" ? 1 : -1;
+      if (x + pawnShift >= 0 && x + pawnShift < V.size.x) {
+        for (let i of [-1, 1]) {
+          if (
+            y + i >= 0 &&
+            y + i < V.size.y &&
+            this.getPiece(x + pawnShift, y + i) == V.PAWN &&
+            this.getColor(x + pawnShift, y + i) == c
+          ) {
+            return true;
+          }
+        }
+      }
+    }
+    return false;
+  }
+
+  // TODO: adapt this function
+  isAttackedBySlideNJump([x, y], colors, piece, steps, oneStep) {
+    for (let step of steps) {
+      let rx = x + step[0],
+          ry = y + step[1];
+      while (V.OnBoard(rx, ry) && this.board[rx][ry] == V.EMPTY && !oneStep) {
+        rx += step[0];
+        ry += step[1];
+      }
+      if (
+        V.OnBoard(rx, ry) &&
+        this.getPiece(rx, ry) === piece &&
+        colors.includes(this.getColor(rx, ry))
+      ) {
+        return true;
+      }
+    }
+    return false;
+  }
+};
diff --git a/server/db/populate.sql b/server/db/populate.sql
index 384b7356..a86a8740 100644
--- a/server/db/populate.sql
+++ b/server/db/populate.sql
@@ -11,6 +11,7 @@ insert or ignore into Variants (name,description) values
   ('Berolina', 'Pawns move diagonally'),
   ('Checkered', 'Shared pieces'),
   ('Chess960', 'Standard rules'),
+  ('Circular', 'Run forward'),
   ('Crazyhouse', 'Captures reborn'),
   ('Dark', 'In the shadow'),
   ('Enpassant', 'Capture en passant'),
-- 
2.44.0