From 3de62e0a608bd2be22c875930bb538b674968a6b Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Fri, 19 Mar 2021 10:30:53 +0100
Subject: [PATCH] Add Knightmate2: two kings, as in Spartan Chess

---
 client/src/translations/en.js                 |   3 +-
 client/src/translations/es.js                 |   3 +-
 client/src/translations/fr.js                 |   3 +-
 client/src/translations/rules/Clorange/en.pug |   1 -
 client/src/translations/rules/Clorange/es.pug |   1 -
 client/src/translations/rules/Clorange/fr.pug |   1 -
 .../rules/{Knightmate => Knightmate1}/en.pug  |   0
 .../rules/{Knightmate => Knightmate1}/es.pug  |   0
 .../rules/{Knightmate => Knightmate1}/fr.pug  |   2 +-
 .../src/translations/rules/Knightmate2/en.pug |  19 ++
 .../src/translations/rules/Knightmate2/es.pug |  20 ++
 .../src/translations/rules/Knightmate2/fr.pug |  20 ++
 client/src/translations/variants/en.pug       |   7 +-
 client/src/translations/variants/es.pug       |   5 +-
 client/src/translations/variants/fr.pug       |   5 +-
 .../{Knightmate.js => Knightmate1.js}         |   4 +-
 client/src/variants/Knightmate2.js            | 194 ++++++++++++++++++
 server/db/populate.sql                        |   3 +-
 18 files changed, 273 insertions(+), 18 deletions(-)
 rename client/src/translations/rules/{Knightmate => Knightmate1}/en.pug (100%)
 rename client/src/translations/rules/{Knightmate => Knightmate1}/es.pug (100%)
 rename client/src/translations/rules/{Knightmate => Knightmate1}/fr.pug (96%)
 create mode 100644 client/src/translations/rules/Knightmate2/en.pug
 create mode 100644 client/src/translations/rules/Knightmate2/es.pug
 create mode 100644 client/src/translations/rules/Knightmate2/fr.pug
 rename client/src/variants/{Knightmate.js => Knightmate1.js} (92%)
 create mode 100644 client/src/variants/Knightmate2.js

diff --git a/client/src/translations/en.js b/client/src/translations/en.js
index 5a5461da..e3f361c7 100644
--- a/client/src/translations/en.js
+++ b/client/src/translations/en.js
@@ -259,7 +259,8 @@ export const translations = {
   "Mandatory captures": "Mandatory captures",
   "Mate any piece (v1)": "Mate any piece (v1)",
   "Mate any piece (v2)": "Mate any piece (v2)",
-  "Mate the knight": "Mate the knight",
+  "Mate the knight (v1)": "Mate the knight (v1)",
+  "Mate the knight (v2)": "Mate the knight (v2)",
   "Meet the Mammoth": "Meet the Mammoth",
   "Middle battle": "Middle battle",
   "Mind control (v1)": "Mind control (v1)",
diff --git a/client/src/translations/es.js b/client/src/translations/es.js
index f868d266..3869b5de 100644
--- a/client/src/translations/es.js
+++ b/client/src/translations/es.js
@@ -259,7 +259,8 @@ export const translations = {
   "Mandatory captures": "Capturas obligatorias",
   "Mate any piece (v1)": "Matar cualquier pieza (v1)",
   "Mate any piece (v2)": "Matar cualquier pieza (v2)",
-  "Mate the knight": "Matar el caballo",
+  "Mate the knight (v1)": "Matar el caballo (v1)",
+  "Mate the knight (v2)": "Matar el caballo (v2)",
   "Meet the Mammoth": "Conoce al Mamut",
   "Middle battle": "Batalla media",
   "Mind control (v1)": "Control telepático(v1)",
diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js
index b0a92165..e75273a5 100644
--- a/client/src/translations/fr.js
+++ b/client/src/translations/fr.js
@@ -259,7 +259,8 @@ export const translations = {
   "Mandatory captures": "Captures obligatoires",
   "Mate any piece (v1)": "Matez n'importe quelle pièce (v1)",
   "Mate any piece (v2)": "Matez n'importe quelle pièce (v2)",
-  "Mate the knight": "Matez le cavalier",
+  "Mate the knight (v1)": "Matez le cavalier (v1)",
+  "Mate the knight (v2)": "Matez le cavalier (v2)",
   "Meet the Mammoth": "Rencontrez le Mammouth",
   "Middle battle": "Bataille du milieu",
   "Mind control (v1)": "Contrôle télépathique (v1)",
diff --git a/client/src/translations/rules/Clorange/en.pug b/client/src/translations/rules/Clorange/en.pug
index 26679f58..695b676d 100644
--- a/client/src/translations/rules/Clorange/en.pug
+++ b/client/src/translations/rules/Clorange/en.pug
@@ -23,7 +23,6 @@ figure.diagram-container
 h3 Source
 
 p
-  | Slightly simplified from 
   a(href="https://www.chessvariants.com/other.dir/clockworkorange.html")
     | Clockwork Orange Chess
   | &nbsp;on chessvariants.com.
diff --git a/client/src/translations/rules/Clorange/es.pug b/client/src/translations/rules/Clorange/es.pug
index a5a8c832..ab73e356 100644
--- a/client/src/translations/rules/Clorange/es.pug
+++ b/client/src/translations/rules/Clorange/es.pug
@@ -26,7 +26,6 @@ figure.diagram-container
 h3 Fuente
 
 p
-  | Ligeramente simplificado desde 
   a(href="https://www.chessvariants.com/other.dir/clockworkorange.html")
     | Clockwork Orange Chess
   | &nbsp;en chessvariants.com.
diff --git a/client/src/translations/rules/Clorange/fr.pug b/client/src/translations/rules/Clorange/fr.pug
index 1e8a4069..44888fa3 100644
--- a/client/src/translations/rules/Clorange/fr.pug
+++ b/client/src/translations/rules/Clorange/fr.pug
@@ -26,7 +26,6 @@ figure.diagram-container
 h3 Source
 
 p
-  | Légèrement simplifié depuis 
   a(href="https://www.chessvariants.com/other.dir/clockworkorange.html")
     | Clockwork Orange Chess
   | &nbsp;sur chessvariants.com.
diff --git a/client/src/translations/rules/Knightmate/en.pug b/client/src/translations/rules/Knightmate1/en.pug
similarity index 100%
rename from client/src/translations/rules/Knightmate/en.pug
rename to client/src/translations/rules/Knightmate1/en.pug
diff --git a/client/src/translations/rules/Knightmate/es.pug b/client/src/translations/rules/Knightmate1/es.pug
similarity index 100%
rename from client/src/translations/rules/Knightmate/es.pug
rename to client/src/translations/rules/Knightmate1/es.pug
diff --git a/client/src/translations/rules/Knightmate/fr.pug b/client/src/translations/rules/Knightmate1/fr.pug
similarity index 96%
rename from client/src/translations/rules/Knightmate/fr.pug
rename to client/src/translations/rules/Knightmate1/fr.pug
index 7e0b3867..dcb7c2b7 100644
--- a/client/src/translations/rules/Knightmate/fr.pug
+++ b/client/src/translations/rules/Knightmate1/fr.pug
@@ -1,6 +1,6 @@
 p.boxed
   | Le roi se déplace comme un cavalier, et les cavaliers comme des rois.
-  | l'objectif est encore de mater le roi.
+  | L'objectif est encore de mater le roi.
 
 p.
   Les "cavaliers se déplaçant comme des rois" sont alors assez logiquement
diff --git a/client/src/translations/rules/Knightmate2/en.pug b/client/src/translations/rules/Knightmate2/en.pug
new file mode 100644
index 00000000..dd759e57
--- /dev/null
+++ b/client/src/translations/rules/Knightmate2/en.pug
@@ -0,0 +1,19 @@
+p.boxed
+  | Kings move like knights, and knights move like kings.
+
+p
+  a(href="/#/variants/Knightmate1") Knightmate1
+  | , with two kings (moving like knights) and without castling.
+
+p.
+  As long as a side has two kings, they are considered like non-royal pieces
+  and one can be captured. Then, the remaining king is royal.
+
+figure.diagram-container
+  .diagram
+    | fen:r6r/1pp1qpp1/p1pcbk1p/2b1p1K1/4P3/2KPBP2/PPPQC1PP/R6R:
+  figcaption g5 king is mated, but the game is not over.
+
+p.
+  Checkmating both kings at the same time also counts as a win:
+  if both are under attacks, you must remove at least one attack.
diff --git a/client/src/translations/rules/Knightmate2/es.pug b/client/src/translations/rules/Knightmate2/es.pug
new file mode 100644
index 00000000..bf0157fa
--- /dev/null
+++ b/client/src/translations/rules/Knightmate2/es.pug
@@ -0,0 +1,20 @@
+p.boxed
+  | Los reyes se mueven como caballos y los caballos como reyes.
+
+p
+  a(href="/#/variants/Knightmate1") Knightmate1
+  | , con dos reyes (muevense como caballos) y sin enroque.
+
+p.
+  Siempre que un lado tenga sus dos reyes, se consideran piezas normales:
+  se puede capturar uno de los dos. Entonces el rey restante
+  tiene estatus real.
+
+figure.diagram-container
+  .diagram
+    | fen:r6r/1pp1qpp1/p1pcbk1p/2b1p1K1/4P3/2KPBP2/PPPQC1PP/R6R:
+  figcaption El rey g5 está atrapado, pero el juego continúa.
+
+p.
+  Matar a ambos reyes al mismo tiempo también gana:
+  si ambos son atacados, debes reprimir al menos un ataque.
diff --git a/client/src/translations/rules/Knightmate2/fr.pug b/client/src/translations/rules/Knightmate2/fr.pug
new file mode 100644
index 00000000..efff2237
--- /dev/null
+++ b/client/src/translations/rules/Knightmate2/fr.pug
@@ -0,0 +1,20 @@
+p.boxed
+  | Les rois se déplacent comme des cavaliers, et les cavaliers comme des rois.
+
+p
+  a(href="/#/variants/Knightmate1") Knightmate1
+  | , avec deux rois (aux déplacements cavaliers) et sans roque.
+
+p.
+  Tant qu'un camp a ses deux rois, ils sont considérés comme des pièces
+  normales : l'un des deux peut être capturé. Ensuite, le roi restant
+  a un statut royal.
+
+figure.diagram-container
+  .diagram
+    | fen:r6r/1pp1qpp1/p1pcbk1p/2b1p1K1/4P3/2KPBP2/PPPQC1PP/R6R:
+  figcaption Le roi g5 est maté, mais la partie continue.
+
+p.
+  Mater les deux rois en même temps gagne également :
+  si les deux sont attaqués, vous devez supprimez au moins une attaque.
diff --git a/client/src/translations/variants/en.pug b/client/src/translations/variants/en.pug
index e5e92f84..15a8aa5e 100644
--- a/client/src/translations/variants/en.pug
+++ b/client/src/translations/variants/en.pug
@@ -90,6 +90,7 @@ p Standard pieces versus a team of different pieces.
     "Empire",
     "Horde",
     "Orda",
+    "Shinobi",
     "Spartan",
     "Synochess"
   ]
@@ -249,7 +250,8 @@ p.
 -
   var varlist = [
     "Balaklava",
-    "Knightmate",
+    "Knightmate1",
+    "Knightmate2",
     "Knightrelay1",
     "Knightrelay2"
   ]
@@ -313,7 +315,7 @@ ul
 
 h3 Repositioning
 
-p Pieces can be drop on the board, either immediately or later in the game.
+p Pieces can be dropped on the board, either immediately or later in the game.
 -
   var varlist = [
     "Clorange",
@@ -321,7 +323,6 @@ p Pieces can be drop on the board, either immediately or later in the game.
     "Madhouse",
     "Rampage",
     "Recycle",
-    "Shinobi",
     "Shogun",
     "Teleport"
   ]
diff --git a/client/src/translations/variants/es.pug b/client/src/translations/variants/es.pug
index a09ce9d3..cc855000 100644
--- a/client/src/translations/variants/es.pug
+++ b/client/src/translations/variants/es.pug
@@ -94,6 +94,7 @@ p Piezas estándar contra un equipo de diferentes piezas.
     "Empire",
     "Horde",
     "Orda",
+    "Shinobi",
     "Spartan",
     "Synochess"
   ]
@@ -256,7 +257,8 @@ p.
 -
   var varlist = [
     "Balaklava",
-    "Knightmate",
+    "Knightmate1",
+    "Knightmate2",
     "Knightrelay1",
     "Knightrelay2"
   ]
@@ -330,7 +332,6 @@ p.
     "Madhouse",
     "Rampage",
     "Recycle",
-    "Shinobi",
     "Shogun",
     "Teleport"
   ]
diff --git a/client/src/translations/variants/fr.pug b/client/src/translations/variants/fr.pug
index 2737859f..f23512ea 100644
--- a/client/src/translations/variants/fr.pug
+++ b/client/src/translations/variants/fr.pug
@@ -93,6 +93,7 @@ p Pièces standard contre une équipe de pièces différentes.
     "Empire",
     "Horde",
     "Orda",
+    "Shinobi",
     "Spartan",
     "Synochess"
   ]
@@ -255,7 +256,8 @@ p.
 -
   var varlist = [
     "Balaklava",
-    "Knightmate",
+    "Knightmate1",
+    "Knightmate2",
     "Knightrelay1",
     "Knightrelay2"
   ]
@@ -329,7 +331,6 @@ p.
     "Madhouse",
     "Rampage",
     "Recycle",
-    "Shinobi",
     "Shogun",
     "Teleport"
   ]
diff --git a/client/src/variants/Knightmate.js b/client/src/variants/Knightmate1.js
similarity index 92%
rename from client/src/variants/Knightmate.js
rename to client/src/variants/Knightmate1.js
index bc886c12..80e7005b 100644
--- a/client/src/variants/Knightmate.js
+++ b/client/src/variants/Knightmate1.js
@@ -1,8 +1,6 @@
 import { ChessRules } from "@/base_rules";
-import { ArrayFun } from "@/utils/array";
-import { randInt } from "@/utils/alea";
 
-export class KnightmateRules extends ChessRules {
+export class Knightmate1Rules extends ChessRules {
 
   static get COMMONER() {
     return "c";
diff --git a/client/src/variants/Knightmate2.js b/client/src/variants/Knightmate2.js
new file mode 100644
index 00000000..c90fb0cc
--- /dev/null
+++ b/client/src/variants/Knightmate2.js
@@ -0,0 +1,194 @@
+import { ChessRules } from "@/base_rules";
+
+export class Knightmate2Rules extends ChessRules {
+
+  static get HasFlags() {
+    return false;
+  }
+
+  static get COMMONER() {
+    return "c";
+  }
+
+  static get PIECES() {
+    return ChessRules.PIECES.concat([V.COMMONER]);
+  }
+
+  getPpath(b) {
+    return ([V.KING, V.COMMONER].includes(b[1]) ? "Knightmate/" : "") + b;
+  }
+
+  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 (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;
+    }
+    // 1 or 2 kings should be on board.
+    if (Object.values(kings).some(k => ![1, 2].includes(k))) return false;
+    return true;
+  }
+
+  scanKings() {}
+
+  static GenRandInitFen(randomness) {
+    return (
+      ChessRules.GenRandInitFen(randomness)
+      .replace(/k/g, 'c').replace(/K/g, 'C')
+      .replace(/n/g, 'k').replace(/N/g, 'K')
+    );
+  }
+
+  getPotentialMovesFrom([x, y]) {
+    switch (this.getPiece(x, y)) {
+      case V.COMMONER:
+        return this.getPotentialCommonerMoves([x, y]);
+      default:
+        return super.getPotentialMovesFrom([x, y]);
+    }
+  }
+
+  getPotentialCommonerMoves(sq) {
+    return this.getSlideNJumpMoves(
+      sq,
+      V.steps[V.ROOK].concat(V.steps[V.BISHOP]),
+      "oneStep"
+    );
+  }
+
+  getPotentialKingMoves(sq) {
+    return super.getPotentialKnightMoves(sq);
+  }
+
+  isAttacked(sq, color) {
+    return (
+      this.isAttackedByCommoner(sq, color) ||
+      this.isAttackedByPawn(sq, color) ||
+      this.isAttackedByRook(sq, color) ||
+      this.isAttackedByBishop(sq, color) ||
+      this.isAttackedByQueen(sq, color) ||
+      this.isAttackedByKing(sq, color)
+    );
+  }
+
+  isAttackedByKing(sq, color) {
+    return this.isAttackedBySlideNJump(
+      sq,
+      color,
+      V.KING,
+      V.steps[V.KNIGHT],
+      "oneStep"
+    );
+  }
+
+  isAttackedByCommoner(sq, color) {
+    return this.isAttackedBySlideNJump(
+      sq,
+      color,
+      V.COMMONER,
+      V.steps[V.ROOK].concat(V.steps[V.BISHOP]),
+      "oneStep"
+    );
+  }
+
+  postPlay() {}
+  postUndo() {}
+
+  // NOTE: 4 next functions (almost) copy-paste from Spartan Chess
+  getKingsPos(color) {
+    let kings = [];
+    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 &&
+          this.getPiece(i, j) == V.KING
+        ) {
+          kings.push({ x: i, y: j });
+        }
+      }
+    }
+    return kings;
+  }
+
+  getCheckSquares() {
+    const color = this.turn;
+    const oppCol = V.GetOppCol(color);
+    const kings = this.getKingsPos(color);
+    let res = [];
+    for (let i of [0, 1]) {
+      if (
+        kings.length >= i+1 &&
+        super.isAttacked([kings[i].x, kings[i].y], oppCol)
+      ) {
+        res.push([kings[i].x, kings[i].y]);
+      }
+    }
+    return res;
+  }
+
+  filterValid(moves) {
+    if (moves.length == 0) return [];
+    const color = moves[0].vanish[0].c;
+    const oppCol = V.GetOppCol(color);
+    // Check if both kings under attack.
+    // If yes, moves must remove at least one attack.
+    const kings = this.getKingsPos(color);
+    return moves.filter(m => {
+      this.play(m);
+      let attacks = 0;
+      for (let k of kings) {
+        const curKingPos =
+          this.board[k.x][k.y] == V.EMPTY
+            ? [m.appear[0].x, m.appear[0].y] //king moved
+            : [k.x, k.y]
+        if (super.isAttacked(curKingPos, oppCol)) attacks++;
+        else break; //no need to check further
+      }
+      this.undo(m);
+      return (
+        (kings.length == 2 && attacks <= 1) ||
+        (kings.length == 1 && attacks == 0)
+      );
+    });
+  }
+
+  getCurrentScore() {
+    if (super.atLeastOneMove()) return "*";
+    // Count kings on board
+    const color = this.turn;
+    const oppCol = V.GetOppCol(color);
+    const kings = this.getKingsPos(color);
+    if (
+      super.isAttacked([kings[0].x, kings[0].y], oppCol) ||
+      (kings.length == 2 && super.isAttacked([kings[1].x, kings[1].y], oppCol))
+    ) {
+      return (color == 'w' ? "0-1" : "1-0");
+    }
+    return "1/2"; //stalemate
+  }
+
+  static get VALUES() {
+    return {
+      p: 1,
+      r: 5,
+      c: 5, //the commoner is valuable
+      b: 3,
+      q: 9,
+      k: 1000
+    };
+  }
+
+};
diff --git a/server/db/populate.sql b/server/db/populate.sql
index 58af05d1..f9722f67 100644
--- a/server/db/populate.sql
+++ b/server/db/populate.sql
@@ -92,7 +92,8 @@ insert or ignore into Variants (name, description) values
   ('Karouk', 'Thai Chess (v3)'),
   ('Kinglet', 'Protect your pawns'),
   ('Kingsmaker', 'Promote into kings'),
-  ('Knightmate', 'Mate the knight'),
+  ('Knightmate1', 'Mate the knight (v1)'),
+  ('Knightmate2', 'Mate the knight (v2)'),
   ('Knightpawns', 'Knight versus pawns'),
   ('Knightrelay1', 'Move like a knight (v1)'),
   ('Knightrelay2', 'Move like a knight (v2)'),
-- 
2.44.0