Implemented a very basic DarkBot + a few fixes + advance on rules
authorBenjamin Auder <benjamin.auder@somewhere>
Wed, 26 Dec 2018 11:17:48 +0000 (12:17 +0100)
committerBenjamin Auder <benjamin.auder@somewhere>
Wed, 26 Dec 2018 11:17:48 +0000 (12:17 +0100)
public/javascripts/base_rules.js
public/javascripts/components/game.js
public/javascripts/components/rules.js
public/javascripts/utils/printDiagram.js
public/javascripts/variants/Dark.js
views/rules/Dark/en.pug
views/rules/Marseille/en.pug
views/rules/Upsidedown/en.pug

index 389ba34..891948b 100644 (file)
@@ -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}
index 94340e6..6627252 100644 (file)
@@ -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);
                }
index d8aaa0f..1a59787 100644 (file)
@@ -25,6 +25,7 @@ Vue.component('my-rules', {
                                position: fenParts[0],
                                marks: fenParts[1],
                                orientation: fenParts[2],
+                               shadow: fenParts[3],
                        };
                },
        },
index 61c726e..b7282fe 100644 (file)
@@ -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>";
                }
index e3e093b..96f50de 100644 (file)
@@ -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;
index 49a4fc8..7dc9aa9 100644 (file)
@@ -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.
index 221bd83..f2a5e18 100644 (file)
@@ -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
+       | &nbsp;page on chessvariants.com.
index 695cbe4..ff70cd3 100644 (file)
@@ -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