X-Git-Url: https://git.auder.net/?a=blobdiff_plain;f=public%2Fjavascripts%2Fbase_rules.js;h=5763af7e1d0f86e40789ff7c9467ce66a3bd1334;hb=7931e479adf93c87771ded1892a0873af72ae46d;hp=8de0176c78b73a8fb6011d369bbba3dbdc74d465;hpb=2eef6db6cdce30fe785e601b88858c7fc743eee8;p=vchess.git diff --git a/public/javascripts/base_rules.js b/public/javascripts/base_rules.js index 8de0176c..5763af7e 100644 --- a/public/javascripts/base_rules.js +++ b/public/javascripts/base_rules.js @@ -54,12 +54,15 @@ 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}; @@ -95,66 +98,120 @@ class ChessRules this.INIT_COL_ROOK['w'][1] = k; break; default: - let num = parseInt(position[i].charAt(j)); + const num = parseInt(position[i].charAt(j)); if (!isNaN(num)) k += (num-1); } k++; } } - const epSq = this.moves.length > 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 @@ -165,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 ''; } @@ -192,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, @@ -205,7 +267,7 @@ class ChessRules // 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); } /////////////////// @@ -216,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]); } } @@ -254,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({ @@ -268,19 +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=0 && j0 && 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 (y0 && 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], {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, @@ -377,33 +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"); @@ -413,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 { @@ -485,9 +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) @@ -510,13 +567,12 @@ class ChessRules const color = this.turn; const oppCol = this.getOppCol(color); let potentialMoves = []; - const [sizeX,sizeY] = VariantRules.size; - for (let i=0; i 0) @@ -566,15 +621,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 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; @@ -702,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)) { @@ -723,12 +769,22 @@ class ChessRules { // (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: +// if (!this.states) this.states = []; +// if (!ingame) this.states.push(JSON.stringify(this.board)); + if (!!ingame) move.notation = [this.getNotation(move), this.getLongNotation(move)]; @@ -736,37 +792,47 @@ class ChessRules 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: +// if (JSON.stringify(this.board) != this.states[this.states.length-1]) +// debugger; +// this.states.pop(); } ////////////// // END OF GAME - // Basic check for 3 repetitions (in the last moves only) - // TODO: extend to usual 3-repetition recognition (storing FEN with move?) + // Check for 3 repetitions (position + flags + turn) checkRepetition() { - 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 ? @@ -814,7 +880,7 @@ 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() { @@ -825,7 +891,7 @@ class ChessRules // NOTE: works also for extinction chess because depth is 3... getComputerMove() { - 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) @@ -835,7 +901,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()) + { + // Try mate (for other variants) + const score = this.checkGameEnd(); + if (score != "1/2") + finish = true; + } this.undo(moves1[i]); if (finish) return moves1[i]; @@ -893,8 +966,7 @@ class ChessRules 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 { return (color=="w" ? 1 : -1) * (b.eval - a.eval); }); @@ -920,7 +992,7 @@ class ChessRules alphabeta(depth, alpha, beta) { - const maxeval = VariantRules.INFINITY; + const maxeval = V.INFINITY; const color = this.turn; if (!this.atLeastOneMove()) { @@ -966,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) @@ -1073,7 +1143,7 @@ class ChessRules // "Flush remainder" fen += emptyCount; } - if (i < sizeX - 1) + if (i < V.size.x - 1) fen += "/"; //separate rows } return fen; @@ -1095,15 +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.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 = ""; @@ -1132,9 +1201,8 @@ class ChessRules 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); + 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 }