Some debug, plan several short + long term TODOs
[vchess.git] / public / javascripts / base_rules.js
index 0bf5114..8f875e4 100644 (file)
@@ -64,22 +64,16 @@ class ChessRules
                if (!V.IsGoodPosition(fenParsed.position))
                        return false;
                // 2) Check turn
-               if (!fenParsed.turn || !["w","b"].includes(fenParsed.turn))
+               if (!fenParsed.turn || !V.IsGoodTurn(fenParsed.turn))
                        return false;
                // 3) Check flags
                if (V.HasFlags && (!fenParsed.flags || !V.IsGoodFlags(fenParsed.flags)))
                        return false;
                // 4) Check enpassant
-               if (V.HasEnpassant)
+               if (V.HasEnpassant &&
+                       (!fenParsed.enpassant || !V.IsGoodEnpassant(fenParsed.enpassant)))
                {
-                       if (!fenParsed.enpassant)
-                               return false;
-                       if (fenParsed.enpassant != "-")
-                       {
-                               const ep = V.SquareToCoords(fenParsed.enpassant);
-                               if (ep.y < 0 || ep.y > V.size.y || isNaN(ep.x) || ep.x < 0 || ep.x > V.size.x)
-                                       return false;
-                       }
+                       return false;
                }
                return true;
        }
@@ -113,22 +107,47 @@ class ChessRules
                return true;
        }
 
+       // For FEN checking
+       static IsGoodTurn(turn)
+       {
+               return ["w","b"].includes(turn);
+       }
+
        // For FEN checking
        static IsGoodFlags(flags)
        {
                return !!flags.match(/^[01]{4,4}$/);
        }
 
-       // 3 --> d (column letter from number)
-       static GetColumn(colnum)
+       static IsGoodEnpassant(enpassant)
+       {
+               if (enpassant != "-")
+               {
+                       const ep = V.SquareToCoords(fenParsed.enpassant);
+                       if (isNaN(ep.x) || !V.OnBoard(ep))
+                               return false;
+               }
+               return true;
+       }
+
+       // 3 --> d (column number to letter)
+       static CoordToColumn(colnum)
        {
                return String.fromCharCode(97 + colnum);
        }
 
+       // d --> 3 (column letter to number)
+       static ColumnToCoord(column)
+       {
+               return column.charCodeAt(0) - 97;
+       }
+
        // a4 --> {x:3,y:0}
        static SquareToCoords(sq)
        {
                return {
+                       // NOTE: column is always one char => max 26 columns
+                       // row is counted from black side => subtraction
                        x: V.size.x - parseInt(sq.substr(1)),
                        y: sq[0].charCodeAt() - 97
                };
@@ -137,7 +156,7 @@ class ChessRules
        // {x:0,y:4} --> e8
        static CoordsToSquare(coords)
        {
-               return V.GetColumn(coords.y) + (V.size.x - coords.x);
+               return V.CoordToColumn(coords.y) + (V.size.x - coords.x);
        }
 
        // Aggregates flags into one object
@@ -167,7 +186,8 @@ class ChessRules
                // Argument is a move:
                const move = moveOrSquare;
                const [sx,sy,ex] = [move.start.x,move.start.y,move.end.x];
-               if (this.getPiece(sx,sy) == V.PAWN && Math.abs(sx - ex) == 2)
+               // TODO: next conditions are first for Atomic, and third for Checkered
+               if (move.appear.length > 0 && move.appear[0].p == V.PAWN && ["w","b"].includes(move.appear[0].c) && Math.abs(sx - ex) == 2)
                {
                        return {
                                x: (sx + ex)/2,
@@ -217,31 +237,32 @@ class ChessRules
 
                        // Get random squares for bishops
                        let randIndex = 2 * _.random(3);
-                       let bishop1Pos = positions[randIndex];
+                       const bishop1Pos = positions[randIndex];
                        // The second bishop must be on a square of different color
                        let randIndex_tmp = 2 * _.random(3) + 1;
-                       let bishop2Pos = positions[randIndex_tmp];
+                       const bishop2Pos = positions[randIndex_tmp];
                        // Remove chosen squares
                        positions.splice(Math.max(randIndex,randIndex_tmp), 1);
                        positions.splice(Math.min(randIndex,randIndex_tmp), 1);
 
                        // Get random squares for knights
                        randIndex = _.random(5);
-                       let knight1Pos = positions[randIndex];
+                       const knight1Pos = positions[randIndex];
                        positions.splice(randIndex, 1);
                        randIndex = _.random(4);
-                       let knight2Pos = positions[randIndex];
+                       const knight2Pos = positions[randIndex];
                        positions.splice(randIndex, 1);
 
                        // Get random square for queen
                        randIndex = _.random(3);
-                       let queenPos = positions[randIndex];
+                       const queenPos = positions[randIndex];
                        positions.splice(randIndex, 1);
 
-                       // Rooks and king positions are now fixed, because of the ordering rook-king-rook
-                       let rook1Pos = positions[0];
-                       let kingPos = positions[1];
-                       let rook2Pos = positions[2];
+                       // Rooks and king positions are now fixed,
+                       // because of the ordering rook-king-rook
+                       const rook1Pos = positions[0];
+                       const kingPos = positions[1];
+                       const rook2Pos = positions[2];
 
                        // Finally put the shuffled pieces in the board array
                        pieces[c][rook1Pos] = 'r';
@@ -279,7 +300,7 @@ class ChessRules
        // Return current fen (game state)
        getFen()
        {
-               return this.getBaseFen() + " " + this.turn +
+               return this.getBaseFen() + " " + this.getTurnFen() +
                        (V.HasFlags ? (" " + this.getFlagsFen()) : "") +
                        (V.HasEnpassant ? (" " + this.getEnpassantFen()) : "");
        }
@@ -317,6 +338,11 @@ class ChessRules
                return position;
        }
 
+       getTurnFen()
+       {
+               return this.turn;
+       }
+
        // Flags part of the FEN string
        getFlagsFen()
        {
@@ -380,7 +406,7 @@ class ChessRules
                this.moves = moves;
                const fenParsed = V.ParseFen(fen);
                this.board = V.GetBoard(fenParsed.position);
-               this.turn = (fenParsed.turn || "w");
+               this.turn = fenParsed.turn[0]; //[0] to work with MarseilleRules
                this.setOtherVariables(fen);
        }
 
@@ -528,7 +554,8 @@ class ChessRules
                }
        }
 
-       // Build a regular move from its initial and destination squares; tr: transformation
+       // Build a regular move from its initial and destination squares.
+       // tr: transformation
        getBasicMove([sx,sy], [ex,ey], tr)
        {
                let mv = new Move({
@@ -565,7 +592,8 @@ class ChessRules
                return mv;
        }
 
-       // Generic method to find possible moves of non-pawn pieces ("sliding or jumping")
+       // Generic method to find possible moves of non-pawn pieces:
+       // "sliding or jumping"
        getSlideNJumpMoves([x,y], steps, oneStep)
        {
                const color = this.getColor(x,y);
@@ -601,7 +629,8 @@ class ChessRules
                const lastRank = (color == "w" ? 0 : sizeX-1);
                const pawnColor = this.getColor(x,y); //can be different for checkered
 
-               if (x+shiftX >= 0 && x+shiftX < sizeX) //TODO: always true
+               // NOTE: next condition is generally true (no pawn on last rank)
+               if (x+shiftX >= 0 && x+shiftX < sizeX)
                {
                        const finalPieces = x + shiftX == lastRank
                                ? [V.ROOK,V.KNIGHT,V.BISHOP,V.QUEEN]
@@ -680,7 +709,8 @@ class ChessRules
        // What are the queen moves from square x,y ?
        getPotentialQueenMoves(sq)
        {
-               return this.getSlideNJumpMoves(sq, V.steps[V.ROOK].concat(V.steps[V.BISHOP]));
+               return this.getSlideNJumpMoves(sq,
+                       V.steps[V.ROOK].concat(V.steps[V.BISHOP]));
        }
 
        // What are the king moves from square x,y ?
@@ -710,13 +740,15 @@ class ChessRules
                                continue;
                        // If this code is reached, rooks and king are on initial position
 
-                       // Nothing on the path of the king (and no checks; OK also if y==finalSquare)?
+                       // Nothing on the path of the king ?
+                       // (And no checks; OK also if y==finalSquare)
                        let step = finalSquares[castleSide][0] < y ? -1 : 1;
                        for (i=y; i!=finalSquares[castleSide][0]; i+=step)
                        {
                                if (this.isAttacked([x,i], [oppCol]) || (this.board[x][i] != V.EMPTY &&
                                        // NOTE: next check is enough, because of chessboard constraints
-                                       (this.getColor(x,i) != c || ![V.KING,V.ROOK].includes(this.getPiece(x,i)))))
+                                       (this.getColor(x,i) != c
+                                               || ![V.KING,V.ROOK].includes(this.getPiece(x,i)))))
                                {
                                        continue castlingCheck;
                                }
@@ -782,7 +814,8 @@ class ChessRules
                });
        }
 
-       // Search for all valid moves considering current turn (for engine and game end)
+       // Search for all valid moves considering current turn
+       // (for engine and game end)
        getAllValidMoves()
        {
                const color = this.turn;
@@ -792,13 +825,14 @@ class ChessRules
                {
                        for (let j=0; j<V.size.y; j++)
                        {
-                               // Next condition "!= oppCol" = harmless hack to work with checkered variant
+                               // Next condition "!= oppCol" to work with checkered variant
                                if (this.board[i][j] != V.EMPTY && this.getColor(i,j) != oppCol)
-                                       Array.prototype.push.apply(potentialMoves, this.getPotentialMovesFrom([i,j]));
+                               {
+                                       Array.prototype.push.apply(potentialMoves,
+                                               this.getPotentialMovesFrom([i,j]));
+                               }
                        }
                }
-               // NOTE: prefer lazy undercheck tests, letting the king being taken?
-               // No: if happen on last 1/2 move, could lead to forbidden moves, wrong evals
                return this.filterValid(potentialMoves);
        }
 
@@ -828,7 +862,7 @@ class ChessRules
                return false;
        }
 
-       // Check if pieces of color in array 'colors' are attacking (king) on square x,y
+       // Check if pieces of color in 'colors' are attacking (king) on square x,y
        isAttacked(sq, colors)
        {
                return (this.isAttackedByPawn(sq, colors)
@@ -940,11 +974,28 @@ class ChessRules
                        board[psq.x][psq.y] = psq.c + psq.p;
        }
 
-       // Before move is played, update variables + flags
+       // After move is played, update variables + flags
        updateVariables(move)
        {
-               const piece = move.vanish[0].p;
-               const c = move.vanish[0].c;
+               let piece = undefined;
+               let c = undefined;
+               if (move.vanish.length >= 1)
+               {
+                       // Usual case, something is moved
+                       piece = move.vanish[0].p;
+                       c = move.vanish[0].c;
+               }
+               else
+               {
+                       // Crazyhouse-like variants
+                       piece = move.appear[0].p;
+                       c = move.appear[0].c;
+               }
+               if (c == "c") //if (!["w","b"].includes(c))
+               {
+                       // 'c = move.vanish[0].c' doesn't work for Checkered
+                       c = this.getOppCol(this.turn);
+               }
                const firstRank = (c == "w" ? V.size.x-1 : 0);
 
                // Update king position + flags
@@ -952,22 +1003,27 @@ class ChessRules
                {
                        this.kingPos[c][0] = move.appear[0].x;
                        this.kingPos[c][1] = move.appear[0].y;
-                       this.castleFlags[c] = [false,false];
+                       if (V.HasFlags)
+                               this.castleFlags[c] = [false,false];
                        return;
                }
-               const oppCol = this.getOppCol(c);
-               const oppFirstRank = (V.size.x-1) - firstRank;
-               if (move.start.x == firstRank //our rook moves?
-                       && this.INIT_COL_ROOK[c].includes(move.start.y))
-               {
-                       const flagIdx = (move.start.y == this.INIT_COL_ROOK[c][0] ? 0 : 1);
-                       this.castleFlags[c][flagIdx] = false;
-               }
-               else if (move.end.x == oppFirstRank //we took opponent rook?
-                       && this.INIT_COL_ROOK[oppCol].includes(move.end.y))
+               if (V.HasFlags)
                {
-                       const flagIdx = (move.end.y == this.INIT_COL_ROOK[oppCol][0] ? 0 : 1);
-                       this.castleFlags[oppCol][flagIdx] = false;
+                       // Update castling flags if rooks are moved
+                       const oppCol = this.getOppCol(c);
+                       const oppFirstRank = (V.size.x-1) - firstRank;
+                       if (move.start.x == firstRank //our rook moves?
+                               && this.INIT_COL_ROOK[c].includes(move.start.y))
+                       {
+                               const flagIdx = (move.start.y == this.INIT_COL_ROOK[c][0] ? 0 : 1);
+                               this.castleFlags[c][flagIdx] = false;
+                       }
+                       else if (move.end.x == oppFirstRank //we took opponent rook?
+                               && this.INIT_COL_ROOK[oppCol].includes(move.end.y))
+                       {
+                               const flagIdx = (move.end.y == this.INIT_COL_ROOK[oppCol][0] ? 0 : 1);
+                               this.castleFlags[oppCol][flagIdx] = false;
+                       }
                }
        }
 
@@ -984,7 +1040,6 @@ class ChessRules
        play(move, ingame)
        {
                // DEBUG:
-//             console.log("DO");
 //             if (!this.states) this.states = [];
 //             if (!ingame) this.states.push(this.getFen());
 
@@ -1019,7 +1074,6 @@ class ChessRules
                this.unupdateVariables(move);
 
                // DEBUG:
-//             console.log("UNDO "+this.getNotation(move));
 //             if (this.getFen() != this.states[this.states.length-1])
 //                     debugger;
 //             this.states.pop();
@@ -1069,7 +1123,7 @@ class ChessRules
                if (!this.isAttacked(this.kingPos[color], [this.getOppCol(color)]))
                        return "1/2";
                // OK, checkmate
-               return color == "w" ? "0-1" : "1-0";
+               return (color == "w" ? "0-1" : "1-0");
        }
 
        ///////////////
@@ -1127,12 +1181,14 @@ class ChessRules
                // Rank moves using a min-max at depth 2
                for (let i=0; i<moves1.length; i++)
                {
-                       moves1[i].eval = (color=="w" ? -1 : 1) * maxeval; //very low, I'm checkmated
+                       // Initial self evaluation is very low: "I'm checkmated"
+                       moves1[i].eval = (color=="w" ? -1 : 1) * maxeval;
                        this.play(moves1[i]);
                        let eval2 = undefined;
                        if (this.atLeastOneMove())
                        {
-                               eval2 = (color=="w" ? 1 : -1) * maxeval; //initialized with checkmate value
+                               // Initial enemy evaluation is very low too, for him
+                               eval2 = (color=="w" ? 1 : -1) * maxeval;
                                // Second half-move:
                                let moves2 = this.getAllValidMoves("computer");
                                for (let j=0; j<moves2.length; j++)
@@ -1147,8 +1203,11 @@ class ChessRules
                                                const score = this.checkGameEnd();
                                                evalPos = (score=="1/2" ? 0 : (score=="1-0" ? 1 : -1) * maxeval);
                                        }
-                                       if ((color == "w" && evalPos < eval2) || (color=="b" && evalPos > eval2))
+                                       if ((color == "w" && evalPos < eval2)
+                                               || (color=="b" && evalPos > eval2))
+                                       {
                                                eval2 = evalPos;
+                                       }
                                        this.undo(moves2[j]);
                                }
                        }
@@ -1187,11 +1246,12 @@ class ChessRules
                                        this.alphabeta(V.SEARCH_DEPTH-1, -maxeval, maxeval);
                                this.undo(moves1[i]);
                        }
-                       moves1.sort( (a,b) => { return (color=="w" ? 1 : -1) * (b.eval - a.eval); });
+                       moves1.sort( (a,b) => {
+                               return (color=="w" ? 1 : -1) * (b.eval - a.eval); });
                }
                else
                        return currentBest;
-               //console.log(moves1.map(m => { return [this.getNotation(m), m.eval]; }));
+//             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++)
@@ -1284,12 +1344,12 @@ class ChessRules
                        if (move.vanish.length > move.appear.length)
                        {
                                // Capture
-                               const startColumn = String.fromCharCode(97 + move.start.y);
+                               const startColumn = V.CoordToColumn(move.start.y);
                                notation = startColumn + "x" + finalSquare;
                        }
                        else //no capture
                                notation = finalSquare;
-                       if (move.appear.length > 0 && piece != move.appear[0].p) //promotion
+                       if (move.appear.length > 0 && move.appear[0].p != V.PAWN) //promotion
                                notation += "=" + move.appear[0].p.toUpperCase();
                        return notation;
                }
@@ -1313,15 +1373,22 @@ class ChessRules
        getPGN(mycolor, score, fenStart, mode)
        {
                let pgn = "";
-               pgn += '[Site "vchess.club"]<br>';
+               pgn += '[Site "vchess.club"]\n';
                const opponent = mode=="human" ? "Anonymous" : "Computer";
-               pgn += '[Variant "' + variant + '"]<br>';
-               pgn += '[Date "' + getDate(new Date()) + '"]<br>';
-               pgn += '[White "' + (mycolor=='w'?'Myself':opponent) + '"]<br>';
-               pgn += '[Black "' + (mycolor=='b'?'Myself':opponent) + '"]<br>';
-               pgn += '[FenStart "' + fenStart + '"]<br>';
-               pgn += '[Fen "' + this.getFen() + '"]<br>';
-               pgn += '[Result "' + score + '"]<br><br>';
+               pgn += '[Variant "' + variant + '"]\n';
+               pgn += '[Date "' + getDate(new Date()) + '"]\n';
+               // TODO: later when users are a bit less anonymous, use better names
+               const whiteName = ["human","computer"].includes(mode)
+                       ? (mycolor=='w'?'Myself':opponent)
+                       : "analyze";
+               const blackName = ["human","computer"].includes(mode)
+                       ? (mycolor=='b'?'Myself':opponent)
+                       : "analyze";
+               pgn += '[White "' + whiteName + '"]\n';
+               pgn += '[Black "' + blackName + '"]\n';
+               pgn += '[FenStart "' + fenStart + '"]\n';
+               pgn += '[Fen "' + this.getFen() + '"]\n';
+               pgn += '[Result "' + score + '"]\n\n';
 
                // Standard PGN
                for (let i=0; i<this.moves.length; i++)
@@ -1330,7 +1397,7 @@ class ChessRules
                                pgn += ((i/2)+1) + ".";
                        pgn += this.moves[i].notation[0] + " ";
                }
-               pgn += "<br><br>";
+               pgn += "\n\n";
 
                // "Complete moves" PGN (helping in ambiguous cases)
                for (let i=0; i<this.moves.length; i++)
@@ -1340,6 +1407,6 @@ class ChessRules
                        pgn += this.moves[i].notation[1] + " ";
                }
 
-               return pgn;
+               return pgn + "\n";
        }
 }