A few fixes (for updateCastleFlags()) + add Madhouse and Pocketknight variants
authorBenjamin Auder <benjamin.auder@somewhere>
Wed, 20 May 2020 20:52:22 +0000 (22:52 +0200)
committerBenjamin Auder <benjamin.auder@somewhere>
Wed, 20 May 2020 20:52:22 +0000 (22:52 +0200)
19 files changed:
TODO
client/src/base_rules.js
client/src/components/BaseGame.vue
client/src/translations/en.js
client/src/translations/es.js
client/src/translations/fr.js
client/src/translations/rules/Madhouse/en.pug [new file with mode: 0644]
client/src/translations/rules/Madhouse/es.pug [new file with mode: 0644]
client/src/translations/rules/Madhouse/fr.pug [new file with mode: 0644]
client/src/translations/rules/Pocketknight/en.pug [new file with mode: 0644]
client/src/translations/rules/Pocketknight/es.pug [new file with mode: 0644]
client/src/translations/rules/Pocketknight/fr.pug [new file with mode: 0644]
client/src/variants/Checkered1.js
client/src/variants/Dynamo.js
client/src/variants/Madhouse.js [new file with mode: 0644]
client/src/variants/Pocketknight.js [new file with mode: 0644]
client/src/variants/Takenmake.js
client/src/variants/Teleport.js
server/db/populate.sql

diff --git a/TODO b/TODO
index f4d2c50..774d82d 100644 (file)
--- a/TODO
+++ b/TODO
@@ -3,16 +3,20 @@ Also: if new live game starts in background, "new game" notify OK but not first
 On smartphone for Teleport, Chakart, Weiqi and some others: option "confirm moves on touch screen"
 (=> comme pour corr) + option "confirm moves in corr games"?
 
 On smartphone for Teleport, Chakart, Weiqi and some others: option "confirm moves on touch screen"
 (=> comme pour corr) + option "confirm moves in corr games"?
 
-https://www.chessvariants.com/difftaking.dir/replacement.html
-
-https://www.chessvariants.com/other.dir/pocket.html
 https://www.chessvariants.com/other.dir/fugue.html
 https://www.chessvariants.com/rules/spartan-chess
 https://www.chessvariants.com/mvopponent.dir/avalanche.html
 https://www.chessvariants.com/other.dir/fugue.html
 https://www.chessvariants.com/rules/spartan-chess
 https://www.chessvariants.com/mvopponent.dir/avalanche.html
-
 https://www.chessvariants.com/mvopponent.dir/hypnotic-chess.html
 https://www.chessvariants.com/mvopponent.dir/mesmer-chess.html
 
 https://www.chessvariants.com/mvopponent.dir/hypnotic-chess.html
 https://www.chessvariants.com/mvopponent.dir/mesmer-chess.html
 
+Squatter Chess: safe on last rank = win
+Companion Chess : pieces of same nature don't attack each others
+Medusa Chess = Isardam
+Crossing Chess = win when the king cross half-board
+Kingmaker: pawns can promote also into enemy king
+Eightkings: 8 pawns + 8 kings (non-royal until the last remains?)
+Crown Chess: place all units on move 1 (similar to Sittuyin, more freely)
+
 =====
 
 fanorona https://fr.wikipedia.org/wiki/Fanorona
 =====
 
 fanorona https://fr.wikipedia.org/wiki/Fanorona
index d219f78..9ee506e 100644 (file)
@@ -1169,8 +1169,8 @@ export const ChessRules = class ChessRules {
     this.postPlay(move);
   }
 
     this.postPlay(move);
   }
 
-  updateCastleFlags(move, piece) {
-    const c = V.GetOppCol(this.turn);
+  updateCastleFlags(move, piece, color) {
+    const c = color || V.GetOppCol(this.turn);
     const firstRank = (c == "w" ? V.size.x - 1 : 0);
     // Update castling flags if rooks are moved
     const oppCol = this.turn;
     const firstRank = (c == "w" ? V.size.x - 1 : 0);
     // Update castling flags if rooks are moved
     const oppCol = this.turn;
index cb65f03..87d18ab 100644 (file)
@@ -252,12 +252,10 @@ export default {
             const checkSquares = this.vr.getCheckSquares();
             if (checkSquares.length > 0) m.notation += "+";
             if (idxM == Lm - 1) m.fen = this.vr.getFen();
             const checkSquares = this.vr.getCheckSquares();
             if (checkSquares.length > 0) m.notation += "+";
             if (idxM == Lm - 1) m.fen = this.vr.getFen();
-            if (idx == L - 1 && idxM == Lm - 1) {
-              this.incheck = checkSquares;
-              this.score = this.vr.getCurrentScore();
-              if (["1-0", "0-1"].includes(this.score)) m.notation += "#";
-            }
+            if (idx == L - 1 && idxM == Lm - 1) this.incheck = checkSquares;
           });
           });
+          this.score = this.vr.getCurrentScore();
+          if (["1-0", "0-1"].includes(this.score)) m.notation += "#";
         });
       }
       if (firstMoveColor == "b") {
         });
       }
       if (firstMoveColor == "b") {
index 78e112c..d223dc1 100644 (file)
@@ -200,11 +200,13 @@ export const translations = {
   "King of the Hill": "King of the Hill",
   "Kings cross the 8x8 board": "Kings cross the 8x8 board",
   "Kings cross the 11x11 board": "Kings cross the 11x11 board",
   "King of the Hill": "King of the Hill",
   "Kings cross the 8x8 board": "Kings cross the 8x8 board",
   "Kings cross the 11x11 board": "Kings cross the 11x11 board",
+  "Knight in pocket": "Knight in pocket",
   "Landing on the board": "Landing on the board",
   "Laws of attraction": "Laws of attraction",
   "Long jumps over pieces": "Long jumps over pieces",
   "Long live the Queen": "Long live the Queen",
   "Lose all pieces": "Lose all pieces",
   "Landing on the board": "Landing on the board",
   "Laws of attraction": "Laws of attraction",
   "Long jumps over pieces": "Long jumps over pieces",
   "Long live the Queen": "Long live the Queen",
   "Lose all pieces": "Lose all pieces",
+  "Rearrange enemy pieces": "Rearrange enemy pieces",
   "Mandatory captures": "Mandatory captures",
   "Mate any piece (v1)": "Mate any piece (v1)",
   "Mate any piece (v2)": "Mate any piece (v2)",
   "Mandatory captures": "Mandatory captures",
   "Mate any piece (v1)": "Mate any piece (v1)",
   "Mate any piece (v2)": "Mate any piece (v2)",
index 0c7fca0..fe610f2 100644 (file)
@@ -200,11 +200,13 @@ export const translations = {
   "King of the Hill": "Rey de la Colina",
   "Kings cross the 8x8 board": "Los reyes cruzan el 8x8 tablero",
   "Kings cross the 11x11 board": "Los reyes cruzan el 11x11 tablero",
   "King of the Hill": "Rey de la Colina",
   "Kings cross the 8x8 board": "Los reyes cruzan el 8x8 tablero",
   "Kings cross the 11x11 board": "Los reyes cruzan el 11x11 tablero",
+  "Knight in pocket": "Caballo en bolsillo",
   "Landing on the board": "Aterrizando en el tablero",
   "Laws of attraction": "Las leyes de las atracciones",
   "Long jumps over pieces": "Saltos largos sobre las piezas",
   "Long live the Queen": "Larga vida a la reina",
   "Lose all pieces": "Perder todas las piezas",
   "Landing on the board": "Aterrizando en el tablero",
   "Laws of attraction": "Las leyes de las atracciones",
   "Long jumps over pieces": "Saltos largos sobre las piezas",
   "Long live the Queen": "Larga vida a la reina",
   "Lose all pieces": "Perder todas las piezas",
+  "Rearrange enemy pieces": "Reorganizar piezas opuestas",
   "Mandatory captures": "Capturas obligatorias",
   "Mate any piece (v1)": "Matar cualquier pieza (v1)",
   "Mate any piece (v2)": "Matar cualquier pieza (v2)",
   "Mandatory captures": "Capturas obligatorias",
   "Mate any piece (v1)": "Matar cualquier pieza (v1)",
   "Mate any piece (v2)": "Matar cualquier pieza (v2)",
index e6b1c48..980beac 100644 (file)
@@ -200,11 +200,13 @@ export const translations = {
   "King of the Hill": "Roi de la Colline",
   "Kings cross the 8x8 board": "Les rois traversent l'échiquier 8x8",
   "Kings cross the 11x11 board": "Les rois traversent l'échiquier 11x11",
   "King of the Hill": "Roi de la Colline",
   "Kings cross the 8x8 board": "Les rois traversent l'échiquier 8x8",
   "Kings cross the 11x11 board": "Les rois traversent l'échiquier 11x11",
+  "Knight in pocket": "Cavalier en poche",
   "Landing on the board": "Débarquement sur l'échiquier",
   "Laws of attraction": "Les lois de l'attraction",
   "Long jumps over pieces": "Sauts longs par dessus les pièces",
   "Long live the Queen": "Long vie à la Reine",
   "Lose all pieces": "Perdez toutes les pièces",
   "Landing on the board": "Débarquement sur l'échiquier",
   "Laws of attraction": "Les lois de l'attraction",
   "Long jumps over pieces": "Sauts longs par dessus les pièces",
   "Long live the Queen": "Long vie à la Reine",
   "Lose all pieces": "Perdez toutes les pièces",
+  "Rearrange enemy pieces": "Réorganisez les pièces adverses",
   "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)",
   "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)",
diff --git a/client/src/translations/rules/Madhouse/en.pug b/client/src/translations/rules/Madhouse/en.pug
new file mode 100644 (file)
index 0000000..2320e8f
--- /dev/null
@@ -0,0 +1,26 @@
+p.boxed
+  | Captured pieces are immediatly repositioned on the board.
+
+p.
+  The orthodox rules apply, with the following exception.
+  Each piece taken from the opponent is placed back on an empty
+  square by the player that made the capture, such that
+ul
+  li Bishops remain on squares of the same color.
+  li Pawns are not placed on the first or last row.
+
+figure.diagram-container
+  .diagram.diag12
+    | fen:r1bqkb1r/pppp1ppp/2n2n2/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R:
+  .diagram.diag22
+    | fen:r1bqkbnr/pppp1ppp/2B2n2/4p3/4P3/5N2/PPPP1PPP/RNBQK2R:
+  figcaption Bishop takes knight, which is repositioned on g8.
+
+p To place the captured piece, just click on an empty square.
+
+h3 Source
+
+p
+  a(href="https://www.chessvariants.com/difftaking.dir/replacement.html")
+    | Replacement chess
+  | &nbsp;on chessvariants.com.
diff --git a/client/src/translations/rules/Madhouse/es.pug b/client/src/translations/rules/Madhouse/es.pug
new file mode 100644 (file)
index 0000000..2aaa4ac
--- /dev/null
@@ -0,0 +1,27 @@
+p.boxed
+  | Las piezas capturadas se reemplazan inmediatamente en el tablero.
+
+p.
+  Se aplican reglas ortodoxas, con la siguiente excepción.
+  Cada pieza tomada del oponente se vuelve a poner inmediatamente en el
+  tablero, en casi cualquier espacio libre:
+ul
+  li Los alfiles se quedan en las casillas del mismo color.
+  li Los peones no se colocan en la primera o última fila.
+
+figure.diagram-container
+  .diagram.diag12
+    | fen:r1bqkb1r/pppp1ppp/2n2n2/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R:
+  .diagram.diag22
+    | fen:r1bqkbnr/pppp1ppp/2B2n2/4p3/4P3/5N2/PPPP1PPP/RNBQK2R:
+  figcaption Alfil toma el caballo, que se reemplaza en g8.
+
+p Para colocar una pieza, simplemente haga clic en una casilla vacía.
+
+h3 Fuente
+
+p
+  | La 
+  a(href="https://www.chessvariants.com/difftaking.dir/replacement.html")
+    | variante Reemplazo
+  | &nbsp;en chessvariants.com.
diff --git a/client/src/translations/rules/Madhouse/fr.pug b/client/src/translations/rules/Madhouse/fr.pug
new file mode 100644 (file)
index 0000000..4c07a73
--- /dev/null
@@ -0,0 +1,27 @@
+p.boxed
+  | Les pièces capturées sont immédiatement replacées sur l'échiquier.
+
+p.
+  Les règles orthodoxes s'appliquent, avec l'exception suivante.
+  Chaque pièce prise à l'adversaire est immédiatement remise sur l'échiquier,
+  sur presque n'importe quelle case libre :
+ul
+  li Les fous restent sur les cases de la même couleur.
+  li Les pions ne sont pas posés sur la première ou dernière rangée.
+
+figure.diagram-container
+  .diagram.diag12
+    | fen:r1bqkb1r/pppp1ppp/2n2n2/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R:
+  .diagram.diag22
+    | fen:r1bqkbnr/pppp1ppp/2B2n2/4p3/4P3/5N2/PPPP1PPP/RNBQK2R:
+  figcaption Fou prend cavalier, qui est replacé en g8.
+
+p Pour poser une pièce, cliquez simplement sur une case vide.
+
+h3 Source
+
+p
+  | La 
+  a(href="https://www.chessvariants.com/difftaking.dir/replacement.html")
+    | variante Remplacement
+  | &nbsp;sur chessvariants.com.
diff --git a/client/src/translations/rules/Pocketknight/en.pug b/client/src/translations/rules/Pocketknight/en.pug
new file mode 100644 (file)
index 0000000..fda5baf
--- /dev/null
@@ -0,0 +1,18 @@
+p.boxed
+  | Each player has a knight in pocket, which can be dropped at any moment.
+
+p.
+  To use your pocket knight, "capture" the enemy king with yours,
+  and then click on any empty square.
+
+figure.diagram-container
+  .diagram
+    | fen:r1bqkbnr/ppNnpppp/2p5/3p4/2PP1B2/8/PP2PPPP/RN1QKBNR:
+  figcaption After N@c7+, black must give the queen.
+
+h3 Source
+
+p
+  a(href="https://www.chessvariants.com/other.dir/pocket.html")
+    | Pocket Knight
+  | &nbsp;on chessvariants.com.
diff --git a/client/src/translations/rules/Pocketknight/es.pug b/client/src/translations/rules/Pocketknight/es.pug
new file mode 100644 (file)
index 0000000..dbb6b0d
--- /dev/null
@@ -0,0 +1,19 @@
+p.boxed
+  | Cada jugador tiene un caballo en su bolsillo,
+  | que puede poner en cualquier momento.
+
+p.
+  Para usar tu caballo de bolsillo, "captura" al rey contrario con el
+  su, luego haga clic en cualquier casilla vacía.
+
+figure.diagram-container
+  .diagram
+    | fen:r1bqkbnr/ppNnpppp/2p5/3p4/2PP1B2/8/PP2PPPP/RN1QKBNR:
+  figcaption Después de N@c7+, las negras deben dar la dama.
+
+h3 Fuente
+
+p
+  a(href="https://www.chessvariants.com/other.dir/pocket.html")
+    | Pocket Knight
+  | &nbsp;en chessvariants.com.
diff --git a/client/src/translations/rules/Pocketknight/fr.pug b/client/src/translations/rules/Pocketknight/fr.pug
new file mode 100644 (file)
index 0000000..ea3a9db
--- /dev/null
@@ -0,0 +1,18 @@
+p.boxed
+  | Chaque jouer a un cavalier en poche, qu'il peut poser à tout moment.
+
+p.
+  Pour utiliser votre cavalier de poche, "capturez" le roi adverse avec le
+  votre, puis cliquez sur n'importe quelle case vide.
+
+figure.diagram-container
+  .diagram
+    | fen:r1bqkbnr/ppNnpppp/2p5/3p4/2PP1B2/8/PP2PPPP/RN1QKBNR:
+  figcaption Après N@c7+, les noirs doivent donner la dame.
+
+h3 Source
+
+p
+  a(href="https://www.chessvariants.com/other.dir/pocket.html")
+    | Pocket Knight
+  | &nbsp;sur chessvariants.com.
index 47e7375..4556f07 100644 (file)
@@ -487,31 +487,6 @@ export class Checkered1Rules extends ChessRules {
     this.postPlay(move);
   }
 
     this.postPlay(move);
   }
 
-  updateCastleFlags(move, piece) {
-    const c = V.GetOppCol(this.turn);
-    const firstRank = (c == "w" ? V.size.x - 1 : 0);
-    // Update castling flags if rooks are moved
-    const oppCol = this.turn;
-    const oppFirstRank = V.size.x - 1 - firstRank;
-    if (piece == V.KING && move.appear.length > 0)
-      this.castleFlags[c] = [V.size.y, V.size.y];
-    else if (
-      move.start.x == firstRank && //our rook moves?
-      this.castleFlags[c].includes(move.start.y)
-    ) {
-      const flagIdx = (move.start.y == this.castleFlags[c][0] ? 0 : 1);
-      this.castleFlags[c][flagIdx] = V.size.y;
-    }
-    // NOTE: not "else if" because a rook could take an opposing rook
-    if (
-      move.end.x == oppFirstRank && //we took opponent rook?
-      this.castleFlags[oppCol].includes(move.end.y)
-    ) {
-      const flagIdx = (move.end.y == this.castleFlags[oppCol][0] ? 0 : 1);
-      this.castleFlags[oppCol][flagIdx] = V.size.y;
-    }
-  }
-
   postPlay(move) {
     if (move.appear.length == 0 && move.vanish.length == 0) {
       this.stage = 2;
   postPlay(move) {
     if (move.appear.length == 0 && move.vanish.length == 0) {
       this.stage = 2;
@@ -524,7 +499,7 @@ export class Checkered1Rules extends ChessRules {
         this.kingPos[c][0] = move.appear[0].x;
         this.kingPos[c][1] = move.appear[0].y;
       }
         this.kingPos[c][0] = move.appear[0].x;
         this.kingPos[c][1] = move.appear[0].y;
       }
-      this.updateCastleFlags(move, piece);
+      super.updateCastleFlags(move, piece);
       // Does this move turn off a 2-squares pawn flag?
       if ([1, 6].includes(move.start.x) && move.vanish[0].p == V.PAWN)
         this.pawnFlags[move.start.x == 6 ? "w" : "b"][move.start.y] = false;
       // Does this move turn off a 2-squares pawn flag?
       if ([1, 6].includes(move.start.x) && move.vanish[0].p == V.PAWN)
         this.pawnFlags[move.start.x == 6 ? "w" : "b"][move.start.y] = false;
@@ -609,7 +584,7 @@ export class Checkered1Rules extends ChessRules {
   }
 
   static GenRandInitFen(randomness) {
   }
 
   static GenRandInitFen(randomness) {
-    // Add 16 pawns flags + empty cmovei + stage == 1:
+    // Add 16 pawns flags + empty cmove + stage == 1:
     return ChessRules.GenRandInitFen(randomness)
       .slice(0, -2) + "1111111111111111 - - 1";
   }
     return ChessRules.GenRandInitFen(randomness)
       .slice(0, -2) + "1111111111111111 - - 1";
   }
index cdb02c6..dbc19c2 100644 (file)
@@ -738,13 +738,6 @@ export class DynamoRules extends ChessRules {
     return potentialMoves;
   }
 
     return potentialMoves;
   }
 
-  getCurrentScore() {
-    if (this.subTurn == 2)
-      // Move not over
-      return "*";
-    return super.getCurrentScore();
-  }
-
   doClick(square) {
     // A click to promote a piece on subTurn 2 would trigger this.
     // For now it would then return [NaN, NaN] because surrounding squares
   doClick(square) {
     // A click to promote a piece on subTurn 2 would trigger this.
     // For now it would then return [NaN, NaN] because surrounding squares
diff --git a/client/src/variants/Madhouse.js b/client/src/variants/Madhouse.js
new file mode 100644 (file)
index 0000000..c157437
--- /dev/null
@@ -0,0 +1,257 @@
+import { ChessRules, Move, PiPo } from "@/base_rules";
+import { randInt } from "@/utils/alea";
+
+export class MadhouseRules extends ChessRules {
+  hoverHighlight(x, y) {
+    // Testing move validity results in an infinite update loop.
+    // TODO: find a way to test validity anyway.
+    return (this.subTurn == 2 && this.board[x][y] == V.EMPTY);
+  }
+
+  setOtherVariables(fen) {
+    super.setOtherVariables(fen);
+    this.subTurn = 1;
+    this.firstMove = [];
+  }
+
+  getPotentialMovesFrom([x, y]) {
+    if (this.subTurn == 1) return super.getPotentialMovesFrom([x, y]);
+    // subTurn == 2: a move is a click, not handled here
+    return [];
+  }
+
+  filterValid(moves) {
+    if (this.subTurn == 2) return super.filterValid(moves);
+    const color = this.turn;
+    return moves.filter(m => {
+      this.play(m);
+      let res = false;
+      if (m.vanish.length == 1 || m.appear.length == 2)
+        // Standard check:
+        res = !this.underCheck(color);
+      else {
+        // Capture: find landing square not resulting in check
+        const boundary = (m.vanish[1].p != V.PAWN ? [0, 7] : [1, 6]);
+        const sqColor =
+          m.vanish[1].p == V.BISHOP
+            ? (m.vanish[1].x + m.vanish[1].y) % 2
+            : null;
+        outerLoop: for (let i = boundary[0]; i <= boundary[1]; i++) {
+          for (let j=0; j<8; j++) {
+            if (
+              this.board[i][j] == V.EMPTY &&
+              (!sqColor || (i + j) % 2 == sqColor)
+            ) {
+              const tMove = new Move({
+                appear: [
+                  new PiPo({
+                    x: i,
+                    y: j,
+                    c: m.vanish[1].c,
+                    p: m.vanish[1].p
+                  })
+                ],
+                vanish: [],
+                start: { x: -1, y: -1 }
+              });
+              this.play(tMove);
+              const moveOk = !this.underCheck(color);
+              this.undo(tMove);
+              if (moveOk) {
+                res = true;
+                break outerLoop;
+              }
+            }
+          }
+        }
+      }
+      this.undo(m);
+      return res;
+    });
+  }
+
+  getAllValidMoves() {
+    if (this.subTurn == 1) return super.getAllValidMoves();
+    // Subturn == 2: only replacements
+    let moves = [];
+    const L = this.firstMove.length;
+    const fm = this.firstMove[L - 1];
+    const color = this.turn;
+    const oppCol = V.GetOppCol(color);
+    const boundary = (fm.vanish[1].p != V.PAWN ? [0, 7] : [1, 6]);
+    const sqColor =
+      fm.vanish[1].p == V.BISHOP
+        ? (fm.vanish[1].x + fm.vanish[1].y) % 2
+        : null;
+    for (let i = boundary[0]; i < boundary[1]; i++) {
+      for (let j=0; j<8; j++) {
+        if (
+          this.board[i][j] == V.EMPTY &&
+          (!sqColor || (i + j) % 2 == sqColor)
+        ) {
+          const tMove = new Move({
+            appear: [
+              new PiPo({
+                x: i,
+                y: j,
+                c: oppCol,
+                p: fm.vanish[1].p
+              })
+            ],
+            vanish: [],
+            start: { x: -1, y: -1 }
+          });
+          this.play(tMove);
+          const moveOk = !this.underCheck(color);
+          this.undo(tMove);
+          if (moveOk) moves.push(tMove);
+        }
+      }
+    }
+    return moves;
+  }
+
+  doClick(square) {
+    if (isNaN(square[0])) return null;
+    // If subTurn == 2 && square is empty && !underCheck, then replacement
+    if (this.subTurn == 2 && this.board[square[0]][square[1]] == V.EMPTY) {
+      const L = this.firstMove.length;
+      const fm = this.firstMove[L - 1];
+      const color = this.turn;
+      const oppCol = V.GetOppCol(color);
+      if (
+        (fm.vanish[1].p == V.PAWN && [0, 7].includes(square[0])) ||
+        (
+          fm.vanish[1].p == V.BISHOP &&
+          (square[0] + square[1] + fm.vanish[1].x + fm.vanish[1].y) % 2 != 0
+        )
+      ) {
+        // Pawns cannot be replaced on first or last rank,
+        // bishops must be replaced on same square color.
+        return null;
+      }
+      const tMove = new Move({
+        appear: [
+          new PiPo({
+            x: square[0],
+            y: square[1],
+            c: oppCol,
+            p: fm.vanish[1].p
+          })
+        ],
+        vanish: [],
+        start: { x: -1, y: -1 }
+      });
+      this.play(tMove);
+      const moveOk = !this.underCheck(color);
+      this.undo(tMove);
+      if (moveOk) return tMove;
+    }
+    return null;
+  }
+
+  play(move) {
+    move.flags = JSON.stringify(this.aggregateFlags());
+    if (move.vanish.length > 0) {
+      this.epSquares.push(this.getEpSquare(move));
+      this.firstMove.push(move);
+    }
+    V.PlayOnBoard(this.board, move);
+    if (
+      this.subTurn == 2 ||
+      move.vanish.length == 1 ||
+      move.appear.length == 2
+    ) {
+      this.turn = V.GetOppCol(this.turn);
+      this.subTurn = 1;
+      this.movesCount++;
+    }
+    else this.subTurn = 2;
+    if (move.vanish.length > 0) this.postPlay(move);
+  }
+
+  postPlay(move) {
+    if (move.appear[0].p == V.KING)
+      this.kingPos[move.appear[0].c] = [move.appear[0].x, move.appear[0].y];
+    this.updateCastleFlags(move, move.appear[0].p, move.appear[0].c);
+  }
+
+  undo(move) {
+    this.disaggregateFlags(JSON.parse(move.flags));
+    if (move.vanish.length > 0) {
+      this.epSquares.pop();
+      this.firstMove.pop();
+    }
+    V.UndoOnBoard(this.board, move);
+    if (this.subTurn == 2) this.subTurn = 1;
+    else {
+      this.turn = V.GetOppCol(this.turn);
+      this.movesCount--;
+      this.subTurn = (move.vanish.length > 0 ? 1 : 2);
+    }
+    if (move.vanish.length > 0) super.postUndo(move);
+  }
+
+  getComputerMove() {
+    let moves = this.getAllValidMoves();
+    if (moves.length == 0) return null;
+    // Custom "search" at depth 1 (for now. TODO?)
+    const maxeval = V.INFINITY;
+    const color = this.turn;
+    const initEval = this.evalPosition();
+    moves.forEach(m => {
+      this.play(m);
+      m.eval = (color == "w" ? -1 : 1) * maxeval;
+      if (m.vanish.length == 2 && m.appear.length == 1) {
+        const moves2 = this.getAllValidMoves();
+        m.next = moves2[0];
+        moves2.forEach(m2 => {
+          this.play(m2);
+          const score = this.getCurrentScore();
+          let mvEval = 0;
+          if (["1-0", "0-1"].includes(score))
+            mvEval = (score == "1-0" ? 1 : -1) * maxeval;
+          else if (score == "*")
+            // Add small fluctuations to avoid dropping pieces always on the
+            // first square available.
+            mvEval = initEval + 0.05 - Math.random() / 10;
+          if (
+            (color == 'w' && mvEval > m.eval) ||
+            (color == 'b' && mvEval < m.eval)
+          ) {
+            m.eval = mvEval;
+            m.next = m2;
+          }
+          this.undo(m2);
+        });
+      }
+      else {
+        const score = this.getCurrentScore();
+        if (score != "1/2") {
+          if (score != "*") m.eval = (score == "1-0" ? 1 : -1) * maxeval;
+          else m.eval = this.evalPosition();
+        }
+      }
+      this.undo(m);
+    });
+    moves.sort((a, b) => {
+      return (color == "w" ? 1 : -1) * (b.eval - a.eval);
+    });
+    let candidates = [0];
+    for (let i = 1; i < moves.length && moves[i].eval == moves[0].eval; i++)
+      candidates.push(i);
+    const mIdx = candidates[randInt(candidates.length)];
+    if (!moves[mIdx].next) return moves[mIdx];
+    const move2 = moves[mIdx].next;
+    delete moves[mIdx]["next"];
+    return [moves[mIdx], move2];
+  }
+
+  getNotation(move) {
+    if (move.vanish.length > 0) return super.getNotation(move);
+    // Replacement:
+    const piece =
+      move.appear[0].p != V.PAWN ? move.appear[0].p.toUpperCase() : "";
+    return piece + "@" + V.CoordsToSquare(move.end);
+  }
+};
diff --git a/client/src/variants/Pocketknight.js b/client/src/variants/Pocketknight.js
new file mode 100644 (file)
index 0000000..6aa94aa
--- /dev/null
@@ -0,0 +1,263 @@
+import { ChessRules, Move, PiPo } from "@/base_rules";
+import { randInt } from "@/utils/alea";
+
+export class PocketknightRules extends ChessRules {
+  hoverHighlight(x, y) {
+    // Testing move validity results in an infinite update loop.
+    // TODO: find a way to test validity anyway.
+    return (this.subTurn == 2 && this.board[x][y] == V.EMPTY);
+  }
+
+  static IsGoodFlags(flags) {
+    // 4 for castle + 2 for knights
+    return !!flags.match(/^[a-z]{4,4}[01]{2,2}$/);
+  }
+
+  setFlags(fenflags) {
+    super.setFlags(fenflags); //castleFlags
+    this.knightFlags = fenflags.substr(4).split("").map(e => e == "1");
+  }
+
+  aggregateFlags() {
+    return [this.castleFlags, this.knightFlags];
+  }
+
+  disaggregateFlags(flags) {
+    this.castleFlags = flags[0];
+    this.knightFlags = flags[1];
+  }
+
+  setOtherVariables(fen) {
+    super.setOtherVariables(fen);
+    this.subTurn = 1;
+  }
+
+  static GenRandInitFen(randomness) {
+    // Add 2 knight flags
+    return ChessRules.GenRandInitFen(randomness)
+      .slice(0, -2) + "11 -";
+  }
+
+  getFlagsFen() {
+    return (
+      super.getFlagsFen() + this.knightFlags.map(e => e ? "1" : "0").join("")
+    );
+  }
+
+  getPotentialMovesFrom([x, y]) {
+    if (this.subTurn == 1) {
+      let moves = super.getPotentialMovesFrom([x, y]);
+      // If flag allow it, add "king capture"
+      if (
+        this.knightFlags[this.turn == 'w' ? 0 : 1] &&
+        this.getPiece(x, y) == V.KING
+      ) {
+        const kp = this.kingPos[V.GetOppCol(this.turn)];
+        moves.push(
+          new Move({
+            appear: [],
+            vanish: [],
+            start: { x: x, y: y },
+            end: { x: kp[0], y: kp[1] }
+          })
+        );
+      }
+      return moves;
+    }
+    // subTurn == 2: a move is a click, not handled here
+    return [];
+  }
+
+  filterValid(moves) {
+    if (this.subTurn == 2) return super.filterValid(moves);
+    const color = this.turn;
+    return moves.filter(m => {
+      this.play(m);
+      let res = false;
+      if (m.appear.length > 0)
+        // Standard check:
+        res = !this.underCheck(color);
+      else {
+        // "Capture king": find landing square not resulting in check
+        outerLoop: for (let i=0; i<8; i++) {
+          for (let j=0; j<8; j++) {
+            if (this.board[i][j] == V.EMPTY) {
+              const tMove = new Move({
+                appear: [
+                  new PiPo({
+                    x: i,
+                    y: j,
+                    c: color,
+                    p: V.KNIGHT
+                  })
+                ],
+                vanish: [],
+                start: { x: -1, y: -1 }
+              });
+              this.play(tMove);
+              const moveOk = !this.underCheck(color);
+              this.undo(tMove);
+              if (moveOk) {
+                res = true;
+                break outerLoop;
+              }
+            }
+          }
+        }
+      }
+      this.undo(m);
+      return res;
+    });
+  }
+
+  getAllValidMoves() {
+    if (this.subTurn == 1) return super.getAllValidMoves();
+    // Subturn == 2: only knight landings
+    let moves = [];
+    const color = this.turn;
+    for (let i=0; i<8; i++) {
+      for (let j=0; j<8; j++) {
+        if (this.board[i][j] == V.EMPTY) {
+          const tMove = new Move({
+            appear: [
+              new PiPo({
+                x: i,
+                y: j,
+                c: color,
+                p: V.KNIGHT
+              })
+            ],
+            vanish: [],
+            start: { x: -1, y: -1 }
+          });
+          this.play(tMove);
+          const moveOk = !this.underCheck(color);
+          this.undo(tMove);
+          if (moveOk) moves.push(tMove);
+        }
+      }
+    }
+    return moves;
+  }
+
+  doClick(square) {
+    if (isNaN(square[0])) return null;
+    // If subTurn == 2 && square is empty && !underCheck, then drop
+    if (this.subTurn == 2 && this.board[square[0]][square[1]] == V.EMPTY) {
+      const color = this.turn;
+      const tMove = new Move({
+        appear: [
+          new PiPo({
+            x: square[0],
+            y: square[1],
+            c: color,
+            p: V.KNIGHT
+          })
+        ],
+        vanish: [],
+        start: { x: -1, y: -1 }
+      });
+      this.play(tMove);
+      const moveOk = !this.underCheck(color);
+      this.undo(tMove);
+      if (moveOk) return tMove;
+    }
+    return null;
+  }
+
+  play(move) {
+    move.flags = JSON.stringify(this.aggregateFlags());
+    if (move.appear.length > 0) {
+      // Usual case or knight landing
+      if (move.vanish.length > 0) this.epSquares.push(this.getEpSquare(move));
+      else this.subTurn = 1;
+      this.turn = V.GetOppCol(this.turn);
+      this.movesCount++;
+      V.PlayOnBoard(this.board, move);
+      if (move.vanish.length > 0) this.postPlay(move);
+    }
+    else {
+      // "king capture"
+      this.subTurn = 2;
+      this.knightFlags[this.turn == 'w' ? 0 : 1] = false;
+    }
+  }
+
+  undo(move) {
+    this.disaggregateFlags(JSON.parse(move.flags));
+    if (move.appear.length > 0) {
+      if (move.vanish.length > 0) this.epSquares.pop();
+      else this.subTurn = 2;
+      this.turn = V.GetOppCol(this.turn);
+      this.movesCount--;
+      V.UndoOnBoard(this.board, move);
+      if (move.vanish.length > 0) this.postUndo(move);
+    }
+    else this.subTurn = 1;
+  }
+
+  getComputerMove() {
+    let moves = this.getAllValidMoves();
+    if (moves.length == 0) return null;
+    // Custom "search" at depth 1 (for now. TODO?)
+    const maxeval = V.INFINITY;
+    const color = this.turn;
+    const initEval = this.evalPosition();
+    moves.forEach(m => {
+      this.play(m);
+      m.eval = (color == "w" ? -1 : 1) * maxeval;
+      if (m.appear.length == 0) {
+        const moves2 = this.getAllValidMoves();
+        m.next = moves2[0];
+        moves2.forEach(m2 => {
+          this.play(m2);
+          const score = this.getCurrentScore();
+          let mvEval = 0;
+          if (["1-0", "0-1"].includes(score))
+            mvEval = (score == "1-0" ? 1 : -1) * maxeval;
+          else if (score == "*")
+            // Add small fluctuations to avoid dropping pieces always on the
+            // first square available.
+            mvEval = initEval + 0.05 - Math.random() / 10;
+          if (
+            (color == 'w' && mvEval > m.eval) ||
+            (color == 'b' && mvEval < m.eval)
+          ) {
+            m.eval = mvEval;
+            m.next = m2;
+          }
+          this.undo(m2);
+        });
+      }
+      else {
+        const score = this.getCurrentScore();
+        if (score != "1/2") {
+          if (score != "*") m.eval = (score == "1-0" ? 1 : -1) * maxeval;
+          else m.eval = this.evalPosition();
+        }
+      }
+      this.undo(m);
+    });
+    moves.sort((a, b) => {
+      return (color == "w" ? 1 : -1) * (b.eval - a.eval);
+    });
+    let candidates = [0];
+    for (let i = 1; i < moves.length && moves[i].eval == moves[0].eval; i++)
+      candidates.push(i);
+    const mIdx = candidates[randInt(candidates.length)];
+    if (!moves[mIdx].next) return moves[mIdx];
+    const move2 = moves[mIdx].next;
+    delete moves[mIdx]["next"];
+    return [moves[mIdx], move2];
+  }
+
+  getNotation(move) {
+    if (move.vanish.length > 0)
+      return super.getNotation(move);
+    if (move.appear.length == 0)
+      // "king capture"
+      return "-";
+    // Knight landing:
+    return "N@" + V.CoordsToSquare(move.end);
+  }
+};
index 7226863..6cea420 100644 (file)
@@ -131,7 +131,7 @@ export class TakenmakeRules extends ChessRules {
       this.kingPos[c][0] = move.appear[0].x;
       this.kingPos[c][1] = move.appear[0].y;
     }
       this.kingPos[c][0] = move.appear[0].x;
       this.kingPos[c][1] = move.appear[0].y;
     }
-    super.updateCastleFlags(move, piece);
+    super.updateCastleFlags(move, piece, c);
   }
 
   undo(move) {
   }
 
   undo(move) {
index 6cf14a8..eadaf13 100644 (file)
@@ -135,13 +135,6 @@ export class TeleportRules extends ChessRules {
     return super.underCheck(color);
   }
 
     return super.underCheck(color);
   }
 
-  getCurrentScore() {
-    if (this.subTurn == 2)
-      // Move not over
-      return "*";
-    return super.getCurrentScore();
-  }
-
   doClick(square) {
     if (isNaN(square[0])) return null;
     // If subTurn == 2 && square is empty && !underCheck, then teleport
   doClick(square) {
     if (isNaN(square[0])) return null;
     // If subTurn == 2 && square is empty && !underCheck, then teleport
@@ -228,28 +221,8 @@ export class TeleportRules extends ChessRules {
         }
       }
     }
         }
       }
     }
-    else {
-      // Normal move
-      const firstRank = (c == "w" ? V.size.x - 1 : 0);
-      const oppCol = V.GetOppCol(c);
-      const oppFirstRank = V.size.x - 1 - firstRank;
-      if (move.vanish[0].p == V.KING && move.appear.length > 0)
-        this.castleFlags[c] = [V.size.y, V.size.y];
-      else if (
-        move.start.x == firstRank &&
-        this.castleFlags[c].includes(move.start.y)
-      ) {
-        const flagIdx = (move.start.y == this.castleFlags[c][0] ? 0 : 1);
-        this.castleFlags[c][flagIdx] = V.size.y;
-      }
-      if (
-        move.end.x == oppFirstRank &&
-        this.castleFlags[oppCol].includes(move.end.y)
-      ) {
-        const flagIdx = (move.end.y == this.castleFlags[oppCol][0] ? 0 : 1);
-        this.castleFlags[oppCol][flagIdx] = V.size.y;
-      }
-    }
+    // Normal check:
+    super.updateCastleFlags(move, move.vanish[0].p, c);
   }
 
   undo(move) {
   }
 
   undo(move) {
index 60578f8..a44a3ab 100644 (file)
@@ -63,6 +63,7 @@ insert or ignore into Variants (name, description) values
   ('Koopa', 'Stun & kick pieces'),
   ('Koth', 'King of the Hill'),
   ('Losers', 'Get strong at self-mate'),
   ('Koopa', 'Stun & kick pieces'),
   ('Koth', 'King of the Hill'),
   ('Losers', 'Get strong at self-mate'),
+  ('Madhouse', 'Rearrange enemy pieces'),
   ('Madrasi', 'Paralyzed pieces'),
   ('Magnetic', 'Laws of attraction'),
   ('Makruk', 'Thai Chess'),
   ('Madrasi', 'Paralyzed pieces'),
   ('Magnetic', 'Laws of attraction'),
   ('Makruk', 'Thai Chess'),
@@ -78,6 +79,7 @@ insert or ignore into Variants (name, description) values
   ('Parachute', 'Landing on the board'),
   ('Pawns', 'Reach the last rank'),
   ('Perfect', 'Powerful pieces'),
   ('Parachute', 'Landing on the board'),
   ('Pawns', 'Reach the last rank'),
   ('Perfect', 'Powerful pieces'),
+  ('Pocketknight', 'Knight in pocket'),
   ('Racingkings', 'Kings cross the 8x8 board'),
   ('Rampage', 'Move under cover'),
   ('Rifle', 'Shoot pieces'),
   ('Racingkings', 'Kings cross the 8x8 board'),
   ('Rampage', 'Move under cover'),
   ('Rifle', 'Shoot pieces'),