From: Benjamin Auder <benjamin.auder@somewhere>
Date: Thu, 13 Dec 2018 02:29:11 +0000 (+0100)
Subject: About to finish completed Ultima rules
X-Git-Url: https://git.auder.net/variants/Chakart/css/assets/current/pieces/common.css?a=commitdiff_plain;h=2f3c845159670ec8bfba8a3999571d6ee1e45320;p=vchess.git

About to finish completed Ultima rules
---

diff --git a/TODO b/TODO
index b3568538..bb4d6e16 100644
--- a/TODO
+++ b/TODO
@@ -1,5 +1 @@
 Full detection of repeated positions (including turn)
-In UltimaChess, consider these rules modifications: http://www.inference.org.uk/mackay/ultima/ultima.html
-Reintroduce isAttackedBy in Ultima, and fix it for pawns --> underCheck + stalemate
---> take into account that an immobilized piece does not give check.
-(chameleons cannot be immobilized)
diff --git a/public/javascripts/variants/Ultima.js b/public/javascripts/variants/Ultima.js
index c98822dc..57f3e25c 100644
--- a/public/javascripts/variants/Ultima.js
+++ b/public/javascripts/variants/Ultima.js
@@ -50,9 +50,39 @@ class UltimaRules extends ChessRules
 	//  - a "bishop" is a chameleon, capturing as its prey
 	//  - a "queen" is a withdrawer, capturing by moving away from pieces
 
-	getPotentialMovesFrom([x,y])
+	// Is piece on square (x,y) immobilized?
+	isImmobilized([x,y])
 	{
-		// Pre-check: is thing on this square immobilized?
+					// Final check: is this knight immobilized?
+					let foundImmobilizer = false;
+					let neutralized = false;
+					outerLoop:
+					for (let step of steps)
+					{
+						const [i2,j2] = [i+step[0],j+step[1]];
+						if (i2>=0 && i2<sizeX && j2>=0 && j2<sizeY
+							&& this.board[i2][j2] != V.EMPTY
+							&& this.getColor(i2,j2) == oppCol
+							&& this.getPiece(i2,j2) == V.IMMOBILIZER)
+						{
+							foundImmobilizer = true;
+							// Moving is possible only if this immobilizer is neutralized
+							for (let step2 of steps)
+							{
+								const [i3,j3] = [i2+step2[0],j2+step2[1]];
+								if (i3>=0 && i3<sizeX && j3>=0 && j3<sizeY
+									&& this.board[i3][j3] != V.EMPTY && this.getColor(i3,j3) == color
+									&& [V.BISHOP,V.IMMOBILIZER].includes(this.getPiece(i3,j3)))
+								{
+									neutralized = true;
+									break outerLoop;
+								}
+							}
+						}
+					}
+					if (!foundImmobilizer || neutralized)
+						return false;
+		
 		const piece = this.getPiece(x,y);
 		const color = this.getColor(x,y);
 		const oppCol = this.getOppCol(color);
@@ -87,6 +117,13 @@ class UltimaRules extends ChessRules
 				}
 			}
 		}
+	}
+
+	getPotentialMovesFrom([x,y])
+	{
+		// Pre-check: is thing on this square immobilized?
+		if (this.isImmobilized([x,y]))
+			return [];
 		switch (this.getPiece(x,y))
 		{
 			case VariantRules.IMMOBILIZER:
@@ -288,13 +325,10 @@ class UltimaRules extends ChessRules
 	{
 		let moves = super.getPotentialQueenMoves([x,y])
 			.concat(this.getKnightCaptures([x,y],"asChameleon"));
+		// No "king capture" because king cannot remain under check
 		this.addPawnCaptures(moves, "asChameleon");
 		this.addRookCaptures(moves, "asChameleon");
 		this.addQueenCaptures(moves, "asChameleon");
-		// Add king capture if it's within range
-		const oppKp = this.kingPos[this.getOppCol(this.turn)];
-		if (Math.abs(x-oppKp[0]) <= 1 && Math.abs(y-oppKp[1]) <= 1)
-			moves.push(this.getBasicMove([x,y],oppKp));
 		// Post-processing: merge similar moves, concatenating vanish arrays
 		let mergedMoves = {};
 		const [sizeX,sizeY] = VariantRules.size;
@@ -376,77 +410,153 @@ class UltimaRules extends ChessRules
 			V.steps[V.ROOK].concat(V.steps[V.BISHOP]), "oneStep");
 	}
 
-	atLeastOneMove()
-	{
-		if (this.kingPos[this.turn][0] < 0)
-			return false;
-		return super.atLeastOneMove();
-	}
+	// isAttacked() is OK because the immobilizer doesn't take
 
-	underCheck(move)
+	// TODO: check if any pawn can reach capturing square + !immobilized
+	isAttackedByPawn([x,y], colors)
 	{
-		return false; //there is no check
+		// Square (x,y) must be surrounded by two enemy pieces,
+		// and one of them at least should be a pawn.
+		const dirs = [ [1,0],[0,1],[1,1],[-1,1] ];
+		const [sizeX,sizeY] = VariantRules.size;
+		for (let dir of dirs)
+		{
+			const [i1,j1] = [x-dir[0],y-dir[1]]; //"before"
+			const [i2,j2] = [x+dir[0],y+dir[1]]; //"after"
+			if (i1>=0 && i1<sizeX && i2>=0 && i2<sizeX
+				&& j1>=0 && j1<sizeY && j2>=0 && j2<sizeY
+				&& this.board[i1][j1]!=VariantRules.EMPTY
+				&& this.board[i2][j2]!=VariantRules.EMPTY
+				&& colors.includes(this.getColor(i1,j1))
+				&& colors.includes(this.getColor(i2,j2))
+				&& [this.getPiece(i1,j1),this.getPiece(i2,j2)].includes(VariantRules.PAWN))
+			{
+				return true;
+			}
+		}
+		return false;
 	}
 
-	getCheckSquares(move)
+	// TODO: check if enemy's rook can reach capturing squares + !immobilized
+	isAttackedByRook([x,y], colors)
 	{
-		const c = this.getOppCol(this.turn); //opponent
-		const saveKingPos = this.kingPos[c]; //king might be taken
-		this.play(move);
-		// The only way to be "under check" is to have lost the king (thus game over)
-		let res = this.kingPos[c][0] < 0
-			? [ JSON.parse(JSON.stringify(saveKingPos)) ]
-			: [ ];
-		this.undo(move);
-		return res;
+		const [sizeX,sizeY] = VariantRules.size;
+		// King must be on same column and a rook on same row (or reverse)
+		if (x == this.kingPos[colors[0]][0]) //using colors[0], only element in this case
+		{
+			// Look for enemy rook on this column
+			for (let i=0; i<sizeY; i++)
+			{
+				if (this.board[x][i] != VariantRules.EMPTY
+					&& colors.includes(this.getColor(x,i))
+					&& this.getPiece(x,i) == VariantRules.ROOK)
+				{
+					return true;
+				}
+			}
+		}
+		else if (y == this.kingPos[colors[0]][1])
+		{
+			// Look for enemy rook on this row
+			for (let i=0; i<sizeX; i++)
+			{
+				if (this.board[i][y] != VariantRules.EMPTY
+					&& colors.includes(this.getColor(i,y))
+					&& this.getPiece(i,y) == VariantRules.ROOK)
+				{
+					return true;
+				}
+			}
+		}
+		return false;
 	}
 
-	updateVariables(move)
+	isAttackedByKnight([x,y], colors)
 	{
-		// Just update king(s) position(s)
-		const piece = this.getPiece(move.start.x,move.start.y);
-		const c = this.getColor(move.start.x,move.start.y);
-		if (piece == VariantRules.KING && move.appear.length > 0)
+		// Square (x,y) must be on same line as a knight,
+		// and there must be empty square(s) behind.
+		const V = VariantRules;
+		const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
+		const [sizeX,sizeY] = V.size;
+		outerLoop:
+		for (let step of steps)
 		{
-			this.kingPos[c][0] = move.appear[0].x;
-			this.kingPos[c][1] = move.appear[0].y;
+			const [i0,j0] = [x+step[0],y+step[1]];
+			if (i0>=0 && i0<sizeX && j0>=0 && j0<sizeY && this.board[i0][j0] == V.EMPTY)
+			{
+				// Try in opposite direction:
+				let [i,j] = [x-step[0],y-step[1]];
+				while (i>=0 && i<sizeX && j>=0 && j<sizeY && this.board[i][j] == V.EMPTY)
+				{
+					i -= step[0];
+					j -= step[1];
+				}
+				if (i>=0 && i<sizeX && j>=0 && j<sizeY && colors.includes(this.getColor(i,j))
+					&& this.getPiece(i,j) == V.KNIGHT)
+				{
+					if (!this.isImmobilized([i,j]))
+						return true;
+				}
+			}
 		}
-		// Does this move takes opponent's king?
-		const oppCol = this.getOppCol(c);
-		for (let i=1; i<move.vanish.length; i++)
+		return false;
+	}
+
+	isAttackedByBishop([x,y], colors)
+	{
+		// We cheat a little here: since this function is used exclusively for king,
+		// it's enough to check the immediate surrounding of the square.
+		const V = VariantRules;
+		const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
+		const [sizeX,sizeY] = V.size;
+		for (let step of adjacentSteps)
 		{
-			if (move.vanish[i].p == VariantRules.KING)
+			const [i,j] = [x+step[0],y+step[1]];
+			if (i>=0 && i<sizeX && j>=0 && j<sizeY && this.board[i][j]!=V.EMPTY
+				&& colors.includes(this.getColor(i,j)) && this.getPiece(i,j) == V.BISHOP)
 			{
-				this.kingPos[oppCol] = [-1,-1];
-				break;
+				return true; //bishops are never immobilized
 			}
 		}
+		return false;
 	}
 
-	unupdateVariables(move)
+	isAttackedByQueen([x,y], colors)
 	{
-		super.unupdateVariables(move);
-		const c = this.getColor(move.start.x,move.start.y);
-		const oppCol = this.getOppCol(c);
-		if (this.kingPos[oppCol][0] < 0)
+		// Square (x,y) must be adjacent to a queen, and the queen must have
+		// some free space in the opposite direction from (x,y)
+		const V = VariantRules;
+		const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
+		const [sizeX,sizeY] = V.size;
+		for (let step of adjacentSteps)
 		{
-			// Last move took opponent's king
-			for (let i=1; i<move.vanish.length; i++)
+			const sq2 = [x+2*step[0],y+2*step[1]];
+			if (sq2[0]>=0 && sq2[0]<sizeX && sq2[1]>=0 && sq2[1]<sizeY
+				&& this.board[sq2[0]][sq2[1]] == V.EMPTY)
 			{
-				const psq = move.vanish[i];
-				if (psq.p == 'k')
+				const sq1 = [x+step[0],y+step[1]];
+				if (this.board[sq1[0]][sq1[1]] != V.EMPTY
+					&& colors.includes(this.getColor(sq1[0],sq1[1]))
+					&& this.getPiece(sq1[0],sq1[1]) == V.QUEEN
+					&& !this.isImmobilized(sq1))
 				{
-					this.kingPos[oppCol] = [psq.x, psq.y];
-					break;
+					return true;
 				}
 			}
 		}
+		return false;
 	}
 
-	checkGameEnd()
+	updateVariables(move)
 	{
-		// Stalemate, or our king disappeared
-		return this.turn == "w" ? "0-1" : "1-0";
+		// Just update king(s) position(s)
+		const piece = this.getPiece(move.start.x,move.start.y);
+		const c = this.getColor(move.start.x,move.start.y);
+		if (piece == VariantRules.KING && move.appear.length > 0)
+		{
+			this.kingPos[c][0] = move.appear[0].x;
+			this.kingPos[c][1] = move.appear[0].y;
+		}
 	}
 
 	static get VALUES() { //TODO: totally experimental!
@@ -463,10 +573,6 @@ class UltimaRules extends ChessRules
 
 	static get SEARCH_DEPTH() { return 2; } //TODO?
 
-	static get THRESHOLD_MATE() {
-		return 500; //checkmates evals may be slightly below 1000
-	}
-
 	static GenRandInitFen()
 	{
 		let pieces = { "w": new Array(8), "b": new Array(8) };
diff --git a/views/rules/Ultima.pug b/views/rules/Ultima.pug
index df8571f4..423ba5d6 100644
--- a/views/rules/Ultima.pug
+++ b/views/rules/Ultima.pug
@@ -1,5 +1,5 @@
 p.boxed
-	| Pieces look the same but behave very differently.
+	| Most pieces look the same but behave very differently.
 	| They generally move like an orthodox queen,
 	| but capturing rules are complex.
 
@@ -11,21 +11,20 @@ ul
 	li Non-capturing moves: often like queen.
 	li Special moves: none.
 	li Captures: very special.
-	li End of game: capture the king.
+	li End of game: standard; see below.
 
 h3 Non-capturing moves
 
 // TODO: short paragraph, only the king moves like an orthodox king
+// Consider these rules modifications: http://www.inference.org.uk/mackay/ultima/ultima.html
 
 h3 Capturing moves
 
 // TODO...
-// TODO: dire comment suicider une pièce immobilisée (self-capture)
 
 h3 End of the game
 
 // TODO: show the situation from Wikipedia page
-// tell that it's much simpler to go until king capture and not very different in this case
 
 h3 Credits