A few small fixes + add Monster variant
authorBenjamin Auder <benjamin.auder@somewhere>
Sun, 29 Mar 2020 02:33:46 +0000 (04:33 +0200)
committerBenjamin Auder <benjamin.auder@somewhere>
Sun, 29 Mar 2020 02:33:46 +0000 (04:33 +0200)
13 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/Monster/en.pug [new file with mode: 0644]
client/src/translations/rules/Monster/es.pug [new file with mode: 0644]
client/src/translations/rules/Monster/fr.pug [new file with mode: 0644]
client/src/variants/Chess960.js
client/src/variants/Dynamo.js
client/src/variants/Monster.js [new file with mode: 0644]
server/db/populate.sql

diff --git a/TODO b/TODO
index 6791298..ecc877a 100644 (file)
--- a/TODO
+++ b/TODO
@@ -1,12 +1,9 @@
 # New variants
-Finish first https://www.chessvariants.com/mvopponent.dir/dynamo.html
+Finish https://www.chessvariants.com/mvopponent.dir/dynamo.html
 https://echekk.fr/spip.php?page=article&id_article=599
 
-Monster, Horde, Colorbound
+Colorbound
 https://www.chessvariants.com/d.betza/chessvar/dan/colclob.html
-https://lichess.org/analysis/horde#0
-https://greenchess.net/rules.php?v=monster
-https://en.wikipedia.org/wiki/Monster_chess
 https://en.wikipedia.org/wiki/Grotesque_(chess) (fun)
 
 Maxima, Interweave, Roccoco
index d35a918..22f57d5 100644 (file)
@@ -862,7 +862,9 @@ export const ChessRules = class ChessRules {
       i = y;
       do {
         if (
-          (!castleInCheck && this.isAttacked([x, i], oppCol)) ||
+          // NOTE: "castling" arg is used by some variants (Monster),
+          // where "isAttacked" is overloaded in an infinite-recursive way.
+          (!castleInCheck && this.isAttacked([x, i], oppCol, "castling")) ||
           (this.board[x][i] != V.EMPTY &&
             // NOTE: next check is enough, because of chessboard constraints
             (this.getColor(x, i) != c ||
@@ -882,9 +884,12 @@ export const ChessRules = class ChessRules {
       // Nothing on final squares, except maybe king and castling rook?
       for (i = 0; i < 2; i++) {
         if (
+          finalSquares[castleSide][i] != rookPos &&
           this.board[x][finalSquares[castleSide][i]] != V.EMPTY &&
-          this.getPiece(x, finalSquares[castleSide][i]) != V.KING &&
-          finalSquares[castleSide][i] != rookPos
+          (
+            this.getPiece(x, finalSquares[castleSide][i]) != V.KING ||
+            this.getColor(x, finalSquares[castleSide][i]) != c
+          )
         ) {
           continue castlingCheck;
         }
@@ -942,9 +947,7 @@ export const ChessRules = class ChessRules {
     });
   }
 
-  // Search for all valid moves considering current turn
-  // (for engine and game end)
-  getAllValidMoves() {
+  getAllPotentialMoves() {
     const color = this.turn;
     let potentialMoves = [];
     for (let i = 0; i < V.size.x; i++) {
@@ -957,7 +960,13 @@ export const ChessRules = class ChessRules {
         }
       }
     }
-    return this.filterValid(potentialMoves);
+    return potentialMoves;
+  }
+
+  // Search for all valid moves considering current turn
+  // (for engine and game end)
+  getAllValidMoves() {
+    return this.filterValid(this.getAllPotentialMoves());
   }
 
   // Stop at the first move found
index 51d087a..1ec412e 100644 (file)
@@ -228,10 +228,8 @@ export default {
       this.incheck = this.vr.getCheckSquares(this.vr.turn);
       const score = this.vr.getCurrentScore();
       if (L > 0 && this.moves[L - 1].notation != "...") {
-        if (["1-0","0-1"].includes(score))
-          this.moves[L - 1].notation += "#";
-        else if (this.vr.getCheckSquares(this.vr.turn).length > 0)
-          this.moves[L - 1].notation += "+";
+        if (["1-0","0-1"].includes(score)) this.moves[L - 1].notation += "#";
+        else if (this.incheck.length > 0) this.moves[L - 1].notation += "+";
       }
     },
     positionCursorTo: function(index) {
@@ -434,10 +432,8 @@ export default {
       const computeScore = () => {
         const score = this.vr.getCurrentScore();
         if (!navigate) {
-          if (["1-0","0-1"].includes(score))
-            this.lastMove.notation += "#";
-          else if (this.vr.getCheckSquares(this.vr.turn).length > 0)
-            this.lastMove.notation += "+";
+          if (["1-0","0-1"].includes(score)) this.lastMove.notation += "#";
+          else if (this.incheck.length > 0) this.lastMove.notation += "+";
         }
         if (score != "*" && this.game.mode == "analyze") {
           const message = getScoreMessage(score);
index 0490020..86b75dd 100644 (file)
@@ -169,6 +169,7 @@ export const translations = {
   "Captures reborn": "Captures reborn",
   "Change colors": "Change colors",
   "Dangerous collisions": "Dangerous collisions",
+  "Double moves": "Double moves",
   "Each piece is unique": "Each piece is unique",
   "Exotic captures": "Exotic captures",
   "Explosive captures": "Explosive captures",
@@ -191,7 +192,6 @@ export const translations = {
   "Mongolian Horde": "Mongolian Horde",
   "Move like a knight (v1)": "Move like a knight (v1)",
   "Move like a knight (v2)": "Move like a knight (v2)",
-  "Move twice": "Move twice",
   "Neverending rows": "Neverending rows",
   "No-check mode": "No-check mode",
   "Pawns move diagonally": "Pawns move diagonally",
@@ -212,5 +212,6 @@ export const translations = {
   "Transform an essay": "Transform an essay",
   "Two kings": "Two kings",
   "Two royal pieces": "Two royal pieces",
-  "Unidentified pieces": "Unidentified pieces"
+  "Unidentified pieces": "Unidentified pieces",
+  "White move twice": "White move twice"
 };
index 32282e2..1cd0b52 100644 (file)
@@ -169,6 +169,7 @@ export const translations = {
   "Captures reborn": "Las capturas renacen",
   "Change colors": "Cambiar colores",
   "Dangerous collisions": "Colisiones peligrosas",
+  "Double moves": "Jugadas doble",
   "Each piece is unique": "Cada pieza es única",
   "Exotic captures": "Capturas exóticas",
   "Explosive captures": "Capturas explosivas",
@@ -191,7 +192,6 @@ export const translations = {
   "Mongolian Horde": "Horda mongol",
   "Move like a knight (v1)": "Moverse como un caballo (v1)",
   "Move like a knight (v2)": "Moverse como un caballo (v2)",
-  "Move twice": "Mover dos veces",
   "Neverending rows": "Filas interminables",
   "No-check mode": "Modo sin jaque",
   "Pawns move diagonally": "Peones se mueven en diagonal",
@@ -212,5 +212,6 @@ export const translations = {
   "Transform an essay": "Transformar un ensayo",
   "Two kings": "Dos reyes",
   "Two royal pieces": "Dos piezas reales",
-  "Unidentified pieces": "Piezas no identificadas"
+  "Unidentified pieces": "Piezas no identificadas",
+  "White move twice": "Las blancas juegan dos veces"
 };
index 0e51684..f4c956f 100644 (file)
@@ -169,6 +169,7 @@ export const translations = {
   "Captures reborn": "Les captures renaissent",
   "Change colors": "Changer les couleurs",
   "Dangerous collisions": "Collisions dangeureuses",
+  "Double moves": "Coups doubles",
   "Each piece is unique": "Chaque pièce est unique",
   "Exotic captures": "Captures exotiques",
   "Explosive captures": "Captures explosives",
@@ -191,7 +192,6 @@ export const translations = {
   "Mongolian Horde": "Horde mongole",
   "Move like a knight (v1)": "Bouger comme un cavalier (v1)",
   "Move like a knight (v2)": "Bouger comme un cavalier (v2)",
-  "Move twice": "Jouer deux coups",
   "Neverending rows": "Rangées sans fin",
   "No-check mode": "Mode sans échec",
   "Pawns move diagonally": "Les pions vont en diagonale",
@@ -212,5 +212,6 @@ export const translations = {
   "Transform an essay": "Transformer un essai",
   "Two kings": "Deux rois",
   "Two royal pieces": "Deux pièces royales",
-  "Unidentified pieces": "Pièces non identifiées"
+  "Unidentified pieces": "Pièces non identifiées",
+  "White move twice": "Les blancs jouent deux fois"
 };
diff --git a/client/src/translations/rules/Monster/en.pug b/client/src/translations/rules/Monster/en.pug
new file mode 100644 (file)
index 0000000..1401cda
--- /dev/null
@@ -0,0 +1,49 @@
+p.boxed.
+  White has only four pawns and the king, but move twice at each turn.
+
+figure.diagram-container
+  .diagram
+    | fen:rnbqkbnr/pppppppp/8/8/8/8/2PPPP2/4K3:
+  figcaption Standard deterministic position
+
+p.
+  The white army can appear much too small, but the power to move twice in a
+  row shouldn't be underestimated. At each turn white plays two moves with
+  only one constraint: do not be under check in the end.
+  So if the white king attacks a defended piece, he can take it anyway by
+  coming back on its initial square on (sub)move 2.
+
+figure.diagram-container
+  .diagram.diag12
+    | fen:rbbknn1r/1p2pp1p/2p3qK/p2p2p1/2PPP3/8/5P2/8:
+  .diagram.diag22
+    | fen:3rq3/1p4p1/1k1pKp2/3P1P1n/p7/5n2/8/8:
+  figcaption Left: not a checkmate! Right: a "Monster-checkmate".
+
+p
+  | The diagram position on the left looks pretty much like a checkmate,
+  | but white can take the queen and come back to the h6 square. Finally,
+  | white can mate in an unusual way, like the following diagram found 
+  a(href="https://en.wikipedia.org/wiki/Monster_chess") on Wikipedia
+  | . There is no way for the black king to avoid being captured since white
+  | plays twice (the threat is 2.d7,dxe8).
+
+figure.diagram-container
+  .diagram.diag12
+    | fen:4k3/8/4P3/8/3P4/8/2q5/7K:
+  .diagram.diag22
+    | fen:4k3/8/3PP3/8/8/8/2q5/7K:
+  figcaption Left: before 1.d5,d6. Right: after this move:, it's checkmate.
+
+h3 More information
+
+p
+  | Ralph Betza analyses this variant and the double move on 
+  a(href="https://www.chessvariants.com/d.betza/chessvar/muenster.html")
+    | this page
+  | . There seems to be a common belief that black should win with accurate
+  | play, but it's clearly hard to demonstrate. And if someone can show a
+  | winning strategy, we'll add some white material to balance this game.
+  | Meanwhile, the variant is also playable 
+  a(href="https://greenchess.net/rules.php?v=monster") on greenchess.net
+  | .
diff --git a/client/src/translations/rules/Monster/es.pug b/client/src/translations/rules/Monster/es.pug
new file mode 100644 (file)
index 0000000..87776c4
--- /dev/null
@@ -0,0 +1,54 @@
+p.boxed.
+  Las blancas solo tienen cuatro peones y un rey, pero hacen dos movimientos
+  a cada turno.
+
+figure.diagram-container
+  .diagram
+    | fen:rnbqkbnr/pppppppp/8/8/8/8/2PPPP2/4K3:
+  figcaption Posición inicial estándar
+
+p.
+  El ejército blanco puede parecer demasiado pequeño, pero el poder de jugar
+  dos veces seguidas y no debe subestimarse. En cada turno las blancas juegan
+  dos jugadas con la única restricción de no estar en jaque hasta el final.
+  Entonces, si tu rey blanco ataca una habitación protegida, él puede
+  tómalo de todos modos y luego regresa a tu caso inicial en el segundo
+  (sub)movimiento.
+
+figure.diagram-container
+  .diagram.diag12
+    | fen:rbbknn1r/1p2pp1p/2p3qK/p2p2p1/2PPP3/8/5P2/8:
+  .diagram.diag22
+    | fen:3rq3/1p4p1/1k1pKp2/3P1P1n/p7/5n2/8/8:
+  figcaption Izquierda: no es una jaque mate! Derecha: un "Monster-mate".
+
+p
+  | La posición del diagrama de la izquierda se ve bien como un jaque mate,
+  | pero las blancas pueden tomar la dama y volver llevar en h6. Finalmente,
+  | las blancas a veces pueden dar jaque mate de una manera inusual como se
+  | muestra en el siguiente diagrama encontrado 
+  a(href="https://en.wikipedia.org/wiki/Monster_chess") en Wikipedia
+  | . El rey negro no tiene forma de escapar de la captura porque las blancas
+  | hacen dos movimientos (la amenaza es 2.d7, dxe8).
+
+figure.diagram-container
+  .diagram.diag12
+    | fen:4k3/8/4P3/8/3P4/8/2q5/7K:
+  .diagram.diag22
+    | fen:4k3/8/3PP3/8/8/8/2q5/7K:
+  figcaption.
+    Izquierda: antes de 1.d5,d6.
+    Derecha: después de esta jugada, es jaque mate.
+
+h3 Más información
+
+p
+  | Ralph Betza analiza esta variante y el doble movimiento en 
+  a(href="https://www.chessvariants.com/d.betza/chessvar/muenster.html")
+    | esta página
+  | . La opinión general parece indicar que las negras deben ganar con un
+  | juego precisa, pero es claramente difícil de demostrar. Y si es necesario,
+  | se podría agregar material a las blancas para reequilibrar el juego.
+  | Dicho esto, la variante también es jugable 
+  a(href="https://greenchess.net/rules.php?v=monster") en greenchess.net
+  | .
diff --git a/client/src/translations/rules/Monster/fr.pug b/client/src/translations/rules/Monster/fr.pug
new file mode 100644 (file)
index 0000000..b97b61c
--- /dev/null
@@ -0,0 +1,51 @@
+p.boxed.
+  Les blancs n'ont que quatre pions et un roi, mais jouent deux coups à chaque
+  tour.
+
+figure.diagram-container
+  .diagram
+    | fen:rnbqkbnr/pppppppp/8/8/8/8/2PPPP2/4K3:
+  figcaption Position initiale standard
+
+p.
+  L'armée blanche peut paraître bien trop réduite, mais le pouvoir de jouer
+  deux fois d'affilée ne doit pas être sous-estimé. À chaque tour les
+  blancs jouent deux coups avec pour seule contrainte de ne pas être en échec
+  à la fin. Ainsi, si le roi blanc attaque une pièce protégée, il peut la
+  prendre quand-même puis revenir sur sa case initiale au second (sous)coup.
+
+figure.diagram-container
+  .diagram.diag12
+    | fen:rbbknn1r/1p2pp1p/2p3qK/p2p2p1/2PPP3/8/5P2/8:
+  .diagram.diag22
+    | fen:3rq3/1p4p1/1k1pKp2/3P1P1n/p7/5n2/8/8:
+  figcaption Gauche : pas un mat ! Droite : un "Monster-mat".
+
+p
+  | La position du diagramme à gauche ressemble bien à un mat, mais les blancs
+  | peuvent prendre la dame et revenir en h6. Enfin, les blancs peuvent parfois
+  | mater d'une manière inhabituelle comme le montre le diagramme suivant
+  | trouvé 
+  a(href="https://en.wikipedia.org/wiki/Monster_chess") sur Wikipedia
+  | . Le roi noir n'a aucun moyen d'échapper à la capture puisque les blancs
+  | jouent deux coups (la menace est 2.d7,dxe8).
+
+figure.diagram-container
+  .diagram.diag12
+    | fen:4k3/8/4P3/8/3P4/8/2q5/7K:
+  .diagram.diag22
+    | fen:4k3/8/3PP3/8/8/8/2q5/7K:
+  figcaption Gauche : avant 1.d5,d6. Droite : après ce coup, c'est mat.
+
+h3 Plus d'information
+
+p
+  | Ralph Betza analyse cette variante et le double coup sur 
+  a(href="https://www.chessvariants.com/d.betza/chessvar/muenster.html")
+    | cette page
+  | . L'avis général semble indiquer que les noirs doivent gagner avec un jeu
+  | précis, mais c'est clairement difficile à démontrer. Et le cas échéant, on
+  | pourrait ajouter du matériel aux blancs pour rééquilibrer le jeu.
+  | Ceci dit, la variante est jouable également 
+  a(href="https://greenchess.net/rules.php?v=monster") sur greenchess.net
+  | .
index 3f6af50..15eb7ec 100644 (file)
@@ -1,4 +1,5 @@
 import { ChessRules } from "@/base_rules";
+
 export class Chess960Rules extends ChessRules {
   // Standard rules
 };
index 9f4cf3f..9d47fd8 100644 (file)
@@ -351,8 +351,7 @@ export class DynamoRules extends ChessRules {
     if (this.subTurn == 2) {
       this.subTurn = 1;
       this.movesCount--;
-    }
-    else {
+    } else {
       // subTurn == 1 (after a move played)
       this.turn = V.GetOppCol(this.turn);
       this.subTurn = 2;
diff --git a/client/src/variants/Monster.js b/client/src/variants/Monster.js
new file mode 100644 (file)
index 0000000..177f71d
--- /dev/null
@@ -0,0 +1,186 @@
+import { ChessRules } from "@/base_rules";
+import { randInt } from "@/utils/alea";
+
+export class MonsterRules extends ChessRules {
+  static IsGoodFlags(flags) {
+    // Only black can castle
+    return !!flags.match(/^[a-z]{2,2}$/);
+  }
+
+  static GenRandInitFen(randomness) {
+    if (randomness == 2) randomness--;
+    const fen = ChessRules.GenRandInitFen(randomness);
+    return (
+      // 26 first chars are 6 rows + 6 slashes
+      fen.substr(0, 26)
+      // En passant available, and "half-castle"
+      .concat("2PPPP2/4K3 w 0 ")
+      .concat(fen.substr(-6, 2))
+      .concat(" -")
+    );
+  }
+
+  getFlagsFen() {
+    return this.castleFlags['b'].map(V.CoordToColumn).join("");
+  }
+
+  setFlags(fenflags) {
+    this.castleFlags = { 'b': [-1, -1] };
+    for (let i = 0; i < 2; i++)
+      this.castleFlags['b'][i] = V.ColumnToCoord(fenflags.charAt(i));
+  }
+
+  setOtherVariables(fen) {
+    super.setOtherVariables(fen);
+    this.subTurn = 1;
+  }
+
+  getPotentialKingMoves([x, y]) {
+    if (this.getColor(x, y) == 'b') return super.getPotentialKingMoves([x, y]);
+    // White doesn't castle:
+    return this.getSlideNJumpMoves(
+      [x, y],
+      V.steps[V.ROOK].concat(V.steps[V.BISHOP]),
+      "oneStep"
+    );
+  }
+
+  isAttacked(sq, color, castling) {
+    const singleMoveAttack = super.isAttacked(sq, color);
+    if (singleMoveAttack) return true;
+    if (color == 'b' || !!castling) return singleMoveAttack;
+    // Attacks by white: double-move allowed
+    const curTurn = this.turn;
+    this.turn = 'w';
+    const w1Moves = super.getAllPotentialMoves();
+    this.turn = curTurn;
+    for (let move of w1Moves) {
+      this.play(move);
+      const res = super.isAttacked(sq, 'w');
+      this.undo(move);
+      if (res) return res;
+    }
+    return false;
+  }
+
+  play(move) {
+    move.flags = JSON.stringify(this.aggregateFlags());
+    if (this.turn == 'b' || this.subTurn == 2)
+      this.epSquares.push(this.getEpSquare(move));
+    else this.epSquares.push(null);
+    V.PlayOnBoard(this.board, move);
+    if (this.turn == 'w') {
+      if (this.subTurn == 1) this.movesCount++;
+      else this.turn = 'b';
+      this.subTurn = 3 - this.subTurn;
+    } else {
+      this.turn = 'w';
+      this.movesCount++;
+    }
+    this.postPlay(move);
+  }
+
+  updateCastleFlags(move, piece) {
+    // Only black can castle:
+    const firstRank = 0;
+    if (piece == V.KING && move.appear[0].c == 'b')
+      this.castleFlags['b'] = [8, 8];
+    else if (
+      move.start.x == firstRank &&
+      this.castleFlags['b'].includes(move.start.y)
+    ) {
+      const flagIdx = (move.start.y == this.castleFlags['b'][0] ? 0 : 1);
+      this.castleFlags['b'][flagIdx] = 8;
+    }
+    else if (
+      move.end.x == firstRank &&
+      this.castleFlags['b'].includes(move.end.y)
+    ) {
+      const flagIdx = (move.end.y == this.castleFlags['b'][0] ? 0 : 1);
+      this.castleFlags['b'][flagIdx] = 8;
+    }
+  }
+
+  postPlay(move) {
+    // Definition of 'c' in base class doesn't work:
+    const c = move.vanish[0].c;
+    const piece = move.vanish[0].p;
+    if (piece == V.KING && move.appear.length > 0) {
+      this.kingPos[c][0] = move.appear[0].x;
+      this.kingPos[c][1] = move.appear[0].y;
+      return;
+    }
+    this.updateCastleFlags(move, piece);
+  }
+
+  undo(move) {
+    this.epSquares.pop();
+    this.disaggregateFlags(JSON.parse(move.flags));
+    V.UndoOnBoard(this.board, move);
+    if (this.turn == 'w') {
+      if (this.subTurn == 2) this.subTurn = 1;
+      else this.turn = 'b';
+      this.movesCount--;
+    } else {
+      this.turn = 'w';
+      this.subTurn = 2;
+    }
+    this.postUndo(move);
+  }
+
+  filterValid(moves) {
+    if (this.turn == 'w' && this.subTurn == 1) {
+      return moves.filter(m1 => {
+        this.play(m1);
+        // NOTE: no recursion because next call will see subTurn == 2
+        const res = super.atLeastOneMove();
+        this.undo(m1);
+        return res;
+      });
+    }
+    return super.filterValid(moves);
+  }
+
+  static get SEARCH_DEPTH() {
+    return 1;
+  }
+
+  getComputerMove() {
+    const color = this.turn;
+    if (color == 'w') {
+      // Generate all sequences of 2-moves
+      const moves1 = this.getAllValidMoves();
+      moves1.forEach(m1 => {
+        m1.eval = -V.INFINITY;
+        m1.move2 = null;
+        this.play(m1);
+        const moves2 = this.getAllValidMoves();
+        moves2.forEach(m2 => {
+          this.play(m2);
+          const eval2 = this.evalPosition();
+          this.undo(m2);
+          if (eval2 > m1.eval) {
+            m1.eval = eval2;
+            m1.move2 = m2;
+          }
+        });
+        this.undo(m1);
+      });
+      moves1.sort((a, b) => b.eval - a.eval);
+      let candidates = [0];
+      for (
+        let i = 1;
+        i < moves1.length && moves1[i].eval == moves1[0].eval;
+        i++
+      ) {
+        candidates.push(i);
+      }
+      const idx = candidates[randInt(candidates.length)];
+      const move2 = moves1[idx].move2;
+      delete moves1[idx]["move2"];
+      return [moves1[idx], move2];
+    }
+    // For black at depth 1, super method is fine:
+    return super.getComputerMove();
+  }
+};
index 147868d..83ba4cc 100644 (file)
@@ -42,7 +42,8 @@ insert or ignore into Variants (name, description) values
   ('Knightrelay2', 'Move like a knight (v2)'),
   ('Losers', 'Get strong at self-mate'),
   ('Magnetic', 'Laws of attraction'),
-  ('Marseille', 'Move twice'),
+  ('Marseille', 'Double moves'),
+  ('Monster', 'White move twice'),
   ('Orda', 'Mongolian Horde'),
   ('Parachute', 'Landing on the board'),
   ('Perfect', 'Powerful pieces'),