From 1a788978e3682ab54b77af3edfe38e0b371edbc4 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Mon, 17 Dec 2018 18:21:01 +0100
Subject: [PATCH] Save current state (unfinished, untested)

---
 TODO                                  |   6 +-
 public/javascripts/base_rules.js      | 657 ++++++++++++++------------
 public/javascripts/components/game.js | 189 +++++---
 public/stylesheets/variant.sass       |   3 +
 4 files changed, 493 insertions(+), 362 deletions(-)

diff --git a/TODO b/TODO
index e6641b48..4d99536b 100644
--- a/TODO
+++ b/TODO
@@ -1,2 +1,4 @@
-Finish showProblem() in components/problemSummary.js (send event with infos, and pass message to game component)
-Add new mode in game component: "problem", in which we show description + hidden solution (reveal on click)
+global lang cookie, + display (remember in each variant what is shown...)
+translations (how ? switch on index page only, then find ideas...)
+for each variant, adapt FEN (Crazyhouse, Grand, Loser, ...)
+Improve style for various screen sizes
diff --git a/public/javascripts/base_rules.js b/public/javascripts/base_rules.js
index 2a962a96..620b86f8 100644
--- a/public/javascripts/base_rules.js
+++ b/public/javascripts/base_rules.js
@@ -31,91 +31,33 @@ class Move
 // NOTE: x coords = top to bottom; y = left to right (from white player perspective)
 class ChessRules
 {
+	//////////////
+	// MISC UTILS
+
 	// Path to pieces
 	static getPpath(b)
 	{
 		return b; //usual pieces in pieces/ folder
 	}
+
 	// Turn "wb" into "B" (for FEN)
 	static board2fen(b)
 	{
 		return b[0]=='w' ? b[1].toUpperCase() : b[1];
 	}
+
 	// Turn "p" into "bp" (for board)
 	static fen2board(f)
 	{
 		return f.charCodeAt()<=90 ? "w"+f.toLowerCase() : "b"+f;
 	}
 
-	/////////////////
-	// INITIALIZATION
-
-	// fen == "position [flags [turn]]"
-	constructor(fen, moves)
-	{
-		this.moves = moves;
-		// Use fen string to initialize variables, flags, turn and board
-		const fenParts = fen.split(" ");
-		this.board = V.GetBoard(fenParts[0]);
-		this.setFlags(fenParts[1]); //NOTE: fenParts[1] might be undefined
-		this.setTurn(fenParts[2]); //Same note
-		this.initVariables(fen);
-	}
-
-	// Some additional variables from FEN (variant dependant)
-	initVariables(fen)
-	{
-		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 fenParts = fen.split(" ");
-		const position = fenParts[0].split("/");
-		for (let i=0; i<position.length; i++)
-		{
-			let k = 0; //column index on board
-			for (let j=0; j<position[i].length; j++)
-			{
-				switch (position[i].charAt(j))
-				{
-					case 'k':
-						this.kingPos['b'] = [i,k];
-						this.INIT_COL_KING['b'] = k;
-						break;
-					case 'K':
-						this.kingPos['w'] = [i,k];
-						this.INIT_COL_KING['w'] = k;
-						break;
-					case 'r':
-						if (this.INIT_COL_ROOK['b'][0] < 0)
-							this.INIT_COL_ROOK['b'][0] = k;
-						else
-							this.INIT_COL_ROOK['b'][1] = k;
-						break;
-					case 'R':
-						if (this.INIT_COL_ROOK['w'][0] < 0)
-							this.INIT_COL_ROOK['w'][0] = k;
-						else
-							this.INIT_COL_ROOK['w'][1] = k;
-						break;
-					default:
-						const num = parseInt(position[i].charAt(j));
-						if (!isNaN(num))
-							k += (num-1);
-				}
-				k++;
-			}
-		}
-		this.epSquares = [ this.getEpSquare(this.lastMove || fenParts[3]) ];
-	}
-
 	// Check if FEN describe a position
 	static IsGoodFen(fen)
 	{
-		const fenParts = fen.split(" ");
-		if (fenParts.length== 0)
-			return false;
+		const fenParsed = V.ParseFen(fen);
 		// 1) Check position
-		const position = fenParts[0];
+		const position = fenParsed.position;
 		const rows = position.split("/");
 		if (rows.length != V.size.x)
 			return false;
@@ -138,15 +80,16 @@ class ChessRules
 				return false;
 		}
 		// 2) Check flags (if present)
-		if (fenParts.length >= 2)
-		{
-			if (!V.IsGoodFlags(fenParts[1]))
-				return false;
-		}
+		if (!!fenParsed.flags && !V.IsGoodFlags(fenParsed.flags))
+			return false;
 		// 3) Check turn (if present)
-		if (fenParts.length >= 3)
+		if (!!fenParsed.turn && !["w","b"].includes(fenParsed.turn))
+			return false;
+		// 4) Check enpassant (if present)
+		if (!!fenParsed.enpassant)
 		{
-			if (!["w","b"].includes(fenParts[2]))
+			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;
@@ -158,10 +101,225 @@ class ChessRules
 		return !!flags.match(/^[01]{4,4}$/);
 	}
 
-	// Turn diagram fen into double array ["wb","wp","bk",...]
-	static GetBoard(fen)
+	// a4 --> {x:3,y:0}
+	static SquareToCoords(sq)
+	{
+		return {
+			x: V.size.x - parseInt(sq.substr(1)),
+			y: sq[0].charCodeAt() - 97
+		};
+	}
+
+	// {x:0,y:4} --> e8
+	static CoordsToSquare(coords)
+	{
+		return String.fromCharCode(97 + coords.y) + (V.size.x - coords.x);
+	}
+
+	// Aggregates flags into one object
+	aggregateFlags()
+	{
+		return this.castleFlags;
+	}
+
+	// Reverse operation
+	disaggregateFlags(flags)
+	{
+		this.castleFlags = flags;
+	}
+
+	// En-passant square, if any
+	getEpSquare(moveOrSquare)
+	{
+		if (!moveOrSquare)
+			return undefined;
+		if (typeof moveOrSquare === "string")
+		{
+			const square = moveOrSquare;
+			if (square == "-")
+				return undefined;
+			return {
+				x: square[0].charCodeAt()-97,
+				y: V.size.x-parseInt(square[1])
+			};
+		}
+		// 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)
+		{
+			return {
+				x: (sx + ex)/2,
+				y: sy
+			};
+		}
+		return undefined; //default
+	}
+
+	// Can thing on square1 take thing on square2
+	canTake([x1,y1], [x2,y2])
+	{
+		return this.getColor(x1,y1) !== this.getColor(x2,y2);
+	}
+
+	// Is (x,y) on the chessboard?
+	static OnBoard(x,y)
+	{
+		return (x>=0 && x<V.size.x && y>=0 && y<V.size.y);
+	}
+
+	// Used in interface: 'side' arg == player color
+	canIplay(side, [x,y])
+	{
+		return (this.turn == side && this.getColor(x,y) == side);
+	}
+
+	// On which squares is opponent under check after our move ? (for interface)
+	getCheckSquares(move)
+	{
+		this.play(move);
+		const color = this.turn; //opponent
+		let res = this.isAttacked(this.kingPos[color], [this.getOppCol(color)])
+			? [JSON.parse(JSON.stringify(this.kingPos[color]))] //need to duplicate!
+			: [];
+		this.undo(move);
+		return res;
+	}
+
+	/////////////
+	// FEN UTILS
+
+	// Setup the initial random (assymetric) position
+	static GenRandInitFen()
+	{
+		let pieces = { "w": new Array(8), "b": new Array(8) };
+		// Shuffle pieces on first and last rank
+		for (let c of ["w","b"])
+		{
+			let positions = _.range(8);
+
+			// Get random squares for bishops
+			let randIndex = 2 * _.random(3);
+			let bishop1Pos = positions[randIndex];
+			// The second bishop must be on a square of different color
+			let randIndex_tmp = 2 * _.random(3) + 1;
+			let bishop2Pos = positions[randIndex_tmp];
+			// Remove chosen squares
+			positions.splice(Math.max(randIndex,randIndex_tmp), 1);
+			positions.splice(Math.min(randIndex,randIndex_tmp), 1);
+
+			// Get random squares for knights
+			randIndex = _.random(5);
+			let knight1Pos = positions[randIndex];
+			positions.splice(randIndex, 1);
+			randIndex = _.random(4);
+			let knight2Pos = positions[randIndex];
+			positions.splice(randIndex, 1);
+
+			// Get random square for queen
+			randIndex = _.random(3);
+			let queenPos = positions[randIndex];
+			positions.splice(randIndex, 1);
+
+			// Rooks and king positions are now fixed, because of the ordering rook-king-rook
+			let rook1Pos = positions[0];
+			let kingPos = positions[1];
+			let rook2Pos = positions[2];
+
+			// Finally put the shuffled pieces in the board array
+			pieces[c][rook1Pos] = 'r';
+			pieces[c][knight1Pos] = 'n';
+			pieces[c][bishop1Pos] = 'b';
+			pieces[c][queenPos] = 'q';
+			pieces[c][kingPos] = 'k';
+			pieces[c][bishop2Pos] = 'b';
+			pieces[c][knight2Pos] = 'n';
+			pieces[c][rook2Pos] = 'r';
+		}
+		return pieces["b"].join("") +
+			"/pppppppp/8/8/8/8/PPPPPPPP/" +
+			pieces["w"].join("").toUpperCase() +
+			" w 1111 -"; //add turn + flags + enpassant
+	}
+
+	// "Parse" FEN: just return untransformed string data
+	static ParseFen(fen)
+	{
+		const fenParts = fen.split(" ");
+		return {
+			position: fenParts[0],
+			turn: fenParts[1],
+			flags: fenParts[2],
+			enpassant: fenParts[3],
+		};
+	}
+
+	// Return current fen (game state)
+	getFen()
+	{
+		return this.getBaseFen() + " " + this.turn + " " +
+			this.getFlagsFen() + " " + this.getEnpassantFen();
+	}
+
+	// Position part of the FEN string
+	getBaseFen()
+	{
+		let position = "";
+		for (let i=0; i<V.size.x; i++)
+		{
+			let emptyCount = 0;
+			for (let j=0; j<V.size.y; j++)
+			{
+				if (this.board[i][j] == V.EMPTY)
+					emptyCount++;
+				else
+				{
+					if (emptyCount > 0)
+					{
+						// Add empty squares in-between
+						position += emptyCount;
+						emptyCount = 0;
+					}
+					fen += V.board2fen(this.board[i][j]);
+				}
+			}
+			if (emptyCount > 0)
+			{
+				// "Flush remainder"
+				position += emptyCount;
+			}
+			if (i < V.size.x - 1)
+				position += "/"; //separate rows
+		}
+		return position;
+	}
+
+	// Flags part of the FEN string
+	getFlagsFen()
+	{
+		let flags = "";
+		// Add castling flags
+		for (let i of ['w','b'])
+		{
+			for (let j=0; j<2; j++)
+				flags += (this.castleFlags[i][j] ? '1' : '0');
+		}
+		return flags;
+	}
+
+	// Enpassant part of the FEN string
+	getEnpassantFen()
+	{
+		const L = this.epSquares.length;
+		if (L == 0)
+			return "-"; //no en-passant
+		return V.CoordsToSquare(this.epSquares[L-1]);
+	}
+
+	// Turn position fen into double array ["wb","wp","bk",...]
+	static GetBoard(position)
 	{
-		const rows = fen.split(" ")[0].split("/");
+		const rows = position.split("/");
 		let board = doubleArray(V.size.x, V.size.y, "");
 		for (let i=0; i<rows.length; i++)
 		{
@@ -190,30 +348,101 @@ class ChessRules
 			this.castleFlags[i < 2 ? 'w' : 'b'][i%2] = (fenflags.charAt(i) == '1');
 	}
 
-	// Initialize turn (white or black)
-	setTurn(turnflag)
+	//////////////////
+	// INITIALIZATION
+
+	// Fen string fully describes the game state
+	constructor(fen, moves)
 	{
-		this.turn = turnflag || "w";
+		this.moves = moves;
+		const fenParsed = V.ParseFen(fen);
+		this.board = V.GetBoard(fenParsed.position);
+		this.turn = (fenParsed.turn || "w");
+		this.setOtherVariables(fen);
 	}
 
-	///////////////////
-	// GETTERS, SETTERS
+	// Some additional variables from FEN (variant dependant)
+	setOtherVariables(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("/");
+		for (let i=0; i<fenRows.length; i++)
+		{
+			let k = 0; //column index on board
+			for (let j=0; j<fenRows[i].length; j++)
+			{
+				switch (fenRows[i].charAt(j))
+				{
+					case 'k':
+						this.kingPos['b'] = [i,k];
+						this.INIT_COL_KING['b'] = k;
+						break;
+					case 'K':
+						this.kingPos['w'] = [i,k];
+						this.INIT_COL_KING['w'] = k;
+						break;
+					case 'r':
+						if (this.INIT_COL_ROOK['b'][0] < 0)
+							this.INIT_COL_ROOK['b'][0] = k;
+						else
+							this.INIT_COL_ROOK['b'][1] = k;
+						break;
+					case 'R':
+						if (this.INIT_COL_ROOK['w'][0] < 0)
+							this.INIT_COL_ROOK['w'][0] = k;
+						else
+							this.INIT_COL_ROOK['w'][1] = k;
+						break;
+					default:
+						const num = parseInt(fenRows[i].charAt(j));
+						if (!isNaN(num))
+							k += (num-1);
+				}
+				k++;
+			}
+		}
+	}
+
+	/////////////////////
+	// GETTERS & SETTERS
+
+	static get size()
+	{
+		return {x:8, y:8};
+	}
 
-	static get size() { return {x:8, y:8}; }
+	// Color of thing on suqare (i,j). 'undefined' if square is empty
+	getColor(i,j)
+	{
+		return this.board[i][j].charAt(0);
+	}
 
-	// Two next functions return 'undefined' if called on empty square
-	getColor(i,j) { return this.board[i][j].charAt(0); }
-	getPiece(i,j) { return this.board[i][j].charAt(1); }
+	// Piece type on square (i,j). 'undefined' if square is empty
+	getPiece(i,j)
+	{
+		return this.board[i][j].charAt(1);
+	}
 
-	// Color
-	getOppCol(color) { return (color=="w" ? "b" : "w"); }
+	// Get opponent color
+	getOppCol(color)
+	{
+		return (color=="w" ? "b" : "w");
+	}
 
-	get lastMove() {
+	get lastMove()
+	{
 		const L = this.moves.length;
 		return (L>0 ? this.moves[L-1] : null);
 	}
 
-	// Pieces codes
+	// Pieces codes (for a clearer code)
 	static get PAWN() { return 'p'; }
 	static get ROOK() { return 'r'; }
 	static get KNIGHT() { return 'n'; }
@@ -222,15 +451,17 @@ class ChessRules
 	static get KING() { return 'k'; }
 
 	// For FEN checking:
-	static get PIECES() {
+	static get PIECES()
+	{
 		return [V.PAWN,V.ROOK,V.KNIGHT,V.BISHOP,V.QUEEN,V.KING];
 	}
 
 	// Empty square
-	static get EMPTY() { return ''; }
+	static get EMPTY() { return ""; }
 
 	// Some pieces movements
-	static get steps() {
+	static get steps()
+	{
 		return {
 			'r': [ [-1,0],[1,0],[0,-1],[0,1] ],
 			'n': [ [-1,-2],[-1,2],[1,-2],[1,2],[-2,-1],[-2,1],[2,-1],[2,1] ],
@@ -238,50 +469,7 @@ class ChessRules
 		};
 	}
 
-	// Aggregates flags into one object
-	get flags() {
-		return this.castleFlags;
-	}
-
-	// Reverse operation
-	parseFlags(flags)
-	{
-		this.castleFlags = flags;
-	}
-
-	// En-passant square, if any
-	getEpSquare(moveOrSquare)
-	{
-		if (typeof moveOrSquare === "string")
-		{
-			const square = moveOrSquare;
-			if (square == "-")
-				return undefined;
-			return {
-				x: square[0].charCodeAt()-97,
-				y: V.size.x-parseInt(square[1])
-			};
-		}
-		// 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)
-		{
-			return {
-				x: (sx + ex)/2,
-				y: sy
-			};
-		}
-		return undefined; //default
-	}
-
-	// Can thing on square1 take thing on square2
-	canTake([x1,y1], [x2,y2])
-	{
-		return this.getColor(x1,y1) !== this.getColor(x2,y2);
-	}
-
-	///////////////////
+	////////////////////
 	// MOVES GENERATION
 
 	// All possible moves from selected square (assumption: color is OK)
@@ -341,12 +529,6 @@ class ChessRules
 		return mv;
 	}
 
-	// Is (x,y) on the chessboard?
-	static OnBoard(x,y)
-	{
-		return (x>=0 && x<V.size.x && y>=0 && y<V.size.y);
-	}
-
 	// Generic method to find possible moves of non-pawn pieces ("sliding or jumping")
 	getSlideNJumpMoves([x,y], steps, oneStep)
 	{
@@ -550,14 +732,9 @@ class ChessRules
 		return moves;
 	}
 
-	///////////////////
+	////////////////////
 	// MOVES VALIDATION
 
-	canIplay(side, [x,y])
-	{
-		return (this.turn == side && this.getColor(x,y) == side);
-	}
-
 	getPossibleMovesFrom(sq)
 	{
 		// Assuming color is right (already checked)
@@ -618,7 +795,7 @@ class ChessRules
 		return false;
 	}
 
-	// Check if pieces of color in array 'colors' are attacking square x,y
+	// Check if pieces of color in array 'colors' are attacking (king) on square x,y
 	isAttacked(sq, colors)
 	{
 		return (this.isAttackedByPawn(sq, colors)
@@ -714,17 +891,8 @@ class ChessRules
 		return res;
 	}
 
-	// On which squares is opponent under check after our move ?
-	getCheckSquares(move)
-	{
-		this.play(move);
-		const color = this.turn; //opponent
-		let res = this.isAttacked(this.kingPos[color], [this.getOppCol(color)])
-			? [ JSON.parse(JSON.stringify(this.kingPos[color])) ] //need to duplicate!
-			: [ ];
-		this.undo(move);
-		return res;
-	}
+	/////////////////
+	// MOVES PLAYING
 
 	// Apply a move on board
 	static PlayOnBoard(board, move)
@@ -774,8 +942,8 @@ class ChessRules
 		}
 	}
 
-	// After move is undo-ed, un-update variables (flags are reset)
-	// TODO: more symmetry, by storing flags increment in move...
+	// After move is undo-ed *and flags resetted*, un-update other variables
+	// TODO: more symmetry, by storing flags increment in move (?!)
 	unupdateVariables(move)
 	{
 		// (Potentially) Reset king position
@@ -784,22 +952,16 @@ class ChessRules
 			this.kingPos[c] = [move.start.x, move.start.y];
 	}
 
-	// Hash of position+flags+turn after a move is played (to detect repetitions)
-	getHashState()
-	{
-		return hex_md5(this.getFen());
-	}
-
 	play(move, ingame)
 	{
 		// DEBUG:
 //		if (!this.states) this.states = [];
-//		if (!ingame) this.states.push(JSON.stringify(this.board));
+//		if (!ingame) this.states.push(this.getFen());
 
 		if (!!ingame)
 			move.notation = [this.getNotation(move), this.getLongNotation(move)];
 
-		move.flags = JSON.stringify(this.flags); //save flags (for undo)
+		move.flags = JSON.stringify(this.aggregateFlags()); //save flags (for undo)
 		this.updateVariables(move);
 		this.moves.push(move);
 		this.epSquares.push( this.getEpSquare(move) );
@@ -807,7 +969,10 @@ class ChessRules
 		V.PlayOnBoard(this.board, move);
 
 		if (!!ingame)
-			move.hash = this.getHashState();
+		{
+			// Hash of current game state *after move*, to detect repetitions
+			move.hash = hex_md5(this.getFen();
+		}
 	}
 
 	undo(move)
@@ -817,15 +982,15 @@ class ChessRules
 		this.epSquares.pop();
 		this.moves.pop();
 		this.unupdateVariables(move);
-		this.parseFlags(JSON.parse(move.flags));
+		this.disaggregateFlags(JSON.parse(move.flags));
 
 		// DEBUG:
-//		if (JSON.stringify(this.board) != this.states[this.states.length-1])
+//		if (this.getFen() != this.states[this.states.length-1])
 //			debugger;
 //		this.states.pop();
 	}
 
-	//////////////
+	///////////////
 	// END OF GAME
 
 	// Check for 3 repetitions (position + flags + turn)
@@ -872,11 +1037,12 @@ class ChessRules
 		return color == "w" ? "0-1" : "1-0";
 	}
 
-	////////
-	//ENGINE
+	///////////////
+	// ENGINE PLAY
 
 	// Pieces values
-	static get VALUES() {
+	static get VALUES()
+	{
 		return {
 			'p': 1,
 			'r': 5,
@@ -887,18 +1053,14 @@ class ChessRules
 		};
 	}
 
-	static get INFINITY() {
-		return 9999; //"checkmate" (unreachable eval)
-	}
+	// "Checkmate" (unreachable eval)
+	static get INFINITY() { return 9999; }
 
-	static get THRESHOLD_MATE() {
-		// At this value or above, the game is over
-		return V.INFINITY;
-	}
+	// At this value or above, the game is over
+	static get THRESHOLD_MATE() { return V.INFINITY; }
 
-	static get SEARCH_DEPTH() {
-		return 3; //2 for high branching factor, 4 for small (Loser chess)
-	}
+	// Search depth: 2 for high branching factor, 4 for small (Loser chess, eg.)
+	static get SEARCH_DEPTH() { return 3; }
 
 	// Assumption: at least one legal move
 	// NOTE: works also for extinction chess because depth is 3...
@@ -917,7 +1079,7 @@ class ChessRules
 			let finish = (Math.abs(this.evalPosition()) >= V.THRESHOLD_MATE);
 			if (!finish && !this.atLeastOneMove())
 			{
-				// Try mate (for other variants)
+				// Test mate (for other variants)
 				const score = this.checkGameEnd();
 				if (score != "1/2")
 					finish = true;
@@ -946,7 +1108,7 @@ class ChessRules
 						evalPos = this.evalPosition()
 					else
 					{
-						// Work with scores for Loser variant
+						// Working with scores is more accurate (necessary for Loser variant)
 						const score = this.checkGameEnd();
 						evalPos = (score=="1/2" ? 0 : (score=="1-0" ? 1 : -1) * maxeval);
 					}
@@ -968,7 +1130,6 @@ class ChessRules
 			this.undo(moves1[i]);
 		}
 		moves1.sort( (a,b) => { return (color=="w" ? 1 : -1) * (b.eval - a.eval); });
-		//console.log(moves1.map(m => { return [this.getNotation(m), m.eval]; }));
 
 		let candidates = [0]; //indices of candidates moves
 		for (let j=1; j<moves1.length && moves1[j].eval == moves1[0].eval; j++)
@@ -1067,113 +1228,9 @@ class ChessRules
 		return evaluation;
 	}
 
-	////////////
-	// FEN utils
-
-	// Setup the initial random (assymetric) position
-	static GenRandInitFen()
-	{
-		let pieces = { "w": new Array(8), "b": new Array(8) };
-		// Shuffle pieces on first and last rank
-		for (let c of ["w","b"])
-		{
-			let positions = _.range(8);
-
-			// Get random squares for bishops
-			let randIndex = 2 * _.random(3);
-			let bishop1Pos = positions[randIndex];
-			// The second bishop must be on a square of different color
-			let randIndex_tmp = 2 * _.random(3) + 1;
-			let bishop2Pos = positions[randIndex_tmp];
-			// Remove chosen squares
-			positions.splice(Math.max(randIndex,randIndex_tmp), 1);
-			positions.splice(Math.min(randIndex,randIndex_tmp), 1);
-
-			// Get random squares for knights
-			randIndex = _.random(5);
-			let knight1Pos = positions[randIndex];
-			positions.splice(randIndex, 1);
-			randIndex = _.random(4);
-			let knight2Pos = positions[randIndex];
-			positions.splice(randIndex, 1);
-
-			// Get random square for queen
-			randIndex = _.random(3);
-			let queenPos = positions[randIndex];
-			positions.splice(randIndex, 1);
-
-			// Rooks and king positions are now fixed, because of the ordering rook-king-rook
-			let rook1Pos = positions[0];
-			let kingPos = positions[1];
-			let rook2Pos = positions[2];
-
-			// Finally put the shuffled pieces in the board array
-			pieces[c][rook1Pos] = 'r';
-			pieces[c][knight1Pos] = 'n';
-			pieces[c][bishop1Pos] = 'b';
-			pieces[c][queenPos] = 'q';
-			pieces[c][kingPos] = 'k';
-			pieces[c][bishop2Pos] = 'b';
-			pieces[c][knight2Pos] = 'n';
-			pieces[c][rook2Pos] = 'r';
-		}
-		return pieces["b"].join("") +
-			"/pppppppp/8/8/8/8/PPPPPPPP/" +
-			pieces["w"].join("").toUpperCase() +
-			" 1111 w"; //add flags + turn
-	}
-
-	// Return current fen according to pieces+colors state
-	getFen()
-	{
-		return this.getBaseFen() + " " + this.getFlagsFen() + " " + this.turn;
-	}
-
-	// Position part of the FEN string
-	getBaseFen()
-	{
-		let fen = "";
-		for (let i=0; i<V.size.x; i++)
-		{
-			let emptyCount = 0;
-			for (let j=0; j<V.size.y; j++)
-			{
-				if (this.board[i][j] == V.EMPTY)
-					emptyCount++;
-				else
-				{
-					if (emptyCount > 0)
-					{
-						// Add empty squares in-between
-						fen += emptyCount;
-						emptyCount = 0;
-					}
-					fen += V.board2fen(this.board[i][j]);
-				}
-			}
-			if (emptyCount > 0)
-			{
-				// "Flush remainder"
-				fen += emptyCount;
-			}
-			if (i < V.size.x - 1)
-				fen += "/"; //separate rows
-		}
-		return fen;
-	}
-
-	// Flags part of the FEN string
-	getFlagsFen()
-	{
-		let fen = "";
-		// Add castling flags
-		for (let i of ['w','b'])
-		{
-			for (let j=0; j<2; j++)
-				fen += (this.castleFlags[i][j] ? '1' : '0');
-		}
-		return fen;
-	}
+	/////////////////////////
+	// MOVES + GAME NOTATION
+	/////////////////////////
 
 	// Context: just before move is played, turn hasn't changed
 	getNotation(move)
@@ -1182,7 +1239,7 @@ class ChessRules
 			return (move.end.y < move.start.y ? "0-0-0" : "0-0");
 
 		// Translate final square
-		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);
 		if (piece == V.PAWN)
@@ -1213,10 +1270,8 @@ class ChessRules
 	// Complete the usual notation, may be required for de-ambiguification
 	getLongNotation(move)
 	{
-		const startSquare =
-			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);
-		return startSquare + finalSquare; //not encoding move. But short+long is enough
+		// Not encoding move. But short+long is enough
+		return V.CoordsToSquare(move.start) + V.CoordsToSquare(move.end);
 	}
 
 	// The score is already computed when calling this function
diff --git a/public/javascripts/components/game.js b/public/javascripts/components/game.js
index 0ab12d62..9d7bb1fb 100644
--- a/public/javascripts/components/game.js
+++ b/public/javascripts/components/game.js
@@ -27,10 +27,7 @@ Vue.component('my-game', {
 	watch: {
 		problem: function(p, pp) {
 			// 'problem' prop changed: update board state
-			// TODO: FEN + turn + flags + rappel instructions / solution on click sous l'échiquier
-			// TODO: trouver moyen de passer la situation des reserves pour Crazyhouse,
-			// et l'état des captures pour Grand... bref compléter le descriptif de l'état.
-			this.newGame("problem", p.fen, p.fen.split(" ")[2]);
+			this.newGame("problem", p.fen, V.ParseFen(p.fen).turn);
 		},
 	},
 	render(h) {
@@ -105,7 +102,7 @@ Vue.component('my-game', {
 				: (smallScreen ? 31 : 37);
 			if (this.mode == "human")
 			{
-				let connectedIndic = h(
+				const connectedIndic = h(
 					'div',
 					{
 						"class": {
@@ -122,7 +119,7 @@ Vue.component('my-game', {
 				);
 				elementArray.push(connectedIndic);
 			}
-			let turnIndic = h(
+			const turnIndic = h(
 				'div',
 				{
 					"class": {
@@ -138,7 +135,7 @@ Vue.component('my-game', {
 				}
 			);
 			elementArray.push(turnIndic);
-			let settingsBtn = h(
+			const settingsBtn = h(
 				'button',
 				{
 					on: { click: this.showSettings },
@@ -157,7 +154,27 @@ Vue.component('my-game', {
 				[h('i', { 'class': { "material-icons": true } }, "settings")]
 			);
 			elementArray.push(settingsBtn);
-			let choices = h('div',
+			if (this.mode == "problem")
+			{
+				// Show problem instructions
+				elementArray.push(
+					h('div',
+						{
+							attrs: { id: "instructions-div" },
+							"class": { "section-content": true },
+						},
+						[
+							h('p',
+								{
+									attrs: { id: "problem-instructions" },
+									domProps: { innerHTML: this.problem.instructions }
+								}
+							)
+						]
+					)
+				);
+			}
+			const choices = h('div',
 				{
 					attrs: { "id": "choices" },
 					'class': { 'row': true },
@@ -195,7 +212,7 @@ Vue.component('my-game', {
 			const lm = this.vr.lastMove;
 			const showLight = this.hints &&
 				(this.mode!="idle" || this.cursor==this.vr.moves.length);
-			let gameDiv = h('div',
+			const gameDiv = h('div',
 				{
 					'class': { 'game': true },
 				},
@@ -410,7 +427,6 @@ Vue.component('my-game', {
 				);
 				elementArray.push(reserves);
 			}
-			const eogMessage = this.getEndgameMessage(this.score);
 			const modalEog = [
 				h('input',
 					{
@@ -437,7 +453,7 @@ Vue.component('my-game', {
 									{
 										attrs: { "id": "eogMessage" },
 										"class": { "section": true },
-										domProps: { innerHTML: eogMessage },
+										domProps: { innerHTML: this.endgameMessage },
 									}
 								)
 							]
@@ -713,7 +729,7 @@ Vue.component('my-game', {
 			actionArray
 		);
 		elementArray.push(actions);
-		if (this.score != "*")
+		if (this.score != "*" && this.pgnTxt.length > 0)
 		{
 			elementArray.push(
 				h('div',
@@ -749,6 +765,32 @@ Vue.component('my-game', {
 		}
 		else if (this.mode != "idle")
 		{
+			if (this.mode == "problem")
+			{
+				// Show problem solution (on click)
+				elementArray.push(
+					h('div',
+						{
+							attrs: { id: "solution-div" },
+							"class": { "section-content": true },
+						},
+						[
+							h('h3',
+								{
+									domProps: { innerHTML: "Show solution" },
+									on: { click: "toggleShowSolution" }
+								}
+							),
+							h('p',
+								{
+									attrs: { id: "problem-solution" },
+									domProps: { innerHTML: this.problem.solution }
+								}
+							)
+						]
+					)
+				);
+			}
 			// Show current FEN
 			elementArray.push(
 				h('div',
@@ -760,7 +802,7 @@ Vue.component('my-game', {
 						h('p',
 							{
 								attrs: { id: "fen-string" },
-								domProps: { innerHTML: this.vr.getFen() }
+								domProps: { innerHTML: this.vr.getBaseFen() }
 							}
 						)
 					]
@@ -790,6 +832,24 @@ Vue.component('my-game', {
 			elementArray
 		);
 	},
+	computed: {
+		endgameMessage: function() {
+			let eogMessage = "Unfinished";
+			switch (this.score)
+			{
+				case "1-0":
+					eogMessage = "White win";
+					break;
+				case "0-1":
+					eogMessage = "Black win";
+					break;
+				case "1/2":
+					eogMessage = "Draw";
+					break;
+			}
+			return eogMessage;
+		},
+	},
 	created: function() {
 		const url = socketUrl;
 		const humanContinuation = (localStorage.getItem("variant") === variant);
@@ -907,6 +967,12 @@ Vue.component('my-game', {
 		};
 	},
 	methods: {
+		toggleShowSolution: function() {
+			let problemSolution = document.getElementById("problem-solution");
+			problemSolution.style.display = problemSolution.style.display == "none"
+				? "block"
+				: "none";
+		},
 		download: function() {
 			let content = document.getElementById("pgn-game").innerHTML;
 			content = content.replace(/<br>/g, "\n");
@@ -917,35 +983,22 @@ Vue.component('my-game', {
 				encodeURIComponent(content);
 			downloadAnchor.click();
 		},
-		endGame: function(score) {
-			this.score = score;
+		showScoreMsg: function() {
 			let modalBox = document.getElementById("modal-eog");
 			modalBox.checked = true;
+			setTimeout(() => { modalBox.checked = false; }, 2000);
+		},
+		endGame: function(score) {
+			this.score = score;
+			this.showScoreMsg();
 			// Variants may have special PGN structure (so next function isn't defined here)
 			this.pgnTxt = this.vr.getPGN(this.mycolor, this.score, this.fenStart, this.mode);
-			setTimeout(() => { modalBox.checked = false; }, 2000);
 			if (["human","computer"].includes(this.mode))
 				this.clearStorage();
 			this.mode = "idle";
 			this.cursor = this.vr.moves.length; //to navigate in finished game
 			this.oppid = "";
 		},
-		getEndgameMessage: function(score) {
-			let eogMessage = "Unfinished";
-			switch (this.score)
-			{
-				case "1-0":
-					eogMessage = "White win";
-					break;
-				case "0-1":
-					eogMessage = "Black win";
-					break;
-				case "1/2":
-					eogMessage = "Draw";
-					break;
-			}
-			return eogMessage;
-		},
 		setStorage: function() {
 			if (this.mode=="human")
 			{
@@ -1042,18 +1095,30 @@ Vue.component('my-game', {
 				if (!!storageVariant && storageVariant !== variant)
 					return alert("Finish your " + storageVariant + " game first!");
 				// Send game request and wait..
-				this.seek = true;
 				try {
 					this.conn.send(JSON.stringify({code:"newgame", fen:fen}));
 				} catch (INVALID_STATE_ERR) {
 					return; //nothing achieved
 				}
+				this.seek = true;
 				let modalBox = document.getElementById("modal-newgame");
 				modalBox.checked = true;
 				setTimeout(() => { modalBox.checked = false; }, 2000);
 				return;
 			}
-			if (this.mode == "computer" && mode == "human") { }
+			if (mode == "computer" && !continuation)
+			{
+				const storageVariant = localStorage.getItem("comp-variant");
+				if (!!storageVariant && storageVariant !== variant)
+				{
+					if (!confirm("Unfinished " + storageVariant +
+						" computer game will be erased"))
+					{
+						return;
+					}
+				}
+			}
+			if (this.mode == "computer" && mode == "human")
 			{
 				// Save current computer game to resume it later
 				this.setStorage();
@@ -1062,45 +1127,42 @@ Vue.component('my-game', {
 			this.score = "*";
 			this.pgnTxt = ""; //redundant with this.score = "*", but cleaner
 			this.mode = mode;
-			this.incheck = continuation
-				? this.vr
-				: [];
-			this.fenStart = (continuation ? localStorage.getItem("fenStart") : fen);
+			if (continuation && moves.length > 0) //NOTE: "continuation": redundant test
+			{
+				const lastMove = moves[moves.length-1];
+				this.vr.undo(lastMove);
+				this.incheck = this.vr.getCheckSquares(lastMove);
+				this.vr.play(lastMove, "ingame");
+			}
+			else
+				this.incheck = [];
+			if (continuation)
+			{
+				const prefix = (mode=="computer" ? "comp-" : "");
+				this.fenStart = localStorage.getItem(prefix+"fenStart");
+			}
+			else
+				this.fenStart = fen;
 			if (mode=="human")
 			{
-
-
-
-//TODO: refactor this. (for computer mode too), lastMove getCheckSquares...
-
-
-
-
 				// Opponent found!
 				if (!continuation) //not playing sound on game continuation
 				{
 					if (this.sound >= 1)
-						new Audio("/sounds/newgame.mp3").play().then(() => {}).catch(err => {});
+						new Audio("/sounds/newgame.mp3").play().catch(err => {});
 					document.getElementById("modal-newgame").checked = false;
 				}
 				this.oppid = oppId;
 				this.oppConnected = !continuation;
 				this.mycolor = color;
 				this.seek = false;
-				if (!!moves && moves.length > 0) //imply continuation
-				{
-					const lastMove = moves[moves.length-1];
-					this.vr.undo(lastMove);
-					this.incheck = this.vr.getCheckSquares(lastMove);
-					this.vr.play(lastMove, "ingame");
-				}
 				this.setStorage(); //in case of interruptions
 			}
 			else if (mode == "computer")
 			{
 				this.mycolor = Math.random() < 0.5 ? 'w' : 'b';
 				if (this.mycolor == 'b')
-					setTimeout(this.playComputerMove, 500);
+					this.playComputerMove();
 			}
 			//else: against a (IRL) friend or problem solving: nothing more to do
 		},
@@ -1250,7 +1312,7 @@ Vue.component('my-game', {
 					squares.item(i).style.zIndex = "auto";
 				movingPiece.style = {}; //required e.g. for 0-0 with KR swap
 				this.play(move);
-			}, 200);
+			}, 250);
 		},
 		play: function(move, programmatic) {
 			if (!move)
@@ -1269,7 +1331,7 @@ Vue.component('my-game', {
 			if (this.mode == "human" && this.vr.turn == this.mycolor)
 				this.conn.send(JSON.stringify({code:"newmove", move:move, oppid:this.oppid}));
 			if (this.sound == 2)
-				new Audio("/sounds/chessmove1.mp3").play().then(() => {}).catch(err => {});
+				new Audio("/sounds/chessmove1.mp3").play().catch(err => {});
 			if (this.mode != "idle")
 			{
 				this.incheck = this.vr.getCheckSquares(move); //is opponent in check?
@@ -1286,10 +1348,19 @@ Vue.component('my-game', {
 			{
 				const eog = this.vr.checkGameOver();
 				if (eog != "*")
-					this.endGame(eog);
+				{
+					if (["human","computer"].includes(this.mode))
+						this.endGame(eog);
+					else
+					{
+						// Just show score on screen (allow undo)
+						this.score = eog;
+						this.showScoreMsg();
+					}
+				}
 			}
 			if (this.mode == "computer" && this.vr.turn != this.mycolor)
-				setTimeout(this.playComputerMove, 500);
+				this.playComputerMove;
 		},
 		undo: function() {
 			// Navigate after game is over
diff --git a/public/stylesheets/variant.sass b/public/stylesheets/variant.sass
index f8047560..6472e3d8 100644
--- a/public/stylesheets/variant.sass
+++ b/public/stylesheets/variant.sass
@@ -267,3 +267,6 @@ ul:not(.browser-default) > li
 
 .mistake-newproblem
   color: #663300
+
+#problem-solution
+  display: none
-- 
2.44.0