From: Benjamin Auder <benjamin.auder@somewhere>
Date: Sat, 16 Jan 2021 02:16:51 +0000 (+0100)
Subject: Add Bario draft. Small bugs to fix in Refusal and Bario
X-Git-Url: https://git.auder.net/doc/html/vendor/pieces/%3C?a=commitdiff_plain;h=2a0672a98b555f0fecfd951d583e69419769d411;p=vchess.git

Add Bario draft. Small bugs to fix in Refusal and Bario
---

diff --git a/TODO b/TODO
index 53cace86..7a50f280 100644
--- a/TODO
+++ b/TODO
@@ -2,14 +2,7 @@ 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.
 
-NEW VARIANTS:
-https://www.pychess.org/variant/shogun
-https://www.chessvariants.com/incinf.dir/bario.html
-  https://www.chessvariants.com/index/listcomments.php?order=DESC&itemid=Bario
-  https://www.bario-chess-checkers-chessphotography-spaceart.de/
-  https://le-cdn.website-editor.net/20ef5f800ea646c29f6852cfc5ceda07/dms3rep/multi/opt/BAR028-e15a849c-960w.jpg
-
-Non-chess: ( won't add draughts: https://lidraughts.org/ )
+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
diff --git a/client/public/images/pieces/Bario/bu.svg b/client/public/images/pieces/Bario/bu.svg
new file mode 120000
index 00000000..330db4d0
--- /dev/null
+++ b/client/public/images/pieces/Bario/bu.svg
@@ -0,0 +1 @@
+../Hidden/bp.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Bario/wu.svg b/client/public/images/pieces/Bario/wu.svg
new file mode 120000
index 00000000..f52e53d7
--- /dev/null
+++ b/client/public/images/pieces/Bario/wu.svg
@@ -0,0 +1 @@
+../Hidden/wp.svg
\ No newline at end of file
diff --git a/client/src/base_rules.js b/client/src/base_rules.js
index 0a68127f..4c90f1d8 100644
--- a/client/src/base_rules.js
+++ b/client/src/base_rules.js
@@ -662,7 +662,7 @@ export const ChessRules = class ChessRules {
       case V.QUEEN: return this.getPotentialQueenMoves(sq);
       case V.KING: return this.getPotentialKingMoves(sq);
     }
-    return []; //never reached
+    return []; //never reached (but some variants may use it: Bario...)
   }
 
   // Build a regular move from its initial and destination squares.
diff --git a/client/src/translations/rules/Bario/en.pug b/client/src/translations/rules/Bario/en.pug
index 21203baa..df8d8b60 100644
--- a/client/src/translations/rules/Bario/en.pug
+++ b/client/src/translations/rules/Bario/en.pug
@@ -1 +1,6 @@
 p.boxed TODO
+
+p Some links - rules unwritten yet - variant slightly buggish for now.
+p https://www.chessvariants.com/incinf.dir/bario.html
+p https://le-cdn.website-editor.net/20ef5f800ea646c29f6852cfc5ceda07/dms3rep/multi/opt/BAR028-e15a849c-960w.jpg
+p https://www.bario-chess-checkers-chessphotography-spaceart.de/
diff --git a/client/src/translations/rules/Refusal/en.pug b/client/src/translations/rules/Refusal/en.pug
index 3a33838b..c5ad81a4 100644
--- a/client/src/translations/rules/Refusal/en.pug
+++ b/client/src/translations/rules/Refusal/en.pug
@@ -1,2 +1,23 @@
 p.boxed
-  | TODO
+  | You can forbid the opponent to play the move he wanted.
+
+p.
+  All rules are as in normal chess, except for the following: each turn,
+  a player has the right to refuse at most one move of the opponent.
+  To refuse a move, re-play it from end to starting square.
+
+p.
+  Two moves promoting the same pawn on the same square, but to a
+  different type of piece, count as two different moves.
+
+p If a player has only one legal move, this move must be accepted.
+
+h3 More information
+
+p
+  | See the 
+  a(href="https://www.chessvariants.com/other.dir/refusal.html")
+    | chessvariants page
+  | .
+
+p Inventor: Fred Galvin (1958)
diff --git a/client/src/translations/rules/Refusal/es.pug b/client/src/translations/rules/Refusal/es.pug
index 3a33838b..c2f75fca 100644
--- a/client/src/translations/rules/Refusal/es.pug
+++ b/client/src/translations/rules/Refusal/es.pug
@@ -1,2 +1,23 @@
 p.boxed
-  | TODO
+  | Puedes evitar que el oponente haga cualquier movimiento que quiera.
+
+p.
+  Todo va como en el ajedrez ortodoxo, excepto por un detalle:
+  en cada turno, un jugador tiene derecho a rechazar como máximo una jugada
+  del oponente. Para rechazar un movimiento, vuelva a reproducirlo al revés.
+
+p.
+  Dos movimientos, cada uno promoviendo un peón en la misma casilla,
+  pero en dos piezas diferentes, se ven como jugadas diferentes.
+
+p Si un jugador tiene solo un movimiento legal, debe ser aceptado.
+
+h3 Más información
+
+p
+  | Ver la 
+  a(href="https://www.chessvariants.com/other.dir/refusal.html")
+    | página chessvariants
+  | .
+
+p Inventor: Fred Galvin (1958)
diff --git a/client/src/translations/rules/Refusal/fr.pug b/client/src/translations/rules/Refusal/fr.pug
index 3a33838b..e1b1750e 100644
--- a/client/src/translations/rules/Refusal/fr.pug
+++ b/client/src/translations/rules/Refusal/fr.pug
@@ -1,2 +1,23 @@
 p.boxed
-  | TODO
+  | Vous pouvez empêcher l'adversaire de jouer le coup qu'il souhaite.
+
+p.
+  Tout se déroule comme aux échecs orthodoxes, à l'exception d'un détail :
+  à chaque tour, un joueur a le droit de refuser au plus un coup adverse.
+  Pour refuser un coup, re-jouez le à l'envers.
+
+p.
+  Deux coups promouvant chacun un pion sur la même case, mais en deux pièces
+  différentes, sont vus comme des coups différents.
+
+p Si un joueur n'a qu'un seul coup légal, celui-ci doit être accepté.
+
+h3 Plus d'information
+
+p
+  | Voir la 
+  a(href="https://www.chessvariants.com/other.dir/refusal.html")
+    | page chessvariants
+  | .
+
+p Inventeur : Fred Galvin (1958)
diff --git a/client/src/variants/Bario.js b/client/src/variants/Bario.js
new file mode 100644
index 00000000..5b5b33c3
--- /dev/null
+++ b/client/src/variants/Bario.js
@@ -0,0 +1,544 @@
+import { ChessRules, PiPo, Move } from "@/base_rules";
+import { ArrayFun } from "@/utils/array";
+import { randInt } from "@/utils/alea";
+
+// TODO: issue with undo of specialisation to cover check, subTurn decremented to 0
+
+export class BarioRules extends ChessRules {
+
+  // Does not really seem necessary (although the author mention it)
+  // Instead, first move = pick a square for the king.
+  static get HasCastle() {
+    return false;
+  }
+
+  // Undetermined piece form:
+  static get UNDEFINED() {
+    return 'u';
+  }
+
+  static get PIECES() {
+    return ChessRules.PIECES.concat(V.UNDEFINED);
+  }
+
+  getPpath(b) {
+    if (b[1] == V.UNDEFINED) return "Bario/" + b;
+    return b;
+  }
+
+  canIplay(side, [x, y]) {
+    if (this.movesCount >= 2) return super.canIplay(side, [x, y]);
+    return (
+      this.turn == side &&
+      (
+        (side == 'w' && x == 7) ||
+        (side == 'b' && x == 0)
+      )
+    );
+  }
+
+  hoverHighlight(x, y) {
+    const c = this.turn;
+    return (
+      this.movesCount <= 1 &&
+      (
+        (c == 'w' && x == 7) ||
+        (c == 'b' && x == 0)
+      )
+    );
+  }
+
+  // Initiate the game by choosing a square for the king:
+  doClick(square) {
+    const c = this.turn;
+    if (
+      this.movesCount >= 2 ||
+      (
+        (c == 'w' && square[0] != 7) ||
+        (c == 'b' && square[0] != 0)
+      )
+    ) {
+      return null;
+    }
+    return new Move({
+      appear: [
+        new PiPo({ x: square[0], y: square[1], c: c, p: V.KING })
+      ],
+      vanish: [],
+      start: { x: -1, y: -1 },
+    });
+  }
+
+  // Do not check kings (TODO: something more subtle!)
+  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 (V.PIECES.includes(row[i].toLowerCase())) 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);
+    if (!fenParsed.reserve || !fenParsed.reserve.match(/^[0-9]{8,8}$/))
+    if (!fenParsed.capture) return false;
+    return true;
+  }
+
+  static ParseFen(fen) {
+    const fenParts = fen.split(" ");
+    return Object.assign(
+      {
+        reserve: fenParts[4],
+        capture: fenParts[5]
+      },
+      ChessRules.ParseFen(fen)
+    );
+  }
+
+  getReserveFen() {
+    let counts = new Array(8);
+    for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
+      counts[i] = this.reserve["w"][V.PIECES[i]];
+      counts[4 + i] = this.reserve["b"][V.PIECES[i]];
+    }
+    return counts.join("");
+  }
+
+  getCaptureFen() {
+    const L = this.captureUndefined.length;
+    const cu = this.captureUndefined[L-1];
+    return (!!cu ? V.CoordsToSquare(cu) : "-");
+  }
+
+  getFen() {
+    return (
+      super.getFen() + " " +
+      this.getReserveFen() + " " +
+      this.getCaptureFen()
+    );
+  }
+
+  getFenForRepeat() {
+    return (
+      super.getFenForRepeat() + "_" +
+      this.getReserveFen() + "_" +
+      this.getCaptureFen()
+    );
+  }
+
+  static GenRandInitFen() {
+    return "8/pppppppp/8/8/8/8/PPPPPPPP/8 w 0 - 22212221 -";
+  }
+
+  setOtherVariables(fen) {
+    super.setOtherVariables(fen);
+    const reserve =
+      V.ParseFen(fen).reserve.split("").map(x => parseInt(x, 10));
+    this.reserve = {
+      w: {
+        [V.ROOK]: reserve[0],
+        [V.KNIGHT]: reserve[1],
+        [V.BISHOP]: reserve[2],
+        [V.QUEEN]: reserve[3]
+      },
+      b: {
+        [V.ROOK]: reserve[4],
+        [V.KNIGHT]: reserve[5],
+        [V.BISHOP]: reserve[6],
+        [V.QUEEN]: reserve[7]
+      }
+    };
+    const cu = V.ParseFen(fen).capture;
+    this.captureUndefined = [cu == '-' ? null : V.SquareToCoords(cu)];
+    this.subTurn = (cu == "-" ? 1 : 0);
+    // Local stack of pieces' definitions
+    this.definitions = [];
+  }
+
+  getColor(i, j) {
+    if (i >= V.size.x) return i == V.size.x ? "w" : "b";
+    return this.board[i][j].charAt(0);
+  }
+
+  getPiece(i, j) {
+    if (i >= V.size.x) return V.RESERVE_PIECES[j];
+    return this.board[i][j].charAt(1);
+  }
+
+  getReservePpath(index, color) {
+    return color + V.RESERVE_PIECES[index];
+  }
+
+  static get RESERVE_PIECES() {
+    return [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN];
+  }
+
+  getReserveMoves([x, y]) {
+    const color = this.turn;
+    const p = V.RESERVE_PIECES[y];
+    if (this.reserve[color][p] == 0) return [];
+    // 2 cases, subTurn == 0 => target this.captureUndefined only (one square)
+    if (this.subTurn == 0) {
+      const L = this.captureUndefined.length;
+      const cu = this.captureUndefined[L-1];
+      return (
+        new Move({
+          appear: [
+            new PiPo({ x: cu.x, y: cu.y, c: color, p: p })
+          ],
+          vanish: [
+            new PiPo({ x: cu.x, y: cu.y, c: color, p: V.UNDEFINED })
+          ],
+          start: { x: x, y: y }
+        })
+      );
+    }
+    // or, subTurn == 1 => target any undefined piece that we own.
+    let moves = [];
+    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 &&
+          this.getPiece(i, j) == V.UNDEFINED
+        ) {
+          let mv = new Move({
+            appear: [
+              new PiPo({ x: i, y: j, c: color, p: p })
+            ],
+            vanish: [
+              new PiPo({ x: i, y: j, c: color, p: V.UNDEFINED })
+            ],
+            start: { x: x, y: y },
+            end: { x: i, y: j }
+          });
+          moves.push(mv);
+        }
+      }
+    }
+    return moves;
+  }
+
+  getPotentialMovesFrom([x, y]) {
+    if (this.subTurn == 0) {
+      if (x < V.size.x) return [];
+      return this.getReserveMoves([x, y]);
+    }
+    if (this.subTurn == 1) {
+      // Both normal move (from defined piece) and definition allowed
+      if (x >= V.size.x) return this.getReserveMoves([x, y]);
+      if (this.getPiece(x, y) == V.UNDEFINED) return [];
+    }
+    // subTurn == 1 and we move any piece, or
+    // subTurn == 2 and we can only move the just-defined piece
+    if (this.subTurn == 2) {
+      const L = this.definitions.length; //at least 1
+      const df = this.definitions[L-1];
+      if (x != df.x || y != df.y) return [];
+    }
+    return super.getPotentialMovesFrom([x, y]);
+  }
+
+  getAllValidMoves() {
+    const getAllReserveMoves = () => {
+      let moves = [];
+      const color = this.turn;
+      for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
+        moves = moves.concat(
+          this.getReserveMoves([V.size.x + (color == "w" ? 0 : 1), i])
+        );
+      }
+      return moves;
+    }
+    if (this.subTurn == 0) return getAllReserveMoves();
+    let moves = super.getAllPotentialMoves();
+    if (this.subTurn == 1)
+      moves = moves.concat(getAllReserveMoves());
+    return this.filterValid(moves);
+  }
+
+  filterValid(moves) {
+    const color = this.turn;
+    return moves.filter(m => {
+      if (m.vanish.length == 0) return true;
+      const start = { x: m.vanish[0].x, y: m.vanish[0].y };
+      const end = { x: m.appear[0].x, y: m.appear[0].y };
+      if (start.x == end.x && start.y == end.y) return true; //unfinished turn
+      this.play(m);
+      const res = !this.underCheck(color);
+      this.undo(m);
+      return res;
+    });
+  }
+
+  atLeastOneMove() {
+    const atLeastOneReserveMove = () => {
+      for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
+        let moves = this.filterValid(
+          this.getReserveMoves([V.size.x + (this.turn == "w" ? 0 : 1), i])
+        );
+        if (moves.length > 0) return true;
+      }
+      return false;
+    };
+    if (this.subTurn == 0) return true; //always one reserve for an undefined
+    if (!super.atLeastOneMove()) return atLeastOneReserveMove();
+    return true;
+  }
+
+  underCheck(color) {
+    if (super.underCheck(color)) return true;
+    // Aux func for piece attack on king (no pawn)
+    const pieceAttackOn = (p, [x1, y1], [x2, y2]) => {
+      const shift = [x2 - x1, y2 - y1];
+      const absShift = shift.map(Math.abs);
+      if (
+        (
+          p == V.KNIGHT &&
+          (absShift[0] + absShift[1] != 3 || shift[0] == 0 || shift[1] == 0)
+        ) ||
+        (p == V.ROOK && shift[0] != 0 && shift[1] != 0) ||
+        (p == V.BISHOP && absShift[0] != absShift[1]) ||
+        (
+          p == V.QUEEN &&
+          shift[0] != 0 && shift[1] != 0 && absShift[0] != absShift[1]
+        )
+      ) {
+        return false;
+      }
+      // Step is compatible with piece:
+      const step = [
+        shift[0] / Math.abs(shift[0]) || 0,
+        shift[1] / Math.abs(shift[1]) || 0
+      ];
+      let [i, j] = [x1 + step[0], y1 + step[1]];
+      while (i != x2 || j != y2) {
+        if (this.board[i][j] != V.EMPTY) return false;
+        i += step[0];
+        j += step[1];
+      }
+      return true;
+    };
+    // Check potential specializations of undefined using reserve:
+    const oppCol = V.GetOppCol(color);
+    for (let i=0; i<8; i++) {
+      for (let j=0; j<8; j++) {
+        if (
+          this.board[i][j] != V.EMPTY &&
+          this.getColor(i, j) == oppCol &&
+          this.getPiece(i, j) == V.UNDEFINED
+        ) {
+          for (let p of V.RESERVE_PIECES) {
+            if (
+              this.reserve[oppCol][p] >= 1 &&
+              pieceAttackOn(p, [i, j], this.kingPos[color])
+            ) {
+              return true;
+            }
+          }
+        }
+      }
+    }
+    return false;
+  }
+
+  play(move) {
+    const toNextPlayer = () => {
+      V.PlayOnBoard(this.board, move);
+      this.turn = V.GetOppCol(this.turn);
+      this.subTurn =
+        (move.vanish.length == 2 && move.vanish[1].p == V.UNDEFINED ? 0 : 1);
+      this.movesCount++;
+      this.postPlay(move);
+    };
+    if (move.vanish.length == 0) {
+      toNextPlayer();
+      return;
+    }
+    const start = { x: move.vanish[0].x, y: move.vanish[0].y };
+    const end = { x: move.appear[0].x, y: move.appear[0].y };
+    if (start.x == end.x && start.y == end.y) {
+      // Specialisation (subTurn == 1 before 2), or Removal (subTurn == 0).
+      // In both cases, turn not over, and a piece removed from reserve
+      this.reserve[this.turn][move.appear[0].p]--;
+      if (move.appear[0].c == move.vanish[0].c) {
+        // Specialisation: play "move" on board
+        V.PlayOnBoard(this.board, move);
+        this.definitions.push(move.end);
+      }
+      this.subTurn++;
+    }
+    else {
+      // Normal move (subTurn 1 or 2: change turn)
+      this.epSquares.push(this.getEpSquare(move));
+      toNextPlayer();
+    }
+  }
+
+  postPlay(move) {
+    const color = V.GetOppCol(this.turn);
+    if (move.vanish.length == 0) {
+      this.kingPos[color] = [move.end.x, move.end.y];
+      const firstRank = (color == 'w' ? 7 : 0);
+      for (let j = 0; j < 8; j++) {
+        if (j != move.end.y) this.board[firstRank][j] = color + V.UNDEFINED;
+      }
+    }
+    else {
+      if (move.vanish.length == 2 && move.vanish[1].p == V.UNDEFINED)
+        this.captureUndefined.push(move.end);
+      if (move.appear[0].p == V.KING) super.postPlay(move);
+      else {
+        // If now all my pieces are defined, back to undefined state,
+        // only if at least two different kind of pieces on board!
+        // Store current state in move (cannot infer it after!)
+        if (
+          this.board.every(b => {
+            return b.every(cell => {
+              return (
+                cell == V.EMPTY ||
+                cell[0] != color ||
+                cell[1] != V.UNDEFINED
+              );
+            });
+          })
+        ) {
+          const piecesList = [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN];
+          let myPieces = {};
+          for (let i=0; i<8; i++) {
+            for (let j=0; j<8; j++) {
+              if (
+                this.board[i][j] != V.EMPTY &&
+                this.getColor(i, j) == color
+              ) {
+                const p = this.getPiece(i, j);
+                if (piecesList.includes(p))
+                  myPieces[p] = (!myPieces[p] ? 1 : myPieces[p] + 1);
+              }
+            }
+          }
+          const pk = Object.keys(myPieces);
+          if (pk.length >= 2) {
+            move.position = this.getBaseFen();
+            for (let p of pk) this.reserve[color][p] = myPieces[p];
+            for (let i=0; i<8; i++) {
+              for (let j=0; j<8; j++) {
+                if (
+                  this.board[i][j] != V.EMPTY &&
+                  this.getColor(i, j) == color &&
+                  piecesList.includes(this.getPiece(i, j))
+                ) {
+                  this.board[i][j] = color + V.UNDEFINED;
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+
+  undo(move) {
+    const toPrevPlayer = () => {
+      V.UndoOnBoard(this.board, move);
+      this.turn = V.GetOppCol(this.turn);
+      this.movesCount--;
+      this.postUndo(move);
+    };
+    if (move.vanish.length == 0) {
+      toPrevPlayer();
+      return;
+    }
+    const start = { x: move.vanish[0].x, y: move.vanish[0].y };
+    const end = { x: move.appear[0].x, y: move.appear[0].y };
+    if (start.x == end.x && start.y == end.y) {
+      this.reserve[this.turn][move.appear[0].p]++;
+      if (move.appear[0].c == move.vanish[0].c) {
+        V.UndoOnBoard(this.board, move);
+        this.definitions.pop();
+      }
+      this.subTurn--;
+    }
+    else {
+      this.epSquares.pop();
+      toPrevPlayer();
+    }
+  }
+
+  postUndo(move) {
+    const color = this.turn;
+    if (move.vanish.length == 0) {
+      this.kingPos[color] = [-1, -1];
+      const firstRank = (color == 'w' ? 7 : 0);
+      for (let j = 0; j < 8; j++) this.board[firstRank][j] = "";
+    }
+    else {
+      if (move.vanish.length == 2 && move.vanish[1].p == V.UNDEFINED)
+        this.captureUndefined.pop();
+      if (move.appear[0].p == V.KING) super.postUndo(move);
+      else {
+        if (!!move.position) {
+          this.board = V.GetBoard(move.position);
+          this.reserve[color] = {
+            [V.ROOK]: 0,
+            [V.KNIGHT]: 0,
+            [V.BISHOP]: 0,
+            [V.QUEEN]: 0
+          }
+        }
+      }
+    }
+  }
+
+  getComputerMove() {
+    const color = this.turn;
+    // Just play at random for now...
+    let mvArray = [];
+    while (this.turn == color) {
+      const moves = this.getAllValidMoves();
+      const choice = moves[randInt(moves.length)];
+      mvArray.push(choice);
+      this.play(choice);
+    }
+    for (let i = mvArray.length - 1; i >= 0; i--) this.undo(mvArray[i]);
+    return (mvArray.length == 1? mvArray[0] : mvArray);
+  }
+
+  static get VALUES() {
+    return Object.assign({ u: 0 }, ChessRules.VALUES);
+  }
+
+  // NOTE: evalPosition is wrong, but unused (random mover)
+
+  getNotation(move) {
+    const end = { x: move.appear[0].x, y: move.appear[0].y };
+    const endSquare = V.CoordsToSquare(end);
+    if (move.vanish.length == 0) return "K@" + endSquare;
+    const start = { x: move.vanish[0].x, y: move.vanish[0].y };
+    if (start.x == end.x && start.y == end.y) {
+      // Something is specialized, or removed
+      const symbol = move.appear[0].p.toUpperCase();
+      if (move.appear[0].c == move.vanish[0].c)
+        // Specialisation
+        return symbol + "@" + endSquare;
+      // Removal:
+      return symbol + endSquare + "X";
+    }
+    // Normal move
+    return super.getNotation(move);
+  }
+
+};
diff --git a/client/src/variants/Refusal.js b/client/src/variants/Refusal.js
new file mode 100644
index 00000000..e4ec04d8
--- /dev/null
+++ b/client/src/variants/Refusal.js
@@ -0,0 +1,201 @@
+import { ChessRules } from "@/base_rules";
+import { randInt } from "@/utils/alea";
+
+// TODO: Two moves, both promoting the same pawn, but to a different type of piece, count as two different moves.
+// ==> need to accept temporarily pawn promotions even if on forbidden square, and check afterward if promoted type changed (info in lastMove)
+
+export class RefusalRules extends ChessRules {
+
+  static IsGoodFen(fen) {
+    if (!ChessRules.IsGoodFen(fen)) return false;
+    if (!V.ParseFen(fen).lastMove) return false;
+    return true;
+  }
+
+  static ParseFen(fen) {
+    return Object.assign(
+      { lastMove: fen.split(" ")[5] },
+      ChessRules.ParseFen(fen)
+    );
+  }
+
+  getFen() {
+    const L = this.lastMove.length;
+    const lm = this.lastMove[L-1];
+    return super.getFen() + " " + JSON.stringify(lm);
+  }
+
+  // NOTE: with this variant's special rule,
+  // some extra repetitions could be detected... TODO (...)
+
+  static GenRandInitFen(randomness) {
+    return ChessRules.GenRandInitFen(randomness) + " null";
+  }
+
+  setOtherVariables(fen) {
+    super.setOtherVariables(fen);
+    this.lastMove = [JSON.parse(V.ParseFen(fen).lastMove)]; //may be null
+  }
+
+  canIplay(side, [x, y]) {
+    if (super.canIplay(side, [x, y])) return true;
+    if (this.turn != side) return false;
+    // Check if playing last move, reversed:
+    const L = this.lastMove.length;
+    const lm = this.lastMove[L-1];
+    return (!!lm && !lm.noRef && x == lm.end.x && y == lm.end.y);
+  }
+
+  getPotentialMovesFrom([x, y]) {
+    if (this.getColor(x, y) != this.turn) {
+      const L = this.lastMove.length;
+      const lm = this.lastMove[L-1];
+      if (!!lm && !lm.noRef && x == lm.end.x && y == lm.end.y) {
+        let revLm = JSON.parse(JSON.stringify(lm));
+        let tmp = revLm.appear;
+        revLm.appear = revLm.vanish;
+        revLm.vanish = tmp;
+        tmp = revLm.start;
+        revLm.start = revLm.end;
+        revLm.end = tmp;
+        return [revLm];
+      }
+      return [];
+    }
+    return super.getPotentialMovesFrom([x, y]);
+  }
+
+  // NOTE: do not take refusal move into account here (two own moves)
+  atLeastTwoMoves() {
+    let movesCounter = 0;
+    const color = this.turn;
+    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) {
+          const moves = this.getPotentialMovesFrom([i, j]);
+          for (let m of moves) {
+            if (m.vanish[0].c == color && this.filterValid([m]).length > 0) {
+              movesCounter++;
+              if (movesCounter >= 2) return true;
+            }
+          }
+        }
+      }
+    }
+    return false;
+  }
+
+  filterValid(moves) {
+    if (moves.length == 0) return [];
+    const color = this.turn;
+    const L = this.lastMove.length;
+    const lm = this.lastMove[L-1];
+    return moves.filter(m => {
+      if (
+        !!lm && !!lm.refusal &&
+        m.start.x == lm.end.x && m.start.y == lm.end.y &&
+        m.end.x == lm.start.x && m.end.y == lm.start.y
+      ) {
+        return false;
+      }
+      // NOTE: not using this.play()/undo() ==> infinite loop
+      V.PlayOnBoard(this.board, m);
+      if (m.appear[0].p == V.KING)
+        this.kingPos[m.appear[0].c] = [m.appear[0].x, m.appear[0].y];
+      const res = !this.underCheck(color);
+      V.UndoOnBoard(this.board, m);
+      if (m.vanish[0].p == V.KING)
+        this.kingPos[m.vanish[0].c] = [m.vanish[0].x, m.vanish[0].y];
+      return res;
+    });
+  }
+
+  prePlay(move) {
+    const L = this.lastMove.length;
+    const lm = this.lastMove[L-1];
+    if (
+      // My previous move was already refused?
+      (!!lm && this.getColor(lm.end.x, lm.end.y) == this.turn) ||
+      // I've only one move available?
+      !this.atLeastTwoMoves()
+    ) {
+      move.noRef = true;
+    }
+    // NOTE: refusal could be recomputed, but, it's easier like this
+    if (move.vanish[0].c != this.turn) move.refusal = true;
+  }
+
+  getEpSquare(move) {
+    if (!move.refusal) return super.getEpSquare(move);
+    return null; //move refusal
+  }
+
+  postPlay(move) {
+    if (!move.refusal) super.postPlay(move);
+    else {
+      const L = this.lastMove.length;
+      const lm = this.lastMove[L-1];
+      this.disaggregateFlags(JSON.parse(lm.flags));
+    }
+    // NOTE: explicitely give fields, because some are assigned in BaseGame
+    let mvInLm = {
+      start: move.start,
+      end: move.end,
+      appear: move.appear,
+      vanish: move.vanish,
+      flags: move.flags
+    };
+    if (!!move.noRef) mvInLm.noRef = true;
+    if (!!move.refusal) mvInLm.refusal = true;
+    this.lastMove.push(mvInLm);
+  }
+
+  postUndo(move) {
+    if (!move.refusal) super.postUndo(move);
+    this.lastMove.pop();
+  }
+
+  getAllPotentialMoves() {
+    const color = this.turn;
+    const L = this.lastMove.length;
+    const lm = this.lastMove[L-1];
+    let potentialMoves = [];
+    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 ||
+            // Add move refusal:
+            (!!lm && lm.end.x == i && lm.end.y == j)
+          )
+        ) {
+          Array.prototype.push.apply(
+            potentialMoves,
+            this.getPotentialMovesFrom([i, j])
+          );
+        }
+      }
+    }
+    return potentialMoves;
+  }
+
+  getComputerMove() {
+    // Just play at random for now... (TODO?)
+    // Refuse last move with odds 1/3.
+    const moves = this.getAllValidMoves();
+    const refusal = moves.find(m => m.vanish[0].c != this.turn);
+    if (!!refusal) {
+      if (Math.random() <= 0.33) return refusal;
+      const others = moves.filter(m => m.vanish[0].c == this.turn);
+      return others[randInt(others.length)];
+    }
+    else return moves[randInt(moves.length)];
+  }
+
+  getNotation(move) {
+    if (move.vanish[0].c != this.turn) return "Refuse";
+    return super.getNotation(move);
+  }
+
+};
diff --git a/client/src/variants/Wormhole.js b/client/src/variants/Wormhole.js
index fcf63b4e..05d8f2ae 100644
--- a/client/src/variants/Wormhole.js
+++ b/client/src/variants/Wormhole.js
@@ -57,7 +57,8 @@ export class WormholeRules extends ChessRules {
       // A knight
       shift1 = movement[0];
       shift2 = movement[1];
-    } else {
+    }
+    else {
       shift1 = movement;
       shift2 = null;
     }