X-Git-Url: https://git.auder.net/?a=blobdiff_plain;f=public%2Fjavascripts%2Fbase_rules.js;h=ba77d7187538a7d98749c257c5ff10a39ab416c2;hb=6e62b1c7d177585003e923d423025dff280a7525;hp=2a962a964c726181c2a480a86cdad7a969137cb4;hpb=8ddc00a072c5a4aa679e5a420a7f9664d18e03f3;p=vchess.git diff --git a/public/javascripts/base_rules.js b/public/javascripts/base_rules.js index 2a962a96..ba77d718 100644 --- a/public/javascripts/base_rules.js +++ b/public/javascripts/base_rules.js @@ -31,91 +31,58 @@ 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; } - ///////////////// - // INITIALIZATION - - // fen == "position [flags [turn]]" - constructor(fen, moves) - { - this.moves = moves; - // 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) + // Check if FEN describe a position + static IsGoodFen(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]}; //squares of white and black king - const fenParts = fen.split(" "); - const position = fenParts[0].split("/"); - for (let i=0; i= 2) + return true; + } + + // For FEN checking + static IsGoodTurn(turn) + { + return ["w","b"].includes(turn); + } + + // For FEN checking + static IsGoodFlags(flags) + { + return !!flags.match(/^[01]{4,4}$/); + } + + static IsGoodEnpassant(enpassant) + { + if (enpassant != "-") { - if (!V.IsGoodFlags(fenParts[1])) + const ep = V.SquareToCoords(fenParsed.enpassant); + if (isNaN(ep.x) || !V.OnBoard(ep)) return false; } - // 3) Check turn (if present) - if (fenParts.length >= 3) + return true; + } + + // 3 --> d (column number to letter) + static CoordToColumn(colnum) + { + return String.fromCharCode(97 + colnum); + } + + // d --> 3 (column letter to number) + static ColumnToCoord(colnum) + { + return String.fromCharCode(97 + colnum); + } + + // 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]; + if (this.getPiece(sx,sy) == V.PAWN && Math.abs(sx - ex) == 2) { - if (!["w","b"].includes(fenParts[2])) - return false; + return { + x: (sx + ex)/2, + y: sy + }; } - return true; + return undefined; //default } - // For FEN checking - static IsGoodFlags(flags) + // Can thing on square1 take thing on square2 + canTake([x1,y1], [x2,y2]) { - return !!flags.match(/^[01]{4,4}$/); + 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; } - // Turn diagram fen into double array ["wb","wp","bk",...] - static GetBoard(fen) + // Flags part of the FEN string + getFlagsFen() { - const rows = fen.split(" ")[0].split("/"); + 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; i0 ? this.moves[L-1] : null); } - // Pieces codes + // Pieces codes (for a clearer code) static get PAWN() { return 'p'; } static get ROOK() { return 'r'; } static get KNIGHT() { return 'n'; } @@ -222,15 +512,17 @@ class ChessRules static get KING() { return 'k'; } // For FEN checking: - static get PIECES() { + 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] ], @@ -238,50 +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(moveOrSquare) - { - if (typeof moveOrSquare === "string") - { - const square = moveOrSquare; - if (square == "-") - return undefined; - return { - x: square[0].charCodeAt()-97, - y: V.size.x-parseInt(square[1]) - }; - } - // Argument is a move: - const move = moveOrSquare; - const [sx,sy,ex] = [move.start.x,move.start.y,move.end.x]; - if (this.getPiece(sx,sy) == V.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) @@ -304,7 +553,8 @@ class ChessRules } } - // 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({ @@ -341,13 +591,8 @@ class ChessRules return mv; } - // Is (x,y) on the chessboard? - static OnBoard(x,y) - { - return (x>=0 && x=0 && y= 0 && x+shift < sizeX && x+shift != lastRank) + if (x+shiftX >= 0 && x+shiftX < sizeX) //TODO: always true { - // 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.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:pawnColor,p:p})); - // Captures - if (y>0 && this.board[x+shift][y-1] != V.EMPTY - && this.canTake([x,y], [x+shift,y-1])) + if (y + shiftY >= 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})); - } - if (y0 ? this.epSquares[Lep-1] : undefined); - if (!!epSquare && epSquare.x == x+shift && Math.abs(epSquare.y - y) == 1) + if (V.HasEnpassant) { - const epStep = epSquare.y - y; - let 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; @@ -471,7 +707,8 @@ class ChessRules // What are the queen moves from square x,y ? getPotentialQueenMoves(sq) { - 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 ? @@ -501,13 +738,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; } @@ -550,17 +789,12 @@ class ChessRules return moves; } - /////////////////// + //////////////////// // MOVES VALIDATION - canIplay(side, [x,y]) - { - return (this.turn == side && 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) ); } @@ -569,10 +803,17 @@ class ChessRules { 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; @@ -582,13 +823,14 @@ class ChessRules { for (let j=0; j= V.THRESHOLD_MATE); if (!finish && !this.atLeastOneMove()) { - // Try mate (for other variants) + // Test mate (for other variants) const score = this.checkGameEnd(); if (score != "1/2") finish = true; @@ -930,12 +1162,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]); } } @@ -968,7 +1205,6 @@ class ChessRules 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 { 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; @@ -1067,113 +1304,9 @@ class ChessRules return evaluation; } - //////////// - // FEN utils - - // Setup the initial random (assymetric) position - static GenRandInitFen() - { - let pieces = { "w": new Array(8), "b": new Array(8) }; - // Shuffle pieces on first and last rank - for (let c of ["w","b"]) - { - let positions = _.range(8); - - // Get random squares for bishops - let randIndex = 2 * _.random(3); - let bishop1Pos = positions[randIndex]; - // The second bishop must be on a square of different color - let randIndex_tmp = 2 * _.random(3) + 1; - let bishop2Pos = positions[randIndex_tmp]; - // Remove chosen squares - positions.splice(Math.max(randIndex,randIndex_tmp), 1); - positions.splice(Math.min(randIndex,randIndex_tmp), 1); - - // Get random squares for knights - randIndex = _.random(5); - let knight1Pos = positions[randIndex]; - positions.splice(randIndex, 1); - randIndex = _.random(4); - let knight2Pos = positions[randIndex]; - positions.splice(randIndex, 1); - - // Get random square for queen - randIndex = _.random(3); - let queenPos = positions[randIndex]; - positions.splice(randIndex, 1); - - // Rooks and king positions are now fixed, because of the ordering rook-king-rook - let rook1Pos = positions[0]; - let kingPos = positions[1]; - let rook2Pos = positions[2]; - - // Finally put the shuffled pieces in the board array - pieces[c][rook1Pos] = 'r'; - pieces[c][knight1Pos] = 'n'; - pieces[c][bishop1Pos] = 'b'; - pieces[c][queenPos] = 'q'; - pieces[c][kingPos] = 'k'; - pieces[c][bishop2Pos] = 'b'; - pieces[c][knight2Pos] = 'n'; - pieces[c][rook2Pos] = 'r'; - } - return pieces["b"].join("") + - "/pppppppp/8/8/8/8/PPPPPPPP/" + - pieces["w"].join("").toUpperCase() + - " 1111 w"; //add flags + turn - } - - // Return current fen according to pieces+colors state - getFen() - { - return this.getBaseFen() + " " + this.getFlagsFen() + " " + this.turn; - } - - // Position part of the FEN string - getBaseFen() - { - let fen = ""; - for (let i=0; i 0) - { - // Add empty squares in-between - fen += emptyCount; - emptyCount = 0; - } - fen += V.board2fen(this.board[i][j]); - } - } - if (emptyCount > 0) - { - // "Flush remainder" - fen += emptyCount; - } - if (i < V.size.x - 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 getNotation(move) @@ -1182,7 +1315,7 @@ class ChessRules return (move.end.y < move.start.y ? "0-0-0" : "0-0"); // Translate final square - const finalSquare = String.fromCharCode(97 + move.end.y) + (V.size.x-move.end.x); + const finalSquare = V.CoordsToSquare(move.end); const piece = this.getPiece(move.start.x, move.start.y); if (piece == V.PAWN) @@ -1192,12 +1325,12 @@ class ChessRules 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; } @@ -1213,10 +1346,8 @@ 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 + // 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