Draft code reorganisation (+ fix Alice rules + stateless VariantRules object)
[vchess.git] / public / javascripts / base_rules.js
index 6cdae32..e405cba 100644 (file)
@@ -64,22 +64,19 @@ 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
+               // 3) Check moves count
+               if (!fenParsed.movesCount || !(parseInt(fenParsed.movesCount) >= 0))
+                       return false;
+               // 4) Check flags
                if (V.HasFlags && (!fenParsed.flags || !V.IsGoodFlags(fenParsed.flags)))
                        return false;
-               // 4) Check enpassant
-               if (V.HasEnpassant)
+               // 5) Check enpassant
+               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 +110,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 +159,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 +189,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,
@@ -195,16 +218,12 @@ class ChessRules
                return (this.turn == side && this.getColor(x,y) == side);
        }
 
-       // On which squares is opponent under check after our move ? (for interface)
-       getCheckSquares(move)
+       // On which squares is color under check ? (for interface)
+       getCheckSquares(color)
        {
-               this.play(move);
-               const color = this.turn; //opponent
-               let res = this.isAttacked(this.kingPos[color], [this.getOppCol(color)])
+               return this.isAttacked(this.kingPos[color], [this.getOppCol(color)])
                        ? [JSON.parse(JSON.stringify(this.kingPos[color]))] //need to duplicate!
                        : [];
-               this.undo(move);
-               return res;
        }
 
        /////////////
@@ -221,31 +240,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';
@@ -271,8 +291,9 @@ class ChessRules
                {
                        position: fenParts[0],
                        turn: fenParts[1],
+                       movesCount: fenParts[2],
                };
-               let nextIdx = 2;
+               let nextIdx = 3;
                if (V.HasFlags)
                        Object.assign(res, {flags: fenParts[nextIdx++]});
                if (V.HasEnpassant)
@@ -283,7 +304,8 @@ class ChessRules
        // Return current fen (game state)
        getFen()
        {
-               return this.getBaseFen() + " " + this.turn +
+               return this.getBaseFen() + " " +
+                       this.getTurnFen() + " " + this.movesCount +
                        (V.HasFlags ? (" " + this.getFlagsFen()) : "") +
                        (V.HasEnpassant ? (" " + this.getEnpassantFen()) : "");
        }
@@ -321,6 +343,11 @@ class ChessRules
                return position;
        }
 
+       getTurnFen()
+       {
+               return this.turn;
+       }
+
        // Flags part of the FEN string
        getFlagsFen()
        {
@@ -379,12 +406,12 @@ class ChessRules
        // INITIALIZATION
 
        // Fen string fully describes the game state
-       constructor(fen, moves)
+       constructor(fen)
        {
-               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.movesCount = parseInt(fenParsed.movesCount);
                this.setOtherVariables(fen);
        }
 
@@ -476,12 +503,6 @@ class ChessRules
                return (color=="w" ? "b" : "w");
        }
 
-       get lastMove()
-       {
-               const L = this.moves.length;
-               return (L>0 ? this.moves[L-1] : null);
-       }
-
        // Pieces codes (for a clearer code)
        static get PAWN() { return 'p'; }
        static get ROOK() { return 'r'; }
@@ -532,7 +553,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({
@@ -569,7 +591,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);
@@ -605,7 +628,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]
@@ -684,7 +708,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 ?
@@ -714,13 +739,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;
                                }
@@ -766,9 +793,9 @@ class ChessRules
        ////////////////////
        // MOVES VALIDATION
 
+       // For the interface: possible moves for the current turn from square sq
        getPossibleMovesFrom(sq)
        {
-               // Assuming color is right (already checked)
                return this.filterValid( this.getPotentialMovesFrom(sq) );
        }
 
@@ -777,10 +804,17 @@ class ChessRules
        {
                if (moves.length == 0)
                        return [];
-               return moves.filter(m => { return !this.underCheck(m); });
+               const color = this.turn;
+               return moves.filter(m => {
+                       this.play(m);
+                       const res = !this.underCheck(color);
+                       this.undo(m);
+                       return res;
+               });
        }
 
-       // 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;
@@ -790,13 +824,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);
        }
 
@@ -826,7 +861,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)
@@ -912,14 +947,10 @@ class ChessRules
                return false;
        }
 
-       // Is current player under check after his move ?
-       underCheck(move)
+       // Is color under check after his move ?
+       underCheck(color)
        {
-               const color = this.turn;
-               this.play(move);
-               let res = this.isAttacked(this.kingPos[color], [this.getOppCol(color)]);
-               this.undo(move);
-               return res;
+               return this.isAttacked(this.kingPos[color], [this.getOppCol(color)]);
        }
 
        /////////////////
@@ -942,11 +973,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 = this.getPiece(move.start.x,move.start.y);
-               const c = this.turn;
+               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
@@ -954,22 +1002,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;
+                       }
                }
        }
 
@@ -990,7 +1043,7 @@ class ChessRules
 //             if (!ingame) this.states.push(this.getFen());
 
                if (!!ingame)
-                       move.notation = [this.getNotation(move), this.getLongNotation(move)];
+                       move.notation = this.getNotation(move);
 
                if (V.HasFlags)
                        move.flags = JSON.stringify(this.aggregateFlags()); //save flags (for undo)
@@ -998,7 +1051,7 @@ class ChessRules
                        this.epSquares.push( this.getEpSquare(move) );
                V.PlayOnBoard(this.board, move);
                this.turn = this.getOppCol(this.turn);
-               this.moves.push(move);
+               this.movesCount++;
                this.updateVariables(move);
 
                if (!!ingame)
@@ -1016,7 +1069,7 @@ class ChessRules
                        this.disaggregateFlags(JSON.parse(move.flags));
                V.UndoOnBoard(this.board, move);
                this.turn = this.getOppCol(this.turn);
-               this.moves.pop();
+               this.movesCount--;
                this.unupdateVariables(move);
 
                // DEBUG:
@@ -1028,32 +1081,9 @@ class ChessRules
        ///////////////
        // END OF GAME
 
-       // Check for 3 repetitions (position + flags + turn)
-       checkRepetition()
-       {
-               if (!this.hashStates)
-                       this.hashStates = {};
-               const startIndex =
-                       Object.values(this.hashStates).reduce((a,b) => { return a+b; }, 0)
-               // Update this.hashStates with last move (or all moves if continuation)
-               // NOTE: redundant storage, but faster and moderate size
-               for (let i=startIndex; i<this.moves.length; i++)
-               {
-                       const move = this.moves[i];
-                       if (!this.hashStates[move.hash])
-                               this.hashStates[move.hash] = 1;
-                       else
-                               this.hashStates[move.hash]++;
-               }
-               return Object.values(this.hashStates).some(elt => { return (elt >= 3); });
-       }
-
        // Is game over ? And if yes, what is the score ?
        checkGameOver()
        {
-               if (this.checkRepetition())
-                       return "1/2";
-
                if (this.atLeastOneMove()) // game not over
                        return "*";
 
@@ -1069,7 +1099,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 +1157,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 +1179,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 +1222,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++)
@@ -1268,6 +1304,7 @@ class ChessRules
        /////////////////////////
 
        // Context: just before move is played, turn hasn't changed
+       // TODO: un-ambiguous notation (switch on piece type, check directions...)
        getNotation(move)
        {
                if (move.appear.length == 2 && move.appear[0].p == V.KING) //castle
@@ -1284,12 +1321,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;
                }
@@ -1310,36 +1347,33 @@ class ChessRules
        }
 
        // The score is already computed when calling this function
-       getPGN(mycolor, score, fenStart, mode)
+       getPGN(moves, 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>';
-
-               // Standard PGN
-               for (let i=0; i<this.moves.length; i++)
-               {
-                       if (i % 2 == 0)
-                               pgn += ((i/2)+1) + ".";
-                       pgn += this.moves[i].notation[0] + " ";
-               }
-               pgn += "<br><br>";
-
-               // "Complete moves" PGN (helping in ambiguous cases)
-               for (let i=0; i<this.moves.length; i++)
+               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 += '[Fen "' + fenStart + '"]\n';
+               pgn += '[Result "' + score + '"]\n\n';
+
+               // Print moves
+               for (let i=0; i<moves.length; i++)
                {
                        if (i % 2 == 0)
                                pgn += ((i/2)+1) + ".";
-                       pgn += this.moves[i].notation[1] + " ";
+                       pgn += moves[i].notation + " ";
                }
 
-               return pgn;
+               return pgn + "\n";
        }
 }