From bc0b9205e41c5db0552e4ccf060b945342e36ed0 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Wed, 29 Apr 2020 21:55:45 +0200
Subject: [PATCH] Simplify Monster + Doublemove2. Smoother example games

---
 client/src/components/ComputerGame.vue        |   6 +-
 .../src/translations/rules/Doublemove2/en.pug |  14 +-
 .../src/translations/rules/Doublemove2/es.pug |  19 +--
 .../src/translations/rules/Doublemove2/fr.pug |  15 ++-
 client/src/translations/rules/Monster/en.pug  |   6 +-
 client/src/translations/rules/Monster/es.pug  |   5 +-
 client/src/translations/rules/Monster/fr.pug  |   5 +-
 client/src/variants/Doublemove1.js            |  10 +-
 client/src/variants/Doublemove2.js            |  98 +++++++-------
 client/src/variants/Monster.js                | 122 +++++++++++-------
 client/src/variants/Teleport.js               |   2 -
 11 files changed, 165 insertions(+), 137 deletions(-)

diff --git a/client/src/components/ComputerGame.vue b/client/src/components/ComputerGame.vue
index ad56543e..f611167d 100644
--- a/client/src/components/ComputerGame.vue
+++ b/client/src/components/ComputerGame.vue
@@ -44,7 +44,11 @@ export default {
       setTimeout(() => {
         if (this.currentUrl != document.location.href) return; //page change
         self.$refs["basegame"].play(compMove, "received");
-        self.processMove(compMove);
+        const animationLength =
+          // 250 = length of animation, 500 = delay between sub-moves
+          // TODO: a callback would be cleaner.
+          250 + (Array.isArray(compMove) ? (compMove.length - 1) * 750 : 0);
+        setTimeout(() => self.processMove(compMove), animationLength);
         self.compThink = false;
         if (self.game.score != "*")
           // User action
diff --git a/client/src/translations/rules/Doublemove2/en.pug b/client/src/translations/rules/Doublemove2/en.pug
index c1bda2ee..ca24491f 100644
--- a/client/src/translations/rules/Doublemove2/en.pug
+++ b/client/src/translations/rules/Doublemove2/en.pug
@@ -1,5 +1,5 @@
 p.boxed
-  | Move twice at every turn.
+  | Move twice at every turn. The goal is to capture the king.
 
 p.
   The only difference with orthodox chess is the double-move rule,
@@ -8,7 +8,7 @@ p.
 p.
   At the very first move of the game, white make only one move - as usual.
   However, after that and for all the game each side must play twice at
-  every turn. The goal is to checkmate.
+  every turn (even if the first move captures the enemy king).
 
 figure.diagram-container
   .diagram.diag12
@@ -17,14 +17,12 @@ figure.diagram-container
     | fen:r1bqkbnr/pppp1ppp/2n5/4p3/2B1P3/5Q2/PPPP1PPP/RNB1K1NR:
   figcaption.
     Left: after the moves 1.e4 e5, the 'd' pawn is pinned.
-    Right: the black king is under check.
+    Right: the black king is "under check".
 
 p.
   In the diagram position on the left, after the first black move ...e5,
-  the 'd' pawn is pinned because moving it would allow 2.Bb5,Bxe8 capturing
-  the king.
-  On the right, after 2.Qf3,Bc4 black king is under check because of
-  the options 3.Bxf7,Bxe8 or 3.Qxf7,Qxe8.
+  moving the 'd' pawn would allow 2.Bb5,Bxe8 capturing the king.
+  On the right, after 2.Qf3,Bc4 the threats are 3.Bxf7,Bxe8 and 3.Qxf7,Qxe8.
 
 h3 En-passant capture
 
@@ -50,3 +48,5 @@ p
   | . It is also playable 
   a(href="https://greenchess.net/rules.php?v=double-move") on greenchess
   | .
+
+p Inventor: Fred Galvin (1957)
diff --git a/client/src/translations/rules/Doublemove2/es.pug b/client/src/translations/rules/Doublemove2/es.pug
index aa4751d6..179025b3 100644
--- a/client/src/translations/rules/Doublemove2/es.pug
+++ b/client/src/translations/rules/Doublemove2/es.pug
@@ -1,15 +1,15 @@
 p.boxed
-  | Juega dos jugadas a cada turno.
+  | Juega dos jugadas a cada turno. El objetivo es capturar al rey.
 
 p.
   La única diferencia con el juego ortodoxo es la regla del doble movimiento,
   pero eso afecta mucho el juego
 
 p.
-  Al comienzo del juego, los blancos solo juegan un movimiento, como
+  Al comienzo del juego, las blancas solo juegan un movimiento, como
   en general. Sin embargo, después de eso y por el resto del juego,
-  cada lado debe jugar dos jugadas cada turno.
-  El objetivo es de dar jaque mate.
+  cada lado debe jugar dos jugadas cada turno
+  (incluso si la primera captura al rey contrario).
 
 figure.diagram-container
   .diagram.diag12
@@ -18,14 +18,13 @@ figure.diagram-container
     | fen:r1bqkbnr/pppp1ppp/2n5/4p3/2B1P3/5Q2/PPPP1PPP/RNB1K1NR:
   figcaption.
     Izquierda: después de las jugadas 1.e4 e5, el peón 'd' se clava.
-    Derecha: el rey negro está en jaque.
+    Derecha: el rey negro está "en jaque".
 
 p.
   En la posición del diagrama de la izquierda, después del primer movimiento
-  negro ...e5, el peón 'd' está clavado porque su movimiento autorizaría
-  2.Bb5,Bxe8 captura el rey.
-  A la derecha, después de 2.Qf3,Bc4 el rey negro está en jaque debido a las
-  posibilidades 3.Bxf7,Bxe8 o 3.Qxf7,Qxe8.
+  negro ...e5, un movimiento del peón 'd' autorizaría 2.Bb5,Bxe8 que
+  captura el rey. A la derecha, después de 2.Qf3,Bc4 las amenazas son
+  3.Bxf7,Bxe8 y 3.Qxf7,Qxe8.
 
 h3 Captura en passant
 
@@ -52,3 +51,5 @@ p
   | . También es jugable 
   a(href="https://greenchess.net/rules.php?v=double-move") en greenchess
   | .
+
+p Inventor: Fred Galvin (1957)
diff --git a/client/src/translations/rules/Doublemove2/fr.pug b/client/src/translations/rules/Doublemove2/fr.pug
index 66021864..0dff04f9 100644
--- a/client/src/translations/rules/Doublemove2/fr.pug
+++ b/client/src/translations/rules/Doublemove2/fr.pug
@@ -1,5 +1,5 @@
 p.boxed
-  | Jouez deux coups à chaque tour.
+  | Jouez deux coups à chaque tour. Le but est de capturer le roi.
 
 p.
   La seule différence avec le jeu orthodoxe est la règle du double-coup, mais
@@ -8,7 +8,8 @@ p.
 p.
   Au tout début de la partie les blancs ne jouent qu'un seul coup, comme
   d'habitude. Cependant, après cela et ce pour tout le reste de la partie
-  chaque camp doit jouer deux coups à chaque tour. L'objectif est de mater.
+  chaque camp doit jouer deux coups à chaque tour
+  (même si le premier capture le roi adverse).
 
 figure.diagram-container
   .diagram.diag12
@@ -17,14 +18,12 @@ figure.diagram-container
     | fen:r1bqkbnr/pppp1ppp/2n5/4p3/2B1P3/5Q2/PPPP1PPP/RNB1K1NR:
   figcaption.
     Gauche : après les coups 1.e4 e5, le pion 'd' est cloué.
-    Droite : le roi noir est en échec.
+    Droite : le roi noir est "en échec".
 
 p.
   Dans la position du diagramme à gauche, après le premier coup noir ...e5,
-  le pion 'd' est cloué car son déplacement autoriserait 2.Bb5,Bxe8 capturant
-  le roi.
-  À droite, après 2.Qf3,Bc4 le roi noir est en échec à cause des possibilités
-  3.Bxf7,Bxe8 ou 3.Qxf7,Qxe8.
+  un déplacement du pion 'd' autoriserait 2.Bb5,Bxe8 capturant le roi.
+  À droite, après 2.Qf3,Bc4 les menaces sont 3.Bxf7,Bxe8 et 3.Qxf7,Qxe8.
 
 h3 Prise en passant
 
@@ -50,3 +49,5 @@ p
   | . Elle est jouable également 
   a(href="https://greenchess.net/rules.php?v=double-move") sur greenchess
   | .
+
+p Inventeur : Fred Galvin (1957)
diff --git a/client/src/translations/rules/Monster/en.pug b/client/src/translations/rules/Monster/en.pug
index 8e7061de..d0551d10 100644
--- a/client/src/translations/rules/Monster/en.pug
+++ b/client/src/translations/rules/Monster/en.pug
@@ -8,10 +8,8 @@ figure.diagram-container
 
 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.
+  row shouldn't be underestimated. At each turn white plays two moves
+  without any constraint. The goal is to capture the king.
 
 figure.diagram-container
   .diagram.diag12
diff --git a/client/src/translations/rules/Monster/es.pug b/client/src/translations/rules/Monster/es.pug
index edf1bdf2..f9149f6e 100644
--- a/client/src/translations/rules/Monster/es.pug
+++ b/client/src/translations/rules/Monster/es.pug
@@ -10,10 +10,7 @@ figure.diagram-container
 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.
+  dos jugadas sin ninguna restricción. El objetivo es capturar al rey.
 
 figure.diagram-container
   .diagram.diag12
diff --git a/client/src/translations/rules/Monster/fr.pug b/client/src/translations/rules/Monster/fr.pug
index 757ae3e6..c0b99100 100644
--- a/client/src/translations/rules/Monster/fr.pug
+++ b/client/src/translations/rules/Monster/fr.pug
@@ -10,9 +10,8 @@ figure.diagram-container
 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.
+  blancs jouent deux coups sans aucune contrainte.
+  L'objectif est de capturer le roi.
 
 figure.diagram-container
   .diagram.diag12
diff --git a/client/src/variants/Doublemove1.js b/client/src/variants/Doublemove1.js
index 7d1ee4dc..c30ad0cf 100644
--- a/client/src/variants/Doublemove1.js
+++ b/client/src/variants/Doublemove1.js
@@ -201,7 +201,7 @@ export class Doublemove1Rules extends ChessRules {
       return res;
     };
 
-    let moves11 = this.getAllValidMoves();
+    const moves11 = this.getAllValidMoves();
     let doubleMoves = [];
     // Rank moves using a min-max at depth 2
     for (let i = 0; i < moves11.length; i++) {
@@ -209,13 +209,14 @@ export class Doublemove1Rules extends ChessRules {
       if (this.turn != color) {
         // We gave check with last move: search the best opponent move
         doubleMoves.push({ moves: [moves11[i]], eval: getBestMoveEval() });
-      } else {
+      }
+      else {
         let moves12 = this.getAllValidMoves();
         for (let j = 0; j < moves12.length; j++) {
           this.play(moves12[j]);
           doubleMoves.push({
             moves: [moves11[i], moves12[j]],
-            eval: getBestMoveEval()
+            eval: getBestMoveEval() + 0.05 - Math.random() / 10
           });
           this.undo(moves12[j]);
         }
@@ -223,6 +224,8 @@ export class Doublemove1Rules extends ChessRules {
       this.undo(moves11[i]);
     }
 
+    // TODO: array + sort + candidates logic not required when adding small
+    // fluctuations to the eval function (could also be generalized).
     doubleMoves.sort((a, b) => {
       return (color == "w" ? 1 : -1) * (b.eval - a.eval);
     });
@@ -234,7 +237,6 @@ export class Doublemove1Rules extends ChessRules {
     ) {
       candidates.push(i);
     }
-
     const selected = doubleMoves[randInt(candidates.length)].moves;
     if (selected.length == 1) return selected[0];
     return selected;
diff --git a/client/src/variants/Doublemove2.js b/client/src/variants/Doublemove2.js
index d1afff83..080d13b6 100644
--- a/client/src/variants/Doublemove2.js
+++ b/client/src/variants/Doublemove2.js
@@ -79,40 +79,23 @@ export class Doublemove2Rules extends ChessRules {
     return moves;
   }
 
-  isAttacked(sq, color, castling) {
-    const singleMoveAttack = super.isAttacked(sq, color);
-    if (singleMoveAttack) return true;
-    if (!!castling) {
-      if (this.subTurn == 1)
-        // Castling at move 1 could be done into check
-        return false;
-      return singleMoveAttack;
-    }
-    // Double-move allowed:
-    const curTurn = this.turn;
-    this.turn = color;
-    const moves1 = super.getAllPotentialMoves();
-    this.turn = curTurn;
-    for (let move of moves1) {
-      this.play(move);
-      const res = super.isAttacked(sq, color);
-      this.undo(move);
-      if (res) return res;
-    }
+  isAttacked(sq, color) {
+    // Goal is king capture => no checks
     return false;
   }
 
   filterValid(moves) {
-    if (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);
+    return moves;
+  }
+
+  getCheckSquares() {
+    return [];
+  }
+
+  getCurrentScore() {
+    const color = this.turn;
+    if (this.kingPos[color][0] < 0) return (color == 'w' ? "0-1" : "1-0");
+    return "*";
   }
 
   play(move) {
@@ -139,12 +122,14 @@ export class Doublemove2Rules extends ChessRules {
     const firstRank = c == "w" ? V.size.x - 1 : 0;
 
     if (piece == V.KING && move.appear.length > 0) {
-      this.kingPos[c][0] = move.appear[0].x;
-      this.kingPos[c][1] = move.appear[0].y;
+      this.kingPos[c] = [move.appear[0].x, move.appear[0].y];
       this.castleFlags[c] = [V.size.y, V.size.y];
       return;
     }
     const oppCol = V.GetOppCol(c);
+    if (move.vanish.length == 2 && move.vanish[1].p == V.KING)
+      // Opponent's king is captured, game over
+      this.kingPos[oppCol] = [-1, -1];
     const oppFirstRank = V.size.x - 1 - firstRank;
     if (
       move.start.x == firstRank && //our rook moves?
@@ -152,7 +137,8 @@ export class Doublemove2Rules extends ChessRules {
     ) {
       const flagIdx = (move.start.y == this.castleFlags[c][0] ? 0 : 1);
       this.castleFlags[c][flagIdx] = V.size.y;
-    } else if (
+    }
+    else if (
       move.end.x == oppFirstRank && //we took opponent rook?
       this.castleFlags[oppCol].includes(move.end.y)
     ) {
@@ -175,28 +161,47 @@ export class Doublemove2Rules extends ChessRules {
       this.turn = V.GetOppCol(this.turn);
     }
     if (this.movesCount > 0) this.subTurn = 3 - this.subTurn;
-    super.postUndo(move);
+    this.postUndo(move);
   }
 
-  static get VALUES() {
-    return {
-      p: 1,
-      r: 5,
-      n: 3,
-      b: 3,
-      q: 7, //slightly less than in orthodox game
-      k: 1000
-    };
+  postUndo(move) {
+    if (move.vanish.length == 2 && move.vanish[1].p == V.KING)
+      // Opponent's king was captured
+      this.kingPos[move.vanish[1].c] = [move.vanish[1].x, move.vanish[1].y];
+    super.postUndo(move);
   }
 
-  // No alpha-beta here, just adapted min-max at depth 1(+1)
+  // No alpha-beta here, just adapted min-max at depth 2(+1)
   getComputerMove() {
+    const maxeval = V.INFINITY;
     const color = this.turn;
+    const oppCol = V.GetOppCol(this.turn);
+
+    // Search best (half) move for opponent turn
+    const getBestMoveEval = () => {
+      let score = this.getCurrentScore();
+      if (score != "*") return maxeval * (score == "1-0" ? 1 : -1);
+      let moves = this.getAllValidMoves();
+      let res = oppCol == "w" ? -maxeval : maxeval;
+      for (let m of moves) {
+        this.play(m);
+        score = this.getCurrentScore();
+        if (score != "*") {
+          // King captured
+          this.undo(m);
+          return maxeval * (score == "1-0" ? 1 : -1);
+        }
+        const evalPos = this.evalPosition();
+        res = oppCol == "w" ? Math.max(res, evalPos) : Math.min(res, evalPos);
+        this.undo(m);
+      }
+      return res;
+    };
+
     const moves11 = this.getAllValidMoves();
     if (this.movesCount == 0)
       // First white move at random:
       return moves11[randInt(moves11.length)];
-
     let doubleMoves = [];
     // Rank moves using a min-max at depth 2
     for (let i = 0; i < moves11.length; i++) {
@@ -206,7 +211,8 @@ export class Doublemove2Rules extends ChessRules {
         this.play(moves12[j]);
         doubleMoves.push({
           moves: [moves11[i], moves12[j]],
-          eval: this.evalPosition()
+          // Small fluctuations to uniformize play a little
+          eval: getBestMoveEval() + 0.05 - Math.random() / 10
         });
         this.undo(moves12[j]);
       }
diff --git a/client/src/variants/Monster.js b/client/src/variants/Monster.js
index 1fb6a773..071c8db0 100644
--- a/client/src/variants/Monster.js
+++ b/client/src/variants/Monster.js
@@ -46,23 +46,24 @@ export class MonsterRules extends ChessRules {
   }
 
   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;
-    }
+    // Goal is king capture => no checks
     return false;
   }
 
+  filterValid(moves) {
+    return moves;
+  }
+
+  getCheckSquares() {
+    return [];
+  }
+
+  getCurrentScore() {
+    const color = this.turn;
+    if (this.kingPos[color][0] < 0) return (color == 'w' ? "0-1" : "1-0");
+    return "*";
+  }
+
   play(move) {
     move.flags = JSON.stringify(this.aggregateFlags());
     if (this.turn == 'b' || this.subTurn == 2)
@@ -105,10 +106,11 @@ export class MonsterRules extends ChessRules {
     // 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) {
-      this.kingPos[c][0] = move.appear[0].x;
-      this.kingPos[c][1] = move.appear[0].y;
-    }
+    if (piece == V.KING)
+      this.kingPos[c] = [move.appear[0].x, move.appear[0].y];
+    if (move.vanish.length == 2 && move.vanish[1].p == V.KING)
+      // Opponent's king is captured, game over
+      this.kingPos[move.vanish[1].c] = [-1, -1];
     this.updateCastleFlags(move, piece);
   }
 
@@ -120,52 +122,49 @@ export class MonsterRules extends ChessRules {
       if (this.subTurn == 2) this.subTurn = 1;
       else this.turn = 'b';
       this.movesCount--;
-    } else {
+    }
+    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;
+  postUndo(move) {
+    if (move.vanish.length == 2 && move.vanish[1].p == V.KING)
+      // Opponent's king was captured
+      this.kingPos[move.vanish[1].c] = [move.vanish[1].x, move.vanish[1].y];
+    super.postUndo(move);
   }
 
+  // Custom search at depth 1(+1)
   getComputerMove() {
-    const color = this.turn;
-    if (color == 'w') {
+    const getBestWhiteMove = (terminal) => {
       // Generate all sequences of 2-moves
-      const moves1 = this.getAllValidMoves();
+      let 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;
-          }
-        });
+        if (!!terminal) m1.eval = this.evalPosition();
+        else {
+          const moves2 = this.getAllValidMoves();
+          moves2.forEach(m2 => {
+            this.play(m2);
+            const eval2 = this.evalPosition() + 0.05 - Math.random() / 10;
+            this.undo(m2);
+            if (eval2 > m1.eval) {
+              m1.eval = eval2;
+              m1.move2 = m2;
+            }
+          });
+        }
         this.undo(m1);
       });
       moves1.sort((a, b) => b.eval - a.eval);
+      if (!!terminal)
+        // The move itself doesn't matter, only its eval:
+        return moves1[0];
       let candidates = [0];
       for (
         let i = 1;
@@ -178,8 +177,31 @@ export class MonsterRules extends ChessRules {
       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();
+    };
+
+    const getBestBlackMove = () => {
+      let moves = this.getAllValidMoves();
+      moves.forEach(m => {
+        m.eval = V.INFINITY;
+        this.play(m);
+        const evalM = getBestWhiteMove("terminal").eval
+        this.undo(m);
+        if (evalM < m.eval) m.eval = evalM;
+      });
+      moves.sort((a, b) => a.eval - b.eval);
+      let candidates = [0];
+      for (
+        let i = 1;
+        i < moves.length && moves[i].eval == moves[0].eval;
+        i++
+      ) {
+        candidates.push(i);
+      }
+      const idx = candidates[randInt(candidates.length)];
+      return moves[idx];
+    };
+
+    const color = this.turn;
+    return (color == 'w' ? getBestWhiteMove() : getBestBlackMove());
   }
 };
diff --git a/client/src/variants/Teleport.js b/client/src/variants/Teleport.js
index 7704184f..6cf14a80 100644
--- a/client/src/variants/Teleport.js
+++ b/client/src/variants/Teleport.js
@@ -311,8 +311,6 @@ export class TeleportRules extends ChessRules {
             (color == 'w' && mvEval > m.eval) ||
             (color == 'b' && mvEval < m.eval)
           ) {
-            // TODO: if many second moves have the same eval, only the
-            // first is kept. Could be randomized.
             m.eval = mvEval;
             m.next = m2;
           }
-- 
2.44.0