From 39fe711a185ee73c907f3d61ddd459a33f40696b Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Wed, 6 Jan 2021 15:54:10 +0100
Subject: [PATCH] Fix PocketKnight + add Screen variant

---
 TODO                                        |   3 +-
 client/src/components/BaseGame.vue          |  12 +-
 client/src/translations/en.js               |   1 +
 client/src/translations/es.js               |   1 +
 client/src/translations/fr.js               |   1 +
 client/src/translations/rules/Screen/en.pug |  18 ++
 client/src/translations/rules/Screen/es.pug |  19 ++
 client/src/translations/rules/Screen/fr.pug |  19 ++
 client/src/translations/variants/en.pug     |   1 +
 client/src/translations/variants/es.pug     |   1 +
 client/src/translations/variants/fr.pug     |   1 +
 client/src/variants/Pocketknight.js         |   6 +
 client/src/variants/Screen.js               | 263 ++++++++++++++++++++
 server/db/populate.sql                      |   1 +
 14 files changed, 342 insertions(+), 5 deletions(-)
 create mode 100644 client/src/translations/rules/Screen/en.pug
 create mode 100644 client/src/translations/rules/Screen/es.pug
 create mode 100644 client/src/translations/rules/Screen/fr.pug
 create mode 100644 client/src/variants/Screen.js

diff --git a/TODO b/TODO
index 68e8b7ad..5f03a4dc 100644
--- a/TODO
+++ b/TODO
@@ -4,7 +4,6 @@ If new live game starts in background, "new game" notify OK but not first move.
 
 NEW VARIANTS:
 https://www.chessvariants.com/incinf.dir/bario.html
-https://www.chessvariants.com/mvopponent.dir/avalanche.html
 https://www.pychess.org/variant/manchu
 https://www.pychess.org/variant/dobutsu
 https://musketeerchess.net/games/musketeer/index.php Attention règle de promotion + SVG / PNG
@@ -15,8 +14,10 @@ https://www.chessvariants.com/other.dir/fugue.html
 https://www.chessvariants.com/rules/spartan-chess
 https://www.chessvariants.com/mvopponent.dir/hypnotic-chess.html
 https://www.chessvariants.com/mvopponent.dir/mesmer-chess.html
+
 https://brainking.com/en/GameRules?tp=47&fwa=ArchivedGame!g=8204276$i=1
 related to: Crown Chess: place all units on move 1 (similar to Sittuyin, more freely --> in own half-board, possible pawns on 1st rank)
+
 http://history.chess.free.fr/rollerball.htm
 Squatter Chess: safe on last rank = win
 Companion Chess : pieces of same nature don't attack each others
diff --git a/client/src/components/BaseGame.vue b/client/src/components/BaseGame.vue
index ee8b1fcb..f1017024 100644
--- a/client/src/components/BaseGame.vue
+++ b/client/src/components/BaseGame.vue
@@ -520,7 +520,8 @@ export default {
           }
           this.inMultimove = true; //potentially
           this.cursor++;
-        } else if (!navigate) {
+        }
+        else if (!navigate) {
           // Already in the middle of a multi-move
           const L = this.moves.length;
           if (!Array.isArray(this.moves[L-1]))
@@ -548,7 +549,8 @@ export default {
               if (moveIdx < move.length) setTimeout(executeMove, 500);
               else afterMove(smove, initurn);
             });
-          } else {
+          }
+          else {
             playSubmove(smove);
             if (moveIdx < move.length) executeMove();
             else afterMove(smove, initurn);
@@ -600,7 +602,8 @@ export default {
               const L = this.moves.length;
               // NOTE: always emit the score, even in unfinished
               this.$emit("newmove", this.moves[L-1], { score: this.score });
-            } else {
+            }
+            else {
               this.inPlay = false;
               if (this.stackToPlay.length > 0)
                 // Move(s) arrived in-between
@@ -659,7 +662,8 @@ export default {
         this.incheck = this.vr.getCheckSquares();
         if (this.cursor >= 0) this.lastMove = this.moves[this.cursor];
         else this.lastMove = null;
-      } else {
+      }
+      else {
         if (!move) {
           const minCursor =
             this.moves.length > 0 && this.moves[0].notation == "..."
diff --git a/client/src/translations/en.js b/client/src/translations/en.js
index 59801a78..0aebead6 100644
--- a/client/src/translations/en.js
+++ b/client/src/translations/en.js
@@ -205,6 +205,7 @@ export const translations = {
   "Extra bishops and knights": "Extra bishops and knights",
   "Faster development": "Faster development",
   "Four new pieces": "Four new pieces",
+  "Free initial setup": "Free initial setup",
   "In the shadow": "In the shadow",
   "Interweaved colorbound teams": "Interweaved colorbound teams",
   "Get strong at self-mate": "Get strong at self-mate",
diff --git a/client/src/translations/es.js b/client/src/translations/es.js
index 5e96bf4e..2a9bb365 100644
--- a/client/src/translations/es.js
+++ b/client/src/translations/es.js
@@ -205,6 +205,7 @@ export const translations = {
   "Extra bishops and knights": "Alfiles y caballos adicionales",
   "Faster development": "Desarrollo acelerado",
   "Four new pieces": "Quatro nuevas piezas",
+  "Free initial setup": "Posición inicial libre",
   "In the shadow": "En la sombra",
   "Interweaved colorbound teams": "Equipos unicolores entrelazados",
   "Get strong at self-mate": "Progreso en mates asistidos",
diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js
index f5e9e6a1..7552b3a2 100644
--- a/client/src/translations/fr.js
+++ b/client/src/translations/fr.js
@@ -205,6 +205,7 @@ export const translations = {
   "Extra bishops and knights": "Fous et cavaliers supplémentaires",
   "Faster development": "Développement accéléré",
   "Four new pieces": "Quatre nouvelles pièces",
+  "Free initial setup": "Position initiale libre",
   "In the shadow": "Dans l'ombre",
   "Interweaved colorbound teams": "Équipes unicolores entremêlées",
   "Get strong at self-mate": "Progressez en mats aidés",
diff --git a/client/src/translations/rules/Screen/en.pug b/client/src/translations/rules/Screen/en.pug
new file mode 100644
index 00000000..f8237a29
--- /dev/null
+++ b/client/src/translations/rules/Screen/en.pug
@@ -0,0 +1,18 @@
+p.boxed
+  | Place all your pieces freely on first turn. Then play normally.
+
+p.
+  Each side first chooses a setup freely, and independently:
+  initial positions are revealed only after both are determined.
+
+p Some constraints apply:
+ul
+  li Bishops on different colors.
+  li One pawn per column, no pawn on first rank.
+
+h3 More information
+
+p
+  | See the 
+  a(href="https://brainking.com/en/GameRules?tp=47") Screen Chess
+  | &nbsp;variant on brainkings.com.
diff --git a/client/src/translations/rules/Screen/es.pug b/client/src/translations/rules/Screen/es.pug
new file mode 100644
index 00000000..924665c4
--- /dev/null
+++ b/client/src/translations/rules/Screen/es.pug
@@ -0,0 +1,19 @@
+p.boxed
+  | Coloca todas tus piezas libremente en el primer turno.
+  | Entonces juega normalmente.
+
+p.
+  Cada lado elige primero una configuración de forma libre e independiente:
+  las posiciones iniciales no se revelan hasta que se hayan determinado ambas.
+
+p Se aplican algunas restricciones:
+ul
+  li Los alfiles en diferentes colores.
+  li Un peón por columna, no hay peones en la primera fila.
+
+h3 Más información
+
+p
+  | Ver la variante 
+  a(href="https://brainking.com/en/GameRules?tp=47") Screen Chess
+  | &nbsp;en brainkings.com.
diff --git a/client/src/translations/rules/Screen/fr.pug b/client/src/translations/rules/Screen/fr.pug
new file mode 100644
index 00000000..980e7948
--- /dev/null
+++ b/client/src/translations/rules/Screen/fr.pug
@@ -0,0 +1,19 @@
+p.boxed
+  | Placez toutes vos pièces librement au premier tour.
+  | Jouez ensuite normalement.
+
+p.
+  Chaque camp choisit d'abord une configuration librement, et indépendamment :
+  les positions initiales ne sont révélées qu'une fois toutes deux déterminées.
+
+p Quelques contraintes s'appliquent :
+ul
+  li Les fous sur des couleurs différentes.
+  li Un pion par colonne, pas de pion sur la première rangée.
+
+h3 Plus d'information
+
+p
+  | Voir la variante 
+  a(href="https://brainking.com/en/GameRules?tp=47") Screen Chess
+  | &nbsp;sur brainkings.com.
diff --git a/client/src/translations/variants/en.pug b/client/src/translations/variants/en.pug
index 6850dc93..9baec9d1 100644
--- a/client/src/translations/variants/en.pug
+++ b/client/src/translations/variants/en.pug
@@ -404,6 +404,7 @@ p.
     "Magnetic",
     "Pacosako",
     "Parachute",
+    "Screen",
     "Takenmake",
     "Titan",
     "Wormhole"
diff --git a/client/src/translations/variants/es.pug b/client/src/translations/variants/es.pug
index d63fe46a..e1eb1902 100644
--- a/client/src/translations/variants/es.pug
+++ b/client/src/translations/variants/es.pug
@@ -415,6 +415,7 @@ p.
     "Magnetic",
     "Pacosako",
     "Parachute",
+    "Screen",
     "Takenmake",
     "Titan",
     "Wormhole"
diff --git a/client/src/translations/variants/fr.pug b/client/src/translations/variants/fr.pug
index c29a5acb..209211ae 100644
--- a/client/src/translations/variants/fr.pug
+++ b/client/src/translations/variants/fr.pug
@@ -414,6 +414,7 @@ p.
     "Magnetic",
     "Pacosako",
     "Parachute",
+    "Screen",
     "Takenmake",
     "Titan",
     "Wormhole"
diff --git a/client/src/variants/Pocketknight.js b/client/src/variants/Pocketknight.js
index 8756ab9f..f40265e6 100644
--- a/client/src/variants/Pocketknight.js
+++ b/client/src/variants/Pocketknight.js
@@ -45,6 +45,12 @@ export class PocketknightRules extends ChessRules {
     );
   }
 
+  canIplay(side, [x, y]) {
+    if (this.subTurn == 1) return super.canIplay(side, [x, y]);
+    // subturn == 2, drop the knight:
+    return side == this.turn && this.board[x][y] == V.EMPTY;
+  }
+
   getPotentialMovesFrom([x, y]) {
     if (this.subTurn == 1) {
       let moves = super.getPotentialMovesFrom([x, y]);
diff --git a/client/src/variants/Screen.js b/client/src/variants/Screen.js
new file mode 100644
index 00000000..0a8a75e2
--- /dev/null
+++ b/client/src/variants/Screen.js
@@ -0,0 +1,263 @@
+import { ChessRules, Move, PiPo } from "@/base_rules";
+import { randInt } from "@/utils/alea";
+import { ArrayFun } from "@/utils/array";
+
+export class ScreenRules extends ChessRules {
+
+  static get HasFlags() {
+    return false;
+  }
+
+  static get HasEnpassant() {
+    return false;
+  }
+
+  get showFirstTurn() {
+    return true;
+  }
+
+  get canAnalyze() {
+    return this.movesCount >= 2;
+  }
+
+  get someHiddenMoves() {
+    return this.movesCount <= 1;
+  }
+
+  static GenRandInitFen() {
+    // Empty board
+    return "8/8/8/8/8/8/8/8 w 0";
+  }
+
+  re_setReserve(subTurn) {
+    const mc = this.movesCount;
+    const wc = (mc == 0 ? 1 : 0);
+    const bc = (mc <= 1 ? 1 : 0);
+    this.reserve = {
+      w: {
+        [V.PAWN]: wc * 8,
+        [V.ROOK]: wc * 2,
+        [V.KNIGHT]: wc * 2,
+        [V.BISHOP]: wc * 2,
+        [V.QUEEN]: wc,
+        [V.KING]: wc
+      },
+      b: {
+        [V.PAWN]: bc * 8,
+        [V.ROOK]: bc * 2,
+        [V.KNIGHT]: bc * 2,
+        [V.BISHOP]: bc * 2,
+        [V.QUEEN]: bc,
+        [V.KING]: bc
+      }
+    }
+    this.subTurn = subTurn || 1;
+  }
+
+  re_setEnlightened(onOff) {
+    if (!onOff) delete this["enlightened"];
+    else {
+      // Turn on:
+      this.enlightened = {
+        'w': ArrayFun.init(8, 8, false),
+        'b': ArrayFun.init(8, 8, false)
+      };
+      for (let i=0; i<4; i++) {
+        for (let j=0; j<8; j++) this.enlightened['b'][i][j] = true;
+      }
+      for (let i=5; i<8; i++) {
+        for (let j=0; j<8; j++) this.enlightened['w'][i][j] = true;
+      }
+    }
+  }
+
+  setOtherVariables(fen) {
+    super.setOtherVariables(fen);
+    if (this.movesCount <= 1) {
+      this.re_setReserve();
+      this.re_setEnlightened(true);
+    }
+  }
+
+  getColor(i, j) {
+    if (i >= V.size.x) return i == V.size.x ? "w" : "b";
+    return this.board[i][j].charAt(0);
+  }
+
+  getPiece(i, j) {
+    if (i >= V.size.x) return V.RESERVE_PIECES[j];
+    return this.board[i][j].charAt(1);
+  }
+
+  getReservePpath(index, color) {
+    return color + V.RESERVE_PIECES[index];
+  }
+
+  static get RESERVE_PIECES() {
+    return [V.PAWN, V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN, V.KING];
+  }
+
+  getPotentialMovesFrom([x, y]) {
+    if (this.movesCount >= 2) return super.getPotentialMovesFrom([x, y]);
+    // Only reserve moves are allowed for now:
+    if (V.OnBoard(x, y)) return [];
+    const color = this.turn;
+    const p = V.RESERVE_PIECES[y];
+    if (this.reserve[color][p] == 0) return [];
+    const shift = (p == V.PAWN ? 1 : 0);
+    let iBound = (color == 'w' ? [4, 7 - shift] : [shift, 3]);
+    let moves = [];
+
+    // Pawns cannot stack on files, one bishop per color
+    let forbiddenFiles = [];
+    if (p == V.PAWN) {
+      const colorShift = (color == 'w' ? 4 : 1);
+      forbiddenFiles =
+        ArrayFun.range(8).filter(jj => {
+          return ArrayFun.range(3).some(ii => {
+            return (
+              this.board[colorShift + ii][jj] != V.EMPTY &&
+              this.getPiece(colorShift + ii, jj) == V.PAWN
+            );
+          })
+        });
+    }
+    let forbiddenColor = -1;
+    if (p == V.BISHOP) {
+      const colorShift = (color == 'w' ? 4 : 0);
+      outerLoop: for (let ii = colorShift; ii < colorShift + 4; ii++) {
+        for (let jj = 0; jj < 8; jj++) {
+          if (
+            this.board[ii][jj] != V.EMPTY &&
+            this.getPiece(ii, jj) == V.BISHOP
+          ) {
+            forbiddenColor = (ii + jj) % 2;
+            break outerLoop;
+          }
+        }
+      }
+    }
+
+    for (let i = iBound[0]; i <= iBound[1]; i++) {
+      for (let j = 0; j < 8; j++) {
+        if (
+          this.board[i][j] == V.EMPTY &&
+          (p != V.PAWN || !forbiddenFiles.includes(j)) &&
+          (p != V.BISHOP || (i + j) % 2 != forbiddenColor)
+        ) {
+          // Ok, move is valid:
+          let mv = new Move({
+            appear: [
+              new PiPo({
+                x: i,
+                y: j,
+                c: color,
+                p: p
+              })
+            ],
+            vanish: [],
+            start: { x: x, y: y },
+            end: { x: i, y: j }
+          });
+          moves.push(mv);
+        }
+      }
+    }
+    moves.forEach(m => { m.end.noHighlight = true; });
+    return moves;
+  }
+
+  underCheck(color) {
+    if (this.movesCount <= 1) return false;
+    return super.underCheck(color);
+  }
+
+  getAllValidMoves() {
+    if (this.movesCount >= 2) return super.getAllValidMoves();
+    const color = this.turn;
+    let moves = [];
+    for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
+      moves = moves.concat(
+        this.getPotentialMovesFrom([V.size.x + (color == "w" ? 0 : 1), i])
+      );
+    }
+    return this.filterValid(moves);
+  }
+
+  play(move) {
+    const color = move.appear[0].c;
+    if (this.movesCount <= 1) {
+      V.PlayOnBoard(this.board, move);
+      const piece = move.appear[0].p;
+      this.reserve[color][piece]--;
+      if (piece == V.KING) this.kingPos[color] = [move.end.x, move.end.y];
+      if (this.subTurn == 16) {
+        // All placement moves are done
+        this.movesCount++;
+        this.turn = V.GetOppCol(color);
+        if (this.movesCount == 1) this.subTurn = 1;
+        else {
+          // Initial placement is over
+          delete this["reserve"];
+          delete this["subTurn"];
+        }
+      }
+      else this.subTurn++;
+    }
+    else {
+      if (this.movesCount == 2) this.re_setEnlightened(false);
+      super.play(move);
+    }
+  }
+
+  undo(move) {
+    const color = move.appear[0].c;
+    if (this.movesCount <= 2) {
+      V.UndoOnBoard(this.board, move);
+      const piece = move.appear[0].p;
+      if (piece == V.KING) this.kingPos[color] = [-1, -1];
+      if (!this.subTurn || this.subTurn == 1) {
+        // All placement moves are undone (if any)
+        if (!this.subTurn) this.re_setReserve(16);
+        else this.subTurn = 16;
+        this.movesCount--;
+        if (this.movesCount == 1) this.re_setEnlightened(true);
+        this.turn = color;
+      }
+      else this.subTurn--;
+      this.reserve[color][piece]++;
+    }
+    else super.undo(move);
+  }
+
+  getCheckSquares() {
+    if (this.movesCount <= 1) return [];
+    return super.getCheckSquares();
+  }
+
+  getCurrentScore() {
+    if (this.movesCount <= 1) return "*";
+    return super.getCurrentScore();
+  }
+
+  getComputerMove() {
+    if (this.movesCount >= 2) return super.getComputerMove();
+    // Play a random "initialization move"
+    let res = [];
+    for (let i=0; i<16; i++) {
+      const moves = this.getAllValidMoves();
+      const moveIdx = randInt(moves.length);
+      this.play(moves[moveIdx]);
+      res.push(moves[moveIdx]);
+    }
+    for (let i=15; i>=0; i--) this.undo(res[i]);
+    return res;
+  }
+
+  getNotation(move) {
+    // Do not note placement moves (complete move would be too long)
+    if (move.vanish.length == 0) return "";
+    return super.getNotation(move);
+  }
+
+};
diff --git a/server/db/populate.sql b/server/db/populate.sql
index 17fe0d0f..e1156e5d 100644
--- a/server/db/populate.sql
+++ b/server/db/populate.sql
@@ -109,6 +109,7 @@ insert or ignore into Variants (name, description) values
   ('Royalrace', 'Kings cross the 11x11 board'),
   ('Rugby', 'Transform an essay'),
   ('Schess', 'Seirawan-Harper Chess'),
+  ('Screen', 'Free initial setup'),
   ('Shako', 'Non-conformism and utopia'),
   ('Shatranj', 'Ancient rules'),
   ('Shogi', 'Japanese Chess'),
-- 
2.44.0