From 6e62b1c7d177585003e923d423025dff280a7525 Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Wed, 26 Dec 2018 01:16:56 +0100 Subject: [PATCH] First working draft of MarseilleRules; almost OK (bug in computerMove turn/subturn) --- public/javascripts/base_rules.js | 49 +++- public/javascripts/components/game.js | 16 +- public/javascripts/variants/Grand.js | 17 ++ public/javascripts/variants/Marseille.js | 322 +++++++++++++++++++++- public/javascripts/variants/Wildebeest.js | 17 ++ 5 files changed, 392 insertions(+), 29 deletions(-) diff --git a/public/javascripts/base_rules.js b/public/javascripts/base_rules.js index 7a42a385..ba77d718 100644 --- a/public/javascripts/base_rules.js +++ b/public/javascripts/base_rules.js @@ -64,22 +64,16 @@ 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 if (V.HasFlags && (!fenParsed.flags || !V.IsGoodFlags(fenParsed.flags))) return false; // 4) Check enpassant - if (V.HasEnpassant) + if (V.HasEnpassant && + (!fenParsed.enpassant || !V.IsGoodEnpassant(fenParsed.enpassant))) { - if (!fenParsed.enpassant) - return false; - if (fenParsed.enpassant != "-") - { - const ep = V.SquareToCoords(fenParsed.enpassant); - if (isNaN(ep.x) || !V.OnBoard(ep)) - return false; - } + return false; } return true; } @@ -113,12 +107,29 @@ 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}$/); } + 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) { @@ -288,7 +299,7 @@ class ChessRules // Return current fen (game state) getFen() { - return this.getBaseFen() + " " + this.turn + + return this.getBaseFen() + " " + this.getTurnFen() + (V.HasFlags ? (" " + this.getFlagsFen()) : "") + (V.HasEnpassant ? (" " + this.getEnpassantFen()) : ""); } @@ -326,6 +337,11 @@ class ChessRules return position; } + getTurnFen() + { + return this.turn; + } + // Flags part of the FEN string getFlagsFen() { @@ -389,7 +405,7 @@ class ChessRules 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.setOtherVariables(fen); } @@ -960,7 +976,12 @@ class ChessRules updateVariables(move) { const piece = move.vanish[0].p; - const c = this.getOppCol(this.turn); //'move.vanish[0].c' doesn't work for Checkered + let c = move.vanish[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 @@ -1083,7 +1104,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"); } /////////////// diff --git a/public/javascripts/components/game.js b/public/javascripts/components/game.js index 085dafa3..1a3d2379 100644 --- a/public/javascripts/components/game.js +++ b/public/javascripts/components/game.js @@ -1120,13 +1120,21 @@ Vue.component('my-game', { const self = this; this.compWorker.onmessage = function(e) { let compMove = e.data; - compMove.computer = true; //TODO: imperfect attempt to avoid ghost move + if (!Array.isArray(compMove)) + compMove = [compMove]; //to deal with MarseilleRules + // TODO: imperfect attempt to avoid ghost move: + compMove.forEach(m => { m.computer = true; }); // (first move) HACK: small delay to avoid selecting elements // before they appear on page: const delay = Math.max(500-(Date.now()-self.timeStart), 0); setTimeout(() => { if (self.mode == "computer") //warning: mode could have changed! - self.play(compMove, "animate") + self.play(compMove[0], "animate"); + if (compMove.length == 2) + setTimeout( () => { + if (self.mode == "computer") + self.play(compMove[1]); + }, 2000); }, delay); } }, @@ -1297,7 +1305,7 @@ Vue.component('my-game', { this.endGame(this.mycolor=="w"?"0-1":"1-0"); }, newGame: function(mode, fenInit, color, oppId) { - const fen = fenInit || VariantRules.GenRandInitFen(); + const fen = "rnbbqkrn/1ppppp1p/p5p1/8/8/3P4/PPP1PPPP/BNQBRKRN w1 1111 -"; //fenInit || VariantRules.GenRandInitFen(); console.log(fen); //DEBUG if (mode=="human" && !oppId) { @@ -1378,7 +1386,7 @@ Vue.component('my-game', { else if (mode == "computer") { this.compWorker.postMessage(["init",this.vr.getFen()]); - this.mycolor = (Math.random() < 0.5 ? 'w' : 'b'); + this.mycolor = "w";//(Math.random() < 0.5 ? 'w' : 'b'); if (this.mycolor != this.vr.turn) this.playComputerMove(); } diff --git a/public/javascripts/variants/Grand.js b/public/javascripts/variants/Grand.js index 7248f578..7f5fe422 100644 --- a/public/javascripts/variants/Grand.js +++ b/public/javascripts/variants/Grand.js @@ -18,6 +18,23 @@ class GrandRules extends ChessRules return true; } + static IsGoodEnpassant(enpassant) + { + if (enpassant != "-") + { + const squares = enpassant.split(","); + if (squares.length > 2) + return false; + for (let sq of squares) + { + const ep = V.SquareToCoords(sq); + if (isNaN(ep.x) || !V.OnBoard(ep)) + return false; + } + } + return true; + } + static ParseFen(fen) { const fenParts = fen.split(" "); diff --git a/public/javascripts/variants/Marseille.js b/public/javascripts/variants/Marseille.js index f4d79298..7ac928f1 100644 --- a/public/javascripts/variants/Marseille.js +++ b/public/javascripts/variants/Marseille.js @@ -1,16 +1,316 @@ -//TODO: -//adapter alphabeta (dans baserules ? --> basé sur turn OK) -// le reste == standard - class MarseilleRules extends ChessRules { - // TODO: fen indication pour turn : w1, w2 ou b1, ou b2 (about to play 1st or 2nd sub-turn) - // + quelque chose pour indiquer si c'est le tout premier coup ("w" sans + d'indications) - // - // this.turn == "w" ou "b" - // this.subTurn == 0 ou 1 (ou 1 et 2) - // - // Alpha-beta ? + static IsGoodEnpassant(enpassant) + { + if (enpassant != "-") + { + const squares = enpassant.split(","); + if (squares.length > 2) + return false; + for (let sq of squares) + { + const ep = V.SquareToCoords(sq); + if (isNaN(ep.x) || !V.OnBoard(ep)) + return false; + } + } + return true; + } + + getTurnFen() + { + if (this.startAtFirstMove && this.moves.length==0) + return "w"; + return this.turn + this.subTurn; + } + + // There may be 2 enPassant squares (if 2 pawns jump 2 squares in same turn) + getEnpassantFen() + { + const L = this.epSquares.length; + if (this.epSquares[L-1].every(epsq => epsq === undefined)) + return "-"; //no en-passant + let res = ""; + this.epSquares[L-1].forEach(epsq => { + if (!!epsq) + res += V.CoordsToSquare(epsq) + ","; + }); + return res.slice(0,-1); //remove last comma + } + + setOtherVariables(fen) + { + const parsedFen = V.ParseFen(fen); + this.setFlags(parsedFen.flags); + if (parsedFen.enpassant == "-") + this.epSquares = [ [undefined,undefined] ]; + else + { + let res = []; + const squares = parsedFen.enpassant.split(","); + for (let sq of squares) + res.push(V.SquareToCoords(sq)); + if (res.length == 1) + res.push(undefined); //always 2 slots in epSquares[i] + this.epSquares = [ res ]; + } + this.scanKingsRooks(fen); + // Extract subTurn from turn indicator: "w" (first move), or + // "w1" or "w2" white subturn 1 or 2, and same for black + const fullTurn = V.ParseFen(fen).turn; + this.startAtFirstMove = (fullTurn == "w"); + this.turn = fullTurn[0]; + this.subTurn = (fullTurn[1] || 1); + } + + getPotentialPawnMoves([x,y]) + { + const color = this.turn; + let moves = []; + const [sizeX,sizeY] = [V.size.x,V.size.y]; + 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+shiftX >= 0 && x+shiftX < sizeX) //TODO: always true + { + 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) + { + 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*shiftX,y])); + } + } + // Captures + for (let shiftY of [-1,1]) + { + if (y + shiftY >= 0 && y + shiftY < sizeY + && this.board[x+shiftX][y+shiftY] != V.EMPTY + && this.canTake([x,y], [x+shiftX,y+shiftY])) + { + for (let piece of finalPieces) + { + moves.push(this.getBasicMove([x,y], [x+shiftX,y+shiftY], + {c:pawnColor,p:piece})); + } + } + } + } + + // En passant: always OK if subturn 1, + // OK on subturn 2 only if enPassant was played at subturn 1 + // (and if there are two e.p. squares available). + const Lep = this.epSquares.length; + const epSquares = this.epSquares[Lep-1]; //always at least one element + let epSqs = []; + epSquares.forEach(sq => { + if (!!sq) + epSqs.push(sq); + }); + if (epSqs.length == 0) + return moves; + for (let sq of epSqs) + { + if (this.subTurn == 1 || (epSqs.length == 2 && + // Was this en-passant capture already played at subturn 1 ? + this.board[epSqs[0].x][epSqs[0].y] != V.EMPTY)) + { + if (sq.x == x+shiftX && Math.abs(sq.y - y) == 1) + { + let epMove = this.getBasicMove([x,y], [sq.x,sq.y]); + epMove.vanish.push({ + x: x, + y: sq.y, + p: 'p', + c: this.getColor(x,sq.y) + }); + moves.push(epMove); + } + } + } + + return moves; + } + + play(move, ingame) + { +// console.log("play " + this.getNotation(move)); +// console.log(this.turn + " "+ this.subTurn); + if (!!ingame) + move.notation = [this.getNotation(move), this.getLongNotation(move)]; + move.flags = JSON.stringify(this.aggregateFlags()); + let lastEpsq = this.epSquares[this.epSquares.length-1]; + const epSq = this.getEpSquare(move); + if (lastEpsq.length == 1) + lastEpsq.push(epSq); + else + { + // New turn + let newEpsqs = [epSq]; + if (this.startAtFirstMove && this.moves.length == 0) + newEpsqs.push(undefined); //at first move, to force length==2 (TODO) + this.epSquares.push(newEpsqs); + } + V.PlayOnBoard(this.board, move); + if (this.startAtFirstMove && this.moves.length == 0) + this.turn = "b"; + // Does this move give check on subturn 1? If yes, skip subturn 2 + else if (this.subTurn==1 && this.underCheck(this.getOppCol(this.turn))) + { + this.epSquares[this.epSquares.length-1].push(undefined); + this.turn = this.getOppCol(this.turn); + move.checkOnSubturn1 = true; + } + else + { + if (this.subTurn == 2) + this.turn = this.getOppCol(this.turn); + this.subTurn = 3 - this.subTurn; + } + this.moves.push(move); + this.updateVariables(move); + if (!!ingame) + move.hash = hex_md5(this.getFen()); + //console.log(move.checkOnSubturn1 + " " +this.turn + " "+ this.subTurn); + } + + undo(move) + { + this.disaggregateFlags(JSON.parse(move.flags)); + let lastEpsq = this.epSquares[this.epSquares.length-1]; + if (lastEpsq.length == 2) + { + if (!!move.checkOnSubturn1 || + (this.startAtFirstMove && this.moves.length == 1)) + { + this.epSquares.pop(); //remove real + artificial e.p. squares + } + else + lastEpsq.pop(); + } + else + this.epSquares.pop(); + V.UndoOnBoard(this.board, move); + if (this.startAtFirstMove && this.moves.length == 1) + this.turn = "w"; + else if (move.checkOnSubturn1) + { + this.turn = this.getOppCol(this.turn); + this.subTurn = 1; + } + else + { + if (this.subTurn == 1) + this.turn = this.getOppCol(this.turn); + this.subTurn = 3 - this.subTurn; + } + this.moves.pop(); + this.unupdateVariables(move); +// console.log("UNDO " + this.getNotation(move)); +// console.log(this.turn + " "+ this.subTurn); + } + + // NOTE: GenRandInitFen() is OK, + // since at first move turn indicator is just "w" + + // No alpha-beta here, just adapted min-max at depth 2(+1) + getComputerMove() + { + const maxeval = V.INFINITY; + const color = this.turn; + const oppCol = this.getOppCol(this.turn); + + // Search best (half) move for opponent turn + const getBestMoveEval = () => { + let moves = this.getAllValidMoves(); + if (moves.length == 0) + { + const score = this.checkGameEnd(); + if (score == "1/2") + return 0; + return maxeval * (score == "1-0" ? 1 : -1); + } + let res = (oppCol == "w" ? -maxeval : maxeval); + for (let m of moves) + { + this.play(m); + this.turn = color; //very artificial... + if (!this.atLeastOneMove()) + { + const score = this.checkGameEnd(); + if (score == "1/2") + res = (oppCol == "w" ? Math.max(res, 0) : Math.min(res, 0)); + else + { + // Found a mate + this.turn = oppCol; + this.undo(m); + return maxeval * (score == "1-0" ? 1 : -1); + } + } + const evalPos = this.evalPosition(); + res = (oppCol == "w" ? Math.max(res, evalPos) : Math.min(res, evalPos)); + this.turn = oppCol; + this.undo(m); + } + return res; + }; + + let moves11 = this.getAllValidMoves(); + let doubleMoves = []; + // Rank moves using a min-max at depth 2 + for (let i=0; i { + return (color=="w" ? 1 : -1) * (b.eval - a.eval); }); + let candidates = [0]; //indices of candidates moves + for (let i=1; + i 2) + return false; + for (let sq of squares) + { + const ep = V.SquareToCoords(sq); + if (isNaN(ep.x) || !V.OnBoard(ep)) + return false; + } + } + return true; + } + // There may be 2 enPassant squares (if pawn jump 3 squares) getEnpassantFen() { -- 2.44.0