From 87f40859ac7fea9f468d6be168df99f501a01198 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Thu, 14 Jan 2021 01:50:33 +0100
Subject: [PATCH] Add Rollerball variant

---
 .gitattributes                                |   1 +
 .../Rollerball/rollerball_directions.gif      |   1 +
 .../variants/Rollerball/rook_example.gif      |   1 +
 client/src/translations/en.js                 |   1 +
 client/src/translations/es.js                 |   1 +
 client/src/translations/fr.js                 |   1 +
 .../src/translations/rules/Rollerball/en.pug  |  48 +-
 .../src/translations/rules/Rollerball/es.pug  |  50 +-
 .../src/translations/rules/Rollerball/fr.pug  |  51 +-
 client/src/translations/variants/en.pug       |   1 +
 client/src/translations/variants/es.pug       |   1 +
 client/src/translations/variants/fr.pug       |   1 +
 client/src/variants/Omega.js                  |   9 +-
 client/src/variants/Rollerball.js             | 570 ++++++++++++++++++
 server/db/populate.sql                        |   1 +
 15 files changed, 729 insertions(+), 9 deletions(-)
 create mode 100644 client/public/variants/Rollerball/rollerball_directions.gif
 create mode 100644 client/public/variants/Rollerball/rook_example.gif
 create mode 100644 client/src/variants/Rollerball.js

diff --git a/.gitattributes b/.gitattributes
index ed553070..ac66ad09 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,4 +1,5 @@
 *.ico filter=fat
 *.pdf filter=fat
 *.png filter=fat
+*.gif filter=fat
 *.flac filter=fat
diff --git a/client/public/variants/Rollerball/rollerball_directions.gif b/client/public/variants/Rollerball/rollerball_directions.gif
new file mode 100644
index 00000000..dff78d67
--- /dev/null
+++ b/client/public/variants/Rollerball/rollerball_directions.gif
@@ -0,0 +1 @@
+#$# git-fat 0086b7f63ad2afec7e27c717ac39ae2c14b707bd                 6736
diff --git a/client/public/variants/Rollerball/rook_example.gif b/client/public/variants/Rollerball/rook_example.gif
new file mode 100644
index 00000000..4a219aa8
--- /dev/null
+++ b/client/public/variants/Rollerball/rook_example.gif
@@ -0,0 +1 @@
+#$# git-fat 0971d21d6aa9f36da7f3442ef28b0b9596939588                 5546
diff --git a/client/src/translations/en.js b/client/src/translations/en.js
index ef2ba14e..6d79e55a 100644
--- a/client/src/translations/en.js
+++ b/client/src/translations/en.js
@@ -171,6 +171,7 @@ export const translations = {
   "Absorb powers": "Absorb powers",
   "All of the same color": "All of the same color",
   "Ancient rules": "Ancient rules",
+  "As in the movie": "As in the movie",
   "Attract opposite king": "Attract opposite king",
   "Augmented Queens": "Augmented Queens",
   "Balanced sliders & leapers": "Balanced sliders & leapers",
diff --git a/client/src/translations/es.js b/client/src/translations/es.js
index 6aa5c10c..a50a9791 100644
--- a/client/src/translations/es.js
+++ b/client/src/translations/es.js
@@ -171,6 +171,7 @@ export const translations = {
   "Absorb powers": "Absorber poderes",
   "All of the same color": "Todo el mismo color",
   "Ancient rules": "Viejas reglas",
+  "As in the movie": "Como en la pelicula",
   "Attract opposite king": "Atraer al rey contrario",
   "Augmented Queens": "Damas aumentadas",
   "Balanced sliders & leapers": "Modos de desplazamiento equilibrados",
diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js
index a79d18cb..925f1412 100644
--- a/client/src/translations/fr.js
+++ b/client/src/translations/fr.js
@@ -171,6 +171,7 @@ export const translations = {
   "Absorb powers": "Absorber les pouvoirs",
   "All of the same color": "Tout de la même couleur",
   "Ancient rules": "Règles anciennes",
+  "As in the movie": "Comme dans le film",
   "Attract opposite king": "Attirer le roi adverse",
   "Augmented Queens": "Dames augmentées",
   "Balanced sliders & leapers": "Modes de déplacement équilibrés",
diff --git a/client/src/translations/rules/Rollerball/en.pug b/client/src/translations/rules/Rollerball/en.pug
index 21203baa..686cc7a6 100644
--- a/client/src/translations/rules/Rollerball/en.pug
+++ b/client/src/translations/rules/Rollerball/en.pug
@@ -1 +1,47 @@
-p.boxed TODO
+p.boxed
+  | Pieces turn around the board, they cannot go in the middle.
+
+p.
+  Each side has only a king, two rooks, one bishop and two pawns.
+  Pieces can never enter the central area.
+  Pawns can only move "clockwise", acording to the image below: they move
+  and capture forward straight and diagonally in the arrow direction.
+
+figure
+  img.img-center(src="/variants/Rollerball/rollerball_directions.gif")
+  figcaption.text-center.
+    Main directions on the board.
+    The squares where direction changes are enlighted with white arrows.
+
+p.
+  When a pawn reaches the starting square of an enemy pawn, it promotes
+  into a bishop or rook.
+
+p.
+  Rooks and bishop move as usual, but are restricted to one square only when
+  moving counter-clockwise.
+  Moreover, they are allowed one rebound in certain circumstances:
+ul
+  li a bishop rebounds on the first wall met,
+  li a rook rebounds (at 90 degrees) if it reaches a corner.
+
+figure
+  img.img-center(src="/variants/Rollerball/rook_example.gif")
+  figcaption.text-center Some rook movements.
+
+p.
+  The king moves as in orthodox chess.
+  The goal is either to bring your king on the initial square of the
+  opponent's king, or to checkmate him.
+
+h3 More information
+
+p
+  | See the 
+  a(href="http://history.chess.free.fr/rollerball.htm") author's presentation
+  | , and the 
+  a(href="https://www.chessvariants.com/40.dir/rollerball/index.html")
+    | chessvariants page
+  | .
+
+p Inventor: Jean-Louis Cazaux (1998)
diff --git a/client/src/translations/rules/Rollerball/es.pug b/client/src/translations/rules/Rollerball/es.pug
index 21203baa..9a8230a6 100644
--- a/client/src/translations/rules/Rollerball/es.pug
+++ b/client/src/translations/rules/Rollerball/es.pug
@@ -1 +1,49 @@
-p.boxed TODO
+p.boxed
+  | Las piezas giran alrededor del tablero, no pueden ir al centro.
+
+p.
+  Cada lado tiene un rey, dos torres, un alfil y dos peones.
+  Las piezas nunca pueden ir al área central.
+  Los peones solo se mueven siguiendo las "agujas del reloj", según la imagen
+  abajo: se mueven y capturan en línea recta o en diagonal
+  en la dirección de la flecha.
+
+figure
+  img.img-center(src="/variants/Rollerball/rollerball_directions.gif")
+  figcaption.text-center.
+    Direcciones principales en el tablero. Las casillas donde ocurre
+    los cambios de dirección están marcados con flechas blancas.
+
+p.
+  Cuando un peón llega a la casilla inicial de un peón enemigo, es promovido
+  en un alfil o en una torre.
+
+p.
+  Las torres y los alfiles se mueven como de costumbre, pero están
+  restringidos por una casilla solo cuando se mueven en sentido antihorario.
+  Además, pueden rebotar una vez en determinadas circunstancias:
+ul
+  li un loco rebota en la primera pared que encuentra,
+  li una torre rebota (90 grados) si golpea una esquina.
+
+figure
+  img.img-center(src="/variants/Rollerball/rook_example.gif")
+  figcaption.text-center Algunos movimientos de la torre.
+
+p.
+  El rey se mueve como en el ajedrez ortodoxo.
+  El objetivo es llevar a tu rey a la casilla de inicio del rey contrario,
+  o matarlo.
+
+h3 Más información
+
+p
+  | Ver la 
+  a(href="http://history.chess.free.fr/rollerball.htm")
+    | presentación del autor
+  | , y la 
+  a(href="https://www.chessvariants.com/40.dir/rollerball/index.html")
+    | página chessvariants
+  | .
+
+p Inventor: Jean-Louis Cazaux (1998)
diff --git a/client/src/translations/rules/Rollerball/fr.pug b/client/src/translations/rules/Rollerball/fr.pug
index 21203baa..d3080c3e 100644
--- a/client/src/translations/rules/Rollerball/fr.pug
+++ b/client/src/translations/rules/Rollerball/fr.pug
@@ -1 +1,50 @@
-p.boxed TODO
+p.boxed
+  | Les pièces tournent autour de l'échiquier,
+  | elles ne peuvent pas aller au centre.
+
+p.
+  Chaque camp dispose d'un roi, deux tours, un fou et deux pions.
+  Les pièces ne peuvent jamais aller dans la zone centrale.
+  Les pions ne se déplacent que dans le "sens horaire", selon l'image
+  ci-dessous : ils se déplacent et capturent tout droit ou en diagonale
+  dans la direction de la flèche.
+
+figure
+  img.img-center(src="/variants/Rollerball/rollerball_directions.gif")
+  figcaption.text-center.
+    Principales directions sur l'échiquier. Les cases où survient
+    un changement de direction sont marquées par des flèches blanches.
+
+p.
+  Quand un pion atteint la case de départ d'un pion ennemi, il est promu
+  en un fou ou une tour.
+
+p.
+  Les tours et fous se déplacent comme d'habitude, mais sont restreints d'une
+  case seulement quand ils se déplacent dans le sens anti-horaire.
+  De plus, ils peuvent rebondir une fois dans certaines circonstances :
+ul
+  li un fou rebondit sur le premier mur rencontré,
+  li une tour rebondit (à 90 degrés) si elle atteint un coin.
+
+figure
+  img.img-center(src="/variants/Rollerball/rook_example.gif")
+  figcaption.text-center Quelques déplacements de la tour.
+
+p.
+  Le roi se déplace comme aux échecs orthodoxes.
+  L'objectif est soit d'amener votre roi sur la case de départ du roi adverse,
+  ou bien de le mater.
+
+h3 Plus d'information
+
+p
+  | Voir la 
+  a(href="http://history.chess.free.fr/rollerball.htm")
+    | présentation de l'auteur
+  | , et la 
+  a(href="https://www.chessvariants.com/40.dir/rollerball/index.html")
+    | page chessvariants
+  | .
+
+p Inventeur : Jean-Louis Cazaux (1998)
diff --git a/client/src/translations/variants/en.pug b/client/src/translations/variants/en.pug
index ee21f973..66c14938 100644
--- a/client/src/translations/variants/en.pug
+++ b/client/src/translations/variants/en.pug
@@ -448,6 +448,7 @@ p.
     "Kingsmaker",
     "Magnetic",
     "Relayup",
+    "Rollerball",
     "Takenmake",
     "Wormhole"
   ]
diff --git a/client/src/translations/variants/es.pug b/client/src/translations/variants/es.pug
index 38743601..39eda808 100644
--- a/client/src/translations/variants/es.pug
+++ b/client/src/translations/variants/es.pug
@@ -458,6 +458,7 @@ p.
     "Kingsmaker",
     "Magnetic",
     "Relayup",
+    "Rollerball",
     "Takenmake",
     "Wormhole"
   ]
diff --git a/client/src/translations/variants/fr.pug b/client/src/translations/variants/fr.pug
index 07756b8b..9cf10ed9 100644
--- a/client/src/translations/variants/fr.pug
+++ b/client/src/translations/variants/fr.pug
@@ -456,6 +456,7 @@ p.
     "Kingsmaker",
     "Magnetic",
     "Relayup",
+    "Rollerball",
     "Takenmake",
     "Wormhole"
   ]
diff --git a/client/src/variants/Omega.js b/client/src/variants/Omega.js
index bac8d50d..a4276ed7 100644
--- a/client/src/variants/Omega.js
+++ b/client/src/variants/Omega.js
@@ -278,12 +278,9 @@ export class OmegaRules extends ChessRules {
 
   getPotentialMovesFrom([x, y]) {
     switch (this.getPiece(x, y)) {
-      case V.CHAMPION:
-        return this.getPotentialChampionMoves([x, y]);
-      case V.WIZARD:
-        return this.getPotentialWizardMoves([x, y]);
-      default:
-        return super.getPotentialMovesFrom([x, y]);
+      case V.CHAMPION: return this.getPotentialChampionMoves([x, y]);
+      case V.WIZARD: return this.getPotentialWizardMoves([x, y]);
+      default: return super.getPotentialMovesFrom([x, y]);
     }
   }
 
diff --git a/client/src/variants/Rollerball.js b/client/src/variants/Rollerball.js
new file mode 100644
index 00000000..36373b29
--- /dev/null
+++ b/client/src/variants/Rollerball.js
@@ -0,0 +1,570 @@
+import { ChessRules } from "@/base_rules";
+
+export class RollerballRules extends ChessRules {
+
+  static get HasEnpassant() {
+    return false;
+  }
+
+  static get HasCastle() {
+    return false;
+  }
+
+  static get DarkBottomRight() {
+    return true;
+  }
+
+  static get PIECES() {
+    return [V.PAWN, V.KING, V.ROOK, V.BISHOP];
+  }
+
+  static get size() {
+    return { x: 7, y: 7 };
+  }
+
+  // TODO: the wall position should be checked too
+  static IsGoodPosition(position) {
+    if (position.length == 0) return false;
+    const rows = position.split("/");
+    if (rows.length != V.size.x) return false;
+    let kings = { "k": 0, "K": 0 };
+    for (let row of rows) {
+      let sumElts = 0;
+      for (let i = 0; i < row.length; i++) {
+        if (['K','k'].includes(row[i])) kings[row[i]]++;
+        if (['x'].concat(V.PIECES).includes(row[i].toLowerCase())) sumElts++;
+        else {
+          const num = parseInt(row[i], 10);
+          if (isNaN(num)) return false;
+          sumElts += num;
+        }
+      }
+      if (sumElts != V.size.y) return false;
+    }
+    if (Object.values(kings).some(v => v != 1)) return false;
+    return true;
+  }
+
+  // NOTE: canTake() is wrong, but next method is enough
+  static OnBoard(x, y) {
+    return (
+      (x >= 0 && x <= 6 && y >= 0 && y <= 6) &&
+      (![2, 3, 4].includes(x) || ![2, 3, 4].includes(y))
+    );
+  }
+
+  static IsGoodFlags(flags) {
+    // 2 for kings: last zone reached
+    return !!flags.match(/^[0-7]{2,2}$/);
+  }
+
+  setFlags(fenflags) {
+    this.kingFlags = {
+      w: parseInt(fenflags.charAt(0), 10),
+      b: parseInt(fenflags.charAt(1), 10)
+    };
+  }
+
+  aggregateFlags() {
+    return this.kingFlags;
+  }
+
+  disaggregateFlags(flags) {
+    this.kingFlags = flags;
+  }
+
+  getFlagsFen() {
+    return this.kingFlags['w'].toString() + this.kingFlags['b'].toString();
+  }
+
+  // For space in the middle:
+  static get NOTHING() {
+    return "xx";
+  }
+
+  static board2fen(b) {
+    if (b[0] == 'x') return 'x';
+    return ChessRules.board2fen(b);
+  }
+
+  static fen2board(f) {
+    if (f == 'x') return V.NOTHING;
+    return ChessRules.fen2board(f);
+  }
+
+  getPpath(b) {
+    if (b[0] == 'x') return "Omega/nothing";
+    return b;
+  }
+
+  static GenRandInitFen() {
+    return "2rbp2/2rkp2/2xxx2/2xxx2/2xxx2/2PKR2/2PBR2 w 0 00";
+  }
+
+  getPotentialMovesFrom(sq) {
+    switch (this.getPiece(sq[0], sq[1])) {
+      case V.PAWN: return this.getPotentialPawnMoves(sq);
+      case V.ROOK: return this.getPotentialRookMoves(sq);
+      case V.BISHOP: return this.getPotentialBishopMoves(sq);
+      case V.KING: return super.getPotentialKingMoves(sq);
+    }
+    return [];
+  }
+
+  getPotentialPawnMoves([x, y]) {
+    const c = this.turn;
+    // Need to know pawn area to deduce move options
+    const inMiddleX = [2, 3, 4].includes(x);
+    const inMiddleY = [2, 3, 4].includes(y);
+    // In rectangular areas on the sides?
+    if (inMiddleX) {
+      const forward = (y <= 1 ? -1 : 1);
+      return (
+        super.getSlideNJumpMoves(
+          [x, y], [[forward, -1], [forward, 0], [forward, 1]], "oneStep")
+      );
+    }
+    if (inMiddleY) {
+      const forward = (x <= 1 ? 1 : -1);
+      let moves =
+        super.getSlideNJumpMoves(
+          [x, y], [[-1, forward], [0, forward], [1, forward]], "oneStep");
+      // Promotions may happen:
+      let extraMoves = [];
+      moves.forEach(m => {
+        if (
+          (c == 'w' && x <= 1 && m.end.y == 4) ||
+          (c == 'b' && x >= 5 && m.end.y == 2)
+        ) {
+          m.appear[0].p = V.ROOK;
+          let m2 = JSON.parse(JSON.stringify(m));
+          m2.appear[0].p = V.BISHOP;
+          extraMoves.push(m2);
+        }
+      });
+      Array.prototype.push.apply(moves, extraMoves);
+      return moves;
+    }
+    // In a corner:
+    const toRight = (x == 0 && [0, 1, 5].includes(y)) || (x == 1 && y == 1);
+    const toLeft = (x == 6 && [1, 5, 6].includes(y)) || (x == 5 && y == 5);
+    const toUp = (y == 0 && [1, 5, 6].includes(x)) || (x == 5 && y == 1);
+    const toBottom = (y == 6 && [0, 1, 5].includes(x)) || (x == 1 && y == 5);
+    if (toRight || toLeft) {
+      const forward = (toRight ? 1 : -1);
+      return (
+        super.getSlideNJumpMoves(
+          [x, y], [[-1, forward], [0, forward], [1, forward]], "oneStep")
+      );
+    }
+    const forward = (toUp ? -1 : 1);
+    return (
+      super.getSlideNJumpMoves(
+        [x, y], [[forward, -1], [forward, 0], [forward, 1]], "oneStep")
+    );
+  }
+
+  getPotentialRookMoves([x, y]) {
+    let multiStep = [],
+        oneStep = [];
+    if (x <= 1) multiStep.push([0, 1]);
+    else oneStep.push([0, 1]);
+    if (y <= 1) multiStep.push([-1, 0]);
+    else oneStep.push([-1, 0]);
+    if (x >= 5) multiStep.push([0, -1]);
+    else oneStep.push([0, -1]);
+    if (y >= 5) multiStep.push([1, 0]);
+    else oneStep.push([1, 0]);
+    const c = this.turn;
+    let moves = super.getSlideNJumpMoves([x, y], oneStep, "oneStep");
+    for (let step of multiStep) {
+      let [i, j] = [x + step[0], y + step[1]];
+      while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
+        moves.push(this.getBasicMove([x, y], [i, j]));
+        i += step[0];
+        j += step[1];
+      }
+      if (V.OnBoard(i, j)) {
+        if (this.getColor(i, j) != c)
+          moves.push(this.getBasicMove([x, y], [i, j]));
+      }
+      else {
+        i -= step[0];
+        j -= step[1];
+        // Potential rebound if away from initial square
+        if (i != x || j != y) {
+          // Corners check
+          let nextStep = null;
+          if (i == 0 && j == 0) nextStep = [0, 1];
+          else if (i == 0 && j == 6) nextStep = [1, 0];
+          else if (i == 6 && j == 6) nextStep = [0, -1];
+          else if (i == 6 && j == 0) nextStep = [-1, 0];
+          if (!!nextStep) {
+            i += nextStep[0];
+            j += nextStep[1];
+            while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
+              moves.push(this.getBasicMove([x, y], [i, j]));
+              i += nextStep[0];
+              j += nextStep[1];
+            }
+            if (V.OnBoard(i, j) && this.getColor(i, j) != c)
+              moves.push(this.getBasicMove([x, y], [i, j]));
+          }
+        }
+      }
+    }
+    return moves;
+  }
+
+  static get DictBishopSteps() {
+    return {
+      "-1_-1": [-1, -1],
+      "-1_1": [-1, 1],
+      "1_-1": [1, -1],
+      "1_1": [1, 1]
+    };
+  }
+
+  getPotentialBishopMoves([x, y]) {
+    let multiStep = {};
+    if (x <= 1) {
+      multiStep["-1_1"] = [-1, 1];
+      multiStep["1_1"] = [1, 1];
+    }
+    if (y <= 1) {
+      multiStep["-1_-1"] = [-1, -1];
+      if (!multiStep["-1_1"]) multiStep["-1_1"] = [-1, 1];
+    }
+    if (x >= 5) {
+      multiStep["1_-1"] = [1, -1];
+      if (!multiStep["-1_-1"]) multiStep["-1_-1"] = [-1, -1];
+    }
+    if (y >= 5) {
+      if (!multiStep["1_-1"]) multiStep["1_-1"] = [1, -1];
+      if (!multiStep["1_1"]) multiStep["1_1"] = [1, 1];
+    }
+    let oneStep = [];
+    Object.keys(V.DictBishopSteps).forEach(str => {
+      if (!multiStep[str]) oneStep.push(V.DictBishopSteps[str]);
+    });
+    const c = this.turn;
+    let moves = super.getSlideNJumpMoves([x, y], oneStep, "oneStep");
+    for (let step of Object.values(multiStep)) {
+      let [i, j] = [x + step[0], y + step[1]];
+      while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
+        moves.push(this.getBasicMove([x, y], [i, j]));
+        i += step[0];
+        j += step[1];
+      }
+      if (V.OnBoard(i, j)) {
+        if (this.getColor(i, j) != c)
+          moves.push(this.getBasicMove([x, y], [i, j]));
+      }
+      else {
+        i -= step[0];
+        j -= step[1];
+        // Rebound, if we moved away from initial square
+        if (i != x || j != y) {
+          let nextStep = null;
+          if (step[0] == -1 && step[1] == -1) {
+            if (j == 0) nextStep = [-1, 1];
+            else nextStep = [1, -1];
+          }
+          else if (step[0] == -1 && step[1] == 1) {
+            if (i == 0) nextStep = [1, 1];
+            else nextStep = [-1, -1];
+          }
+          else if (step[0] == 1 && step[1] == -1) {
+            if (i == 6) nextStep = [-1, -1];
+            else nextStep = [1, 1];
+          }
+          else {
+            // step == [1, 1]
+            if (j == 6) nextStep = [1, -1];
+            else nextStep = [-1, 1];
+          }
+          i += nextStep[0];
+          j += nextStep[1];
+          while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
+            moves.push(this.getBasicMove([x, y], [i, j]));
+            i += nextStep[0];
+            j += nextStep[1];
+          }
+          if (V.OnBoard(i, j) && this.getColor(i, j) != c)
+            moves.push(this.getBasicMove([x, y], [i, j]));
+        }
+      }
+    }
+    return moves;
+  }
+
+  isAttacked(sq, color) {
+    return (
+      super.isAttackedByKing(sq, color) ||
+      this.isAttackedByRook(sq, color) ||
+      this.isAttackedByBishop(sq, color) ||
+      this.isAttackedByPawn(sq, color)
+    );
+  }
+
+  isAttackedByPawn([x, y], color) {
+    // Determine zone, shifted according to pawn movement
+    let attackDir = "";
+    let forward = 0;
+    if (
+      ([1, 2, 3, 4].includes(x) && y <= 1) ||
+      (x == 5 && y == 0)
+    ) {
+      attackDir = "vertical";
+      forward = 1;
+    }
+    else if (
+      ([2, 3, 4, 5].includes(x) && [5, 6].includes(y)) ||
+      (x == 1 && y == 6)
+    ) {
+      attackDir = "vertical";
+      forward = -1;
+    }
+    else if (
+      (x <= 1 && [2, 3, 4, 5].includes(y)) ||
+      (x == 0 && y == 1)
+    ) {
+      attackDir = "horizontal";
+      forward = -1;
+    }
+    else if (
+      (x >= 5 && [1, 2, 3, 4].includes(y)) ||
+      (x == 6 && y == 5)
+    ) {
+      attackDir = "horizontal";
+      forward = 1;
+    }
+    if (forward != 0) {
+      const steps =
+        attackDir == "vertical"
+          ? [ [forward, -1], [forward, 0], [forward, 1] ]
+          : [ [-1, forward], [0, forward], [1, forward] ];
+      return (
+        super.isAttackedBySlideNJump([x, y], color, V.PAWN, steps, "oneStep")
+      );
+    }
+    // In a corner: can be attacked by one square only
+    let step = null;
+    if (x == 0) {
+      if (y == 0) step = [1, 0];
+      else step = [0, -1];
+    }
+    else {
+      if (y == 0) step = [0, 1];
+      else step = [-1, 0];
+    }
+    return (
+      super.isAttackedBySlideNJump([x, y], color, V.PAWN, [step], "oneStep")
+    );
+  }
+
+  isAttackedByRook([x, y], color) {
+    // "Reversing" the code of getPotentialRookMoves()
+    let multiStep = [],
+        oneStep = [];
+    if (x <= 1) multiStep.push([0, -1]);
+    else oneStep.push([0, -1]);
+    if (y <= 1) multiStep.push([1, 0]);
+    else oneStep.push([1, 0]);
+    if (x >= 5) multiStep.push([0, 1]);
+    else oneStep.push([0, 1]);
+    if (y >= 5) multiStep.push([-1, 0]);
+    else oneStep.push([-1, 0]);
+    if (
+      super.isAttackedBySlideNJump([x, y], color, V.ROOK, oneStep, "oneStep")
+    ) {
+      return true;
+    }
+    for (let step of multiStep) {
+      let [i, j] = [x + step[0], y + step[1]];
+      while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
+        i += step[0];
+        j += step[1];
+      }
+      if (V.OnBoard(i, j)) {
+        if (this.getColor(i, j) == color && this.getPiece(i, j) == V.ROOK)
+          return true;
+      }
+      else {
+        i -= step[0];
+        j -= step[1];
+        if (i != x || j != y) {
+          let nextStep = null;
+          if (i == 0 && j == 0) nextStep = [1, 0];
+          else if (i == 0 && j == 6) nextStep = [0, -1];
+          else if (i == 6 && j == 6) nextStep = [-1, 0];
+          else if (i == 6 && j == 0) nextStep = [0, 1];
+          if (!!nextStep) {
+            i += nextStep[0];
+            j += nextStep[1];
+            while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
+              i += nextStep[0];
+              j += nextStep[1];
+            }
+            if (
+              V.OnBoard(i, j) &&
+              this.getColor(i, j) == color &&
+              this.getPiece(i, j) == V.ROOK
+            ) {
+              return true;
+            }
+          }
+        }
+      }
+    }
+    return false;
+  }
+
+  isAttackedByBishop([x, y], color) {
+    // "Reversing" the code of getPotentiaBishopMoves()
+    let multiStep = {};
+    if (x <= 1) {
+      multiStep["1_-1"] = [1, -1];
+      multiStep["-1_-1"] = [-1, -1];
+    }
+    if (y <= 1) {
+      multiStep["1_1"] = [1, 1];
+      if (!multiStep["1_-1"]) multiStep["1_-1"] = [1, -1];
+    }
+    if (x >= 5) {
+      multiStep["-1_1"] = [-1, 1];
+      if (!multiStep["1_1"]) multiStep["1_1"] = [1, 1];
+    }
+    if (y >= 5) {
+      if (!multiStep["-1_-1"]) multiStep["-1_-1"] = [-1, -1];
+      if (!multiStep["-1_1"]) multiStep["-1_1"] = [-1, 1];
+    }
+    let oneStep = [];
+    Object.keys(V.DictBishopSteps).forEach(str => {
+      if (!multiStep[str]) oneStep.push(V.DictBishopSteps[str]);
+    });
+    if (
+      super.isAttackedBySlideNJump([x, y], color, V.BISHOP, oneStep, "oneStep")
+    ) {
+      return true;
+    }
+    for (let step of Object.values(multiStep)) {
+      let [i, j] = [x + step[0], y + step[1]];
+      while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
+        i += step[0];
+        j += step[1];
+      }
+      if (V.OnBoard(i, j)) {
+        if (this.getColor(i, j) == color && this.getPiece(i, j) == V.BISHOP)
+          return true;
+      }
+      else {
+        i -= step[0];
+        j -= step[1];
+        if (i != x || j != y) {
+          let nextStep = null;
+          if (step[0] == -1 && step[1] == -1) {
+            if (j == 0) nextStep = [-1, 1];
+            else nextStep = [1, -1];
+          }
+          else if (step[0] == -1 && step[1] == 1) {
+            if (i == 0) nextStep = [1, 1];
+            else nextStep = [-1, -1];
+          }
+          else if (step[0] == 1 && step[1] == -1) {
+            if (i == 6) nextStep = [-1, -1];
+            else nextStep = [1, 1];
+          }
+          else {
+            // step == [1, 1]
+            if (j == 6) nextStep = [1, -1];
+            else nextStep = [-1, 1];
+          }
+          i += nextStep[0];
+          j += nextStep[1];
+          while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
+            i += nextStep[0];
+            j += nextStep[1];
+          }
+          if (
+            V.OnBoard(i, j) &&
+            this.getColor(i, j) == color &&
+            this.getPiece(i, j) == V.BISHOP
+          ) {
+            return true;
+          }
+        }
+      }
+    }
+    return false;
+  }
+
+  // The board is divided in areas determined by "distance to target"
+  // A zone n+1 must be reached from a zone n.
+  getKingZone([x, y], color) {
+    if (color == 'w') {
+      if (y >= 4) return -1; //"out of zone"
+      if (y == 3 && [5, 6].includes(x)) return 0;
+      if (x == 6) return 1;
+      if (x == 5) return 2;
+      if (x == 4) return 3;
+      if (x == 3 || y == 0) return 4;
+      if (y == 1) return 5;
+      if (x == 0 || y == 2) return 6;
+      return 7; //x == 1 && y == 3
+    }
+    // color == 'b':
+    if (y <= 2) return -1; //"out of zone"
+    if (y == 3 && [0, 1].includes(x)) return 0;
+    if (x == 0) return 1;
+    if (x == 1) return 2;
+    if (x == 2) return 3;
+    if (x == 3 || y == 6) return 4;
+    if (y == 5) return 5;
+    if (x == 6 || y == 4) return 6;
+    return 7; //x == 5 && y == 3
+  }
+
+  postPlay(move) {
+    super.postPlay(move);
+    if (move.vanish[0].p == V.KING) {
+      const c = move.vanish[0].c;
+      const z1 = this.getKingZone([move.vanish[0].x, move.vanish[0].y], c),
+            z2 = this.getKingZone([move.appear[0].x, move.appear[0].y], c);
+      if (
+        z1 >= 0 && z2 >= 0 && z1 < z2 &&
+        // There exist "zone jumps" (0 to 2 for example),
+        // so the following test "flag >= z1" is required.
+        this.kingFlags[c] >= z1 && this.kingFlags[c] < z2
+      ) {
+        this.kingFlags[c] = z2;
+      }
+    }
+  }
+
+  getCurrentScore() {
+    const oppCol = V.GetOppCol(this.turn);
+    if (this.kingFlags[oppCol] == 7) return (oppCol == 'w' ? "1-0" : "0-1");
+    return super.getCurrentScore();
+  }
+
+  static get SEARCH_DEPTH() {
+    return 4;
+  }
+
+  evalPosition() {
+    let evaluation = 0;
+    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) {
+          const sign = this.getColor(i, j) == "w" ? 1 : -1;
+          const piece = this.getPiece(i, j);
+          if (piece != 'x') evaluation += sign * V.VALUES[piece];
+        }
+      }
+    }
+    // Taking flags into account in a rather naive way
+    return evaluation + this.kingFlags['w'] - this.kingFlags['b'];
+  }
+
+};
diff --git a/server/db/populate.sql b/server/db/populate.sql
index dd36fc9c..13f2e8a7 100644
--- a/server/db/populate.sql
+++ b/server/db/populate.sql
@@ -119,6 +119,7 @@ insert or ignore into Variants (name, description) values
   ('Relayup', 'Upgrade pieces'),
   ('Rifle', 'Shoot pieces'),
   ('Recycle', 'Reuse pieces'),
+  ('Rollerball', 'As in the movie'),
   ('Rococo', 'Capture on the edge'),
   ('Rookpawns', 'Rook versus pawns'),
   ('Royalrace', 'Kings cross the 11x11 board'),
-- 
2.44.0