X-Git-Url: https://git.auder.net/?a=blobdiff_plain;f=public%2Fjavascripts%2Fbase_rules.js;h=860495a4236c3109bf77874172f353b7b878be2b;hb=098e8468ae7a52a55850c09f90506f52b8133567;hp=b604e7970ffc357c4b05b0217836756485bde3c6;hpb=4b5fe3061829e184f9ad86a13d831eda22d95343;p=vchess.git diff --git a/public/javascripts/base_rules.js b/public/javascripts/base_rules.js index b604e797..860495a4 100644 --- a/public/javascripts/base_rules.js +++ b/public/javascripts/base_rules.js @@ -10,6 +10,7 @@ class PiPo //Piece+Position } } +// TODO: for animation, moves should contains "moving" and "fading" maybe... class Move { // o: {appear, vanish, [start,] [end,]} @@ -46,14 +47,14 @@ 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 - this.initVariables(fen); - this.flags = VariantRules.GetFlags(fen); this.board = VariantRules.GetBoard(fen); + this.setFlags(fen); + this.initVariables(fen); } initVariables(fen) @@ -65,54 +66,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,83 +266,88 @@ 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]; + let i = x + step[0]; + let j = y + step[1]; while (i>=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=0 && j= 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)) + 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)); + 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)) + 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, p)); + moves.push(this.getBasicMove([x,y], [x+shift,y-1], {c:pawnColor,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; - for (var i=0; i 0) { - for (let i=0; i 0) + if (this.filterValid([moves[k]]).length > 0) return true; } } @@ -544,84 +547,93 @@ 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) + const [sizeX,sizeY] = VariantRules.size; + 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 (rx>=0 && rx=0 && ry=0 && rx<8 && ry>=0 && ry<8 && this.board[rx][ry] != VariantRules.EMPTY - && this.getPiece(rx,ry) == piece && this.getColor(rx,ry) == c) + if (rx>=0 && rx=0 && ry 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; } } - play(move, ingame) + // After move is undo-ed, un-update variables (flags are reset) + // TODO: more symmetry, by storing flags increment in move... + unupdateVariables(move) { - // Save flags (for undo) - move.flags = JSON.stringify(this.flags); //TODO: less costly - this.updateVariables(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) + { 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++; } - undo(move, ingame) + undo(move) { VariantRules.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)); } ////////////// // END OF GAME - checkGameOver(color) + // Basic check for 3 repetitions (in the last moves only) + checkRepetition() { - // Check for 3 repetitions if (this.moves.length >= 8) { - // NOTE: crude detection, only moves repetition 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 "1/2 (repetition)"; + return true; } } + return false; + } - 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"; @@ -785,77 +802,136 @@ 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) + // NOTE: works also for extinction chess because depth is 3... + getComputerMove() { - const oppCol = this.getOppCol(color); + this.shouldReturn = false; + const maxeval = VariantRules.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) + let moves1 = this.getAllValidMoves("computer"); + + // Can I mate in 1 ? (for Magnetic & Extinction) + for (let i of _.shuffle(_.range(moves1.length))) + { + this.play(moves1[i]); + const finish = (Math.abs(this.evalPosition()) >= VariantRules.THRESHOLD_MATE); + this.undo(moves1[i]); + if (finish) + return moves1[i]; + } // Rank moves using a min-max at depth 2 - let moves1 = this.getAllValidMoves(color); - for (let i=0; i eval2)) + eval2 = evalPos; + this.undo(moves2[j]); + } + } + else { - this.play(moves2[j]); - let evalPos = this.evalPosition(); - if ((color == "w" && evalPos < eval2) || (color=="b" && evalPos > eval2)) - eval2 = evalPos; - this.undo(moves2[j]); + 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); }); - - // 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); }); + //console.log(moves1.map(m => { return [this.getNotation(m), m.eval]; })); let candidates = [0]; //indices of candidates moves for (let j=1; j= 3 + && Math.abs(moves1[0].eval) < VariantRules.THRESHOLD_MATE) + { + 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= beta) @@ -867,7 +943,7 @@ class ChessRules for (let i=0; i= beta) @@ -881,7 +957,7 @@ class ChessRules { const [sizeX,sizeY] = VariantRules.size; let evaluation = 0; - //Just count material for now + // Just count material for now for (let i=0; i 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 @@ -1047,30 +1113,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) + (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 + } + // 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