Add a soft timer to avoid spending much more than 5 seconds on a move
[vchess.git] / public / javascripts / base_rules.js
index 9cfab5a..26c7447 100644 (file)
@@ -171,7 +171,6 @@ class ChessRules
                        'r': [ [-1,0],[1,0],[0,-1],[0,1] ],
                        'n': [ [-1,-2],[-1,2],[1,-2],[1,2],[-2,-1],[-2,1],[2,-1],[2,1] ],
                        'b': [ [-1,-1],[-1,1],[1,-1],[1,1] ],
-                       'q': [ [-1,0],[1,0],[0,-1],[0,1],[-1,-1],[-1,1],[1,-1],[1,1] ]
                };
        }
 
@@ -379,21 +378,25 @@ class ChessRules
        // What are the queen moves from square x,y ?
        getPotentialQueenMoves(sq)
        {
-               return this.getSlideNJumpMoves(sq, VariantRules.steps[VariantRules.QUEEN]);
+               const V = VariantRules;
+               return this.getSlideNJumpMoves(sq, V.steps[V.ROOK].concat(V.steps[V.BISHOP]));
        }
 
        // What are the king moves from square x,y ?
        getPotentialKingMoves(sq)
        {
+               const V = VariantRules;
                // Initialize with normal moves
-               let moves = this.getSlideNJumpMoves(sq, VariantRules.steps[VariantRules.QUEEN], "oneStep");
+               let moves = this.getSlideNJumpMoves(sq,
+                       V.steps[V.ROOK].concat(V.steps[V.BISHOP]), "oneStep");
                return moves.concat(this.getCastleMoves(sq));
        }
 
        getCastleMoves([x,y])
        {
                const c = this.getColor(x,y);
-               if (x != (c=="w" ? 7 : 0) || y != this.INIT_COL_KING[c])
+               const [sizeX,sizeY] = VariantRules.size;
+               if (x != (c=="w" ? sizeX-1 : 0) || y != this.INIT_COL_KING[c])
                        return []; //x isn't first rank, or king has moved (shortcut)
 
                const V = VariantRules;
@@ -402,7 +405,7 @@ class ChessRules
                const oppCol = this.getOppCol(c);
                let moves = [];
                let i = 0;
-               const finalSquares = [ [2,3], [6,5] ]; //king, then rook
+               const finalSquares = [ [2,3], [sizeY-2,sizeY-3] ]; //king, then rook
                castlingCheck:
                for (let castleSide=0; castleSide < 2; castleSide++) //large, then small
                {
@@ -487,8 +490,8 @@ class ChessRules
        {
                const color = this.turn;
                const oppCol = this.getOppCol(color);
-               var potentialMoves = [];
-               let [sizeX,sizeY] = VariantRules.size;
+               let potentialMoves = [];
+               const [sizeX,sizeY] = VariantRules.size;
                for (var i=0; i<sizeX; i++)
                {
                        for (var j=0; j<sizeY; j++)
@@ -508,7 +511,7 @@ class ChessRules
        {
                const color = this.turn;
                const oppCol = this.getOppCol(color);
-               let [sizeX,sizeY] = VariantRules.size;
+               const [sizeX,sizeY] = VariantRules.size;
                for (let i=0; i<sizeX; i++)
                {
                        for (let j=0; j<sizeY; j++)
@@ -586,15 +589,17 @@ class ChessRules
        // Is square x,y attacked by queens of color c ?
        isAttackedByQueen(sq, colors)
        {
-               return this.isAttackedBySlideNJump(sq, colors,
-                       VariantRules.QUEEN, VariantRules.steps[VariantRules.QUEEN]);
+               const V = VariantRules;
+               return this.isAttackedBySlideNJump(sq, colors, V.QUEEN,
+                       V.steps[V.ROOK].concat(V.steps[V.BISHOP]));
        }
 
        // Is square x,y attacked by king of color c ?
        isAttackedByKing(sq, colors)
        {
-               return this.isAttackedBySlideNJump(sq, colors,
-                       VariantRules.KING, VariantRules.steps[VariantRules.QUEEN], "oneStep");
+               const V = VariantRules;
+               return this.isAttackedBySlideNJump(sq, colors, V.KING,
+                       V.steps[V.ROOK].concat(V.steps[V.BISHOP]), "oneStep");
        }
 
        // Generic method for non-pawn pieces ("sliding or jumping"): is x,y attacked by piece != color ?
@@ -662,7 +667,8 @@ class ChessRules
        {
                const piece = this.getPiece(move.start.x,move.start.y);
                const c = this.getColor(move.start.x,move.start.y);
-               const firstRank = (c == "w" ? 7 : 0);
+               const [sizeX,sizeY] = VariantRules.size;
+               const firstRank = (c == "w" ? sizeX-1 : 0);
 
                // Update king position + flags
                if (piece == VariantRules.KING && move.appear.length > 0)
@@ -673,7 +679,7 @@ class ChessRules
                        return;
                }
                const oppCol = this.getOppCol(c);
-               const oppFirstRank = 7 - firstRank;
+               const oppFirstRank = (sizeX-1) - firstRank;
                if (move.start.x == firstRank //our rook moves?
                        && this.INIT_COL_ROOK[c].includes(move.start.y))
                {
@@ -794,9 +800,14 @@ class ChessRules
                return VariantRules.INFINITY;
        }
 
+       static get SEARCH_DEPTH() {
+               return 3; //2 for high branching factor, 4 for small (Loser chess)
+       }
+
        // Assumption: at least one legal move
        getComputerMove(moves1) //moves1 might be precomputed (Magnetic chess)
        {
+               this.shouldReturn = false;
                const maxeval = VariantRules.INFINITY;
                const color = this.turn;
                if (!moves1)
@@ -828,21 +839,32 @@ class ChessRules
                }
                moves1.sort( (a,b) => { return (color=="w" ? 1 : -1) * (b.eval - a.eval); });
 
+               let candidates = [0]; //indices of candidates moves
+               for (let j=1; j<moves1.length && moves1[j].eval == moves1[0].eval; j++)
+                       candidates.push(j);
+               let currentBest = moves1[_.sample(candidates, 1)];
+
                // Skip depth 3 if we found a checkmate (or if we are checkmated in 1...)
-               if (Math.abs(moves1[0].eval) < VariantRules.THRESHOLD_MATE)
+               if (VariantRules.SEARCH_DEPTH >= 3
+                       && Math.abs(moves1[0].eval) < VariantRules.THRESHOLD_MATE)
                {
                        // TODO: show current analyzed move for depth 3, allow stopping eval (return moves1[0])
                        for (let i=0; i<moves1.length; i++)
                        {
+                               if (this.shouldReturn)
+                                       return currentBest; //depth-2, minimum
                                this.play(moves1[i]);
                                // 0.1 * oldEval : heuristic to avoid some bad moves (not all...)
-                               moves1[i].eval = 0.1*moves1[i].eval + this.alphabeta(2, -maxeval, maxeval);
+                               moves1[i].eval = 0.1*moves1[i].eval +
+                                       this.alphabeta(VariantRules.SEARCH_DEPTH-1, -maxeval, maxeval);
                                this.undo(moves1[i]);
                        }
                        moves1.sort( (a,b) => { return (color=="w" ? 1 : -1) * (b.eval - a.eval); });
                }
+               else
+                       return currentBest;
 
-               let candidates = [0]; //indices of candidates moves
+               candidates = [0];
                for (let j=1; j<moves1.length && moves1[j].eval == moves1[0].eval; j++)
                        candidates.push(j);
 //             console.log(moves1.map(m => { return [this.getNotation(m), m.eval]; }));