X-Git-Url: https://git.auder.net/?a=blobdiff_plain;f=public%2Fjavascripts%2Fbase_rules.js;h=5763af7e1d0f86e40789ff7c9467ce66a3bd1334;hb=7931e479adf93c87771ded1892a0873af72ae46d;hp=d8ec34a0f7969d25f62f675e52bd8e8659e10cd2;hpb=33ee191669b40afbd03d8fbf5406382fa2de1bf9;p=vchess.git diff --git a/public/javascripts/base_rules.js b/public/javascripts/base_rules.js index d8ec34a0..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,]} @@ -46,120 +50,168 @@ class ChessRules ///////////////// // INITIALIZATION - // fen = "position flags epSquare movesCount" + // fen == "position flags" constructor(fen, moves) { this.moves = moves; - // Use fen string to initialize variables, flags and board + // 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); - this.flags = VariantRules.GetFlags(fen); - this.board = VariantRules.GetBoard(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); + 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) { - const digits = fenParts[2].split(","); //3,2 ... - epSq = { x:Number.parseInt(digits[0]), y:Number.parseInt(digits[1]) }; + if (!V.IsGoodFlags(fenParts[1])) + return false; } - this.epSquares = [ epSq ]; - this.movesCount = Number.parseInt(fenParts[3]); + // 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("/"); - let [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.movesCount%2==0 ? 'w' : 'b'; + return (L>0 ? this.moves[L-1] : null); } // Pieces codes @@ -170,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 ''; } @@ -179,15 +236,25 @@ class ChessRules 'r': [ [-1,0],[1,0],[0,-1],[0,1] ], 'n': [ [-1,-2],[-1,2],[1,-2],[1,2],[-2,-1],[-2,1],[2,-1],[2,1] ], 'b': [ [-1,-1],[-1,1],[1,-1],[1,1] ], - 'q': [ [-1,0],[1,0],[0,-1],[0,1],[-1,-1],[-1,1],[1,-1],[1,1] ] }; } + // Aggregates flags into one object + get flags() { + return this.castleFlags; + } + + // Reverse operation + parseFlags(flags) + { + this.castleFlags = flags; + } + // En-passant square, if any 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, @@ -197,10 +264,10 @@ class ChessRules return undefined; //default } - // can color1 take color2? - canTake(color1, color2) + // Can thing on square1 take thing on square2 + canTake([x1,y1], [x2,y2]) { - return color1 != color2; + return this.getColor(x1,y1) !== this.getColor(x2,y2); } /////////////////// @@ -209,35 +276,33 @@ class ChessRules // All possible moves from selected square (assumption: color is OK) getPotentialMovesFrom([x,y]) { - let c = this.getColor(x,y); - // Fill possible moves according to piece type switch (this.getPiece(x,y)) { - case VariantRules.PAWN: - return this.getPotentialPawnMoves(x,y,c); - case VariantRules.ROOK: - return this.getPotentialRookMoves(x,y,c); - case VariantRules.KNIGHT: - return this.getPotentialKnightMoves(x,y,c); - case VariantRules.BISHOP: - return this.getPotentialBishopMoves(x,y,c); - case VariantRules.QUEEN: - return this.getPotentialQueenMoves(x,y,c); - case VariantRules.KING: - return this.getPotentialKingMoves(x,y,c); + case V.PAWN: + return this.getPotentialPawnMoves([x,y]); + case V.ROOK: + return this.getPotentialRookMoves([x,y]); + case V.KNIGHT: + return this.getPotentialKnightMoves([x,y]); + case V.BISHOP: + return this.getPotentialBishopMoves([x,y]); + case V.QUEEN: + return this.getPotentialQueenMoves([x,y]); + case V.KING: + return this.getPotentialKingMoves([x,y]); } } // Build a regular move from its initial and destination squares; tr: transformation - getBasicMove(sx, sy, ex, ey, tr) + getBasicMove([sx,sy], [ex,ey], tr) { - var mv = new Move({ + let mv = new Move({ appear: [ new PiPo({ x: ex, y: ey, - c: this.getColor(sx,sy), - p: !!tr ? tr : this.getPiece(sx,sy) + c: !!tr ? tr.c : this.getColor(sx,sy), + p: !!tr ? tr.p : this.getPiece(sx,sy) }) ], vanish: [ @@ -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,95 +330,103 @@ 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(color, this.getColor(i,j))) - moves.push(this.getBasicMove(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" ? - getPotentialPawnMoves(x, y, color) + // What are the pawn moves from square x,y ? + getPotentialPawnMoves([x,y]) { - var moves = []; - var V = VariantRules; - let [sizeX,sizeY] = VariantRules.size; - let shift = (color == "w" ? -1 : 1); - let startRank = (color == "w" ? sizeY-2 : 1); - let lastRank = (color == "w" ? 0 : sizeY-1); + const color = this.turn; + let moves = []; + const [sizeX,sizeY] = [V.size.x,V.size.y]; + const shift = (color == "w" ? -1 : 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) { // Normal moves if (this.board[x+shift][y] == V.EMPTY) { - moves.push(this.getBasicMove(x, y, x+shift, y)); - if (x==startRank && this.board[x+2*shift][y] == V.EMPTY) + moves.push(this.getBasicMove([x,y], [x+shift,y])); + // 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 - moves.push(this.getBasicMove(x, y, x+2*shift, y)); + moves.push(this.getBasicMove([x,y], [x+2*shift,y])); } } // Captures - if (y>0 && this.canTake(this.getColor(x,y), this.getColor(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)); + 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, p)); + moves.push(this.getBasicMove([x,y], [x+shift,y], {c:pawnColor,p:p})); // Captures - if (y>0 && this.canTake(this.getColor(x,y), this.getColor(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, p)); + 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, @@ -367,59 +440,53 @@ class ChessRules } // What are the rook moves from square x,y ? - getPotentialRookMoves(x, y, color) + getPotentialRookMoves(sq) { - return this.getSlideNJumpMoves( - x, y, color, VariantRules.steps[VariantRules.ROOK]); + return this.getSlideNJumpMoves(sq, V.steps[V.ROOK]); } // What are the knight moves from square x,y ? - getPotentialKnightMoves(x, y, color) + getPotentialKnightMoves(sq) { - return this.getSlideNJumpMoves( - x, y, color, VariantRules.steps[VariantRules.KNIGHT], "oneStep"); + return this.getSlideNJumpMoves(sq, V.steps[V.KNIGHT], "oneStep"); } // What are the bishop moves from square x,y ? - getPotentialBishopMoves(x, y, color) + getPotentialBishopMoves(sq) { - return this.getSlideNJumpMoves( - x, y, color, VariantRules.steps[VariantRules.BISHOP]); + return this.getSlideNJumpMoves(sq, V.steps[V.BISHOP]); } // What are the queen moves from square x,y ? - getPotentialQueenMoves(x, y, color) + getPotentialQueenMoves(sq) { - return this.getSlideNJumpMoves( - x, y, color, VariantRules.steps[VariantRules.QUEEN]); + return this.getSlideNJumpMoves(sq, V.steps[V.ROOK].concat(V.steps[V.BISHOP])); } // What are the king moves from square x,y ? - getPotentialKingMoves(x, y, c) + getPotentialKingMoves(sq) { // Initialize with normal moves - var moves = this.getSlideNJumpMoves(x, y, c, - VariantRules.steps[VariantRules.QUEEN], "oneStep"); - - return moves.concat(this.getCastleMoves(x,y,c)); + let moves = this.getSlideNJumpMoves(sq, + V.steps[V.ROOK].concat(V.steps[V.BISHOP]), "oneStep"); + return moves.concat(this.getCastleMoves(sq)); } - getCastleMoves(x,y,c) + getCastleMoves([x,y]) { - if (x != (c=="w" ? 7 : 0) || y != this.INIT_COL_KING[c]) + const c = this.getColor(x,y); + 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], [6,5] ]; //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 { - if (!this.flags[c][castleSide]) + if (!this.castleFlags[c][castleSide]) continue; // If this code is reached, rooks and king are on initial position @@ -427,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))))) { @@ -475,11 +542,9 @@ class ChessRules /////////////////// // MOVES VALIDATION - canIplay(color, sq) + canIplay(side, [x,y]) { - return ((color=='w' && this.movesCount%2==0) - || (color=='b' && this.movesCount%2==1)) - && this.getColor(sq[0], sq[1]) == color; + return (this.turn == side && this.getColor(x,y) == side); } getPossibleMovesFrom(sq) @@ -488,29 +553,26 @@ 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) return []; - let color = this.getColor( moves[0].start.x, moves[0].start.y ); - return moves.filter(m => { - return !this.underCheck(m, color); - }); + return moves.filter(m => { return !this.underCheck(m); }); } // Search for all valid moves considering current turn (for engine and game end) - getAllValidMoves(color) + getAllValidMoves() { + const color = this.turn; const oppCol = this.getOppCol(color); - var potentialMoves = []; - let [sizeX,sizeY] = VariantRules.size; - for (var i=0; i 0) { - for (let i=0; i 0) + if (this.filterValid([moves[k]]).length > 0) return true; } } @@ -545,84 +607,85 @@ class ChessRules return false; } - // Check if pieces of color 'color' are attacking square x,y - isAttacked(sq, color) + // Check if pieces of color in array 'colors' are attacking square x,y + isAttacked(sq, colors) { - return (this.isAttackedByPawn(sq, color) - || this.isAttackedByRook(sq, color) - || this.isAttackedByKnight(sq, color) - || this.isAttackedByBishop(sq, color) - || this.isAttackedByQueen(sq, color) - || this.isAttackedByKing(sq, color)); + return (this.isAttackedByPawn(sq, colors) + || this.isAttackedByRook(sq, colors) + || this.isAttackedByKnight(sq, colors) + || this.isAttackedByBishop(sq, colors) + || this.isAttackedByQueen(sq, colors) + || this.isAttackedByKing(sq, colors)); } - // Is square x,y attacked by pawns of color c ? - isAttackedByPawn([x,y], c) + // Is square x,y attacked by 'colors' pawns ? + isAttackedByPawn([x,y], colors) { - let pawnShift = (c=="w" ? 1 : -1); - if (x+pawnShift>=0 && x+pawnShift<8) + for (let c of colors) { - for (let i of [-1,1]) + let pawnShift = (c=="w" ? 1 : -1); + if (x+pawnShift>=0 && x+pawnShift=0 && y+i<8 && this.getPiece(x+pawnShift,y+i)==VariantRules.PAWN - && this.getColor(x+pawnShift,y+i)==c) + for (let i of [-1,1]) { - return true; + 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 && this.getColor(rx,ry) == c) + if (V.OnBoard(rx,ry) && this.getPiece(rx,ry) === piece + && colors.includes(this.getColor(rx,ry))) { return true; } @@ -630,21 +693,23 @@ class ChessRules return false; } - // Is color c under check after move ? - underCheck(move, c) + // Is current player under check after his move ? + underCheck(move) { + const color = this.turn; this.play(move); - let res = this.isAttacked(this.kingPos[c], this.getOppCol(c)); + 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) ? - getCheckSquares(move, c) + // On which squares is opponent under check after our move ? + getCheckSquares(move) { this.play(move); - let res = this.isAttacked(this.kingPos[c], this.getOppCol(c)) - ? [ JSON.parse(JSON.stringify(this.kingPos[c])) ] //need to duplicate! + const color = this.turn; //opponent + let res = this.isAttacked(this.kingPos[color], [this.getOppCol(color)]) + ? [ JSON.parse(JSON.stringify(this.kingPos[color])) ] //need to duplicate! : [ ]; this.undo(move); return res; @@ -654,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; } @@ -662,110 +727,133 @@ 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; } - // Before move is played: + // Before move is played, update variables + flags updateVariables(move) { const piece = this.getPiece(move.start.x,move.start.y); - const c = this.getColor(move.start.x,move.start.y); - const firstRank = (c == "w" ? 7 : 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; - this.flags[c] = [false,false]; + this.castleFlags[c] = [false,false]; return; } const oppCol = this.getOppCol(c); - const oppFirstRank = 7 - 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)) { - const flagIdx = move.start.y == this.INIT_COL_ROOK[c][0] ? 0 : 1; - this.flags[c][flagIdx] = false; + 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[c].includes(move.end.y)) + && this.INIT_COL_ROOK[oppCol].includes(move.end.y)) { - const flagIdx = move.end.y == this.INIT_COL_ROOK[oppCol][0] ? 0 : 1; - this.flags[oppCol][flagIdx] = false; + const flagIdx = (move.end.y == this.INIT_COL_ROOK[oppCol][0] ? 0 : 1); + this.castleFlags[oppCol][flagIdx] = false; } } + // 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) == 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) { - // Save flags (for undo) - move.flags = JSON.stringify(this.flags); //TODO: less costly - this.updateVariables(move); + // DEBUG: +// if (!this.states) this.states = []; +// if (!ingame) this.states.push(JSON.stringify(this.board)); if (!!ingame) - { - move.notation = this.getNotation(move); - this.moves.push(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); - this.movesCount++; + V.PlayOnBoard(this.board, move); + + if (!!ingame) + move.hash = this.getHashState(); } - undo(move, ingame) + undo(move) { - VariantRules.UndoOnBoard(this.board, move); + V.UndoOnBoard(this.board, move); this.epSquares.pop(); - this.movesCount--; - - if (!!ingame) - this.moves.pop(); - - // Update king position, and reset stored/computed flags - const c = this.getColor(move.start.x,move.start.y); - if (this.getPiece(move.start.x,move.start.y) == VariantRules.KING) - this.kingPos[c] = [move.start.x, move.start.y]; - - this.flags = JSON.parse(move.flags); + this.moves.pop(); + this.unupdateVariables(move); + this.parseFlags(JSON.parse(move.flags)); + + // DEBUG: +// if (JSON.stringify(this.board) != this.states[this.states.length-1]) +// debugger; +// this.states.pop(); } ////////////// // END OF GAME - checkGameOver(color) + // 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); }); + } - if (this.atLeastOneMove(color)) - { - // game not over + // 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(color); + return this.checkGameEnd(); } - // Useful stand-alone for engine - checkGameEnd(color) + // 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], [this.getOppCol(color)])) return "1/2"; // OK, checkmate return color == "w" ? "0-1" : "1-0"; @@ -786,77 +874,147 @@ class ChessRules }; } - // Assumption: at least one legal move - getComputerMove(color) - { - const oppCol = this.getOppCol(color); + static get INFINITY() { + return 9999; //"checkmate" (unreachable eval) + } - // Rank moves using a min-max at depth 2 - let moves1 = this.getAllValidMoves(color); + static get THRESHOLD_MATE() { + // At this value or above, the game is over + return V.INFINITY; + } - for (let i=0; i= V.THRESHOLD_MATE); + if (!finish && !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]); + // Try mate (for other variants) + const score = this.checkGameEnd(); + if (score != "1/2") + finish = true; } - if ((color=="w" && eval2 > moves1[i].eval) || (color=="b" && eval2 < moves1[i].eval)) - moves1[i].eval = eval2; this.undo(moves1[i]); + if (finish) + return moves1[i]; } - moves1.sort( (a,b) => { return (color=="w" ? 1 : -1) * (b.eval - a.eval); }); - // TODO: show current analyzed move for depth 3, allow stopping eval (return moves1[0]) + // Rank moves using a min-max at depth 2 for (let i=0; i eval2)) + eval2 = evalPos; + this.undo(moves2[j]); + } + } + 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= 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) + { + 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(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]; })); + + candidates = [0]; + for (let j=1; j= beta) @@ -868,7 +1026,7 @@ class ChessRules for (let i=0; i= beta) @@ -880,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) @@ -992,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 = ""; @@ -1006,7 +1157,7 @@ class ChessRules for (let i of ['w','b']) { for (let j=0; j<2; j++) - fen += this.flags[i][j] ? '1' : '0'; + fen += (this.castleFlags[i][j] ? '1' : '0'); } return fen; } @@ -1014,28 +1165,21 @@ class ChessRules // Context: just before move is played, turn hasn't changed getNotation(move) { - if (move.appear.length == 2) - { - // 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 - let 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); - let piece = this.getPiece(move.start.x, move.start.y); - if (piece == VariantRules.PAWN) + const piece = this.getPiece(move.start.x, move.start.y); + if (piece == V.PAWN) { // Pawn move let notation = ""; - if (move.vanish.length > 1) + if (move.vanish.length > move.appear.length) { // Capture - let startColumn = String.fromCharCode(97 + move.start.y); + const startColumn = String.fromCharCode(97 + move.start.y); notation = startColumn + "x" + finalSquare; } else //no capture @@ -1048,30 +1192,54 @@ class ChessRules else { // Piece movement - return piece.toUpperCase() + (move.vanish.length > 1 ? "x" : "") + finalSquare; + return piece.toUpperCase() + + (move.vanish.length > move.appear.length ? "x" : "") + finalSquare; } } + // 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) + getPGN(mycolor, score, fenStart, mode) { + const zeroPad = x => { return (x<10 ? "0" : "") + x; }; let pgn = ""; pgn += '[Site "vchess.club"]
'; const d = new Date(); - pgn += '[Date "' + d.getFullYear() + '-' + d.getMonth() + '-' + d.getDate() + '"]
'; - pgn += '[White "' + (mycolor=='w'?'Myself':'Anonymous') + '"]
'; - pgn += '[Black "' + (mycolor=='b'?'Myself':'Anonymous') + '"]
'; - pgn += '[Fen "' + fenStart + '"]
'; + const opponent = mode=="human" ? "Anonymous" : "Computer"; + pgn += '[Variant "' + variant + '"]
'; + pgn += '[Date "' + d.getFullYear() + '-' + (d.getMonth()+1) + + '-' + zeroPad(d.getDate()) + '"]
'; + pgn += '[White "' + (mycolor=='w'?'Myself':opponent) + '"]
'; + pgn += '[Black "' + (mycolor=='b'?'Myself':opponent) + '"]
'; + pgn += '[FenStart "' + fenStart + '"]
'; + pgn += '[Fen "' + this.getFen() + '"]
'; pgn += '[Result "' + score + '"]

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