From f54357573d4fdf87a05b19f78506c11f16bb3a26 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Tue, 21 Jun 2022 09:45:23 +0200
Subject: [PATCH] Add Refusal

---
 app.js                               |   6 +-
 base_rules.js                        |   6 +-
 variants.js                          |   2 +-
 variants/Absorption/rules.html       |   5 +-
 variants/Atomic/rules.html           |   5 +-
 variants/Balanced/rules.html         |   5 +-
 variants/Chakart/class.js            |  95 ++++++++++----------
 variants/Chakart/complete_rules.html |   1 +
 variants/Chakart/rules.html          |   7 +-
 variants/Chess960/rules.html         |   4 +-
 variants/Cylinder/rules.html         |   5 +-
 variants/Hex/rules.html              |   4 +-
 variants/Progressive/rules.html      |   5 +-
 variants/Refusal/class.js            | 128 +++++++++++++++++++++++++++
 variants/Refusal/rules.html          |   6 ++
 variants/Refusal/style.css           |   1 +
 variants/Suction/class.js            |   2 +-
 variants/Suction/rules.html          |   5 +-
 variants/Zen/rules.html              |   5 +-
 19 files changed, 235 insertions(+), 62 deletions(-)
 create mode 100644 variants/Chakart/complete_rules.html
 create mode 100644 variants/Refusal/class.js
 create mode 100644 variants/Refusal/rules.html
 create mode 100644 variants/Refusal/style.css

diff --git a/app.js b/app.js
index bb16537..7d387c5 100644
--- a/app.js
+++ b/app.js
@@ -191,7 +191,9 @@ function getGameLink() {
   const vname = $.getElementById("selectVariant").value;
   const color = $.getElementById("selectColor").value;
   for (const select of $.querySelectorAll("#gameOptions select")) {
-    const value = parseInt(select.value, 10) || select.value;
+    let value = parseInt(select.value, 10);
+    if (isNaN(value)) //not an integer
+      value = select.value;
     options[ select.id.split("_")[1] ] = value;
   }
   for (const input of $.querySelectorAll("#gameOptions input")) {
@@ -229,7 +231,7 @@ function fillGameInfos(gameInfos, oppIndex) {
           if (j == options.length)
             break;
           const opt = options[j];
-          if (!opt[1])
+          if (!opt[1]) //includes 0 and false (lighter display)
             continue;
           htmlContent +=
             '<span class="option">' +
diff --git a/base_rules.js b/base_rules.js
index 8b07059..1b22ae5 100644
--- a/base_rules.js
+++ b/base_rules.js
@@ -1908,10 +1908,12 @@ export default class ChessRules {
     return [-1, -1]; //king not found
   }
 
-  filterValid(moves) {
+  // Some variants (e.g. Refusal) may need to check opponent moves too
+  filterValid(moves, color) {
     if (moves.length == 0)
       return [];
-    const color = this.turn;
+    if (!color)
+      color = this.turn;
     const oppCol = C.GetOppCol(color);
     if (this.options["balance"] && [1, 3].includes(this.movesCount)) {
       // Forbid moves either giving check or exploding opponent's king:
diff --git a/variants.js b/variants.js
index eb7203e..aafd02a 100644
--- a/variants.js
+++ b/variants.js
@@ -120,7 +120,7 @@ const variants = [
 //  {name: 'Relayup', desc: 'Upgrade pieces', disp: 'Relay-up'},
   {name: 'Rifle', desc: 'Shoot pieces'},
   {name: 'Recycle', desc: 'Reuse pieces'},
-//  {name: 'Refusal', desc: 'Do not play that!'},
+  {name: 'Refusal', desc: 'Do not play that!'},
 //  {name: 'Rollerball', desc: 'As in the movie'},
 //  {name: 'Rococo', desc: 'Capture on the edge'},
 //  {name: 'Royalrace', desc: 'Kings cross the 11x11 board', disp: 'Royal Race'},
diff --git a/variants/Absorption/rules.html b/variants/Absorption/rules.html
index 53d5e45..1149295 100644
--- a/variants/Absorption/rules.html
+++ b/variants/Absorption/rules.html
@@ -1 +1,4 @@
-<p>The capturer absorbs abilities of the captured piece (except for pawns and king).</p>
+<p>
+  The capturer absorbs the abilities of the captured piece
+  (except for pawns and king).
+</p>
diff --git a/variants/Atomic/rules.html b/variants/Atomic/rules.html
index 05829a5..c46cf22 100644
--- a/variants/Atomic/rules.html
+++ b/variants/Atomic/rules.html
@@ -1,3 +1,6 @@
-<p>The capturer explodes after each capture, as well as all pieces standing on the adjacent squares - pawns excepted.</p>
+<p>
+  The capturer explodes after each capture, as well as all pieces standing
+  on the adjacent squares - pawns excepted.
+</p>
 
 <p>Win by checkmate or by exploding the enemy king.</p>
diff --git a/variants/Balanced/rules.html b/variants/Balanced/rules.html
index 6d5fa49..e373943 100644
--- a/variants/Balanced/rules.html
+++ b/variants/Balanced/rules.html
@@ -1,3 +1,6 @@
-<p>White plays first, then Black plays two moves, then White plays two moves, and after that the game proceeds normally.</p>
+<p>
+  White plays first, then Black plays two moves, then White plays two moves,
+  and after that the game proceeds normally.
+</p>
 
 <p>See <a href="https://arxiv.org/abs/2108.02547">the (draft) article</a>.</p>
diff --git a/variants/Chakart/class.js b/variants/Chakart/class.js
index c527b8c..29fdee9 100644
--- a/variants/Chakart/class.js
+++ b/variants/Chakart/class.js
@@ -112,7 +112,22 @@ export default class ChakartRules extends ChessRules {
       't': {"class": ["immobilized", "queen"]},
       'l': {"class": ["immobilized", "king"]}
     };
-    return Object.assign({}, specials, bowsered, super.pieces(color, x, y));
+    return Object.assign(
+      {
+        'y': {
+          // Virtual piece for "king remote shell captures"
+          moves: [],
+          attack: [
+            {
+              steps: [
+                [0, 1], [0, -1], [1, 0], [-1, 0],
+                [1, 1], [1, -1], [-1, 1], [-1, -1]
+              ]
+            }
+          ]
+        }
+      },
+      specials, bowsered, super.pieces(color, x, y));
   }
 
   genRandInitFen(seed) {
@@ -255,7 +270,7 @@ export default class ChakartRules extends ChessRules {
         case 'b':
         case 'r':
           // Explicitely listing types to avoid moving immobilized piece
-          moves = super.getPotentialMovesOf(piece, [x, y]);
+          moves = this.getPotentialMovesOf(piece, [x, y]);
           break;
       }
     }
@@ -295,33 +310,39 @@ export default class ChakartRules extends ChessRules {
       }
     }
     for (let shiftY of [-1, 1]) {
+      const nextY = this.getY(y + shiftY);
       if (
-        y + shiftY >= 0 &&
-        y + shiftY < this.size.y &&
-        this.board[x + shiftX][y + shiftY] != "" &&
+        nextY >= 0 &&
+        nextY < this.size.y &&
+        this.board[x + shiftX][nextY] != "" &&
         // Pawns cannot capture invisible queen this way!
-        this.getPiece(x + shiftX, y + shiftY) != 'i' &&
-        ['a', oppCol].includes(this.getColor(x + shiftX, y + shiftY))
+        this.getPiece(x + shiftX, nextY) != 'i' &&
+        ['a', oppCol].includes(this.getColor(x + shiftX, nextY))
       ) {
-        moves.push(this.getBasicMove([x, y], [x + shiftX, y + shiftY]));
+        moves.push(this.getBasicMove([x, y], [x + shiftX, nextY]));
       }
     }
-    super.pawnPostProcess(moves, color, oppCol);
-    // Add mushroom on before-last square
+    this.pawnPostProcess(moves, color, oppCol);
+    // Add mushroom on before-last square (+ potential segments)
     moves.forEach(m => {
-      let revStep = [m.start.x - m.end.x, m.start.y - m.end.y];
-      for (let i of [0, 1])
-        revStep[i] = revStep[i] / Math.abs(revStep[i]) || 0;
-      const [blx, bly] = [m.end.x + revStep[0], m.end.y + revStep[1]];
-      m.appear.push(new PiPo({x: blx, y: bly, c: 'a', p: 'm'}));
-      if (blx != x && this.board[blx][bly] != "") {
+      let [mx, my] = [x, y];
+      if (Math.abs(m.end.x - m.start.x) == 2)
+        mx = (m.start.x + m.end.x) / 2;
+      m.appear.push(new PiPo({x: mx, y: my, c: 'a', p: 'm'}));
+      if (mx != x && this.board[mx][my] != "") {
         m.vanish.push(new PiPo({
-          x: blx,
-          y: bly,
-          c: this.getColor(blx, bly),
-          p: this.getPiece(blx, bly)
+          x: mx,
+          y: my,
+          c: this.getColor(mx, my),
+          p: this.getPiece(mx, my)
         }));
       }
+      if (Math.abs(m.end.y - m.start.y) > 1) {
+        m.segments = [
+          [[x, y], [x, y]],
+          [[m.end.x, m.end.y], [m.end.x, m.end.y]]
+        ];
+      }
     });
     return moves;
   }
@@ -360,30 +381,15 @@ export default class ChakartRules extends ChessRules {
     let moves = this.getPotentialMovesOf('k', [x, y]);
     // If flag allows it, add 'remote shell captures'
     if (this.powerFlags[this.turn]['k']) {
-      super.pieces()['k'].moves[0].steps.forEach(step => {
-        let [i, j] = [x + step[0], y + step[1]];
-        while (this.onBoard(i, j) && this.canStepOver(i, j)) {
-          i += step[0];
-          j += step[1];
-        }
-        if (this.onBoard(i, j)) {
-          const colIJ = this.getColor(i, j);
-          if (colIJ != this.turn) {
-            // May just destroy a bomb or banana:
-            let shellCapture = new Move({
-              start: {x: x, y: y},
-              end: {x: i, y: j},
-              appear: [],
-              vanish: [
-                new PiPo({x: i, y: j, c: colIJ, p: this.getPiece(i, j)})
-              ]
-            });
-            shellCapture.shell = true; //easier play()
-            shellCapture.choice = 'z'; //to display in showChoices()
-            moves.push(shellCapture);
-          }
-        }
+      let shellCaptures = this.getPotentialMovesOf('y', [x, y]);
+      shellCaptures.forEach(sc => {
+        sc.shell = true; //easier play()
+        sc.choice = 'z'; //to display in showChoices()
+        // Fix move (Rifle style):
+        sc.vanish.shift();
+        sc.appear.shift();
       });
+      Array.prototype.push.apply(moves, shellCaptures);
     }
     return moves;
   }
@@ -619,7 +625,6 @@ export default class ChakartRules extends ChessRules {
             p: this.getPiece(move.start.x, move.start.y)
           }));
         }
-        em.koopa = true; //to cancel mushroom effect
         break;
       case "chomp":
         // Eat piece
@@ -640,7 +645,7 @@ export default class ChakartRules extends ChessRules {
   }
 
   getMushroomEffect(move) {
-    if (move.koopa || typeof move.start.x == "string")
+    if (typeof move.start.x == "string") //drop move (toadette)
       return null;
     let step = [move.end.x - move.start.x, move.end.y - move.start.y];
     if ([0, 1].some(i => Math.abs(step[i]) >= 2 && Math.abs(step[1-i]) != 1)) {
diff --git a/variants/Chakart/complete_rules.html b/variants/Chakart/complete_rules.html
new file mode 100644
index 0000000..c65158e
--- /dev/null
+++ b/variants/Chakart/complete_rules.html
@@ -0,0 +1 @@
+<p>TODO</p>
diff --git a/variants/Chakart/rules.html b/variants/Chakart/rules.html
index f2e5068..e48a4f7 100644
--- a/variants/Chakart/rules.html
+++ b/variants/Chakart/rules.html
@@ -1,4 +1,7 @@
-<p>Pawn, Knight, Bishop and Rook add an object on the board every time they move...</p>
+<p>
+  Pawn, Knight, Bishop and Rook add an object on the board
+  every time they move. King and Queen have special powers.
+</p>
 
 <ul>
   <li>Mushrooms speed-up your pieces.</li>
@@ -7,6 +10,6 @@
   <li>Eggs hide either a bonus or malus: see full description.</li>
 </ul>
 
-<a href="">Full rules description</a>.
+<a href="/variants/Chakart/complete_rules.html">Full rules description</a>.
 
 <p class="author">Charlotte Blard &amp; Benjamin Auder (2020).</p>
diff --git a/variants/Chess960/rules.html b/variants/Chess960/rules.html
index 46751e1..776fa4a 100644
--- a/variants/Chess960/rules.html
+++ b/variants/Chess960/rules.html
@@ -1 +1,3 @@
-<a href="https://en.wikipedia.org/wiki/Rules_of_chess">Orthodox chess rules</a>.
+<a href="https://en.wikipedia.org/wiki/Rules_of_chess">
+  Orthodox chess rules.
+</a>
diff --git a/variants/Cylinder/rules.html b/variants/Cylinder/rules.html
index 6a9db5b..49ea15f 100644
--- a/variants/Cylinder/rules.html
+++ b/variants/Cylinder/rules.html
@@ -1 +1,4 @@
-<p>Columns 'a' and 'h' communicate: a king on h3 can also go to a2, a3 and a4.</p>
+<p>
+  Columns 'a' and 'h' communicate:
+  a king on h3 can also go to a2, a3 and a4.
+</p>
diff --git a/variants/Hex/rules.html b/variants/Hex/rules.html
index 33be5a2..04e700d 100644
--- a/variants/Hex/rules.html
+++ b/variants/Hex/rules.html
@@ -1,5 +1,7 @@
 <p>Win by connecting both edges of your color.</p>
 
-<a href="https://www.maths.ed.ac.uk/~csangwin/hex/index.html">Detailed rules.</a>
+<a href="https://www.maths.ed.ac.uk/~csangwin/hex/index.html">
+  Detailed rules.
+</a>
 
 <p class="author">Piet Hein (1942).</p>
diff --git a/variants/Progressive/rules.html b/variants/Progressive/rules.html
index ee9e9b0..51ec7f2 100644
--- a/variants/Progressive/rules.html
+++ b/variants/Progressive/rules.html
@@ -1 +1,4 @@
-<p>White play one move, then Black play two in a row, then White play 3, and so on.</p>
+<p>
+  White play one move, then Black play two in a row,
+  then White play 3, and so on.
+</p>
diff --git a/variants/Refusal/class.js b/variants/Refusal/class.js
new file mode 100644
index 0000000..2cd9ca6
--- /dev/null
+++ b/variants/Refusal/class.js
@@ -0,0 +1,128 @@
+import ChessRules from "/base_rules.js";
+
+export default class RefusalRules extends ChessRules {
+
+  static get Options() {
+    return {
+      select: C.Options.select,
+      input: [
+        {
+          label: "Refuse any",
+          variable: "refuseany",
+          type: "checkbox",
+          defaut: true
+        }
+      ],
+      styles: ["cylinder"]
+    };
+  }
+
+  get hasFlags() {
+    return false;
+  }
+
+  genRandInitFen(seed) {
+    return super.genRandInitFen(seed).slice(0, -1)  + ',"lastmove":"null"}';
+  }
+
+  getFen() {
+    return (
+      super.getFen().slice(0, -1) + ',"lastmove":"' +
+        JSON.stringify(this.lastMove) + '"}');
+  }
+
+  setOtherVariables(fenParsed) {
+    super.setOtherVariables(fenParsed);
+    this.lastMove = JSON.parse(fenParsed.lastmove);
+    if (!this.lastMove) {
+      // Fill with empty values to avoid checking lastMove != null
+      this.lastMove = {
+        start: {x: -1, y: -1}, end: {x: -1, y: -1}, vanish: [{c: ''}]
+      };
+    }
+  }
+
+  canIplay(x, y) {
+    if (super.canIplay(x, y))
+      return true;
+    // Check if playing last move, reversed:
+    const lm = this.lastMove;
+    return (!lm.noRef && x == lm.end.x && y == lm.end.y);
+  }
+
+  getPotentialMovesFrom([x, y]) {
+    const moveColor = this.getColor(x, y);
+    if (moveColor != this.turn) {
+      let revLm = JSON.parse(JSON.stringify(this.lastMove));
+      [revLm.appear, revLm.vanish] = [revLm.vanish, revLm.appear];
+      [revLm.start, revLm.end] = [revLm.end, revLm.start];
+      if (!this.options["refuseany"]) {
+        // After refusing this move, can my opponent play a different move?
+        this.playOnBoard(revLm);
+        let totOppMoves = 0;
+        outerLoop: for (let i=0; i<this.size.x; i++) {
+          for (let j=0; j<this.size.y; j++) {
+            if (this.getColor(i, j) == moveColor) {
+              const potentialOppMoves = super.getPotentialMovesFrom([i, j]);
+              totOppMoves +=
+                super.filterValid(potentialOppMoves, moveColor).length;
+              if (totOppMoves >= 2)
+                break outerLoop;
+            }
+          }
+        }
+        this.undoOnBoard(revLm);
+        if (totOppMoves <= 1)
+          return [];
+      }
+      // Also reverse segments in Cylinder mode:
+      if (this.options["cylinder"])
+        revLm.segments = revLm.segments.map(seg => [seg[1], seg[0]]);
+      else
+        delete revLm["segments"];
+      revLm.refusal = true;
+      revLm.noRef = true; //cannot refuse a refusal move :)
+      return [revLm];
+    }
+    return super.getPotentialMovesFrom([x, y]);
+  }
+
+  getEpSquare(move) {
+    if (!move.refusal)
+      return super.getEpSquare(move);
+    return null;
+  }
+
+  filterValid(moves) {
+    const color = this.turn;
+    const lm = this.lastMove;
+    let rMoves = moves.filter(m => {
+      return (
+        !lm.refusal || //it's my first move attempt on this turn
+        m.start.x != lm.end.x || m.start.y != lm.end.y ||
+        m.end.x != lm.start.x || m.end.y != lm.start.y ||
+        // Doing the same move again: maybe pawn promotion?
+        (m.vanish[0].p == 'p' && m.appear[0].p != lm.appear[0].p)
+      );
+    });
+    return super.filterValid(rMoves);
+  }
+
+  prePlay(move) {
+    if (!move.noRef)
+      // My previous move was already refused?
+      move.noRef = this.lastMove.vanish[0].c == this.turn;
+  }
+
+  postPlay(move) {
+    this.lastMove = move;
+    super.postPlay(move);
+  }
+
+  atLeastOneMove() {
+    if (!this.lastMove.noRef)
+      return true;
+    return super.atLeastOneMove();
+  }
+
+};
diff --git a/variants/Refusal/rules.html b/variants/Refusal/rules.html
new file mode 100644
index 0000000..8efc3ce
--- /dev/null
+++ b/variants/Refusal/rules.html
@@ -0,0 +1,6 @@
+<p>
+  At each turn, you can refuse one opponent move.
+  Different pawn promotions count as different moves.
+</p>
+
+<p class="author">Fred Galvin (1958).</p>
diff --git a/variants/Refusal/style.css b/variants/Refusal/style.css
new file mode 100644
index 0000000..a3550bc
--- /dev/null
+++ b/variants/Refusal/style.css
@@ -0,0 +1 @@
+@import url("/base_pieces.css");
diff --git a/variants/Suction/class.js b/variants/Suction/class.js
index db10e58..f9cf26d 100644
--- a/variants/Suction/class.js
+++ b/variants/Suction/class.js
@@ -53,7 +53,7 @@ export default class SuctionRules extends ChessRules {
     const cmoveFen = !this.cmove
       ? "-"
       : C.CoordsToSquare(this.cmove.start) + C.CoordsToSquare(this.cmove.end);
-    return super.getFen().slice(0, -1) + ',"' + cmoveFen + '"}';
+    return super.getFen().slice(0, -1) + ',"cmove":"' + cmoveFen + '"}';
   }
 
   getBasicMove([sx, sy], [ex, ey]) {
diff --git a/variants/Suction/rules.html b/variants/Suction/rules.html
index 09a70cb..4006ce0 100644
--- a/variants/Suction/rules.html
+++ b/variants/Suction/rules.html
@@ -1,4 +1,7 @@
-<p>Pieces are swapped after captures. Kings cannot move except by being captured.</p>
+<p>
+  Pieces are swapped after captures.
+  Kings cannot move except by being captured.
+</p>
 
 <p>Win by bringing the enemy king on your first rank.</p>
 
diff --git a/variants/Zen/rules.html b/variants/Zen/rules.html
index 1508496..0386371 100644
--- a/variants/Zen/rules.html
+++ b/variants/Zen/rules.html
@@ -1,4 +1,7 @@
-<p>Pieces capture enemy units which threaten them (normal captures are disabled).</p>
+<p>
+  Pieces capture enemy units which threaten them
+  (normal captures are disabled).
+</p>
 
 <p>Exception: the king is attacked as usual.</p>
 
-- 
2.44.0