From 2d7194bd9c976f444e43e5dc0a725823b6472eb9 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Tue, 18 Dec 2018 02:59:43 +0100
Subject: [PATCH] Some refactoring in variants logic: more robust FEN handling
 (untested)

---
 db/vchess.sqlite                          |   2 +-
 public/javascripts/base_rules.js          | 120 +++++++++++++++-------
 public/javascripts/variants/Alice.js      |  27 ++---
 public/javascripts/variants/Antiking.js   |  19 ++--
 public/javascripts/variants/Checkered.js  |  30 +++---
 public/javascripts/variants/Crazyhouse.js | 108 +++++++++++++++----
 public/javascripts/variants/Extinction.js |  33 +++---
 public/javascripts/variants/Grand.js      | 113 ++++++++++++++++----
 public/javascripts/variants/Loser.js      |  35 +++----
 public/javascripts/variants/Magnetic.js   |  77 +++++++++++++-
 public/javascripts/variants/Ultima.js     |  38 +++----
 public/javascripts/variants/Wildebeest.js |  38 ++++++-
 public/javascripts/variants/Zen.js        |  15 ++-
 13 files changed, 462 insertions(+), 193 deletions(-)

diff --git a/db/vchess.sqlite b/db/vchess.sqlite
index 9d7e4a2a..f1d4f13e 100644
--- a/db/vchess.sqlite
+++ b/db/vchess.sqlite
@@ -1 +1 @@
-#$# git-fat 25d1d22208da0bc58bf3076bfe684bbcb98894b2                16384
+#$# git-fat 039e3c0aadcb97c144dc2d12a35aeb83b4a3849e                16384
diff --git a/public/javascripts/base_rules.js b/public/javascripts/base_rules.js
index 620b86f8..ae0800cd 100644
--- a/public/javascripts/base_rules.js
+++ b/public/javascripts/base_rules.js
@@ -34,6 +34,10 @@ class ChessRules
 	//////////////
 	// MISC UTILS
 
+	static get HasFlags() { return true; } //some variants don't have flags
+
+	static get HasEnpassant() { return true; } //some variants don't have ep.
+
 	// Path to pieces
 	static getPpath(b)
 	{
@@ -57,7 +61,34 @@ class ChessRules
 	{
 		const fenParsed = V.ParseFen(fen);
 		// 1) Check position
-		const position = fenParsed.position;
+		if (!V.IsGoodPosition(fenParsed.position))
+			return false;
+		// 2) Check turn
+		if (!fenParsed.turn || !["w","b"].includes(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 (!fenParsed.enpassant)
+				return false;
+			if (fenParsed.enpassant != "-")
+			{
+				const ep = V.SquareToCoords(fenParsed.enpassant);
+				if (ep.y < 0 || ep.y > V.size.y || isNaN(ep.x) || ep.x < 0 || ep.x > V.size.x)
+					return false;
+			}
+		}
+		return true;
+	}
+
+	// Is position part of the FEN a priori correct?
+	static IsGoodPosition(position)
+	{
+		if (position.length == 0)
+			return false;
 		const rows = position.split("/");
 		if (rows.length != V.size.x)
 			return false;
@@ -79,19 +110,6 @@ class ChessRules
 			if (sumElts != V.size.y)
 				return false;
 		}
-		// 2) Check flags (if present)
-		if (!!fenParsed.flags && !V.IsGoodFlags(fenParsed.flags))
-			return false;
-		// 3) Check turn (if present)
-		if (!!fenParsed.turn && !["w","b"].includes(fenParsed.turn))
-			return false;
-		// 4) Check enpassant (if present)
-		if (!!fenParsed.enpassant)
-		{
-			const ep = V.SquareToCoords(fenParsed.enpassant);
-			if (ep.y < 0 || ep.y > V.size.y || isNaN(ep.x) || ep.x < 0 || ep.x > V.size.x)
-				return false;
-		}
 		return true;
 	}
 
@@ -101,6 +119,12 @@ class ChessRules
 		return !!flags.match(/^[01]{4,4}$/);
 	}
 
+	// 3 --> d (column letter from number)
+	static GetColumn(colnum)
+	{
+		return String.fromCharCode(97 + colnum);
+	}
+
 	// a4 --> {x:3,y:0}
 	static SquareToCoords(sq)
 	{
@@ -113,7 +137,7 @@ class ChessRules
 	// {x:0,y:4} --> e8
 	static CoordsToSquare(coords)
 	{
-		return String.fromCharCode(97 + coords.y) + (V.size.x - coords.x);
+		return V.GetColumn(coords.y) + (V.size.x - coords.x);
 	}
 
 	// Aggregates flags into one object
@@ -138,10 +162,7 @@ class ChessRules
 			const square = moveOrSquare;
 			if (square == "-")
 				return undefined;
-			return {
-				x: square[0].charCodeAt()-97,
-				y: V.size.x-parseInt(square[1])
-			};
+			return V.SquareToCoords(square);
 		}
 		// Argument is a move:
 		const move = moveOrSquare;
@@ -246,19 +267,25 @@ class ChessRules
 	static ParseFen(fen)
 	{
 		const fenParts = fen.split(" ");
-		return {
+		let res =
+		{
 			position: fenParts[0],
 			turn: fenParts[1],
-			flags: fenParts[2],
-			enpassant: fenParts[3],
 		};
+		let nextIdx = 2;
+		if (V.hasFlags)
+			Object.assign(res, {flags: fenParts[nextIdx++]});
+		if (V.HasEnpassant)
+			Object.assign(res, {enpassant: fenParts[nextIdx]});
+		return res;
 	}
 
 	// Return current fen (game state)
 	getFen()
 	{
-		return this.getBaseFen() + " " + this.turn + " " +
-			this.getFlagsFen() + " " + this.getEnpassantFen();
+		return this.getBaseFen() + " " + this.turn +
+			(V.HasFlags ? (" " + this.getFlagsFen()) : "") +
+			(V.HasEnpassant ? (" " + this.getEnpassantFen()) : "");
 	}
 
 	// Position part of the FEN string
@@ -311,7 +338,7 @@ class ChessRules
 	getEnpassantFen()
 	{
 		const L = this.epSquares.length;
-		if (L == 0)
+		if (!this.epSquares[L-1])
 			return "-"; //no en-passant
 		return V.CoordsToSquare(this.epSquares[L-1]);
 	}
@@ -361,18 +388,13 @@ class ChessRules
 		this.setOtherVariables(fen);
 	}
 
-	// Some additional variables from FEN (variant dependant)
-	setOtherVariables(fen)
+	// Scan board for kings and rooks positions
+	scanKingsRooks(fen)
 	{
-		// Set flags and enpassant:
-		const parsedFen = V.ParseFen(fen);
-		this.setFlags(fenParsed.flags);
-		this.epSquares = [ V.SquareToCoords(parsedFen.enpassant) ];
-		// Search for king and rooks positions:
 		this.INIT_COL_KING = {'w':-1, 'b':-1};
 		this.INIT_COL_ROOK = {'w':[-1,-1], 'b':[-1,-1]};
 		this.kingPos = {'w':[-1,-1], 'b':[-1,-1]}; //squares of white and black king
-		const fenRows = parsedFen.position.split("/");
+		const fenRows = V.ParseFen(fen).position.split("/");
 		for (let i=0; i<fenRows.length; i++)
 		{
 			let k = 0; //column index on board
@@ -410,6 +432,24 @@ class ChessRules
 		}
 	}
 
+	// Some additional variables from FEN (variant dependant)
+	setOtherVariables(fen)
+	{
+		// Set flags and enpassant:
+		const parsedFen = V.ParseFen(fen);
+		if (V.HasFlags)
+			this.setFlags(fenParsed.flags);
+		if (V.HasEnpassant)
+		{
+			const epSq = parsedFen.enpassant != "-"
+				? V.SquareToCoords(parsedFen.enpassant)
+				: undefined;
+			this.epSquares = [ epSq ];
+		}
+		// Search for king and rooks positions:
+		this.scanKingsRooks(fen);
+	}
+
 	/////////////////////
 	// GETTERS & SETTERS
 
@@ -615,7 +655,7 @@ class ChessRules
 
 		// En passant
 		const Lep = this.epSquares.length;
-		const epSquare = (Lep>0 ? this.epSquares[Lep-1] : undefined);
+		const epSquare = this.epSquares[Lep-1]; //always at least one element
 		if (!!epSquare && epSquare.x == x+shift && Math.abs(epSquare.y - y) == 1)
 		{
 			const epStep = epSquare.y - y;
@@ -961,10 +1001,12 @@ class ChessRules
 		if (!!ingame)
 			move.notation = [this.getNotation(move), this.getLongNotation(move)];
 
-		move.flags = JSON.stringify(this.aggregateFlags()); //save flags (for undo)
+		if (V.HasFlags)
+			move.flags = JSON.stringify(this.aggregateFlags()); //save flags (for undo)
 		this.updateVariables(move);
 		this.moves.push(move);
-		this.epSquares.push( this.getEpSquare(move) );
+		if (V.HasEnpassant)
+			this.epSquares.push( this.getEpSquare(move) );
 		this.turn = this.getOppCol(this.turn);
 		V.PlayOnBoard(this.board, move);
 
@@ -979,10 +1021,12 @@ class ChessRules
 	{
 		V.UndoOnBoard(this.board, move);
 		this.turn = this.getOppCol(this.turn);
-		this.epSquares.pop();
+		if (V.HasEnpassant)
+			this.epSquares.pop();
 		this.moves.pop();
 		this.unupdateVariables(move);
-		this.disaggregateFlags(JSON.parse(move.flags));
+		if (V.HasFlags)
+			this.disaggregateFlags(JSON.parse(move.flags));
 
 		// DEBUG:
 //		if (this.getFen() != this.states[this.states.length-1])
diff --git a/public/javascripts/variants/Alice.js b/public/javascripts/variants/Alice.js
index ece59cdc..00103a7b 100644
--- a/public/javascripts/variants/Alice.js
+++ b/public/javascripts/variants/Alice.js
@@ -29,24 +29,24 @@ class AliceRules extends ChessRules
 		return (Object.keys(this.ALICE_PIECES).includes(b[1]) ? "Alice/" : "") + b;
 	}
 
-	static get PIECES() {
+	static get PIECES()
+	{
 		return ChessRules.PIECES.concat(Object.keys(V.ALICE_PIECES));
 	}
 
-	initVariables(fen)
+	setOtherVariables(fen)
 	{
-		super.initVariables(fen);
-		const fenParts = fen.split(" ");
-		const position = fenParts[0].split("/");
+		super.setOtherVariables(fen);
+		const rows = V.ParseFen(fen).position.split("/");
 		if (this.kingPos["w"][0] < 0 || this.kingPos["b"][0] < 0)
 		{
-			// INIT_COL_XXX won't be used, so no need to set them for Alice kings
-			for (let i=0; i<position.length; i++)
+			// INIT_COL_XXX won't be required if Alice kings are found (means 'king moved')
+			for (let i=0; i<rows.length; i++)
 			{
 				let k = 0; //column index on board
-				for (let j=0; j<position[i].length; j++)
+				for (let j=0; j<rows[i].length; j++)
 				{
-					switch (position[i].charAt(j))
+					switch (rows[i].charAt(j))
 					{
 						case 'l':
 							this.kingPos['b'] = [i,k];
@@ -55,7 +55,7 @@ class AliceRules extends ChessRules
 							this.kingPos['w'] = [i,k];
 							break;
 						default:
-							let num = parseInt(position[i].charAt(j));
+							const num = parseInt(rows[i].charAt(j));
 							if (!isNaN(num))
 								k += (num-1);
 					}
@@ -293,7 +293,8 @@ class AliceRules extends ChessRules
 		return res;
 	}
 
-	static get VALUES() {
+	static get VALUES()
+	{
 		return Object.assign(
 			ChessRules.VALUES,
 			{
@@ -317,13 +318,13 @@ class AliceRules extends ChessRules
 				return "0-0";
 		}
 
-		const finalSquare = String.fromCharCode(97 + move.end.y) + (V.size.x-move.end.x);
+		const finalSquare = V.CoordsToSquare(move.end);
 		const piece = this.getPiece(move.start.x, move.start.y);
 
 		const captureMark = (move.vanish.length > move.appear.length ? "x" : "");
 		let pawnMark = "";
 		if (["p","s"].includes(piece) && captureMark.length == 1)
-			pawnMark = String.fromCharCode(97 + move.start.y); //start column
+			pawnMark = V.GetColumn(move.start.y); //start column
 
 		// Piece or pawn movement
 		let notation = piece.toUpperCase() + pawnMark + captureMark + finalSquare;
diff --git a/public/javascripts/variants/Antiking.js b/public/javascripts/variants/Antiking.js
index 2b07cfbe..38cadf22 100644
--- a/public/javascripts/variants/Antiking.js
+++ b/public/javascripts/variants/Antiking.js
@@ -7,21 +7,22 @@ class AntikingRules extends ChessRules
 
 	static get ANTIKING() { return 'a'; }
 
-	static get PIECES() {
+	static get PIECES()
+	{
 		return ChessRules.PIECES.concat([V.ANTIKING]);
 	}
 
-	initVariables(fen)
+	setOtherVariables(fen)
 	{
-		super.initVariables(fen);
+		super.setOtherVariables(fen);
 		this.antikingPos = {'w':[-1,-1], 'b':[-1,-1]};
-		const position = fen.split(" ")[0].split("/");
-		for (let i=0; i<position.length; i++)
+		const rows = V.ParseFen(fen).position.split("/");
+		for (let i=0; i<rows.length; i++)
 		{
 			let k = 0;
-			for (let j=0; j<position[i].length; j++)
+			for (let j=0; j<rows[i].length; j++)
 			{
-				switch (position[i].charAt(j))
+				switch (rows[i].charAt(j))
 				{
 					case 'a':
 						this.antikingPos['b'] = [i,k];
@@ -30,7 +31,7 @@ class AntikingRules extends ChessRules
 						this.antikingPos['w'] = [i,k];
 						break;
 					default:
-						let num = parseInt(position[i].charAt(j));
+						const num = parseInt(rows[i].charAt(j));
 						if (!isNaN(num))
 							k += (num-1);
 				}
@@ -200,6 +201,6 @@ class AntikingRules extends ChessRules
 		return pieces["b"].join("") + "/" + ranks23_black +
 			"/8/8/" +
 			ranks23_white + "/" + pieces["w"].join("").toUpperCase() +
-			" 1111 w";
+			" w 1111";
 	}
 }
diff --git a/public/javascripts/variants/Checkered.js b/public/javascripts/variants/Checkered.js
index 370c6896..314cd8e4 100644
--- a/public/javascripts/variants/Checkered.js
+++ b/public/javascripts/variants/Checkered.js
@@ -4,6 +4,7 @@ class CheckeredRules extends ChessRules
 	{
 		return b[0]=='c' ? "Checkered/"+b : b;
 	}
+
 	static board2fen(b)
 	{
 		const checkered_codes = {
@@ -17,6 +18,7 @@ class CheckeredRules extends ChessRules
 			return checkered_codes[b[1]];
 		return ChessRules.board2fen(b);
 	}
+
 	static fen2board(f)
 	{
 		// Tolerate upper-case versions of checkered pieces (why not?)
@@ -37,7 +39,8 @@ class CheckeredRules extends ChessRules
 		return ChessRules.fen2board(f);
 	}
 
-	static get PIECES() {
+	static get PIECES()
+	{
 		return ChessRules.PIECES.concat(['s','t','u','c','o']);
 	}
 
@@ -65,13 +68,12 @@ class CheckeredRules extends ChessRules
 		}
 	}
 
-	// Aggregates flags into one object
-	get flags() {
+	aggregateFlags()
+	{
 		return [this.castleFlags, this.pawnFlags];
 	}
 
-	// Reverse operation
-	parseFlags(flags)
+	disaggregateFlags(flags)
 	{
 		this.castleFlags = flags[0];
 		this.pawnFlags = flags[1];
@@ -187,13 +189,14 @@ class CheckeredRules extends ChessRules
 	{
 		this.play(move);
 		const color = this.turn;
-		this.moves.push(move); //artifically change turn, for checkered pawns (TODO)
+		// Artifically change turn, for checkered pawns
+		this.turn = this.getOppCol(color);
 		const kingAttacked = this.isAttacked(
 			this.kingPos[color], [this.getOppCol(color),'c']);
 		let res = kingAttacked
-			? [ JSON.parse(JSON.stringify(this.kingPos[color])) ] //need to duplicate!
-			: [ ];
-		this.moves.pop();
+			? [JSON.parse(JSON.stringify(this.kingPos[color]))] //need to duplicate!
+			: [];
+		this.turn = color;
 		this.undo(move);
 		return res;
 	}
@@ -242,7 +245,7 @@ class CheckeredRules extends ChessRules
 	{
 		const randFen = ChessRules.GenRandInitFen();
 		// Add 16 pawns flags:
-		return randFen.replace(" 1111 w", " 11111111111111111111 w");
+		return randFen.replace(" w 1111", " w 11111111111111111111");
 	}
 
 	getFlagsFen()
@@ -269,10 +272,9 @@ class CheckeredRules extends ChessRules
 		}
 
 		// Translate final square
-		let finalSquare =
-			String.fromCharCode(97 + move.end.y) + (V.size.x-move.end.x);
+		const finalSquare = V.CoordsToSquare(move.end);
 
-		let piece = this.getPiece(move.start.x, move.start.y);
+		const piece = this.getPiece(move.start.x, move.start.y);
 		if (piece == V.PAWN)
 		{
 			// Pawn move
@@ -280,7 +282,7 @@ class CheckeredRules extends ChessRules
 			if (move.vanish.length > 1)
 			{
 				// Capture
-				let startColumn = String.fromCharCode(97 + move.start.y);
+				const startColumn = V.GetColumn(move.start.y);
 				notation = startColumn + "x" + finalSquare +
 					"=" + move.appear[0].p.toUpperCase();
 			}
diff --git a/public/javascripts/variants/Crazyhouse.js b/public/javascripts/variants/Crazyhouse.js
index bf47197d..00452cbd 100644
--- a/public/javascripts/variants/Crazyhouse.js
+++ b/public/javascripts/variants/Crazyhouse.js
@@ -1,31 +1,97 @@
 class CrazyhouseRules extends ChessRules
 {
-	initVariables(fen)
+	static IsGoodFen(fen)
 	{
-		super.initVariables(fen);
-		// Also init reserves (used by the interface to show landing pieces)
+		if (!ChessRules.IsGoodFen(fen))
+			return false;
+		const fenParsed = V.ParseFen(fen);
+		// 5) Check reserves
+		if (!fenParsed.reserve || !fenParsed.reserve.match(/^[0-9]{10,10}$/))
+			return false;
+		// 6) Check promoted array
+		if (!fenParsed.promoted)
+			return false;
+		fenpromoted = fenParsed.promoted;
+		if (fenpromoted == "-")
+			return true; //no promoted piece on board
+		const squares = fenpromoted.split(",");
+		for (let square of squares)
+		{
+			const c = V.SquareToCoords(square);
+			if (c.y < 0 || c.y > V.size.y || isNaN(c.x) || c.x < 0 || c.x > V.size.x)
+				return false;
+		}
+		return true;
+	}
+
+	static GenRandInitFen()
+	{
+		const fen = ChessRules.GenRandInitFen();
+		return fen.replace(" w 1111", " w 1111 0000000000 -");
+	}
+
+	getFen()
+	{
+		return super.getFen() + " " + this.getReserveFen() + " " + this.getPromotedFen();
+	}
+
+	getReserveFen()
+	{
+		let counts = _.map(_.range(10), 0);
+		for (let i=0; i<V.PIECES.length; i++)
+		{
+			counts[i] = this.reserve["w"][V.PIECES[i]];
+			counts[5+i] = this.reserve["b"][V.PIECES[i]];
+		}
+		return counts.join("");
+	}
+
+	getPromotedFen()
+	{
+		let res = "";
+		for (let i=0; i<V.size.x; i++)
+		{
+			for (let j=0; j<V.size.y; j++)
+			{
+				if (this.promoted[i][j])
+					res += V.CoordsToSquare({x:i,y:j});
+			}
+		}
+		if (res.length > 0)
+			res = res.slice(0,-1); //remove last comma
+		return res;
+	}
+
+	setOtherVariables(fen)
+	{
+		super.setOtherVariables(fen);
+		const fenParsed = V.ParseFen(fen);
+		// Also init reserves (used by the interface to show landable pieces)
 		this.reserve =
 		{
 			"w":
 			{
-				[V.PAWN]: 0,
-				[V.ROOK]: 0,
-				[V.KNIGHT]: 0,
-				[V.BISHOP]: 0,
-				[V.QUEEN]: 0,
+				[V.PAWN]: parseInt(fenParsed.reserve[0]),
+				[V.ROOK]: parseInt(fenParsed.reserve[1]),
+				[V.KNIGHT]: parseInt(fenParsed.reserve[2]),
+				[V.BISHOP]: parseInt(fenParsed.reserve[3]),
+				[V.QUEEN]: parseInt(fenParsed.reserve[4]),
 			},
 			"b":
 			{
-				[V.PAWN]: 0,
-				[V.ROOK]: 0,
-				[V.KNIGHT]: 0,
-				[V.BISHOP]: 0,
-				[V.QUEEN]: 0,
+				[V.PAWN]: parseInt(fenParsed.reserve[5]),
+				[V.ROOK]: parseInt(fenParsed.reserve[6]),
+				[V.KNIGHT]: parseInt(fenParsed.reserve[7]),
+				[V.BISHOP]: parseInt(fenParsed.reserve[8]),
+				[V.QUEEN]: parseInt(fenParsed.reserve[9]),
 			}
 		};
 		this.promoted = doubleArray(V.size.x, V.size.y, false);
-		// May be a continuation: adjust numbers of pieces in reserve + promoted pieces
-		this.moves.forEach(m => { this.updateVariables(m); });
+		for (let square of fenParsd.promoted.split(","))
+		{
+			const [x,y] = V.SquareToCoords(square);
+			promoted[x][y] = true;
+		}
 	}
 
 	getColor(i,j)
@@ -34,6 +100,7 @@ class CrazyhouseRules extends ChessRules
 			return (i==V.size.x ? "w" : "b");
 		return this.board[i][j].charAt(0);
 	}
+
 	getPiece(i,j)
 	{
 		if (i >= V.size.x)
@@ -48,7 +115,8 @@ class CrazyhouseRules extends ChessRules
 	}
 
 	// Ordering on reserve pieces
-	static get RESERVE_PIECES() {
+	static get RESERVE_PIECES()
+	{
 		return [V.PAWN,V.ROOK,V.KNIGHT,V.BISHOP,V.QUEEN];
 	}
 
@@ -188,17 +256,13 @@ class CrazyhouseRules extends ChessRules
 		// Rebirth:
 		const piece =
 			(move.appear[0].p != V.PAWN ? move.appear[0].p.toUpperCase() : "");
-		const finalSquare =
-			String.fromCharCode(97 + move.end.y) + (V.size.x-move.end.x);
-		return piece + "@" + finalSquare;
+		return piece + "@" + V.CoordsToSquare(move.end);
 	}
 
 	getLongNotation(move)
 	{
 		if (move.vanish.length > 0)
 			return super.getLongNotation(move);
-		const finalSquare =
-			String.fromCharCode(97 + move.end.y) + (V.size.x-move.end.x);
-		return "@" + finalSquare;
+		return "@" + V.CoordsToSquare(move.end);
 	}
 }
diff --git a/public/javascripts/variants/Extinction.js b/public/javascripts/variants/Extinction.js
index 2b0aecaa..49d61b2d 100644
--- a/public/javascripts/variants/Extinction.js
+++ b/public/javascripts/variants/Extinction.js
@@ -1,27 +1,30 @@
 class ExtinctionRules extends ChessRules
 {
-	initVariables(fen)
+	setOtherVariables(fen)
 	{
-		super.initVariables(fen);
+		super.setOtherVariables(fen);
+		const pos = V.ParseFen(fen).position;
+		// NOTE: no need for safety "|| []", because each piece type must be present
+		// (otherwise game is already over!)
 		this.material =
 		{
 			"w":
 			{
-				[V.KING]: 1,
-				[V.QUEEN]: 1,
-				[V.ROOK]: 2,
-				[V.KNIGHT]: 2,
-				[V.BISHOP]: 2,
-				[V.PAWN]: 8
+				[V.KING]: pos.match(/K/g).length,
+				[V.QUEEN]: pos.match(/Q/g).length,
+				[V.ROOK]: pos.match(/R/g).length,
+				[V.KNIGHT]: pos.match(/N/g).length,
+				[V.BISHOP]: pos.match(/B/g).length,
+				[V.PAWN]: pos.match(/P/g).length
 			},
 			"b":
 			{
-				[V.KING]: 1,
-				[V.QUEEN]: 1,
-				[V.ROOK]: 2,
-				[V.KNIGHT]: 2,
-				[V.BISHOP]: 2,
-				[V.PAWN]: 8
+				[V.KING]: pos.match(/k/g).length,
+				[V.QUEEN]: pos.match(/q/g).length,
+				[V.ROOK]: pos.match(/r/g).length,
+				[V.KNIGHT]: pos.match(/n/g).length,
+				[V.BISHOP]: pos.match(/b/g).length,
+				[V.PAWN]: pos.match(/p/g).length
 			}
 		};
 	}
@@ -117,7 +120,7 @@ class ExtinctionRules extends ChessRules
 
 	checkGameEnd()
 	{
-		return this.turn == "w" ? "0-1" : "1-0";
+		return (this.turn == "w" ? "0-1" : "1-0");
 	}
 
 	evalPosition()
diff --git a/public/javascripts/variants/Grand.js b/public/javascripts/variants/Grand.js
index 276acbc3..b598f2d2 100644
--- a/public/javascripts/variants/Grand.js
+++ b/public/javascripts/variants/Grand.js
@@ -7,10 +7,63 @@ class GrandRules extends ChessRules
 		return ([V.MARSHALL,V.CARDINAL].includes(b[1]) ? "Grand/" : "") + b;
 	}
 
-	initVariables(fen)
+	static IsGoodFen(fen)
 	{
-		super.initVariables(fen);
-		this.captures = { "w": {}, "b": {} }; //for promotions
+		if (!ChessRules.IsGoodFen(fen))
+			return false;
+		const fenParsed = V.ParseFen(fen);
+		// 5) Check captures
+		if (!fenParsed.captured || !fenParsed.captured.match(/^[0-9]{10,10}$/))
+			return false;
+		return true;
+	}
+
+	static GenRandInitFen()
+	{
+		const fen = ChessRules.GenRandInitFen();
+		return fen.replace(" w 1111", " w 1111 0000000000");
+	}
+
+	getFen()
+	{
+		return super.getFen() + " " + this.getCapturedFen();
+	}
+
+	getCapturedFen()
+	{
+		let counts = _.map(_.range(10), 0);
+		for (let i=0; i<V.PIECES.length; i++)
+		{
+			counts[i] = this.captured["w"][V.PIECES[i]];
+			counts[5+i] = this.captured["b"][V.PIECES[i]];
+		}
+		return counts.join("");
+	}
+
+	setOtherVariables(fen)
+	{
+		super.setOtherVariables(fen);
+		const fenParsed = V.ParseFen(fen);
+		// Initialize captured pieces' counts from FEN
+		this.captured =
+		{
+			"w":
+			{
+				[V.PAWN]: parseInt(fenParsed.captured[0]),
+				[V.ROOK]: parseInt(fenParsed.captured[1]),
+				[V.KNIGHT]: parseInt(fenParsed.captured[2]),
+				[V.BISHOP]: parseInt(fenParsed.captured[3]),
+				[V.QUEEN]: parseInt(fenParsed.captured[4]),
+			},
+			"b":
+			{
+				[V.PAWN]: parseInt(fenParsed.captured[5]),
+				[V.ROOK]: parseInt(fenParsed.captured[6]),
+				[V.KNIGHT]: parseInt(fenParsed.captured[7]),
+				[V.BISHOP]: parseInt(fenParsed.captured[8]),
+				[V.QUEEN]: parseInt(fenParsed.captured[9]),
+			}
+		};
 	}
 
 	static get size() { return {x:10,y:10}; }
@@ -18,13 +71,42 @@ class GrandRules extends ChessRules
 	static get MARSHALL() { return 'm'; } //rook+knight
 	static get CARDINAL() { return 'c'; } //bishop+knight
 
-	static get PIECES() {
+	static get PIECES()
+	{
 		return ChessRules.PIECES.concat([V.MARSHALL,V.CARDINAL]);
 	}
 
+	// There may be 2 enPassant squares (if pawn jump 3 squares)
+	getEnpassantFen()
+	{
+		const L = this.epSquares.length;
+		if (!this.epSquares[L-1])
+			return "-"; //no en-passant
+		let res = "";
+		this.epSquares[L-1].forEach(sq => {
+			res += V.CoordsToSquare(sq) + ",";
+		});
+		return res.slice(0,-1); //remove last comma
+	}
+
 	// En-passant after 2-sq or 3-sq jumps
-	getEpSquare(move)
+	getEpSquare(moveOrSquare)
 	{
+		if (!moveOrSquare)
+			return undefined;
+		if (typeof moveOrSquare === "string")
+		{
+			const square = moveOrSquare;
+			if (square == "-")
+				return undefined;
+			let res = [];
+			square.split(",").forEach(sq => {
+				res.push(V.SquareToCoords(sq));
+			});
+			return res;
+		}
+		// Argument is a move:
+		const move = moveOrSquare;
 		const [sx,sy,ex] = [move.start.x,move.start.y,move.end.x];
 		if (this.getPiece(sx,sy) == V.PAWN && Math.abs(sx - ex) >= 2)
 		{
@@ -104,7 +186,7 @@ class GrandRules extends ChessRules
 			// Promotion
 			let promotionPieces = [V.ROOK,V.KNIGHT,V.BISHOP,V.QUEEN,V.MARSHALL,V.CARDINAL];
 			promotionPieces.forEach(p => {
-				if (!this.captures[color][p] || this.captures[color][p]==0)
+				if (this.captured[color][p]==0)
 					return;
 				// Normal move
 				if (this.board[x+shift][y] == V.EMPTY)
@@ -189,11 +271,8 @@ class GrandRules extends ChessRules
 		super.updateVariables(move);
 		if (move.vanish.length==2 && move.appear.length==1 && move.vanish[1].p != V.PAWN)
 		{
-			// Capture: update this.captures
-			if (!this.captures[move.vanish[1].c][move.vanish[1].p])
-				this.captures[move.vanish[1].c][move.vanish[1].p] = 1;
-			else
-				this.captures[move.vanish[1].c][move.vanish[1].p]++;
+			// Capture: update this.captured
+			this.captured[move.vanish[1].c][move.vanish[1].p]++;
 		}
 	}
 
@@ -201,13 +280,11 @@ class GrandRules extends ChessRules
 	{
 		super.unupdateVariables(move);
 		if (move.vanish.length==2 && move.appear.length==1 && move.vanish[1].p != V.PAWN)
-		{
-			this.captures[move.vanish[1].c][move.vanish[1].p] =
-				Math.max(0, this.captures[move.vanish[1].c][move.vanish[1].p]-1);
-		}
+			this.captured[move.vanish[1].c][move.vanish[1].p]--;
 	}
 
-	static get VALUES() {
+	static get VALUES()
+	{
 		return Object.assign(
 			ChessRules.VALUES,
 			{'c': 5, 'm': 7} //experimental
@@ -216,7 +293,7 @@ class GrandRules extends ChessRules
 
 	static get SEARCH_DEPTH() { return 2; }
 
-	// TODO: this function could be generalized and shared better
+	// TODO: this function could be generalized and shared better (how ?!...)
 	static GenRandInitFen()
 	{
 		let pieces = { "w": new Array(10), "b": new Array(10) };
@@ -278,6 +355,6 @@ class GrandRules extends ChessRules
 		return pieces["b"].join("") +
 			"/pppppppppp/10/10/10/10/10/10/PPPPPPPPPP/" +
 			pieces["w"].join("").toUpperCase() +
-			" 1111 w";
+			" w 1111 -";
 	}
 }
diff --git a/public/javascripts/variants/Loser.js b/public/javascripts/variants/Loser.js
index 2d60d6e8..5f156129 100644
--- a/public/javascripts/variants/Loser.js
+++ b/public/javascripts/variants/Loser.js
@@ -1,20 +1,15 @@
 class LoserRules extends ChessRules
 {
-	initVariables(fen)
-	{
-		const epSq = this.moves.length > 0 ? this.getEpSquare(this.lastMove) : undefined;
-		this.epSquares = [ epSq ];
-	}
-
-	static IsGoodFlags(flags)
-	{
-		return true; //anything is good: no flags
-	}
+	static get HasFlags() { return false; }
 
-	setFlags(fenflags)
+	setOtherVariables(fen)
 	{
-		// No castling, hence no flags; but flags defined for compatibility
-		this.castleFlags = { "w":[false,false], "b":[false,false] };
+		const parsedFen = V.ParseFen(fen);
+		const epSq = parsedFen.enpassant != "-"
+			? V.SquareToCoords(parsedFen.enpassant)
+			: undefined;
+		this.epSquares = [ epSq ];
+		this.scanKingsRooks(fen);
 	}
 
 	getPotentialPawnMoves([x,y])
@@ -48,6 +43,7 @@ class LoserRules extends ChessRules
 
 	getPotentialKingMoves(sq)
 	{
+		// No castle:
 		return this.getSlideNJumpMoves(sq,
 			V.steps[V.ROOK].concat(V.steps[V.BISHOP]), "oneStep");
 	}
@@ -111,22 +107,19 @@ class LoserRules extends ChessRules
 		return [];
 	}
 
-	// Unused:
+	// No variables update because no castling
 	updateVariables(move) { }
 	unupdateVariables(move) { }
 
-	getFlagsFen()
-	{
-		return "-";
-	}
-
 	checkGameEnd()
 	{
 		// No valid move: you win!
 		return this.turn == "w" ? "1-0" : "0-1";
 	}
 
-	static get VALUES() { //experimental...
+	static get VALUES()
+	{
+		// Experimental...
 		return {
 			'p': 1,
 			'r': 7,
@@ -197,6 +190,6 @@ class LoserRules extends ChessRules
 		return pieces["b"].join("") +
 			"/pppppppp/8/8/8/8/PPPPPPPP/" +
 			pieces["w"].join("").toUpperCase() +
-			" 0000 w"; //add flags (TODO?!)
+			" w -"; //no en-passant
 	}
 }
diff --git a/public/javascripts/variants/Magnetic.js b/public/javascripts/variants/Magnetic.js
index 2e90e6c0..225489ec 100644
--- a/public/javascripts/variants/Magnetic.js
+++ b/public/javascripts/variants/Magnetic.js
@@ -1,8 +1,13 @@
 class MagneticRules extends ChessRules
 {
-	getEpSquare(move)
+	static get HasEnpassant { return false; }
+
+	setOtherVariables(fen)
 	{
-		return undefined; //no en-passant
+		// No en-passant:
+		const parsedFen = V.ParseFen(fen);
+		this.setFlags(fenParsed.flags);
+		this.scanKingsRooks(fen);
 	}
 
 	getPotentialMovesFrom([x,y])
@@ -19,6 +24,67 @@ class MagneticRules extends ChessRules
 		return moves;
 	}
 
+	getPotentialPawnMoves([x,y])
+	{
+		const color = this.turn;
+		let moves = [];
+		const [sizeX,sizeY] = [V.size.x,V.size.y];
+		const shift = (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);
+
+		if (x+shift >= 0 && x+shift < sizeX && x+shift != lastRank)
+		{
+			// Normal moves
+			if (this.board[x+shift][y] == V.EMPTY)
+			{
+				moves.push(this.getBasicMove([x,y], [x+shift,y]));
+				// Next condition because variants with pawns on 1st rank allow them to jump
+				if ([startRank,firstRank].includes(x) && this.board[x+2*shift][y] == V.EMPTY)
+				{
+					// Two squares jump
+					moves.push(this.getBasicMove([x,y], [x+2*shift,y]));
+				}
+			}
+			// Captures
+			if (y>0 && this.board[x+shift][y-1] != V.EMPTY
+				&& this.canTake([x,y], [x+shift,y-1]))
+			{
+				moves.push(this.getBasicMove([x,y], [x+shift,y-1]));
+			}
+			if (y<sizeY-1 && this.board[x+shift][y+1] != V.EMPTY
+				&& this.canTake([x,y], [x+shift,y+1]))
+			{
+				moves.push(this.getBasicMove([x,y], [x+shift,y+1]));
+			}
+		}
+
+		if (x+shift == lastRank)
+		{
+			// Promotion
+			const pawnColor = this.getColor(x,y); //can be different for checkered
+			let promotionPieces = [V.ROOK,V.KNIGHT,V.BISHOP,V.QUEEN];
+			promotionPieces.forEach(p => {
+				// Normal move
+				if (this.board[x+shift][y] == V.EMPTY)
+					moves.push(this.getBasicMove([x,y], [x+shift,y], {c:pawnColor,p:p}));
+				// Captures
+				if (y>0 && this.board[x+shift][y-1] != V.EMPTY
+					&& this.canTake([x,y], [x+shift,y-1]))
+				{
+					moves.push(this.getBasicMove([x,y], [x+shift,y-1], {c:pawnColor,p:p}));
+				}
+				if (y<sizeY-1 && this.board[x+shift][y+1] != V.EMPTY
+					&& this.canTake([x,y], [x+shift,y+1]))
+				{
+					moves.push(this.getBasicMove([x,y], [x+shift,y+1], {c:pawnColor,p:p}));
+				}
+			});
+		}
+		return moves; //no en-passant
+	}
+
 	// Complete a move with magnetic actions
 	// TODO: job is done multiple times for (normal) promotions.
 	applyMagneticLaws(move)
@@ -154,8 +220,8 @@ class MagneticRules extends ChessRules
 		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)) ]
-			: [ ];
+			? [JSON.parse(JSON.stringify(saveKingPos))]
+			: [];
 		this.undo(move);
 		return res;
 	}
@@ -210,7 +276,8 @@ class MagneticRules extends ChessRules
 		return this.turn == "w" ? "0-1" : "1-0";
 	}
 
-	static get THRESHOLD_MATE() {
+	static get THRESHOLD_MATE()
+	{
 		return 500; //checkmates evals may be slightly below 1000
 	}
 }
diff --git a/public/javascripts/variants/Ultima.js b/public/javascripts/variants/Ultima.js
index 05fba2f5..7f4a7ebe 100644
--- a/public/javascripts/variants/Ultima.js
+++ b/public/javascripts/variants/Ultima.js
@@ -1,5 +1,9 @@
 class UltimaRules extends ChessRules
 {
+	static get HasFlags { return false; }
+
+	static get HasEnpassant { return false; }
+
 	static getPpath(b)
 	{
 		if (b[1] == "m") //'m' for Immobilizer (I is too similar to 1)
@@ -7,16 +11,13 @@ class UltimaRules extends ChessRules
 		return b; //usual piece
 	}
 
-	static get PIECES() {
-		return ChessRules.PIECES.concat([V.IMMOBILIZER]);
-	}
-
-	static IsGoodFlags(flags)
+	static get PIECES()
 	{
-		return true; //anything is good: no flags
+		return ChessRules.PIECES.concat([V.IMMOBILIZER]);
 	}
 
-	initVariables(fen)
+	// No castling, but checks, so keep track of kings
+	setOtherVariables(fen)
 	{
 		this.kingPos = {'w':[-1,-1], 'b':[-1,-1]};
 		const fenParts = fen.split(" ");
@@ -42,13 +43,6 @@ class UltimaRules extends ChessRules
 				k++;
 			}
 		}
-		this.epSquares = []; //no en-passant here
-	}
-
-	setFlags(fenflags)
-	{
-		// TODO: for compatibility?
-		this.castleFlags = {"w":[false,false], "b":[false,false]};
 	}
 
 	static get IMMOBILIZER() { return 'm'; }
@@ -544,7 +538,9 @@ class UltimaRules extends ChessRules
 		}
 	}
 
-	static get VALUES() { //TODO: totally experimental!
+	static get VALUES()
+	{
+		// TODO: totally experimental!
 		return {
 			'p': 1,
 			'r': 2,
@@ -608,19 +604,13 @@ class UltimaRules extends ChessRules
 		return pieces["b"].join("") +
 			"/pppppppp/8/8/8/8/PPPPPPPP/" +
 			pieces["w"].join("").toUpperCase() +
-			" 0000 w"; //TODO: flags?!
-	}
-
-	getFlagsFen()
-	{
-		return "0000"; //TODO: or "-" ?
+			" w";
 	}
 
 	getNotation(move)
 	{
-		const initialSquare =
-			String.fromCharCode(97 + move.start.y) + (V.size.x-move.start.x);
-		const finalSquare = String.fromCharCode(97 + move.end.y) + (V.size.x-move.end.x);
+		const initialSquare = V.CoordsToSquare(move.start);
+		const finalSquare = V.CoordsToSquare(move.end);
 		let notation = undefined;
 		if (move.appear[0].p == V.PAWN)
 		{
diff --git a/public/javascripts/variants/Wildebeest.js b/public/javascripts/variants/Wildebeest.js
index 9b074108..4194521e 100644
--- a/public/javascripts/variants/Wildebeest.js
+++ b/public/javascripts/variants/Wildebeest.js
@@ -10,20 +10,50 @@ class WildebeestRules extends ChessRules
 	static get CAMEL() { return 'c'; }
 	static get WILDEBEEST() { return 'w'; }
 
-	static get PIECES() {
+	static get PIECES()
+	{
 		return ChessRules.PIECES.concat([V.CAMEL,V.WILDEBEEST]);
 	}
 
-	static get steps() {
+	static get steps()
+	{
 		return Object.assign(
 			ChessRules.steps, //add camel moves:
 			{'c': [ [-3,-1],[-3,1],[-1,-3],[-1,3],[1,-3],[1,3],[3,-1],[3,1] ]}
 		);
 	}
 
+	// There may be 2 enPassant squares (if pawn jump 3 squares)
+	getEnpassantFen()
+	{
+		const L = this.epSquares.length;
+		if (!this.epSquares[L-1])
+			return "-"; //no en-passant
+		let res = "";
+		this.epSquares[L-1].forEach(sq => {
+			res += V.CoordsToSquare(sq) + ",";
+		});
+		return res.slice(0,-1); //remove last comma
+	}
+
 	// En-passant after 2-sq or 3-sq jumps
-	getEpSquare(move)
+	getEpSquare(moveOrSquare)
 	{
+		if (!moveOrSquare)
+			return undefined;
+		if (typeof moveOrSquare === "string")
+		{
+			const square = moveOrSquare;
+			if (square == "-")
+				return undefined;
+			let res = [];
+			square.split(",").forEach(sq => {
+				res.push(V.SquareToCoords(sq));
+			});
+			return res;
+		}
+		// Argument is a move:
+		const move = moveOrSquare;
 		const [sx,sy,ex] = [move.start.x,move.start.y,move.end.x];
 		if (this.getPiece(sx,sy) == V.PAWN && Math.abs(sx - ex) >= 2)
 		{
@@ -248,6 +278,6 @@ class WildebeestRules extends ChessRules
 		return pieces["b"].join("") +
 			"/ppppppppppp/11/11/11/11/11/11/PPPPPPPPPPP/" +
 			pieces["w"].join("").toUpperCase() +
-			" 1111 w";
+			" w 1111 -";
 	}
 }
diff --git a/public/javascripts/variants/Zen.js b/public/javascripts/variants/Zen.js
index 64587d20..da4dd7af 100644
--- a/public/javascripts/variants/Zen.js
+++ b/public/javascripts/variants/Zen.js
@@ -1,10 +1,7 @@
 class ZenRules extends ChessRules
 {
 	// NOTE: enPassant, if enabled, would need to redefine carefully getEpSquare
-	getEpSquare(move)
-	{
-		return undefined;
-	}
+	static get HasEnpassant { return false; }
 
 	// TODO(?): some duplicated code in 2 next functions
 	getSlideNJumpMoves([x,y], steps, oneStep)
@@ -184,12 +181,10 @@ class ZenRules extends ChessRules
 		}
 
 		// Translate initial square (because pieces may fly unusually in this variant!)
-		const initialSquare =
-			String.fromCharCode(97 + move.start.y) + (V.size.x-move.start.x);
+		const initialSquare = V.CoordsToSquare(move.start);
 
 		// Translate final square
-		const finalSquare =
-			String.fromCharCode(97 + move.end.y) + (V.size.x-move.end.x);
+		const finalSquare = V.CoordsToSquare(move.end);
 
 		let notation = "";
 		const piece = this.getPiece(move.start.x, move.start.y);
@@ -218,7 +213,9 @@ class ZenRules extends ChessRules
 		return notation;
 	}
 
-	static get VALUES() { //TODO: experimental
+	static get VALUES()
+	{
+		// TODO: experimental
 		return {
 			'p': 1,
 			'r': 3,
-- 
2.44.0