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
+	| &nbsp;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