X-Git-Url: https://git.auder.net/?a=blobdiff_plain;f=public%2Fjavascripts%2Fbase_rules.js;h=26c744777c1b4a4b77f311363e372ce2dd63a3bb;hb=3c09dc498791ac478679bf2f42f441342c4fa22c;hp=576bdd9b0eb1b47cb4287ab977529a4531f85279;hpb=1d184b4c016a645228251ce984d4c980e60420b0;p=vchess.git diff --git a/public/javascripts/base_rules.js b/public/javascripts/base_rules.js index 576bdd9b..26c74477 100644 --- a/public/javascripts/base_rules.js +++ b/public/javascripts/base_rules.js @@ -46,14 +46,14 @@ class ChessRules ///////////////// // INITIALIZATION - // fen = "position flags epSquare movesCount" - constructor(fen) + // fen == "position flags" + constructor(fen, moves) { - this.moves = []; + this.moves = moves; // Use fen string to initialize variables, flags and board - this.initVariables(fen); - this.flags = VariantRules.GetFlags(fen); this.board = VariantRules.GetBoard(fen); + this.setFlags(fen); + this.initVariables(fen); } initVariables(fen) @@ -65,54 +65,48 @@ class ChessRules const position = fenParts[0].split("/"); for (let i=0; i 0 ? this.getEpSquare(this.lastMove) : undefined; this.epSquares = [ epSq ]; - this.movesCount = Number.parseInt(fenParts[3]); } // Turn diagram fen into double array ["wb","wp","bk",...] static GetBoard(fen) { let rows = fen.split(" ")[0].split("/"); - let [sizeX,sizeY] = VariantRules.size; + const [sizeX,sizeY] = VariantRules.size; let board = doubleArray(sizeX, sizeY, ""); for (let i=0; i0 ? this.moves[L-1] : null; } get turn() { - return this.movesCount%2==0 ? 'w' : 'b'; + return this.moves.length%2==0 ? 'w' : 'b'; } // Pieces codes @@ -178,10 +171,20 @@ 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) { @@ -196,10 +199,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); } /////////////////// @@ -208,35 +211,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); + return this.getPotentialPawnMoves([x,y]); case VariantRules.ROOK: - return this.getPotentialRookMoves(x,y,c); + return this.getPotentialRookMoves([x,y]); case VariantRules.KNIGHT: - return this.getPotentialKnightMoves(x,y,c); + return this.getPotentialKnightMoves([x,y]); case VariantRules.BISHOP: - return this.getPotentialBishopMoves(x,y,c); + return this.getPotentialBishopMoves([x,y]); case VariantRules.QUEEN: - return this.getPotentialQueenMoves(x,y,c); + return this.getPotentialQueenMoves([x,y]); case VariantRules.KING: - return this.getPotentialKingMoves(x,y,c); + 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: [ @@ -265,63 +266,60 @@ class ChessRules } // Generic method to find possible moves of non-pawn pieces ("sliding or jumping") - getSlideNJumpMoves(x, y, color, steps, oneStep) + getSlideNJumpMoves([x,y], steps, oneStep) { - var moves = []; - let [sizeX,sizeY] = VariantRules.size; + const color = this.getColor(x,y); + let moves = []; + const [sizeX,sizeY] = VariantRules.size; outerLoop: for (let step of steps) { - var i = x + step[0]; - var j = y + step[1]; - while (i>=0 && i=0 && j=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 (i>=0 && i<8 && j>=0 && j<8 && 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) + 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 V = VariantRules; + const [sizeX,sizeY] = VariantRules.size; + 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); 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 generally 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) - { - 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) + 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:color,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) - { - moves.push(this.getBasicMove(x, y, x+shift, y-1, p)); - } - if (y0 && 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 (y { - 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; + let potentialMoves = []; + const [sizeX,sizeY] = VariantRules.size; for (var i=0; i 0) + { + for (let k=0; k 0) + return true; + } + } + } + } + } + return false; + } + + // Check if pieces of color '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) + 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<8) { - if (y+i>=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<8 && this.getPiece(x+pawnShift,y+i)==VariantRules.PAWN + && this.getColor(x+pawnShift,y+i)==c) + { + return true; + } } } } @@ -548,42 +566,44 @@ class ChessRules } // Is square x,y attacked by rooks of color c ? - isAttackedByRook(sq, color) + isAttackedByRook(sq, colors) { - return this.isAttackedBySlideNJump(sq, color, + return this.isAttackedBySlideNJump(sq, colors, VariantRules.ROOK, VariantRules.steps[VariantRules.ROOK]); } // Is square x,y attacked by knights of color c ? - isAttackedByKnight(sq, color) + isAttackedByKnight(sq, colors) { - return this.isAttackedBySlideNJump(sq, color, + return this.isAttackedBySlideNJump(sq, colors, VariantRules.KNIGHT, VariantRules.steps[VariantRules.KNIGHT], "oneStep"); } // Is square x,y attacked by bishops of color c ? - isAttackedByBishop(sq, color) + isAttackedByBishop(sq, colors) { - return this.isAttackedBySlideNJump(sq, color, + return this.isAttackedBySlideNJump(sq, colors, VariantRules.BISHOP, VariantRules.steps[VariantRules.BISHOP]); } // Is square x,y attacked by queens of color c ? - isAttackedByQueen(sq, color) + isAttackedByQueen(sq, colors) { - return this.isAttackedBySlideNJump(sq, color, - VariantRules.QUEEN, VariantRules.steps[VariantRules.QUEEN]); + const V = VariantRules; + return this.isAttackedBySlideNJump(sq, colors, V.QUEEN, + V.steps[V.ROOK].concat(V.steps[V.BISHOP])); } // Is square x,y attacked by king of color c ? - isAttackedByKing(sq, color) + isAttackedByKing(sq, colors) { - return this.isAttackedBySlideNJump(sq, color, - VariantRules.KING, VariantRules.steps[VariantRules.QUEEN], "oneStep"); + const V = VariantRules; + return this.isAttackedBySlideNJump(sq, colors, V.KING, + V.steps[V.ROOK].concat(V.steps[V.BISHOP]), "oneStep"); } // Generic method for non-pawn pieces ("sliding or jumping"): is x,y attacked by piece != color ? - isAttackedBySlideNJump([x,y], c,piece,steps,oneStep) + isAttackedBySlideNJump([x,y], colors, piece, steps, oneStep) { for (let step of steps) { @@ -595,7 +615,7 @@ class ChessRules 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) + && this.getPiece(rx,ry) == piece && colors.includes(this.getColor(rx,ry))) { return true; } @@ -603,10 +623,24 @@ class ChessRules return false; } - underCheck(move, c) + // Is color c under check after 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) + { + this.play(move); + 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; } @@ -628,69 +662,80 @@ class ChessRules 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 [sizeX,sizeY] = VariantRules.size; + const firstRank = (c == "w" ? sizeX-1 : 0); // Update king position + flags if (piece == VariantRules.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 = (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.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; } } + 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) + this.kingPos[c] = [move.start.x, move.start.y]; + } + 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); + + 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++; - - if (!!ingame) - this.moves.push(move); } undo(move) { VariantRules.UndoOnBoard(this.board, move); this.epSquares.pop(); - this.movesCount--; - - // 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: +// let state = this.states.pop(); +// if (JSON.stringify(this.board) != state) +// debugger; } ////////////// // END OF GAME - checkGameOver(color) + checkRepetition() { // Check for 3 repetitions if (this.moves.length >= 8) @@ -702,24 +747,28 @@ class ChessRules _.isEqual(this.moves[L-3], this.moves[L-7]) && _.isEqual(this.moves[L-4], this.moves[L-8])) { - return "1/2 (repetition)"; + return true; } } + return false; + } - // TODO: not required to generate ALL: just need one (callback ? hook ? ...) - if (this.getAllValidMoves(color).length > 0) - { - // game not over + 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))) return "1/2"; @@ -742,24 +791,39 @@ class ChessRules }; } + static get INFINITY() { + return 9999; //"checkmate" (unreachable eval) + } + + static get THRESHOLD_MATE() { + // At this value or above, the game is over + return VariantRules.INFINITY; + } + + static get SEARCH_DEPTH() { + return 3; //2 for high branching factor, 4 for small (Loser chess) + } + // Assumption: at least one legal move - getComputerMove(color) + getComputerMove(moves1) //moves1 might be precomputed (Magnetic chess) { - const oppCol = this.getOppCol(color); + this.shouldReturn = false; + const maxeval = VariantRules.INFINITY; + const color = this.turn; + if (!moves1) + moves1 = this.getAllValidMoves(); // Rank moves using a min-max at depth 2 - let moves1 = this.getAllValidMoves(color); - for (let i=0; i { return (color=="w" ? 1 : -1) * (b.eval - a.eval); }); - // TODO: show current analyzed move for depth 3, allow stopping eval (return moves1[0]) -// for (let i=0; i { return (color=="w" ? 1 : -1) * (b.eval - a.eval); }); - let candidates = [0]; //indices of candidates moves for (let j=1; j= 3 + && Math.abs(moves1[0].eval) < VariantRules.THRESHOLD_MATE) + { + // TODO: show current analyzed move for depth 3, allow stopping eval (return moves1[0]) + for (let i=0; i { 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 { return [this.getNotation(m), m.eval]; })); return moves1[_.sample(candidates, 1)]; } - alphabeta(color, oppCol, depth, alpha, beta) + alphabeta(depth, alpha, beta) { - let moves = this.getAllValidMoves(color); - if (moves.length == 0) + const maxeval = VariantRules.INFINITY; + const color = this.turn; + if (!this.atLeastOneMove()) { - switch (this.checkGameEnd(color)) + switch (this.checkGameEnd()) { case "1/2": return 0; - default: return color=="w" ? -1000 : 1000; + default: return color=="w" ? -maxeval : maxeval; } } if (depth == 0) return this.evalPosition(); - let v = color=="w" ? -1000 : 1000; + const moves = this.getAllValidMoves(); + let v = color=="w" ? -maxeval : maxeval; if (color == "w") { for (let i=0; i= beta) @@ -824,7 +904,7 @@ class ChessRules for (let i=0; i= beta) @@ -906,19 +986,14 @@ class ChessRules let fen = pieces[0].join("") + "/pppppppp/8/8/8/8/PPPPPPPP/" + pieces[1].join("").toUpperCase() + - " 1111 - 0"; //flags + enPassant + movesCount + " 1111"; //add flags return fen; } // Return current fen according to pieces+colors state getFen() { - const L = this.epSquares.length; - const epSq = this.epSquares[L-1]===undefined - ? "-" - : this.epSquares[L-1].x+","+this.epSquares[L-1].y; - return this.getBaseFen() + " " + this.getFlagsFen() - + " " + epSq + " " + this.movesCount; + return this.getBaseFen() + " " + this.getFlagsFen(); } getBaseFen() @@ -962,7 +1037,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; } @@ -970,7 +1045,7 @@ class ChessRules // Context: just before move is played, turn hasn't changed getNotation(move) { - if (move.appear.length == 2) + if (move.appear.length == 2 && move.appear[0].p == VariantRules.KING) { // Castle if (move.end.y < move.start.y) @@ -980,18 +1055,18 @@ class ChessRules } // Translate final square - let finalSquare = + const finalSquare = String.fromCharCode(97 + move.end.y) + (VariantRules.size[0]-move.end.x); - let piece = this.getPiece(move.start.x, move.start.y); + const piece = this.getPiece(move.start.x, move.start.y); if (piece == VariantRules.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 @@ -1004,7 +1079,33 @@ 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; } } + + // The score is already computed when calling this function + getPGN(mycolor, score, fenStart, mode) + { + 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 += '[White "' + (mycolor=='w'?'Myself':opponent) + '"]
'; + pgn += '[Black "' + (mycolor=='b'?'Myself':opponent) + '"]
'; + pgn += '[Fen "' + fenStart + '"]
'; + pgn += '[Result "' + score + '"]

'; + + for (let i=0; i