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"?
 
-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/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
index d219f78..9ee506e 100644 (file)
@@ -1169,8 +1169,8 @@ export const ChessRules = class ChessRules {
     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;
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();
-            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") {
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",
+  "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",
+  "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)",
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",
+  "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",
+  "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)",
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",
+  "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",
+  "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)",
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);
   }
 
-  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;
@@ -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.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;
@@ -609,7 +584,7 @@ export class Checkered1Rules extends ChessRules {
   }
 
   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";
   }
index cdb02c6..dbc19c2 100644 (file)
@@ -738,13 +738,6 @@ export class DynamoRules extends ChessRules {
     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
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;
     }
-    super.updateCastleFlags(move, piece);
+    super.updateCastleFlags(move, piece, c);
   }
 
   undo(move) {
index 6cf14a8..eadaf13 100644 (file)
@@ -135,13 +135,6 @@ export class TeleportRules extends ChessRules {
     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
@@ -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) {
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'),
+  ('Madhouse', 'Rearrange enemy pieces'),
   ('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'),
+  ('Pocketknight', 'Knight in pocket'),
   ('Racingkings', 'Kings cross the 8x8 board'),
   ('Rampage', 'Move under cover'),
   ('Rifle', 'Shoot pieces'),