From 2fac4d67083700a1f1e85ed8662c176c24cdea6b Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Mon, 11 Jan 2021 00:56:08 +0100
Subject: [PATCH] Add Hypnotic + Mesmer variants

---
 TODO                                          |   2 -
 client/public/images/pieces/Mesmer/bm.svg     |   1 +
 client/public/images/pieces/Mesmer/wm.svg     |   1 +
 client/src/base_rules.js                      |   1 -
 client/src/translations/en.js                 |   2 +
 client/src/translations/es.js                 |   2 +
 client/src/translations/fr.js                 |   2 +
 client/src/translations/rules/Hypnotic/en.pug |  29 +-
 client/src/translations/rules/Hypnotic/es.pug |  30 +-
 client/src/translations/rules/Hypnotic/fr.pug |  30 +-
 client/src/translations/rules/Mesmer/en.pug   |  32 ++-
 client/src/translations/rules/Mesmer/es.pug   |  32 ++-
 client/src/translations/rules/Mesmer/fr.pug   |  32 ++-
 client/src/translations/variants/en.pug       |   2 +
 client/src/translations/variants/es.pug       |   2 +
 client/src/translations/variants/fr.pug       |   2 +
 client/src/variants/Hypnotic.js               | 181 ++++++++++++
 client/src/variants/Mesmer.js                 | 262 ++++++++++++++++++
 server/db/populate.sql                        |   2 +
 19 files changed, 638 insertions(+), 9 deletions(-)
 create mode 120000 client/public/images/pieces/Mesmer/bm.svg
 create mode 120000 client/public/images/pieces/Mesmer/wm.svg
 create mode 100644 client/src/variants/Hypnotic.js
 create mode 100644 client/src/variants/Mesmer.js

diff --git a/TODO b/TODO
index a6d77bf4..4707a60c 100644
--- a/TODO
+++ b/TODO
@@ -3,8 +3,6 @@ Embedded rules language not updated when language is set (in Analyse, Game and P
 If new live game starts in background, "new game" notify OK but not first move.
 
 NEW VARIANTS:
-https://www.chessvariants.com/mvopponent.dir/hypnotic-chess.html
-https://www.chessvariants.com/mvopponent.dir/mesmer-chess.html
 https://www.reddit.com/r/TotemChess/comments/imi3v7/totem_rules/
 --> replaced by "Joker" chess --> start on bishop of our color (instead),
     moves like a Bishop + Dabbabah, can swap with a piece *on same color* (as a move) at anytime. hmm
diff --git a/client/public/images/pieces/Mesmer/bm.svg b/client/public/images/pieces/Mesmer/bm.svg
new file mode 120000
index 00000000..000f539e
--- /dev/null
+++ b/client/public/images/pieces/Mesmer/bm.svg
@@ -0,0 +1 @@
+../Maxima/bg.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Mesmer/wm.svg b/client/public/images/pieces/Mesmer/wm.svg
new file mode 120000
index 00000000..436f84d6
--- /dev/null
+++ b/client/public/images/pieces/Mesmer/wm.svg
@@ -0,0 +1 @@
+../Maxima/wg.svg
\ No newline at end of file
diff --git a/client/src/base_rules.js b/client/src/base_rules.js
index f1049b05..c849626d 100644
--- a/client/src/base_rules.js
+++ b/client/src/base_rules.js
@@ -163,7 +163,6 @@ export const ChessRules = class ChessRules {
 
   // Check if FEN describes a board situation correctly
   static IsGoodFen(fen) {
-console.log("ddd");
     const fenParsed = V.ParseFen(fen);
     // 1) Check position
     if (!V.IsGoodPosition(fenParsed.position)) return false;
diff --git a/client/src/translations/en.js b/client/src/translations/en.js
index 41683bd1..10ac4f86 100644
--- a/client/src/translations/en.js
+++ b/client/src/translations/en.js
@@ -241,6 +241,8 @@ export const translations = {
   "Mate the knight": "Mate the knight",
   "Meet the Mammoth": "Meet the Mammoth",
   "Middle battle": "Middle battle",
+  "Mind control (v1)": "Mind control (v1)",
+  "Mind control (v2)": "Mind control (v2)",
   "Mongolian Horde (v1)": "Mongolian Horde (v1)",
   "Mongolian Horde (v2)": "Mongolian Horde (v2)",
   "Move like a knight (v1)": "Move like a knight (v1)",
diff --git a/client/src/translations/es.js b/client/src/translations/es.js
index 14d67eba..7ae7098b 100644
--- a/client/src/translations/es.js
+++ b/client/src/translations/es.js
@@ -241,6 +241,8 @@ export const translations = {
   "Mate the knight": "Matar el caballo",
   "Meet the Mammoth": "Conoce al Mamut",
   "Middle battle": "Batalla media",
+  "Mind control (v1)": "Control telepático(v1)",
+  "Mind control (v2)": "Control telepático(v2)",
   "Mongolian Horde (v1)": "Horda mongol (v1)",
   "Mongolian Horde (v2)": "Horda mongol (v2)",
   "Move like a knight (v1)": "Moverse como un caballo (v1)",
diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js
index 255595ec..f905ab3a 100644
--- a/client/src/translations/fr.js
+++ b/client/src/translations/fr.js
@@ -241,6 +241,8 @@ export const translations = {
   "Mate the knight": "Matez le cavalier",
   "Meet the Mammoth": "Rencontrez le Mammouth",
   "Middle battle": "Bataille du milieu",
+  "Mind control (v1)": "Contrôle télépathique (v1)",
+  "Mind control (v2)": "Contrôle télépathique (v2)",
   "Mongolian Horde (v1)": "Horde mongole (v1)",
   "Mongolian Horde (v2)": "Horde mongole (v2)",
   "Move like a knight (v1)": "Bouger comme un cavalier (v1)",
diff --git a/client/src/translations/rules/Hypnotic/en.pug b/client/src/translations/rules/Hypnotic/en.pug
index 21203baa..d3263999 100644
--- a/client/src/translations/rules/Hypnotic/en.pug
+++ b/client/src/translations/rules/Hypnotic/en.pug
@@ -1 +1,28 @@
-p.boxed TODO
+p.boxed
+  | You can control enemy pieces which are attacked.
+
+p.
+  Rather than moving one of his own pieces, a player may elect to move any
+  enemy piece that is attacked. The move must be a legal move for the enemy
+  piece, except that the (hypnotized) piece now captures its friends.
+
+p.
+  The hypnotized piece is temporarily frozen and thus cannot be moved by its
+  owner on the next turn.
+
+figure.diagram-container
+  .diagram.diag12
+    | fen:r1bqk1nr/pppp1ppp/2n5/1Bb1p3/4P3/5N2/PPPP1PPP/RNBQK2R:
+  .diagram.diag22
+    | fen:r1bnk1nr/pppp1ppp/8/1Bb1p3/4P3/5N2/PPPP1PPP/RNBQK2R:
+  figcaption Before and after Nxd8 (controlled by bishop on b5).
+
+h3 Source
+
+p
+  | Hypnotic Chess on 
+  a(href="https://www.chessvariants.com/mvopponent.dir/hypnotic-chess.html")
+    | chessvariants.com
+  | .
+
+p Inventor: W. D. Troyka (2003)
diff --git a/client/src/translations/rules/Hypnotic/es.pug b/client/src/translations/rules/Hypnotic/es.pug
index 21203baa..e76cb56d 100644
--- a/client/src/translations/rules/Hypnotic/es.pug
+++ b/client/src/translations/rules/Hypnotic/es.pug
@@ -1 +1,29 @@
-p.boxed TODO
+p.boxed
+  | Puedes controlar las piezas enemigas qué son atacadas.
+
+p.
+  En lugar de mover una de sus piezas, un jugador puede decidir mover
+  una pieza del oponente que está atacando. La jugada debe ser legal al punto
+  de vista del adversario, con el detalle que la pieza (hipnotizada)
+  ahora captura sus amigos.
+
+p.
+  La pieza hipnotizada está temporalmente inmovilizada y, por lo tanto,
+  no puede movido por su dueño en el siguiente turno.
+
+figure.diagram-container
+  .diagram.diag12
+    | fen:r1bqk1nr/pppp1ppp/2n5/1Bb1p3/4P3/5N2/PPPP1PPP/RNBQK2R:
+  .diagram.diag22
+    | fen:r1bnk1nr/pppp1ppp/8/1Bb1p3/4P3/5N2/PPPP1PPP/RNBQK2R:
+  figcaption Antes y después de Nxd8 (controlado por el alfil en b5).
+
+h3 Fuente
+
+p
+  | Hypnotic Chess en 
+  a(href="https://www.chessvariants.com/mvopponent.dir/hypnotic-chess.html")
+    | chessvariants.com
+  | .
+
+p Inventor: W. D. Troyka (2003)
diff --git a/client/src/translations/rules/Hypnotic/fr.pug b/client/src/translations/rules/Hypnotic/fr.pug
index 21203baa..378231b4 100644
--- a/client/src/translations/rules/Hypnotic/fr.pug
+++ b/client/src/translations/rules/Hypnotic/fr.pug
@@ -1 +1,29 @@
-p.boxed TODO
+p.boxed
+  | Vous pouvez contrôler les pièces ennemies qui sont attaquées.
+
+p.
+  Plutôt que de déplacer une de ses pièces, un joueur peut décider de
+  déplacer une pièce adverse qu'il attaque. Le coup doit être légal du point
+  de vue de l'adversaire, au détail près que la pièce (hypnotisée) capture
+  désormais ses amis.
+
+p.
+  La pièce hypnotisée est temporairement immobilisée et ne peut donc pas être
+  déplacée par son propriétaire au tour suivant.
+
+figure.diagram-container
+  .diagram.diag12
+    | fen:r1bqk1nr/pppp1ppp/2n5/1Bb1p3/4P3/5N2/PPPP1PPP/RNBQK2R:
+  .diagram.diag22
+    | fen:r1bnk1nr/pppp1ppp/8/1Bb1p3/4P3/5N2/PPPP1PPP/RNBQK2R:
+  figcaption Avant et après Nxd8 (contrôlé par le fou en b5).
+
+h3 Source
+
+p
+  | Hypnotic Chess sur 
+  a(href="https://www.chessvariants.com/mvopponent.dir/hypnotic-chess.html")
+    | chessvariants.com
+  | .
+
+p Inventeur : W. D. Troyka (2003)
diff --git a/client/src/translations/rules/Mesmer/en.pug b/client/src/translations/rules/Mesmer/en.pug
index 21203baa..1f5b1bc6 100644
--- a/client/src/translations/rules/Mesmer/en.pug
+++ b/client/src/translations/rules/Mesmer/en.pug
@@ -1 +1,31 @@
-p.boxed TODO
+p.boxed
+  | Pieces attacked by the Mesmerist can be controlled.
+
+p.
+  A new piece &mdash; the Mesmerist &mdash; has the power of mind control,
+  and may elect to move any enemy piece which he observes.
+  The Mesmerist moves like an orthodox queen, but cannot capture (except
+  through mind control). His victim is said to be mesmerized.
+
+p The Mesmerist is represented by an upside-down queen for now.
+
+figure.diagram-container
+  .diagram
+    | fen:rnbqkbnr/pppppppp/3m4/8/8/3M4/PPPPPPPP/RNBQKBNR:
+  figcaption Deterministic initial position.
+
+p.
+  The mesmerized piece is temporarily frozen and thus cannot be moved by its
+  owner on the next turn.
+
+p The game is won by capturing the enemy King or Mesmerist.
+
+h3 Source
+
+p
+  | Mesmer Chess on 
+  a(href="https://www.chessvariants.com/mvopponent.dir/mesmer-chess.html")
+    | chessvariants.com
+  | .
+
+p Inventor: W. D. Troyka (2003)
diff --git a/client/src/translations/rules/Mesmer/es.pug b/client/src/translations/rules/Mesmer/es.pug
index 21203baa..371ba39a 100644
--- a/client/src/translations/rules/Mesmer/es.pug
+++ b/client/src/translations/rules/Mesmer/es.pug
@@ -1 +1,31 @@
-p.boxed TODO
+p.boxed
+  | Las piezas atacadas por el Hipnotizador pueden controlarse.
+
+p.
+  Una nueva pieza &mdash; el Hipnotizador &mdash; puede elegir mudarse
+  una pieza enemiga que observa, porque luego controla su mente.
+  El Hipnotizador se mueve como una dama ortodoxa, pero no puede capturar
+  (excepto por telepatía). Se dice que su víctima está hipnotizada.
+
+p El Hipnotizador está representado por una dama al revés por ahora.
+
+figure.diagram-container
+  .diagram
+    | fen:rnbqkbnr/pppppppp/3m4/8/8/3M4/PPPPPPPP/RNBQKBNR:
+  figcaption Posición inicial determinista.
+
+p.
+  La pieza hipnotizada está temporalmente inmovilizada y, por lo tanto,
+  no puede ser movido por su dueño en el siguiente turno.
+
+p El juego se gana capturando al Rey o al Hipnotizador oponente.
+
+h3 Fuente
+
+p
+  | Mesmer Chess en 
+  a(href="https://www.chessvariants.com/mvopponent.dir/mesmer-chess.html")
+    | chessvariants.com
+  | .
+
+p Inventor: W. D. Troyka (2003)
diff --git a/client/src/translations/rules/Mesmer/fr.pug b/client/src/translations/rules/Mesmer/fr.pug
index 21203baa..9c5255f3 100644
--- a/client/src/translations/rules/Mesmer/fr.pug
+++ b/client/src/translations/rules/Mesmer/fr.pug
@@ -1 +1,31 @@
-p.boxed TODO
+p.boxed
+  | Les pièces attaquées par l'Hypnotiseur peuvent être contrôlées.
+
+p.
+  Une nouvelle pièce &mdash; l'Hypnotiseur &mdash; peut choisir de déplacer
+  une pièce ennemie qu'il observe, car il contrôle alors son esprit.
+  L'Hypnotiseur se déplace comme une dame orthodoxe, mais ne peut pas capturer
+  (sauf par télépathie). Sa victime est dite hypnotisée.
+
+p L'Hypnotiseur est représenté par une dame inversé pour l'instant.
+
+figure.diagram-container
+  .diagram
+    | fen:rnbqkbnr/pppppppp/3m4/8/8/3M4/PPPPPPPP/RNBQKBNR:
+  figcaption Position initiale déterministe.
+
+p.
+  La pièce hypnotisée est temporairement immobilisée, et ne peut donc pas
+  être déplacée par son propriétaire au tour suivant.
+
+p La partie est gagnée en capturant le Roi ou l'Hypnotiseur adverse.
+
+h3 Source
+
+p
+  | Mesmer Chess sur 
+  a(href="https://www.chessvariants.com/mvopponent.dir/mesmer-chess.html")
+    | chessvariants.com
+  | .
+
+p Inventeur : W. D. Troyka (2003)
diff --git a/client/src/translations/variants/en.pug b/client/src/translations/variants/en.pug
index 312df305..f77c4b15 100644
--- a/client/src/translations/variants/en.pug
+++ b/client/src/translations/variants/en.pug
@@ -405,10 +405,12 @@ p.
     "Freecapture",
     "Gridolina",
     "Hamilton",
+    "Hypnotic",
     "Isardam",
     "Kingsmaker",
     "Magnetic",
     "Maharajah",
+    "Mesmer",
     "Otage",
     "Pacosako",
     "Parachute",
diff --git a/client/src/translations/variants/es.pug b/client/src/translations/variants/es.pug
index 9d360c8e..e3a287fb 100644
--- a/client/src/translations/variants/es.pug
+++ b/client/src/translations/variants/es.pug
@@ -416,10 +416,12 @@ p.
     "Freecapture",
     "Gridolina",
     "Hamilton",
+    "Hypnotic",
     "Isardam",
     "Kingsmaker",
     "Magnetic",
     "Maharajah",
+    "Mesmer",
     "Otage",
     "Pacosako",
     "Parachute",
diff --git a/client/src/translations/variants/fr.pug b/client/src/translations/variants/fr.pug
index 411a2056..cd1a6e40 100644
--- a/client/src/translations/variants/fr.pug
+++ b/client/src/translations/variants/fr.pug
@@ -415,10 +415,12 @@ p.
     "Freecapture",
     "Gridolina",
     "Hamilton",
+    "Hypnotic",
     "Isardam",
     "Kingsmaker",
     "Magnetic",
     "Maharajah",
+    "Mesmer",
     "Otage",
     "Pacosako",
     "Parachute",
diff --git a/client/src/variants/Hypnotic.js b/client/src/variants/Hypnotic.js
new file mode 100644
index 00000000..130b1279
--- /dev/null
+++ b/client/src/variants/Hypnotic.js
@@ -0,0 +1,181 @@
+import { ChessRules } from "@/base_rules";
+
+export class HypnoticRules extends ChessRules {
+
+  static IsGoodFen(fen) {
+    if (!ChessRules.IsGoodFen(fen)) return false;
+    const fenParsed = V.ParseFen(fen);
+    // 5) Check arrival of last hypnotizing move (if any)
+    if (
+      !fenParsed.hSquare ||
+      (fenParsed.hSquare != "-" && !fenParsed.hSquare.match(/^[a-h][1-8]$/))
+    ) {
+      return false;
+    }
+    return true;
+  }
+
+  static ParseFen(fen) {
+    const fenParts = fen.split(" ");
+    return Object.assign(
+      { hSquare: fenParts[5] },
+      ChessRules.ParseFen(fen)
+    );
+  }
+
+  static GenRandInitFen(randomness) {
+    return ChessRules.GenRandInitFen(randomness) + " -";
+  }
+
+  setOtherVariables(fen) {
+    super.setOtherVariables(fen);
+    const parsedFen = V.ParseFen(fen);
+    this.hSquares = [
+      parsedFen.hSquare != "-"
+        ? V.SquareToCoords(parsedFen.hSquare)
+        : null
+    ];
+  }
+
+  getFen() {
+    const L = this.hSquares.length;
+    return (
+      super.getFen() + " " +
+      (!this.hSquares[L-1] ? "-" : V.CoordsToSquare(this.hSquares[L-1]))
+    );
+  }
+
+  canIplay(side) {
+    // Wrong, but sufficient approximation let's say
+    return this.turn == side;
+  }
+
+  canTake([x1, y1], [x2, y2]) {
+    const c = this.turn;
+    const c1 = this.getColor(x1, y1);
+    const c2 = this.getColor(x2, y2);
+    return (c == c1 && c1 != c2) || (c != c1 && c1 == c2);
+  }
+
+  getPotentialMovesFrom([x, y]) {
+    const L = this.hSquares.length;
+    const lh = this.hSquares[L-1];
+    if (!!lh && lh.x == x && lh.y == y) return [];
+    const c = this.getColor(x, y);
+    if (c == this.turn) return super.getPotentialMovesFrom([x, y]);
+    // Playing opponent's pieces: hypnotizing moves. Allowed?
+    if (!this.isAttacked([x, y], this.turn)) return [];
+    const moves =
+      this.getPiece(x, y) == V.KING
+        // No castling with enemy king (...yes, should eat it but...)
+        ? super.getSlideNJumpMoves(
+          [x, y], V.steps[V.ROOK].concat(V.steps[V.BISHOP]), "oneStep")
+        : super.getPotentialMovesFrom([x, y]);
+    return moves;
+  }
+
+  getEnpassantCaptures([x, y], shiftX) {
+    const Lep = this.epSquares.length;
+    const epSquare = this.epSquares[Lep - 1]; //always at least one element
+    let enpassantMove = null;
+    const c = this.getColor(x, y);
+    if (
+      !!epSquare &&
+      epSquare.x == x + shiftX &&
+      Math.abs(epSquare.y - y) == 1 &&
+      // Next conditions to avoid capturing self hypnotized pawns:
+      this.board[x][epSquare.y] != V.EMPTY &&
+      this.getColor(x, epSquare.y) != c //TODO: probably redundant
+    ) {
+      enpassantMove = this.getBasicMove([x, y], [epSquare.x, epSquare.y]);
+      enpassantMove.vanish.push({
+        x: x,
+        y: epSquare.y,
+        p: this.board[x][epSquare.y].charAt(1),
+        c: this.getColor(x, epSquare.y)
+      });
+    }
+    return !!enpassantMove ? [enpassantMove] : [];
+  }
+
+  // TODO: avoid following code duplication, by using getColor()
+  // instead of this.turn at the beginning of 2 next methods
+  addPawnMoves([x1, y1], [x2, y2], moves, promotions) {
+    let finalPieces = [V.PAWN];
+    const color = this.getColor(x1, y1);
+    const lastRank = (color == "w" ? 0 : V.size.x - 1);
+    if (x2 == lastRank) finalPieces = V.PawnSpecs.promotions;
+    let tr = null;
+    for (let piece of finalPieces) {
+      tr = (piece != V.PAWN ? { c: color, p: piece } : null);
+      moves.push(this.getBasicMove([x1, y1], [x2, y2], tr));
+    }
+  }
+
+  getPotentialPawnMoves([x, y], promotions) {
+    const color = this.getColor(x, y);
+    const [sizeX, sizeY] = [V.size.x, V.size.y];
+    const forward = (color == 'w' ? -1 : 1);
+
+    let moves = [];
+    if (x + forward >= 0 && x + forward < sizeX) {
+      if (this.board[x + forward][y] == V.EMPTY) {
+        this.addPawnMoves([x, y], [x + forward, y], moves, promotions);
+        if (
+          ((color == 'w' && x == 6) || (color == 'b' && x == 1)) &&
+          this.board[x + 2 * forward][y] == V.EMPTY
+        ) {
+          moves.push(this.getBasicMove([x, y], [x + 2 * forward, y]));
+        }
+      }
+      for (let shiftY of [-1, 1]) {
+        if (
+          y + shiftY >= 0 && y + shiftY < sizeY &&
+          this.board[x + forward][y + shiftY] != V.EMPTY &&
+          this.canTake([x, y], [x + forward, y + shiftY])
+        ) {
+          this.addPawnMoves(
+            [x, y], [x + forward, y + shiftY],
+            moves, promotions
+          );
+        }
+      }
+    }
+    Array.prototype.push.apply(moves,
+                               this.getEnpassantCaptures([x, y], forward));
+    return moves;
+  }
+
+  postPlay(move) {
+    super.postPlay(move);
+    if (move.vanish[0].c == this.turn)
+      this.hSquares.push({ x: move.appear[0].x, y: move.appear[0].y });
+    else this.hSquares.push(null);
+    if (move.vanish.length == 2 && move.vanish[1].p == V.KING)
+      this.kingPos[move.vanish[1].c] = [-1, -1];
+  }
+  postUndo(move) {
+    super.postUndo(move);
+    this.hSquares.pop();
+    if (move.vanish.length == 2 && move.vanish[1].p == V.KING)
+      this.kingPos[move.vanish[1].c] = [move.vanish[1].x, move.vanish[1].y];
+  }
+
+  getCheckSquares() {
+    return [];
+  }
+  filterValid(moves) {
+    return moves;
+  }
+
+  getCurrentScore() {
+    const c = this.turn;
+    if (this.kingPos[c][0] < 0) return (c == 'w' ? "0-1" : "1-0");
+    return "*";
+  }
+
+  static get SEARCH_DEPTH() {
+    return 2;
+  }
+
+};
diff --git a/client/src/variants/Mesmer.js b/client/src/variants/Mesmer.js
new file mode 100644
index 00000000..ec40b482
--- /dev/null
+++ b/client/src/variants/Mesmer.js
@@ -0,0 +1,262 @@
+import { ChessRules } from "@/base_rules";
+import { Antiking2Rules } from "@/variants/Antiking2";
+
+export class MesmerRules extends ChessRules {
+
+  static IsGoodFen(fen) {
+    if (!ChessRules.IsGoodFen(fen)) return false;
+    const fenParsed = V.ParseFen(fen);
+    // 5) Check arrival of last hypnotizing move (if any)
+    if (
+      !fenParsed.hSquare ||
+      (fenParsed.hSquare != "-" && !fenParsed.hSquare.match(/^[a-h][1-8]$/))
+    ) {
+      return false;
+    }
+    return true;
+  }
+
+  static get MESMERIST() {
+    return 'm';
+  }
+
+  static get PIECES() {
+    return ChessRules.PIECES.concat([V.MESMERIST]);
+  }
+
+  getPpath(b) {
+    return (b.charAt(1) == 'm' ? "Mesmer/" : "") + b;
+  }
+
+  static ParseFen(fen) {
+    const fenParts = fen.split(" ");
+    return Object.assign(
+      { hSquare: fenParts[5] },
+      ChessRules.ParseFen(fen)
+    );
+  }
+
+  static GenRandInitFen(randomness) {
+    const antikingFen = Antiking2Rules.GenRandInitFen(randomness);
+    return antikingFen.replace('a', 'M').replace('A', 'm') + " -";
+  }
+
+  setOtherVariables(fen) {
+    super.setOtherVariables(fen);
+    const parsedFen = V.ParseFen(fen);
+    this.hSquares = [
+      parsedFen.hSquare != "-"
+        ? V.SquareToCoords(parsedFen.hSquare)
+        : null
+    ];
+  }
+
+  scanKings(fen) {
+    super.scanKings(fen);
+    // Squares of white and black mesmerist:
+    this.mesmerPos = { w: [-1, -1], b: [-1, -1] };
+    const fenRows = V.ParseFen(fen).position.split("/");
+    for (let i = 0; i < fenRows.length; i++) {
+      let k = 0;
+      for (let j = 0; j < fenRows[i].length; j++) {
+        switch (fenRows[i].charAt(j)) {
+          case "m":
+            this.mesmerPos["b"] = [i, k];
+            break;
+          case "M":
+            this.mesmerPos["w"] = [i, k];
+            break;
+          default: {
+            const num = parseInt(fenRows[i].charAt(j), 10);
+            if (!isNaN(num)) k += num - 1;
+          }
+        }
+        k++;
+      }
+    }
+  }
+
+  getFen() {
+    const L = this.hSquares.length;
+    return (
+      super.getFen() + " " +
+      (!this.hSquares[L-1] ? "-" : V.CoordsToSquare(this.hSquares[L-1]))
+    );
+  }
+
+  canIplay(side) {
+    // Wrong, but sufficient approximation let's say
+    return this.turn == side;
+  }
+
+  canTake([x1, y1], [x2, y2]) {
+    const c = this.turn;
+    const c1 = this.getColor(x1, y1);
+    const c2 = this.getColor(x2, y2);
+    return (c == c1 && c1 != c2) || (c != c1 && c1 == c2);
+  }
+
+  getPotentialMovesFrom([x, y]) {
+    const L = this.hSquares.length;
+    const lh = this.hSquares[L-1];
+    if (!!lh && lh.x == x && lh.y == y) return [];
+    const c = this.getColor(x, y);
+    const piece = this.getPiece(x, y);
+    if (c == this.turn) {
+      if (piece == V.MESMERIST) return this.getPotentialMesmeristMoves([x, y]);
+      return super.getPotentialMovesFrom([x, y]);
+    }
+    // Playing opponent's pieces: hypnotizing moves. Allowed?
+    if (piece == V.MESMERIST || !this.isAttackedByMesmerist([x, y], this.turn))
+      return [];
+    const moves =
+      piece == V.KING
+        // No castling with enemy king (...yes, should eat it but...)
+        ? super.getSlideNJumpMoves(
+          [x, y], V.steps[V.ROOK].concat(V.steps[V.BISHOP]), "oneStep")
+        : super.getPotentialMovesFrom([x, y]);
+    return moves;
+  }
+
+  // Moves like a queen without capturing
+  getPotentialMesmeristMoves([x, y]) {
+    const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
+    let moves = [];
+    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]));
+        i += step[0];
+        j += step[1];
+      }
+    }
+    return moves;
+  }
+
+  isAttackedByMesmerist(sq, color) {
+    return (
+      super.isAttackedBySlideNJump(
+        sq, color, V.MESMERIST, V.steps[V.ROOK].concat(V.steps[V.BISHOP]))
+    );
+  }
+
+  getEnpassantCaptures([x, y], shiftX) {
+    const Lep = this.epSquares.length;
+    const epSquare = this.epSquares[Lep - 1]; //always at least one element
+    let enpassantMove = null;
+    const c = this.getColor(x, y);
+    if (
+      !!epSquare &&
+      epSquare.x == x + shiftX &&
+      Math.abs(epSquare.y - y) == 1 &&
+      // Next conditions to avoid capturing self hypnotized pawns:
+      this.board[x][epSquare.y] != V.EMPTY &&
+      this.getColor(x, epSquare.y) != c //TODO: probably redundant
+    ) {
+      enpassantMove = this.getBasicMove([x, y], [epSquare.x, epSquare.y]);
+      enpassantMove.vanish.push({
+        x: x,
+        y: epSquare.y,
+        p: this.board[x][epSquare.y].charAt(1),
+        c: this.getColor(x, epSquare.y)
+      });
+    }
+    return !!enpassantMove ? [enpassantMove] : [];
+  }
+
+  // TODO: avoid following code duplication, by using getColor()
+  // instead of this.turn at the beginning of 2 next methods
+  addPawnMoves([x1, y1], [x2, y2], moves, promotions) {
+    let finalPieces = [V.PAWN];
+    const color = this.getColor(x1, y1);
+    const lastRank = (color == "w" ? 0 : V.size.x - 1);
+    if (x2 == lastRank) finalPieces = V.PawnSpecs.promotions;
+    let tr = null;
+    for (let piece of finalPieces) {
+      tr = (piece != V.PAWN ? { c: color, p: piece } : null);
+      moves.push(this.getBasicMove([x1, y1], [x2, y2], tr));
+    }
+  }
+
+  getPotentialPawnMoves([x, y], promotions) {
+    const color = this.getColor(x, y);
+    const [sizeX, sizeY] = [V.size.x, V.size.y];
+    const forward = (color == 'w' ? -1 : 1);
+
+    let moves = [];
+    if (x + forward >= 0 && x + forward < sizeX) {
+      if (this.board[x + forward][y] == V.EMPTY) {
+        this.addPawnMoves([x, y], [x + forward, y], moves, promotions);
+        if (
+          ((color == 'w' && x == 6) || (color == 'b' && x == 1)) &&
+          this.board[x + 2 * forward][y] == V.EMPTY
+        ) {
+          moves.push(this.getBasicMove([x, y], [x + 2 * forward, y]));
+        }
+      }
+      for (let shiftY of [-1, 1]) {
+        if (
+          y + shiftY >= 0 && y + shiftY < sizeY &&
+          this.board[x + forward][y + shiftY] != V.EMPTY &&
+          this.canTake([x, y], [x + forward, y + shiftY])
+        ) {
+          this.addPawnMoves(
+            [x, y], [x + forward, y + shiftY],
+            moves, promotions
+          );
+        }
+      }
+    }
+    Array.prototype.push.apply(moves,
+                               this.getEnpassantCaptures([x, y], forward));
+    return moves;
+  }
+
+  postPlay(move) {
+    super.postPlay(move);
+    if (move.vanish[0].p == V.MESMERIST)
+      this.mesmerPos[move.vanish[0].c] = [move.appear[0].x, move.appear[0].y];
+    if (move.vanish[0].c == this.turn)
+      this.hSquares.push({ x: move.appear[0].x, y: move.appear[0].y });
+    else this.hSquares.push(null);
+    if (move.vanish.length == 2) {
+      if (move.vanish[1].p == V.KING)
+        this.kingPos[move.vanish[1].c] = [-1, -1];
+      else if (move.vanish[1].p == V.MESMERIST)
+        this.mesmerPos[move.vanish[1].c] = [-1, -1]
+    }
+  }
+  postUndo(move) {
+    super.postUndo(move);
+    if (move.vanish[0].p == V.MESMERIST)
+      this.mesmerPos[move.vanish[0].c] = [move.vanish[0].x, move.vanish[0].y];
+    this.hSquares.pop();
+    if (move.vanish.length == 2) {
+      const v = move.vanish[1];
+      if (v.p == V.KING)
+        this.kingPos[v.c] = [v.x, v.y];
+      else if (v.p == V.MESMERIST)
+        this.mesmerPos[v.c] = [v.x, v.y];
+    }
+  }
+
+  getCheckSquares() {
+    return [];
+  }
+  filterValid(moves) {
+    return moves;
+  }
+
+  getCurrentScore() {
+    const c = this.turn;
+    if (this.kingPos[c][0] < 0) return (c == 'w' ? "0-1" : "1-0");
+    if (this.mesmerPos[c][0] < 0) return (c == 'w' ? "0-1" : "1-0");
+    return "*";
+  }
+
+  static get SEARCH_DEPTH() {
+    return 2;
+  }
+
+};
diff --git a/server/db/populate.sql b/server/db/populate.sql
index 3c703532..be0f09bc 100644
--- a/server/db/populate.sql
+++ b/server/db/populate.sql
@@ -69,6 +69,7 @@ insert or ignore into Variants (name, description) values
   ('Hamilton', 'Walk on a graph'),
   ('Hoppelpoppel', 'Knibis and Bisknis'),
   ('Horde', 'A pawns cloud'),
+  ('Hypnotic', 'Mind control (v1)'),
   ('Interweave', 'Interweaved colorbound teams'),
   ('Isardam', 'No paralyzed pieces'),
   ('Janggi', 'Korean Chess'),
@@ -88,6 +89,7 @@ insert or ignore into Variants (name, description) values
   ('Makpong', 'Thai Chess (v2)'),
   ('Makruk', 'Thai Chess (v1)'),
   ('Maxima', 'Occupy the enemy palace'),
+  ('Mesmer', 'Mind control (v2)'),
   ('Minishogi', 'Shogi 5 x 5'),
   ('Minixiangqi', 'Xiangqi 7 x 7'),
   ('Monochrome', 'All of the same color'),
-- 
2.44.0