X-Git-Url: https://git.auder.net/?p=vchess.git;a=blobdiff_plain;f=public%2Fjavascripts%2Fbase_rules.js;h=e405cbaa4e595da56210f09a7e0920c27c406be1;hp=ee5ddd02a2372c28f70ef04ab01edb15c8df7390;hb=b6487fb9c41705187cf97215fc9e8f86a59057c7;hpb=15952ada1d73262371654351ab8a4471f64010fd diff --git a/public/javascripts/base_rules.js b/public/javascripts/base_rules.js index ee5ddd02..e405cbaa 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,]} @@ -27,48 +31,403 @@ class Move // NOTE: x coords = top to bottom; y = left to right (from white player perspective) class ChessRules { + ////////////// + // MISC UTILS + + static get HasFlags() { return true; } //some variants don't have flags + + static get HasEnpassant() { return true; } //some variants don't have ep. + // Path to pieces static getPpath(b) { return b; //usual pieces in pieces/ folder } + // Turn "wb" into "B" (for FEN) static board2fen(b) { return b[0]=='w' ? b[1].toUpperCase() : b[1]; } + // Turn "p" into "bp" (for board) static fen2board(f) { return f.charCodeAt()<=90 ? "w"+f.toLowerCase() : "b"+f; } - ///////////////// + // Check if FEN describe a position + static IsGoodFen(fen) + { + const fenParsed = V.ParseFen(fen); + // 1) Check position + if (!V.IsGoodPosition(fenParsed.position)) + return false; + // 2) Check turn + if (!fenParsed.turn || !V.IsGoodTurn(fenParsed.turn)) + return false; + // 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; + // 5) Check enpassant + if (V.HasEnpassant && + (!fenParsed.enpassant || !V.IsGoodEnpassant(fenParsed.enpassant))) + { + return false; + } + return true; + } + + // Is position part of the FEN a priori correct? + static IsGoodPosition(position) + { + if (position.length == 0) + return false; + 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 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 + }; + } + + // {x:0,y:4} --> e8 + static CoordsToSquare(coords) + { + return V.CoordToColumn(coords.y) + (V.size.x - coords.x); + } + + // Aggregates flags into one object + aggregateFlags() + { + return this.castleFlags; + } + + // Reverse operation + disaggregateFlags(flags) + { + this.castleFlags = flags; + } + + // En-passant square, if any + getEpSquare(moveOrSquare) + { + if (!moveOrSquare) + return undefined; + if (typeof moveOrSquare === "string") + { + const square = moveOrSquare; + if (square == "-") + return undefined; + return V.SquareToCoords(square); + } + // Argument is a move: + const move = moveOrSquare; + const [sx,sy,ex] = [move.start.x,move.start.y,move.end.x]; + // 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, + y: sy + }; + } + return undefined; //default + } + + // Can thing on square1 take thing on square2 + canTake([x1,y1], [x2,y2]) + { + return this.getColor(x1,y1) !== this.getColor(x2,y2); + } + + // Is (x,y) on the chessboard? + static OnBoard(x,y) + { + return (x>=0 && x=0 && y 0) + { + // Add empty squares in-between + position += emptyCount; + emptyCount = 0; + } + position += V.board2fen(this.board[i][j]); + } + } + if (emptyCount > 0) + { + // "Flush remainder" + position += emptyCount; + } + if (i < V.size.x - 1) + position += "/"; //separate rows + } + return position; + } + + getTurnFen() + { + return this.turn; + } + + // Flags part of the FEN string + getFlagsFen() + { + let flags = ""; + // Add castling flags + for (let i of ['w','b']) + { + for (let j=0; j<2; j++) + flags += (this.castleFlags[i][j] ? '1' : '0'); + } + return flags; + } + + // Enpassant part of the FEN string + getEnpassantFen() + { + const L = this.epSquares.length; + if (!this.epSquares[L-1]) + return "-"; //no en-passant + return V.CoordsToSquare(this.epSquares[L-1]); + } + + // Turn position fen into double array ["wb","wp","bk",...] + static GetBoard(position) + { + const rows = position.split("/"); + let board = doubleArray(V.size.x, V.size.y, ""); + for (let i=0; i 0 ? this.getEpSquare(this.lastMove) : undefined; - this.epSquares = [ epSq ]; } - // Turn diagram fen into double array ["wb","wp","bk",...] - static GetBoard(fen) + // Some additional variables from FEN (variant dependant) + setOtherVariables(fen) { - let rows = fen.split(" ")[0].split("/"); - const [sizeX,sizeY] = VariantRules.size; - let board = doubleArray(sizeX, sizeY, ""); - for (let i=0; i0 ? this.moves[L-1] : null; + // Piece type on square (i,j). 'undefined' if square is empty + getPiece(i,j) + { + return this.board[i][j].charAt(1); } - get turn() { - return this.moves.length%2==0 ? 'w' : 'b'; + + // Get opponent color + getOppCol(color) + { + return (color=="w" ? "b" : "w"); } - // Pieces codes + // Pieces codes (for a clearer code) static get PAWN() { return 'p'; } static get ROOK() { return 'r'; } static get KNIGHT() { return 'n'; } @@ -161,11 +511,18 @@ 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 ''; } + static get EMPTY() { return ""; } // Some pieces movements - static get steps() { + static get steps() + { return { '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] ], @@ -173,38 +530,7 @@ class ChessRules }; } - // 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) - { - return { - x: (sx + ex)/2, - y: sy - }; - } - return undefined; //default - } - - // Can thing on square1 take thing on square2 - canTake([x1,y1], [x2,y2]) - { - return this.getColor(x1,y1) != this.getColor(x2,y2); - } - - /////////////////// + //////////////////// // MOVES GENERATION // All possible moves from selected square (assumption: color is OK) @@ -212,22 +538,23 @@ 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]); } } - // 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({ @@ -250,7 +577,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({ @@ -264,19 +591,18 @@ 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); let moves = []; - const [sizeX,sizeY] = VariantRules.size; outerLoop: for (let step of steps) { let i = x + step[0]; let j = y + step[1]; - while (i>=0 && i=0 && j=0 && i=0 && j= 0 && x+shift < sizeX && x+shift != lastRank) + // NOTE: next condition is generally true (no pawn on last rank) + if (x+shiftX >= 0 && x+shiftX < sizeX) { - // Normal moves - if (this.board[x+shift][y] == V.EMPTY) + const finalPieces = x + shiftX == lastRank + ? [V.ROOK,V.KNIGHT,V.BISHOP,V.QUEEN] + : [V.PAWN] + // One square forward + if (this.board[x+shiftX][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) + for (let piece of finalPieces) + { + moves.push(this.getBasicMove([x,y], [x+shiftX,y], + {c:pawnColor,p:piece})); + } + // Next condition because pawns on 1st rank can generally jump + if ([startRank,firstRank].includes(x) + && this.board[x+2*shiftX][y] == V.EMPTY) { // Two squares jump - moves.push(this.getBasicMove([x,y], [x+2*shift,y])); + moves.push(this.getBasicMove([x,y], [x+2*shiftX,y])); } } // 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])); - } - if (y { - // Normal move - if (this.board[x+shift][y] == V.EMPTY) - 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:pawnColor,p:p})); - } - if (y= 0 && y + shiftY < sizeY + && this.board[x+shiftX][y+shiftY] != V.EMPTY + && this.canTake([x,y], [x+shiftX,y+shiftY])) { - moves.push(this.getBasicMove([x,y], [x+shift,y+1], {c:pawnColor,p:p})); + for (let piece of finalPieces) + { + moves.push(this.getBasicMove([x,y], [x+shiftX,y+shiftY], + {c:pawnColor,p:piece})); + } } - }); + } } - // En passant - const Lep = this.epSquares.length; - const epSquare = Lep>0 ? this.epSquares[Lep-1] : undefined; - if (!!epSquare && epSquare.x == x+shift && Math.abs(epSquare.y - y) == 1) + if (V.HasEnpassant) { - let epStep = epSquare.y - y; - var enpassantMove = this.getBasicMove([x,y], [x+shift,y+epStep]); - enpassantMove.vanish.push({ - x: x, - y: y+epStep, - p: 'p', - c: this.getColor(x,y+epStep) - }); - moves.push(enpassantMove); + // En passant + const Lep = this.epSquares.length; + const epSquare = this.epSquares[Lep-1]; //always at least one element + if (!!epSquare && epSquare.x == x+shiftX && Math.abs(epSquare.y - y) == 1) + { + let enpassantMove = this.getBasicMove([x,y], [epSquare.x,epSquare.y]); + enpassantMove.vanish.push({ + x: x, + y: epSquare.y, + p: 'p', + c: this.getColor(x,epSquare.y) + }); + moves.push(enpassantMove); + } } return moves; @@ -373,32 +690,31 @@ 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])); + 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"); @@ -408,17 +724,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 { @@ -426,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; } @@ -475,47 +790,48 @@ class ChessRules return moves; } - /////////////////// + //////////////////// // MOVES VALIDATION - canIplay(side, [x,y]) - { - return ((side=='w' && this.moves.length%2==0) || (side=='b' && this.moves.length%2==1)) - && this.getColor(x,y) == side; - } - + // 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) ); } - // 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 []; - 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; const oppCol = this.getOppCol(color); let potentialMoves = []; - const [sizeX,sizeY] = VariantRules.size; - for (let i=0; i 0) @@ -546,7 +861,7 @@ class ChessRules return false; } - // Check if pieces of color in array 'colors' are attacking 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) @@ -560,15 +875,14 @@ class ChessRules // Is square x,y attacked by 'colors' pawns ? isAttackedByPawn([x,y], colors) { - const [sizeX,sizeY] = VariantRules.size; for (let c of colors) { let pawnShift = (c=="w" ? 1 : -1); - if (x+pawnShift>=0 && x+pawnShift=0 && x+pawnShift=0 && y+i=0 && y+i=0 && rx=0 && ry=0 && rx=0 && ry= 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 - 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.castleFlags[c] = [false,false]; + if (V.HasFlags) + this.castleFlags[c] = [false,false]; return; } - const oppCol = this.getOppCol(c); - const oppFirstRank = (sizeX-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; + } } } - // After move is undo-ed, un-update variables (flags are reset) - // TODO: more symmetry, by storing flags increment in move... + // After move is undo-ed *and flags resetted*, un-update other variables + // 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]; } play(move, ingame) { - if (!!ingame) - move.notation = [this.getNotation(move), this.getLongNotation(move)]; + // DEBUG: +// if (!this.states) this.states = []; +// if (!ingame) this.states.push(this.getFen()); - move.flags = JSON.stringify(this.flags); //save flags (for undo) + if (!!ingame) + move.notation = this.getNotation(move); + + if (V.HasFlags) + move.flags = JSON.stringify(this.aggregateFlags()); //save flags (for undo) + if (V.HasEnpassant) + this.epSquares.push( this.getEpSquare(move) ); + V.PlayOnBoard(this.board, move); + this.turn = this.getOppCol(this.turn); + this.movesCount++; this.updateVariables(move); - this.moves.push(move); - this.epSquares.push( this.getEpSquare(move) ); - VariantRules.PlayOnBoard(this.board, move); + + if (!!ingame) + { + // Hash of current game state *after move*, to detect repetitions + move.hash = hex_md5(this.getFen()); + } } undo(move) { - VariantRules.UndoOnBoard(this.board, move); - this.epSquares.pop(); - this.moves.pop(); + if (V.HasEnpassant) + this.epSquares.pop(); + if (V.HasFlags) + this.disaggregateFlags(JSON.parse(move.flags)); + V.UndoOnBoard(this.board, move); + this.turn = this.getOppCol(this.turn); + this.movesCount--; this.unupdateVariables(move); - this.parseFlags(JSON.parse(move.flags)); + + // DEBUG: +// if (this.getFen() != this.states[this.states.length-1]) +// debugger; +// this.states.pop(); } - ////////////// + /////////////// // END OF GAME - // Basic check for 3 repetitions (in the last moves only) - checkRepetition() - { - if (this.moves.length >= 8) - { - const L = this.moves.length; - if (_.isEqual(this.moves[L-1], this.moves[L-5]) && - _.isEqual(this.moves[L-2], this.moves[L-6]) && - _.isEqual(this.moves[L-3], this.moves[L-7]) && - _.isEqual(this.moves[L-4], this.moves[L-8])) - { - return true; - } - } - return false; - } - // Is game over ? And if yes, what is the score ? checkGameOver() { - if (this.checkRepetition()) - return "1/2"; - if (this.atLeastOneMove()) // game not over return "*"; @@ -783,14 +1099,15 @@ 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"); } - //////// - //ENGINE + /////////////// + // ENGINE PLAY // Pieces values - static get VALUES() { + static get VALUES() + { return { 'p': 1, 'r': 5, @@ -801,25 +1118,20 @@ class ChessRules }; } - static get INFINITY() { - return 9999; //"checkmate" (unreachable eval) - } + // "Checkmate" (unreachable eval) + static get INFINITY() { return 9999; } - static get THRESHOLD_MATE() { - // At this value or above, the game is over - return VariantRules.INFINITY; - } + // At this value or above, the game is over + static get THRESHOLD_MATE() { return V.INFINITY; } - static get SEARCH_DEPTH() { - return 3; //2 for high branching factor, 4 for small (Loser chess) - } + // Search depth: 2 for high branching factor, 4 for small (Loser chess, eg.) + static get SEARCH_DEPTH() { return 3; } // Assumption: at least one legal move // NOTE: works also for extinction chess because depth is 3... getComputerMove() { - this.shouldReturn = false; - const maxeval = VariantRules.INFINITY; + const maxeval = V.INFINITY; const color = this.turn; // Some variants may show a bigger moves list to the human (Switching), // thus the argument "computer" below (which is generally ignored) @@ -829,7 +1141,14 @@ class ChessRules for (let i of _.shuffle(_.range(moves1.length))) { this.play(moves1[i]); - const finish = (Math.abs(this.evalPosition()) >= VariantRules.THRESHOLD_MATE); + let finish = (Math.abs(this.evalPosition()) >= V.THRESHOLD_MATE); + if (!finish && !this.atLeastOneMove()) + { + // Test mate (for other variants) + const score = this.checkGameEnd(); + if (score != "1/2") + finish = true; + } this.undo(moves1[i]); if (finish) return moves1[i]; @@ -838,12 +1157,14 @@ class ChessRules // Rank moves using a min-max at depth 2 for (let i=0; i eval2)) + if ((color == "w" && evalPos < eval2) + || (color=="b" && evalPos > eval2)) + { eval2 = evalPos; + } this.undo(moves2[j]); } } @@ -868,37 +1192,42 @@ class ChessRules 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 (VariantRules.SEARCH_DEPTH >= 3 - && Math.abs(moves1[0].eval) < VariantRules.THRESHOLD_MATE) + 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(VariantRules.SEARCH_DEPTH-1, -maxeval, maxeval); + 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 0) - { - // Add empty squares in-between - fen += emptyCount; - emptyCount = 0; - } - fen += VariantRules.board2fen(this.board[i][j]); - } - } - if (emptyCount > 0) - { - // "Flush remainder" - fen += emptyCount; - } - if (i < sizeX - 1) - fen += "/"; //separate rows - } - return fen; - } - - // Flags part of the FEN string - getFlagsFen() - { - let fen = ""; - // Add castling flags - for (let i of ['w','b']) - { - for (let j=0; j<2; j++) - fen += this.castleFlags[i][j] ? '1' : '0'; - } - return fen; - } + ///////////////////////// + // MOVES + GAME NOTATION + ///////////////////////// // 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 == VariantRules.KING) //castle + 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 = V.CoordsToSquare(move.end); const piece = this.getPiece(move.start.x, move.start.y); - if (piece == VariantRules.PAWN) + if (piece == V.PAWN) { // Pawn move let notation = ""; 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; } @@ -1120,46 +1342,38 @@ class ChessRules // Complete the usual notation, may be required for de-ambiguification getLongNotation(move) { - const startSquare = - String.fromCharCode(97 + move.start.y) + (VariantRules.size[0]-move.start.x); - const finalSquare = - String.fromCharCode(97 + move.end.y) + (VariantRules.size[0]-move.end.x); - return startSquare + finalSquare; //not encoding move. But short+long is enough + // 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) + getPGN(moves, mycolor, score, fenStart, mode) { - const zeroPad = x => { return (x<10 ? "0" : "") + x; }; let pgn = ""; - pgn += '[Site "vchess.club"]
'; - const d = new Date(); + pgn += '[Site "vchess.club"]\n'; 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