From b90120e062404b8c656d4f38e66727df8a7e1c5a Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Tue, 12 Jan 2021 17:18:29 +0100
Subject: [PATCH] Add Spartan Chess

---
 TODO                                         |   1 -
 client/public/images/pieces/Spartan/bc.svg   |   1 +
 client/public/images/pieces/Spartan/bg.svg   |   1 +
 client/public/images/pieces/Spartan/bl.svg   |   1 +
 client/public/images/pieces/Spartan/bw.svg   |   1 +
 client/src/translations/en.js                |   1 +
 client/src/translations/es.js                |   1 +
 client/src/translations/fr.js                |   1 +
 client/src/translations/rules/Spartan/en.pug |  74 +++-
 client/src/translations/rules/Spartan/es.pug |  78 +++-
 client/src/translations/rules/Spartan/fr.pug |  78 +++-
 client/src/translations/variants/en.pug      |   1 +
 client/src/translations/variants/es.pug      |   1 +
 client/src/translations/variants/fr.pug      |   1 +
 client/src/variants/Colorbound.js            |  15 +-
 client/src/variants/Spartan.js               | 401 +++++++++++++++++++
 server/db/populate.sql                       |   1 +
 17 files changed, 644 insertions(+), 14 deletions(-)
 create mode 120000 client/public/images/pieces/Spartan/bc.svg
 create mode 120000 client/public/images/pieces/Spartan/bg.svg
 create mode 120000 client/public/images/pieces/Spartan/bl.svg
 create mode 120000 client/public/images/pieces/Spartan/bw.svg
 create mode 100644 client/src/variants/Spartan.js

diff --git a/TODO b/TODO
index 0bd4ab0d..2f72e33e 100644
--- a/TODO
+++ b/TODO
@@ -8,7 +8,6 @@ http://history.chess.free.fr/rollerball.htm
 
 https://www.pychess.org/variant/shogun
 https://www.chessvariants.com/other.dir/fugue.html
-https://www.chessvariants.com/rules/spartan-chess
 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/
diff --git a/client/public/images/pieces/Spartan/bc.svg b/client/public/images/pieces/Spartan/bc.svg
new file mode 120000
index 00000000..f7661a29
--- /dev/null
+++ b/client/public/images/pieces/Spartan/bc.svg
@@ -0,0 +1 @@
+../br.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Spartan/bg.svg b/client/public/images/pieces/Spartan/bg.svg
new file mode 120000
index 00000000..a41e488c
--- /dev/null
+++ b/client/public/images/pieces/Spartan/bg.svg
@@ -0,0 +1 @@
+../Schess/be.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Spartan/bl.svg b/client/public/images/pieces/Spartan/bl.svg
new file mode 120000
index 00000000..dfaa0688
--- /dev/null
+++ b/client/public/images/pieces/Spartan/bl.svg
@@ -0,0 +1 @@
+../bb.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Spartan/bw.svg b/client/public/images/pieces/Spartan/bw.svg
new file mode 120000
index 00000000..3a672eb8
--- /dev/null
+++ b/client/public/images/pieces/Spartan/bw.svg
@@ -0,0 +1 @@
+../Schess/bh.svg
\ No newline at end of file
diff --git a/client/src/translations/en.js b/client/src/translations/en.js
index bfd3f2ea..6dcbab1f 100644
--- a/client/src/translations/en.js
+++ b/client/src/translations/en.js
@@ -289,6 +289,7 @@ export const translations = {
   "Shared pieces (v2)": "Shared pieces (v2)",
   "Shogi 5 x 5": "Shogi 5 x 5",
   "Shoot pieces": "Shoot pieces",
+  "Spartan versus Persians": "Spartan versus Persians",
   "Squares disappear": "Squares disappear",
   "Squat last rank": "Squat last rank",
   "Standard rules": "Standard rules",
diff --git a/client/src/translations/es.js b/client/src/translations/es.js
index 03be9bbf..6f6181f4 100644
--- a/client/src/translations/es.js
+++ b/client/src/translations/es.js
@@ -289,6 +289,7 @@ export const translations = {
   "Shared pieces (v2)": "Piezas compartidas (v2)",
   "Shogi 5 x 5": "Shogi 5 x 5",
   "Shoot pieces": "Tirar de las piezas",
+  "Spartan versus Persians": "Espartanos contra Persas",
   "Squares disappear": "Las casillas desaparecen",
   "Squat last rank": "Ocupa la última fila",
   "Standard rules": "Reglas estandar",
diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js
index 22f333af..e4e3c34c 100644
--- a/client/src/translations/fr.js
+++ b/client/src/translations/fr.js
@@ -289,6 +289,7 @@ export const translations = {
   "Shared pieces (v2)": "Pièces partagées (v2)",
   "Shogi 5 x 5": "Shogi 5 x 5",
   "Shoot pieces": "Tirez sur les pièces",
+  "Spartan versus Persians": "Spartiates contre Perses",
   "Squares disappear": "Les cases disparaissent",
   "Squat last rank": "Occupez la dernière rangée",
   "Standard rules": "Règles usuelles",
diff --git a/client/src/translations/rules/Spartan/en.pug b/client/src/translations/rules/Spartan/en.pug
index 21203baa..25205d15 100644
--- a/client/src/translations/rules/Spartan/en.pug
+++ b/client/src/translations/rules/Spartan/en.pug
@@ -1 +1,73 @@
-p.boxed TODO
+p.boxed
+  | Two different armies: Persians versus Spartians.
+
+p.
+  The Black side represents the Spartans and the White the Persians.
+  The Persians pawns and pieces follow the rules of orthodox chess.
+  The Spartans have two Kings and with the exception of their Kings,
+  every Spartan playing piece moves in a non-orthodox fashion.
+
+figure.diagram-container
+  .diagram
+    | fen:lgkcckwl/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR:
+  figcaption Initial deterministic position.
+
+p Approximative correspondances
+table
+  tr
+    th White
+    th Black
+  tr
+    td Rook
+    td Captain (on d8 and e8)
+  tr
+    td Bishop
+    td Lieutenant (on a8 and h8)
+  tr
+    td King
+    td Kings (on c8 and f8)
+  tr
+    td Pawn
+    td Hoplite
+p.
+  Queen and Knights don't have counterparts on black side, but,
+  the General on b8 and the Warlord on g8 are strong pieces.
+
+h3 Movements of black pieces
+
+ul
+  li.
+    Hoplite (Pawn) = Berolina pawn, with an extra option on its initial rank:
+    it can jump over an obstacle for the 2-squares advance.
+  li Captain = Rook limited to 2 squares maximum.
+  li.
+    Lieutenant = Bishop limted to two squares maximum.
+    It can also move horizontally by one square, without capturing.
+  li General = Rook + King
+  li Warlord = Bishop + Knight
+
+h3 Some details
+
+p The game essentially follow usual chess rules, with a few exceptions:
+ul
+  li.
+    A black (Spartian) King can go or remain under check, and even be captured,
+    as long as another king is still on the board.
+  li.
+    White (Persians) win either if only one checkmated black king remains,
+    or, if they can checkmate both kings at the same time.
+  li.
+    Pawns promote in a piece of their army. Hoplite can additionally
+    be promoted into King if only one remains.
+
+p No en passant captures, and Black cannot castle.
+
+h3 More information
+
+p
+  | See the 
+  a(href="https://www.chessvariants.com/rules/spartan-chess")
+    | chessvariants page
+  | .
+
+p Inventor: Steven Streetman (2010)
diff --git a/client/src/translations/rules/Spartan/es.pug b/client/src/translations/rules/Spartan/es.pug
index 21203baa..bb3b41da 100644
--- a/client/src/translations/rules/Spartan/es.pug
+++ b/client/src/translations/rules/Spartan/es.pug
@@ -1 +1,77 @@
-p.boxed TODO
+p.boxed
+  | Dos ejércitos diferentes: los persas contra los espartanos.
+
+p.
+  El campo negro representa a los espartanos y el blanco a los persas.
+  Los peones y piezas persas siguen las reglas del ajedrez ortodoxo.
+  Los espartanos tienen dos Reyes y, a excepción de este último,
+  cada unidad espartana se mueve de una manera poco ortodoxa.
+
+figure.diagram-container
+  .diagram
+    | fen:lgkcckwl/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR:
+  figcaption Posición inicial determinista.
+
+p Coincidencias aproximadas
+table
+  tr
+    th Blancas
+    th Negras
+  tr
+    td Torre
+    td Capitán (en d8 y e8)
+  tr
+    td Alfil
+    td Teniente (en a8 y h8)
+  tr
+    td Rey
+    td Reyes (en c8 y f8)
+  tr
+    td Peón
+    td Hoplita
+p.
+  Dama y Caballo no tienen contrapartes del lado negro, pero,
+  el general en b8 y el "Señor de la Guerra" (Warlord) en g8
+  son dos piezas bastante fuertes.
+
+h3 Movimientos de piezas negras
+
+ul
+  li.
+    Hoplite (Peón) = peón Berolina, con una opción adicional de su
+    fila inicial: puede saltar un obstáculo para
+    el avance de dos casillas.
+  li Capitán = Torre limitada a dos casillas como máximo.
+  li.
+    Teniente = Loco limitado a dos casillas como máximo.
+    También puede moverse horizontalmente un cuadrado, sin capturar.
+  li General = Torre + Rey
+  li Warlord = Alfil + Caballo
+
+h3 Algunos detalles
+
+p.
+  El juego sigue esencialmente las reglas habituales del ajedrez,
+  con algunas excepciones:
+ul
+  li.
+    Un Rey negro (espartano) puede ir o permanecer en jaque, e incluso ser
+    capturado, siempre que quede otro rey en el tablero.
+  li.
+    Las blancas (persas) ganan si hacen jaque mate al único rey negro
+    restante, o si pueden matar a ambos reyes al mismo tiempo.
+  li.
+    Los peones se promocionan a una pieza de su ejército. Los hoplitas pueden
+    además, ser promovidos a Rey si solo hay uno presente.
+
+p No hay capturas en passant, y las negras no pueden enrocar.
+
+h3 Más información
+
+p
+  | Ver la 
+  a(href="https://www.chessvariants.com/rules/spartan-chess")
+    | página chessvariants
+  | .
+
+p Inventor: Steven Streetman (2010)
diff --git a/client/src/translations/rules/Spartan/fr.pug b/client/src/translations/rules/Spartan/fr.pug
index 21203baa..399a1858 100644
--- a/client/src/translations/rules/Spartan/fr.pug
+++ b/client/src/translations/rules/Spartan/fr.pug
@@ -1 +1,77 @@
-p.boxed TODO
+p.boxed
+  | Deux différentes armées : les Perses contre les Spartiates.
+
+p.
+  Le camp noir représente les spartiates, et le blanc les perses.
+  Les pions et pièces perses suivent les règles des échecs orthodoxes.
+  Les spartiates ont deux Rois et à l'exception de ces derniers,
+  chaque unité spartiate se déplace de façon non-orthodoxe.
+
+figure.diagram-container
+  .diagram
+    | fen:lgkcckwl/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR:
+  figcaption Position initiale déterministe.
+
+p Correspondances approximatives
+table
+  tr
+    th Blancs
+    th Noirs
+  tr
+    td Tour
+    td Capitaine (sur d8 et e8)
+  tr
+    td Fou
+    td Lieutenant (sur a8 et h8)
+  tr
+    td Roi
+    td Rois (sur c8 et f8)
+  tr
+    td Pion
+    td Hoplite
+p.
+  Dame et Cavalier n'ont pas de contreparties côté noir, mais,
+  le Général en b8 ainsi que le "Chef de Guerre" (Warlord) en g8
+  sont deux pièces plutôt fortes.
+
+h3 Déplacements des pièces noires
+
+ul
+  li.
+    Hoplite (Pion) = pion Berolina, avec une option supplémentaire depuis sa
+    rangée initiale : il peut sauter par dessus un obstacle pour
+    l'avancée de deux cases.
+  li Capitaine = Tour limitée à deux cases maximum.
+  li.
+    Lieutenant = Fou limité à deux cases maximum.
+    Il peut aussi se mouvoir horizontalement d'une case, sans capturer.
+  li Général = Tour + Roi
+  li Warlord = Fou + Cavalier
+
+h3 Quelques détails
+
+p.
+  La partie suit essentiellement les règles usuelles des échecs,
+  à quelques exceptions près :
+ul
+  li.
+    Un Roi noir (spartiate) peut aller ou rester en échec, et même être
+    capturé, du moment qu'il reste un autre roi sur l'échiquier.
+  li.
+    Les blancs (perses) gagnent s'ils matent le seul roi noir restant, ou,
+    s'ils peuvent mater les deux rois en même temps.
+  li.
+    Les pions se promeuvent en une pièce de leur armée. Les hoplites peuvent
+    en outre être promus en Roi si un seul est présent.
+
+p Pas de prises en passant, et les noirs ne peuvent roquer.
+
+h3 Plus d'information
+
+p
+  | Voir la 
+  a(href="https://www.chessvariants.com/rules/spartan-chess")
+    | page chessvariants
+  | .
+
+p Inventeur : Steven Streetman (2010)
diff --git a/client/src/translations/variants/en.pug b/client/src/translations/variants/en.pug
index 6e7d42ad..5421bfa3 100644
--- a/client/src/translations/variants/en.pug
+++ b/client/src/translations/variants/en.pug
@@ -419,6 +419,7 @@ p.
     "Parachute",
     "Relayup",
     "Screen",
+    "Spartan",
     "Squatter",
     "Takenmake",
     "Titan",
diff --git a/client/src/translations/variants/es.pug b/client/src/translations/variants/es.pug
index 2b767403..143d6212 100644
--- a/client/src/translations/variants/es.pug
+++ b/client/src/translations/variants/es.pug
@@ -430,6 +430,7 @@ p.
     "Parachute",
     "Relayup",
     "Screen",
+    "Spartan",
     "Squatter",
     "Takenmake",
     "Titan",
diff --git a/client/src/translations/variants/fr.pug b/client/src/translations/variants/fr.pug
index 431a725b..27af18d7 100644
--- a/client/src/translations/variants/fr.pug
+++ b/client/src/translations/variants/fr.pug
@@ -429,6 +429,7 @@ p.
     "Parachute",
     "Relayup",
     "Screen",
+    "Spartan",
     "Squatter",
     "Takenmake",
     "Titan",
diff --git a/client/src/variants/Colorbound.js b/client/src/variants/Colorbound.js
index 85f81ef3..90e54dc4 100644
--- a/client/src/variants/Colorbound.js
+++ b/client/src/variants/Colorbound.js
@@ -63,16 +63,11 @@ export class ColorboundRules extends ChessRules {
 
   getPotentialMovesFrom([x, y]) {
     switch (this.getPiece(x, y)) {
-      case V.C_ROOK:
-        return this.getPotentialC_rookMoves([x, y]);
-      case V.C_KNIGHT:
-        return this.getPotentialC_knightMoves([x, y]);
-      case V.C_BISHOP:
-        return this.getPotentialC_bishopMoves([x, y]);
-      case V.C_QUEEN:
-        return this.getPotentialC_queenMoves([x, y]);
-      default:
-        return super.getPotentialMovesFrom([x, y]);
+      case V.C_ROOK: return this.getPotentialC_rookMoves([x, y]);
+      case V.C_KNIGHT: return this.getPotentialC_knightMoves([x, y]);
+      case V.C_BISHOP: return this.getPotentialC_bishopMoves([x, y]);
+      case V.C_QUEEN: return this.getPotentialC_queenMoves([x, y]);
+      default: return super.getPotentialMovesFrom([x, y]);
     }
     return [];
   }
diff --git a/client/src/variants/Spartan.js b/client/src/variants/Spartan.js
new file mode 100644
index 00000000..5b0ccecb
--- /dev/null
+++ b/client/src/variants/Spartan.js
@@ -0,0 +1,401 @@
+import { ChessRules } from "@/base_rules";
+
+export class SpartanRules extends ChessRules {
+
+  static get HasEnpassant() {
+    return false;
+  }
+
+  static IsGoodFlags(flags) {
+    // Only white can castle
+    return !!flags.match(/^[a-z]{2,2}$/);
+  }
+
+  getPpath(b) {
+    if ([V.LIEUTENANT, V.GENERAL, V.CAPTAIN, V.WARLORD].includes(b[1]))
+      return "Spartan/" + b;
+    return b;
+  }
+
+  static GenRandInitFen(randomness) {
+    if (randomness == 0)
+      return "lgkcckwl/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w 0 ah";
+
+    // Mapping white --> black (first knight --> general; TODO):
+    const piecesMap = {
+      'r': 'c',
+      'n': 'w',
+      'b': 'l',
+      'q': 'k',
+      'k': 'k',
+      'g': 'g'
+    };
+
+    const baseFen = ChessRules.GenRandInitFen(randomness).replace('n', 'g');
+    return (
+      baseFen.substr(0, 8).split('').map(p => piecesMap[p]).join('') +
+      baseFen.substr(8)
+    );
+  }
+
+  getFlagsFen() {
+    return this.castleFlags['w'].map(V.CoordToColumn).join("");
+  }
+
+  setFlags(fenflags) {
+    this.castleFlags = { 'w': [-1, -1] };
+    for (let i = 0; i < 2; i++)
+      this.castleFlags['w'][i] = V.ColumnToCoord(fenflags.charAt(i));
+  }
+
+  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;
+    }
+    // Both kings should be on board. One for white, 1 or 2 for black.
+    if (kings['K'] != 1 || ![1, 2].includes(kings['k'])) return false;
+    return true;
+  }
+
+  scanKings(fen) {
+    // Scan white king only:
+    this.kingPos = { w: [-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 "K":
+            this.kingPos["w"] = [i, k];
+            break;
+          default: {
+            const num = parseInt(fenRows[i].charAt(j), 10);
+            if (!isNaN(num)) k += num - 1;
+          }
+        }
+        k++;
+      }
+    }
+  }
+
+  static get LIEUTENANT() {
+    return 'l';
+  }
+  static get GENERAL() {
+    return 'g';
+  }
+  static get CAPTAIN() {
+    return 'c';
+  }
+  static get WARLORD() {
+    return 'w';
+  }
+
+  static get PIECES() {
+    return (
+      ChessRules.PIECES.concat([V.LIEUTENANT, V.GENERAL, V.CAPTAIN, V.WARLORD])
+    );
+  }
+
+  getPotentialMovesFrom([x, y]) {
+    if (this.getColor(x, y) == 'w') return super.getPotentialMovesFrom([x, y]);
+    switch (this.getPiece(x, y)) {
+      case V.PAWN: {
+        const kings = this.getKingsPos();
+        const moves = this.getPotentialHopliteMoves([x, y]);
+        if (kings.length == 1) return moves;
+        return moves.filter(m => m.appear[0].p != V.KING);
+      }
+      case V.KING: return this.getPotentialSpartanKingMoves([x, y]);
+      case V.LIEUTENANT: return this.getPotentialLieutenantMoves([x, y]);
+      case V.GENERAL: return this.getPotentialGeneralMoves([x, y]);
+      case V.CAPTAIN: return this.getPotentialCaptainMoves([x, y]);
+      case V.WARLORD: return this.getPotentialWarlordMoves([x, y]);
+    }
+    return [];
+  }
+
+  static get steps() {
+    return Object.assign(
+      {},
+      ChessRules.steps,
+      {
+        // Dabbabah
+        'd': [
+          [-2, 0],
+          [0, -2],
+          [2, 0],
+          [0, 2]
+        ],
+        // Alfil
+        'a': [
+          [2, 2],
+          [2, -2],
+          [-2, 2],
+          [-2, -2]
+        ]
+      }
+    );
+  }
+
+  getPotentialSpartanKingMoves(sq) {
+    // No castle:
+    const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
+    return super.getSlideNJumpMoves(sq, steps, "oneStep");
+  }
+
+  getPotentialHopliteMoves([x, y]) {
+    // Berolina pawn, with initial jumping option
+    let moves = [];
+    if (x == 6) {
+      const finalPieces =
+        [V.LIEUTENANT, V.GENERAL, V.CAPTAIN, V.KING, V.WARLORD];
+      for (let shiftY of [-1, 0, 1]) {
+        const [i, j] = [7, y + shiftY];
+        if (
+          V.OnBoard(i, j) &&
+          (
+            (shiftY != 0 && this.board[i][j] == V.EMPTY) ||
+            (shiftY == 0 && this.getColor(i, j) == 'w')
+          )
+        ) {
+          for (let p of finalPieces)
+            moves.push(this.getBasicMove([x, y], [i, j], { c: 'b', p: p }));
+        }
+      }
+    }
+    else {
+      for (let shiftY of [-1, 0, 1]) {
+        const [i, j] = [x + 1, y + shiftY];
+        if (
+          V.OnBoard(i, j) &&
+          (
+            (shiftY != 0 && this.board[i][j] == V.EMPTY) ||
+            (shiftY == 0 && this.getColor(i, j) == 'w')
+          )
+        ) {
+          moves.push(this.getBasicMove([x, y], [i, j]));
+        }
+      }
+      // Add initial 2 squares jumps:
+      if (x == 1) {
+        for (let shiftY of [-2, 2]) {
+          const [i, j] = [3, y + shiftY];
+          if (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY)
+            moves.push(this.getBasicMove([x, y], [i, j]));
+        }
+      }
+    }
+    return moves;
+  }
+
+  getPotentialLieutenantMoves([x, y]) {
+    let moves = [];
+    for (let shiftY of [-1, 1]) {
+      const [i, j] = [x, y + shiftY];
+      if (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY)
+        moves.push(this.getBasicMove([x, y], [i, j]));
+    }
+    const steps = V.steps[V.BISHOP].concat(V.steps['a']);
+    Array.prototype.push.apply(
+      moves,
+      super.getSlideNJumpMoves([x, y], steps, "oneStep")
+    );
+    return moves;
+  }
+
+  getPotentialCaptainMoves([x, y]) {
+    const steps = V.steps[V.ROOK].concat(V.steps['d']);
+    return super.getSlideNJumpMoves([x, y], steps, "oneStep")
+  }
+
+  getPotentialGeneralMoves([x, y]) {
+    return (
+      super.getSlideNJumpMoves([x, y], V.steps[V.BISHOP], "oneStep")
+      .concat(super.getSlideNJumpMoves([x, y], V.steps[V.ROOK]))
+    );
+  }
+
+  getPotentialWarlordMoves([x, y]) {
+    return (
+      super.getSlideNJumpMoves([x, y], V.steps[V.KNIGHT], "oneStep")
+      .concat(super.getSlideNJumpMoves([x, y], V.steps[V.BISHOP]))
+    );
+  }
+
+  isAttacked(sq, color) {
+    if (color == 'w') return super.isAttacked(sq, 'w');
+    return (
+      this.isAttackedByHoplite(sq) ||
+      super.isAttackedByKing(sq, 'b') ||
+      this.isAttackedByLieutenant(sq) ||
+      this.isAttackedByGeneral(sq) ||
+      this.isAttackedByCaptain(sq) ||
+      this.isAttackedByWarlord(sq)
+    );
+  }
+
+  isAttackedByHoplite(sq) {
+    return super.isAttackedBySlideNJump(sq, 'b', V.PAWN, [[-1,0]], "oneStep");
+  }
+
+  isAttackedByLieutenant(sq) {
+    const steps = V.steps[V.BISHOP].concat(V.steps['a']);
+    return (
+      super.isAttackedBySlideNJump(sq, 'b', V.LIEUTENANT, steps, "oneStep")
+    );
+  }
+
+  isAttackedByCaptain(sq) {
+    const steps = V.steps[V.ROOK].concat(V.steps['d']);
+    return super.isAttackedBySlideNJump(sq, 'b', V.CAPTAIN, steps, "oneStep");
+  }
+
+  isAttackedByGeneral(sq) {
+    return (
+      super.isAttackedBySlideNJump(
+        sq, 'b', V.GENERAL, V.steps[V.BISHOP], "oneStep") ||
+      super.isAttackedBySlideNJump(sq, 'b', V.GENERAL, V.steps[V.ROOK])
+    );
+  }
+
+  isAttackedByWarlord(sq) {
+    return (
+      super.isAttackedBySlideNJump(sq, 'b', V.GENERAL,
+        V.steps[V.ROOK].concat(V.steps[V.BISHOP]), "oneStep") ||
+      super.isAttackedBySlideNJump(sq, 'b', V.GENERAL, V.steps[V.ROOK])
+    );
+  }
+
+  updateCastleFlags(move, piece) {
+    // Only white can castle:
+    const firstRank = 7;
+    if (piece == V.KING && move.appear[0].c == 'w')
+      this.castleFlags['w'] = [8, 8];
+    else if (
+      move.start.x == firstRank &&
+      this.castleFlags['w'].includes(move.start.y)
+    ) {
+      const flagIdx = (move.start.y == this.castleFlags['w'][0] ? 0 : 1);
+      this.castleFlags['w'][flagIdx] = 8;
+    }
+    else if (
+      move.end.x == firstRank &&
+      this.castleFlags['w'].includes(move.end.y)
+    ) {
+      const flagIdx = (move.end.y == this.castleFlags['w'][0] ? 0 : 1);
+      this.castleFlags['w'][flagIdx] = 8;
+    }
+  }
+
+  postPlay(move) {
+    if (move.vanish[0].c == 'w') super.postPlay(move);
+  }
+
+  postUndo(move) {
+    if (move.vanish[0].c == 'w') super.postUndo(move);
+  }
+
+  getKingsPos() {
+    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) == 'b' &&
+          this.getPiece(i, j) == V.KING
+        ) {
+          kings.push({ x: i, y: j });
+        }
+      }
+    }
+    return kings;
+  }
+
+  getCheckSquares() {
+    if (this.turn == 'w') return super.getCheckSquares();
+    const kings = this.getKingsPos();
+    let res = [];
+    for (let i of [0, 1]) {
+      if (
+        kings.length >= i+1 &&
+        super.isAttacked([kings[i].x, kings[i].y], 'w')
+      ) {
+        res.push([kings[i].x, kings[i].y]);
+      }
+    }
+    return res;
+  }
+
+  filterValid(moves) {
+    if (moves.length == 0) return [];
+    const color = moves[0].vanish[0].c;
+    if (color == 'w') return super.filterValid(moves);
+    // Black moves: check if both kings under attack
+    // If yes, moves must remove at least one attack.
+    const kings = this.getKingsPos();
+    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, 'w')) attacks++;
+        else break; //no need to check further
+      }
+      this.undo(m);
+      return (
+        (kings.length == 2 && attacks <= 1) ||
+        (kings.length == 1 && attacks == 0)
+      );
+    });
+  }
+
+  getCurrentScore() {
+    if (this.turn == 'w') return super.getCurrentScore();
+    if (super.atLeastOneMove()) return "*";
+    // Count kings on board
+    const kings = this.getKingsPos();
+    if (
+      super.isAttacked([kings[0].x, kings[0].y], 'w') ||
+      (kings.length == 2 && super.isAttacked([kings[1].x, kings[1].y], 'w'))
+    ) {
+      return "1-0";
+    }
+    return "1/2"; //stalemate
+  }
+
+  static get VALUES() {
+    return Object.assign(
+      {},
+      ChessRules.VALUES,
+      {
+        l: 3,
+        g: 7,
+        c: 3,
+        w: 7
+      }
+    );
+  }
+
+  static get SEARCH_DEPTH() {
+    return 2;
+  }
+
+};
diff --git a/server/db/populate.sql b/server/db/populate.sql
index 043c8a8f..2510ace0 100644
--- a/server/db/populate.sql
+++ b/server/db/populate.sql
@@ -128,6 +128,7 @@ insert or ignore into Variants (name, description) values
   ('Shatranj', 'Ancient rules'),
   ('Shogi', 'Japanese Chess'),
   ('Sittuyin', 'Burmese Chess'),
+  ('Spartan', 'Spartan versus Persians'),
   ('Squatter', 'Squat last rank'),
   ('Suicide', 'Lose all pieces'),
   ('Suction', 'Attract opposite king'),
-- 
2.44.0