From 5915f72002ae63b04620cebe47adf778174b1bee Mon Sep 17 00:00:00 2001 From: Benjamin Auder <benjamin.auder@somewhere> Date: Wed, 26 Dec 2018 12:17:48 +0100 Subject: [PATCH] Implemented a very basic DarkBot + a few fixes + advance on rules --- public/javascripts/base_rules.js | 4 +- public/javascripts/components/game.js | 15 ++- public/javascripts/components/rules.js | 1 + public/javascripts/utils/printDiagram.js | 40 ++++++- public/javascripts/variants/Dark.js | 144 +++++++++++++++++++++++ views/rules/Dark/en.pug | 28 ++++- views/rules/Marseille/en.pug | 17 ++- views/rules/Upsidedown/en.pug | 19 ++- 8 files changed, 232 insertions(+), 36 deletions(-) diff --git a/public/javascripts/base_rules.js b/public/javascripts/base_rules.js index 389ba342..891948b9 100644 --- a/public/javascripts/base_rules.js +++ b/public/javascripts/base_rules.js @@ -137,9 +137,9 @@ class ChessRules } // d --> 3 (column letter to number) - static ColumnToCoord(colnum) + static ColumnToCoord(column) { - return String.fromCharCode(97 + colnum); + return column.charCodeAt(0) - 97; } // a4 --> {x:3,y:0} diff --git a/public/javascripts/components/game.js b/public/javascripts/components/game.js index 94340e66..6627252f 100644 --- a/public/javascripts/components/game.js +++ b/public/javascripts/components/game.js @@ -62,9 +62,8 @@ Vue.component('my-game', { }, [h('i', { 'class': { "material-icons": true } }, "accessibility")]) ); - if (variant != "Dark" && - (["idle","computer","friend"].includes(this.mode) - || ["friend","human"].includes(this.mode) && this.score != "*")) + if (["idle","computer","friend"].includes(this.mode) + || (this.mode == "human" && this.score != "*")) { actionArray.push( h('button', @@ -81,9 +80,8 @@ Vue.component('my-game', { [h('i', { 'class': { "material-icons": true } }, "computer")]) ); } - if (variant != "Dark" && - (["idle","friend"].includes(this.mode) - || ["computer","human"].includes(this.mode) && this.score != "*")) + if (variant != "Dark" && (["idle","friend"].includes(this.mode) + || (["computer","human"].includes(this.mode) && this.score != "*"))) { actionArray.push( h('button', @@ -1130,12 +1128,13 @@ Vue.component('my-game', { // before they appear on page: const delay = Math.max(500-(Date.now()-self.timeStart), 0); setTimeout(() => { + const animate = (variant!="Dark" ? "animate" : null); if (self.mode == "computer") //warning: mode could have changed! - self.play(compMove[0], "animate"); + self.play(compMove[0], animate); if (compMove.length == 2) setTimeout( () => { if (self.mode == "computer") - self.play(compMove[1], "animate"); + self.play(compMove[1], animate); }, 750); }, delay); } diff --git a/public/javascripts/components/rules.js b/public/javascripts/components/rules.js index d8aaa0fc..1a597878 100644 --- a/public/javascripts/components/rules.js +++ b/public/javascripts/components/rules.js @@ -25,6 +25,7 @@ Vue.component('my-rules', { position: fenParts[0], marks: fenParts[1], orientation: fenParts[2], + shadow: fenParts[3], }; }, }, diff --git a/public/javascripts/utils/printDiagram.js b/public/javascripts/utils/printDiagram.js index 61c726eb..b7282fee 100644 --- a/public/javascripts/utils/printDiagram.js +++ b/public/javascripts/utils/printDiagram.js @@ -7,18 +7,46 @@ function getDiagram(args) const board = VariantRules.GetBoard(args.position); const orientation = args.orientation || "w"; let markArray = []; - if (!!args.marks) + if (!!args.marks && args.marks != "-") { // Turn (human) marks into coordinates markArray = doubleArray(sizeX, sizeY, false); let squares = args.marks.split(","); for (let i=0; i<squares.length; i++) { - const res = /^([a-z]+)([0-9]+)$/i.exec(squares[i]); - const coords = V.SquareToCoords(res); + const coords = V.SquareToCoords(squares[i]); markArray[coords.x][coords.y] = true; } } + let shadowArray = []; + if (!!args.shadow && args.shadow != "-") + { + // Turn (human) shadow indications into coordinates + shadowArray = doubleArray(sizeX, sizeY, false); + let squares = args.shadow.split(","); + for (let i=0; i<squares.length; i++) + { + const rownum = V.size.x - parseInt(squares[i]); + if (!isNaN(rownum)) + { + // Shadow a full row + for (let i=0; i<V.size.y; i++) + shadowArray[rownum][i] = true; + continue; + } + if (squares[i].length == 1) + { + // Shadow a full column + const colnum = V.ColumnToCoord(squares[i]); + for (let i=0; i<V.size.x; i++) + shadowArray[i][colnum] = true; + continue; + } + // Shadow just one square: + const coords = V.SquareToCoords(squares[i]); + shadowArray[coords.x][coords.y] = true; + } + } let boardDiv = ""; const [startX,startY,inc] = orientation == 'w' ? [0, 0, 1] @@ -29,13 +57,15 @@ function getDiagram(args) for (let j=startY; j>=0 && j<sizeY; j+=inc) { boardDiv += "<div class='board board" + sizeY + " " + - ((i+j)%2==0 ? "light-square-diag" : "dark-square-diag") + "'>"; + ((i+j)%2==0 ? "light-square-diag" : "dark-square-diag") + + (shadowArray.length > 0 && shadowArray[i][j] ? " in-shadow" : "") + + "'>"; if (board[i][j] != V.EMPTY) { boardDiv += "<img src='/images/pieces/" + V.getPpath(board[i][j]) + ".svg' class='piece'/>"; } - if (!!args.marks && markArray[i][j]) + if (markArray.length > 0 && markArray[i][j]) boardDiv += "<img src='/images/mark.svg' class='mark-square'/>"; boardDiv += "</div>"; } diff --git a/public/javascripts/variants/Dark.js b/public/javascripts/variants/Dark.js index e3e093b1..96f50de7 100644 --- a/public/javascripts/variants/Dark.js +++ b/public/javascripts/variants/Dark.js @@ -126,6 +126,150 @@ class DarkRules extends ChessRules { return 500; //checkmates evals may be slightly below 1000 } + + // In this special situation, we just look 1 half move ahead + getComputerMove() + { + const maxeval = V.INFINITY; + const color = this.turn; + const oppCol = this.getOppCol(color); + const pawnShift = (color == "w" ? -1 : 1); + const kp = this.kingPos[color]; + + // Do not cheat: the current enlightment is all we can see + const myLight = JSON.parse(JSON.stringify(this.enlightened[color])); + + // Can a slider on (i,j) apparently take my king? + // NOTE: inaccurate because assume yes if some squares are shadowed + const sliderTake = ([i,j], piece) => { + let step = undefined; + if (piece == V.BISHOP) + { + if (Math.abs(kp[0] - i) == Math.abs(kp[1] - j)) + { + step = + [ + (i-kp[0]) / Math.abs(i-kp[0]), + (j-kp[1]) / Math.abs(j-kp[1]) + ]; + } + } + else if (piece == V.ROOK) + { + if (kp[0] == i) + step = [0, (j-kp[1]) / Math.abs(j-kp[1])]; + else if (kp[1] == j) + step = [(i-kp[0]) / Math.abs(i-kp[0]), 0]; + } + if (!step) + return false; + // Check for obstacles + let obstacle = false; + for ( + let x=kp[0]+step[0], y=kp[1]+step[1]; + x != i && y != j; + x += step[0], y+= step[1]) + { + if (myLight[x][y] && this.board[x][y] != V.EMPTY) + { + obstacle = true; + break; + } + } + if (!obstacle) + return true; + return false; + }; + + // Do I see something which can take my king ? + const kingThreats = () => { + for (let i=0; i<V.size.x; i++) + { + for (let j=0; j<V.size.y; j++) + { + if (myLight[i][j] && this.board[i][j] != V.EMPTY + && this.getColor(i,j) != color) + { + switch (this.getPiece(i,j)) + { + case V.PAWN: + if (kp[0] + pawnShift == i && Math.abs(kp[1]-j) == 1) + return true; + break; + case V.KNIGHT: + if ((Math.abs(kp[0] - i) == 2 && Math.abs(kp[1] - j) == 1) || + (Math.abs(kp[0] - i) == 1 && Math.abs(kp[1] - j) == 2)) + { + return true; + } + break; + case V.KING: + if (Math.abs(kp[0] - i) == 1 && Math.abs(kp[1] - j) == 1) + return true; + break; + case V.BISHOP: + if (sliderTake([i,j], V.BISHOP)) + return true; + break; + case V.ROOK: + if (sliderTake([i,j], V.ROOK)) + return true; + break; + case V.QUEEN: + if (sliderTake([i,j], V.BISHOP) || sliderTake([i,j], V.ROOK)) + return true; + break; + } + } + } + } + return false; + }; + + let moves = this.getAllValidMoves(); + for (let move of moves) + { + this.play(move); + if (this.kingPos[oppCol][0] >= 0 && kingThreats()) + { + // We didn't take opponent king, and our king will be captured: bad + move.eval = -maxeval; + } + this.undo(move); + if (!!move.eval) + continue; + + move.eval = 0; //a priori... + + // Can I take something ? If yes, do it if it seems good... + if (move.vanish.length == 2 && move.vanish[1].c != color) //avoid castle + { + const myPieceVal = V.VALUES[move.appear[0].p]; + const hisPieceVal = V.VALUES[move.vanish[1].p]; + if (myPieceVal <= hisPieceVal) + move.eval = hisPieceVal - myPieceVal + 2; //favor captures + else + { + // Taking a pawn with minor piece, + // or minor piece or pawn with a rook, + // or anything but a queen with a queen, + // or anything with a king. + // ==> Do it at random, although + // this is clearly inferior to what a human can deduce... + move.eval = (Math.random() < 0.5 ? 1 : -1); + } + } + } + + // TODO: also need to implement the case when an opponent piece (in light) + // is threatening something - maybe not the king, but e.g. pawn takes rook. + + moves.sort((a,b) => b.eval - a.eval); + let candidates = [0]; + for (let j=1; j<moves.length && moves[j].eval == moves[0].eval; j++) + candidates.push(j); + return moves[_.sample(candidates, 1)]; + } } const VariantRules = DarkRules; diff --git a/views/rules/Dark/en.pug b/views/rules/Dark/en.pug index 49a4fc8d..7dc9aa9e 100644 --- a/views/rules/Dark/en.pug +++ b/views/rules/Dark/en.pug @@ -16,12 +16,26 @@ p Incomplete information version of the orthodox game (also shuffled). h3 Basics p. - TODO + Before every move, players see only what their pieces can reach. + That is to say: empty squares where your pieces can go, + but also occupied squares where captures can be made. + For example on the illustration next, after 1.e4 d5 white sees a black + pawn on d5, the squares e5, h5, b5, a6 and all four first ranks. figure.diagram-container .diagram - | fen:r3kbnr/pp3ppp/3p4/4p3/8/8/PPPPPPPP/R1BQKBNR: - figcaption After the moves 1.Nc3 d6?? 2.Nd5 e5 3.Nxc7 + | fen:8/8/8/3p4/4P3/8/PPPP1PPP/RNBQKBNR - w 8,7,b6,c6,d6,e6,f6,g6,h6,a5,c5,f5,g5: + figcaption Standard initial position after 1.e4 d5 + +p. + Choose your move carefully, based on what you can infer from the + opponent's past moves. Counting material, in particular, is crucial. + Good luck! + +p. + Note: the bot is not cheating - it really uses only the information described earlier. + Moreover, it is very basic and clearly less challenging that a human. + But it may be a fun start :) h3 End of the game @@ -29,5 +43,9 @@ p Win by capturing the king (no checks, no stalemate). h3 More information -p. - TODO: quote buho21, ... +p + | I discovered this variant on + a(href="https://www.buho21.com/") Buho21 + | , which is a nice website but its spirit is very different than here + | (ranking system, VIP membership or ads, etc.). + | It seemed to be the only place to play DarkChess in live. diff --git a/views/rules/Marseille/en.pug b/views/rules/Marseille/en.pug index 221bd831..f2a5e180 100644 --- a/views/rules/Marseille/en.pug +++ b/views/rules/Marseille/en.pug @@ -24,12 +24,21 @@ p. otherwise has to be the first move. OK even if opponent moved his pawn at subturn 1. +p. + PGN game notation: since there are two moves at each turn except on move 1, + a double move in the game is indicated as two comma-separated (ordered) moves, + as in 3.Na5,Bd3 e6,f4 (the two first are white moves, + the two others are black moves). + figure.diagram-container .diagram - | fen:r3kbnr/pp3ppp/3p4/4p3/8/8/PPPPPPPP/R1BQKBNR: - figcaption After the moves 1.Nc3 d6?? 2.Nd5 e5 3.Nxc7 + | fen:r1bqkbnr/pppp1p1p/2n5/4p2p/4P3/5N2/PPPP1PPP/RNB1KB1R: + figcaption After the moves 1.e4 e5,Nc6 2.Qh5,Nf3 g6,gxh5 h3 More information -p. - Possible starting point Wikipedia page ? +p + | See for example the + a(href="https://www.chessvariants.com/multimove.dir/marseill.html") + | Marseillais Chess + | page on chessvariants.com. diff --git a/views/rules/Upsidedown/en.pug b/views/rules/Upsidedown/en.pug index 695cbe48..ff70cd30 100644 --- a/views/rules/Upsidedown/en.pug +++ b/views/rules/Upsidedown/en.pug @@ -1,13 +1,6 @@ p.boxed | Pawns start on the 7th rank. Move a knight to promote them. -Diagram: ... threatens Nc5-Nd3# and allow 2.b8=Q - -figure.diagram-container - .diagram - | fen:: - figcaption Standard initial position after 1.Na6 - h3 Specifications ul @@ -29,14 +22,16 @@ p. Since truly random start can allow un-defendable mate in 3 with a knight, the kings touch at least one knight in the initial position. This allows to move free out of potential check from the very beginning. - Here is an example: -// TODO: diagram +p. + To illustrate this phenomenon, although it's defendable the standard initial + position allows the attack as 1.Na6 (threatening Nc5-Nd3#), + forcing the defense Nf3-Ne5. With a knight next to the king you have more options. + figure.diagram-container .diagram - | fen:r7/2n5/1q6/5k2/8/8/K7/8: - figcaption. - The king cannot take on a8 because it's guarded by the knight: it's checkmate + | fen:R1BQKBNR/PPPPPPP/N7/8/8/8/pppppppp/rnbqkbnr: + figcaption Standard initial position after 1.Na6 h3 Source -- 2.44.0