Step toward a one-page application
[vchess.git] / public / javascripts / base_rules.js
index 50766b0..9f6ec9e 100644 (file)
@@ -66,10 +66,13 @@ class ChessRules
                // 2) Check 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
+               // 5) Check enpassant
                if (V.HasEnpassant &&
                        (!fenParsed.enpassant || !V.IsGoodEnpassant(fenParsed.enpassant)))
                {
@@ -186,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 (move.appear[0].p == 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,7 +221,7 @@ class ChessRules
        // On which squares is color under check ? (for interface)
        getCheckSquares(color)
        {
-               return this.isAttacked(this.kingPos[color], [this.getOppCol(color)])
+               return this.isAttacked(this.kingPos[color], [V.GetOppCol(color)])
                        ? [JSON.parse(JSON.stringify(this.kingPos[color]))] //need to duplicate!
                        : [];
        }
@@ -276,7 +280,7 @@ class ChessRules
                return pieces["b"].join("") +
                        "/pppppppp/8/8/8/8/PPPPPPPP/" +
                        pieces["w"].join("").toUpperCase() +
-                       " w 1111 -"; //add turn + flags + enpassant
+                       " w 1111 -"; //add turn + flags + enpassant
        }
 
        // "Parse" FEN: just return untransformed string data
@@ -287,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)
@@ -299,7 +304,8 @@ class ChessRules
        // Return current fen (game state)
        getFen()
        {
-               return this.getBaseFen() + " " + this.getTurnFen() +
+               return this.getBaseFen() + " " +
+                       this.getTurnFen() + " " + this.movesCount +
                        (V.HasFlags ? (" " + this.getFlagsFen()) : "") +
                        (V.HasEnpassant ? (" " + this.getEnpassantFen()) : "");
        }
@@ -400,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[0]; //[0] to work with MarseilleRules
+               this.movesCount = parseInt(fenParsed.movesCount);
                this.setOtherVariables(fen);
        }
 
@@ -492,15 +498,15 @@ class ChessRules
        }
 
        // Get opponent color
-       getOppCol(color)
+       static GetOppCol(color)
        {
                return (color=="w" ? "b" : "w");
        }
 
-       get lastMove()
+       // Get next color (for compatibility with 3 and 4 players games)
+       static GetNextCol(color)
        {
-               const L = this.moves.length;
-               return (L>0 ? this.moves[L-1] : null);
+               return V.GetOppCol(color);
        }
 
        // Pieces codes (for a clearer code)
@@ -728,7 +734,7 @@ class ChessRules
                        return []; //x isn't first rank, or king has moved (shortcut)
 
                // Castling ?
-               const oppCol = this.getOppCol(c);
+               const oppCol = V.GetOppCol(c);
                let moves = [];
                let i = 0;
                const finalSquares = [ [2,3], [V.size.y-2,V.size.y-3] ]; //king, then rook
@@ -818,7 +824,7 @@ class ChessRules
        getAllValidMoves()
        {
                const color = this.turn;
-               const oppCol = this.getOppCol(color);
+               const oppCol = V.GetOppCol(color);
                let potentialMoves = [];
                for (let i=0; i<V.size.x; i++)
                {
@@ -839,7 +845,7 @@ class ChessRules
        atLeastOneMove()
        {
                const color = this.turn;
-               const oppCol = this.getOppCol(color);
+               const oppCol = V.GetOppCol(color);
                for (let i=0; i<V.size.x; i++)
                {
                        for (let j=0; j<V.size.y; j++)
@@ -950,7 +956,7 @@ class ChessRules
        // Is color under check after his move ?
        underCheck(color)
        {
-               return this.isAttacked(this.kingPos[color], [this.getOppCol(color)]);
+               return this.isAttacked(this.kingPos[color], [V.GetOppCol(color)]);
        }
 
        /////////////////
@@ -993,7 +999,7 @@ class ChessRules
                if (c == "c") //if (!["w","b"].includes(c))
                {
                        // 'c = move.vanish[0].c' doesn't work for Checkered
-                       c = this.getOppCol(this.turn);
+                       c = V.GetOppCol(this.turn);
                }
                const firstRank = (c == "w" ? V.size.x-1 : 0);
 
@@ -1009,7 +1015,7 @@ class ChessRules
                if (V.HasFlags)
                {
                        // Update castling flags if rooks are moved
-                       const oppCol = this.getOppCol(c);
+                       const oppCol = V.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))
@@ -1036,29 +1042,23 @@ class ChessRules
                        this.kingPos[c] = [move.start.x, move.start.y];
        }
 
-       play(move, ingame)
+       play(move)
        {
                // DEBUG:
 //             if (!this.states) this.states = [];
-//             if (!ingame) this.states.push(this.getFen());
-
-               if (!!ingame)
-                       move.notation = [this.getNotation(move), this.getLongNotation(move)];
+//             const stateFen = this.getBaseFen() + this.getTurnFen() + this.getFlagsFen();
+//             this.states.push(stateFen);
 
                if (V.HasFlags)
                        move.flags = JSON.stringify(this.aggregateFlags()); //save flags (for undo)
                if (V.HasEnpassant)
                        this.epSquares.push( this.getEpSquare(move) );
+               if (!move.color)
+                       move.color = this.turn; //for interface
                V.PlayOnBoard(this.board, move);
-               this.turn = this.getOppCol(this.turn);
-               this.moves.push(move);
+               this.turn = V.GetOppCol(this.turn);
+               this.movesCount++;
                this.updateVariables(move);
-
-               if (!!ingame)
-               {
-                       // Hash of current game state *after move*, to detect repetitions
-                       move.hash = hex_md5(this.getFen());
-               }
        }
 
        undo(move)
@@ -1068,58 +1068,29 @@ class ChessRules
                if (V.HasFlags)
                        this.disaggregateFlags(JSON.parse(move.flags));
                V.UndoOnBoard(this.board, move);
-               this.turn = this.getOppCol(this.turn);
-               this.moves.pop();
+               this.turn = V.GetOppCol(this.turn);
+               this.movesCount--;
                this.unupdateVariables(move);
 
                // DEBUG:
-//             if (this.getFen() != this.states[this.states.length-1])
-//                     debugger;
+//             const stateFen = this.getBaseFen() + this.getTurnFen() + this.getFlagsFen();
+//             if (stateFen != this.states[this.states.length-1]) debugger;
 //             this.states.pop();
        }
 
        ///////////////
        // END OF GAME
 
-       // Check for 3 repetitions (position + flags + turn)
-       checkRepetition()
+       // What is the score ? (Interesting if game is over)
+       getCurrentScore()
        {
-               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 "*";
 
                // Game over
-               return this.checkGameEnd();
-       }
-
-       // No moves are possible: compute score
-       checkGameEnd()
-       {
                const color = this.turn;
                // No valid move: stalemate or checkmate?
-               if (!this.isAttacked(this.kingPos[color], [this.getOppCol(color)]))
+               if (!this.isAttacked(this.kingPos[color], [V.GetOppCol(color)]))
                        return "1/2";
                // OK, checkmate
                return (color == "w" ? "0-1" : "1-0");
@@ -1165,11 +1136,10 @@ class ChessRules
                {
                        this.play(moves1[i]);
                        let finish = (Math.abs(this.evalPosition()) >= V.THRESHOLD_MATE);
-                       if (!finish && !this.atLeastOneMove())
+                       if (!finish)
                        {
-                               // Test mate (for other variants)
-                               const score = this.checkGameEnd();
-                               if (score != "1/2")
+                               const score = this.getCurrentScore();
+                               if (["1-0","0-1"].includes(score))
                                        finish = true;
                        }
                        this.undo(moves1[i]);
@@ -1183,8 +1153,9 @@ class ChessRules
                        // Initial self evaluation is very low: "I'm checkmated"
                        moves1[i].eval = (color=="w" ? -1 : 1) * maxeval;
                        this.play(moves1[i]);
+                       const score1 = this.getCurrentScore();
                        let eval2 = undefined;
-                       if (this.atLeastOneMove())
+                       if (score1 == "*")
                        {
                                // Initial enemy evaluation is very low too, for him
                                eval2 = (color=="w" ? 1 : -1) * maxeval;
@@ -1193,15 +1164,10 @@ class ChessRules
                                for (let j=0; j<moves2.length; j++)
                                {
                                        this.play(moves2[j]);
-                                       let evalPos = undefined;
-                                       if (this.atLeastOneMove())
-                                               evalPos = this.evalPosition()
-                                       else
-                                       {
-                                               // Working with scores is more accurate (necessary for Loser variant)
-                                               const score = this.checkGameEnd();
-                                               evalPos = (score=="1/2" ? 0 : (score=="1-0" ? 1 : -1) * maxeval);
-                                       }
+                                       const score2 = this.getCurrentScore();
+                                       const evalPos = score2 == "*"
+                                               ? this.evalPosition()
+                                               : (score2=="1/2" ? 0 : (score2=="1-0" ? 1 : -1) * maxeval);
                                        if ((color == "w" && evalPos < eval2)
                                                || (color=="b" && evalPos > eval2))
                                        {
@@ -1211,10 +1177,7 @@ class ChessRules
                                }
                        }
                        else
-                       {
-                               const score = this.checkGameEnd();
-                               eval2 = (score=="1/2" ? 0 : (score=="1-0" ? 1 : -1) * maxeval);
-                       }
+                               eval2 = (score1=="1/2" ? 0 : (score1=="1-0" ? 1 : -1) * maxeval);
                        if ((color=="w" && eval2 > moves1[i].eval)
                                || (color=="b" && eval2 < moves1[i].eval))
                        {
@@ -1262,17 +1225,9 @@ class ChessRules
   {
                const maxeval = V.INFINITY;
                const color = this.turn;
-               if (!this.atLeastOneMove())
-               {
-                       switch (this.checkGameEnd())
-                       {
-                               case "1/2":
-                                       return 0;
-                               default:
-                                       const score = this.checkGameEnd();
-                                       return (score=="1/2" ? 0 : (score=="1-0" ? 1 : -1) * maxeval);
-                       }
-               }
+               const score = this.getCurrentScore();
+               if (score != "*")
+                       return (score=="1/2" ? 0 : (score=="1-0" ? 1 : -1) * maxeval);
                if (depth == 0)
       return this.evalPosition();
                const moves = this.getAllValidMoves("computer");
@@ -1327,6 +1282,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
@@ -1360,45 +1316,4 @@ class ChessRules
                                (move.vanish.length > move.appear.length ? "x" : "") + finalSquare;
                }
        }
-
-       // Complete the usual notation, may be required for de-ambiguification
-       getLongNotation(move)
-       {
-               // Not encoding move. But short+long is enough
-               return V.CoordsToSquare(move.start) + V.CoordsToSquare(move.end);
-       }
-
-       // The score is already computed when calling this function
-       getPGN(mycolor, score, fenStart, mode)
-       {
-               let pgn = "";
-               pgn += '[Site "vchess.club"]<br>';
-               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++)
-               {
-                       if (i % 2 == 0)
-                               pgn += ((i/2)+1) + ".";
-                       pgn += this.moves[i].notation[1] + " ";
-               }
-
-               return pgn;
-       }
 }