Simplify Monster + Doublemove2. Smoother example games
authorBenjamin Auder <benjamin.auder@somewhere>
Wed, 29 Apr 2020 19:55:45 +0000 (21:55 +0200)
committerBenjamin Auder <benjamin.auder@somewhere>
Wed, 29 Apr 2020 19:55:45 +0000 (21:55 +0200)
client/src/components/ComputerGame.vue
client/src/translations/rules/Doublemove2/en.pug
client/src/translations/rules/Doublemove2/es.pug
client/src/translations/rules/Doublemove2/fr.pug
client/src/translations/rules/Monster/en.pug
client/src/translations/rules/Monster/es.pug
client/src/translations/rules/Monster/fr.pug
client/src/variants/Doublemove1.js
client/src/variants/Doublemove2.js
client/src/variants/Monster.js
client/src/variants/Teleport.js

index ad56543..f611167 100644 (file)
@@ -44,7 +44,11 @@ export default {
       setTimeout(() => {
         if (this.currentUrl != document.location.href) return; //page change
         self.$refs["basegame"].play(compMove, "received");
       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
         self.compThink = false;
         if (self.game.score != "*")
           // User action
index c1bda2e..ca24491 100644 (file)
@@ -1,5 +1,5 @@
 p.boxed
 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,
 
 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
 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
 
 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.
     | 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,
 
 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
 
 
 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
   | .
   | . It is also playable 
   a(href="https://greenchess.net/rules.php?v=double-move") on greenchess
   | .
+
+p Inventor: Fred Galvin (1957)
index aa4751d..179025b 100644 (file)
@@ -1,15 +1,15 @@
 p.boxed
 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.
 
 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,
   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
 
 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.
     | 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
 
 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
 
 
 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
   | .
   | . También es jugable 
   a(href="https://greenchess.net/rules.php?v=double-move") en greenchess
   | .
+
+p Inventor: Fred Galvin (1957)
index 6602186..0dff04f 100644 (file)
@@ -1,5 +1,5 @@
 p.boxed
 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
 
 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
 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
 
 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é.
     | 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,
 
 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
 
 
 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
   | .
   | . Elle est jouable également 
   a(href="https://greenchess.net/rules.php?v=double-move") sur greenchess
   | .
+
+p Inventeur : Fred Galvin (1957)
index 8e7061d..d0551d1 100644 (file)
@@ -8,10 +8,8 @@ figure.diagram-container
 
 p.
   The white army can appear much too small, but the power to move twice in a
 
 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
 
 figure.diagram-container
   .diagram.diag12
index edf1bdf..f9149f6 100644 (file)
@@ -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
 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
 
 figure.diagram-container
   .diagram.diag12
index 757ae3e..c0b9910 100644 (file)
@@ -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
 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
 
 figure.diagram-container
   .diagram.diag12
index 7d1ee4d..c30ad0c 100644 (file)
@@ -201,7 +201,7 @@ export class Doublemove1Rules extends ChessRules {
       return res;
     };
 
       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++) {
     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() });
       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]],
         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]);
         }
           });
           this.undo(moves12[j]);
         }
@@ -223,6 +224,8 @@ export class Doublemove1Rules extends ChessRules {
       this.undo(moves11[i]);
     }
 
       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);
     });
     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);
     }
     ) {
       candidates.push(i);
     }
-
     const selected = doubleMoves[randInt(candidates.length)].moves;
     if (selected.length == 1) return selected[0];
     return selected;
     const selected = doubleMoves[randInt(candidates.length)].moves;
     if (selected.length == 1) return selected[0];
     return selected;
index d1afff8..080d13b 100644 (file)
@@ -79,40 +79,23 @@ export class Doublemove2Rules extends ChessRules {
     return moves;
   }
 
     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) {
     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) {
   }
 
   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) {
     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);
       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?
     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;
     ) {
       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)
     ) {
       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;
       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() {
   getComputerMove() {
+    const maxeval = V.INFINITY;
     const color = this.turn;
     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)];
     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++) {
     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]],
         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]);
       }
         });
         this.undo(moves12[j]);
       }
index 1fb6a77..071c8db 100644 (file)
@@ -46,23 +46,24 @@ export class MonsterRules extends ChessRules {
   }
 
   isAttacked(sq, color, castling) {
   }
 
   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;
   }
 
     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)
   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;
     // 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);
   }
 
     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--;
       if (this.subTurn == 2) this.subTurn = 1;
       else this.turn = 'b';
       this.movesCount--;
-    } else {
+    }
+    else {
       this.turn = 'w';
       this.subTurn = 2;
     }
     this.postUndo(move);
   }
 
       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() {
   getComputerMove() {
-    const color = this.turn;
-    if (color == 'w') {
+    const getBestWhiteMove = (terminal) => {
       // Generate all sequences of 2-moves
       // 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);
       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);
         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;
       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];
       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());
   }
 };
   }
 };
index 7704184..6cf14a8 100644 (file)
@@ -311,8 +311,6 @@ export class TeleportRules extends ChessRules {
             (color == 'w' && mvEval > m.eval) ||
             (color == 'b' && mvEval < m.eval)
           ) {
             (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;
           }
             m.eval = mvEval;
             m.next = m2;
           }