From 0c3fe8a6c3e02af46e0bc646b40c1a0c420f9dcd Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Sun, 2 Feb 2020 02:29:39 +0100
Subject: [PATCH] Check variants. All OK except Dark (bug), Checkered (missing
 internal moves stack), Marseille (old bot using checkGameEnd())

---
 client/src/base_rules.js          |   4 +-
 client/src/variants/Alice.js      |   7 +-
 client/src/variants/Antiking.js   |  17 +++-
 client/src/variants/Atomic.js     | 148 +++++++++++++++++++++++++++++-
 client/src/variants/Atomic_OLD.js | 143 -----------------------------
 client/src/variants/Baroque.js    |  10 +-
 client/src/variants/Berolina.js   |   4 +-
 client/src/variants/Checkered.js  |  16 +++-
 client/src/variants/Crazyhouse.js |   7 +-
 client/src/variants/Dark.js       |  25 +++--
 client/src/variants/Extinction.js |  18 ++--
 client/src/variants/Grand.js      |  10 +-
 client/src/variants/Losers.js     |  21 +++--
 client/src/variants/Magnetic.js   |  15 ++-
 client/src/variants/Marseille.js  |   5 +-
 client/src/variants/Switching.js  | 135 ---------------------------
 client/src/variants/Upsidedown.js |  12 ++-
 client/src/variants/Wildebeest.js |  25 +++--
 client/src/variants/Zen.js        |   4 +-
 19 files changed, 276 insertions(+), 350 deletions(-)
 delete mode 100644 client/src/variants/Atomic_OLD.js
 delete mode 100644 client/src/variants/Switching.js

diff --git a/client/src/base_rules.js b/client/src/base_rules.js
index 169fb2d9..8cf86a86 100644
--- a/client/src/base_rules.js
+++ b/client/src/base_rules.js
@@ -2,7 +2,7 @@
 // Variants generally inherit from it, and modify some parts.
 
 import { ArrayFun } from "@/utils/array";
-import { randInt, sample, shuffle } from "@/utils/alea";
+import { randInt, shuffle } from "@/utils/alea";
 
 export const PiPo = class PiPo //Piece+Position
 {
@@ -1198,7 +1198,7 @@ export const ChessRules = class ChessRules
     let candidates = [0]; //indices of candidates moves
     for (let j=1; j<moves1.length && moves1[j].eval == moves1[0].eval; j++)
       candidates.push(j);
-    let currentBest = moves1[sample(candidates)];
+    let currentBest = moves1[candidates[randInt(candidates.length)]];
 
     // Skip depth 3+ if we found a checkmate (or if we are checkmated in 1...)
     if (V.SEARCH_DEPTH >= 3 && Math.abs(moves1[0].eval) < V.THRESHOLD_MATE)
diff --git a/client/src/variants/Alice.js b/client/src/variants/Alice.js
index e80e13b6..e81d3fc8 100644
--- a/client/src/variants/Alice.js
+++ b/client/src/variants/Alice.js
@@ -293,9 +293,12 @@ export const VariantRules = class AliceRules extends ChessRules
 			this.kingPos[c] = [move.start.x, move.start.y];
 	}
 
-	checkGameEnd()
+	getCurrentScore()
 	{
-		const pieces = Object.keys(V.ALICE_CODES);
+    if (this.atLeastOneMove()) // game not over
+      return "*";
+
+    const pieces = Object.keys(V.ALICE_CODES);
 		const color = this.turn;
 		const kp = this.kingPos[color];
 		const mirrorSide = (pieces.includes(this.getPiece(kp[0],kp[1])) ? 1 : 2);
diff --git a/client/src/variants/Antiking.js b/client/src/variants/Antiking.js
index 22fd2b40..7b57e74a 100644
--- a/client/src/variants/Antiking.js
+++ b/client/src/variants/Antiking.js
@@ -1,4 +1,8 @@
-class AntikingRules extends ChessRules
+import { ChessRules } from "@/base_rules";
+import { ArrayFun} from "@/utils/array";
+import { randInt } from "@/utils/alea";
+
+export const VariantRules = class AntikingRules extends ChessRules
 {
 	static getPpath(b)
 	{
@@ -125,9 +129,12 @@ class AntikingRules extends ChessRules
 			this.antikingPos[c] = [move.start.x, move.start.y];
 	}
 
-	checkGameEnd()
+	getCurrentScore()
 	{
-		const color = this.turn;
+    if (this.atLeastOneMove()) // game not over
+      return "*";
+
+    const color = this.turn;
 		const oppCol = V.GetOppCol(color);
 		if (!this.isAttacked(this.kingPos[color], [oppCol])
 			&& this.isAttacked(this.antikingPos[color], [oppCol]))
@@ -150,7 +157,7 @@ class AntikingRules extends ChessRules
 		let antikingPos = { "w": -1, "b": -1 };
 		for (let c of ["w","b"])
 		{
-			let positions = range(8);
+			let positions = ArrayFun.range(8);
 
 			// Get random squares for bishops, but avoid corners; because,
 			// if an antiking blocks a cornered bishop, it can never be checkmated
@@ -195,6 +202,6 @@ class AntikingRules extends ChessRules
 		return pieces["b"].join("") + "/" + ranks23_black +
 			"/8/8/" +
 			ranks23_white + "/" + pieces["w"].join("").toUpperCase() +
-			" w 1111 -";
+			" w 0 1111 -";
 	}
 }
diff --git a/client/src/variants/Atomic.js b/client/src/variants/Atomic.js
index 622e9d00..e1b1c161 100644
--- a/client/src/variants/Atomic.js
+++ b/client/src/variants/Atomic.js
@@ -1,6 +1,146 @@
-export const V = class AtomicRules {
-	show() {
-		console.log("AtomicRules");
+import { ChessRules, PiPo } from "@/base_rules";
+
+export const VariantRules = class AtomicRules extends ChessRules
+{
+	getPotentialMovesFrom([x,y])
+	{
+		let moves = super.getPotentialMovesFrom([x,y]);
+
+		// Handle explosions
+		moves.forEach(m => {
+			if (m.vanish.length > 1 && m.appear.length <= 1) //avoid castles
+			{
+				// Explosion! TODO(?): drop moves which explode our king here
+				let steps = [ [-1,-1],[-1,0],[-1,1],[0,-1],[0,1],[1,-1],[1,0],[1,1] ];
+				for (let step of steps)
+				{
+					let x = m.end.x + step[0];
+					let y = m.end.y + step[1];
+					if (V.OnBoard(x,y) && this.board[x][y] != V.EMPTY
+						&& this.getPiece(x,y) != V.PAWN)
+					{
+						m.vanish.push(
+							new PiPo({p:this.getPiece(x,y),c:this.getColor(x,y),x:x,y:y}));
+					}
+				}
+				m.end = {x:m.appear[0].x, y:m.appear[0].y};
+				m.appear.pop(); //Nothin appears in this case
+			}
+		});
+
+		return moves;
+	}
+
+	getPotentialKingMoves([x,y])
+	{
+		// King cannot capture:
+		let moves = [];
+		const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
+		for (let step of steps)
+		{
+			const i = x + step[0];
+			const j = y + step[1];
+			if (V.OnBoard(i,j) && this.board[i][j] == V.EMPTY)
+				moves.push(this.getBasicMove([x,y], [i,j]));
+		}
+		return moves.concat(this.getCastleMoves([x,y]));
+	}
+
+	isAttacked(sq, colors)
+	{
+		if (this.getPiece(sq[0],sq[1]) == V.KING && this.isAttackedByKing(sq, colors))
+			return false; //king cannot take...
+		return (this.isAttackedByPawn(sq, colors)
+			|| this.isAttackedByRook(sq, colors)
+			|| this.isAttackedByKnight(sq, colors)
+			|| this.isAttackedByBishop(sq, colors)
+			|| this.isAttackedByQueen(sq, colors));
+	}
+
+	updateVariables(move)
+	{
+		super.updateVariables(move);
+		const color = move.vanish[0].c;
+		if (move.appear.length == 0) //capture
+		{
+			const firstRank = {"w": 7, "b": 0};
+			for (let c of ["w","b"])
+			{
+				// Did we explode king of color c ? (TODO: remove move earlier)
+				if (Math.abs(this.kingPos[c][0]-move.end.x) <= 1
+					&& Math.abs(this.kingPos[c][1]-move.end.y) <= 1)
+				{
+					this.kingPos[c] = [-1,-1];
+					this.castleFlags[c] = [false,false];
+				}
+				else
+				{
+					// Now check if init rook(s) exploded
+					if (Math.abs(move.end.x-firstRank[c]) <= 1)
+					{
+						if (Math.abs(move.end.y-this.INIT_COL_ROOK[c][0]) <= 1)
+							this.castleFlags[c][0] = false;
+						if (Math.abs(move.end.y-this.INIT_COL_ROOK[c][1]) <= 1)
+							this.castleFlags[c][1] = false;
+					}
+				}
+			}
+		}
+	}
+
+	unupdateVariables(move)
+	{
+		super.unupdateVariables(move);
+		const c = move.vanish[0].c;
+		const oppCol = V.GetOppCol(c);
+		if ([this.kingPos[c][0],this.kingPos[oppCol][0]].some(e => { return e < 0; }))
+		{
+			// There is a chance that last move blowed some king away..
+			for (let psq of move.vanish)
+			{
+				if (psq.p == 'k')
+					this.kingPos[psq.c==c ? c : oppCol] = [psq.x, psq.y];
+			}
+		}
+	}
+
+	underCheck(color)
+	{
+		const oppCol = V.GetOppCol(color);
+		let res = undefined;
+		// If our king disappeared, move is not valid
+		if (this.kingPos[color][0] < 0)
+			res = true;
+		// If opponent king disappeared, move is valid
+		else if (this.kingPos[oppCol][0] < 0)
+			res = false;
+		// Otherwise, if we remain under check, move is not valid
+		else
+			res = this.isAttacked(this.kingPos[color], [oppCol]);
+		return res;
+	}
+
+	getCheckSquares(color)
+	{
+		let res = [ ];
+		if (this.kingPos[color][0] >= 0 //king might have exploded
+			&& this.isAttacked(this.kingPos[color], [V.GetOppCol(color)]))
+		{
+			res = [ JSON.parse(JSON.stringify(this.kingPos[color])) ]
+		}
+		return res;
+	}
+
+	getCurrentScore()
+	{
+		const color = this.turn;
+		const kp = this.kingPos[color];
+		if (kp[0] < 0) //king disappeared
+			return color == "w" ? "0-1" : "1-0";
+    if (this.atLeastOneMove()) // game not over
+      return "*";
+		if (!this.isAttacked(kp, [V.GetOppCol(color)]))
+			return "1/2";
+		return color == "w" ? "0-1" : "1-0"; //checkmate
 	}
 }
-//export default V = AtomicRules;
diff --git a/client/src/variants/Atomic_OLD.js b/client/src/variants/Atomic_OLD.js
deleted file mode 100644
index 1ee5b98f..00000000
--- a/client/src/variants/Atomic_OLD.js
+++ /dev/null
@@ -1,143 +0,0 @@
-import { ChessRules } from "@/base_rules";
-export const VariantRules = class AtomicRules extends ChessRules
-{
-	getPotentialMovesFrom([x,y])
-	{
-		let moves = super.getPotentialMovesFrom([x,y]);
-
-		// Handle explosions
-		moves.forEach(m => {
-			if (m.vanish.length > 1 && m.appear.length <= 1) //avoid castles
-			{
-				// Explosion! TODO(?): drop moves which explode our king here
-				let steps = [ [-1,-1],[-1,0],[-1,1],[0,-1],[0,1],[1,-1],[1,0],[1,1] ];
-				for (let step of steps)
-				{
-					let x = m.end.x + step[0];
-					let y = m.end.y + step[1];
-					if (V.OnBoard(x,y) && this.board[x][y] != V.EMPTY
-						&& this.getPiece(x,y) != V.PAWN)
-					{
-						m.vanish.push(
-							new PiPo({p:this.getPiece(x,y),c:this.getColor(x,y),x:x,y:y}));
-					}
-				}
-				m.end = {x:m.appear[0].x, y:m.appear[0].y};
-				m.appear.pop(); //Nothin appears in this case
-			}
-		});
-
-		return moves;
-	}
-
-	getPotentialKingMoves([x,y])
-	{
-		// King cannot capture:
-		let moves = [];
-		const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
-		for (let step of steps)
-		{
-			const i = x + step[0];
-			const j = y + step[1];
-			if (V.OnBoard(i,j) && this.board[i][j] == V.EMPTY)
-				moves.push(this.getBasicMove([x,y], [i,j]));
-		}
-		return moves.concat(this.getCastleMoves([x,y]));
-	}
-
-	isAttacked(sq, colors)
-	{
-		if (this.getPiece(sq[0],sq[1]) == V.KING && this.isAttackedByKing(sq, colors))
-			return false; //king cannot take...
-		return (this.isAttackedByPawn(sq, colors)
-			|| this.isAttackedByRook(sq, colors)
-			|| this.isAttackedByKnight(sq, colors)
-			|| this.isAttackedByBishop(sq, colors)
-			|| this.isAttackedByQueen(sq, colors));
-	}
-
-	updateVariables(move)
-	{
-		super.updateVariables(move);
-		const color = move.vanish[0].c;
-		if (move.appear.length == 0) //capture
-		{
-			const firstRank = {"w": 7, "b": 0};
-			for (let c of ["w","b"])
-			{
-				// Did we explode king of color c ? (TODO: remove move earlier)
-				if (Math.abs(this.kingPos[c][0]-move.end.x) <= 1
-					&& Math.abs(this.kingPos[c][1]-move.end.y) <= 1)
-				{
-					this.kingPos[c] = [-1,-1];
-					this.castleFlags[c] = [false,false];
-				}
-				else
-				{
-					// Now check if init rook(s) exploded
-					if (Math.abs(move.end.x-firstRank[c]) <= 1)
-					{
-						if (Math.abs(move.end.y-this.INIT_COL_ROOK[c][0]) <= 1)
-							this.castleFlags[c][0] = false;
-						if (Math.abs(move.end.y-this.INIT_COL_ROOK[c][1]) <= 1)
-							this.castleFlags[c][1] = false;
-					}
-				}
-			}
-		}
-	}
-
-	unupdateVariables(move)
-	{
-		super.unupdateVariables(move);
-		const c = move.vanish[0].c;
-		const oppCol = V.GetOppCol(c);
-		if ([this.kingPos[c][0],this.kingPos[oppCol][0]].some(e => { return e < 0; }))
-		{
-			// There is a chance that last move blowed some king away..
-			for (let psq of move.vanish)
-			{
-				if (psq.p == 'k')
-					this.kingPos[psq.c==c ? c : oppCol] = [psq.x, psq.y];
-			}
-		}
-	}
-
-	underCheck(color)
-	{
-		const oppCol = V.GetOppCol(color);
-		let res = undefined;
-		// If our king disappeared, move is not valid
-		if (this.kingPos[color][0] < 0)
-			res = true;
-		// If opponent king disappeared, move is valid
-		else if (this.kingPos[oppCol][0] < 0)
-			res = false;
-		// Otherwise, if we remain under check, move is not valid
-		else
-			res = this.isAttacked(this.kingPos[color], [oppCol]);
-		return res;
-	}
-
-	getCheckSquares(color)
-	{
-		let res = [ ];
-		if (this.kingPos[color][0] >= 0 //king might have exploded
-			&& this.isAttacked(this.kingPos[color], [V.GetOppCol(color)]))
-		{
-			res = [ JSON.parse(JSON.stringify(this.kingPos[color])) ]
-		}
-		return res;
-	}
-
-	checkGameEnd()
-	{
-		const color = this.turn;
-		const kp = this.kingPos[color];
-		if (kp[0] < 0) //king disappeared
-			return color == "w" ? "0-1" : "1-0";
-		if (!this.isAttacked(kp, [V.GetOppCol(color)]))
-			return "1/2";
-		return color == "w" ? "0-1" : "1-0"; //checkmate
-	}
-}
diff --git a/client/src/variants/Baroque.js b/client/src/variants/Baroque.js
index ce9e6672..9b5a3cdb 100644
--- a/client/src/variants/Baroque.js
+++ b/client/src/variants/Baroque.js
@@ -1,4 +1,8 @@
-class BaroqueRules extends ChessRules
+import { ChessRules, PiPo, Move } from "@/base_rules";
+import { ArrayFun } from "@/utils/array";
+import { randInt } from "@/utils/alea";
+
+export const VariantRules = class BaroqueRules extends ChessRules
 {
 	static get HasFlags() { return false; }
 
@@ -548,7 +552,7 @@ class BaroqueRules extends ChessRules
 		// Shuffle pieces on first and last rank
 		for (let c of ["w","b"])
 		{
-			let positions = range(8);
+			let positions = ArrayFun.range(8);
 			// Get random squares for every piece, totally freely
 
 			let randIndex = randInt(8);
@@ -592,7 +596,7 @@ class BaroqueRules extends ChessRules
 		return pieces["b"].join("") +
 			"/pppppppp/8/8/8/8/PPPPPPPP/" +
 			pieces["w"].join("").toUpperCase() +
-			" w";
+			" w 0";
 	}
 
 	getNotation(move)
diff --git a/client/src/variants/Berolina.js b/client/src/variants/Berolina.js
index 517a93e5..05152b43 100644
--- a/client/src/variants/Berolina.js
+++ b/client/src/variants/Berolina.js
@@ -1,4 +1,6 @@
-class BerolinaRules extends ChessRules
+import { ChessRules } from "@/base_rules";
+
+export const VariantRules = class  BerolinaRules extends ChessRules
 {
 	// En-passant after 2-sq jump
 	getEpSquare(moveOrSquare)
diff --git a/client/src/variants/Checkered.js b/client/src/variants/Checkered.js
index 1f6750b9..f8176cd3 100644
--- a/client/src/variants/Checkered.js
+++ b/client/src/variants/Checkered.js
@@ -1,4 +1,9 @@
-class CheckeredRules extends ChessRules
+// TODO: to detect oppositeMoves, we need last move --> encoded in FEN
+// + local moves stack (for AlphaBeta) + lastMove (in FEN)
+
+import { ChessRules } from "@/base_rules";
+
+export const VariantRules = class CheckeredRules extends ChessRules
 {
 	static getPpath(b)
 	{
@@ -206,9 +211,12 @@ class CheckeredRules extends ChessRules
 			this.pawnFlags[move.start.x==6 ? "w" : "b"][move.start.y] = false;
 	}
 
-	checkGameEnd()
+	getCurrentScore()
 	{
-		const color = this.turn;
+    if (this.atLeastOneMove()) // game not over
+      return "*";
+
+    const color = this.turn;
 		// Artifically change turn, for checkered pawns
 		this.turn = V.GetOppCol(this.turn);
 		const res = this.isAttacked(this.kingPos[color], [V.GetOppCol(color),'c'])
@@ -241,7 +249,7 @@ class CheckeredRules extends ChessRules
 	{
 		const randFen = ChessRules.GenRandInitFen();
 		// Add 16 pawns flags:
-		return randFen.replace(" w 1111", " w 11111111111111111111");
+		return randFen.replace(" w 0 1111", " w 0 11111111111111111111");
 	}
 
 	getFlagsFen()
diff --git a/client/src/variants/Crazyhouse.js b/client/src/variants/Crazyhouse.js
index 179fffa5..ec903c2f 100644
--- a/client/src/variants/Crazyhouse.js
+++ b/client/src/variants/Crazyhouse.js
@@ -1,4 +1,7 @@
-class CrazyhouseRules extends ChessRules
+import { ChessRules, PiPo, Move } from "@/base_rules";
+import { ArrayFun} from "@/utils/array";
+
+export const VariantRules = class CrazyhouseRules extends ChessRules
 {
 	static IsGoodFen(fen)
 	{
@@ -98,7 +101,7 @@ class CrazyhouseRules extends ChessRules
 				[V.QUEEN]: parseInt(fenParsed.reserve[9]),
 			}
 		};
-		this.promoted = doubleArray(V.size.x, V.size.y, false);
+		this.promoted = ArrayFun.init(V.size.x, V.size.y, false);
 		if (fenParsed.promoted != "-")
 		{
 			for (let square of fenParsed.promoted.split(","))
diff --git a/client/src/variants/Dark.js b/client/src/variants/Dark.js
index 76e9462e..777aacc8 100644
--- a/client/src/variants/Dark.js
+++ b/client/src/variants/Dark.js
@@ -1,4 +1,8 @@
-class DarkRules extends ChessRules
+import { ChessRules } from "@/base_rules";
+import { ArrayFun} from "@/utils/array";
+import { randInt } from "@/utils/alea";
+
+export const VariantRules = class DarkRules extends ChessRules
 {
 	// Standard rules, in the shadow
 	setOtherVariables(fen)
@@ -6,8 +10,8 @@ class DarkRules extends ChessRules
 		super.setOtherVariables(fen);
 		const [sizeX,sizeY] = [V.size.x,V.size.y];
 		this.enlightened = {
-			"w": doubleArray(sizeX,sizeY),
-			"b": doubleArray(sizeX,sizeY)
+			"w": ArrayFun.init(sizeX,sizeY),
+			"b": ArrayFun.init(sizeX,sizeY)
 		};
 		// Setup enlightened: squares reachable by each side
 		// (TODO: one side would be enough ?)
@@ -130,10 +134,15 @@ class DarkRules extends ChessRules
 		this.updateEnlightened();
 	}
 
-	checkGameEnd()
-	{
-		// No valid move: our king disappeared
-		return this.turn == "w" ? "0-1" : "1-0";
+  getCurrentScore()
+  {
+		const color = this.turn;
+		const kp = this.kingPos[color];
+		if (kp[0] < 0) //king disappeared
+			return (color == "w" ? "0-1" : "1-0");
+    if (this.atLeastOneMove()) // game not over
+      return "*";
+    return "1/2"; //no moves but kings still there (seems impossible)
 	}
 
 	static get THRESHOLD_MATE()
@@ -282,6 +291,6 @@ class DarkRules extends ChessRules
 		let candidates = [0];
 		for (let j=1; j<moves.length && moves[j].eval == moves[0].eval; j++)
 			candidates.push(j);
-		return moves[sample(candidates)];
+		return moves[candidates[randInt(candidates.length)]];
 	}
 }
diff --git a/client/src/variants/Extinction.js b/client/src/variants/Extinction.js
index aab359f8..748f9e46 100644
--- a/client/src/variants/Extinction.js
+++ b/client/src/variants/Extinction.js
@@ -1,4 +1,6 @@
-class ExtinctionRules extends ChessRules
+import { ChessRules } from "@/base_rules";
+
+export const VariantRules = class  ExtinctionRules extends ChessRules
 {
 	setOtherVariables(fen)
 	{
@@ -99,28 +101,20 @@ class ExtinctionRules extends ChessRules
 			this.material[move.vanish[1].c][move.vanish[1].p]++;
 	}
 
-	checkGameOver()
+	getCurrentScore()
 	{
-		if (this.checkRepetition())
-			return "1/2";
-
 		if (this.atLeastOneMove()) // game not over?
 		{
 			const color = this.turn;
 			if (Object.keys(this.material[color]).some(
 				p => { return this.material[color][p] == 0; }))
 			{
-				return this.checkGameEnd();
+				return (this.turn == "w" ? "0-1" : "1-0");
 			}
 			return "*";
 		}
 
-		return this.checkGameEnd(); //NOTE: currently unreachable...
-	}
-
-	checkGameEnd()
-	{
-		return (this.turn == "w" ? "0-1" : "1-0");
+		return (this.turn == "w" ? "0-1" : "1-0"); //NOTE: currently unreachable...
 	}
 
 	evalPosition()
diff --git a/client/src/variants/Grand.js b/client/src/variants/Grand.js
index 2ce85422..6ddfbd7c 100644
--- a/client/src/variants/Grand.js
+++ b/client/src/variants/Grand.js
@@ -1,6 +1,10 @@
+import { ChessRules } from "@/base_rules";
+import { ArrayFun } from "@/utils/array";
+import { randInt } from "@/utils/alea";
+
 // NOTE: initial setup differs from the original; see
 // https://www.chessvariants.com/large.dir/freeling.html
-class GrandRules extends ChessRules
+export const VariantRules = class GrandRules extends ChessRules
 {
 	static getPpath(b)
 	{
@@ -329,7 +333,7 @@ class GrandRules extends ChessRules
 		// Shuffle pieces on first and last rank
 		for (let c of ["w","b"])
 		{
-			let positions = range(10);
+			let positions = ArrayFun.range(10);
 
 			// Get random squares for bishops
 			let randIndex = 2 * randInt(5);
@@ -384,6 +388,6 @@ class GrandRules extends ChessRules
 		return pieces["b"].join("") +
 			"/pppppppppp/10/10/10/10/10/10/PPPPPPPPPP/" +
 			pieces["w"].join("").toUpperCase() +
-			" w 1111 - 00000000000000";
+			" w 0 1111 - 00000000000000";
 	}
 }
diff --git a/client/src/variants/Losers.js b/client/src/variants/Losers.js
index 7c6b4220..86b00f90 100644
--- a/client/src/variants/Losers.js
+++ b/client/src/variants/Losers.js
@@ -1,4 +1,8 @@
-class LosersRules extends ChessRules
+import { ChessRules } from "@/base_rules";
+import { ArrayFun } from "@/utils/array";
+import { randInt } from "@/utils/alea";
+
+export const VariantRules = class LosersRules extends ChessRules
 {
 	static get HasFlags() { return false; }
 
@@ -101,10 +105,13 @@ class LosersRules extends ChessRules
 	updateVariables(move) { }
 	unupdateVariables(move) { }
 
-	checkGameEnd()
-	{
-		// No valid move: you win!
-		return this.turn == "w" ? "1-0" : "0-1";
+  getCurrentScore()
+  {
+    if (this.atLeastOneMove()) // game not over
+      return "*";
+
+    // No valid move: the side who cannot move wins
+		return (this.turn == "w" ? "1-0" : "0-1");
 	}
 
 	static get VALUES()
@@ -133,7 +140,7 @@ class LosersRules extends ChessRules
 		// Shuffle pieces on first and last rank
 		for (let c of ["w","b"])
 		{
-			let positions = range(8);
+			let positions = ArrayFun.range(8);
 
 			// Get random squares for bishops
 			let randIndex = 2 * randInt(4);
@@ -180,6 +187,6 @@ class LosersRules extends ChessRules
 		return pieces["b"].join("") +
 			"/pppppppp/8/8/8/8/PPPPPPPP/" +
 			pieces["w"].join("").toUpperCase() +
-			" w -"; //no en-passant
+			" w 0 -"; //en-passant allowed, but no flags
 	}
 }
diff --git a/client/src/variants/Magnetic.js b/client/src/variants/Magnetic.js
index 32279905..ea745dd6 100644
--- a/client/src/variants/Magnetic.js
+++ b/client/src/variants/Magnetic.js
@@ -1,4 +1,6 @@
-class MagneticRules extends ChessRules
+import { ChessRules, PiPo } from "@/base_rules";
+
+export const VariantRules = class  MagneticRules extends ChessRules
 {
 	static get HasEnpassant() { return false; }
 
@@ -191,10 +193,15 @@ class MagneticRules extends ChessRules
 		}
 	}
 
-	checkGameEnd()
+	getCurrentScore()
 	{
-		// No valid move: our king disappeared
-		return this.turn == "w" ? "0-1" : "1-0";
+		const color = this.turn;
+		const kp = this.kingPos[color];
+		if (kp[0] < 0) //king disappeared
+			return (color == "w" ? "0-1" : "1-0");
+    if (this.atLeastOneMove()) // game not over
+      return "*";
+    return "1/2"; //no moves but kings still there
 	}
 
 	static get THRESHOLD_MATE()
diff --git a/client/src/variants/Marseille.js b/client/src/variants/Marseille.js
index 2d4ecfa4..0439ab4d 100644
--- a/client/src/variants/Marseille.js
+++ b/client/src/variants/Marseille.js
@@ -205,6 +205,7 @@ class MarseilleRules extends ChessRules
 		};
 	}
 
+  // TODO: this is wrong: revise following base_rules.getComputerMove()
 	// No alpha-beta here, just adapted min-max at depth 2(+1)
 	getComputerMove()
 	{
@@ -221,7 +222,7 @@ class MarseilleRules extends ChessRules
 			let moves = this.getAllValidMoves();
 			if (moves.length == 0)
 			{
-				const score = this.checkGameEnd();
+				const score = this.getCurrentScore();
 				if (score == "1/2")
 					return 0;
 				return maxeval * (score == "1-0" ? 1 : -1);
@@ -234,7 +235,7 @@ class MarseilleRules extends ChessRules
 				// Otherwise it's color,1. In both cases the next test makes sense
 				if (!this.atLeastOneMove())
 				{
-					const score = this.checkGameEnd();
+					const score = this.getCurrentScore();
 					if (score == "1/2")
 						res = (oppCol == "w" ? Math.max(res, 0) : Math.min(res, 0));
 					else
diff --git a/client/src/variants/Switching.js b/client/src/variants/Switching.js
deleted file mode 100644
index 04bb110c..00000000
--- a/client/src/variants/Switching.js
+++ /dev/null
@@ -1,135 +0,0 @@
-class SwitchingRules extends ChessRules
-{
-	// Build switch move between squares x1,y1 and x2,y2
-	getSwitchMove_s([x1,y1],[x2,y2])
-	{
-		const c = this.getColor(x1,y1); //same as color at square 2
-		const p1 = this.getPiece(x1,y1);
-		const p2 = this.getPiece(x2,y2);
-		if (p1 == V.KING && p2 == V.ROOK)
-			return []; //avoid duplicate moves (potential conflict with castle)
-		let move = new Move({
-			appear: [
-				new PiPo({x:x2,y:y2,c:c,p:p1}),
-				new PiPo({x:x1,y:y1,c:c,p:p2})
-			],
-			vanish: [
-				new PiPo({x:x1,y:y1,c:c,p:p1}),
-				new PiPo({x:x2,y:y2,c:c,p:p2})
-			],
-			start: {x:x1,y:y1},
-			end: {x:x2,y:y2}
-		});
-		// Move completion: promote switched pawns (as in Magnetic)
-		const lastRank = (c == "w" ? 0 : V.size.x-1);
-		let moves = [];
-		if ((p1==V.PAWN && x2==lastRank) || (p2==V.PAWN && x1==lastRank))
-		{
-			const idx = (p1==V.PAWN ? 0 : 1);
-			move.appear[idx].p = V.ROOK;
-			moves.push(move);
-			for (let piece of [V.KNIGHT, V.BISHOP, V.QUEEN])
-			{
-				let cmove = JSON.parse(JSON.stringify(move));
-				cmove.appear[idx].p = piece;
-				moves.push(cmove);
-			}
-			if (idx == 1)
-			{
-				// Swap moves[i].appear[0] and [1] for moves presentation [TODO...]
-				moves.forEach(m => {
-					let tmp = m.appear[0];
-					m.appear[0] = m.appear[1];
-					m.appear[1] = tmp;
-				});
-			}
-		}
-		else //other cases
-			moves.push(move);
-		return moves;
-	}
-
-	getPotentialMovesFrom([x,y], computer)
-	{
-		let moves = super.getPotentialMovesFrom([x,y]);
-		// Add switches: respecting chessboard ordering if "computer" is on
-		const color = this.turn;
-		const piece = this.getPiece(x,y);
-		const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
-		const kp = this.kingPos[color];
-		const oppCol = V.GetOppCol(color);
-		for (let step of steps)
-		{
-			let [i,j] = [x+step[0],y+step[1]];
-			if (!!computer && (i<x || (i==x && j<y)))
-				continue; //only switch with superior indices
-			if (V.OnBoard(i,j) && this.board[i][j]!=V.EMPTY
-				&& this.getColor(i,j)==color && this.getPiece(i,j)!=piece
-				// No switching under check (theoretically non-king pieces could, but not)
-				&& !this.isAttacked(kp, [oppCol]))
-			{
-				let switchMove_s = this.getSwitchMove_s([x,y],[i,j]);
-				if (switchMove_s.length == 1)
-					moves.push(switchMove_s[0]);
-				else //promotion
-					moves = moves.concat(switchMove_s);
-			}
-		}
-		return moves;
-	}
-
-	getAllValidMoves(computer)
-	{
-		const color = this.turn;
-		const oppCol = V.GetOppCol(color);
-		let potentialMoves = [];
-		for (let i=0; i<V.size.x; i++)
-		{
-			for (let j=0; j<V.size.y; j++)
-			{
-				if (this.board[i][j] != V.EMPTY && this.getColor(i,j) == color)
-				{
-					Array.prototype.push.apply(potentialMoves,
-						this.getPotentialMovesFrom([i,j], computer));
-				}
-			}
-		}
-		return this.filterValid(potentialMoves);
-	}
-
-	updateVariables(move)
-	{
-		super.updateVariables(move);
-		if (move.appear.length == 2 && move.vanish.length == 2
-			&& move.appear[1].p == V.KING)
-		{
-			// Switch with the king; not castle, and not handled by main class
-			const color = move.vanish[0].c;
-			this.kingPos[color] = [move.appear[1].x, move.appear[1].y];
-		}
-	}
-
-	unupdateVariables(move)
-	{
-		super.unupdateVariables(move);
-		if (move.appear.length == 2 && move.vanish.length == 2
-			&& move.appear[1].p == V.KING)
-		{
-			const color = move.vanish[0].c;
-			this.kingPos[color] = [move.appear[0].x, move.appear[0].y];
-		}
-	}
-
-	static get SEARCH_DEPTH() { return 2; } //high branching factor
-
-	getNotation(move)
-	{
-		if (move.appear.length == 1)
-			return super.getNotation(move); //no switch
-		// Switch or castle
-		if (move.appear[0].p == V.KING && move.appear[1].p == V.ROOK)
-			return (move.end.y < move.start.y ? "0-0-0" : "0-0");
-		// Switch:
-		return "S" + V.CoordsToSquare(move.start) + V.CoordsToSquare(move.end);
-	}
-}
diff --git a/client/src/variants/Upsidedown.js b/client/src/variants/Upsidedown.js
index c3e8716e..de722168 100644
--- a/client/src/variants/Upsidedown.js
+++ b/client/src/variants/Upsidedown.js
@@ -1,4 +1,8 @@
-class UpsidedownRules extends ChessRules
+import { ChessRules } from "@/base_rules";
+import { randInt } from "@/utils/alea";
+import { ArrayFun } from "@/utils/array";
+
+export const VariantRules = class UpsidedownRules extends ChessRules
 {
 	static get HasFlags() { return false; }
 
@@ -16,7 +20,7 @@ class UpsidedownRules extends ChessRules
 		let pieces = { "w": new Array(8), "b": new Array(8) };
 		for (let c of ["w","b"])
 		{
-			let positions = range(8);
+			let positions = ArrayFun.range(8);
 
 			let randIndex = randInt(8);
 			const kingPos = positions[randIndex];
@@ -29,7 +33,7 @@ class UpsidedownRules extends ChessRules
 			else if (kingPos == V.size.y-1)
 				knight1Pos = V.size.y-2;
 			else
-				knight1Pos = kingPos + (Math.randInt() < 0.5 ? 1 : -1);
+				knight1Pos = kingPos + (Math.random() < 0.5 ? 1 : -1);
 			// Search for knight1Pos index in positions and remove it
 			const knight1Index = positions.indexOf(knight1Pos);
 			positions.splice(knight1Index, 1);
@@ -65,6 +69,6 @@ class UpsidedownRules extends ChessRules
 		return pieces["w"].join("").toUpperCase() +
 			"/PPPPPPPP/8/8/8/8/pppppppp/" +
 			pieces["b"].join("") +
-			" w"; //no castle, no en-passant
+			" w 0"; //no castle, no en-passant
 	}
 }
diff --git a/client/src/variants/Wildebeest.js b/client/src/variants/Wildebeest.js
index d0baa6f9..20a32ccf 100644
--- a/client/src/variants/Wildebeest.js
+++ b/client/src/variants/Wildebeest.js
@@ -1,4 +1,8 @@
-class WildebeestRules extends ChessRules
+import { ChessRules } from "@/base_rules";
+import { ArrayFun } from "@/utils/array";
+import { sample, randInt } from "@/utils/alea";
+
+export const VariantRules = class  WildebeestRules extends ChessRules
 {
 	static getPpath(b)
 	{
@@ -211,10 +215,13 @@ class WildebeestRules extends ChessRules
 			V.steps[V.KNIGHT].concat(V.steps[V.CAMEL]), "oneStep");
 	}
 
-	checkGameEnd()
-	{
+  getCurrentScore()
+  {
+    if (this.atLeastOneMove()) // game not over
+      return "*";
+
 		// No valid move: game is lost (stalemate is a win)
-		return this.turn == "w" ? "0-1" : "1-0";
+		return (this.turn == "w" ? "0-1" : "1-0");
 	}
 
 	static get VALUES() {
@@ -231,14 +238,16 @@ class WildebeestRules extends ChessRules
 		let pieces = { "w": new Array(10), "b": new Array(10) };
 		for (let c of ["w","b"])
 		{
-			let positions = range(11);
+			let positions = ArrayFun.range(11);
 
 			// Get random squares for bishops + camels (different colors)
-			let randIndexes = sample(range(6), 2).map(i => { return 2*i; });
+			let randIndexes = sample(ArrayFun.range(6), 2)
+        .map(i => { return 2*i; });
 			let bishop1Pos = positions[randIndexes[0]];
 			let camel1Pos = positions[randIndexes[1]];
 			// The second bishop (camel) must be on a square of different color
-			let randIndexes_tmp = sample(range(5), 2).map(i => { return 2*i+1; });
+			let randIndexes_tmp = sample(ArrayFun.range(5), 2)
+        .map(i => { return 2*i+1; });
 			let bishop2Pos = positions[randIndexes_tmp[0]];
 			let camel2Pos = positions[randIndexes_tmp[1]];
 			for (let idx of randIndexes.concat(randIndexes_tmp)
@@ -282,6 +291,6 @@ class WildebeestRules extends ChessRules
 		return pieces["b"].join("") +
 			"/ppppppppppp/11/11/11/11/11/11/PPPPPPPPPPP/" +
 			pieces["w"].join("").toUpperCase() +
-			" w 1111 -";
+			" w 0 1111 -";
 	}
 }
diff --git a/client/src/variants/Zen.js b/client/src/variants/Zen.js
index 6a568db9..db2146a7 100644
--- a/client/src/variants/Zen.js
+++ b/client/src/variants/Zen.js
@@ -1,4 +1,6 @@
-class ZenRules extends ChessRules
+import { ChessRules } from "@/base_rules";
+
+export const VariantRules = class ZenRules extends ChessRules
 {
 	// NOTE: enPassant, if enabled, would need to redefine carefully getEpSquare
 	static get HasEnpassant() { return false; }
-- 
2.44.0