From 65203419968be4b63acf55172fe4b28658b96f38 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Sat, 11 Mar 2023 09:32:05 +0100
Subject: [PATCH] Draft Circular chess. Move animation not per segments for now

---
 base_rules.js                | 46 +++++++++++------
 variants.js                  |  2 +-
 variants/Circular/class.js   | 99 ++++++++++++++++++++++++++++++++++++
 variants/Circular/rules.html |  4 ++
 variants/Circular/style.css  |  1 +
 5 files changed, 135 insertions(+), 17 deletions(-)
 create mode 100644 variants/Circular/class.js
 create mode 100644 variants/Circular/rules.html
 create mode 100644 variants/Circular/style.css

diff --git a/base_rules.js b/base_rules.js
index e8cf4f4..98c6bd4 100644
--- a/base_rules.js
+++ b/base_rules.js
@@ -122,6 +122,11 @@ export default class ChessRules {
     return false;
   }
 
+  // Some variants do not flip board as black
+  get flippedBoard() {
+    return (this.playerColor == 'b');
+  }
+
   // Some variants use click infos:
   doClick(coords) {
     if (typeof coords.x != "number")
@@ -566,7 +571,7 @@ export default class ChessRules {
 
   // Get SVG board (background, no pieces)
   getSvgChessboard() {
-    const flipped = (this.playerColor == 'b');
+    const flipped = this.flippedBoard;
     let board = `
       <svg
         viewBox="0 0 ${10*this.size.y} ${10*this.size.x}"
@@ -830,7 +835,7 @@ export default class ChessRules {
     }
     else {
       const sqSize = r.width / this.size.y;
-      const flipped = (this.playerColor == 'b');
+      const flipped = this.flippedBoard;
       x = (flipped ? this.size.y - 1 - j : j) * sqSize;
       y = (flipped ? this.size.x - 1 - i : i) * sqSize;
     }
@@ -1067,7 +1072,7 @@ export default class ChessRules {
     // TODO: (0, 0) is wrong, would need to place an attacker here...
     const steps = this.pieces(this.playerColor, 0, 0)["p"].attack[0].steps;
     for (let step of steps) {
-      const x = this.epSquare.x - step[0],
+      const x = this.epSquare.x - step[0], //NOTE: epSquare.x not on edge
             y = this.getY(this.epSquare.y - step[1]);
       if (
         this.onBoard(x, y) &&
@@ -1178,18 +1183,19 @@ export default class ChessRules {
   getPawnShift(color) {
     return (color == "w" ? -1 : 1);
   }
+  isPawnInitRank(x, color) {
+    return (color == 'w' && x >= 6) || (color == 'b' && x <= 1);
+  }
 
   pieces(color, x, y) {
     const pawnShift = this.getPawnShift(color);
-    // NOTE: jump 2 squares from first rank (pawns can be here sometimes)
-    const initRank = ((color == 'w' && x >= 6) || (color == 'b' && x <= 1));
     return {
       'p': {
         "class": "pawn",
         moves: [
           {
             steps: [[pawnShift, 0]],
-            range: (initRank ? 2 : 1)
+            range: (this.isPawnInitRank(x, color) ? 2 : 1)
           }
         ],
         attack: [
@@ -1290,6 +1296,17 @@ export default class ChessRules {
       res += this.size.y;
     return res;
   }
+  // Circular?
+  getX(x) {
+    return x; //generally, no
+  }
+
+  increment([x, y], step) {
+    return [
+      this.getX(x + step[0]),
+      this.getY(y + step[1])
+    ];
+  }
 
   getSegments(curSeg, segStart, segEnd) {
     if (curSeg.length == 0)
@@ -1355,13 +1372,12 @@ export default class ChessRules {
     const attacks = stepSpec.both.concat(stepSpec.attack);
     for (let a of attacks) {
       outerLoop: for (let step of a.steps) {
-        let [i, j] = [x + step[0], y + step[1]];
-        let stepCounter = 1;
+        let [i, j] = this.increment([x, y], step);
+        let stepCounter = 0;
         while (this.onBoard(i, j) && this.board[i][j] == "") {
           if (a.range <= stepCounter++)
             continue outerLoop;
-          i += step[0];
-          j = this.getY(j + step[1]);
+          [i, j] = this.increment([i, j], step);
         }
         if (
           this.onBoard(i, j) &&
@@ -1570,8 +1586,7 @@ export default class ChessRules {
           vanish: []
         });
         for (let step of steps) {
-          let x = m.end.x + step[0];
-          let y = this.getY(m.end.y + step[1]);
+          let [x, y] = this.increment([m.end.x, m.end.y], step);
           if (
             this.onBoard(x, y) &&
             this.board[x][y] != "" &&
@@ -1659,7 +1674,7 @@ export default class ChessRules {
         m.appear[0].p = this.pawnPromotions[0];
         for (let i=1; i<this.pawnPromotions.length; i++) {
           let newMv = JSON.parse(JSON.stringify(m));
-          newMv.appear[0].p = this.pawnSpecs.promotions[i];
+          newMv.appear[0].p = this.pawnPromotions[i];
           newMoves.push(newMv);
         }
       }
@@ -1785,8 +1800,7 @@ export default class ChessRules {
             if (s.range <= stepCounter++)
               continue outerLoop;
             const oldIJ = [i, j];
-            i += step[0];
-            j = this.getY(j + step[1]);
+            [i, j] = this.increment([i, j], step);
             if (o.segments && Math.abs(j - oldIJ[1]) > 1) {
               // Boundary between segments (cylinder mode)
               segments.push([[segStart[0], segStart[1]], oldIJ]);
@@ -1990,7 +2004,7 @@ export default class ChessRules {
     const oppCols = this.getOppCols(color);
     if (
       this.epSquare &&
-      this.epSquare.x == x + shiftX &&
+      this.epSquare.x == x + shiftX && //NOTE: epSquare.x not on edge
       Math.abs(this.getY(this.epSquare.y - y)) == 1 &&
       // Doublemove (and Progressive?) guards:
       this.board[this.epSquare.x][this.epSquare.y] == "" &&
diff --git a/variants.js b/variants.js
index 8353853..c20f800 100644
--- a/variants.js
+++ b/variants.js
@@ -29,7 +29,7 @@ const variants = [
   {name: 'Checkered', desc: 'Shared pieces'},
   {name: 'Checkless', desc: 'No-check mode'},
   {name: 'Chess960', disp: "Chess 960", desc: "Standard rules"},
-//  {name: 'Circular', desc: 'Run forward'},
+  {name: 'Circular', desc: 'Run forward'},
 //  {name: 'Clorange', desc: 'A Clockwork Orange', disp: 'Clockwork Orange'},
 //  {name: 'Convert', desc: 'Convert enemy pieces'},
 //  {name: 'Copycat', desc: 'Borrow powers'},
diff --git a/variants/Circular/class.js b/variants/Circular/class.js
new file mode 100644
index 0000000..24aa98e
--- /dev/null
+++ b/variants/Circular/class.js
@@ -0,0 +1,99 @@
+import ChessRules from "/base_rules.js";
+import {FenUtil} from "/utils/setupPieces.js";
+
+export default class CircularRules extends ChessRules {
+
+  get hasCastle() {
+    return false;
+  }
+  get hasEnpassant() {
+    return false;
+  }
+
+  // Everypawn is going up!
+  getPawnShift(color) {
+    return -1;
+  }
+  isPawnInitRank(x, color) {
+    return (color == 'w' && x == 6) || (color == 'b' && x == 2);
+  }
+
+  get flippedBoard() {
+    return false;
+  }
+
+  // Circular board:
+  // TODO: graph with segments, as for cylindrical...
+  getX(x) {
+    let res = x % this.size.x;
+    if (res < 0)
+      res += this.size.x;
+    return res;
+  }
+
+  // TODO: rewrite in more elegant way
+  getFlagsFen() {
+    let flags = "";
+    for (let c of ["w", "b"]) {
+      for (let i = 0; i < 8; i++)
+        flags += this.pawnFlags[c][i] ? "1" : "0";
+    }
+    return flags;
+  }
+
+  setFlags(fenflags) {
+    this.pawnFlags = {
+      w: [...Array(8)], //pawns can move 2 squares?
+      b: [...Array(8)]
+    };
+    for (let c of ["w", "b"]) {
+      for (let i = 0; i < 8; i++)
+        this.pawnFlags[c][i] = fenflags.charAt((c == "w" ? 0 : 8) + i) == "1";
+    }
+  }
+
+  genRandInitBaseFen() {
+    let setupOpts = {
+      randomness: this.options["randomness"],
+      diffCol: ['b']
+    };
+    const s = FenUtil.setupPieces(
+      ['r', 'n', 'b', 'q', 'k', 'b', 'n', 'r'], setupOpts);
+    return {
+      fen: "8/8/pppppppp/" + s.b.join("") + "/8/8/PPPPPPPP/" +
+           s.w.join("").toUpperCase(),
+      o: {flags: "11111111"}
+    };
+  }
+
+  filterValid(moves) {
+    const filteredMoves = super.filterValid(moves);
+    // If at least one full move made, everything is allowed:
+    if (this.movesCount >= 2)
+      return filteredMoves;
+    // Else, forbid checks:
+    const oppCol = C.GetOppTurn(this.turn);
+    const oppKingPos = this.searchKingPos(oppCol);
+    return filteredMoves.filter(m => {
+      this.playOnBoard(m);
+      const res = !this.underCheck(oppKingPos, [this.turn]);
+      this.undoOnBoard(m);
+      return res;
+    });
+  }
+
+  prePlay(move) {
+    if (move.appear.length > 0 && move.vanish.length > 0) {
+      super.prePlay(move);
+      if (
+        [2, 6].includes(move.start.x) &&
+        move.vanish[0].p == 'p' &&
+        Math.abs(move.end.x - move.start.x) == 2
+      ) {
+        // This move turns off a 2-squares pawn flag
+        this.pawnFlags[move.start.x == 6 ? "w" : "b"][move.start.y] = false;
+      }
+    }
+  }
+
+};
diff --git a/variants/Circular/rules.html b/variants/Circular/rules.html
new file mode 100644
index 0000000..6db9dd8
--- /dev/null
+++ b/variants/Circular/rules.html
@@ -0,0 +1,4 @@
+<p>
+Lower and upper sides of the board communicate.<br>
+  All pawns move forward (up).
+</p>
diff --git a/variants/Circular/style.css b/variants/Circular/style.css
new file mode 100644
index 0000000..290a6f4
--- /dev/null
+++ b/variants/Circular/style.css
@@ -0,0 +1 @@
+@import url("/base_pieces.css")
-- 
2.44.0