X-Git-Url: https://git.auder.net/?a=blobdiff_plain;f=public%2Fjavascripts%2Fbase_rules.js;h=5763af7e1d0f86e40789ff7c9467ce66a3bd1334;hb=7931e479adf93c87771ded1892a0873af72ae46d;hp=d4315c73724b5572437e3e7b3203b12bc7fefda4;hpb=c605216162dfc09cbbe61b0969f8890f28625372;p=vchess.git diff --git a/public/javascripts/base_rules.js b/public/javascripts/base_rules.js index d4315c73..5763af7e 100644 --- a/public/javascripts/base_rules.js +++ b/public/javascripts/base_rules.js @@ -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,]} @@ -50,17 +54,20 @@ class ChessRules constructor(fen, moves) { this.moves = moves; - // Use fen string to initialize variables, flags and board - this.board = VariantRules.GetBoard(fen); - this.setFlags(fen); + // Use fen string to initialize variables, flags, turn and board + const fenParts = fen.split(" "); + this.board = V.GetBoard(fenParts[0]); + this.setFlags(fenParts[1]); //NOTE: fenParts[1] might be undefined + this.setTurn(fenParts[2]); //Same note this.initVariables(fen); } + // Some additional variables from FEN (variant dependant) initVariables(fen) { 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 0 ? this.getEpSquare(this.lastMove) : undefined; + const epSq = (this.moves.length > 0 ? this.getEpSquare(this.lastMove) : undefined); this.epSquares = [ epSq ]; } + // Check if FEN describe a position + static IsGoodFen(fen) + { + const fenParts = fen.split(" "); + if (fenParts.length== 0 || fenParts.length > 3) + return false; + // 1) Check position + const position = fenParts[0]; + const rows = position.split("/"); + if (rows.length != V.size.x) + return false; + for (let row of rows) + { + let sumElts = 0; + for (let i=0; i= 2) + { + if (!V.IsGoodFlags(fenParts[1])) + return false; + } + // 3) Check turn (if present) + if (fenParts.length == 3) + { + if (!["w","b"].includes(fenParts[2])) + return false; + } + return true; + } + + // For FEN checking + static IsGoodFlags(flags) + { + return !!flags.match(/^[01]{4,4}$/); + } + // Turn diagram fen into double array ["wb","wp","bk",...] static GetBoard(fen) { - let rows = fen.split(" ")[0].split("/"); - const [sizeX,sizeY] = VariantRules.size; - let board = doubleArray(sizeX, sizeY, ""); + const rows = fen.split(" ")[0].split("/"); + let board = doubleArray(V.size.x, V.size.y, ""); for (let i=0; i0 ? this.moves[L-1] : null; - } - get turn() { - return this.moves.length%2==0 ? 'w' : 'b'; + return (L>0 ? this.moves[L-1] : null); } // Pieces codes @@ -162,6 +222,11 @@ class ChessRules static get QUEEN() { return 'q'; } static get KING() { return 'k'; } + // For FEN checking: + static get PIECES() { + return [V.PAWN,V.ROOK,V.KNIGHT,V.BISHOP,V.QUEEN,V.KING]; + } + // Empty square static get EMPTY() { return ''; } @@ -189,7 +254,7 @@ class ChessRules getEpSquare(move) { const [sx,sy,ex] = [move.start.x,move.start.y,move.end.x]; - if (this.getPiece(sx,sy) == VariantRules.PAWN && Math.abs(sx - ex) == 2) + if (this.getPiece(sx,sy) == V.PAWN && Math.abs(sx - ex) == 2) { return { x: (sx + ex)/2, @@ -199,10 +264,10 @@ 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); + return this.getColor(x1,y1) !== this.getColor(x2,y2); } /////////////////// @@ -213,17 +278,17 @@ class ChessRules { switch (this.getPiece(x,y)) { - case VariantRules.PAWN: + case V.PAWN: return this.getPotentialPawnMoves([x,y]); - case VariantRules.ROOK: + case V.ROOK: return this.getPotentialRookMoves([x,y]); - case VariantRules.KNIGHT: + case V.KNIGHT: return this.getPotentialKnightMoves([x,y]); - case VariantRules.BISHOP: + case V.BISHOP: return this.getPotentialBishopMoves([x,y]); - case VariantRules.QUEEN: + case V.QUEEN: return this.getPotentialQueenMoves([x,y]); - case VariantRules.KING: + case V.KING: return this.getPotentialKingMoves([x,y]); } } @@ -251,7 +316,7 @@ class ChessRules }); // The opponent piece disappears if we take it - if (this.board[ex][ey] != VariantRules.EMPTY) + if (this.board[ex][ey] != V.EMPTY) { mv.vanish.push( new PiPo({ @@ -265,18 +330,23 @@ class ChessRules return mv; } + // Is (x,y) on the chessboard? + static OnBoard(x,y) + { + return (x>=0 && x=0 && y=0 && i=0 && j=0 && i<8 && j>=0 && j<8 && this.canTake([x,y], [i,j])) + if (V.OnBoard(i,j) && this.canTake([x,y], [i,j])) moves.push(this.getBasicMove([x,y], [i,j])); } 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; let moves = []; - const V = VariantRules; - const [sizeX,sizeY] = VariantRules.size; + const [sizeX,sizeY] = [V.size.x,V.size.y]; const shift = (color == "w" ? -1 : 1); - const firstRank = (color == 'w' ? sizeY-1 : 0); - const startRank = (color == "w" ? sizeY-2 : 1); - const lastRank = (color == "w" ? 0 : sizeY-1); + const firstRank = (color == 'w' ? sizeX-1 : 0); + const startRank = (color == "w" ? sizeX-2 : 1); + const lastRank = (color == "w" ? 0 : sizeX-1); if (x+shift >= 0 && x+shift < sizeX && x+shift != lastRank) { @@ -308,7 +377,7 @@ class ChessRules if (this.board[x+shift][y] == V.EMPTY) { moves.push(this.getBasicMove([x,y], [x+shift,y])); - // Next condition because variants with pawns on 1st rank generally allow them to jump + // Next condition because variants with pawns on 1st rank allow them to jump if ([startRank,firstRank].includes(x) && this.board[x+2*shift][y] == V.EMPTY) { // Two squares jump @@ -316,35 +385,48 @@ class ChessRules } } // Captures - if (y>0 && this.canTake([x,y], [x+shift,y-1]) && this.board[x+shift][y-1] != V.EMPTY) + if (y>0 && this.board[x+shift][y-1] != V.EMPTY + && this.canTake([x,y], [x+shift,y-1])) + { moves.push(this.getBasicMove([x,y], [x+shift,y-1])); - if (y { // 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})); - if (y0 && this.board[x+shift][y-1] != V.EMPTY + && this.canTake([x,y], [x+shift,y-1])) + { + moves.push(this.getBasicMove([x,y], [x+shift,y-1], {c:pawnColor,p:p})); + } + if (y0 ? this.epSquares[Lep-1] : undefined; + const epSquare = (Lep>0 ? this.epSquares[Lep-1] : undefined); if (!!epSquare && epSquare.x == x+shift && Math.abs(epSquare.y - y) == 1) { - let epStep = epSquare.y - y; - var enpassantMove = this.getBasicMove([x,y], [x+shift,y+epStep]); + const epStep = epSquare.y - y; + let enpassantMove = this.getBasicMove([x,y], [x+shift,y+epStep]); enpassantMove.vanish.push({ x: x, y: y+epStep, @@ -360,32 +442,30 @@ class ChessRules // What are the rook moves from square x,y ? getPotentialRookMoves(sq) { - return this.getSlideNJumpMoves(sq, VariantRules.steps[VariantRules.ROOK]); + return this.getSlideNJumpMoves(sq, V.steps[V.ROOK]); } // What are the knight moves from square x,y ? getPotentialKnightMoves(sq) { - return this.getSlideNJumpMoves(sq, VariantRules.steps[VariantRules.KNIGHT], "oneStep"); + return this.getSlideNJumpMoves(sq, V.steps[V.KNIGHT], "oneStep"); } // What are the bishop moves from square x,y ? getPotentialBishopMoves(sq) { - return this.getSlideNJumpMoves(sq, VariantRules.steps[VariantRules.BISHOP]); + return this.getSlideNJumpMoves(sq, V.steps[V.BISHOP]); } // What are the queen moves from square x,y ? getPotentialQueenMoves(sq) { - 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, V.steps[V.ROOK].concat(V.steps[V.BISHOP]), "oneStep"); @@ -395,17 +475,14 @@ class ChessRules getCastleMoves([x,y]) { const c = this.getColor(x,y); - const [sizeX,sizeY] = VariantRules.size; - if (x != (c=="w" ? sizeX-1 : 0) || y != this.INIT_COL_KING[c]) + if (x != (c=="w" ? V.size.x-1 : 0) || y != this.INIT_COL_KING[c]) return []; //x isn't first rank, or king has moved (shortcut) - const V = VariantRules; - // Castling ? const oppCol = this.getOppCol(c); let moves = []; let i = 0; - const finalSquares = [ [2,3], [sizeY-2,sizeY-3] ]; //king, then rook + const finalSquares = [ [2,3], [V.size.y-2,V.size.y-3] ]; //king, then rook castlingCheck: for (let castleSide=0; castleSide < 2; castleSide++) //large, then small { @@ -417,7 +494,7 @@ class ChessRules 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 && + 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))))) { @@ -467,8 +544,7 @@ class ChessRules canIplay(side, [x,y]) { - return ((side=='w' && this.moves.length%2==0) || (side=='b' && this.moves.length%2==1)) - && this.getColor(x,y) == side; + return (this.turn == side && this.getColor(x,y) == side); } getPossibleMovesFrom(sq) @@ -477,7 +553,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) @@ -491,13 +567,12 @@ class ChessRules const color = this.turn; const oppCol = this.getOppCol(color); let potentialMoves = []; - const [sizeX,sizeY] = VariantRules.size; - for (var i=0; i 0) @@ -533,7 +607,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) @@ -544,17 +618,17 @@ 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) { for (let c of colors) { let pawnShift = (c=="w" ? 1 : -1); - if (x+pawnShift>=0 && x+pawnShift<8) + if (x+pawnShift>=0 && x+pawnShift=0 && y+i<8 && this.getPiece(x+pawnShift,y+i)==VariantRules.PAWN + if (y+i>=0 && y+i=0 && rx<8 && ry>=0 && ry<8 && this.board[rx][ry] == VariantRules.EMPTY - && !oneStep) + while (V.OnBoard(rx,ry) && this.board[rx][ry] == V.EMPTY && !oneStep) { rx += step[0]; ry += step[1]; } - if (rx>=0 && rx<8 && ry>=0 && ry<8 && this.board[rx][ry] != VariantRules.EMPTY - && this.getPiece(rx,ry) == piece && colors.includes(this.getColor(rx,ry))) + if (V.OnBoard(rx,ry) && this.getPiece(rx,ry) === piece + && colors.includes(this.getColor(rx,ry))) { return true; } @@ -623,22 +693,22 @@ 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; this.play(move); - let res = this.isAttacked(this.kingPos[color], this.getOppCol(color)); + let res = this.isAttacked(this.kingPos[color], [this.getOppCol(color)]); this.undo(move); 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); const color = this.turn; //opponent - let res = this.isAttacked(this.kingPos[color], this.getOppCol(color)) + let res = this.isAttacked(this.kingPos[color], [this.getOppCol(color)]) ? [ JSON.parse(JSON.stringify(this.kingPos[color])) ] //need to duplicate! : [ ]; this.undo(move); @@ -649,7 +719,7 @@ class ChessRules static PlayOnBoard(board, move) { for (let psq of move.vanish) - board[psq.x][psq.y] = VariantRules.EMPTY; + board[psq.x][psq.y] = V.EMPTY; for (let psq of move.appear) board[psq.x][psq.y] = psq.c + psq.p; } @@ -657,7 +727,7 @@ class ChessRules static UndoOnBoard(board, move) { for (let psq of move.appear) - board[psq.x][psq.y] = VariantRules.EMPTY; + board[psq.x][psq.y] = V.EMPTY; for (let psq of move.vanish) board[psq.x][psq.y] = psq.c + psq.p; } @@ -666,12 +736,11 @@ class ChessRules updateVariables(move) { const piece = this.getPiece(move.start.x,move.start.y); - const c = this.getColor(move.start.x,move.start.y); - const [sizeX,sizeY] = VariantRules.size; - const firstRank = (c == "w" ? sizeX-1 : 0); + const c = this.turn; + const firstRank = (c == "w" ? V.size.x-1 : 0); // Update king position + flags - if (piece == VariantRules.KING && move.appear.length > 0) + if (piece == V.KING && move.appear.length > 0) { this.kingPos[c][0] = move.appear[0].x; this.kingPos[c][1] = move.appear[0].y; @@ -679,7 +748,7 @@ class ChessRules return; } const oppCol = this.getOppCol(c); - const oppFirstRank = (sizeX-1) - firstRank; + const oppFirstRank = (V.size.x-1) - firstRank; if (move.start.x == firstRank //our rook moves? && this.INIT_COL_ROOK[c].includes(move.start.y)) { @@ -694,14 +763,22 @@ 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 const c = this.getColor(move.start.x,move.start.y); - if (this.getPiece(move.start.x,move.start.y) == VariantRules.KING) + if (this.getPiece(move.start.x,move.start.y) == V.KING) this.kingPos[c] = [move.start.x, move.start.y]; } + // Hash of position+flags+turn after a move is played (to detect repetitions) + getHashState() + { + return hex_md5(this.getFen()); + } + play(move, ingame) { // DEBUG: @@ -709,50 +786,56 @@ class ChessRules // if (!ingame) this.states.push(JSON.stringify(this.board)); if (!!ingame) - move.notation = this.getNotation(move); + move.notation = [this.getNotation(move), this.getLongNotation(move)]; move.flags = JSON.stringify(this.flags); //save flags (for undo) this.updateVariables(move); this.moves.push(move); this.epSquares.push( this.getEpSquare(move) ); - VariantRules.PlayOnBoard(this.board, move); + V.PlayOnBoard(this.board, move); + + if (!!ingame) + move.hash = this.getHashState(); } undo(move) { - VariantRules.UndoOnBoard(this.board, move); + V.UndoOnBoard(this.board, move); this.epSquares.pop(); this.moves.pop(); this.unupdateVariables(move); this.parseFlags(JSON.parse(move.flags)); // DEBUG: -// let state = this.states.pop(); -// if (JSON.stringify(this.board) != state) +// if (JSON.stringify(this.board) != this.states[this.states.length-1]) // debugger; +// this.states.pop(); } ////////////// // END OF GAME + // Check for 3 repetitions (position + flags + turn) checkRepetition() { - // Check for 3 repetitions - if (this.moves.length >= 8) + 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 { return (elt >= 3); }); } + // Is game over ? And if yes, what is the score ? checkGameOver() { if (this.checkRepetition()) @@ -770,7 +853,7 @@ class ChessRules { 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], [this.getOppCol(color)])) return "1/2"; // OK, checkmate return color == "w" ? "0-1" : "1-0"; @@ -797,79 +880,134 @@ class ChessRules static get THRESHOLD_MATE() { // At this value or above, the game is over - return VariantRules.INFINITY; + return V.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) + // NOTE: works also for extinction chess because depth is 3... + getComputerMove() { - const maxeval = VariantRules.INFINITY; + const maxeval = V.INFINITY; const color = this.turn; - if (!moves1) - 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))) + { + this.play(moves1[i]); + let finish = (Math.abs(this.evalPosition()) >= V.THRESHOLD_MATE); + if (!finish && !this.atLeastOneMove()) + { + // Try mate (for other variants) + const score = this.checkGameEnd(); + if (score != "1/2") + finish = true; + } + this.undo(moves1[i]); + if (finish) + return moves1[i]; + } // Rank moves using a min-max at depth 2 for (let i=0; i eval2)) + eval2 = evalPos; + this.undo(moves2[j]); + } + } + else { - this.play(moves2[j]); - let evalPos = this.evalPosition(); - if ((color == "w" && evalPos < eval2) || (color=="b" && evalPos > eval2)) - eval2 = evalPos; - this.undo(moves2[j]); + 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)) + 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= 3: may take a while, so we control time + const timeStart = Date.now(); + + // Skip depth 3+ if we found a checkmate (or if we are checkmated in 1...) + if (V.SEARCH_DEPTH >= 3 && Math.abs(moves1[0].eval) < V.THRESHOLD_MATE) { - // TODO: show current analyzed move for depth 3, allow stopping eval (return moves1[0]) for (let i=0; i= 5000) //more than 5 seconds + return currentBest; //depth 2 at least 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(V.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; + //console.log(moves1.map(m => { return [this.getNotation(m), m.eval]; })); - let candidates = [0]; //indices of candidates moves + candidates = [0]; for (let j=1; j { return [this.getNotation(m), m.eval]; })); return moves1[_.sample(candidates, 1)]; } alphabeta(depth, alpha, beta) { - const maxeval = VariantRules.INFINITY; + const maxeval = V.INFINITY; const color = this.turn; if (!this.atLeastOneMove()) { 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") { @@ -900,17 +1038,16 @@ class ChessRules evalPosition() { - const [sizeX,sizeY] = VariantRules.size; let evaluation = 0; - //Just count material for now - for (let i=0; i 0) @@ -1007,13 +1143,13 @@ class ChessRules // "Flush remainder" fen += emptyCount; } - if (i < sizeX - 1) + if (i < V.size.x - 1) fen += "/"; //separate rows } return fen; } - // Overridable.. + // Flags part of the FEN string getFlagsFen() { let fen = ""; @@ -1021,7 +1157,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; } @@ -1029,21 +1165,14 @@ 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 == V.KING) //castle + return (move.end.y < move.start.y ? "0-0-0" : "0-0"); // Translate final square - const finalSquare = - String.fromCharCode(97 + move.end.y) + (VariantRules.size[0]-move.end.x); + const finalSquare = String.fromCharCode(97 + move.end.y) + (V.size.x-move.end.x); const piece = this.getPiece(move.start.x, move.start.y); - if (piece == VariantRules.PAWN) + if (piece == V.PAWN) { // Pawn move let notation = ""; @@ -1068,28 +1197,49 @@ class ChessRules } } + // Complete the usual notation, may be required for de-ambiguification + getLongNotation(move) + { + const startSquare = + String.fromCharCode(97 + move.start.y) + (V.size.x-move.start.x); + const finalSquare = String.fromCharCode(97 + move.end.y) + (V.size.x-move.end.x); + return startSquare + finalSquare; //not encoding move. But short+long is enough + } + // 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"]
'; const d = new Date(); const opponent = mode=="human" ? "Anonymous" : "Computer"; pgn += '[Variant "' + variant + '"]
'; - pgn += '[Date "' + d.getFullYear() + '-' + (d.getMonth()+1) + '-' + d.getDate() + '"]
'; + pgn += '[Date "' + d.getFullYear() + '-' + (d.getMonth()+1) + + '-' + zeroPad(d.getDate()) + '"]
'; pgn += '[White "' + (mycolor=='w'?'Myself':opponent) + '"]
'; pgn += '[Black "' + (mycolor=='b'?'Myself':opponent) + '"]
'; - pgn += '[Fen "' + fenStart + '"]
'; + pgn += '[FenStart "' + fenStart + '"]
'; + pgn += '[Fen "' + this.getFen() + '"]
'; pgn += '[Result "' + score + '"]

'; + // Standard PGN + for (let i=0; i