Some code cleaning + clarifying (TODO: work on variables names)
[vchess.git] / public / javascripts / base_rules.js
index e886273..5c1ef97 100644 (file)
@@ -1,3 +1,6 @@
+// (Orthodox) Chess rules are defined in ChessRules class.
+// Variants generally inherit from it, and modify some parts.
+
 class PiPo //Piece+Position
 {
        // o: {piece[p], color[c], posX[x], posY[y]}
@@ -10,6 +13,7 @@ class PiPo //Piece+Position
        }
 }
 
+// TODO: for animation, moves should contains "moving" and "fading" maybe...
 class Move
 {
        // o: {appear, vanish, [start,] [end,]}
@@ -60,7 +64,7 @@ class ChessRules
        {
                this.INIT_COL_KING = {'w':-1, 'b':-1};
                this.INIT_COL_ROOK = {'w':[-1,-1], 'b':[-1,-1]};
-               this.kingPos = {'w':[-1,-1], 'b':[-1,-1]}; //respective squares of white and black king
+               this.kingPos = {'w':[-1,-1], 'b':[-1,-1]}; //squares of white and black king
                const fenParts = fen.split(" ");
                const position = fenParts[0].split("/");
                for (let i=0; i<position.length; i++)
@@ -124,7 +128,7 @@ class ChessRules
                return board;
        }
 
-       // Overridable: flags can change a lot
+       // Extract (relevant) flags from fen
        setFlags(fen)
        {
                // white a-castle, h-castle, black a-castle, h-castle
@@ -137,7 +141,6 @@ class ChessRules
        ///////////////////
        // GETTERS, SETTERS
 
-       // Simple useful getters
        static get size() { return [8,8]; }
        // Two next functions return 'undefined' if called on empty square
        getColor(i,j) { return this.board[i][j].charAt(0); }
@@ -199,7 +202,7 @@ class ChessRules
                return undefined; //default
        }
 
-       // can thing on square1 take thing on square2
+       // Can thing on square1 take thing on square2
        canTake([x1,y1], [x2,y2])
        {
                return this.getColor(x1,y1) != this.getColor(x2,y2);
@@ -291,7 +294,7 @@ class ChessRules
                return moves;
        }
 
-       // What are the pawn moves from square x,y considering color "color" ?
+       // What are the pawn moves from square x,y ?
        getPotentialPawnMoves([x,y])
        {
                const color = this.turn;
@@ -332,21 +335,22 @@ class ChessRules
                if (x+shift == lastRank)
                {
                        // Promotion
+                       const pawnColor = this.getColor(x,y); //can be different for checkered
                        let promotionPieces = [V.ROOK,V.KNIGHT,V.BISHOP,V.QUEEN];
                        promotionPieces.forEach(p => {
                                // Normal move
                                if (this.board[x+shift][y] == V.EMPTY)
-                                       moves.push(this.getBasicMove([x,y], [x+shift,y], {c:color,p:p}));
+                                       moves.push(this.getBasicMove([x,y], [x+shift,y], {c:pawnColor,p:p}));
                                // Captures
                                if (y>0 && this.canTake([x,y], [x+shift,y-1])
                                        && this.board[x+shift][y-1] != V.EMPTY)
                                {
-                                       moves.push(this.getBasicMove([x,y], [x+shift,y-1], {c:color,p:p}));
+                                       moves.push(this.getBasicMove([x,y], [x+shift,y-1], {c:pawnColor,p:p}));
                                }
                                if (y<sizeY-1 && this.canTake([x,y], [x+shift,y+1])
                                        && this.board[x+shift][y+1] != V.EMPTY)
                                {
-                                       moves.push(this.getBasicMove([x,y], [x+shift,y+1], {c:color,p:p}));
+                                       moves.push(this.getBasicMove([x,y], [x+shift,y+1], {c:pawnColor,p:p}));
                                }
                        });
                }
@@ -379,7 +383,8 @@ class ChessRules
        // What are the knight moves from square x,y ?
        getPotentialKnightMoves(sq)
        {
-               return this.getSlideNJumpMoves(sq, VariantRules.steps[VariantRules.KNIGHT], "oneStep");
+               return this.getSlideNJumpMoves(
+                       sq, VariantRules.steps[VariantRules.KNIGHT], "oneStep");
        }
 
        // What are the bishop moves from square x,y ?
@@ -480,7 +485,8 @@ class ChessRules
 
        canIplay(side, [x,y])
        {
-               return ((side=='w' && this.moves.length%2==0) || (side=='b' && this.moves.length%2==1))
+               return ((side=='w' && this.moves.length%2==0)
+                               || (side=='b' && this.moves.length%2==1))
                        && this.getColor(x,y) == side;
        }
 
@@ -490,7 +496,7 @@ class ChessRules
                return this.filterValid( this.getPotentialMovesFrom(sq) );
        }
 
-       // TODO: once a promotion is filtered, the others results are same: useless computations
+       // TODO: promotions (into R,B,N,Q) should be filtered only once
        filterValid(moves)
        {
                if (moves.length == 0)
@@ -509,7 +515,7 @@ class ChessRules
                {
                        for (let j=0; j<sizeY; j++)
                        {
-                               // Next condition ... != oppCol is a little HACK to work with checkered variant
+                               // Next condition "!= oppCol" = harmless hack to work with checkered variant
                                if (this.board[i][j] != VariantRules.EMPTY && this.getColor(i,j) != oppCol)
                                        Array.prototype.push.apply(potentialMoves, this.getPotentialMovesFrom([i,j]));
                        }
@@ -546,7 +552,7 @@ class ChessRules
                return false;
        }
 
-       // Check if pieces of color 'colors' are attacking square x,y
+       // Check if pieces of color in array 'colors' are attacking square x,y
        isAttacked(sq, colors)
        {
                return (this.isAttackedByPawn(sq, colors)
@@ -557,7 +563,7 @@ class ChessRules
                        || this.isAttackedByKing(sq, colors));
        }
 
-       // Is square x,y attacked by pawns of color c ?
+       // Is square x,y attacked by 'colors' pawns ?
        isAttackedByPawn([x,y], colors)
        {
                const [sizeX,sizeY] = VariantRules.size;
@@ -579,28 +585,28 @@ class ChessRules
                return false;
        }
 
-       // Is square x,y attacked by rooks of color c ?
+       // Is square x,y attacked by 'colors' rooks ?
        isAttackedByRook(sq, colors)
        {
                return this.isAttackedBySlideNJump(sq, colors,
                        VariantRules.ROOK, VariantRules.steps[VariantRules.ROOK]);
        }
 
-       // Is square x,y attacked by knights of color c ?
+       // Is square x,y attacked by 'colors' knights ?
        isAttackedByKnight(sq, colors)
        {
                return this.isAttackedBySlideNJump(sq, colors,
                        VariantRules.KNIGHT, VariantRules.steps[VariantRules.KNIGHT], "oneStep");
        }
 
-       // Is square x,y attacked by bishops of color c ?
+       // Is square x,y attacked by 'colors' bishops ?
        isAttackedByBishop(sq, colors)
        {
                return this.isAttackedBySlideNJump(sq, colors,
                        VariantRules.BISHOP, VariantRules.steps[VariantRules.BISHOP]);
        }
 
-       // Is square x,y attacked by queens of color c ?
+       // Is square x,y attacked by 'colors' queens ?
        isAttackedByQueen(sq, colors)
        {
                const V = VariantRules;
@@ -608,7 +614,7 @@ class ChessRules
                        V.steps[V.ROOK].concat(V.steps[V.BISHOP]));
        }
 
-       // Is square x,y attacked by king of color c ?
+       // Is square x,y attacked by 'colors' king(s) ?
        isAttackedByKing(sq, colors)
        {
                const V = VariantRules;
@@ -617,7 +623,7 @@ class ChessRules
        }
 
        // Generic method for non-pawn pieces ("sliding or jumping"):
-       // is x,y attacked by piece !of color in colors?
+       // is x,y attacked by a piece of color in array 'colors' ?
        isAttackedBySlideNJump([x,y], colors, piece, steps, oneStep)
        {
                const [sizeX,sizeY] = VariantRules.size;
@@ -640,7 +646,7 @@ class ChessRules
                return false;
        }
 
-       // Is color c under check after move ?
+       // Is current player under check after his move ?
        underCheck(move)
        {
                const color = this.turn;
@@ -650,7 +656,7 @@ class ChessRules
                return res;
        }
 
-       // On which squares is color c under check (after move) ?
+       // On which squares is opponent under check after our move ?
        getCheckSquares(move)
        {
                this.play(move);
@@ -711,6 +717,8 @@ class ChessRules
                }
        }
 
+       // After move is undo-ed, un-update variables (flags are reset)
+       // TODO: more symmetry, by storing flags increment in move...
        unupdateVariables(move)
        {
                // (Potentially) Reset king position
@@ -721,10 +729,6 @@ class ChessRules
 
        play(move, ingame)
        {
-               // DEBUG:
-//             if (!this.states) this.states = [];
-//             if (!ingame) this.states.push(JSON.stringify(this.board));
-
                if (!!ingame)
                        move.notation = [this.getNotation(move), this.getLongNotation(move)];
 
@@ -742,22 +746,16 @@ class ChessRules
                this.moves.pop();
                this.unupdateVariables(move);
                this.parseFlags(JSON.parse(move.flags));
-
-               // DEBUG:
-//             let state = this.states.pop();
-//             if (JSON.stringify(this.board) != state)
-//                     debugger;
        }
 
        //////////////
        // END OF GAME
 
+       // Basic check for 3 repetitions (in the last moves only)
        checkRepetition()
        {
-               // Check for 3 repetitions
                if (this.moves.length >= 8)
                {
-                       // NOTE: crude detection, only moves repetition
                        const L = this.moves.length;
                        if (_.isEqual(this.moves[L-1], this.moves[L-5]) &&
                                _.isEqual(this.moves[L-2], this.moves[L-6]) &&
@@ -770,6 +768,7 @@ class ChessRules
                return false;
        }
 
+       // Is game over ? And if yes, what is the score ?
        checkGameOver()
        {
                if (this.checkRepetition())
@@ -828,7 +827,9 @@ class ChessRules
                this.shouldReturn = false;
                const maxeval = VariantRules.INFINITY;
                const color = this.turn;
-               let moves1 = this.getAllValidMoves();
+               // Some variants may show a bigger moves list to the human (Switching),
+               // thus the argument "computer" below (which is generally ignored)
+               let moves1 = this.getAllValidMoves("computer");
 
                // Can I mate in 1 ? (for Magnetic & Extinction)
                for (let i of _.shuffle(_.range(moves1.length)))
@@ -844,27 +845,44 @@ class ChessRules
                for (let i=0; i<moves1.length; i++)
                {
                        moves1[i].eval = (color=="w" ? -1 : 1) * maxeval; //very low, I'm checkmated
-                       let eval2 = (color=="w" ? 1 : -1) * maxeval; //initialized with checkmate value
                        this.play(moves1[i]);
-                       // Second half-move:
-                       let moves2 = this.getAllValidMoves();
-                       // If no possible moves AND underCheck, eval2 is correct.
-                       // If !underCheck, eval2 is 0 (stalemate).
-                       if (moves2.length == 0 && this.checkGameEnd() == "1/2")
-                               eval2 = 0;
-                       for (let j=0; j<moves2.length; j++)
+                       let eval2 = undefined;
+                       if (this.atLeastOneMove())
                        {
-                               this.play(moves2[j]);
-                               let evalPos = this.evalPosition();
-                               if ((color == "w" && evalPos < eval2) || (color=="b" && evalPos > eval2))
-                                       eval2 = evalPos;
-                               this.undo(moves2[j]);
+                               eval2 = (color=="w" ? 1 : -1) * maxeval; //initialized with checkmate value
+                               // Second half-move:
+                               let moves2 = this.getAllValidMoves("computer");
+                               for (let j=0; j<moves2.length; j++)
+                               {
+                                       this.play(moves2[j]);
+                                       let evalPos = undefined;
+                                       if (this.atLeastOneMove())
+                                               evalPos = this.evalPosition()
+                                       else
+                                       {
+                                               // Work with scores for Loser variant
+                                               const score = this.checkGameEnd();
+                                               evalPos = (score=="1/2" ? 0 : (score=="1-0" ? 1 : -1) * maxeval);
+                                       }
+                                       if ((color == "w" && evalPos < eval2) || (color=="b" && evalPos > eval2))
+                                               eval2 = evalPos;
+                                       this.undo(moves2[j]);
+                               }
                        }
-                       if ((color=="w" && eval2 > moves1[i].eval) || (color=="b" && eval2 < moves1[i].eval))
+                       else
+                       {
+                               const score = this.checkGameEnd();
+                               eval2 = (score=="1/2" ? 0 : (score=="1-0" ? 1 : -1) * maxeval);
+                       }
+                       if ((color=="w" && eval2 > moves1[i].eval)
+                               || (color=="b" && eval2 < moves1[i].eval))
+                       {
                                moves1[i].eval = eval2;
+                       }
                        this.undo(moves1[i]);
                }
                moves1.sort( (a,b) => { return (color=="w" ? 1 : -1) * (b.eval - a.eval); });
+               //console.log(moves1.map(m => { return [this.getNotation(m), m.eval]; }));
 
                let candidates = [0]; //indices of candidates moves
                for (let j=1; j<moves1.length && moves1[j].eval == moves1[0].eval; j++)
@@ -889,11 +907,11 @@ class ChessRules
                }
                else
                        return currentBest;
+               //console.log(moves1.map(m => { return [this.getNotation(m), m.eval]; }));
 
                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]; }));
                return moves1[_.sample(candidates, 1)];
        }
 
@@ -905,13 +923,16 @@ class ChessRules
                {
                        switch (this.checkGameEnd())
                        {
-                               case "1/2": return 0;
-                               default: return color=="w" ? -maxeval : maxeval;
+                               case "1/2":
+                                       return 0;
+                               default:
+                                       const score = this.checkGameEnd();
+                                       return (score=="1/2" ? 0 : (score=="1-0" ? 1 : -1) * maxeval);
                        }
                }
                if (depth == 0)
       return this.evalPosition();
-               const moves = this.getAllValidMoves();
+               const moves = this.getAllValidMoves("computer");
     let v = color=="w" ? -maxeval : maxeval;
                if (color == "w")
                {
@@ -962,7 +983,7 @@ class ChessRules
        ////////////
        // FEN utils
 
-       // Overridable..
+       // Setup the initial random (assymetric) position
        static GenRandInitFen()
        {
                let pieces = [new Array(8), new Array(8)];
@@ -1022,6 +1043,7 @@ class ChessRules
                return this.getBaseFen() + " " + this.getFlagsFen();
        }
 
+       // Position part of the FEN string
        getBaseFen()
        {
                let fen = "";
@@ -1055,7 +1077,7 @@ class ChessRules
                return fen;
        }
 
-       // Overridable..
+       // Flags part of the FEN string
        getFlagsFen()
        {
                let fen = "";
@@ -1063,7 +1085,7 @@ class ChessRules
                for (let i of ['w','b'])
                {
                        for (let j=0; j<2; j++)
-                               fen += this.castleFlags[i][j] ? '1' : '0';
+                               fen += (this.castleFlags[i][j] ? '1' : '0');
                }
                return fen;
        }
@@ -1071,14 +1093,8 @@ class ChessRules
        // Context: just before move is played, turn hasn't changed
        getNotation(move)
        {
-               if (move.appear.length == 2 && move.appear[0].p == VariantRules.KING)
-               {
-                       // Castle
-                       if (move.end.y < move.start.y)
-                               return "0-0-0";
-                       else
-                               return "0-0";
-               }
+               if (move.appear.length == 2 && move.appear[0].p == VariantRules.KING) //castle
+                       return (move.end.y < move.start.y ? "0-0-0" : "0-0");
 
                // Translate final square
                const finalSquare =
@@ -1123,15 +1139,18 @@ class ChessRules
        // The score is already computed when calling this function
        getPGN(mycolor, score, fenStart, mode)
        {
+               const zeroPad = x => { return (x<10 ? "0" : "") + x; };
                let pgn = "";
                pgn += '[Site "vchess.club"]<br>';
                const d = new Date();
                const opponent = mode=="human" ? "Anonymous" : "Computer";
                pgn += '[Variant "' + variant + '"]<br>';
-               pgn += '[Date "' + d.getFullYear() + '-' + (d.getMonth()+1) + '-' + d.getDate() + '"]<br>';
+               pgn += '[Date "' + d.getFullYear() + '-' + (d.getMonth()+1) +
+                       '-' + zeroPad(d.getDate()) + '"]<br>';
                pgn += '[White "' + (mycolor=='w'?'Myself':opponent) + '"]<br>';
                pgn += '[Black "' + (mycolor=='b'?'Myself':opponent) + '"]<br>';
-               pgn += '[Fen "' + fenStart + '"]<br>';
+               pgn += '[FenStart "' + fenStart + '"]<br>';
+               pgn += '[Fen "' + this.getFen() + '"]<br>';
                pgn += '[Result "' + score + '"]<br><br>';
 
                // Standard PGN
@@ -1141,7 +1160,7 @@ class ChessRules
                                pgn += ((i/2)+1) + ".";
                        pgn += this.moves[i].notation[0] + " ";
                }
-               pgn += score + "<br><br>";
+               pgn += "<br><br>";
 
                // "Complete moves" PGN (helping in ambiguous cases)
                for (let i=0; i<this.moves.length; i++)
@@ -1150,7 +1169,6 @@ class ChessRules
                                pgn += ((i/2)+1) + ".";
                        pgn += this.moves[i].notation[1] + " ";
                }
-               pgn += score;
 
                return pgn;
        }