X-Git-Url: https://git.auder.net/?p=vchess.git;a=blobdiff_plain;f=public%2Fjavascripts%2Fbase_rules.js;h=e405cbaa4e595da56210f09a7e0920c27c406be1;hp=ae0800cdc5abc8d3680e36a7e3f1133785d6f1db;hb=b6487fb9c41705187cf97215fc9e8f86a59057c7;hpb=2d7194bd9c976f444e43e5dc0a725823b6472eb9 diff --git a/public/javascripts/base_rules.js b/public/javascripts/base_rules.js index ae0800cd..e405cbaa 100644 --- a/public/javascripts/base_rules.js +++ b/public/javascripts/base_rules.js @@ -64,22 +64,19 @@ class ChessRules if (!V.IsGoodPosition(fenParsed.position)) return false; // 2) Check turn - if (!fenParsed.turn || !["w","b"].includes(fenParsed.turn)) + if (!fenParsed.turn || !V.IsGoodTurn(fenParsed.turn)) return false; - // 3) Check flags + // 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; - // 4) Check enpassant - if (V.HasEnpassant) + // 5) Check enpassant + if (V.HasEnpassant && + (!fenParsed.enpassant || !V.IsGoodEnpassant(fenParsed.enpassant))) { - if (!fenParsed.enpassant) - return false; - if (fenParsed.enpassant != "-") - { - const ep = V.SquareToCoords(fenParsed.enpassant); - if (ep.y < 0 || ep.y > V.size.y || isNaN(ep.x) || ep.x < 0 || ep.x > V.size.x) - return false; - } + return false; } return true; } @@ -113,22 +110,47 @@ class ChessRules 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}$/); } - // 3 --> d (column letter from number) - static GetColumn(colnum) + static IsGoodEnpassant(enpassant) + { + if (enpassant != "-") + { + const ep = V.SquareToCoords(fenParsed.enpassant); + if (isNaN(ep.x) || !V.OnBoard(ep)) + return false; + } + return true; + } + + // 3 --> 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 }; @@ -137,7 +159,7 @@ class ChessRules // {x:0,y:4} --> e8 static CoordsToSquare(coords) { - return V.GetColumn(coords.y) + (V.size.x - coords.x); + return V.CoordToColumn(coords.y) + (V.size.x - coords.x); } // Aggregates flags into one object @@ -167,7 +189,8 @@ class ChessRules // 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) + // 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, @@ -195,16 +218,12 @@ class ChessRules return (this.turn == side && this.getColor(x,y) == side); } - // On which squares is opponent under check after our move ? (for interface) - getCheckSquares(move) + // On which squares is color under check ? (for interface) + getCheckSquares(color) { - this.play(move); - const color = this.turn; //opponent - let res = this.isAttacked(this.kingPos[color], [this.getOppCol(color)]) + return this.isAttacked(this.kingPos[color], [this.getOppCol(color)]) ? [JSON.parse(JSON.stringify(this.kingPos[color]))] //need to duplicate! : []; - this.undo(move); - return res; } ///////////// @@ -221,31 +240,32 @@ class ChessRules // Get random squares for bishops let randIndex = 2 * _.random(3); - let bishop1Pos = positions[randIndex]; + const 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]; + const 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]; + const knight1Pos = positions[randIndex]; positions.splice(randIndex, 1); randIndex = _.random(4); - let knight2Pos = positions[randIndex]; + const knight2Pos = positions[randIndex]; positions.splice(randIndex, 1); // Get random square for queen randIndex = _.random(3); - let queenPos = positions[randIndex]; + const 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]; + // Rooks and king positions are now fixed, + // because of the ordering rook-king-rook + const rook1Pos = positions[0]; + const kingPos = positions[1]; + const rook2Pos = positions[2]; // Finally put the shuffled pieces in the board array pieces[c][rook1Pos] = 'r'; @@ -271,9 +291,10 @@ class ChessRules { position: fenParts[0], turn: fenParts[1], + movesCount: fenParts[2], }; - let nextIdx = 2; - if (V.hasFlags) + let nextIdx = 3; + if (V.HasFlags) Object.assign(res, {flags: fenParts[nextIdx++]}); if (V.HasEnpassant) Object.assign(res, {enpassant: fenParts[nextIdx]}); @@ -283,7 +304,8 @@ class ChessRules // Return current fen (game state) getFen() { - return this.getBaseFen() + " " + this.turn + + return this.getBaseFen() + " " + + this.getTurnFen() + " " + this.movesCount + (V.HasFlags ? (" " + this.getFlagsFen()) : "") + (V.HasEnpassant ? (" " + this.getEnpassantFen()) : ""); } @@ -307,7 +329,7 @@ class ChessRules position += emptyCount; emptyCount = 0; } - fen += V.board2fen(this.board[i][j]); + position += V.board2fen(this.board[i][j]); } } if (emptyCount > 0) @@ -321,6 +343,11 @@ class ChessRules return position; } + getTurnFen() + { + return this.turn; + } + // Flags part of the FEN string getFlagsFen() { @@ -379,12 +406,12 @@ class ChessRules // INITIALIZATION // Fen string fully describes the game state - constructor(fen, moves) + constructor(fen) { - this.moves = moves; const fenParsed = V.ParseFen(fen); this.board = V.GetBoard(fenParsed.position); - this.turn = (fenParsed.turn || "w"); + this.turn = fenParsed.turn[0]; //[0] to work with MarseilleRules + this.movesCount = parseInt(fenParsed.movesCount); this.setOtherVariables(fen); } @@ -438,7 +465,7 @@ class ChessRules // Set flags and enpassant: const parsedFen = V.ParseFen(fen); if (V.HasFlags) - this.setFlags(fenParsed.flags); + this.setFlags(parsedFen.flags); if (V.HasEnpassant) { const epSq = parsedFen.enpassant != "-" @@ -476,12 +503,6 @@ class ChessRules return (color=="w" ? "b" : "w"); } - get lastMove() - { - const L = this.moves.length; - return (L>0 ? this.moves[L-1] : null); - } - // Pieces codes (for a clearer code) static get PAWN() { return 'p'; } static get ROOK() { return 'r'; } @@ -532,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({ @@ -569,7 +591,8 @@ 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); @@ -599,74 +622,66 @@ class ChessRules const color = this.turn; let moves = []; const [sizeX,sizeY] = [V.size.x,V.size.y]; - const shift = (color == "w" ? -1 : 1); + const shiftX = (color == "w" ? -1 : 1); const firstRank = (color == 'w' ? sizeX-1 : 0); const startRank = (color == "w" ? sizeX-2 : 1); const lastRank = (color == "w" ? 0 : sizeX-1); + const pawnColor = this.getColor(x,y); //can be different for checkered - if (x+shift >= 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.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])) - { - 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 = this.epSquares[Lep-1]; //always at least one element - 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; @@ -693,7 +708,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 ? @@ -723,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; } @@ -775,9 +793,9 @@ class ChessRules //////////////////// // MOVES VALIDATION + // 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) ); } @@ -786,10 +804,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; @@ -799,13 +824,14 @@ class ChessRules { for (let j=0; j= 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 @@ -963,22 +1002,27 @@ class ChessRules { 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 = (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)) + 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; + } } } @@ -999,34 +1043,34 @@ class ChessRules // if (!ingame) this.states.push(this.getFen()); if (!!ingame) - move.notation = [this.getNotation(move), this.getLongNotation(move)]; + move.notation = this.getNotation(move); if (V.HasFlags) move.flags = JSON.stringify(this.aggregateFlags()); //save flags (for undo) - this.updateVariables(move); - this.moves.push(move); if (V.HasEnpassant) this.epSquares.push( this.getEpSquare(move) ); - this.turn = this.getOppCol(this.turn); V.PlayOnBoard(this.board, move); + this.turn = this.getOppCol(this.turn); + this.movesCount++; + this.updateVariables(move); if (!!ingame) { // Hash of current game state *after move*, to detect repetitions - move.hash = hex_md5(this.getFen(); + move.hash = hex_md5(this.getFen()); } } undo(move) { - V.UndoOnBoard(this.board, move); - this.turn = this.getOppCol(this.turn); if (V.HasEnpassant) this.epSquares.pop(); - this.moves.pop(); - this.unupdateVariables(move); 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); // DEBUG: // if (this.getFen() != this.states[this.states.length-1]) @@ -1037,32 +1081,9 @@ class ChessRules /////////////// // END OF GAME - // Check for 3 repetitions (position + flags + turn) - checkRepetition() - { - 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 ? checkGameOver() { - if (this.checkRepetition()) - return "1/2"; - if (this.atLeastOneMove()) // game not over return "*"; @@ -1078,7 +1099,7 @@ 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"); } /////////////// @@ -1136,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]); } } @@ -1196,11 +1222,12 @@ class ChessRules 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 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; } @@ -1319,36 +1347,33 @@ class ChessRules } // The score is already computed when calling this function - getPGN(mycolor, score, fenStart, mode) + getPGN(moves, mycolor, score, fenStart, mode) { let pgn = ""; - pgn += '[Site "vchess.club"]
'; + pgn += '[Site "vchess.club"]\n'; const opponent = mode=="human" ? "Anonymous" : "Computer"; - pgn += '[Variant "' + variant + '"]
'; - pgn += '[Date "' + getDate(new Date()) + '"]
'; - 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